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/pidinode 时设置安全信息(通常模块不直接调用)。
信号处理:
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]需要谨慎,通常通过用户空间setrlimitsyscall 或特权操作进行。
进程关系:
- 直接修改
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!
