Linux----信号(产生、保存、处理)

目录

简单介绍

信号捕捉

信号产生

系统调用

首先看kill函数的使用:​编辑

类似的系统调用还有raise函数,他是进程给自己发信号;还有abort函数

软件条件

[示例 1:最基本的 alarm 使用](#示例 1:最基本的 alarm 使用)

[示例 2:覆盖原来的 alarm](#示例 2:覆盖原来的 alarm)

[示例 3:取消 alarm](#示例 3:取消 alarm)

[alarm() 返回值](#alarm() 返回值)

硬件异常

信号保存

pending、block、handler

信号集sigset_t

信息集操作函数

sigprocmask(操作block位图的函数)

sigpending(检查pending信号集,获取当前进程pending位图)

小栗子

总结

信号处理

用户态和内核态

sigaction函数

函数重入现象

volatile

SIGCHLD信号


简单介绍

平时使用最多的ctrl+c就是信号,每次按这个就会让运行的进程终止。

1 .Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

2.Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到键盘的信号。

3.前台进程在运行过程中用户随时可能按下Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的。

信号是进程之间事件异步通知的一种方式,属于软中断。

通过kill -l命令可以查看信号列表,其中1到31号是正常信号,34号之后是实时信号,本节不讨论实时信号。每个信号有一个宏定义名称,可在signal.h中找到定义。

可以使用man 7 signal命令查看关于信号的信息,比如什么时候产生什么信号,进程接收该信号后的默认动作是什么。

core dump与term都是终止程序,有什么区别?core 文件可以用 gdb 查问题。

动作类型 是否终止程序 是否产生 core 文件 常见来源 用途
TERM ✔ 是 ❌ 否 kill 命令、Ctrl+C、SIGTERM 正常结束
CORE ✔ 是 ✔ 是 错误、异常、SIGSEGV、SIGABRT 调试崩溃

信号处理常见方式有以下三种: 1. 忽略此信号。

**2. 执行该信号的默认处理动作。**如果进程没有注册信号处理函数且没有选择忽略信号,则系统会按照默认的处理方式来处理该信号。通常情况下,默认处理方式会导致进程终止或停止。

**3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号。**如果进程没有注册信号处理函数且没有选择忽略信号,则系统会按照默认的处理方式来处理该信号。通常情况下,默认处理方式会导致进程终止或停止。

信号捕捉

捕捉信号是操作系统中进程处理信号的一种方式,允许进程在接收到信号时执行自定义的操作,而不是执行信号的默认动作。

引入系统调用signal:

signal接口是Unix和Linux系统中用于处理信号的一个系统调用。它允许程序指定一个函数,当接收到特定信号时,该函数会被自动调用。通过这种方式,程序可以对信号进行自定义处理,或者忽略某些信号。

我们举一个捕捉2号信号的例子,对前台进程ctrl+c就会发送2号命令,看看他是否会执行hander:

注意,有一些信号是不能被捕获执行自定义动作的,比如9号和19号,因为系统需要一个"绝对可靠"的手段来杀掉进程。如果一个进程可以忽略 SIGKILL,它可能会变成永远无法杀死的"僵尸/恶意"进程。

信号产生

信号产生有五种方法:1.kill命令;2.向前台进程按键产生信号,比如ctrl+c;3.系统调用kill; 4.软件条件(管道通信,读端关闭,会向写端发送SIGPIPE信号);5.硬件异常产生信号

系统调用

首先看kill函数的使用:

类似的系统调用还有raise函数,他是进程给自己发信号;还有abort函数

abort() 是 C/C++ 中用于 立即终止程序 的函数,它比 exit() 更激烈,比 kill(SIGKILL) 弱一点,但作用非常接近崩溃程序abort() 会让程序立即异常终止,并且向自身发送 SIGABRT(信号 6)

特点:

  • 不会执行任何清理工作: 不调用 atexit 注册的函数,不执行 C++ 析构函数,不清理缓存

  • 立刻终止

  • 通常产生 core dump(如果系统开启 core dump)

函数 会不会清理资源 是否终止所有线程 是否产生 core dump
exit() ✔ 会执行清理(析构、flush) 否,只退出当前线程
abort() ❌ 不执行清理 是,整个进程终止 可能(取决于 ulimit)
kill(SIGKILL) ❌ 不执行清理 否(一般不产生)

程序遇到严重逻辑错误时,可以使用:

复制代码
assert(condition);

其实 assert 失败时内部就是调用 abort()。它会让程序崩溃,从而生成 core dump,便于使用 gdb 调试。abort() 发送的是 SIGABRT,该信号是 可捕捉的

可以这样捕捉:

注意这是一个信号捕捉的例外,abort发出的信号可以被捕捉,但是虽然hander没有写执行终止进程,最后仍然终止进程了。

软件条件

当管道通信时,读端关闭,会向写端发送SIGPIPE信号从而关闭写端。还有一种情况就是闹钟。alarm() 是 Linux 下用来 设置定时器 的函数,它会在指定秒数后向当前进程发送 SIGALRM 信号

可以触发:

  • 信号处理函数(如果你设置了 handler)

  • 或者(默认情况)让程序终止

特点

  1. 只精确到秒

  2. 一个进程只能维持 一个 alarm 计时器,重新调用 alarm() 会覆盖之前的。

  3. 传入 0 可以取消定时器。


示例 1:最基本的 alarm 使用

复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int sig)
{
    cout << "SIGALRM received!" << endl;
}

int main()
{
    signal(SIGALRM, handler);
    alarm(3);   // 3秒后触发信号

    while(1)
    {
        cout << "working..." << endl;
        sleep(1);
    }
}

输出:

复制代码
working...
working...
working...
SIGALRM received!
working...
...

示例 2:覆盖原来的 alarm

复制代码
alarm(10); // 10秒后触发
alarm(3);  // 覆盖成3秒后的

示例 3:取消 alarm

复制代码
alarm(0);  // 不再触发 SIGALRM

alarm() 返回值

alarm() 会返回 上一个定时器剩余的秒数。例如:

复制代码
alarm(10);
sleep(3);
unsigned int left = alarm(5);
cout << left << endl;  // 打印 7

解释:第一次 alarm(10),3 秒过去 → 还剩 7 秒,调用 alarm(5) 覆盖掉旧的定时,返回"旧的剩余秒数":7。

功能 说明
alarm(n) n 秒后发送 SIGALRM
alarm(0) 取消 alarm
返回值 上一个 alarm 剩余时间
一个进程只能有一个 alarm 后设的覆盖前一个
SIGALRM 默认行为是终止进程 但你可以用 signal 捕捉

附:在底层,alarm 函数通常与内核的定时器机制(如 ITIMER_REAL 类型的 POSIX 定时器)相关联。当 alarm 被调用时,内核会设置一个软定时器(soft timer),该定时器在指定的秒数后到期。当定时器到期时,内核会生成 SIGALRM 信号,并将其发送给调用 alarm 的进程。当操作系统中多个进程使用alarm的时候,OS就会借助最小堆,进行判断,要先向谁发送SIGALRM信号。直接取堆顶。

硬件异常

空指针解引用问题,除0问题。看一下代码:

分别受到了SIGSEGV和SIGFPE并且异常终止。这是进程接收到这些信号的默认行为,那么进程是必须退出的吗?不是,可以捕捉以上的异常信号,但是更推荐终止进程,为什么呢?

1.空指针解引用(野指针)问题

这个问题涉及 页表、MMU 以及 CR2、CR3 寄存器等虚拟内存管理机制。

在现代操作系统中,MMU 与页表共同负责虚拟地址到物理地址的转换,并提供内存访问保护。CR3 寄存器指向当前进程使用的页表根,用于支持进程切换时的地址空间切换;而 CR2 则用于记录导致页错误(Page Fault)的那个虚拟地址,方便内核进行错误定位与处理。

当 MMU 在地址转换时发现某个虚拟地址没有对应的有效物理页(例如程序访问空指针或野指针),就会触发页错误异常。此时 CPU 会把触发错误的虚拟地址写入 CR2 寄存器,然后将控制权交给操作系统。内核判断这是非法访问后,会向进程发送 SIGSEGV(信号 11),最终导致程序异常终止。

2.除0问题

在进程执行计算任务时,所有算术运算都由 CPU 完成。计算过程中可能出现错误,比如除以 0,因此 CPU 必须能准确判断异常的发生。

CPU 内部有多种寄存器,其中包含一组 状态寄存器(如 x86 的 EFLAGS),里面的标志位如:溢出标志 OF、符号标志 SF、零标志 ZF、进位标志 CF、辅助进位 AF、奇偶标志 PF 等。这些标志记录了每条算术/逻辑指令的结果。一旦出现非法操作(例如除零),CPU 会根据这些标志判断指令异常,并立即向操作系统报告。

操作系统作为硬件资源的管理者,会捕获这种由硬件触发的异常,然后向目标进程递送相应的信号(如 SIGFPE),默认行为通常是终止进程。

需要注意,CPU 只有一套寄存器,但每个进程都有自己独立的寄存器内容。这些内容会在进程切换时保存到进程的 PCB 中,下次恢复执行时再加载回来。如果一个进程因除零异常没有立刻被终止,而是暂时挂起,那么调度器在重新切换到它时会恢复原来的寄存器状态,其中包括异常触发时的标志位。这样进程在恢复后仍会不断触发同样的异常,导致频繁的上下文切换,增加系统负担。

因此,系统通常会直接终止因严重硬件异常而无法继续执行的进程,这样能及时释放其上下文并避免异常状态反复出现,提高系统整体稳定性。
一个现象说明为什么更推荐终止进程:

明明没有写死循环,但是仍然不断打印除零错误。

因为SIGFPE 是一个 同步信号 (synchronous signal),类似非法指令 SIGILL、段错误 SIGSEGV。同步信号的特点:handler 返回后会继续从导致错误的指令重新开始执行。

所以:没有"跳过去",没有"继续走下一行",永远卡在出错的那条指令上。

所以在运行到a/=0时触发错误,发送SIGFPE信号,进程捕捉到信号执行hander,返回主程序时又重新执行同一条除零指令 → 再次触发 SIGFPE → 再次进入 handler → 无限循环。
如何区别对应虚拟地址在页表中不存在物理地址映射是真不存在内容还是内容在磁盘上,还未加载到物理内存?

MMU根据页表项的标志位判断两个事情:页面是否在内存(看 P 位 )以及权限是否满足(看 R/W、U/S 位 )。当 MMU 看到 P=0 时,只会触发一个 not-present 类型的 Page Fault;如果 P=1 但权限不符 ,则触发 protection fault

至于 为什么 P=0 ------究竟是该地址从未映射(NULL、野指针)、是合法页面但尚未调入(缺页)、还是被 free 后页表项被清空------MMU 无法判断,这部分逻辑由操作系统完成。当出现 Page Fault 后,操作系统会根据"错误地址"去查对应的进程虚拟内存区域(VMA);如果该地址不在任何 VMA 范围内,说明进程从未拥有这块地址,即"未映射/非法访问"。如果地址属于某个有效 VMA,则再查看对应的页表项:如果页表项存在但 P=0 且带有 swap 信息(如记录页面在交换区的位置),说明页面是合法的,只是还没被加载到内存,即"缺页";如果页表项存在但其物理地址字段已经被清空、标记为未映射,同时 VMA 仍包含该区域,则说明页面曾经分配过但后来被释放,即"已释放"。

通过"VMA 是否覆盖该地址" + "页表项内容是否有 swap 或有效映射"这两个信息,OS 就能准确区分三种情况。因此:MMU 负责发现问题(页不存在或权限错误),而 OS 负责解释问题是为什么发生的。

信号保存

当信号传递给进程时,进程不一定立即响应信号,因为此时进程可能在进行其他操作,比如IO,除非是9号命令。那么进程如何保存信号,从而在处理完其他事情后响应信号呢?首先看几个概念:

信号递达(Delivery):实际执行信号的处理动作;(如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号)

信号未决(Pending):信号从产生到递达之间的状态;

进程可以选择阻塞 (Block )某个信号。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

pending、block、handler

在task_struct结构体中,有这么两个位图和一个函数指针数组:

其中pending用于存放哪些信号处于未决状态。这个位图由32个比特位组成,分别代表32个不同的信号,如果对应的比特位为1,表示该信号已经产生但尚未处理。当一个信号被多次发送时,只在位图记录一次。

block位图用于记录哪些信号被进程阻塞。当信号被阻塞时,对应的比特位会被设置为1

而函数指针数组handler则存放每个信号对应的处理函数(有31个普通信号,信号的编号就是数组的下标,可以采用信号编号,索引信号处理方法),当未被阻塞的信号产生时,内核通过查表调用相应 handler。如果进程没有为某个信号注册自己的 handler,那么在 task_struct 的信号处理函数数组中,对应的指针通常为 NULL 或特殊标记(如 SIG_DFL),表示使用内核定义的默认动作;如果信号被显式忽略,则会存放 SIG_IGN。当未被阻塞的信号产生时,内核会根据数组中存放的值执行默认行为或忽略该信号。

信号集sigset_t

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)。只在linux下有效。大小一般是128字节(虽然 64 bit 足够表示 64 个信号,但 glibc 采用 1024 bit 的 sigset_t 是为了兼容历史、保证未来可扩展、稳定系统 ABI,而不是什么技术限制导致。)

信息集操作函数

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,对其进行位操作,比如用printf直接打印sigset_t变量是没有意义的。set是否是输出型参数依照函数而定。

cpp 复制代码
#include <signal.h>
 int sigemptyset(sigset_t *set);
 int sigfillset(sigset_t *set);
 int sigaddset (sigset_t *set, int signo);
 int sigdelset(sigset_t *set, int signo);
 int sigismember(const sigset_t *set, int signo); 

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化 ,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。signo表示的是signo号信号,在set所指位图的第signo位

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask(操作block位图的函数)

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集),也就是block位图。

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

how:指定对信号屏蔽集(block位图)的操作方式,有以下几种方式:

  • SIG_BLOCK:将set所指向的信号集中包含的信号添加到当前的信号屏蔽集中,即信号屏蔽集和set信号集进行逻辑或操作。
  • SIG_UNBLOCK:将set所指向的信号集中包含的信号从当前的信号屏蔽集中删除,即信号屏蔽集和set信号集的补集进行逻辑与操作。
  • SIG_SETMASK:将set的值设定为新的进程信号屏蔽集,即set直接对信号屏蔽集进行了赋值操作。

set:指向一个sigset_t类型的指针,表示需要修改的信号集合。如果只想读取当前的屏蔽值而不进行修改,可以将其置为NULL。

oldset:指向一个sigset_t类型的指针,用于存储修改前的内核阻塞信号集。如果不关心旧的信号屏蔽集,可以传递NULL。输出型参数。(保存是为了恢复)

如果oset是非空指针,则读取进程的当前信号屏蔽字(block位图)通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字(block位图),参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字(block位图)备份到oset里,然后根据set和how参数更改信号屏蔽字。 假设当前的信号屏蔽字(block位图)为mask,下表说明了how参数的可选值。

sigpending(检查pending信号集,获取当前进程pending位图)

cpp 复制代码
#include <signal.h>  
int sigpending(sigset_t *set);
  • 参数set 是一个指向 sigset_t 类型的指针,用于存储当前进程的未决信号集合。输出型参数
  • 返回值 :函数调用成功时返回 0,失败时返回 -1,并设置 errno 以指示错误原因。

小栗子

基于上面的操作方法我们来做一个实验:我们把2号信号block对应的位图置为1,那么2号信号就会被屏蔽掉了,此时我们给当前进程发送2号信号,但2号信号已经被屏蔽了,2号信号永远不会递达,发完之后我们再不断的获取当前进程的pending表,我们就能肉眼看见2号信号被pending的效果:

cpp 复制代码
#include<signal.h>
#include<iostream>
#include<unistd.h>
using namespace std;

void handler(int signo)
{
    cout<<"已经取消对2号信号的屏蔽"<<endl;
}

void PrintPending(sigset_t& set)
{
    for(int signo=1;signo<=31;signo++){
        if(sigismember(&set,signo))
            cout<<"1";
        else cout<<"0";
    }
    cout<<endl;
}

int main()
{
    sigset_t set;
    sigset_t oset;
    //使用前先清空
    sigemptyset(&set);
    sigemptyset(&oset);

    //屏蔽2号信号
    sigaddset(&set,2);
    sigprocmask(SIG_SETMASK,&set,&oset);

    //
    signal(2,handler);

    int cnt=10;
    while(1)
    {
        //取pending
        sigpending(&set);
        PrintPending(set);
        sleep(1);

        //当cnt为0时,取消对2号信号的屏蔽
        if(cnt--==0) 
            sigprocmask(SIG_SETMASK,&oset,nullptr);
    }

}

可以看到初始时pending都是0,当按下ctrl+c后发送2号信号,这时pending中第二个位置出现1,但是由于被阻塞了,所以迟迟不能递达该信号,所以一直在pending中,直到10秒后,取消对2号信号的的阻塞,这时执行2号信号的handler函数,并将pending对应位置置0。当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。

总结

sigset_t就是操作系统提供给用户访问内核中pending和block位图的自定义数据,再通过操作函数对sigset_t进行处理,然后通过系统接口sigpromask和sigpending以及signal函数访问或者修改内核中的block、pending,handler表。

信号处理

用户态和内核态

什么是用户态,什么是内核态?首先要知道进程地址空间中内核区与用户区的区别:

用户程序在代码中调用系统调用时,会执行一个特殊的中断指令,如int 0x80(在x86架构中)或syscall指令。在执行中断指令前,将系统调用号放入特定的寄存器中(如eax寄存器)。CPU暂停当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序(如Linux中的system_call),并调用它。中断处理程序会检查系统调用号的有效性,并从系统调用表中找到相应的系统调用函数进行调用。

汇编中,int 80是陷入内核指令,就是**从用户态转为内核态,此时CPU中的ecs寄存器的低两位从11变成00,当从内核态转为用户态时从00变成11。**要访问内核区的数据必须调用系统调用,转为内核态。
在接收到信号后可能不会被立即处理,而是在合适的时候处理,那么合适的时候是什么时候呢?从进程的内核态返回到用户态的时候,进行处理。

简单来说,执行自己的代码,访问自己的数据,这就叫做用户态。

当进程接收到信号时,内核不会立即在用户态执行处理函数,而是等到进程从内核态返回用户态的时机再进行处理。这是因为信号相关的数据结构(如 pending、blocked 位图和 handler 数组)都存放在内核管理的 PCB 中,只有在内核态才能安全访问和检查这些信息。因此,当系统调用或中断处理完成,内核准备恢复用户态执行时,会先检查是否有待处理的信号,并根据信号的注册方式(默认动作、忽略或自定义 handler)决定下一步操作,然后再安全地安排用户态执行相应的处理。举一个例子:

cpp 复制代码
 用户态 (main 函数)
+---------------------+
|       main()        |
|   正常执行程序      |
+---------------------+
           |
           | 系统调用/中断等等
           v
     内核态 (Kernel)
+---------------------+
| 检查信号表         |
| 找到 SIGQUIT handler|
+---------------------+
           |
           | 返回用户态执行 handler
           v
 用户态 (sighandler)
+---------------------+
|   sighandler()      |
| 使用独立堆栈执行   |
+---------------------+
           |
           | handler 执行完毕
           | 自动调用 sigreturn
           v
     内核态 (Kernel)
+---------------------+
| 恢复 main() 上下文  |
+---------------------+
           |
           v
 用户态 (main 函数)
+---------------------+
|   main() 继续执行   |
+---------------------+

handler 和 main 是独立控制流,所以要恢复上下文确保 handler 返回后程序从中断点继续执行

  • 使用不同堆栈

  • 不是普通函数调用关系

sigaction函数

sigaction函数与signal函数的作用类似,但是多了一些参数和用法。

  • signum:指定要设置或获取处理程序的信号编号。可以指定SIGKILL和SIGSTOP以外的所有信号。
  • act:指向sigaction结构体的指针,用于指定新的信号处理方式。如果此参数非空,则根据此参数修改信号的处理动作。
  • oldact:如果非空,则通过此参数传出该信号原来的处理动作。(如果你想恢复以前的方式,此参数就是保存之前的操作方式)与上面的oset类似,保存原来的处理动作。

sigaction结构体

cpp 复制代码
struct sigaction {  
    void (*sa_handler)(int);  // 指向信号处理函数的指针,接收信号编号作为参数  
    void (*sa_sigaction)(int, siginfo_t *, void *);  // 另一个信号处理函数指针,支持更丰富的信号信息  
    sigset_t sa_mask;  // 设置在处理该信号时暂时屏蔽的信号集  
    int sa_flags;  // 指定信号处理的其他相关操作  
    void (*sa_restorer)(void);  // 已废弃,不用关心  
};

目前1到31号信号只关心1,3参数。例子:与signal函数十分类似,不过为自定义函数嵌套了一层结构体。

cpp 复制代码
#include <signal.h>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<string.h>
using namespace std;

void handler(int signo)
{
    cout<<"recived signo:"<<signo<<endl;
}

int main()
{
    struct sigaction act;
    struct sigaction oact;
    memset(&act,0,sizeof(struct sigaction));

    act.sa_handler=handler;
    sigaction(2,&act,&oact);
    while(true)
    {
        std::cout<<"I am a process,pid: "<<getpid()<<std::endl;
        sleep(1);
    }

}

如果你还想在处理2号信号时,对其它型号也进行屏蔽,你可以设置sa_mask变量,比如对1,3,4信号进行屏蔽,注意**sa_mask 只是"执行信号处理函数期间的临时阻塞列表",不是永久屏蔽信号的机制,****"在执行 handler 的时候自动阻塞这些信号":**

cpp 复制代码
#include <signal.h>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<string.h>
using namespace std;

void Print(sigset_t &pending)
{
    for(int sig = 31; sig > 0; sig--)
    {
        if(sigismember(&pending, sig))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}
void handler(int signum)
{

    std::cout<<"get a sig"<<signum<<std::endl;
    while(true)
    {
        sigset_t pending;
        sigpending(&pending);
 
        Print(pending);
        sleep(1);
    }

}
int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,1);
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);

 
    sigaction(2,&act,&oact);
 
    while(true)
    {
        std::cout<<"I am a process,pid: "<<getpid()<<std::endl;
        sleep(1);
    }
 
    return 0;
}

解释现象,在进程正常运行时打印i am a porcess,然后按ctrl+c向进程发信号2,递达信号执行hanlder函数,**这时由于2号信号已经被递达,所以pending中已经没有2号信号了!(事实也的确如此,pending表中就是当某个信号刚要递达执行自定义函数时就已经置0了),**当我们再按ctrl+c时,发送2号信号,但是由于正在执行hanlder函数处理2号信号,所以他被阻塞了,所以可以看到pending中2位置被置成了1,其他的也是,由于已经在执行handler过程中对1,3,4号信号阻塞,所以也会在pending中看到相应的位置置1

函数重入现象

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为**时钟中断使进程切换到内核,再次回用户态之前检查到有信号待处理,**于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。node2节点会出现内存泄漏,不能被释放。

像上例这样**,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,**insert函数访问一个全局链表,有可能因为重入而造成错乱,这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

如果一个函数符合以下条件之一则是不可重入的:(大部分函数是不可被重入的,可重入或者不可重入,描述的是函数的特征)

  • 调用了malloc或new,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

volatile

cpp 复制代码
int gflag = 0;
 
void changedata(int signo)
{
    std::cout << "get a signo:" << signo << ", change gflag 0->1" << std::endl;
    gflag = 1;
}
 
int main() // 没有任何代码对gflag进行修改!!!
{
    signal(2, changedata);
 
    while(!gflag); // while不要其他代码
    std::cout << "process quit normal" << std::endl;
}

我们使用信号捕捉了2号信号,当我们执行了2号信号后,全局变量gflag就会被更改为1,那么main函数中的while就会停止执行,因为cpu在执行while循环的时候,实时的从内存中取gflag来进行比较。如果在这里我们对编译进行优化,**这会让cpu保存之前在内存中取的gflag的值到CPU的寄存器中,只在内存中取最开始的一次值,**这就会导致gflag的变化无法被在while中实时更新,导致while循环无法结束:g++ -o test test.cc -O1(O1,是基础优化):

如果想让gflag在此优化下生效,就要使用volatile(volatile关键字可以确保变量的可见性(即确保变量每次访问时都直接从内存中读取)关键字:volatile int gflag=0;

SIGCHLD信号

子进程退出时,会给父进程发送17号信号--SIGCHLD信号。继wait函数后,我们可以使用信号来对子进程进行回收处理。

cpp 复制代码
#include <signal.h>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

void handler(int signo)
{
    while (1)
    {
        int ret = waitpid(-1, nullptr, WNOHANG);
        if (ret > 0)
            cout << "i am process:" << getpid() << ", receive signo:" << signo<<endl;
        else break;
    }
}

int main()
{
    signal(17, handler);
    for (int i = 0; i <= 5; i++)
    {
        int id = fork();
        if (id == 0) // child
        {
            cout << "i am child,pid:" << getpid() << endl;
            sleep(2);
            exit(0);
        }
    }
    // father
    while (1)
    {
        cout << "i am father,pid:" << getpid() << endl;
        sleep(1);
    }
}

这段代码创建了多个子进程,并在子进程结束时通过SIGCHLD信号进行处理。当SIGCHLD信号被捕获时,自定义函数会被调用。这个函数会进入一个无限循环,尝试使用waitpid以非阻塞方式(WNOHANG)等待任何已终止的子进程。这是合理的,因为它允许父进程在子进程终止时及时回收资源,同时不阻塞父进程的其他操作。

等待子进程的两个作用:1.接收子进程退出信息,查看退出内容;2.回收子进程资源

signal(SIGCHLD, SIG_IGN) 会让所有子进程在退出时自动被内核回收 。通常,当子进程结束时,父进程需要处理这个信号以回收子进程的资源,但在这里,通过将其设置为SIG_IGN,这意味着子进程的资源将由内核自动回收。(如果你不关心子进程的退出信息就可以使用这种方法,否则还是要进行等待):

cpp 复制代码
int main()
{
    signal(17, SIG_IGN);
    for (int i = 0; i <= 5; i++)
    {
        int id = fork();
        if (id == 0) // child
        {
            cout << "i am child,pid:" << getpid() << endl;
            sleep(2);
            exit(0);
        }
    }
    // father
    while (1)
    {
        cout << "i am father,pid:" << getpid() << endl;
        sleep(1);
    }
}

发现并没有出现Z状态的子进程,说明结束后就立马被内核回收!

但是!注意与 SIG_DFL 的差别,默认行为(SIG_DFL)下,SIGCHLD 是忽略的,但默认忽略不会自动回收僵尸。因为默认忽略只是表示父进程不关心这个信号,但 POSIX 规定"默认忽略并不意味着自动 wait",显式写 SIG_IGN 才会触发 Linux 的额外行为:自动回收,这是 Linux 的特例:

相关推荐
专家大圣1 小时前
远程调式不复杂!内网服务器调试用对工具很简单
运维·服务器·网络·内网穿透·cpolar
集大周杰伦1 小时前
RV1126开发板烧录与SSH登录实践
linux·ssh·嵌入式·rv1126·瑞芯微开发工具·ssh 远程登录
gs801401 小时前
Ascend 服务器是什么?(Ascend Server / 昇腾服务器)
运维·服务器·人工智能
Xの哲學1 小时前
Linux RTC深度剖析:从硬件原理到驱动实践
linux·服务器·算法·架构·边缘计算
了一梨2 小时前
使用Docker配置适配泰山派的交叉编译环境
linux·docker
飞飞传输2 小时前
选对国产FTP服务器,筑牢数据传输安全防线,合规高效双达标
大数据·运维·安全
卷到起飞的数分2 小时前
22.Maven高级——继承与聚合
服务器·spring boot
西格电力科技2 小时前
光伏策略控制服务器的核心价值与应用必要性
运维·服务器
拾忆,想起2 小时前
Dubbo配置方式大全:七种配置任你选,轻松玩转微服务!
服务器·网络·网络协议·微服务·云原生·架构·dubbo