前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >信号初相识:Linux 内核的 “隐形使者”

信号初相识:Linux 内核的 “隐形使者”

作者头像
用户11396661
发布2025-02-28 09:38:38
发布2025-02-28 09:38:38
6600
代码可运行
举报
文章被收录于专栏:C++开发C++开发
运行总次数:0
代码可运行

在 Linux 系统的广袤世界里,信号(Signal)宛如一位神秘的隐形使者,默默地在后台发挥着至关重要的作用。它是一种异步通信机制,如同古代的烽火台,当特定事件发生时,便会燃起 “烽火”,通知进程做出相应的反应。信号可以来自硬件异常,如内存访问错误、除零错误;也能由软件条件触发,像是用户按下特定的按键组合,或者程序主动调用系统函数发送信号 。

信号在 Linux 系统中有着广泛的应用场景。例如,当你在终端中运行一个程序时,按下Ctrl+C组合键,系统会发送一个SIGINT信号给正在运行的进程,通常这个信号会使进程终止运行。这就好比你在玩游戏时,突然按下暂停键,游戏进程接收到这个 “暂停信号” 后,就会停止当前的操作。又比如,在服务器程序中,通过捕获特定的信号,如SIGHUP信号,可以实现不重启服务而重新加载配置文件的功能,就像给正在工作的机器更换零件,却不影响其整体运行。

那么,这些信号在 Linux 内核中是如何被表示和管理的呢?这就如同揭开神秘使者的面纱,深入探索其背后的奥秘。接下来,我们将一同走进 Linux 内核的世界,探寻信号的 “内核之旅”。

信号的 “内核身份证”:数据结构解析

(一)sigset_t:信号集的神秘面纱

在 Linux 内核的信号管理体系中,sigset_t是一个极为关键的数据结构,它就像是一个精巧的信号容器,以位图的形式来表示信号集合。简单来说,sigset_t本质上是一个 64 位宽度的整数,每一个比特位都对应着一个信号 。例如,当第 1 位被设置为 1 时,就表示信号 1(SIGHUP)在这个信号集中是有效的;若为 0,则表示该信号无效。这就好比一个拥有 64 个房间的酒店,每个房间都可以容纳一个信号,房间的门开着(对应位为 1)表示信号在里面,门关着(对应位为 0)则表示信号不在。

为了方便对sigset_t进行操作,Linux 内核提供了一系列实用的函数。这些函数就像是酒店的管理人员,负责对房间(信号)进行各种操作。

  • sigemptyset(sigset_t *set):这个函数用于清空信号集,就像是把酒店所有房间的客人都请出去,将信号集中的所有比特位都置为 0 。
  • sigfillset(sigset_t *set):与sigemptyset相反,它会填充信号集,把所有比特位都置为 1,也就是让所有信号都进入酒店。
  • sigaddset(sigset_t *set, int signo):用于将指定的信号signo添加到信号集set中,就像把一位客人安排进对应的房间。
  • sigdelset(sigset_t *set, int signo):从信号集中删除指定的信号,相当于把房间里的客人请出去。
  • sigismember(const sigset_t *set, int signo):判断指定的信号是否在信号集中,就像查看某个房间是否住着特定的客人,如果是则返回 1,不是返回 0,出错返回 - 1。

下面是一个简单的示例代码,展示了如何使用这些函数来操作sigset_t:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

#include <signal.h>

void printsigset(const sigset_t *set) {

for (int i = 1; i <= 64; i++) {

if (i == 33) putchar(' ');

if (sigismember(set, i) == 1)

putchar('1');

else

putchar('0');

}

puts("");

}

int main() {

sigset_t st;

printf("1. 创建信号集\n");

printsigset(&st);

printf("\n2. 填充信号集\n");

sigfillset(&st);

printsigset(&st);

printf("\n3. 清空信号集\n");

sigemptyset(&st);

printsigset(&st);

printf("\n4. 添加信号SIGHUP(1), SIGINT(2), SIGKILL(9)到信号集\n");

sigaddset(&st, SIGHUP);

sigaddset(&st, SIGINT);

sigaddset(&st, SIGKILL);

printsigset(&st);

printf("\n5. 从信号集中删除SIGKILL(9)\n");

sigdelset(&st, SIGKILL);

printsigset(&st);

printf("\n6. 判断信号集是否包含SIGKILL(9)\n");

if (sigismember(&st, SIGKILL)) {

printf("SIGKILL is member\n");

} else {

printf("SIGKILL is not member\n");

}

return 0;

}
(二)task_struct 中的信号印记

在 Linux 内核中,task_struct是进程控制块(PCB)的核心结构体,它就像是进程的 “身份证”,记录了进程的各种信息。而在这个结构体中,有几个字段与信号处理密切相关,它们就像是 “身份证” 上与信号相关的特殊标识。

  • sighand:这是一个指向sighand_struct结构体的指针,它的作用是存放信号处理函数。在sighand_struct结构体中,包含了一个action[]数组,这个数组就像是一个信号处理函数的 “仓库”,每个元素都对应着一个信号的处理函数。例如,action[0]可能存放着信号 1 的处理函数,action[1]存放着信号 2 的处理函数。
  • signal:是一个指向signal_struct结构体的指针,signal_struct是嵌套在task_struct中的结构体,包含了一系列信号相关的字段 。其中,sig_blocked(信号屏蔽集)用于记录当前进程中被阻塞的信号集合,就像一个 “黑名单”,列出了进程不想立即接收的信号;sig_pending(未决信号集)表示当前进程中所有已到达但尚未被处理的信号,如同一个 “待办事项” 列表,记录着等待处理的信号。

这些字段与信号处理的关联紧密。当一个信号产生时,内核首先会检查该信号是否在sig_blocked集合中,如果在,那么这个信号就会被阻塞,暂时不会被处理,而是被添加到sig_pending集合中。只有当信号不在sig_blocked集合中时,它才有可能被递达给进程,进而触发相应的处理函数。而处理函数的地址就存储在sighand所指向的sighand_struct结构体的action[]数组中 。

例如,当进程接收到一个SIGINT信号时,内核会先查看sig_blocked集合中是否有SIGINT信号,如果没有,再根据action[]数组中SIGINT信号对应的处理函数来执行相应的操作。如果SIGINT信号在sig_blocked集合中,那么它就会被放入sig_pending集合,等待后续处理。

信号的三种 “人生状态”

(一)信号未决:等待召唤的 “预备役”

信号未决,是信号从产生到被处理之间的一种过渡状态 ,就像是一位士兵进入了 “预备役”,等待着被召唤执行任务。当一个信号产生时,内核会在进程控制块(PCB)中设置该信号的未决标识,表明这个信号已经到达,但还未被处理。这个信号就像是被放在了一个 “待处理” 的队列中,等待着合适的时机被处理。

例如,当进程接收到一个SIGINT信号(通常是用户按下Ctrl+C组合键产生),如果此时该信号被阻塞,那么它就会进入未决状态。在未决状态下,信号不会立即被处理,而是被暂时保存起来。直到信号的阻塞被解除,它才有可能被递达给进程进行处理 。

在task_struct结构体中,sig_pending字段用于记录未决信号集。这个信号集就像是一个 “待办事项” 清单,列出了所有已到达但尚未被处理的信号。当一个信号进入未决状态时,它会被添加到sig_pending信号集中,等待后续处理。

(二)信号阻塞:被 “暂停” 的执行

信号阻塞是进程对信号的一种控制方式,它可以让进程在特定的时间段内暂时不处理某些信号 ,就像是给信号的执行按下了 “暂停键”。进程通过设置阻塞信号集,来决定哪些信号在当前不被递达。被阻塞的信号在产生时,会进入未决状态,直到进程解除对该信号的阻塞,它才有可能被处理 。

在 Linux 中,进程可以使用sigprocmask函数来设置阻塞信号集。该函数的原型如下:

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

  • how参数决定了如何修改阻塞信号集,它有以下几种取值:
    • SIG_BLOCK:将set中的信号添加到当前的阻塞信号集中,即当前阻塞信号集 = 当前阻塞信号集 | set。
    • SIG_UNBLOCK:从当前阻塞信号集中移除set中的信号,即当前阻塞信号集 = 当前阻塞信号集 & ~set。
    • SIG_SETMASK:将当前阻塞信号集设置为set,即当前阻塞信号集 = set。
  • set参数是一个指向sigset_t类型的指针,它表示要设置或修改的信号集。
  • oldset参数是一个指向sigset_t类型的指针,用于保存原来的阻塞信号集。如果不需要保存原来的阻塞信号集,可以将其设置为NULL。

例如,以下代码展示了如何使用sigprocmask函数来阻塞SIGINT信号:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

#include <signal.h>

#include <unistd.h>

int main() {

sigset_t set;

// 初始化信号集

sigemptyset(&set);

// 将SIGINT信号添加到信号集

sigaddset(&set, SIGINT);

// 设置阻塞信号集,将SIGINT信号阻塞

sigprocmask(SIG_BLOCK, &set, NULL);

while (1) {

printf("I'm running, and SIGINT is blocked.\n");

sleep(1);

}

return 0;

}

在上述代码中,通过sigaddset函数将SIGINT信号添加到信号集set中,然后使用sigprocmask函数将set设置为阻塞信号集,这样SIGINT信号就被阻塞了。在循环中,进程会不断打印信息,即使用户按下Ctrl+C组合键,也不会终止进程,因为SIGINT信号被阻塞了 。

需要注意的是,阻塞和忽略是不同的概念。阻塞是指信号不会被递达,即信号不会进入处理流程;而忽略是在信号递达之后,进程选择不执行任何操作,直接丢弃该信号 。

(三)信号递达:最终的 “使命完成”

信号递达是信号生命周期的最后一个阶段,当信号从产生,经过可能的未决和阻塞状态后,最终被进程处理,就达到了信号递达状态 ,这就像是士兵完成了使命,执行了最终的任务。当信号递达时,进程会根据信号的类型和预先设置的处理方式来执行相应的动作。

信号递达时的处理动作通常有以下三种:

  • 执行默认动作:对于大多数信号,系统都有默认的处理动作。例如,SIGINT信号的默认动作是终止进程,SIGQUIT信号的默认动作是终止进程并产生核心转储文件 。
  • 忽略信号:进程可以选择忽略某些信号,即当信号递达时,不执行任何操作。例如,通过signal函数将某个信号的处理方式设置为SIG_IGN,就可以忽略该信号。
  • 捕捉信号:进程可以自定义信号处理函数,当信号递达时,调用自定义的处理函数来处理信号。例如,使用signal函数或sigaction函数来注册信号处理函数。

以下是一个使用signal函数捕捉SIGINT信号的示例代码:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

#include <signal.h>

#include <unistd.h>

// 自定义信号处理函数

void signal_handler(int signum) {

printf("Caught SIGINT, I won't be terminated!\n");

}

int main() {

// 注册信号处理函数,将SIGINT信号的处理函数设置为signal_handler

signal(SIGINT, signal_handler);

while (1) {

printf("I'm running, you can't terminate me by Ctrl+C easily.\n");

sleep(1);

}

return 0;

}

在上述代码中,通过signal函数将SIGINT信号的处理函数设置为signal_handler。当用户按下Ctrl+C组合键产生SIGINT信号时,信号递达后会调用signal_handler函数,而不是执行默认的终止进程动作,从而实现了对SIGINT信号的捕捉和自定义处理 。

实战演练:代码中的信号世界

(一)设置信号屏蔽字:sigprocmask 的魔法

在 Linux 系统中,sigprocmask函数就像是一把神奇的钥匙,用于设置和读取进程的信号屏蔽字。它可以帮助我们控制哪些信号在当前进程中被阻塞,哪些信号可以被递达 。下面通过一个具体的代码示例,来深入了解sigprocmask函数的使用方法和参数含义。

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

#include <signal.h>

#include <unistd.h>

int main() {

sigset_t set, oldset;

// 初始化信号集

sigemptyset(&set);

// 将SIGINT信号添加到信号集

sigaddset(&set, SIGINT);

// 设置信号屏蔽字,将SIGINT信号阻塞,保存原来的信号屏蔽字到oldset

if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {

perror("sigprocmask");

return 1;

}

printf("SIGINT信号已被阻塞,现在按下Ctrl+C不会终止进程。\n");

// 模拟一些工作

sleep(5);

// 恢复原来的信号屏蔽字,即解除对SIGINT信号的阻塞

if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {

perror("sigprocmask");

return 1;

}

printf("SIGINT信号阻塞已解除,现在按下Ctrl+C可以终止进程。\n");

// 防止程序立即退出

while (1) {

sleep(1);

}

return 0;

}

在上述代码中:

  • sigemptyset(&set):用于初始化一个空的信号集set,就像是清空一个容器,准备放入需要的信号。
  • sigaddset(&set, SIGINT):将SIGINT信号添加到信号集set中,这里SIGINT信号通常是由用户按下Ctrl+C组合键产生的 。
  • sigprocmask(SIG_BLOCK, &set, &oldset):SIG_BLOCK表示将set中的信号添加到当前进程的信号屏蔽字中,即阻塞SIGINT信号。&set是指向要操作的信号集的指针,&oldset用于保存原来的信号屏蔽字,以便后续恢复 。
  • sigprocmask(SIG_SETMASK, &oldset, NULL):SIG_SETMASK表示将当前进程的信号屏蔽字设置为oldset,即恢复原来的信号屏蔽状态,解除对SIGINT信号的阻塞 。

通过这个示例,可以清晰地看到sigprocmask函数如何实现信号的阻塞和解除阻塞操作,从而控制进程对信号的响应。

(二)获取未决信号集:sigpending 的洞察

sigpending函数就像是一个 “探测器”,用于获取当前进程的未决信号集 。通过这个函数,我们可以了解到哪些信号已经产生,但由于被阻塞等原因还未被处理。下面通过代码示例来展示如何使用sigpending函数获取未决信号集,并判断信号是否处于未决状态。

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>

#include <signal.h>

#include <unistd.h>

int main() {

sigset_t set, pendset;

// 初始化信号集

sigemptyset(&set);

// 将SIGUSR1信号添加到信号集

sigaddset(&set, SIGUSR1);

// 设置信号屏蔽字,将SIGUSR1信号阻塞

if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {

perror("sigprocmask");

return 1;

}

printf("SIGUSR1信号已被阻塞,现在发送SIGUSR1信号。\n");

// 发送SIGUSR1信号

if (raise(SIGUSR1) == -1) {

perror("raise");

return 1;

}

// 获取当前进程的未决信号集

if (sigpending(&pendset) == -1) {

perror("sigpending");

return 1;

}

// 判断SIGUSR1信号是否在未决信号集中

if (sigismember(&pendset, SIGUSR1)) {

printf("SIGUSR1信号处于未决状态。\n");

} else {

printf("SIGUSR1信号不处于未决状态。\n");

}

// 解除对SIGUSR1信号的阻塞

if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {

perror("sigprocmask");

return 1;

}

printf("SIGUSR1信号阻塞已解除。\n");

return 0;

}

在上述代码中:

  • sigemptyset(&set)和sigaddset(&set, SIGUSR1):初始化信号集并添加SIGUSR1信号。
  • sigprocmask(SIG_BLOCK, &set, NULL):阻塞SIGUSR1信号。
  • raise(SIGUSR1):发送SIGUSR1信号,由于该信号被阻塞,会进入未决状态。
  • sigpending(&pendset):获取当前进程的未决信号集,并存储在pendset中。
  • sigismember(&pendset, SIGUSR1):判断SIGUSR1信号是否在未决信号集pendset中,如果是则表示该信号处于未决状态 。
  • sigprocmask(SIG_UNBLOCK, &set, NULL):解除对SIGUSR1信号的阻塞 。

通过这个示例,我们可以直观地了解到sigpending函数的作用和使用方法,以及如何判断信号是否处于未决状态 。这对于深入理解信号在 Linux 内核中的处理机制非常有帮助。

总结与展望:信号知识的拓展

在 Linux 内核的复杂世界中,信号犹如一条条无形的纽带,连接着进程与系统事件,扮演着不可或缺的角色。通过对信号在内核中的表示方式的深入探索,我们逐渐揭开了信号神秘的面纱。从信号集的位图表示,到进程控制块中与信号相关的字段,每一个细节都蕴含着 Linux 内核设计的精妙之处。

信号的三种状态 —— 未决、阻塞和递达,清晰地描绘了信号从产生到被处理的完整生命周期。在这个过程中,信号屏蔽字和未决信号集的操作函数,如sigprocmask和sigpending,为我们提供了控制和了解信号状态的有力工具 。

然而,信号的世界远不止于此。在实际应用中,信号处理还涉及到更多高级话题,如信号的可靠传递、信号处理函数的可重入性、以及信号与多线程编程的交互等。这些内容将进一步深化我们对信号机制的理解,提升我们在复杂系统编程中的能力。

希望通过本文的介绍,能激发大家对 Linux 内核中信号机制的兴趣,促使大家在信号处理的领域中不断探索前行,挖掘更多关于信号的奥秘,为 Linux 系统编程打下坚实的基础 。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 信号的 “内核身份证”:数据结构解析
    • (一)sigset_t:信号集的神秘面纱
    • (二)task_struct 中的信号印记
  • 信号的三种 “人生状态”
    • (一)信号未决:等待召唤的 “预备役”
    • (二)信号阻塞:被 “暂停” 的执行
    • (三)信号递达:最终的 “使命完成”
  • 实战演练:代码中的信号世界
    • (一)设置信号屏蔽字:sigprocmask 的魔法
    • (二)获取未决信号集:sigpending 的洞察
  • 总结与展望:信号知识的拓展
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档