Linux操作系统进程信号
Posted Ricky_0528
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统进程信号相关的知识,希望对你有一定的参考价值。
文章目录
3. 信号的保存
-
实际执行信号的处理动作称为信号递达(Delivery)
- 自定义捕捉
- 默认
- 忽略
-
信号从产生到递达之间的状态,称为信号未决(Pending)
- 本质是这个信号被暂存在task_struct信号位图中,未决
-
进程可以选择阻塞 (Block )某个信号
-
本质是操作系统允许进程暂时屏蔽指定的信号
-
该信号依旧是未决的
-
该信号不会被递达,直到解除阻塞,方可递达
递达中的忽略 跟 阻塞 有啥区别
- 忽略是递达的一种方式
- 阻塞是没有被递达,是一个独立的状态
-
-
-
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
-
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
3.1 信号在内核中的表示示意图
SIG_DFL:默认 SIG_IGN:忽略
默认动作其实就是被强转的0,忽略动作为1
pending位图保存的是已经收到但还没有被递达的信号,会在合适的时机执行handler中的方法,即完成递达
block表本质上也是位图结构——uint32_t block;
,比特位的位置代表信号的编号,比特位的内容代表信号是否被阻塞
block位图称作阻塞位图,也叫信号屏蔽字,表示哪些信号不应该被递达,知道解除阻塞
handler为函数指针数组,每个信号的编号就是该数组的下标
3.2 sigset_t
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
虽然sigset_t是一个位图结构,但是不同的操作系统实现是不一样的,不能让用户直接修改该变量,应该使用特定的函数
sigset_t声明的变量同样也保存在用户栈上,与之前的int、double没有任何区别
3.3 信号集操作函数
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所指向的信号集,使系统支持的所有信号都加入到该集合内,表示该信号集的有效信号包括系统支持的所有信号
- 在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
- 前四个函数的返回值都是成功返回0,出错返回-1,sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1
3.4 sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(block表)
set:输入型参数,传入你想设置成的位图
oldset:输出型参数,返回老的block位图
how | 作用 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
9号信号被称为管理员信号,不能被屏蔽也不能被自定义捕捉,必须永远遵守默认行为
3.5 sigpending
不会对pending位图做修改,而是单纯的获取进程的pending位图,修改只由操作系统完成
set:输出型参数,pending位图
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sig_show(sigset_t *sig)
int i;
printf("current pending: ");
for (i = 1; i < 31; i++)
if (sigismember(sig, i))
printf("1");
else
printf("0");
printf("\\n");
void handler(int sig)
printf("%d号信号递达,处理完毕!\\n", sig);
int main()
signal(2, handler);
sigset_t iset, oset;
sigemptyset(&iset);
sigemptyset(&oset);
sigaddset(&iset, 2);
sigprocmask(SIG_SETMASK, &iset, &oset);
sigset_t pending;
int count = 0;
while (1)
sigemptyset(&pending);
sigpending(&pending);
sig_show(&pending);
sleep(1);
count++;
if (count == 5)
sigprocmask(SIG_SETMASK, &oset, NULL);
printf("2号信号取消阻塞!\\n");
return 0;
4. 信号的处理
信号是被保存在进程的PCB中,pending位图里的,对其的处理分为:检测、递达(默认、忽略、自定义)
当进程从内核态返回到用户态的时候,进行上面的检测并处理工作
内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS代码的执行全都是在内核态
用户态:就是用户代码和数据被访问或者执行的时候所处的状态,我们自己写的代码全都是在用户态执行的
主要区别:主要在于权限
感性的理解
用户调用系统函数的的时候,除了进入函数,身份也会发生变化,用户身份变成了内核身份
理性的认识
用户的身份是以进程为代表的
用户的数据和代码一定要被加载到内存,OS的数据和代码也一定要被加载到内存中的,这个工作由页表来完成
地址空间中的用户空间由用户级页表负责映射到物理内存当中,不同进程对应的用户级页表各不相同,而内核空间由系统级页表——内核页表映射到物理内存当中,这个内核页表被所有进程共享
进程具有了地址空间是能够看到用户和内核的所有内容的,但不一定能访问
CPU内有寄存器保存了当前进程的状态
-
用户态使用的是用户级页表,只能访问用户数据和代码
-
内核态使用的是内核级页表,只能访问内核级的数据和代码
进程之间无论如何切换,我们能够保障一定能找到同一个OS,因为每个进程都有3~4G的地址空间,而且使用同一张内核页表
所谓的系统调用就是进程的身份转化成为内核,然后根据内核页表找到系统函数,执行就可以了
在大部分情况下,实际OS都是可以在进程的上下文中直接运行的
4.1 自定义捕捉
默认和忽略都不需要再从内核态转到用户态,直接在内核态就可以完成
可以抽象为
内核为什么不直接执行用户的代码,而要切换到用户态来执行
因为操作系统不相信任何人,操作系统身份特殊,不能直接执行用户的代码(OS权限太大)
4.2 signal
typedef void (*sighandler_t)(int)
:为一个函数指针
signum
:信号对应的整型值
handler
:修改进程对信号的默认处理动作
4.3 sigaction
修改的是handler函数指针数组
act:输入型参数
oact:输出型参数,待会老的信号处理方法,不需要置NULL
- sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号,若act指针非空,则根据act修改该信号的处理动作;若oact指针非空,则通过oact传出该信号原来的处理动作,act和oact指向sigaction结构体
- 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号,显然这也是一个回调函数,不是被main函数调用,而是被系统所调用
sigaction结构体
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,可以暂时将sa_flags设为0,sa_sigaction是实时信号的处理函数
5. 可重入函数
在信号捕捉的时候再次插入了节点,这是就会导致有两个节点被同时插入了,head头指针只能指向一个节点,另一个节点就造成了内存泄漏
- insert函数一旦重入,有可能出现问题,该函数被称为不可重入函数
- insert函数一旦重入,不会出现问题,该函数被称为可重入函数
- 我们学到的大部分函数,STL、boost库中的函数,大部分都是不可重入的
6. volatile
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int signo)
flag = 1;
printf("flag has changed to 1\\n");
int main()
signal(2, handler);
while (!flag);
printf("exit success\\n");
return 0;
mytest:test.c
gcc $^ -o $@ -O3
.PHONYE:clean
clean:
rm -f mytest
加上O3优化之后,如果没有使用volatile修饰flag,会出现如下情况,发送2号信号时,flag的值好像并没有被改变
加上volatile之后
加上优化之后,编译器发现在main函数中并没有对flag进行修改,因此将其放到了寄存器中以提高程序执行效率,但这就相当于在CPU与内存之间建立了一道屏障,handler中修改flag的值是修改的内存中的,而在main函数中,while循环读取的是寄存器中的flag值,这就造成了上面的现象
volatile的作用就是告诉编译器,不要对我这个变量做任何的优化,必需贯穿式的读取内存,不要读取中间缓冲区寄存器中的数据,即保存内存的可见性
7. SIGCHLD
子进程退出的时候会向父进程发送SIGCHLD信号,也就是说收到了SIGCHLD信号就知道子进程就退出了
显式设置忽略子进程的SIGCHLD,子进程退出就不会变成僵尸进程,也就不需要我们去进程等待
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int main()
// 显式设置忽略17号信号,当进程退出后自动释放僵尸进程,也就不需要我们去wait,如果我们不需要获取退出码的话
// 只在Linux下有效
signal(SIGCHLD, SIG_IGN);
pid_t id = fork();
if (id == 0)
// child
int cnt = 5;
while(cnt)
printf("I am child process: %d\\n", getpid());
sleep(1);
cnt--;
exit(0);
// parent
while(1);
return 0;
以上是关于Linux操作系统进程信号的主要内容,如果未能解决你的问题,请参考以下文章