马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
用户态与内核态:信号到底在什么时候被处理
这是 Linux 进程编程里最容易讲糊的一组概念:为什么信号不是"一到就被执行"、为什么处理函数必须在用户态跑、sigaction 比 signal 强在哪。把它们串起来讲一遍,很多看不懂的代码就通了。
一、两种执行模式
1.1 用户态
用户态是用户进程的运行状态。这个状态下进程只能访问受限的系统资源,不能执行某些特权操作,也无权直接访问硬件或执行敏感的系统调用,一切要内核帮忙的请求都得走系统调用接口。
1.2 内核态
内核态是操作系统核心部分(内核)的运行状态。此时代码可以访问系统内所有内存空间,能执行特权指令,权限级别最高:访问硬件、执行敏感操作、管理系统资源都在这里做。
1.3 什么时候会切换
- 系统调用:用户态进程需要执行特权操作时,通过系统调用接口向内核发请求。
- 硬件中断:外设产生中断信号,CPU 切到内核态处理。
- 异常:除零、缺页这类软件异常。
这三种情况下操作系统都会先保存用户态上下文,再切到内核态处理完,最后切回用户态继续。切换的本质就是保存/恢复上下文并改变权限级别。
二、信号的处理时机
关键结论:进程从内核态切回用户态时,信号会被检测并处理。
如果信号的处置方式是默认动作,直接由内核完成即可,不需要切回用户态执行我们写的方法;如果处置方式是自定义的处理函数,就必须从内核态切回用户态去执行,执行完再进内核。
为什么非得切回用户态?因为要防止我们自己写的处理函数里做非法操作——用用户态的权限来约束它,内核没法替你兜底所有越界行为。这就是为什么"信号来了立刻打印一行"看着简单,却是整套机制里最讲究的一环。
未决信号的集合(pending 位图)在内核里维护,处理函数在用户态执行,中间经过一次权限切换,这就是信号完整生命周期。
三、内核级理解:1G 内核空间
每个进程的地址空间里都有一段内核空间(传统 32 位下是 1G),用来映射加载到内存中的操作系统。
- 内核中所有系统调用函数其实由一个函数指针数组管理,这个数组通过页表映射与那 1G 内核空间的某些虚拟地址建立对应关系。
- 正文代码要调用某个系统调用时,拿着虚拟地址在内核空间里找,再经页表映射到物理内存里定位到函数。
- 系统里有多个进程,各自 [0,3G] 用户空间映射内容不同,但都必须做系统调用,所以 [3GB,4GB] 这 1G 内核级空间映射的内容可以完全一致——换句话说,进程无论如何总能找到操作系统。
操作系统既然被映射进进程地址空间,是不是就能随便访问?当然不行。CPU 里有寄存器标识当前进程状态(例如 00 表示用户态、11 表示内核态),处于用户态时禁止进程访问操作系统内核内容。
四、用 sigaction 设置自定义处理函数
除了 signal,还有一个更强大的 sigaction。函数原型级别的用法:
- 第一个参数:信号编号(几号信号)。
- 第二个参数:描述新处置方式的
struct sigaction。
- 第三个参数:描述旧处置方式的
struct sigaction(不需要就传 NULL,用来保存旧的便于恢复)。
结构体各成员:
- 第一项:新设置的信号处理函数(类似
sa_handler)。
- 第二项:类似
sa_sigaction,功能更强,能拿到发送者的信息;不需要可设 NULL。
- 第三项:一个信号集,用来设置在处理 signum 信号的同时要屏蔽哪些信号。
- 第四项:修改某些默认行为(如
SA_RESTART 让被中断的系统调用自动重启)。
- 第五项:现代系统基本不用,不设置。
示例:自定义 2 号信号,持续打印 pending 位图
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void print(sigset_t &pending)
{
for (int i = 31; i >= 1; i--)
{
if (sigismember(&pending, i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void handler(int sig)
{
printf("catch signal:%d\n", sig);
}
int main()
{
struct sigaction act;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, NULL);
while (1)
{
pause();
}
return 0;
}
要点:修改结构体后要用 sigemptyset 初始化信号集,别直接拿一块未初始化的栈内存当 sa_mask 用,否则屏蔽集内容是垃圾值。
五、常见误区
- 误区:信号来了立刻中断当前代码——实际是"从内核态返回用户态时"才检测,所以处理函数的执行时刻有一定的延迟,取决于进程当时在做什么。
- 误区:处理函数里可以随便调库函数——信号处理函数是异步上下文,里面尽量只做
write 这类异步信号安全的操作,别做 malloc、printf 这类可能加锁的动作。
- 误区:屏蔽集是全局生效的——
sa_mask 只在该信号的处理函数执行期间屏蔽指定信号,处理完自动恢复。
- 误区:sigaction 的旧结构体可以不理——传 NULL 可以,但要恢复原处置就必须用第三个参数保存。
小结
把这条链记牢就够用了:系统调用/中断/异常 → 进入内核态 → 内核记录信号并维护 pending 位图 → 返回用户态时检测 → 自定义处理函数在用户态执行 → 再进内核恢复。理解了"切换时机",信号那一章的其余细节基本都顺着这条线自然展开。
qidao123.com企服评测-应用市场 编辑提示 |