【Linux】详解用户态和内核态&&内核中信号被处置惩罚的时机&&sigaction信号自定

[复制链接]
发表于 2026-10-3 15:36:37 来自手机 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

用户态与内核态:信号到底在什么时候被处理

这是 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企服评测-应用市场 编辑提示

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表