Linux内核中创建进程、进程退出、修改进程属性的导出符号接口

在 Linux 内核中,进程管理是核心功能之一。内核提供了许多函数(作为导出符号 EXPORT_SYMBOLEXPORT_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_createwake_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)

重要提示

  1. 内核版本差异: 内核 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 文件。
  2. 安全性与稳定性: 修改进程属性(尤其是凭证、调度策略)或强制终止进程的操作具有高风险,可能破坏系统稳定性和安全性。模块代码必须非常谨慎,确保有充分的理由和正确的同步。
  3. 引用计数: 操作其他进程的 task_struct 时,必须使用 get_task_struct()put_task_struct() 来管理引用计数,防止目标进程在你使用其结构时意外退出导致 Use-After-Free 错误。
  4. 替代方案: 优先考虑通过用户空间机制(syscall, procfs, sysfs)或标准内核设施(如 cgroups)来影响进程行为,而不是在内核模块中直接修改进程属性。
  5. 查找导出符号:
    • 查看内核源码树中的 Module.symvers 文件。
    • 在编译好的内核上使用 nm vmlinux | grep <function_name>
    • 查看 /proc/kallsyms (需要 root 权限)。

理解这些接口对于进行深入的内核开发(如编写调度器、安全模块、容器技术)至关重要,但务必遵循内核开发的最佳实践和安全准则。

内核中修改用户态进程的euid

在 Linux 内核中修改用户态进程的 euid(effective user ID)需要通过进程凭证(credentials)操作接口。以下是详细步骤和关键接口:

核心步骤

  1. 获取目标进程的凭证结构体 (struct cred)
  2. 修改凭证中的 euid 字段
  3. 应用新凭证到目标进程
  4. 管理引用计数

关键导出符号接口

1. 凭证准备与修改

1
2
3
4
5
6
7
8
9
10
11
/* 获取当前进程凭证的可修改副本 */
struct cred *prepare_creds(void);
EXPORT_SYMBOL(prepare_creds);

/* 创建新的空白凭证 */
struct cred *cred_alloc_blank(void);
EXPORT_SYMBOL(cred_alloc_blank);

/* 复制凭证 */
struct cred *copy_creds(struct task_struct *tsk);
EXPORT_SYMBOL(copy_creds);

2. 修改 euid 的核心操作

1
2
3
4
5
6
7
/* 应用新凭证到当前进程 */
int commit_creds(struct cred *new);
EXPORT_SYMBOL(commit_creds);

/* 安全地修改目标进程的凭证 */
int set_cred_uc(struct task_struct *tsk, struct cred *new_cred);
EXPORT_SYMBOL_GPL(set_cred_uc);

3. 进程和凭证引用管理

1
2
3
4
5
6
7
8
9
10
11
/* 获取任务结构引用 */
struct task_struct *get_pid_task(struct pid *pid, enum pid_type type);
EXPORT_SYMBOL_GPL(get_pid_task);

/* 获取凭证引用 */
const struct cred *get_task_cred(struct task_struct *task);
EXPORT_SYMBOL(get_task_cred);

/* 释放凭证引用 */
void put_cred(const struct cred *cred);
EXPORT_SYMBOL(put_cred);

修改 euid 的典型流程

场景1:修改当前进程的 euid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void set_current_euid(uid_t new_euid)
{
struct cred *new;

// 1. 准备当前凭证的可修改副本
new = prepare_creds();
if (!new) return;

// 2. 修改 euid 字段
new->euid = new_euid;

// 3. 同时更新 fsuid (文件系统 UID)
new->fsuid = new_euid;

// 4. 应用新凭证
commit_creds(new);
}

场景2:修改其他进程的 euid(需 CAP_SETUID 权限)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int set_process_euid(pid_t pid, uid_t new_euid)
{
struct task_struct *target;
struct cred *new_cred;

// 1. 通过 PID 获取目标进程
target = pid_task(find_vpid(pid), PIDTYPE_PID);
if (!target) return -ESRCH;

// 2. 获取目标进程凭证引用
const struct cred *old_cred = get_task_cred(target);

// 3. 创建新凭证副本
new_cred = prepare_creds();
if (!new_cred) {
put_cred(old_cred);
return -ENOMEM;
}

// 4. 修改凭证字段
new_cred->euid = new_euid;
new_cred->fsuid = new_euid;

// 5. 应用新凭证到目标进程
int ret = set_cred_uc(target, new_cred);

// 6. 清理引用
put_cred(old_cred);
abort_creds(new_cred); // 仅当set_cred_uc失败时使用

return ret;
}

关键注意事项

  1. 权限要求

    • 修改自身:需要当前 euid==目标 euid 或具有 CAP_SETUID
    • 修改其他进程:必须具有 CAP_SETUID 能力
  2. 安全规则

    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;
    }
  3. 关联 ID 更新

    • 修改 euid 时必须同步更新 fsuid(文件系统 UID)
    • 真实 UID (uid) 和保存 UID (suid) 通常不变
  4. 审计跟踪

    1
    2
    // 修改凭证时触发审计
    security_task_fix_setuid(new, old, LSM_SETID_EUID);
  5. 内核版本差异

    版本范围 关键变化
    < 4.3 使用 __task_cred() 直接访问
    ≥ 4.3 引入 cred_has_capability()
    ≥ 5.9 struct cred 添加安全标签字段

完整示例(修改当前进程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <linux/cred.h>
#include <linux/sched.h>
#include <linux/security.h>

int safe_set_euid(uid_t euid)
{
struct cred *new = prepare_creds();
if (!new) return -ENOMEM;

const struct cred *old = current_cred();

// 权限检查
if (!capable(CAP_SETUID) &&
euid != old->uid &&
euid != old->euid &&
euid != old->suid) {
abort_creds(new);
return -EPERM;
}

// 修改凭证
new->euid = euid;
new->fsuid = euid;

// LSM 安全钩子
security_task_fix_setuid(new, old, LSM_SETID_EUID);

return commit_creds(new);
}

风险提示

  1. 竞态条件:修改其他进程凭证时,目标进程可能正在退出
  2. 能力泄漏:不当修改可能导致权限提升漏洞
  3. 审计追踪:所有凭证修改都会被记录到审计日志
  4. 容器兼容性:在容器内修改需考虑 user namespace 映射

重要:生产环境应优先使用用户空间工具(如 /bin/setuidlibcseteuid()),内核模块直接修改凭证仅适用于特殊场景(如安全模块、容器运行时)。