实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)
进程可以选择阻塞 (Block )某个信号
本质是操作系统允许进程暂时屏蔽指定的信号
该信号依旧是未决的
该信号不会被递达,直到解除阻塞,方可递达
递达中的忽略 跟 阻塞 有啥区别
- 忽略是递达的一种方式
- 阻塞是没有被递达,是一个独立的状态
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
SIG_DFL:默认 SIG_IGN:忽略
默认动作其实就是被强转的0,忽略动作为1
pending位图保存的是已经收到但还没有被递达的信号,会在合适的时机执行handler中的方法,即完成递达
block表本质上也是位图结构——uint32_t block;
,比特位的位置代表信号的编号,比特位的内容代表信号是否被阻塞
block位图称作阻塞位图,也叫信号屏蔽字,表示哪些信号不应该被递达,知道解除阻塞
handler为函数指针数组,每个信号的编号就是该数组的下标
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
虽然sigset_t是一个位图结构,但是不同的操作系统实现是不一样的,不能让用户直接修改该变量,应该使用特定的函数
sigset_t声明的变量同样也保存在用户栈上,与之前的int、double没有任何区别
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
#include
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);
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(block表)
set:输入型参数,传入你想设置成的位图
oldset:输出型参数,返回老的block位图
how | 作用 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
9号信号被称为管理员信号,不能被屏蔽也不能被自定义捕捉,必须永远遵守默认行为
不会对pending位图做修改,而是单纯的获取进程的pending位图,修改只由操作系统完成
set:输出型参数,pending位图
#include
#include
#include
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;
}
信号是被保存在进程的PCB中,pending位图里的,对其的处理分为:检测、递达(默认、忽略、自定义)
当进程从内核态返回到用户态的时候,进行上面的检测并处理工作
内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS代码的执行全都是在内核态
用户态:就是用户代码和数据被访问或者执行的时候所处的状态,我们自己写的代码全都是在用户态执行的
主要区别:主要在于权限
感性的理解
用户调用系统函数的的时候,除了进入函数,身份也会发生变化,用户身份变成了内核身份
理性的认识
用户的身份是以进程为代表的
用户的数据和代码一定要被加载到内存,OS的数据和代码也一定要被加载到内存中的,这个工作由页表来完成
地址空间中的用户空间由用户级页表负责映射到物理内存当中,不同进程对应的用户级页表各不相同,而内核空间由系统级页表——内核页表映射到物理内存当中,这个内核页表被所有进程共享
进程具有了地址空间是能够看到用户和内核的所有内容的,但不一定能访问
CPU内有寄存器保存了当前进程的状态
用户态使用的是用户级页表,只能访问用户数据和代码
内核态使用的是内核级页表,只能访问内核级的数据和代码
进程之间无论如何切换,我们能够保障一定能找到同一个OS,因为每个进程都有3~4G的地址空间,而且使用同一张内核页表
所谓的系统调用就是进程的身份转化成为内核,然后根据内核页表找到系统函数,执行就可以了
在大部分情况下,实际OS都是可以在进程的上下文中直接运行的
默认和忽略都不需要再从内核态转到用户态,直接在内核态就可以完成
可以抽象为
内核为什么不直接执行用户的代码,而要切换到用户态来执行
因为操作系统不相信任何人,操作系统身份特殊,不能直接执行用户的代码(OS权限太大)
typedef void (*sighandler_t)(int)
:为一个函数指针
signum
:信号对应的整型值
handler
:修改进程对信号的默认处理动作
修改的是handler函数指针数组
act:输入型参数
oact:输出型参数,待会老的信号处理方法,不需要置NULL
sigaction结构体
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,可以暂时将sa_flags设为0,sa_sigaction是实时信号的处理函数
在信号捕捉的时候再次插入了节点,这是就会导致有两个节点被同时插入了,head头指针只能指向一个节点,另一个节点就造成了内存泄漏
#include
#include
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的作用就是告诉编译器,不要对我这个变量做任何的优化,必需贯穿式的读取内存,不要读取中间缓冲区寄存器中的数据,即保存内存的可见性
子进程退出的时候会向父进程发送SIGCHLD信号,也就是说收到了SIGCHLD信号就知道子进程就退出了
显式设置忽略子进程的SIGCHLD,子进程退出就不会变成僵尸进程,也就不需要我们去进程等待
#include
#include
#include
#include
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;
}