Linux中内核调用用户空间程序的实现
从内核空间启动用户空间程序call_usermodehelper
int call_usermodehelper(char *path, char **argv, char **envp, int wait)
{DECLARE_COMPLETION(done);struct subprocess_info sub_info = {.complete = &done,.path = path,.argv = argv,.envp = envp,.wait = wait,.retval = 0,};DECLARE_WORK(work, __call_usermodehelper, &sub_info);if (!khelper_wq)return -EBUSY;if (path[0] == '\0')return 0;queue_work(khelper_wq, &work);wait_for_completion(&done);return sub_info.retval;
}
void __init usermodehelper_init(void)
{khelper_wq = create_singlethread_workqueue("khelper");BUG_ON(!khelper_wq);
}
函数功能
call_usermodehelper
用于从内核空间启动用户空间程序,是内核与用户空间交互的重要机制
代码逐段解释
call_usermodehelper
函数
第1-2行:函数声明和完成量定义
int call_usermodehelper(char *path, char **argv, char **envp, int wait)
{DECLARE_COMPLETION(done);
- 参数:
path
: 要执行的用户空间程序路径argv
: 参数数组(以NULL结束)envp
: 环境变量数组(以NULL结束)wait
: 是否等待程序执行完成
DECLARE_COMPLETION(done)
:- 声明并初始化一个完成量(completion)
- 用于同步,当工作完成后会通知等待者
第3-10行:子进程信息结构体初始化
struct subprocess_info sub_info = {.complete = &done,.path = path,.argv = argv,.envp = envp,.wait = wait,.retval = 0,};
struct subprocess_info
: 包含执行用户空间程序所需的所有信息- 字段说明:
.complete = &done
: 指向完成量,用于通知执行完成.path = path
: 要执行的程序路径.argv = argv
: 程序参数.envp = envp
: 环境变量.wait = wait
: 是否等待标志.retval = 0
: 返回值初始化为0
第11行:工作项声明
DECLARE_WORK(work, __call_usermodehelper, &sub_info);
DECLARE_WORK
: 声明一个工作队列项- 参数:
work
: 工作项名称__call_usermodehelper
: 实际执行工作的函数&sub_info
: 传递给工作函数的参数
第13-14行:工作队列检查
if (!khelper_wq)return -EBUSY;
khelper_wq
: 全局工作队列指针- 检查工作队列是否已初始化,如果没有则返回
-EBUSY
(系统忙)
第16-17行:空路径检查
if (path[0] == '\0')return 0;
- 检查路径是否为空字符串
- 如果是空路径,直接返回0(成功),不执行任何操作
第19行:提交工作到工作队列
queue_work(khelper_wq, &work);
queue_work
: 将工作项提交到工作队列khelper_wq
: 内核工作队列&work
: 要执行的工作项
第20-21行:等待完成并返回结果
wait_for_completion(&done);return sub_info.retval;
wait_for_completion(&done)
:- 等待完成量,阻塞当前线程直到工作完成
- 当
__call_usermodehelper
执行完成后会触发完成量
return sub_info.retval
: 返回子进程的执行结果
usermodehelper_init
函数
第1行:初始化函数声明
void __init usermodehelper_init(void)
__init
: 表示该函数只在初始化阶段使用,初始化完成后内存会被释放- 系统启动时调用的初始化函数
第2-3行:创建工作队列
khelper_wq = create_singlethread_workqueue("khelper");BUG_ON(!khelper_wq);
create_singlethread_workqueue("khelper")
:- 创建单线程的工作队列
- "
khelper
"是工作队列的名称 - 返回工作队列指针赋值给全局变量
khelper_wq
BUG_ON(!khelper_wq)
:- 如果创建工作队列失败(返回NULL),触发内核BUG
- 这是严重错误,系统无法正常运行
创建内核线程来启动用户空间程序__call_usermodehelper
static void __call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;/* CLONE_VFORK: wait until the usermode helper has execve'd* successfully We need the data structures to stay around* until that is done. */if (sub_info->wait)pid = kernel_thread(wait_for_helper, sub_info,CLONE_FS | CLONE_FILES | SIGCHLD);elsepid = kernel_thread(____call_usermodehelper, sub_info,CLONE_VFORK | SIGCHLD);if (pid < 0) {sub_info->retval = pid;complete(sub_info->complete);} else if (!sub_info->wait)complete(sub_info->complete);
}
函数功能
这个函数是用户模式辅助函数,负责创建内核线程来启动用户空间程序
代码逐段解释
第1-3行:函数声明和变量定义
static void __call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;
static void
: 静态函数,只在当前文件内可见void *data
: 泛型指针参数,实际指向struct subprocess_info
struct subprocess_info *sub_info = data
:- 将泛型指针转换为具体的结构体指针
- 包含执行用户空间程序所需的所有信息
pid_t pid
: 存储新创建的内核线程的进程ID
第5-8行:注释说明克隆标志的含义
/* CLONE_VFORK: wait until the usermode helper has execve'd* successfully We need the data structures to stay around* until that is done. */
CLONE_VFORK
: 重要的克隆标志- 作用: 父线程(内核线程)会等待子线程成功执行
execve()
系统调用 - 必要性: 确保子进程在执行
execve()
之前,父进程的数据结构保持有效
第9-14行:根据等待标志选择执行策略
if (sub_info->wait)pid = kernel_thread(wait_for_helper, sub_info,CLONE_FS | CLONE_FILES | SIGCHLD);elsepid = kernel_thread(____call_usermodehelper, sub_info,CLONE_VFORK | SIGCHLD);
情况1:需要等待完成(sub_info->wait为真)
pid = kernel_thread(wait_for_helper, sub_info, CLONE_FS | CLONE_FILES | SIGCHLD);
- 执行函数:
wait_for_helper
- 克隆标志:
CLONE_FS
: 共享文件系统信息CLONE_FILES
: 共享文件描述符表SIGCHLD
: 子进程终止时向父进程发送信号
- 特点: 父线程会等待子进程完全执行完毕
情况2:不需要等待完成(sub_info->wait为假)
pid = kernel_thread(____call_usermodehelper, sub_info, CLONE_VFORK | SIGCHLD);
- 执行函数:
____call_usermodehelper
(实际启动用户程序的函数) - 克隆标志:
CLONE_VFORK
: 关键标志,父线程等待子线程执行execve()
SIGCHLD
: 子进程终止时发送信号
- 特点: 父线程只等待到子进程成功执行
execve()
,不等待用户程序执行完成
第16-21行:处理线程创建结果
if (pid < 0) {sub_info->retval = pid;complete(sub_info->complete);} else if (!sub_info->wait)complete(sub_info->complete);
情况1:线程创建失败(pid
< 0)
if (pid < 0) {sub_info->retval = pid;complete(sub_info->complete);
}
pid < 0
: 表示kernel_thread
调用失败sub_info->retval = pid
: 将错误码保存到返回结果中complete(sub_info->complete)
: 立即触发完成量,通知等待者- 结果: 调用者立即得到错误返回
情况2:线程创建成功且不需要等待
else if (!sub_info->wait)complete(sub_info->complete);
- 条件: 线程创建成功且
sub_info->wait
为假 complete(sub_info->complete)
: 立即触发完成量- 逻辑: 对于非等待模式,一旦成功创建了执行用户程序的内核线程,就认为任务完成
情况3:线程创建成功且需要等待
- 隐含逻辑:
- 不立即触发完成量
wait_for_helper
函数会在用户程序执行完成后触发完成量
克隆标志的深层含义
CLONE_VFORK
的关键作用:
// 没有CLONE_VFORK的情况
父线程创建子线程 → 立即继续执行
子线程准备execve时,父线程可能已经释放了sub_info内存 → 崩溃!// 有CLONE_VFORK的情况
父线程创建子线程 → 父线程阻塞等待
子线程执行execve成功 → 父线程被唤醒继续执行
子线程执行execve失败 → 父线程也被唤醒
错误处理策略
// 线程创建失败的场景
kernel_thread失败 → pid = 负数错误码
立即设置retval = 错误码
立即触发complete通知调用者
调用者得到错误信息// 线程创建成功的场景
pid = 有效的进程ID
根据wait标志决定何时通知调用者
管理用户空间进程的执行和状态收集wait_for_helper
static int wait_for_helper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;struct k_sigaction sa;/* Install a handler: if SIGCLD isn't handled sys_wait4 won't* populate the status, but will return -ECHILD. */sa.sa.sa_handler = SIG_IGN;sa.sa.sa_flags = 0;siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);allow_signal(SIGCHLD);pid = kernel_thread(____call_usermodehelper, sub_info, SIGCHLD);if (pid < 0) {sub_info->retval = pid;} else {/** Normally it is bogus to call wait4() from in-kernel because* wait4() wants to write the exit code to a userspace address.* But wait_for_helper() always runs as keventd, and put_user()* to a kernel address works OK for kernel threads, due to their* having an mm_segment_t which spans the entire address space.** Thus the __user pointer cast is valid here.*/sys_wait4(pid, (int __user *) &sub_info->retval, 0, NULL);}complete(sub_info->complete);return 0;
}
函数功能
这个函数负责等待用户模式助手进程执行完成,并收集其退出状态。它在独立的内核线程中运行,专门用于管理用户空间进程的执行和状态收集
代码逐段解释
第1-4行:函数声明和变量定义
static int wait_for_helper(void *data)
{struct subprocess_info *sub_info = data;pid_t pid;struct k_sigaction sa;
static int
: 静态函数,返回整型状态void *data
: 泛型指针参数,实际指向struct subprocess_info
struct subprocess_info *sub_info = data
: 转换为具体的子进程信息结构体pid_t pid
: 存储实际执行用户程序的内核线程PIDstruct k_sigaction sa
: 内核信号处理结构,用于配置SIGCHLD信号处理
第6-12行:信号处理配置
/* Install a handler: if SIGCLD isn't handled sys_wait4 won't* populate the status, but will return -ECHILD. */sa.sa.sa_handler = SIG_IGN;sa.sa.sa_flags = 0;siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);allow_signal(SIGCHLD);
详细信号配置过程:
sa.sa.sa_handler = SIG_IGN; // 设置信号处理函数为忽略
sa.sa.sa_flags = 0; // 无特殊标志
siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD)); // 设置信号掩码,阻塞SIGCHLD
do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0); // 应用信号处理配置
allow_signal(SIGCHLD); // 允许当前线程接收SIGCHLD信号
信号处理的重要性:
- 注释说明: 如果不处理SIGCHLD信号,
sys_wait4
会返回-ECHILD
错误而不会填充状态 SIG_IGN
: 忽略SIGCHLD信号,让wait4
正常工作- 信号掩码: 确保在执行期间不会意外处理SIGCHLD信号
第14-16行:创建实际的工作线程
pid = kernel_thread(____call_usermodehelper, sub_info, SIGCHLD);if (pid < 0) {sub_info->retval = pid;
kernel_thread
: 创建新的内核线程- 执行函数:
____call_usermodehelper
- 实际启动用户空间程序的函数 - 克隆标志:
SIGCHLD
- 子进程终止时发送信号 - 错误处理: 如果线程创建失败(
pid
< 0),将错误码保存到retval
第17-28行:等待子进程完成
} else {/** Normally it is bogus to call wait4() from in-kernel because* wait4() wants to write the exit code to a userspace address.* But wait_for_helper() always runs as keventd, and put_user()* to a kernel address works OK for kernel threads, due to their* having an mm_segment_t which spans the entire address space.** Thus the __user pointer cast is valid here.*/sys_wait4(pid, (int __user *) &sub_info->retval, 0, NULL);}
关键注释解释:
- 通常问题: 在内核中调用
wait4
是有问题的,因为wait4
期望将退出代码写入用户空间地址 - 特殊情况:
wait_for_helper
总是作为keventd
运行,内核线程的mm_segment_t
覆盖整个地址空间 - 结论: 将内核地址强制转换为
__user
指针在这里是有效的
sys_wait4(pid, (int __user *) &sub_info->retval, 0, NULL);
- 参数:
pid
: 要等待的子进程ID(int __user *) &sub_info->retval
: 存储退出状态的位置(强制转换)0
: 无选项NULL
: 不返回资源使用信息
- 作用: 阻塞等待指定PID的进程退出,并将退出状态保存到
sub_info->retval
第30-31行:通知完成并返回
complete(sub_info->complete);return 0;
complete(sub_info->complete)
: 触发完成量,通知等待的调用者进程已结束return 0
: 函数成功返回0(wait_for_helper
自身的状态,不是用户进程的状态)
完整的执行流程
初始化信号集siginitset
static inline void siginitset(sigset_t *set, unsigned long mask)
{set->sig[0] = mask;switch (_NSIG_WORDS) {default:memset(&set->sig[1], 0, sizeof(long)*(_NSIG_WORDS-1));break;case 2: set->sig[1] = 0;case 1: ;}
}
函数功能
这个函数用于初始化信号集(sigset_t
),设置第一个信号字的掩码,并将其余信号字清零。它是信号处理的基础函数,确保信号集处于已知的初始状态
代码逐段解释
第1行:函数声明
static inline void siginitset(sigset_t *set, unsigned long mask)
static inline
: 静态内联函数,建议编译器将代码直接插入调用处void
: 无返回值sigset_t *set
: 指向信号集结构的指针,信号集用于表示一组信号unsigned long mask
: 位掩码,每个位代表一个信号(1-32)
第2行:设置第一个信号字
set->sig[0] = mask;
set->sig[0]
: 信号集的第一个"字"(word)mask
: 32位(或64位)的位掩码- 作用: 设置信号1-32的掩码,每个位对应一个信号号
第3-10行:清零其余信号字
switch (_NSIG_WORDS) {default:memset(&set->sig[1], 0, sizeof(long)*(_NSIG_WORDS-1));break;case 2: set->sig[1] = 0;case 1: ;}
情况1: default
(信号字数大于2)
default:memset(&set->sig[1], 0, sizeof(long)*(_NSIG_WORDS-1));break;
- 条件: 当
_NSIG_WORDS > 2
时执行 &set->sig[1]
: 从第二个信号字开始sizeof(long)*(_NSIG_WORDS-1)
: 计算需要清零的字节数memset
: 批量清零剩余的所有信号字
情况2: case 2
(正好2个信号字)
case 2: set->sig[1] = 0;
- 条件: 当
_NSIG_WORDS == 2
时执行 set->sig[1] = 0
: 手动清零第二个信号字- 注意: 没有break语句,会继续执行到case 1
情况3: case 1
(只有1个信号字)
case 1: ;
- 条件: 当
_NSIG_WORDS == 1
时执行 - 空语句
: ;
: 什么都不做,因为只有一个信号字,已经在第2行设置过了
允许当前线程接收指定的信号allow_signal
int allow_signal(int sig)
{if (sig < 1 || sig > _NSIG)return -EINVAL;spin_lock_irq(¤t->sighand->siglock);sigdelset(¤t->blocked, sig);if (!current->mm) {/* Kernel threads handle their own signals.Let the signal code know it'll be handled, sothat they don't get converted to SIGKILL orjust silently dropped */current->sighand->action[(sig)-1].sa.sa_handler = (void __user *)2;}recalc_sigpending();spin_unlock_irq(¤t->sighand->siglock);return 0;
}
函数功能
allow_signal
函数用于允许当前线程接收指定的信号。它主要在内核线程中使用,因为内核线程默认阻塞所有信号,需要显式允许才能接收特定信号
代码逐段解释
第1-2行:函数声明和参数验证
int allow_signal(int sig)
{if (sig < 1 || sig > _NSIG)return -EINVAL;
- 参数:
sig
- 要允许的信号编号 - 验证: 检查信号编号是否在有效范围内(1到_NSIG)
- 错误处理: 如果信号编号无效,返回
-EINVAL
(无效参数错误)
第4行:获取信号处理锁
spin_lock_irq(¤t->sighand->siglock);
spin_lock_irq
: 获取自旋锁并禁用中断current->sighand->siglock
: 当前进程信号处理器的自旋锁- 目的: 防止在修改信号状态时发生竞争条件
第5行:从阻塞掩码中移除信号
sigdelset(¤t->blocked, sig);
current->blocked
: 当前线程的阻塞信号集sigdelset
: 从阻塞集中删除指定信号- 效果: 该信号现在可以被传递给当前线程
第6-13行:内核线程的特殊处理
if (!current->mm) {/* Kernel threads handle their own signals.Let the signal code know it'll be handled, sothat they don't get converted to SIGKILL orjust silently dropped */current->sighand->action[(sig)-1].sa.sa_handler = (void __user *)2;}
-
!current->mm
: 检查当前线程是否是内核线程- 用户空间进程有内存描述符(
mm
) - 内核线程的
mm
为NULL
- 用户空间进程有内存描述符(
-
内核线程自己处理信号:与用户进程不同,内核线程需要显式处理信号
-
目的:让信号处理代码知道这个信号会被处理,避免信号被转换为SIGKILL或静默丢弃
current->sighand->action[(sig)-1].sa.sa_handler = (void __user *)2;
(void __user *)2
: 魔数指针值,不是真正的函数指针- 含义:告诉内核信号处理系统,这个信号由内核线程自己处理
- 索引:
action[(sig)-1]
- 信号动作数组从0开始索引
第14行:重新计算挂起信号状态
recalc_sigpending();
- 功能:重新计算当前线程的挂起信号状态
- 必要性:由于阻塞掩码改变,可能需要更新
TIF_SIGPENDING
标志 - 作用:确保信号传递机制能够正确工作
第15-16行:释放锁并返回
spin_unlock_irq(¤t->sighand->siglock);return 0;
spin_unlock_irq
: 释放自旋锁并恢复中断状态return 0
: 返回成功状态
设计原理
为什么内核线程需要特殊处理?
- 无用户上下文: 内核线程没有用户空间,无法执行用户注册的信号处理函数
- 默认阻塞: 内核线程默认阻塞所有信号,避免意外中断内核操作
- 显式控制: 要求内核线程显式声明它们要处理哪些信号
总结
allow_signal
函数是Linux内核信号处理机制的关键组成部分,特别是对于内核线程的信号管理:
- 主要用途: 允许内核线程接收特定信号
- 核心操作: 从阻塞掩码中移除信号
- 特殊处理: 为内核线程设置特殊的信号处理标记
- 线程安全: 使用自旋锁保护信号状态修改
- 状态同步: 确保信号挂起状态正确更新
调用execve
启动用户空间程序____call_usermodehelper
static int ____call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;int retval;/* Unblock all signals. */flush_signals(current);spin_lock_irq(¤t->sighand->siglock);flush_signal_handlers(current, 1);sigemptyset(¤t->blocked);recalc_sigpending();spin_unlock_irq(¤t->sighand->siglock);/* We can run anywhere, unlike our parent keventd(). */set_cpus_allowed(current, CPU_MASK_ALL);retval = -EPERM;if (current->fs->root)retval = execve(sub_info->path, sub_info->argv,sub_info->envp);/* Exec failed? */sub_info->retval = retval;do_exit(0);
}
函数功能
这个函数是用户模式助手机制的核心执行函数,负责在内核线程的上下文中执行用户空间程序。它是实际调用execve
来启动用户空间程序的地方
代码逐段解释
第1-3行:函数声明和变量定义
static int ____call_usermodehelper(void *data)
{struct subprocess_info *sub_info = data;int retval;
static int
: 静态函数,返回整型状态void *data
: 泛型指针参数,实际指向struct subprocess_info
struct subprocess_info *sub_info = data
: 转换为子进程信息结构体指针retval
: 存储执行结果的变量
第5-6行:刷新信号处理
/* Unblock all signals. */flush_signals(current);
- 解除所有信号的阻塞
flush_signals(current)
: 刷新当前线程的信号状态- 清除挂起的信号
- 重置信号处理状态
- 确保执行用户程序时信号处理处于干净状态
第7-12行:信号处理环境清理
spin_lock_irq(¤t->sighand->siglock);flush_signal_handlers(current, 1);sigemptyset(¤t->blocked);recalc_sigpending();spin_unlock_irq(¤t->sighand->siglock);
spin_lock_irq(¤t->sighand->siglock)
: 获取信号处理锁并禁用中断flush_signal_handlers(current, 1)
:- 刷新信号处理程序
- 第二个参数
1
表示重置为默认处理(SIG_DFL)
sigemptyset(¤t->blocked)
:- 清空阻塞信号集
- 确保没有信号被阻塞,用户程序可以接收所有信号
recalc_sigpending()
: 重新计算挂起信号状态spin_unlock_irq(¤t->sighand->siglock)
: 释放锁并恢复中断
第14-15行:设置CPU亲和性
/* We can run anywhere, unlike our parent keventd(). */set_cpus_allowed(current, CPU_MASK_ALL);
- 可以在任何CPU上运行,与父进程
keventd
不同 set_cpus_allowed(current, CPU_MASK_ALL)
:- 设置当前线程可以在所有CPU上运行
CPU_MASK_ALL
: 所有CPU的位掩码- 提供更好的负载均衡和性能
第17-19行:执行用户空间程序
retval = -EPERM;if (current->fs->root)retval = execve(sub_info->path, sub_info->argv, sub_info->envp);
retval = -EPERM
: 默认返回值设为"操作不允许"错误if (current->fs->root)
: 检查当前线程是否有有效的根文件系统- 这是执行
execve
的前提条件 - 内核线程必须有有效的文件系统上下文才能执行用户程序
- 这是执行
execve(sub_info->path, sub_info->argv, sub_info->envp)
:- 执行用户空间程序
sub_info->path
: 程序路径sub_info->argv
: 参数数组sub_info->envp
: 环境变量数组
execve
的特殊性:
- 如果成功,这个函数不会返回(进程被用户程序替换)
- 如果失败,返回错误码
第21-23行:处理执行失败
/* Exec failed? */sub_info->retval = retval;do_exit(0);
sub_info->retval = retval
: 将执行结果保存到子进程信息结构中- 如果
execve
成功,这行代码不会执行 - 只有
execve
失败时才会执行到这里
- 如果
do_exit(0)
: 终止当前内核线程- 参数
0
是退出码 - 线程资源被回收
- 参数
关键设计原理
信号环境清理的重要性
// 为什么需要彻底清理信号环境?
// 1. 内核线程可能设置了特殊的信号处理
// 2. 用户程序应该从干净的信号状态开始
// 3. 避免内核信号处理影响用户程序行为// 具体清理内容:
flush_signals: 清除挂起信号
flush_signal_handlers: 重置处理程序为默认
sigemptyset: 清空阻塞信号集
执行上下文准备
// 内核线程执行用户程序的挑战:
// 1. 文件系统上下文: 需要有效的root
// 2. 信号处理: 需要重置为用户预期状态
// 3. CPU调度: 允许在所有CPU上运行提高性能
// 4. 内存管理: execve会处理地址空间切换
错误处理策略
// 执行失败时的处理:
// 1. 保存错误码: 让调用者知道失败原因
// 2. 线程退出: 清理资源,避免僵尸内核线程
// 3. 通过完成量机制通知等待者