今天,带来Linux下进程信号的讲解。文中不足错漏之处望请斧正!
例子:
比如来电铃声:
顺便再带一嘴:
老师上着课,我去上厕所
我们也能感受到,信号是某种标识,如外卖小哥的电话,就标识着我外卖到了。
看看Linux下支持的信号:
[bacon@VM-12-5-centos linux]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-*斜体样式*13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
以上显示的是 信号编号:信号描述,其实就是根据编号进行宏替换。类似于下面:
#define 1 SIGHUP
其中,
[1, 31]:普通信号
[34, 64]:实时信号(不过多了解)
SIGCHLD: 父进程退出时会给子进程发送的信号
9号信号无法被捕捉, 是保险措施, 以防无法终止异常进程.
进程中的信号是OS给进程发送的一种标识(如标识某种事件发生)。
*处理信号也叫 信号被捕捉。
进程的task_struct
里。
想想刚刚用kill看到的信号,[1, 31],共32个信号……聪明的你一定想到了,可以用32位整数保存,某位的0/1代表是否收到此信号,其实就是用位图结构保存。
如第一个比特位若为1,就代表收到了“1) SIGHUP ”,反之没收到。
发送信号 = 修改PCB中的信号位图。
task_struct
是OS维护的内核数据结构,也只有OS能改——任何发送信号的方式都必须通过OS发送。
那么,OS也应该提供修改PCB中信号位图的系统调用。老样子,我们就可以学底层系统调用而知上层封装的接口。
信号表明某个事件发生, 方便我们追溯错误.
那又为什么需要这么多不同的信号? 不同信号可以准确表示不同事件.
回头看看,我们学过热键ctrl+c
来终止前台进程,是怎么做到的?
键盘是硬件,要通过OS,OS就会把ctrl+c
解释成2号信号2) SIGINT
。
我们来man 7 signal
,查看一下2号信号对应的默认动作:
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGINT 2 Term Interrupt from keyboard
翻译过来就是通过键盘终止进程。
那除了默认动作,我们提到的自定义动作怎么玩?
sighandler_t signal(int signum, sighandler_t handler);
*typedef void (*sighandler_t)(int);
signal函数用到了回调函数: 接收到某个信号的时候,调用handler
函数
用用吧!
#include
#include
#include
using namespace std;
void handler(int signo)
{
printf("进程%d捕捉到了一个信号,信号编号为%d\n", getpid(), signo);
}
int main()
{
//捕捉到2号信号后调用handler
//仅仅是设置了对2号信号的捕捉方法
//可以kill -2 pid
//也可以ctrl+c
signal(2, handler);
while(1)
{
printf("我是一个进程 |pid=%d|\n", getpid());
sleep(1);
}
return 0;
}
为什么ctrl+c
不能终止进程?因为2号信号被设置了自定义动作,就没有默认动作的事了。
除了ctrl+c
,ctrl+\
也能终止进程:
[bacon@VM-12-5-centos 8-signal]$ ./signal
我是一个进程 |pid=24809|
我是一个进程 |pid=24809|
我是一个进程 |pid=24809|
我是一个进程 |pid=24809|
^\Quit
ctrl+\
对应的是3号信号3) SIGQUIT
还有一点可以提提:Linux下,默认只允许有一个前台进程。
平常我们用shell和系统交互的时候,shell就是唯一的前台进程;当我们运行别的程序,shell就被切到后台。
这也是为什么我们运行别的程序时,输入指令没用:
[bacon@VM-12-5-centos 8-signal]$ ./signal
我是一个进程 |pid=25594|
我是一个进程 |pid=25594|
pwd
我是一个进程 |pid=25594|
ls
cd我是一个进程 |pid=25594|
到这我们就知道了第一种产生信号的方式:键盘热键。
我们提过,系统肯定要提供给进程发信号的系统调用,这就是我们第二种方式。
int kill(pid_t pid, int sig);
sender.cc
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(const string& procName)
{
printf("%s: pid/signo not found\n");
printf("\t-pid -signo\n");
}
//使用方法:命令行参数额外传递pid和信号编号
//argv[0] = ./sender
//argv[1] = pid
//argv[2] = signo
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(-1);
}
pid_t pid = atoi(argv[1]);
int signo = atoi(argv[2]);
int killRet = kill(pid, signo);
if(killRet == -1)
{
perror("kill:");
}
return 0;
}
testproc.cc
#include
#include
#include
using namespace std;
int main()
{
while(1)
{
printf("我是一个进程 |pid=%d|\n", getpid());
sleep(1);
}
return 0;
}
测试效果:
再看些库函数:
int raise(int sig);
int main()
{
int cnt = 0;
while(1)
{
if(cnt == 3)
{
cout << getpid() << "给自己发2号信号..." << endl;
raise(2);
}
printf("我是一个进程 |pid=%d|\n", getpid());
sleep(1);
++cnt;
}
return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc
我是一个进程 |pid=8836|
我是一个进程 |pid=8836|
我是一个进程 |pid=8836|
8836给自己发2号信号...
用系统调用kill模拟:kill(getpid(), 2(SIGINT);
void abort(void);
int main()
{
int cnt = 0;
while(1)
{
if(cnt == 3)
{
cout << getpid() << "调用abort()..." << endl;
abort();
}
printf("我是一个进程 |pid=%d|\n", getpid());
sleep(1);
++cnt;
}
return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc
我是一个进程 |pid=10185|
我是一个进程 |pid=10185|
我是一个进程 |pid=10185|
10185调用abort()...
Aborted
用系统调用kill模拟:kill(getpid(), 6);
(6) SIGABRT
)。
库函数基于系统调用kill,系统调用kill基于操作系统本身。
int main()
{
while(1)
{
printf("我是一个进程,准备除0...\n");
sleep(1);
int a = 0;
a /= 0;
}
return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc
我是一个进程,准备除0...
Floating point exception
乘法的含义是累加。如3 * 5 = 3 + 3 + 3 + 3 + 3。
相反地,除法的含义是累减。如 15 / 5 的含义是能从15中减多少个5,15 - 5 - 5 - 5 = 0,总共3个5。那么x/0的含义就是能从x中减多少个0,x - 0 - 0 - …
在现实中,这是没有意义;在计算机中,这是让硬件进行不合理的死循环运算,足以算得上异常。
因此OS会给/0的进程发送8) SIGFPE
,直接终止掉。
void catchSig(int signo)
{
printf("获取到一个信号,其编号为: %d\n", signo);
}
int main()
{
signal(SIGFPE, catchSig);
while(1)
{
printf("我是一个进程,准备除0...\n");
sleep(1);
int a = 0;
a /= 0;
}
return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./testproc
我是一个进程,准备除0...
获取到一个信号,其编号为: 8
获取到一个信号,其编号为: 8
获取到一个信号,其编号为: 8
获取到一个信号,其编号为: 8
//...
现象有点问题:OS在疯狂发信号。不过我们先按下不表。
操作系统怎么知道我除0了?
因为除0会触发CPU异常, CPU会通知OS
OS怎么知道我除0了?因为除0CPU会异常,而OS会被CPU通知。
我们现在再谈之前”OS疯狂发信号“的问题,它怎么就疯狂地发了呢?
进程收到信号后是可以不终止的(刚刚的程序捕捉到某信号后就没退出)。当异常进程不终止时,每次CPU切换到异常进程,都会
因此,才有了“疯狂发信号”的现象。
都讲到这了, 顺便说说, 我们为什么无法真正执行解引用空指针的操作.
[bacon@VM-12-5-centos 8-signal]$ ./testproc
我是一个进程,准备解引用空指针...
Segmentation fault
这就是11) SIGSEGV
。
而我们需要对虚拟地址与物理地址的映射有新一层理解才能解答这个问题。
MMU,内存管理单元,是一种集成在CPU上的硬件。可以为页表的虚拟地址和某个物理地址建立映射。
所以建立映射是这样:
解引用空指针是这样:
越界访问MMU肯定不答应啦!不仅拒绝,它还觉得你这种操作不正常,大概率程序出错了,所以会告诉OS这个进程闹事儿,让它治你。于是OS就发送了11号信号。
典型的场景就是管道了。当管道的读端关闭,写端也会关闭,因为接着写没有意义且浪费。
把读端关闭是一种软件行为,而读端是否关闭是写端是否关闭的条件,这种软件行为使得写端继续写的软件条件不满足,OS就向写端发送信号来终止写端进程。
接下来谈谈定关于闹钟的软件条件。
需要用到一个系统调用。
unsigned int alarm(unsigned int seconds);
14) SIGALRM
(默认动作是终止进程)int main()
{
cout << "设置定时器:1秒后发送14号信号..." << endl;
alarm(1);
int cnt = 0;
while(true)
{
cout << "count: " << cnt++ << endl;
}
return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./sigtest
设置定时器:1秒后发送14号信号...
count: 0
count: 1
...
count: 192449
count: 192450
Alarm clock
设置定时器(秒后发送14号信号) → 3秒 → 满足软件条件,发送信号。
这是加了打印后一秒内计算机能累加多少次,我们试试不加打印。
int cnt = 0;
void catchSig(int xxx)
{
cout << "cnt = " << cnt << endl;
}
int main()
{
signal(SIGALRM, catchSig);
alarm(1);
while(true)
{
++cnt;
}
return 0;
}
[bacon@VM-12-5-centos 8-signal]$ ./sigtest
cnt = 567799259
//没有直接退出,OS却没有“疯狂发信号了”?
看看这差距,虽然我们用云服务器跑的,IO还要经过一层网络,但这也很能说明IO很慢。
诶,没有直接退出,OS却没有“疯狂发信号了”?
这说明这个定时器(闹钟)只响一次。
怎么理解闹钟作为一种软件条件?
我们从“闹钟的管理入手”。每个进程都能设置自己的闹钟,多了以后OS肯定要管理,怎么管理呢?
OS会获取当前时间戳,跟myalarm1.when
对比,如果前者更大,说明超时了。
闹钟的组织也可以用堆结构,让最近一个会超时的闹钟保持在堆顶。
闹钟.when
就是一种软件条件。还有个问题:我们man 7 signal
能看到信号对应的行为,但是这个Action列中的Term和Core有什么区别呢?
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
说说Core,以11号信号为例,我们看看解引用空指针的现象:
[bacon@VM-12-5-centos 8-signal]$ ./sigtest
我是一个进程,准备解引用空指针...
Segmentation fault
但Core终止产生的额外动作在云服务器上看不到明显的体现:
[bacon@VM-12-5-centos 8-signal]$ ulimit -a
core file size (blocks, -c) 0
#...
因为云服务器上默认没有core file。
如果我们想看到明显的体现,可以设置一些core file
[bacon@VM-12-5-centos 8-signal]$ ulimit -c 1024
[bacon@VM-12-5-centos 8-signal]$ ulimit -a
core file size (blocks, -c) 1024
[bacon@VM-12-5-centos 8-signal]$ ./sigtest
我是一个进程,准备解引用空指针...
Segmentation fault (core dumped)
[bacon@VM-12-5-centos 8-signal]$ ll
total 288
-rw------- 1 bacon bacon 561152 Mar 1 10:14 core.8865
#...
core dumped
是核心转储的意思。
支持事后调试。
核心转储:进程异常退出(Core)前,会把其在内存中的有效数据转储到磁盘上。
core.8865中,就是pid为8865的进程异常退出时,从内存中转储来的数据。
方便定位错误, 支持事后调试.
gdb 中可以用 core-file 来加载
[bacon@VM-12-5-centos 8-signal]$ ./sigtest
我是一个进程,准备解引用空指针...
Segmentation fault (core dumped)
[bacon@VM-12-5-centos 8-signal]$ ll
total 300
-rw------- 1 bacon bacon 561152 Mar 1 10:31 core.12165
-rw-rw-r-- 1 bacon bacon 163 Mar 1 10:23 makefile
-rwxrwxr-x 1 bacon bacon 13664 Mar 1 10:31 sender
-rw-rw-r-- 1 bacon bacon 673 Feb 28 18:13 sender.cc
-rwxrwxr-x 1 bacon bacon 24512 Mar 1 10:31 sigtest
-rw-rw-r-- 1 bacon bacon 2109 Mar 1 10:14 sigtest.cc
-rwxrwxr-x 1 bacon bacon 8984 Feb 28 21:14 testproc
[bacon@VM-12-5-centos 8-signal]$ gdb sigtest
#...
(gdb) core-file core.12165
[New LWP 12165]
Core was generated by `./sigtest'.
Program terminated with signal 11, Segmentation fault.
#0 0x0000000000400705 in main () at sigtest.cc:113
113 *p = 10;
#...
如果进程是Term终止,就不会进行核心转储。
int main()
{
while(1)
{
printf("我是一个进程 |pid=%d|\n", getpid());
sleep(1);
}
return 0;
}
进程正常终止,Action为Term,无需事后调试,OS不会进行核心转储。
进程异常终止,Action为Core,可能需要事后调试,OS会进行核心转储。
之前我们的说法都是图方便理解,现在来列出以上各种过程、操作的庐山真面目。
举个例子:
老师课间布置作业,我记下来,晚上回家了才写作业。
这就是没被阻塞的信号的一生:信号产生 → 信号发送 → 信号递达 → 信号处理
我很讨厌的一个老师课间布置作业,我不情愿地记下来,晚上回家了,不想写他的作业,将来实在不行了再写。
这就是被阻塞的信号的一生:信号产生 → 信号发送 → 信号被阻塞 → … → 解除对信号的阻塞 → 信号递达 → 信号处理
信号发送, 就是向内核中信号相关的数据结构写入. 对信号发送的学习可以转化为对相关数据结构的学习. 也就是信号的保存.
和信号保存相关的数据结构主要是三个: pending位图、block位图和handler表.
所以,从这些数据结构中理解信号是这样的:
我们可由此得出些结论:
也能进一步理解系统调用signal
:
在从内核态返回用户态的时候 (后面才能理解).
告知:
系统调用或者需要访问内核资源,或者需要访问硬件,都需要切换成内核态。
本质上只不过是从主执行流直接跳转到1G的内核空间执行OS的代码罢了
进程需要切换代表进程没执行完,那么就需要放到run_queue或者wait_queue内,也就是内核数据结构中,管理起来,这就涉及内核资源的访问。那自然需要切换成内核态。
是一种安全管理,道理类似权限,是OS对内核的保护。
通过CPU上名为 CR3 的不可见寄存器来管理. 当其中的值为0 = 内核态, 3 = 用户态.
运行级别的切换消耗较大 (需要陷入内核) , 因此系统调用效率不是太高.
进程运行级别的切换开销大, 如果切换时顺带做点别的事, 比如递达和处理信号, 就提高了整体效率
详细:
简单:
详细:
简单:
为什么自定义动作和其他二者的递达处理过程不一样?
因为自定义动作不一定安全, 不能给进程内核态的运行级别来执行未知的代码.
我们看了内核中信号相关的数据结构后,发现每个信号都只有1个比特位来标识是否阻塞或未决,而不记录次数。既然阻塞和未决的表示都只有“有效”或“无效”,那就可以用一个sigset_t信号集类型来存储阻塞和未决状态。
sigset_t用1bit表示每种信号的“有效”或“无效”的状态。
sigset_t主要是OS为了方便用户修改底层的pending位图和block位图。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
mask |= set
mask = maks & (~set)
mask = set
int sigpending(sigset_t *set);
[bacon@VM-12-5-centos 2]$ ./sigtest
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
^C0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
再试试解除对信号的屏蔽:
int main()
{
//1. 屏蔽2号信号
sigset_t block, oblock, pending;
//1.1 初始化
sigemptyset(&block);
sigemptyset(&oblock);
//1.2 填入要屏蔽的信号
sigaddset(&block, SIGNAL_TO_BLOCK);
//1.3 把block设置进内核(block位图中)
sigprocmask(SIG_SETMASK, &block, &oblock);
//2. 打印pending信号集
int cnt = 5;
while(true)
{
sigpending(&pending);
displayPending(pending);
sleep(1);
if(cnt-- == 0)
{
sigprocmask(SIG_SETMASK, &oblock, &block);
cout << "解除对信号的屏蔽" << endl;
}
}
return 0;
}
[bacon@VM-12-5-centos 2]$ ./sigtest
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
^C0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
[bacon@VM-12-5-centos 2]$
诶,怎么不打印信息?因为这些接口都是系统调用,准备从内核态返回之前就会捕捉并处理信号(递达)。我们的2号信号默认终止进程,于是就直接结束了。可以验证一下:
void myhandler(int signo)
{
printf("%d号信号已经递达!\n", signo);
}
//阻塞2号信号
int main()
{
signal(SIGNAL_TO_BLOCK, myhandler);
//1. 屏蔽2号信号
sigset_t block, oblock, pending;
//1.1 初始化
sigemptyset(&block);
sigemptyset(&oblock);
//1.2 填入要屏蔽的信号
sigaddset(&block, SIGNAL_TO_BLOCK);
//1.3 把block设置进内核(block位图中)
sigprocmask(SIG_SETMASK, &block, &oblock);
//2. 打印pending信号集
int cnt = 5;
while(true)
{
sigpending(&pending);
displayPending(pending);
sleep(1);
if(cnt-- == 0)
{
sigprocmask(SIG_SETMASK, &oblock, &block);
cout << "解除对信号的屏蔽" << endl;
}
}
return 0;
}
[bacon@VM-12-5-centos 2]$ ./sigtest
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
^C0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
2号信号已经递达!
解除对信号的屏蔽
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
那如果我们想随意地屏蔽或接触屏蔽呢?
我们3个核心的信号数据结构都有了操作方法:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigpending(sigset_t *set);
sighandler_t signal(int signum, sighandler_t handler);
除了signal,还有一个。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
作用:把signum信号的自定义动作设为act,把原来的动作放进oldact
参数
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
#include
#include
#include
using namespace std;
void myhandler(int signo)
{
cout << "got signal: " << signo << endl;
}
int main()
{
struct sigaction act, oact;
act.sa_handler = myhandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while(1) sleep(1);
return 0;
}
[bacon@VM-12-5-centos 3]$ ./sigtest
^Cgot signal: 2
^Cgot signal: 2
^Cgot signal: 2
那如果信号正在被递达处理的期间,我持续给进程发同一个信号会怎么样?
#include
#include
#include
using namespace std;
void count(int cnt)
{
for(int i = cnt; i >= 0; --i)
{
printf("cnt: %2d\r", i);
fflush(stdout);
sleep(1);
}
cout << endl;
}
void myhandler(int signo)
{
cout << "got signal: " << signo << endl;
cout << "正在处理中..." << endl;
count(20);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = myhandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while(1) sleep(1);
return 0;
}
可得结论:递达某信号期间,再收到此信号也是无法被递达的。
为什么?而且,为什么它能阻塞了,但还是又递达了一次?
告知:
第一次收到信号,pending位图上某个位置0变1。
要递达此信号时,OS会把此信号从pending位图上移除(1变0),然后对此信号阻塞,最后才开始递达处理。
此时的2号信号在pending位图上的位置是0!再发新的2号信号给进程,虽然它被阻塞了,不会被递达,但是可以放在pending位图里!随后第一次收到的2号信号递达完毕,解除对2号信号的阻塞,此时就自动进行。
0→1→0→1→0
没收到信号→收到信号→OS递达信号前置0并阻塞2号信号→递达2号信号→收到新信号→2号信号递达完毕并解除对2号信号的阻塞→自动尝试递达2号信号→递达新的2号信号。
所以处理同类信号的原则是串行处理。
我们再看看参数sa_mask,这有啥用呢?当我们递达某个信号,可以同时对别的信号阻塞,这个mask就是阻塞信号集。
#include
#include
#include
using namespace std;
void count(int cnt)
{
for(int i = cnt; i >= 0; --i)
{
printf("cnt: %-2d\r", i);
fflush(stdout);
sleep(1);
}
cout << endl;
}
void myhandler(int signo)
{
cout << "got signal: " << signo << endl;
cout << "正在处理中..." << endl;
count(10);
}
int main()
{
struct sigaction act, oact;
act.sa_handler = myhandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaction(2, &act, &oact);
while(1) sleep(1);
return 0;
}
[bacon@VM-12-5-centos 3]$ kill -2 10452
[bacon@VM-12-5-centos 3]$ kill -3 10452
[bacon@VM-12-5-centos 3]$ ./sigtest
got signal: 2
正在处理中...
cnt: 0
Quit
发送2号信号后发3号,虽然被阻塞,但是等2号信号递达完毕,也会自动对2、3号解除阻塞,并自动尝试递达。
先看示例:
被 “重新进入” 后, 函数运行和其结果不受影响的函数就是可重入函数
确保变量每次被访问的方式都是通过内存读取.
编译器可能会对程序优化, 对某些变量的访问通过寄存器或缓存访问, 且变量值实时从内存更新到寄存器或缓存, 因此变量值可能出现不一致.
volatile int var = 10;
今天的分享就到这里了,感谢您能看到这里。
这里是培根的blog,期待与你共同进步!