Linux内核进程管理导出符号接口
Linux内核中创建进程、进程退出、修改进程属性的导出符号接口
在 Linux 内核中,进程管理是核心功能之一。内核提供了许多函数(作为导出符号 EXPORT_SYMBOL
或 EXPORT_SYMBOL_GPL
)供其他内核模块使用,用于创建进程、处理进程退出和修改进程属性。以下是一些关键的接口:
1. 创建进程
kernel_thread
(传统方式,逐渐被替代):- 函数:
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
- 功能:创建一个新的内核线程。
- 导出状态:
EXPORT_SYMBOL(kernel_thread)
- 注意:虽然仍在使用,但更推荐使用
kthread_create
系列函数,因为kernel_thread
创建的线程在用户空间不可见(没有对应的/proc/pid
条目),且资源清理不如kthread
方便。
- 函数:
kthread_create
/kthread_run
(推荐方式):- 函数:
struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
(创建但未运行)struct task_struct *kthread_run(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
(宏,创建并立即运行)
- 功能:创建并管理内核线程。提供了更好的资源管理和可见性(可通过
ps
看到)。 - 导出状态:
EXPORT_SYMBOL(kthread_create)
,kthread_run
是宏,依赖于kthread_create
和wake_up_process
。 - 相关重要函数:
int wake_up_process(struct task_struct *tsk)
: 唤醒创建的线程(如果使用kthread_create
)。EXPORT_SYMBOL(wake_up_process)
bool kthread_should_stop(void)
: 线程函数中检查是否应退出。EXPORT_SYMBOL(kthread_should_stop)
int kthread_stop(struct task_struct *k)
: 请求停止一个由kthread_create
创建的线程并等待其结束。EXPORT_SYMBOL(kthread_stop)
- 函数:
fork
/vfork
/clone
(用户空间进程创建的核心):- 函数:这些是系统调用的核心实现。
- 导出状态:它们本身 不是 直接作为
EXPORT_SYMBOL
导出的函数。用户空间通过fork()
,vfork()
,clone()
系统调用触发。 - 内部机制:它们最终会调用内部的
_do_fork
/kernel_clone
函数(具体名称和实现细节随内核版本变化)。这些内部函数处理复制进程描述符、复制资源(COW)、设置栈等复杂工作。 - 重点:模块通常不需要直接调用这些底层创建用户进程的函数。用户进程是由用户空间程序通过 syscall 创建的。内核模块创建新执行上下文主要是创建内核线程(
kthread_*
)。
2. 进程退出
do_exit
(核心退出函数):- 函数:
void do_exit(long code)
- 功能:执行进程退出的主要清理工作。释放进程的大部分资源,设置退出状态码,通知父进程,将进程状态设为
EXIT_ZOMBIE
。这是进程终止路径上的关键函数。 - 导出状态:
EXPORT_SYMBOL_GPL(do_exit)
- 警告: 在模块中直接调用
do_exit()
来终止当前进程(例如在自定义内核线程中)是可能的,但需极其谨慎。它不会执行文件描述符刷新等用户空间相关的清理。对于内核线程,更推荐使用kthread_stop
机制或complete_and_exit
。
- 函数:
complete_and_exit
(内核线程退出常用):- 函数:
void complete_and_exit(struct completion *comp, long code)
- 功能:主要用于内核线程的优雅退出。它先完成(唤醒等待者)一个
completion
变量,然后调用do_exit(code)
。 - 导出状态:
EXPORT_SYMBOL(complete_and_exit)
- 用途:常用于让内核线程在完成其最后一项任务(通过
completion
信号通知创建者)后安全退出。
- 函数:
make_task_dead
(更高级别的退出入口 - 较新内核):- 函数:
void make_task_dead(int signr)
- 功能:一个更上层的函数,最终调用
do_exit
。它处理一些额外的簿记工作,比如发送信号(SIGKILL
)给线程组中的其他线程。通常在致命错误路径(如oops
处理)或强制终止时使用。 - 导出状态:
EXPORT_SYMBOL_GPL(make_task_dead)
- 注意:普通模块代码通常不需要直接调用这个。
- 函数:
panic
(系统级崩溃):- 函数:
void panic(const char *fmt, ...)
- 功能:导致内核崩溃,打印消息并(可能)尝试重启。虽然不是针对单个进程的退出,但它会导致所有进程终止。
- 导出状态:
EXPORT_SYMBOL(panic)
- 注意:仅在遇到不可恢复的、影响整个系统的致命错误时使用。
- 函数:
3. 修改进程属性
这部分涉及的接口非常广泛,以下列举一些常见且重要的:
进程状态与调度:
void set_task_state(struct task_struct *tsk, int state)
: 设置进程状态 (e.g.,TASK_RUNNING
,TASK_INTERRUPTIBLE
,TASK_UNINTERRUPTIBLE
)。更安全,包含内存屏障。EXPORT_SYMBOL(set_task_state)
void set_current_state(int state)
: 设置当前进程状态。EXPORT_SYMBOL(set_current_state)
int wake_up_state(struct task_struct *tsk, unsigned int state)
: 唤醒处于特定状态的进程。EXPORT_SYMBOL(wake_up_state)
void set_user_nice(struct task_struct *p, long nice)
: 设置进程的nice
值(影响调度优先级)。EXPORT_SYMBOL(set_user_nice)
int sched_setscheduler(struct task_struct *p, int policy, const struct sched_param *param)
: 设置进程的调度策略(SCHED_FIFO
,SCHED_RR
,SCHED_OTHER
,SCHED_BATCH
,SCHED_IDLE
,SCHED_DEADLINE
)和参数(如优先级)。EXPORT_SYMBOL_GPL(sched_setscheduler)
void set_cpus_allowed_ptr(struct task_struct *p, const struct cpumask *new_mask)
: 设置进程允许运行的CPU亲和性掩码。EXPORT_SYMBOL(set_cpus_allowed_ptr)
进程标识与凭证:
void set_task_comm(struct task_struct *tsk, const char *buf)
: 设置进程的命令行名称(出现在ps
等命令中)。EXPORT_SYMBOL(set_task_comm)
int commit_creds(struct cred *new)
: 应用一组新的凭证到当前进程(权限更改的核心)。极其危险,需谨慎!EXPORT_SYMBOL(commit_creds)
struct cred *prepare_creds(void)
: 为当前进程准备一份可修改的凭证副本。EXPORT_SYMBOL(prepare_creds)
void __put_cred(struct cred *cred)
: 减少凭证的引用计数,可能在引用为0时释放。EXPORT_SYMBOL(__put_cred)
const struct cred *get_task_cred(struct task_struct *task)
: 获取指定进程凭证的引用(需调用put_cred
释放)。EXPORT_SYMBOL(get_task_cred)
int capable(int cap)
: 检查当前进程是否具有指定的内核能力(Capability)。EXPORT_SYMBOL(capable)
int has_capability(struct task_struct *t, int cap)
: 检查指定进程是否具有指定的内核能力。EXPORT_SYMBOL(has_capability)
void security_task_to_inode(struct task_struct *p, struct inode *inode)
: LSM Hook,在创建/proc/pid
inode 时设置安全信息(通常模块不直接调用)。
信号处理:
int send_sig_info(int sig, struct kernel_siginfo *info, struct task_struct *p)
: 向指定进程发送信号。EXPORT_SYMBOL(send_sig_info)
int kill_pid(struct pid *pid, int sig, int priv)
: 向指定 PID 发送信号。EXPORT_SYMBOL(kill_pid)
int force_sig(int sig)
: 强制发送信号给当前进程(不可阻塞或忽略)。EXPORT_SYMBOL(force_sig)
int allow_signal(int sig)
: (较旧) 允许内核线程接收特定信号。现代内核线程通常使用kthread_should_stop()
机制。
命名空间:
struct nsproxy *create_nsproxy(void)
: 创建新的命名空间代理结构。EXPORT_SYMBOL_GPL(create_nsproxy)
int switch_task_namespaces(struct task_struct *p, struct nsproxy *new)
: 切换任务的命名空间视图。EXPORT_SYMBOL_GPL(switch_task_namespaces)
- 特定命名空间操作函数(如
copy_pid_ns
,copy_net_ns
等)通常也导出。
资源限制 (rlimits):
int security_task_setrlimit(struct task_struct *p, unsigned int resource, struct rlimit *new_rlim)
: LSM Hook,在设置资源限制前调用(模块通常不直接调用)。- 修改
task_struct->signal->rlim[resource]
需要谨慎,通常通过用户空间setrlimit
syscall 或特权操作进行。
进程关系:
- 直接修改
task_struct
中的parent
,real_parent
,children
,sibling
等链表指针是极其危险且不被允许的。内核提供内部函数维护这些关系(如__ptrace_link
,__ptrace_unlink
用于 ptrace)。
- 直接修改
获取进程引用:
struct task_struct *get_pid_task(struct pid *pid, enum pid_type type)
: 通过 PID 结构获取task_struct
引用。EXPORT_SYMBOL_GPL(get_pid_task)
struct pid *get_task_pid(struct task_struct *task, enum pid_type type)
: 获取任务指定类型 PID 的引用。EXPORT_SYMBOL_GPL(get_task_pid)
struct task_struct *get_task_struct(struct task_struct *tsk)
: 增加任务的引用计数。在持有指向task_struct
指针操作前,通常需要调用此函数防止任务消失。EXPORT_SYMBOL_GPL(get_task_struct)
void put_task_struct(struct task_struct *tsk)
: 减少任务的引用计数,引用为0时释放。必须与get_task_struct
配对使用。EXPORT_SYMBOL_GPL(put_task_struct)
重要提示
- 内核版本差异: 内核 API 会随版本变化。上面列出的函数名和导出状态是基于较新稳定内核(如 5.x, 6.x)。开发时务必查阅目标内核版本的源代码头文件(
include/linux/sched.h
,include/linux/kthread.h
,kernel/exit.c
,kernel/fork.c
,kernel/signal.c
,kernel/capability.c
,kernel/nsproxy.c
等)和Module.symvers
文件。 - 安全性与稳定性: 修改进程属性(尤其是凭证、调度策略)或强制终止进程的操作具有高风险,可能破坏系统稳定性和安全性。模块代码必须非常谨慎,确保有充分的理由和正确的同步。
- 引用计数: 操作其他进程的
task_struct
时,必须使用get_task_struct()
和put_task_struct()
来管理引用计数,防止目标进程在你使用其结构时意外退出导致 Use-After-Free 错误。 - 替代方案: 优先考虑通过用户空间机制(syscall, procfs, sysfs)或标准内核设施(如 cgroups)来影响进程行为,而不是在内核模块中直接修改进程属性。
- 查找导出符号:
- 查看内核源码树中的
Module.symvers
文件。 - 在编译好的内核上使用
nm vmlinux | grep <function_name>
。 - 查看
/proc/kallsyms
(需要 root 权限)。
- 查看内核源码树中的
理解这些接口对于进行深入的内核开发(如编写调度器、安全模块、容器技术)至关重要,但务必遵循内核开发的最佳实践和安全准则。
内核中修改用户态进程的euid
在 Linux 内核中修改用户态进程的 euid(effective user ID)需要通过进程凭证(credentials)操作接口。以下是详细步骤和关键接口:
核心步骤
- 获取目标进程的凭证结构体 (
struct cred
) - 修改凭证中的 euid 字段
- 应用新凭证到目标进程
- 管理引用计数
关键导出符号接口
1. 凭证准备与修改
1 | /* 获取当前进程凭证的可修改副本 */ |
2. 修改 euid 的核心操作
1 | /* 应用新凭证到当前进程 */ |
3. 进程和凭证引用管理
1 | /* 获取任务结构引用 */ |
修改 euid 的典型流程
场景1:修改当前进程的 euid
1 | void set_current_euid(uid_t new_euid) |
场景2:修改其他进程的 euid(需 CAP_SETUID 权限)
1 | int set_process_euid(pid_t pid, uid_t new_euid) |
关键注意事项
权限要求:
- 修改自身:需要当前 euid==目标 euid 或具有 CAP_SETUID
- 修改其他进程:必须具有 CAP_SETUID 能力
安全规则:
1
2
3
4
5
6
7
8
9
10
11// 检查权限的底层函数
static int set_euid_checks(struct cred *new, const struct cred *old)
{
if (capable(CAP_SETUID))
return 0;
if (new->euid == old->uid ||
new->euid == old->euid ||
new->euid == old->suid)
return 0;
return -EPERM;
}关联 ID 更新:
- 修改 euid 时必须同步更新 fsuid(文件系统 UID)
- 真实 UID (uid) 和保存 UID (suid) 通常不变
审计跟踪:
1
2// 修改凭证时触发审计
security_task_fix_setuid(new, old, LSM_SETID_EUID);内核版本差异:
版本范围 关键变化 < 4.3 使用 __task_cred()
直接访问≥ 4.3 引入 cred_has_capability()
≥ 5.9 struct cred
添加安全标签字段
完整示例(修改当前进程)
1 |
|
风险提示
- 竞态条件:修改其他进程凭证时,目标进程可能正在退出
- 能力泄漏:不当修改可能导致权限提升漏洞
- 审计追踪:所有凭证修改都会被记录到审计日志
- 容器兼容性:在容器内修改需考虑 user namespace 映射
重要:生产环境应优先使用用户空间工具(如
/bin/setuid
或libc
的seteuid()
),内核模块直接修改凭证仅适用于特殊场景(如安全模块、容器运行时)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Helloeuler!