Linux:进程信号

📅 2026/7/4 4:36:22
Linux:进程信号
1.信号的概念信号没有产生的时候其实我们已经能够知道怎么处理这个信号了。使用kill -l 可以查看所有信号信息信号的到来我们并不清楚具体什么时候信号到来相对于我正在做的工作是异步产生的。同步做完一件再做下一件调用方等待结果返回全程阻塞。异步发起请求就立刻返回调用方不等待结果后续通过通知 / 回调处理全程不阻塞。信号产生了我们不一定要立即处理它而是我们在合适的时候处理。我们有一种能力将已经到来的信号进行暂时保存在PCB中。进程在运行的时候前台命令行操作的时候只能有一个后台./XXX jobs 查看后台任务。使用fg 后台任务的编号 命令将内存提到前台shell 会自动把自己放到后台。区分是前台进程还是后台进程主要是看有没有从键盘接收数据的能力。一般ctrl c 终止的是前台进程shell 是做了一些操作保证自己不会被终止。OS会自动的把shell提到前台或者后台。前台进程不能不能被暂停ctrl z) 如果被暂停由 shell 接管该前台进程会立即被放到后台状态为stopped。我们可以使用bg 任务编号重启。信号本质就是用软件模拟中断的行为的处理进程之间的管理而中断是处理外设与操作系统的交互IO关系。crtl c 等价 kill -2sighandler_t 是一个函数指针signal 系统调用就是将默认的 signum 拦截并自定义信号行为修改完成后一直有效#include iostream #include unistd.h #include signal.h using namespace std; void handler(int signo) { std::cout 获得了2号信号endl; exit(1); } int main() { signal(2,handler);//将2号信号改为打印一语句话再终止程序 while (true) { cout running... endl; sleep(1); } return 0; }2.信号的产生2.1 键盘输入上面已经讲过可以通过键盘输入 ctrl c 向前台进程发送2号信号既能使用名字也能使用编号说明是信号是使用宏定义的。没有0号信号因为进程退出时会有退出状态和终止信号两个信息如果表示没有收到信号默认是0。还没有32号和33号信号。1到31是普通信号34到64是实时信号。实时信号一般需要及时处理不会出现丢失的情况我们这里重点讲一下普通信号。13SIGPIPE 读端关闭写端一直写。写端就会收到这个信号然后关闭写端。每一个进程都有一张自己的函数指针数组数组的下标就和信号的编号强相关内容指向处理该信号的方式。对于普通信号来讲进程收到信号后进程要表示自己是否收到了某种信号。是否-位图比特位的位置决定信号编号比特位的内容决定是否收到信号 收到信号相应位置变为1。OS向目标发信号实质是写信号直接根据进程 pid 找到该进程然后修改位图。无论信号有多少种产生方式永远只能让 OS 向目标进程发信号因为 OS 是进程的管理者。所以之前ctrl c 被操作系统识别到了然后被 OS 解释为2号信号向目标进程的PCB 信号位图由0置1合适的时候根据函数指针数组完成信号的处理。每个进程对于信号函数指针数组对应不同信号的处理方式信号位图信号的保存signal就是将输入的下标在函数指针数组中对应的函数指针内容改变指向自定义的处理方法。void handler(int signo) { std::cout 获得了 signo 号信号endl; exit(1); } int main() { //signal(2,handler);//将2号信号改为打印一语句话再终止程序 signal(19,handler); signal(20,handler); signal(3,handler); while (true) { cout running...pid: getpid() endl; sleep(1); } return 0; }ctrl \ - Quit 对应3SIGQUIT 信号可以键盘输入的信号ctrl z进程暂停ctrl \进程退出ctrl c终止进程我们使用kill -2 -3 -20都可以使用我们自定义的方法但是19号信号不行因为Linux 系统设计中有两个特殊信号是绝对的、不可违抗的SIGKILL强制终止进程无法捕获。SIGSTOP (19)暂停停止进程同样无法捕获。使用signal 无法为这两个信号注册自定义处理函数。当你试图这样做时系统会忽略你的注册请求信号的默认行为仍然会执行。我们可以试试把9号命令修改为什么都不做但结果9号命令仍然可以终止进程。void handler(int signo) { std::cout 获得了 signo 号信号endl; } int main() { signal(9,handler); while (true) { cout running...pid: getpid() endl; sleep(1); } return 0; }man 7 signal列出了系统支持的所有信号Signal并详细说明了每个信号的编号、默认行为和含义2.2 系统调用kill向目标进程发送一个信号static void Usage(const std::stringproc) { std::cout\nUsage:proc -signumber process\nstd::endl; } void handler(int signo) { std::cout 获得了 signo 号信号endl; exit(1); } int main(int argc,char* argv[]) { if(argc ! 3) { Usage(argv[0]); exit(0); } int signumber std::stoi(argv[1]1);//说如果加- 就字符1第2个字符 int processpid std::stoi(argv[2]); kill(processpid,signumber); }./mykill -9 989343raise给自己发送一个信号void handler(int signo) { std::cout 获得了 signo 号信号endl; } int main() { signal(2,handler); while(1) { raise(2); sleep(1); } return 0; }abort自己引起进程异常终止void handler(int signo) { std::cout 获得了 signo 号信号endl; } int main() { signal(SIGABRT,handler); abort(); return 0; }2.3 异常发生除0错误8号信号时CPU内部寄存器溢出标记位会置1在硬件上是能表示出来的然后通知OS系统向目标进程发送终止信号CPU执行其他任务由硬件问题转换为信号问题。void handler(int signo) { std::cout 获得了 signo 号信号endl; } //异常 int main() { signal(8,handler); int a 10; a / 0; return 0; }这样之后只会警报不会终止进程但会一直打印收到8号信号。CPU内的寄存器 ! 寄存器里的内容内容属于当前进程出现异常时把进程杀掉默认就是处理问题的方式之一杀掉进程溢出标志位恢复。如果进程没有被杀掉溢出标志位不会恢复每次调度都因为CPU发现溢出标志位为1CPU都会让OS将异常转换为信号发送给相应进程可以是处理进程的方式就是打印一句话依旧不被杀死所以会不断打印。CPU识别到我们的进程有异常不再继续执行后续代码。通知操作系统处理cpu调度其他进程操作系统打印一句话没有终止下面方式写就不会循环打印。void handler(int signo) { std::cout 获得了 signo 号信号endl; exit(1); }野指针零号地址在页表中并没有真正的映射在页表中转换失败会有mmu内容管理单元这个硬件的标志位表示这次失败然后通知CPUCPU通知OS。本质是虚拟到物理出现的硬件问题由操作系统识别然后向进程写信号void handler(int signo) { std::cout 获得了 signo 号信号endl; sleep(1); } //异常 int main() { signal(11,handler); int *p nullptr; *p 1; return 0; }也是不断打印11SIGSEGV信号。所有代码出现异常进程不是一定要退出的要看用户想怎么处理这个信号。在vs中发生异常会直接被终止是因为这个异常被 windows 操作系统识别出来了然后强制终止。另外产生信号的方式可以有很多但是发送信号只能是操作系统。2.4 软件条件管道通信中当读端关闭后操作系统识别到写端还在一直写会发送13SIGPIPE直接关闭写端。这不属于硬件因为这些空间进程可以访问。软件也可以产生信号操作系统是软硬件资源的管理者出现问题都需要处理大部分信号都是异常产生还有让进程暂停继续和其他事情的信号非异常情况也会产生信号。闹钟使用 alarm 系统调用可以在系统中设置闹钟。对应14SIGALRM信号。seconds 想设置几秒后的闹钟返回值一般为0如果闹钟提前返回再定义一个闹钟时 alarm 的返回值返回值是曾经设置闹钟值的剩余时间。如果seconds 为 0则取消所有已设置的闹钟并且不会产生 SIGALRM 信号返回已设置闹钟的剩余时间。int cnt 0; void handler(int signo) { std::cout get a signo: signo alarm: cnt std::endl; exit(0); } int main() { signal(14,handler); alarm(1); while(true) { cnt; } return 0; }int count 0; int main() { alarm(1); while(true) { std::cout alarm: count std::endl; } return 0; }下面那种方式是因为 std::endl 强制刷新内存缓冲区导致缓冲区失效每次都调用系统调用 write。第一段代码快是因为它完全不涉及任何类型的缓冲区seconds 为 0则取消所有已设置的闹钟并且不会产生 SIGALRM 信号返回已设置闹钟的剩余时间int cnt 0; int result 0; void handler(int signo) { result alarm(0);//历史闹钟取消掉会返回历史闹钟还剩多少时间 cout result: result endl; exit(0); } int main() { signal(14,handler); cout pid: getpid() endl; alarm(30); return 0; }每个两秒重新定一个闹钟打印两次runingint cnt 0; int result 0; void handler(int signo) { result alarm(2);//历史闹钟取消掉设置新的闹钟时长 cout result: result endl; //std::cout get a signo: signo alarm: cnt std::endl; //exit(0); } //异常 int main() { signal(14,handler); cout pid: getpid() endl; alarm(2); while(true) { coutruningendl; sleep(1); } }这里没有exit(0)。信号处理函数本质上就是一个普通的 C 函数它的执行流是这样的内核收到闹钟信号暂停 main 中正在执行的代码比如正在 sleep(1)。CPU 跳转去执行 handler 函数。handler 函数执行完最后一条语句遇到结尾的 }函数正常返回。CPU 跳转回 main 函数恢复执行刚才被打断的那行代码。这里第二次打印runing后sleep(1)其实是没有执行完被中断的因为cout result 是需要时间的时间不够。操作系统中的时间问题所有用户的行为都是以进程的形式在OS中表现的操作系统只要把进程调度好就能完成所有的用户任务CMOS可以周期性的高频率的向CPU发送时钟中断。朴素地对操作系统进行理解操作系统的本质是处理好前置工作各种中断的陷阱的初始化工作然后死循环暂停。COMS通过向CPU发送时钟中断号CPU调用中断向量表中相应的操作系统调度方法此时我们的进程就在硬件驱动的情况下开始调度执行轮转操作系统在COMS的驱动下开始运行。不同外设可以绑定不同的中断向量号对应不同的方法。 进程启动起来硬件就会推动操作系统执行下去操作系统的执行是基于硬件中断的。进程运行期间COMS一直在工作检查当前进程的时间片是否到了。核心转储把错误的原因概括给我们通过man 7 signal可以发现 Action 中有 core指更严重的报错终止终止的原因需要用户进一步排查Term 指从键盘输入导致的终止man 7 signal: Core Default action is to terminate the process and dump core (see core(5)).进程终止触发核心转储生成进程 core.pid 文件保存进程运行时的数据上下文。使用ulimit 查看系统基本配置项core file size 默认被设置为0不保存数据但我们可以打开。ulimit -a 查看所有 ulimit -c 改 core file size 。这个设置只对当前终端会话有效。如果想永久生效需要把这条命令加到用户启动时的配置文件中。为什么云服务系统默认关闭core dump呢我们多执行几次除0错误可以发现这个文件的大小是非常大的。如果我们不小心写出bug且没有及时处理磁盘会被写满所以线上云服务器一般禁止core dump。为什么还要有 core dump 可以支撑我们的程序员执行后续的调试。使用gdb test_signal然后(gdb) core-file core.18881就可以看到错误的原因。3.信号的保存信号不被立即处理时被纪录在PCB中实际执行信号的处理动作称为信号递达处理(Delivery)信号从产生到递达之间的状态称为信号未决(Pending)。进程可以选择阻塞 (Block )某个信号。被阻塞的信号产生时将保持在未决状态直到进程解除对此信号的阻塞才执行递达的动作.注意阻塞和忽略是不同的只要信号被阻塞就不会递达而忽略是在递达之后可选的一种处理动作。忽略就是一种信号的处理信号的递达。信号的递达分为三种信号的忽略信号的默认信号的自定义捕捉信号的自定义我们前面已经讲过了。信号的默认signal(2,SIG_DFL)恢复默认的2号信号处理方式。我们还发现 sleep(10) 是可中断的系统调用使用下面这种方式可以实现休息10秒。void handler(int signo) { std::cout handler: signo std::endl; //exit(100); } int main() { cout getpid: getpid() endl; signal(2,handler); sleep(10);//可中断的系统调用 //执行你注册的 handler。 //handler 返回后sleep 被中断不再继续睡眠而是立即返回。 //返回值为剩余未睡的秒数如果没有剩余返回 0。 int remain 10; while ((remain sleep(remain)) 0) { // 如果被信号打断继续睡剩余时间 } signal(2,SIG_DFL);//恢复默认的2号信号处理方式 cout getpid: getpid() endl; cout getpid: getpid() endl; cout getpid: getpid() endl; while(true) { sleep(1); } return 0; }信号的忽略signal(2,SIG_IGN)更改代码后结果如下忽略代表已经对这个信号进行了处理。SIG_DFL 和 SIG_IGN 是什么不是真的让你访问地址而是可以通过强转为 int 去判断是默认还是忽略而用户自定义的处理方法地址是很大的。信号未决Pending信号从产生到递达之间的状态称为信号未决就是指在进程信号位图中的时候信号等待处理。阻塞Block信号是可以被阻塞的就是信号可以暂时不进行递达一直处于未决状态直到解除阻塞。阻塞的信号一定是未决的。未决的信号不一定是阻塞的。没有收到信号前可以设置对信号的处理也可以设置阻塞和解除阻塞该信号。信号在内核中的表示示意图从上到下编号表示不同信号block位图记录不同信号是否阻塞pending位图记录是否收到该信号是否处于未决状态handler是一个函数指针数组指向不同信号的处理方式有SIG_DFL默认方式SIG_IGN忽略还有自定义的处理方式。三张表要横着去看这里block可以提前设置为1比如3号信号这里pending是0表示收到该信号后阻塞所以出现了上面的情况2号3号信号都不会执行。当block为0pending为1该信号会在合适的时间被处理。每个信号都有两个标志位分别表示阻塞(block)和未决(pending)还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过当它递达时执行默认处理动作。SIGINT信号产生过但正在被阻塞所以暂时不能递达。虽然它的处理动作是忽略但在没有解除阻塞之前不能忽略这个信号因为进程仍有机会改变处理动作之后再解除阻塞。SIGQUIT信号未产生过一旦产生SIGQUIT信号将被阻塞它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次将如何处理POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的常规信号在递达之前产生多次只计一次而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号sigset_t是操作系统提供的一种类型从上图来看。每个信号只有一个bit的未决标志非0即1不记录该信号产生了多少次阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储sigset_t 称为信号集这个类型可以表示每个信号的“有效”或“无效”状态在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)这里的“屏蔽”应该理解为阻塞而不是忽略。位图 sigset_t本质是位图结构信号集操作函数#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 sigismemberconst sigset_t *set, int signo);函数sigemptyset 初始化set 所指向的信号集,使其中所有信号的对应bit 清零,表示该信号集不包含任何有效信号。函数sigfillset初始化 set 所指向的信号集,使其中所有信号的对应bit 置1,表示该信号集的有效信号包括系统支持的所有信号。注意,在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号这四个函数都是成功返回0,出错返回- 1。sigismember是一个布尔函数用于判断一个信号集的有效信号中是否包含某种信号若包含则返回1不包含则返回0出错返回-1。sigprocmask调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。#include signal.h int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值:若成功则为0,若出错则为-1如果 oset 是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里o-old ,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号相当与mashmask|setSIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号相当于maskmask~setSIG_SETMASK设置当前信号屏蔽字为set所有值相当于masksetvoid handler(int signo) { std::cout handler: signo std::endl; //exit(100); } int main() { std::cout getpid: getpid() std::endl; signal(2,handler); sigset_t block,oblock;//属于用户空间局部变量并没有给操作系统 sigemptyset(block); sigemptyset(oblock); sigaddset(block,2);//在这里将2号信号添加到信号集里还没有设置屏蔽 //老屏蔽字保存在oblock sigprocmask(SIG_BLOCK,block,oblock);//系统调用将用户层设置好的block表设置进当前调用的进程的PCBblock表中成功修改内核 while(1) { sleep(1); } return 0; }我们试试将所有信号都屏蔽for(int signo 1;signo 31; signo) sigaddset(block,signo);//所有信号都屏蔽 //老屏蔽字保存在oblock sigprocmask(SIG_SETMASK,block,oblock);//直接替换结果发现9号19号仍然无法被屏蔽。sigpendingpending位图修改已经学过一部分信号的产生中都是在修改pending表。sigpending是用来检查pending表的参数是输出型参数读取当前进程的未决信号集,通过set参数传出。调用成功则返回0出错则返回-1。下面用刚学的几个函数做个实验自定义2号信号先屏蔽2号信号打印pending表然后发送2好信号打印pending表然后解除屏蔽void handler(int signo) { std::cout handler: signo std::endl; } void PrintPending(const sigset_t pending) { for(int signo 31; signo 1; signo--) { if(sigismember(pending,signo))//判断 { cout 1; } else { cout 0; } } cout endl; } int main() { signal(2,handler); cout pid: getpid() endl; //1.屏蔽2号信号q sigset_t block,oblock; sigemptyset(block);//初始化 sigemptyset(oblock); sigaddset(block,2);//添加 sigprocmask(SIG_BLOCK,block,oblock);//屏蔽 //2.不断读取pending表 sigset_t pending; int cnt 0; while(true) { sigpending(pending); PrintPending(pending); sleep(1); cnt; if(cnt 5) { std::cout 解除对2号信号的o屏蔽2号信号准备递达std::endl; sigprocmask(SIG_SETMASK,oblock,nullptr);//使用之前的block表 } } return 0; }可以发现未决信号会一直保存直到不被屏蔽。那处理未决信号时什么时候修改pending表呢我们可以执行下面代码测试一下可以发现在处理该信号之前pending表已经被修改而不是中间和处理完。void handler(int signo) { std::cout############################endl; sigset_t pending; sigpending(pending); PrintPending(pending); cout#############################endl; std::cout handler: signo std::endl; } int main() { signal(2,handler); while(1); return 0; }4.信号的处理递达4.1 处理时间信号在合适的时候被处理---什么时候进程从内核态返回到用户态的时候进行信号的检测和信号的处理用户态是一种受控的状态能访问的资源是有限的内核态是一种操作系统的工作状态能访问大部分系统资源 系统调用背后就包含了身份的变化。每个进程都有自己的进程地址空间前0到3G是用户空间3到4G是内核空间。用户空间是属于用户态的进程可以直接访问通过用户级页表映射每个进程都有且私有一份。内核空间属于操作系统通过内核级页表映射。进程启动时要加载数据和代码操作系统内核空间比进程更早被加载到地址空间。每个进程都有3GB的地址空间内核级页表只需要共用一张就够了大家对操作系统的代码共享cpu进行进程调度时可以通过进程的地址空间直接找到操作系统。CPU内部有寄存器CS来区分是内核还是用户两个bit1指内核3指用户态。还有CR系类寄存器CR2指向当前调用进程的用户级页表这一套机制是操作系统帮我们维持的操作系统才认识虚拟地址CPU寄存器CR2中是物理地址可以直接访问。CR1是保存访问内存时页表发生缺页中断的虚拟地址。从用户态转换到内核态不仅需要从0到3GB跳到3GB到4GB还需要CPU寄存器CS的改变。所以对于系统调用除了帮我们完成功能还完成了不同状态的跳转。从内核态返回到用户态我们会进行信号的处理说明我们之前已经进入内核态 。当进程调用系统调用时会进入内核态完成相应的工作后我们会转到当前进程对应的信号属于内核空间中来对信号进行处理当发现 block 信号为0pending 信号为1时会调用处理方法。忽略直接pending 由1改为0自定义的需要用户自定义捕捉我们先将pending调为 0 然后返回用户态身份调用sighandler因为sighandler 是用户自定义的可以在其中做了非法的操作如果使用内核身份的话就绕过了权限限制。然后我们通过特殊的 sigreturn 再次进入内核。信号捕捉中一共会涉及4次状态切换交点是信号检测。访问操作系统只能由系统调用它在内核中存在数组中因为数组下标就那么多所有只能用系统调用。我们写的代码一定会调用系统调用吗当然不是所以我们的代码不是一定要通过系统调用进入内核态比如 while(1) 死循环时 ctrlc 仍然能使进程停止。因为你的进程在执行中一定会由很多的进程间切换时间片注定了一定会从用户态代码返回到内核态从cpu上剥离。4.2 信号的捕捉c语言中允许结构体与函数名相同结构体中我们只关心第1个和第3个第一个就是handler处理方法与signal 不同的是在处理期间可以自动屏蔽正在处理的信号防止信号的嵌套sa_mask选项来添加处理期间额外屏蔽的其他信号。sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo 是指定信号的编号。若 act 指针非空,则根据 act 修改该信号的处理动作。若 oact 指针非 空,则通过oact 传出该信号原来的处理动作。act 和 oact 指向 sigaction结构体:将 sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号赋值为常数 SIG_DFL 表示执行系统默认动作赋值为一个函数指针表示用自定义函数捕捉信号或者说向内核注册了一个信号处理函数该函数返回值为 void,可以带一个 int 参数通过参数可以得知当前信号的编号这样就可以用同一个函数处理多种信号。显然这也是一个回调函数不是被 main 函数调用而是被系统所调用当某个信号的处理函数被调用时内核自动将当前信号加入进程的信号屏蔽字当信号处理函数返回时自动恢复原来的信号屏蔽字这样就保证了在处理某个信号时如果这种信号再次产生那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时除了当前信号被自动屏蔽之外还希望自动屏蔽另外一些信号则用sa_mask字段说明这些需要额外屏蔽的信号当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,本章的代码都把sa_flags设为0sa_sigaction是实时信号的处理函数本章不详细解释这两个字段有兴趣的同学可以在了解一下。处理时自动暂时屏蔽正在处理的一些信号pending为1还没处理sa_mask添加额外屏蔽的信号void Print(const sigset_t pending) { for(int signo 31; signo 1; signo--) { if(sigismember(pending,signo)) cout 1; else cout 0; } cout endl; } void handler(int signo) { cout get a sig: signo endl; while (1) { sigset_t pending; sigpending(pending); Print(pending); sleep(1); } } int main() { cout pid: getpid() endl; struct sigaction act,oact; act.sa_handler handler; sigaction(2,act,oact);//自定义2号信号 sigset_t block,oblock; sigemptyset(block); sigemptyset(oblock); sigaddset(block,3);//将3号信号加入信号集 sigprocmask(SIG_BLOCK,block,oblock);//将3号信号加入屏蔽 while(true) sleep(1); return 0; }同时存在多个信号时处理完一个信号操作系统会判断有没有其他信号如果有不一定按信号顺序处理因为有优先级把所有信号全部处理后再返回用户态5.可重入函数一个函数被 main 和 sighandler 两个执行流同时进入了这种状态被成为该函数被重入了重复进入。因重入可能出现问题那么这个函数称为不可重入函数。没有好坏之分只是描述函数的特点可重入函数在能在多执行流下运行不可重入函数不在多执行流函数下运行就行。main函数调用 insert 函数向一个链表 head 中插入节点 node1插入操作分为两步刚做完第一步的时候因为硬件中断使进程切换到内核再次回用户态之前检查到有信号待处理于是切换到sighandler函数sighandler 也调用 insert 函数向同一个链表 head中插入节点 node2插入操作的两步都做完之后从 sighandler 返回内核态再次回到用户态就从 main 函数调用的insert函数中继续往下执行先前做第一步之后被打断现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。像上例这样insert函数被不同的控制流程调用有可能在第一次调用还没返回时就再次进入该函数这称为重入insert 函数访问一个全局链表有可能因为重入而造成错乱像这样的函数称为不可重入函数反之如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下为什么两个不同的控制流程调用同一个函数访问它的同一个局部变量或参数就不会造成错乱?因为代码使用同一份数据使用了写时拷贝。如的果一个函数符合以下条件之一则是不可重入:调用了malloc或free因为malloc也是用全局链表来管理堆的。调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。6.volatilevolatile 英语可变的告知编译器该变量的值可能随时被程序控制流之外的未知因素改变禁止编译器对其进行任何优化每次访问都必须直接从内存地址中读取或写入保持内存的可见性。gcc/g 默认没有优化 -O0我们可以加 -O1 选项提高优化级别int flag 0; void handler(int signo) { std::cout signo: signo std::endl; flag 1; std::cout change flag to: flag std::endl; } int main() { signal(2,handler); while(!flag); std::cout quit normal! std::endl; return 0; }优化前优化后可以发现优化后程序并不会停止是因为执行 while 循环时CPU要读取数据然后检测判断优化前每次判断都重新读取一遍内存中的 flag。优化后汇编会变化只会有第一次读入以后都检测这个保存 flag 的寄存器因为main函数中没有人修改和查看 flag所有直接优化到了寄存器不保存在内存。形成了一层内存屏障CPU只访问寄存器不访问内存。此时我们就可以通过volatile修饰保证当前 flag 这个变量内存当中的可见性。所有使用flag的操作都要从内存拿数据volatile int flag 0;7.SIGCHLD选学了解进程一章讲过用wait和waitpid函数清理僵尸进程父进程可以阻塞等待子进程结束也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式父进程阻塞了就不能处理自己的工作了采用第二种方式父进程在处理自己的工作的同时还要记得时不时地轮询一 下程序实现复杂。其实子进程在终止时会给父进程发SIGCHLD信号该信号的默认处理动作是忽略父进程会收到父进程可以自定义SIGCHLD信号的处理函数这样父进程只需专心处理自己的工作不必关心子进程了子进程终止时会通知父进程父进程在信号处理函数中调用 wait 清理子进程即可。事实上由于UNIX 的历史原因要想不产生僵尸进程还有另外一种办法父进程调用sigaction 将 SIGCHLD 的处理动作置为 SIG_IGN这样fork出来的子进程在终止时会自动清理掉不会产生僵尸进程,也不会通知父进程。系统默认对17号信号忽略和用户用sigaction函数自定义的忽略通常是没有区别的但这是一个特例。此方法对于Linux可用但不保证在其它UNIX系统上都可用。第一种如果 handler 使用阻塞等待void handler(int signo) { std::cout signo: signo std::endl; pid_t id 0; while((id waitpid(-1,nullptr,0)))//阻塞等待基于信号对子进程回收 { if(id 0) break;//没有子进程了子进程全部退出了 cout 回收进程id: id endl; } } int main() { signal(SIGCHLD,handler); for (int i 10; i 10; i) { pid_t id fork(); if (id 0) { cout child is runing endl; sleep(5); exit(10); } } return 0; }当10个子进程都会退出的时候是没有问题我们可以阻塞到他们全部退出此时父进程是不工作的但6个子进程退出4个没退出waitpid永远不返回了父进程不再进行这里就会出现问题所有使用WNOHANG。这样就实现了基于信号使用 waitpid 非阻塞对子进程进行回收。void handler(int signo) { std::cout signo: signo std::endl; pid_t id 0; while((id waitpid(-1,nullptr,WNOHANG)))//阻塞等待基于信号对子进程回收 { if(id 0) break;//没有子进程了子进程全部退出了 cout 回收进程id: id endl; } }如果我们不想自己释放子进程Linux支持手动忽略SIGCHLD所有的子进程都不再需要父进程进行等待了退出自动由系统回收不会产生僵尸进程。当然如果需要获取子进程退出信息那必须手动等待。signal(SIGCHLD, SIG_IGN);另外fork() 会继承exec() 不会继承自定义处理函数。本篇结束