目录
1:信号保存概念
2:sigset_t
3:信号集操作函数
3.1:sigprocmask
3.2:9号进程不能被阻塞(验证)
3.3:sigpending
4:信号处理
5:sigaction
6:可重入函数
7:volatile
8:SIGCHLD
上一节我们谈了信号的产生,这一节课我们谈信号的保存和处理。
1:信号保存概念
- 信号的处理动作称作信号递达
- 信号从产生到递达之间的状态称作信号未决
- 进程可以选择阻塞(Block)某个信号
- 被阻塞的信号将保持信号未决状态,直到进程解决该信号的阻塞,才执行递达
- 阻塞和忽略是不同的,忽略是递达后的动作,而阻塞了就不会递达
Linux中PCB有以下结构
右边的3个数据结构都是位图
- block位图中下标表示哪个信号,0或者1表示信号有无阻塞
- pending位图中下标表示哪个信号,0或者1表示信号有无被OS接收到
- handler位图中下标表示哪个信号,数组元素对应信号处理动作的函数地址。
2:sigset_t
从上图看,信号阻塞和未决的标志都是0或者1,而不记录阻塞了多少次接收了多少,因此可以用相同的数据类型保存,sigset_t称作信号集。阻塞信号集也叫做当前进程的信号屏蔽字,而屏蔽是阻塞,不是忽略。信号集是一个类似位图结构
3:信号集操作函数
#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表示初始化信号集。
- sigfillset表示初始化信号集并将每一个信号置位1,表示该信号集的有效信号包括操作系统所有的信号。
- sigaddset表示向信号集中添加某个有效信号。
- delset表示向信号集中删除某个有效信号。
- sigismember是判断信号signo是否在信号集set中。
- 前4个函数成功返回0,失败返回-1。
- 最后一个函数包含返回1,不包含返回0,失败返回-1。
3.1:sigprocmask
用途:读取或更改当前进程的信号屏蔽字
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
set与oset情况:
- set不为空:修改当前进程的信号屏蔽字
- oset不为空:获取进程被修改前的信号屏蔽字
- 都不为空:修改当前进程的信号屏蔽字并且获取之前进程的信号屏蔽字
how参数有3个:
SIG_BLOCK | set表示希望添加到当前进程的信号屏蔽字,相当于mask=mask|set |
SIG_UNBLOCK | set表示希望希望从当前进程解除的信号屏蔽字,相当于mask=mask&~set |
SIG_SETMASK | 设置当前屏蔽字为set值,相当于mask=set |
注意:如果sigprocmask解除了若干个未决信号的阻塞,则在procmask返回之前,至少有一个信号递达。信号什么时候被处理?在下面第四大点会说。如果某进程在sigprocmask之前阻塞了2号信号,如果调用procmask解除2号信号阻塞,那么进程会立马接收到2号信号,并在合适的时候(内核态到用户态)执行处理动作。
3.2:9号进程不能被屏蔽(验证)
#include<signal.h>
#include<unistd.h>
#include<iostream>
using namespace std;
int main()
{
sigset_t s;
sigemptyset(&s);
sigaddset(&s,2);
sigaddset(&s,9);
sigprocmask(SIG_BLOCK,&s,nullptr);
while(1)
{
sleep(1);
cout<<"我是一个进程 pid:"<<getpid()<<endl;
}
return 0;
}
通过键盘发送2号信号是没用的,而通过kill -9 5553则可以。
这证明9号信号无法被阻塞。
3.3:sigpending
#include <signal.h>
int sigpending(sigset_t* set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
表示将pending位图输入进set中,该参数不能用于向进程发送信号。下面来演示一下:
void myhandle(int signo)
{
cout<<"成功解除二号信号屏蔽" <<endl;
exit(0);
}
void printpend(sigset_t* p)
{
for(int i = 0;i<=31;++i)
{
if(sigismember(p,i))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
sigset_t s,p;
signal(2,myhandle);
sigemptyset(&s);
sigaddset(&s,SIGINT);//2号信号
sigprocmask(SIG_BLOCK,&s,nullptr);
int cnt = 0;
while(true)
{
++cnt;
cout<<"我是一个进程 我即将打印位图 pid:"<<getpid()<<endl;
sigpending(&p);
printpend(&p);
sleep(1);
if(cnt == 10)
{
cout<<"我即将解除2号信号屏蔽"<<endl;
sigprocmask(SIG_UNBLOCK,&s,nullptr);
}
}
return 0;
}
只有block位图中是0,pending为1,信号才有可能递达。
可以看到发送2号信号的时候,pending位图的2号信号下标0变1,表示接收到2号信号,但是由于sigprocmask屏蔽了2号信号,所以没有递达,因此进程暂时没有执行2号信号的处理动作(成功解除二号信号屏蔽并且退出进程),当cnt等于10了解除屏蔽,信号递达,执行自定义动作。
4:信号处理
信号是什么时候被接收并且处理的呢?
直接下结论:从内核态切换成用户态的时候。
内核态:执行操作系统代码的时候,计算机所处的状态。
用户态:执行用户代码的时候,计算机所处的状态。
之所以区分状态,是因为如果只有用户态,随心所欲的访问,会造成严重问题!
所以OS提供的所有系统调用,会在执行的时候更改执行级别。
我们都知道程序运行起来,操作系统会创建该进程的pcb,然后会通过页表页框的映射将虚拟地址空间映射至物理内存中,那么操作系统的代码和数据会不会load到内存呢?
答案是肯定的,我们在windows下开机所等待的时间就是在等待系统服务的loading。
答案知晓:用户代码和OS代码数据都会load到内存中。
在X86环境下,0-3G为用户态空间,3-4G为内核级空间,而对于不同的进程来说,3-4G这一部分内容是相同的,因此所有进程都可以通过进程地址空间看到相同的一份OS,因此OS运行的本质就是在地址空间上运行的!所以,所谓的系统调用就是在进程的地址空间中进行函数跳转返回即可!
所以进程是如何被调度的?
OS的本质是一个死循环,其中有时钟硬件,每隔很短的时间就会发送时钟中断 ,OS就要执行对应的中断处理方法,然后检测进程的时间片,执行系统调用,这是因为中断进入内核态。
当进程被调度的时候,就是时间片到了,操作系统会保存进程的上下文并且切换,选择合适的进程,然后执行某条语句的系统调用。
那么用户态和内核态具体切换是什么样子的呢?我们可以画一个无穷符号
与横线的交点就是切换状态的时候。
而无穷的正中心的点表示检查pending表的时候。而且是在内核态检查pending
如果信号动作是默认的,就不会切回用户态了。
可以不进行切换状态,直接在内核态操作吗?
答案是可以但不可行,因为用户如果执行非法操作,比如删库,那就完蛋了。
5:sigaction
除了上一章提到的signal可以捕捉信号外,sigaction也是可以。
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
成功返回0,失败返回-1
参数:
- signo是信号编号
- act非空,则将signo的处理动作更改为act
- oact非空,则通过oact传出修改之前的动作
struct sigaction是一个结构体变量,定义如下:
struct sigaction {
void(*sa_handler)(int);
void(*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void(*sa_restorer)(void);
};
第一个变量sa_handler有3种设置方式:
- SIG_IGN,表示动作为忽略
- SIG_DFL,表示默认处理动作
- 传用户自定义的动作函数指针,表示执行自定义动作
第二个变量不用理会,置空即可。是实时信号的处理函数。
第三个变量:sa_mask
这是一个信号屏蔽字。
当某个信号的处理函数被调用的时候,内核自动将该信号加入该进程的信号屏蔽字中,当信号处理函数返回时自动恢复原来的信号屏蔽字(不包含该信号或者自定义了其他一起被屏蔽的信号时候的屏蔽字),这就保证了在处理一个信号的时候,当该信号再次产生,那么会一直阻塞直到处理动作结束。如果想要当前信号被屏蔽以外,还需要其他信号也被屏蔽,可以修改sa_mask字段。
第四个变量:sa_flags包含一些选项,给0即可。
第五个变量:sa_restorer不使用他
下面展示一下:第三点是什么意思
void printpend(sigset_t* p)
{
for(int i = 1;i<=31;++i)
{
if(sigismember(p,i))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
void myhandler(int signo)
{
cout<<"我执行"<<signo<<"号信号的动作"<<endl;
int cnt = 30;
while(cnt--)
{
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
printpend(&pending);
sleep(1);
}
}
int main()
{
struct sigaction act,oact;
memset(&act,0,sizeof(act));
memset(&oact,0,sizeof(oact));
act.sa_handler = myhandler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
sigaction(2,&act,&oact);
while(true)
{
cout<<"我是一个进程 pid:"<<getpid()<<endl;
sleep(1);
}
}
我们将345信号屏蔽字都添加进sa_mask中,自定义2号信号动作,并且同时发送345信号,根据3的说法,在执行某个动作的时候,会阻塞sa_mask和目前执行动作对应的信号,如图所示,果真如此:
6:可重入函数
这样一个函数,当调用insert(&node1)的时候,p->next=head;执行完发送信号,执行信号动作。这个时候可以具象化一下:
在执行完毕自定义动作后是这样:
这个时候切换回内核态,再执行上次中断处代码,也就是head=p,这个时候是这样:
如此以来就造成了内存泄漏,node2不知道怎么访问了。
在这种第一次调用还没返回时候再一次进入该函数称为重入。
如果一个函数满足以下条件则可以成为不可重入:
-
调用了 malloc 或 free, 因为 malloc 也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
7:volatile
作用是保持内存可见性
int flag = 0;
void myhandle(int signo)
{
cout<<"捕捉到信号"<<signo<endl;
flag = 1;
}
int main()
{
signal(2.myhandle);
while(!flag);
cout<<"进程退出正常"<<endl;
return 0;
}
理想情况是ctrl+c后,进程正常退出。
确实是这样,但是如果加上编译器优化呢?-o 表示优化级别
这个时候就算捕捉到信号,flag变了1,while也仍在死循环?为什么?
不优化前,cpu可以在寄存器中读取,而i =1则保存在寄存器中,而这样的一个死循环,也没有干什么事情,flag却在被高频访问,就需要不停的将寄存器中的内容load到cpu里,效率差。优化后就是告诉cpu只去内存中访问,不去访问寄存器,因此就变成图上所示,flag永远是0。
解决办法:
volatile int flag = 0;
8:SIGCHLD
进程讲过对于一个僵尸子进程,父进程既可以阻塞的等,也可以非阻塞的轮询(WNOHANG),采用第一种方式父进程就不能做自己的事情了,采用第二种方式,父进程就要在做自己工作的时候不断的询问子进程,程序实现复杂。
有没有办法让子进程退出成为僵尸进程的时候告诉一下父进程,父进程来进行处理呢?答案是有的,子进程退出的时候会向父进程发送SIGCHLD信号,因此基于这样一个原理我们可以写一个例子:
pid_t id;
void myhandle(int signo)
{
printf("捕捉到信号 %d,who:%d \n",signo,getpid());
sleep(2);
while(1)
{
pid_t res = waitpid(-1,nullptr,WNOHANG);
if(res>0)
{
printf("wait success,res: %d id: %d\n",res,id);
}
else break;
}
cout<<"wait done"<<endl;
}
int main()
{
signal(SIGCHLD,myhandle);
id = fork();
if(id == 0)
{
int cnt = 3;
//子进程
while(cnt--)
{
cout<<"我是子进程 pid是"<<getpid()<<" ppid是"<<getppid()<<endl;
sleep(1);
}
exit(-1);
}
//父进程
while(true)
{
cout<<"我是父进程 我在做自己的事情"<<endl;
sleep(1);
}
}
退出后发送信号,这个时候父进程再去等待。
如果创建了多个子进程,怎么办?
创建多个子进程,因为位图不会记录多少个信号,只会记录有无,所以只会释放一个子进程。所以可以像我上面这样设置一个while循环,不断的去等待,只要有子进程就等待,这样就可以释放僵尸进程了。