信号
什么是信号?
在生活中,我们可以看到交通信号,火警信号,急救信号,这些都是信号,通知我们做出相应的反映,在操作系统中,我们也有信号这一个机制,来确保进程的合理运行,当一个程序运行起来的时候,我们在键盘上按一个ctrl+c,进程就会停下来,这实际上就是给进程发了一个信号,cpu从用户态切入内核态,来处理这个硬件中断,把它理解成一个SIGINT的信号记录在当前pcb下,然后当某个时候需要从内核切回用户态的时候,会处理当前pcb中的信号,发现有一个SIGINT的信号存在,就会终止当前进程。linux下的各种信号
我们可以用kill -l 命令来查看linux中的信号列表
其中,1-31号为普通信号,34到64是实时信号。我们这里只讨论普通信号。- ctrl+c (2号SIGINT信号),ctrl +\(3号SIGQUIT信号),ctrl+z(20SIGTSTP信号),kell-9(9号 SIGKILL信号),闹钟alarm超时(14号SIGALRM信号),除0错误(8号SIGFPE信号),访问非法内存(11号SIGSEGV信号),向读端关闭的管道中写(13号SIGPIPE信号)
信号的处理方式
- 执行默认动作,一般以上都是终止该进程。
- 忽略此信号
- 用户自定义捕捉该信号,执行捕捉函数。
Core Dump
什么是core dump,当一个进程异常终止的时候,可以把用户进程的内存信息全部保存到磁盘上,文件名通常是core,方便我们利用这个文件来调试程序,找出bug。一般默认是不允许产生core dump文件的,因为我们的程序可能包含一写重要信息,但是我们在实际的开发当中,还是可以运行操作系统产生core dump文件的,为了方便我们进行调试。
我们可以用ulimit -a来查看系统的core dump信息,我们可以看出 现在已经禁止生成core dump文件了,文件大小为0,我们可以用ulimit -c 1024 ,一般设置为4k,来重置。我们可以用gdb来对这个文件进行调试。
系统接口
kill和abort
- 我们可以kill命令来向指定进程发信号,当然我们也可以利用系统调用函数来发送命令。
int kill(pid_t pid,int signal)向指定进程发信号 int raise(int signal)向自己发信号
alarm
unsigned int alarm(unsigned int seconds)
调用这个函数的作用是,经过seconds秒之后,操作系统向进程发出一个SIGALRM信号,这个信号的默认终止动作是终止进程。这个函数返回0或者以前设定闹钟还剩下的秒数,如果second为0,表示取消掉闹钟。
信号的阻塞
- 执行一个信号的处理动作叫做信号的递达。
- 一个信号从产生到递达之间的过程叫做未决。
- 当然进程可以选择阻塞某个信号的递达。
- 被阻塞的信号一直处于未决状态,知道进程解除对这个信号的阻塞,才可以被递达
- 注意信号的阻塞和忽略是两个不同的概念,忽略是信号递达之后执行的另一种操作。
如何理解操作系统向进程发信号
每一个进程都有一个PCB来维护它,而PCB中又有信号的位图,而操作系统向进程发信号,实际上就是操作系统在修改目标进程的PCB中的信号位图中的对应比特位,只要将0该为1,就表示该进程接收到了这个比特位所对应的信号。当然,
- pending表表示信号是否处于未决状态,block表表示该信号是否阻塞,hander表表示进程的处理动作。我们可以把hander表形象的理解成一个函数指针数组。里面存放着每一个信号的处理函数。
- 根据上表可以看出,SIGHUP信号没有处于未决状态,同时也没有被阻塞,它执行的处理动作是默认处理动作。SIGINT信号处于未决状态,这个信号被阻塞着,它执行的操作是忽略,SIGQUIT没有位于未决状态,但是它被阻塞着,它执行的操作是用户自定义的sighanler方法。
信号集操作函数
- sigset_t 系统通过sigset_t这个类型来标记信号的未决和阻塞状态,我们称之为信号集,我们把阻塞信号集称之为信号屏蔽字。这个变量只能操纵以下函数来调用,任何尝试打印它或其他操作都是无意义的。
- 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
int sigprocmask(int how,const sigset_t* sig,sigset_t *osig);
how参数
sig如果非空,则表示根据how参数来更改当前进程的信号屏蔽字,osig非空,表示的是以前旧的信号屏蔽字,通过osig带出。
sigpending
int sigpending (sigsset_t* set)
将当前进程的未决信号集通过set带出来
下面我们就来写一个小程序。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void sigprint(sigset_t * old)
{
int i=0;
for(;i<32;i++)
{
if(sigismember(old,i))
{
printf("1");
fflush(stdout);
}
else
{
printf("0");
fflush(stdout);
}
}
printf("\n");
}
int main()
{
sigset_t new, old;
sigemptyset(&new);
sigaddset(&new,SIGINT);
sigprocmask(SIG_BLOCK,&new,NULL);
while(1)
{
sigpending(&old);
sigprint(&old);
sleep(1);
}
return 0;
}
这个小程序就是把我们的2号信号加入当前进程的信号屏蔽字,然后打印进程的信号未决信号集。当我们按ctrl+c的时候无法结束进程,原因是我们把2号信号给阻塞了,但是我们依旧可以用ctrl+\来结束这个进程。
下面我们站在cpu的角度来理解一个信号从产生到递达的过程。
- 当执行控制流的因为某条指令或者硬件中断或者异常从用户态进入内核
- 当执行完异常的处理准备从内核返回用户态时,会检查当前进程的PCB的信号位图,查看是否有可以递达的信号,如果有的话就会对这个信号进行处理,如果处理动作是忽略,那么就返回到用户态接着往下执行,而如果是终止该进程,那么进程就停止。
- 如果信号的处理动作是用户定义的处理动作,则返回到用户态系统定义的处理函数,此时处理函数和main函数使用不同的栈空间,不存在调用被调用的关系,是两个不同的执行流。处理完该信号,再次进入内核态,将PCB中的信号位图中已递达的信号的未决状态修改,然后再次返回到上次被中断的地方接着向下执行。
sigaction
int siaction(int signo,const struct sigaction *act,struct sigaction *oact);
这个函数主要用来进行信号的捕捉,signo表示要捕捉的信号编号,act表如果非空表示act修改该信号的捕捉动作,而oact表示的是信号原来的处理动作。
在这里,我们通常把sa_handler赋值为一个指向我们自定义处理函数的函数指针,sa_flags通常设置为0;如果我们想屏蔽某些信号的话,就用sa_mask字段来说明。
pause
int pause(void)这个函数的作用是挂起当前进程,直到有信号递达,如果信号的处理动作是终止,则进程中止,pause无法返回,如果是忽略,则继续挂起等待,只有当信号处理动作是我们自定义的函数的时候,才回去执行捕捉函数,pause返回-1,errno设置为EINTER,也就是说只有错误的时候才会返回。
下面我们来用alarm和pause来进行实现一个功能和sleep一样的程序。
1 #include<stdio.h>
2 #include<signal.h>
3 #include<unistd.h>
4 void sig_alarm(int signo)
5 {
6 //
7 }
8 int mysleep(int seconds)
9 {
10
11 int unslept=0;
12 struct sigaction new,old;
13 new.sa_handler=sig_alarm;
14 new.sa_flags=0;
15 sigemptyset(&new.sa_mask);
16 sigaction(SIGALRM,&new,&old);
17 alarm(seconds);
18 pause();
19 unslept=alarm(0);
20 sigaction(SIGALRM,&old,NULL);
21 return unslept;
22
23 }
24 int main()
25 {
26 while(1)
27 {
28 mysleep(5);
29 printf("5 seconds passed\n");
30 }
31 return 0;
32 }
这个程序我们设置了一个闹钟,每隔5秒向进程发送一个alarm信号,然后又挂起进程,直到信号递达才会去做我们的SIGALRM捕捉函数,虽然捕捉函数什么也不干。所以每隔五秒钟,程序打印一句话。
可重入函数
我们把函数在不同的控制流程下,重复的进入函数里面,而且修改一些全局属性的时候出现错误的函数就叫做不可重入函数。
相反的,如果一个函数只修改自己的局部变量或者参数,就算重入也不会,造成错误,这样的函数就叫做可重入函数。
符合什么样的特性的函数就是不可重入的
- 调用了malloc或者free,因为malloc也是利用全局链表来管理堆的。
- 调用了标准I/O库函数,因为标准I/O库中很多操作都是以不可重入的方式使用全局数据结构。
volatile
volatile关键字的作用是保证内存的可见性,即每次都到内存中去取数据。为了防止系统在编译时候的优化措施。
竞态条件和sigsuspend函数
现在我们重新思考一下我们上文提到的mysleep程序,设想有没有这样一种场景。
- 注册SIGALRM的信号捕捉函数
- 调用alarm设定闹钟
- 此时内核中有很多优先级比当前进程高的进程在运行,内核一直在调度它们,而且每一个都需要很长时间。把这个进程放在一边。
- 时间到了,内核向这个进程发送SIGALRM信号,使其处于未决状态。
- 内核把这些优先级高的进程调度调度完成了,调用mysleep进程,执行它的信号捕捉函数。
- 返回主控制流程,alarm返回,接着执行pause()挂起等待
- 但是信号的捕捉函数都处理完了,还等待什么呢?
造成这种原因就是因为系统运行的时许不像我们写程序所设想的那样,虽然alarm的下一句就是pause但是,无法保证pause会在alarm之后的设定秒数内执行,所以异步时间在任何时候都有可能发生,因此我们在写程序的时候不考虑周全,就会造成因为时序问题而导致的错误,这就叫做竞态条件。
解决方法 - 如果我们能在做到先屏蔽掉SIGALRM信号,此时就算收到该信号也不会去执行捕捉函数,然后再在接触屏蔽的同时挂起当前进程,就能很好的解决这个问题。
int sigsuspend(const sigset_t* sigset)
和pause一样,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。 调用sigsuspend时,进程的 信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某 个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值, 如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
SIGCHLD
实际上,在子进程退出的时候,会给父进程发送一个SIGCHLD的信号,通知父进程子进程退出,已经子进程的退出信息,该信号的默认处理动作是忽略,我们也可以自定义SIGCHLD的处理函数,这样父进程在创建子进程之后就可以安心的干自己的事情了,不必关心子进程的信息,当子进程退出的时候,父进程在SIGCHLD处理函数中就会对子进程进行回收。
1 #include<stdio.h>
2 #include<signal.h>
3 #include<stdlib.h>
4
5 void handler(int sig)
6 {
7 pid_t id;
8 while((id==waitpid(-1,NULL,WNOHANG))>0)
9 {
10 printf("wait child success %d\n",id);
11
12 }
13 printf("child is quie, id is:%d\n",getpid());
14 }
15 int main()
16 {
17 signal(SIGCHLD,handler);
18 pid_t pid;
19 pid=fork();
20 if(pid<0)
21 {
22 perror("fork");
23 return -1;
24 }
25 else if(pid==0)
26 {
27 //child
28 printf("child ID is:%d\n",getpid());
29 sleep(3);
30 exit(2);
31 }
32 while(1)
33 {
34 printf("father is doing something\n");
35 sleep(1);
36 }
37 return 0;
38 }