Linux 5.10内核如何获取父进程的启动参数信息

在Linux 5.10内核中,获取父进程的启动参数需要访问父进程的task_struct结构,并通过其内存描述符(mm_struct)提取命令行参数。以下是详细步骤和示例代码:

关键步骤

  1. 获取父进程的task_struct
    通过当前进程的real_parent指针访问父进程的task_struct

  2. 安全引用父进程
    使用get_task_struct()增加父进程的引用计数,防止其意外释放。

  3. 获取父进程的内存描述符
    使用get_task_mm()获取父进程的mm_struct(注意:内核线程无mm_struct)。

  4. 提取命令行参数
    mm_struct中读取arg_startarg_end之间的数据,即命令行参数。

  5. 安全读取数据
    使用access_process_vm()从父进程的用户空间安全复制数据。

  6. 释放资源
    递减mm_struct和父进程的引用计数。


示例代码

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/mm.h>

char *get_parent_cmdline(void) {
struct task_struct *parent = current->real_parent;
char *cmdline = NULL;
unsigned long len;
struct mm_struct *mm;

// 增加父进程引用计数
get_task_struct(parent);

// 获取父进程的 mm_struct
mm = get_task_mm(parent);
if (!mm) {
put_task_struct(parent);
return NULL; // 父进程是内核线程或无 mm
}

// 计算命令行参数长度
len = mm->arg_end - mm->arg_start;
if (len <= 0 || len > PAGE_SIZE) {
len = PAGE_SIZE; // 限制最大长度
}

// 分配缓冲区
cmdline = kmalloc(len, GFP_KERNEL);
if (!cmdline) {
mmput(mm);
put_task_struct(parent);
return NULL;
}

// 从父进程空间复制命令行参数
if (access_process_vm(parent, mm->arg_start, cmdline, len, 0) != len) {
kfree(cmdline);
cmdline = NULL;
}

// 释放资源
mmput(mm);
put_task_struct(parent);

return cmdline; // 调用者需用 kfree 释放
}

注意事项

  1. 内核线程处理
    内核线程的mmNULL,需检查get_task_mm()返回值。

  2. 用户空间地址
    arg_start指向用户空间地址,必须使用access_process_vm()安全访问。

  3. 资源管理

    • 使用get_task_struct()/put_task_struct()管理进程引用。
    • 使用get_task_mm()/mmput()管理内存描述符引用。
    • 调用者需用kfree()释放返回的缓冲区。
  4. 参数长度限制
    命令行参数通常不超过一页(PAGE_SIZE),但需防范溢出。

  5. 进程状态变化
    父进程可能退出或修改命令行,高可靠性场景需额外同步。


替代方法:通过/proc文件系统

用户空间可直接读取/proc/<pid>/cmdline获取任意进程参数:

1
cat /proc/$(ps -o ppid= -p $$)/cmdline

内核中实现类似功能需解析proc_pid_cmdline()等逻辑,较为复杂。

此方案适用于内核模块开发,需确保代码符合内核同步和安全规范。

Linux 5.10内核如何获取父进程的启动参数信息,要求不使用可能睡眠的函数

在Linux 5.10内核中,获取父进程的启动参数信息且不使用可能睡眠的函数,需要采用不同的策略。由于access_process_vm()可能睡眠(处理缺页异常),我们可以通过直接访问父进程的mm_struct中的参数指针来实现,但需注意这仅适用于父进程与当前进程共享内存映射的场景(如线程组)。以下是优化方案:

原子上下文安全方案

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
33
34
35
36
37
38
39
40
41
42
#include <linux/sched.h>
#include <linux/string.h>

int get_parent_cmdline(char *buf, size_t buf_size) {
struct task_struct *parent = current->real_parent;
struct mm_struct *mm;
unsigned long len;
int ret = 0;

// 增加父进程引用计数(非睡眠)
get_task_struct(parent);

// 获取父进程的内存描述符
mm = get_task_mm(parent);
if (!mm) {
ret = -ESRCH; // 父进程是内核线程
goto out_put_task;
}

// 计算命令行长度(确保不越界)
len = mm->arg_end - mm->arg_start;
if (len > buf_size - 1) len = buf_size - 1;

if (len > 0) {
// 直接访问参数内存区域(仅当内存映射相同时安全)
if (mm == current->mm) {
memcpy(buf, (char *)mm->arg_start, len);
} else {
ret = -EACCES; // 内存映射不同,无法直接访问
goto out_put_mm;
}
buf[len] = '\0'; // 确保字符串终止
} else {
ret = -ENODATA; // 无命令行数据
}

out_put_mm:
mmput(mm); // 释放mm引用
out_put_task:
put_task_struct(parent); // 释放进程引用
return (ret < 0) ? ret : len;
}

关键说明

  1. 免睡眠设计

    • 使用get_task_struct()get_task_mm()增加引用计数(原子操作)
    • 通过memcpy()直接复制内存(非睡眠)
    • 避免使用kmalloc/access_process_vm等可能阻塞的函数
  2. 使用限制

    1
    if (mm == current->mm) { ... }
    • 仅当父进程与当前进程共享内存映射时有效(常见于线程组内)
    • 适用于父进程是线程组长(group_leader)的场景
  3. 安全边界检查

    • 严格验证arg_start/arg_end范围
    • 限制拷贝长度防止缓冲区溢出
    • 添加NULL终止符保证字符串安全
  4. 错误处理

    • -ESRCH:父进程是内核线程(无mm
    • -EACCES:内存映射不匹配(无法直接访问)
    • -ENODATA:命令行参数长度为0

替代方案(适用于任意上下文)

通过procfs接口获取(无睡眠风险):

1
2
3
4
5
6
7
8
char buf[256];
struct path p;
char *pathname = "/proc/xxxx/cmdline"; // 父进程PID

kern_path(pathname, LOOKUP_FOLLOW, &p);
struct file *f = dentry_open(&p, O_RDONLY, current_cred());
kernel_read(f, buf, sizeof(buf), 0);
fput(f);

各方案适用场景对比

方案 原子安全 通用性 复杂度 适用场景
直接内存访问 线程组内共享内存的进程
procfs接口读取 任意进程(需构造proc路径)
access_process_vm 非原子上下文

注意事项

  1. 内存映射一致性

    • 直接访问方案依赖mm == current->mm条件
    • 在进程克隆(clone(CLONE_VM))或线程场景下有效
    • 常规父子进程关系通常不共享内存映射
  2. 性能考量

    • 直接内存访问速度最快(无上下文切换)
    • procfs方案需文件系统操作,但有内核缓存优化
  3. 安全建议

    • 生产环境优先使用procfs方案
    • 调试用途可使用直接访问方案
    • 确保缓冲区大小足够(建议至少PAGE_SIZE

重要提示:在标准父子进程关系中(非线程组),推荐通过/proc/$PPID/cmdline获取参数,这是最安全可靠且符合内核设计的方法。

上述方案替换掉可能睡眠的mmput函数

在Linux 5.10内核的原子上下文中,确实需要避免使用可能睡眠的函数如mmput()。以下是完全无睡眠的解决方案,使用引用计数原子操作:

原子安全实现方案

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <linux/sched.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/rcupdate.h>

int get_parent_cmdline_atomic(char *buf, size_t buf_size) {
struct task_struct *parent;
struct mm_struct *mm;
unsigned long len;
int ret = -ESRCH;

rcu_read_lock(); // RCU保护,防止父进程消失

// 获取父进程指针(RCU保护)
parent = rcu_dereference(current->real_parent);
if (!parent || !get_task_struct_rcu_user(parent)) {
rcu_read_unlock();
return -ESRCH;
}

rcu_read_unlock();

// 原子获取内存描述符
mm = get_task_mm(parent);
if (!mm) {
ret = -ENOENT; // 父进程是内核线程
goto out_put_task;
}

// 安全获取参数长度
len = mm->arg_end - mm->arg_start;
if (len == 0) {
ret = -ENODATA;
goto out_put_mm;
}

// 限制最大长度
if (len > buf_size - 1)
len = buf_size - 1;

// 仅当共享地址空间时直接访问
if (mm == current->mm) {
memcpy(buf, (char *)mm->arg_start, len);
buf[len] = '\0';
ret = len;
} else {
ret = -EACCES; // 内存空间不共享
}

out_put_mm:
// 原子递减mm引用计数
mmput(mm);

out_put_task:
// 原子递减进程引用计数
put_task_struct(parent);

return ret;
}

关键优化点

  1. 完全无睡眠的引用计数管理

    1
    2
    3
    4
    5
    6
    7
    // 使用RCU保护的进程获取
    parent = rcu_dereference(current->real_parent);
    get_task_struct_rcu_user(parent);

    // 原子递减引用计数
    mmput(mm); // 使用原子操作版本
    put_task_struct(parent);
  2. 安全内存访问保证

    • 通过mm == current->mm检查确保共享地址空间
    • 直接使用memcpy()避免access_process_vm()的睡眠风险
    • 严格长度检查防止越界访问
  3. 错误处理代码路径

    • -ESRCH: 父进程不可访问
    • -ENOENT: 父进程是内核线程
    • -ENODATA: 无命令行参数
    • -EACCES: 内存空间不共享
    • >0: 成功复制的字节数

替代方案:完全无引用计数操作

对于需要极致原子性的场景(如中断处理程序):

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
33
34
35
36
37
38
#include <linux/sched.h>

int get_parent_cmdline_pure_atomic(char *buf, size_t buf_size) {
struct task_struct *parent;
struct mm_struct *mm;
unsigned long len;

// 纯RCU读取,无引用计数操作
rcu_read_lock();

parent = rcu_dereference(current->real_parent);
if (!parent || thread_group_leader(parent)) {
rcu_read_unlock();
return -ESRCH;
}

mm = rcu_dereference(parent->mm);
if (!mm) {
rcu_read_unlock();
return -ENOENT;
}

// 快速检查参数区域
len = mm->arg_end - mm->arg_start;
if (len == 0 || len > buf_size - 1) {
rcu_read_unlock();
return len ? -ENOSPC : -ENODATA;
}

// 仅当当前进程共享内存空间时
if (mm == current->mm) {
memcpy(buf, (char *)mm->arg_start, len);
buf[len] = '\0';
}

rcu_read_unlock();
return (mm == current->mm) ? len : -EACCES;
}

方案对比

特性 RCU+引用计数方案 纯RCU方案
进程生命周期保证 高(引用计数保持) 中(RCU保护期间安全)
内存访问安全性 中(需快速完成)
适用上下文 进程上下文/原子上下文 严格原子上下文
内存消耗 有临时引用计数开销 零开销
代码复杂度
睡眠风险

使用限制说明

  1. 共享内存空间要求

    • 仅当mm == current->mm时有效(同线程组内)
    • 典型父子进程通常不满足此条件
    • 适用于pthread_create()创建的线程
  2. 原子上下文约束

    1
    2
    3
    4
    // 以下操作在原子上下文安全
    rcu_read_lock();
    memcpy();
    pointer_dereference();
  3. 实际应用建议

    • 线程间通信:优先使用此方案
    • 标准父子进程:通过proc_pid_cmdline()实现
    • 性能敏感场景:预先在进程创建时缓存参数

关键提示:在Linux 5.10中,task_struct->mmmm_struct字段在RCU读取侧临界区内是稳定的,但操作完成后必须立即释放RCU锁。生产环境建议添加CONFIG_DEBUG_ATOMIC_SLEEP验证。