文章目录

Linux进程信号
1、信号概念
信号是Linux提供给用户(进程)给其他进程发送异步(Asynchronous)信息的一种方式,属于软中断。
进程看待信号的方式:
- 信号在没有发生的时候,我们已经知道当发生的时候怎么处理了。
- 信号到来的时候,我们正在处理更重要的事情,我们暂时不能处理到来的信号,我们必须暂时要将到来的信号进行临时保存。
- 信号到了,可以不立即处理,可以到合适的时候处理。
- 信号的产生是随时产生的,我们无法预料,所以信号是异步发送的。
2、信号原理
这里浅谈,为后面多线程作准备。
2.1、信号原理
- 对共享资源进行保护,是一个多执行流场景下,一个比较常见和重要的话题。
- 互斥和同步
- 互斥:在访问任何一部分共享资源的时候,任何时刻只有我一个人访问,这就是互斥。
- 同步:访问资源在安全的前提下,具有一定的顺序性。
- 被保护起来的,任何时刻只允许一个执行访问的公共资源叫做临界资源。
- 访问临界资源的代码,我们叫做临界区。
- 原子性:操作对象的时候,只有两种状态,要么还没开始,要么已经结束。
2.2、信号量理论
- 信号量本质:对资源的预订机制,资源不一定被我持有,才是我的,只要我预订了,在未来的某个时间,就是我的。举例:看电影 – 提前买票 – 入场。
- 信号量:信号量本身是一个计数器,描述临界资源的数量的计数器。
- 所有的进程,访问临界资源,就必须申请信号量。那么信号量就是一个共享资源,得让所有进程看到同一个信号量!
- 信号量的申请(++)和释放(–)必须得是原子性的。因为++和–本身是一行代码,但是其汇编代码一般有3个操作,那么在进行进程切换的时候,如果是在这3个操作之间修改了信号量,那么信号量的值就是不准确的,可能还会出错。 – PV操作。
3、系统信号列表
使用命令行查看系统信号列表:
kill -l
。
每个信号都有一个编号和宏定义名称,这些宏定义可以在
signal.h
文件中找到。例如#define SIGQUIT 3
。
编号34以上的信号是实时信号(优先级最高,收到得立即处理)。这里不讨论实时信号。
这些信号各自在什么条件下产生,默认处理动作是什么,可以在signal(7)手册中查看:
man 7 signal
。
4、信号的常见处理方式
- 忽略该信号。
- 默认处理方式。
- 自定义处理信号:提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(catch)一个信号。
5、信号的产生
5.1、kill命令
代码:
#include <iostream> #include <unistd.h> using namespace std; int main() { while (true) { cout << "I am process,pid :" << getpid() << endl; sleep(1); } return 0; }
命令:
kill -信号编号 进程号
,如kill -2 1406628
运行结果:
5.2、键盘产生信号
- 按键按下了
- 哪些按键按下了
- 字符输入(字符设备),组合键输入(输入的是命令)。
比如输入abcd和按下ctrl + c ,是通过键盘驱动和操作系统进行联合解释的。
操作系统怎么知道输入数据了?通过硬件中断的技术。
代码:
#include <iostream> #include <unistd.h> using namespace std; int main() { while (true) { cout << "I am process,pid :" << getpid() << endl; sleep(1); } return 0; }
命令:键盘按下 ctrl + c
运行结果:
5.3、系统调用
kill函数系统调用:使用信号
sig
杀死进程pid
。
代码:
#include <iostream> #include <csignal> #include <assert.h> #include <unistd.h> using namespace std; int main() { int cnt = 0; while (true) { if (cnt == 5) { int n = kill(getpid(), SIGINT); assert(n == 0); } cout << "I am process,pid :" << getpid() << endl; cnt++; sleep(1); } return 0; }
运行结果:
raise函数系统调用:相当于调用kill(getpid(), sig)。
代码:
#include <iostream> #include <csignal> #include <assert.h> #include <unistd.h> using namespace std; int main() { int cnt = 0; while (true) { if (cnt == 5) { int n = raise(SIGQUIT); assert(n == 0); } cout << "I am process,pid :" << getpid() << endl; cnt++; sleep(1); } return 0; }
运行结果:
abort函数系统调用:相当于发送SIGABRT信号给当前进程。
代码:
#include <iostream> #include <csignal> #include <assert.h> #include <unistd.h> using namespace std; int main() { int cnt = 0; while (true) { if (cnt == 5) { abort(); } cout << "I am process,pid :" << getpid() << endl; cnt++; sleep(1); } return 0; }
运行结果:
5.3.1、自定义处理信号
使用signal函数完成对信号的自定义处理。
代码:
#include <iostream> #include <csignal> #include <unistd.h> using namespace std; void handler(int signo) { cout << "get a signal,no: " << signo << endl; } int main() { // handler调用完了,handler方法会被立即执行吗?不是,只是设置对应信号的处理方法 // 为未来我们收到对应的信号才执行handler方法 // ctrl + c 退出进程其实也是用的SIGINT信号 可以使用signal(SIGINT, handler);来测试,也就是运行时候ctrl + c看会不会退出 signal(SIGINT, handler); // ctrl + c 是SIGINT信号,编号2 signal(SIGQUIT, handler); // ctrt + \ 是SIGQUIT信号,编号3 while (true) { cout << "I am process,pid :" << getpid() << endl; sleep(1); } return 0; }
运行结果:
5.3.2、自己封装一个命令行完成kill指令
代码:
#include <iostream> #include <csignal> #include <assert.h> #include <string.h> #include <unistd.h> using namespace std; int main(int argc, char *argv[]) { if (argc != 3) { cout << "Usage:" << argv[0] << " -signo pid" << endl; exit(1); } int signo = stoi(argv[1] + 1); int pid = stoi(argv[2]); int n = kill(pid, signo); if (n < 0) { cerr << "kill error ,errno:" << errno << ", error string: " << strerror(errno) << endl; } return 0; }
5.4、软件条件
SIGPIPE是一种由软件条件产生的信号,之前在管道中使用过(父进程读,子进程写,父进程关闭读端,子进程就会收到SIGPIPE信号退出)。这里我们要讲的是SIGARLM信号和alarm函数。
alarm函数:如果second不为0,那么返回值就是上一个闹钟的剩余时间,如果second为0,就会取消之前的所有闹钟。
代码:设置闹钟,并处理闹钟。
#include <iostream> #include <csignal> #include <unistd.h> using namespace std; void handler(int sig) { int n = alarm(2); // 每隔2秒响一次 , alarm的返回值是:如果之前有闹钟还没到时间,这个返回值是最近的闹钟还剩余的时间,如果之前没有闹钟,那么返回0Í cout << "还剩:" << n << " 秒" << endl; // exit(0); } int main() { signal(SIGALRM, handler); alarm(50); // 响一次 int cnt = 0; while (true) { sleep(1); cout << "cnt :" << cnt++ << ", pid: " << getpid() << endl; // IO很慢 } return 0; }
运行结果:
代码:设置闹钟,取消闹钟
#include <iostream> #include <csignal> #include <unistd.h> using namespace std; void handler(int sig) { // cout << "get a sig, signo:" << sig << ", g_val:" << g_val << endl; // alarm(2); // cout << "闹钟到啦!" << endl; int n = alarm(2); // 每隔2秒响一次 , alarm的返回值是:如果之前有闹钟还没到时间,这个返回值是最近的闹钟还剩余的时间,如果之前没有闹钟,那么返回0Í cout << "还剩:" << n << " 秒" << endl; // exit(0); } int main() { signal(SIGALRM, handler); alarm(50); // 响一次 int cnt = 0; while (true) { sleep(1); cout << "cnt :" << cnt++ << ", pid: " << getpid() << endl; // IO很慢 if (cnt == 20) { // 取消所有闹钟 int ret = alarm(0); cout << "ret: " << ret << endl; } } return 0; }
运行结果:
5.5、异常
- 代码除0了 – SIGFPE
- 野指针错误 – SIGSEGV
代码:
#include <iostream> #include <csignal> #include <assert.h> #include <string.h> #include <unistd.h> using namespace std; // 异常 -- 异常没有终止,操作系统还有正常执行进程调度,那么上下文会进行保护,下一次调度到该进程的时候又会恢复,进程在运行的时候就会一直异常 void handler(int sig) { cout << "get a sig, signo: " << sig << endl; } int main() { signal(SIGFPE, handler); signal(SIGSEGV, handler); signal(SIGQUIT, handler); //除0 int a = 10; a /= 0; // 空指针 // int *p = nullptr; // *p = 1; while (true) { cout << "pid :" << getpid() << endl; sleep(1); } return 0; }
运行结果:
总结:
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
答:OS是进程的管理者
信号的处理是否是立即处理的?
答:不是,是在合适的时候(有可能是当时是阻塞的)。
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
答:需要暂时被记录下来放在pending位图中。
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
答:知道,非实时信号总共就31个,处理动作都已经设置好了。
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
答:
6、core dump 核心转储
6.1、core dump 核心转储概念
core dump核心转储是什么?
答:将进程在内核中的核心数据转储到磁盘中形成core或者core.pid(centos 7下是这样的形式)文件。
为什么要使用core dump核心转储?
答:想通过core定位到进程为什么退出,以及执行到哪句代码退出的。
core dump核心转储有什么用?
答:协助我们调试代码。
6.2、core dump 核心转储的配置
使用core dump功能:
在使用core dump功能之前,我们先确认我们的启用核心转储功能的配置是否正确。
在Linux系统中,
/proc/sys/kernel/core_pattern
文件用于控制当程序崩溃并产生core dump文件时,这些文件的命名和存储位置。默认的core_pattern
设置可能因不同的Linux发行版和系统配置而异。不过,一个常见的默认设置可能是:
core
或者可能是:
|/usr/lib/systemd/systemd-coredump %e %u %g %s %t %c %h
这里的
%
后面的字符是格式化的占位符,用于在core dump文件名中包含各种信息。例如:
%e
:可执行文件名%u
:用户ID%g
:组ID%s
:信号导致的core dump%t
:core dump的时间戳(以秒为单位)%c
:产生core dump的CPU%h
:主机名当
core_pattern
被设置为一个文件名(如core
)时,core dump文件通常会被创建在程序崩溃时所在的目录下,并且名为core
(或者可能带有.pid
后缀,其中pid
是崩溃进程的进程ID)。如果
core_pattern
被设置为一个管道(如|/path/to/program
),那么core dump的内容会被发送到指定的程序,而不是写入到文件中。在上面的例子中,systemd-coredump
是systemd提供的用于处理core dump的程序。你可以通过以下命令查看当前的
core_pattern
设置:cat /proc/sys/kernel/core_pattern
这个命令将输出当前系统上
core_pattern
的设置值。如果/proc/sys/kernel/core_pattern文件默认的内容是:
|/usr/lib/systemd/systemd-coredump %e %u %g %s %t %c %h
。那么我们需要把这个内容修改为core
,不然我们就算开启了核心转储功能,在当前目录下也生成不了core文件。
先切换到更高权限的root用户下(该文件权限较高),在命令行输入:
echo "core" > /proc/sys/kernel/core_pattern
6.3、core dump 核心转储的使用
testSig.cc
文件:#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <signal.h> using namespace std; int main() { pid_t id = fork(); if (id == 0) { int a = 10; a /= 0; exit(100); } int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid > 0) { cout << "exit code : " << ((status >> 8) & 0xFF) << endl; // 没有意义,因为是异常退出,那么exit(100)不会执行 cout << "exit core dump : " << ((status >> 7) & 0x1) << endl; cout << "exit sig : " << (status & 0x7f) << endl; } return 0; }
Makefile
文件:testSig:testSig.cc g++ -o $@ $^ -std=c++11 -g .PHONY:clean clean: rm -f testSig
查看核心转储功能是否开启,命令行输入:
ulimit -a
观察core file size,如果是0,则未开启。
这时候执行程序不会生成core文件。
开启方法,在命令行输入:ulimit -c (size大小,单位是KB)。如:
ulimit -c 10240
。
这时候会生成core文件。
这时候,我们进入调试模式,并输入core-file core。就可以找到代码异常了。
7、信号的保存
- 实际执行信号的处理动作称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
7.1、信号在内核中的表示
信号在内核中的表示示意图:
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,因为当在处理一个信号的时候,会将该信号阻塞,直到处理完该信号,那么处理中相同信号到来的时候会被阻塞,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
7.2、阻塞信号
7.2.1、sigset_t 信号集
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。下面将详细介绍信号集的各种操作。
#ifndef __sigset_t_defined #define __sigset_t_defined 1 #include <bits/types/__sigset_t.h> /* A set of signals to be blocked, unblocked, or waited for. */ typedef __sigset_t sigset_t; #endif
#ifndef ____sigset_t_defined #define ____sigset_t_defined #define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } __sigset_t; #endif
sigset_t其实就是位图类型。
7.2.2、信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印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 sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
- 函数sigismember是查看某个信号是否在信号集中。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1
7.2.3、sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
返回值:成功返回0,错误返回-1。
如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
注意:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
7.2.4、sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
7.2.5、阻塞信号和解除阻塞信号实验
场景:
- 屏蔽2号信号
- 获取进程的pending位图
- 打印pending位图中的收到的信号
- 解除对2号信号的屏蔽
代码:
#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <signal.h> #include <assert.h> using namespace std; void PrintSigset(const sigset_t &block) { cout << "pending bitmap : "; for (int signo = 31; signo >= 1; --signo) { if (sigismember(&block, signo)) cout << "1"; else cout << "0"; } } int main() { // 1. 屏蔽2号信号 sigset_t block; // 用户在栈上开辟了空间 sigemptyset(&block); sigaddset(&block, 2); sigset_t oblock; // 1.1 开始屏蔽2号信号,其实就是设置进入内核中 int n = sigprocmask(SIG_SETMASK, &block, &oblock); assert(n == 0); cout << "block 2 sig success ..." << endl; cout << "pid : " << getpid() << endl; int cnt = 1; while (true) { // 2. 获取进程的pending位图 sigset_t pending; sigemptyset(&pending); n = sigpending(&pending); assert(n == 0); // 3. 打印pending位图中的收到的信号 PrintSigset(pending); cout << " cnt : " << cnt; cout << endl; // 4. 解除对2号信号的屏蔽 if (cnt == 20) { cout << "解除对2号信号的屏蔽...." << endl; n = sigprocmask(SIG_UNBLOCK, &block, &oblock); assert(n == 0); } cnt++; sleep(1); } return 0; }
运行结果:
细节:信号递达的时候,一定会把对应的pending位图的位置清0。那么是先清0再递达还是先递达再清0?答案是先清0再递达。
我们用下面代码验证:
#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <signal.h> #include <assert.h> using namespace std; void PrintSigset(const sigset_t &block) { cout << "pending bitmap : "; for (int signo = 31; signo >= 1; --signo) { if (sigismember(&block, signo)) cout << "1"; else cout << "0"; } } void handler(int sig) { sigset_t pending; sigemptyset(&pending); int n = sigpending(&pending); assert(n == 0); cout << "递达中..." << endl; PrintSigset(pending); // 0 ,递达之前pending位图2号信号已经清零 cout << endl; cout << sig << " 号信号已递达..." << endl; } int main() { // 捕捉2号信号 signal(2, handler); // 1. 屏蔽2号信号 sigset_t block; // 用户在栈上开辟了空间 sigemptyset(&block); sigaddset(&block, 2); sigset_t oblock; // 1.1 开始屏蔽2号信号,其实就是设置进入内核中 int n = sigprocmask(SIG_SETMASK, &block, &oblock); assert(n == 0); cout << "block 2 sig success ..." << endl; cout << "pid : " << getpid() << endl; int cnt = 1; while (true) { // 2. 获取进程的pending位图 sigset_t pending; sigemptyset(&pending); n = sigpending(&pending); assert(n == 0); // 3. 打印pending位图中的收到的信号 PrintSigset(pending); cout << " cnt : " << cnt; cout << endl; // 4. 解除对2号信号的屏蔽 if (cnt == 20) { cout << "解除对2号信号的屏蔽...." << endl; n = sigprocmask(SIG_UNBLOCK, &block, &oblock); assert(n == 0); } cnt++; sleep(1); } return 0; }
运行结果:
我们可以屏蔽所有信号吗?
我们用下面代码来验证:
#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <signal.h> #include <assert.h> using namespace std; void PrintSigset(const sigset_t &block) { cout << "pending bitmap : "; for (int signo = 31; signo >= 1; --signo) { if (sigismember(&block, signo)) cout << "1"; else cout << "0"; } } int main() { // 1. 屏蔽2号信号 sigset_t block; // 用户在栈上开辟了空间 sigemptyset(&block); // test:屏蔽所有信号? 9,19号信号不能被屏蔽,18号特殊信号这里也不能屏蔽(看编译器) for (int i = 1; i <= 31; ++i) sigaddset(&block, i); sigset_t oblock; // 1.1 开始屏蔽2号信号,其实就是设置进入内核中 int n = sigprocmask(SIG_SETMASK, &block, &oblock); assert(n == 0); cout << "block 2 sig success ..." << endl; cout << "pid : " << getpid() << endl; int cnt = 1; while (true) { // 2. 获取进程的pending位图 sigset_t pending; sigemptyset(&pending); n = sigpending(&pending); assert(n == 0); // 3. 打印pending位图中的收到的信号 PrintSigset(pending); cout << " cnt : " << cnt; cout << endl; cnt++; sleep(1); } return 0; }
运行结果:我们发现,9号信号不能屏蔽。
我们发现19号信号也不能屏蔽。
其他信号都可以屏蔽:18号信号特殊,在不同编译器下运行效果不同。
8、信号的处理
8.1、信号的捕捉过程
在信号处理(捕捉)的过程中,一共会有4次的状态切换(内核态和用户态)。
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂。举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数。这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
8.2、信号的捕捉
前面我们有谈到过信号的捕捉(自定义处理信号),使用signal函数调用。
这里我们介绍一个新的信号捕捉函数sigaction。
- sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signum是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
- 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数(和signal函数调用的handler函数指针一样),通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然。这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止(不想让信号嵌套式进行捕捉处理,防止栈溢出)。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段。
使用sigaction函数:
代码:
#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <signal.h> #include <assert.h> using namespace std; void PrintSigset(const sigset_t &block) { cout << "pending bitmap : "; for (int signo = 31; signo >= 1; --signo) { if (sigismember(&block, signo)) cout << "1"; else cout << "0"; } } void handler(int sig) { sigset_t pending; sigemptyset(&pending); while (true) { int n = sigpending(&pending); assert(n == 0); PrintSigset(pending); cout << endl; sleep(1); } } int main() { cout << "pid : " << getpid() << endl; struct sigaction act, oact; act.sa_handler = handler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, 2); sigaddset(&act.sa_mask, 3); sigaction(2, &act, &oact); while (true) sleep(1); return 0; }
运行结果:
9、可重入函数
- main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个结点而最后只有一个结点真正插入链表中了。
- 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?因为每个函数调用都有其独立的栈帧来存储局部变量和参数,这些栈帧是隔离的,因此不同的控制流程在调用同一个函数时不会相互干扰。
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
10、volatile
volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
我们来看以下代码:
#include <stdio.h> #include <signal.h> int flag = 0; void handler(int sig) { printf("chage flag 0 to 1\n"); flag = 1; } int main() { signal(2, handler); while (!flag); printf("process quit normal\n"); return 0; }
命令行输入:
gcc sig.c
。运行结果:ctrl + c 后进程退出了。因为捕捉2号信号后flag变为1,那么在while(!flag);就直接跳过循环。
命令行输入:
gcc sig.c -O1
。注意这个是大写的O,不是数字0。运行结果:进程并没有退出!原因是:捕捉2号信号后flag变为1。但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile,不让flag放到寄存器中。
修改后的代码:flag加上volatile关键字。
#include <stdio.h> #include <signal.h> volatile int flag = 0; void handler(int sig) { printf("chage flag 0 to 1\n"); flag = 1; } int main() { signal(2, handler); while (!flag); printf("process quit normal\n"); return 0; }
运行结果:ctrl + c 后进程退出了。因为捕捉2号信号后flag变为1,那么在while(!flag);就直接跳过循环。
11、SIGCHLD信号
- 进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
- 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
代码:
#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <signal.h> using namespace std; void CleanupChild(int signo) { if (signo == SIGCHLD) { while (true) { // 50个退出,50个没有 的情况 -- 非阻塞等待 pid_t rid = waitpid(-1, nullptr, WNOHANG); // -1 : 回收任意一个子进程 if (rid > 0) { cout << "wait child success: " << rid << endl; } else if (rid <= 0) break; } } cout << "wait sub process done" << endl; } int main() { signal(SIGCHLD, CleanupChild); for (int i = 0; i < 100; i++) { pid_t id = fork(); if (id == 0) { // child int cnt = 5; while (cnt--) { cout << "I am child process: " << getpid() << endl; sleep(1); } cout << "child process died" << endl; exit(0); } } // father while (true) sleep(1); return 0; }
运行结果:我们可以看到,最后只剩一个父进程,子进程没有僵尸。
- 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction或者signal将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
代码:
#include <iostream> #include <unistd.h> #include <sys/wait.h> #include <signal.h> using namespace std; int main() { signal(SIGCHLD, SIG_IGN); for (int i = 0; i < 100; i++) { pid_t id = fork(); if (id == 0) { // child int cnt = 5; while (cnt--) { cout << "I am child process: " << getpid() << endl; sleep(1); } cout << "child process died" << endl; exit(0); } } // father while (true) sleep(1); return 0; }
运行结果:我们可以看到,最后只剩一个父进程,子进程没有僵尸。
OKOK,Linux进程信号就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。