操作系统(linux)中信号工作的原理分析
信号
首先我们先理解一下信号是什么?在linux下我们先看看都有那些信号,
我们输入kill -l 就会出现
这就是信号的全部种类,总共有62种信号,其中1到31是普通信号,也是这篇主要理解的,后面34到64的信号为实时信号。
信号是干什么的呢?
我们举个例子:
最简单的理解,在linux下我们在运行某个进程的时候,通常在shell下启动一个前台进程,但是我们进程运行过程中我们,按下Ctrl+c,这个时候我们的进程就会停止或者说结束。其实,这就是信号起了作用,在我们按下Ctrl+c的时候,将会产生硬件中断,从而cpu转到内核去处理中处理,同时终端会将Ctrl+c被解析成一个SIGINT信号,发送给进程PBC,这个时候PCB中的会被记录下一个SIGINT信号,当某个时候cup从内核切回到用户空间时候,所以会处理这个进行中记录的信号,而这个信号的默认处理动作是结束进程。
信号的产生
关于信号的产生有三种情况:
1)键盘按键产生信号
键盘按键产生信号很好理解,比如我按的 Ctrl+c 产生信号SIGINT,按下Ctrl+z 产生SIGSTP 信号,Ctrl+\ 产生SIGOUT信号。这里要说一一下Ctrl+\ 这个产生信号,将程序终止掉后会产生core dump文件。
core dump文件干什么用的?core dump 是在程序运行过程中收到信号,或者在程序异常终止后,系统会把程序在内核中程序终止时候的信息保存到core dump文件中。我们可以用ulimit -c 来进行对core dump文件大小进行调整。在不修改的前提下默认的core dump文件的大小为0。
2)调用系统函数
调用系统函数向进程发送信号,我们看看系统调用函数。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
上面的两个函数,我们来解释一下,
kill函数两个参数,pid为给哪个进程要发信号,signo为哪中信号。
raise函数其实是kill函数的封装,raise函数是自己给自己发送任意信号。
上面两个函数都是成功返回1,失败返回0。
还有个函数,是c语言中的函数
#include <stdlib.h>
void abort(void);
abort函数是给自己发送abort信号。就像exit函数一样,但是abort总会成功。
3)软件产生信号
如果了解进程间的通信,我们就会知道有个匿名管道,如果我们在打开管道,如果打开写端关闭读端,系统会自动的发送一个SIGPIPE信号,关闭管道。
信号的阻塞
信号阻塞我们就需要了解信号在PCB中是怎么样的一种方式来表示它的信号。
在进程中当一个进程收到一个信号的时候,信号其实是先到该进程中的PCB中找到一个表,我们就叫未决信号表,其实是二进制表示的位图,先将 sigset_t 动作叫做信号递达。
我们用张图来理解:
我们要注意
进程可以选择阻塞某个信号
如果在信号未决后,我们设置了阻塞,那么该信号会在未决表中等待解除阻塞
在信号中我们要明白,信号的阻塞和忽略不同,忽略是信号的处理方式,而阻塞只是信号在传递过程中,对信号的一种延后处理的行为。
信号操作函数
在这里我们了解几个信号集操作的函数
#include <signal.h>
int sigemptyset(sigset_t* set); // 将全部的比特为0
int sigfillset(sigset_t* set); // 全部比特为1
int sigaddset(sigset_t* set, int signo); // 在某一为上设置1
int sigdelset(sigset_t* set, int signo); // 某一位设置为0
int sigismember(const sigset_t* set, int signo);//查看是否被设置
这五个函数中,前面四个函数,都是成功返回1,失败返回0.
最后一个函数是一个bool的如果被设置返回1,没有返回0;
信号屏蔽
在看过对信号集操作函数后,我们就来看看信号阻塞函数。
函数不仅仅是可以阻塞信号,还可以读取和修改。
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oset);
返回值是,成功返回0,失败则返回1;
参数说明,how是采用那种方式,set是输入型参数,oset是输出型参数。
这里的how有这么几个要参数:
- SIG_BLOCK 相当于在原有的信号屏蔽字中添加新的屏蔽字。
- SIG_UNBLOCK 解除set的当前信号屏蔽字,只解除set对应的。
- SIG_SETMASK 设置当前屏蔽字,就不用管原来的字是什么,就设置成set
这里我们用一个例子来验证一下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void Handle(int sig) // 当信号捕捉后,就会跳到这个函数中
{
printf("sig = %d\n",sig);
}
void PrintSigSet(sigset_t* set)
{
int i = 1;
for (; i <= 31; ++i)
{
if (sigismember(set,i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
int main()
{
// 捕捉SIGINT信号
signal(SIGINT, Handle); // 捕捉函数
// 再屏蔽SIGINT信号
sigset_t set;
sigemptyset(&set); // 把要设置的set全部bit位设置为0
sigaddset(&set, SIGINT); // 添加要设置的位,SIGINT是2号信号
sigprocmask(SIG_BLOCK, &set, NULL); // 把set设置到信号屏蔽字中
// 循环读取未决信号集
while (1)
{
sigset_t pending_set;
//这个函数是读取未决信号集,且只能读,不能修改,
//因为我们屏蔽了2号信号,所以在未决信号集中会在2号位置出现1
sigpending(&pending_set);
PrintSigSet(&pending_set);
sleep(1);
}
return 0;
}
上面就是我们在用函数 sigprocmask 把信号的屏蔽字中的2号信号SIGINT设置为屏,所以我们在程序运行时候,观察未决信号集表中,当我们按下Ctrl+c时候,未决信号集表中2号位置,会变成1,当时就不会变回去,是因为我们没有解除对二号信号的屏蔽,当我们解除屏蔽,信号才能递达。
我们来看看结果:
信号屏蔽字中对信号的解除
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void Handle(int sig) // 当信号捕捉后,就会跳到这个函数中
{
printf("sig = %d\n",sig);
}
void PrintSigSet(sigset_t* set)
{
int i = 1;
for (; i <= 31; ++i)
{
if (sigismember(set,i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
int main()
{
// 捕捉SIGINT信号
signal(SIGINT, Handle); // 捕捉函数
// 再屏蔽SIGINT信号
sigset_t set; // 定义一个set用来设置信号屏蔽字
sigemptyset(&set); // 把要设置的set全部bit位设置为0
sigaddset(&set, SIGINT); // 添加要设置的位,SIGINT是2号信号
sigprocmask(SIG_BLOCK, &set, NULL); // 把set设置到信号屏蔽字中
// 循环读取未决信号集
int i = 10;
while (1)
{
sigset_t pending_set;
//这个函数是读取未决信号集,且只能读,不能修改,
//因为我们屏蔽了2号信号,所以在未决信号集中会在2号位置出现1
sigpending(&pending_set);
if (i == 0)
{
// 3秒后就会解除信号屏蔽字中的SIGINT,未决表中也会变成0
// 解除信号屏蔽字
sigprocmask(SIG_UNBLOCK, &set, NULL);
--i;
}
PrintSigSet(&pending_set);
sleep(1);
}
return 0;
}
这里我们为了更清楚的演示信号被解除,就是信号未决表中的2号位置的1被置为0,所以我们不然程序立马收到SIGINT信号退出,所以我们用了信号捕捉。
我们来看看上面代码的运行结果:
图中的1,是我们按下Ctrl+c产生SIGINT信号
途中的2,是我们再次按下Ctrl+c,为了验证我们用了信号的捕捉。
图中的3,是我们再10秒后信号解除函数执行,未决表中全为0,因为信号已经被处理。
信号的捕捉
在看完阻塞,我们还需要知道信号的的处理方式,信号的处理方式有三种:
1)忽略
2)默认
3)捕捉
信号的忽略我们可以用
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
其中hangdler是一个函数指针。
只要将handler参数填写成常数SIGIGN,就会忽略相对应的signum信号,但是在信号中会有几个函数是不能被忽略或者不能被捕捉。
我们主要介绍的是函数的捕捉。
函数捕捉,我们用图来了解一下在一个程序执行的过程中收到信号,然后程序自己进行对信号的捕捉的过程。
上图中我们就可以看出,在捕获过程中进行了四次的用户与内核之间的交互。
要注意的是:
子啊主控制流的收到信号、异常、或者中断时候,会把当前进程的阻塞,一直等处理完信号。
SIGCHLD信号
这个信号我们来单独认识一下。
它在linux中用kill -l我们就可以看出来,是一个17号信号,为什么说它呢?
因为在对与父进程与子进程间的进程等待问题。
如果父进程没有读取子进程的返回码,就会产生僵尸进程,造成内存泄漏,所以父进程就需要进程等待。
这个信号会在子进程终止时候,父进程发送一个SIGCHLD信号,但是父进程对该信号默认处理的是忽略的,所以我们改变信号的处理方式为捕捉,在捕捉函数中我们就可以wait子进程的退出码。所以父进程就不必关心子进程会变成僵尸进程。
下面我们就用一段代码来具体实现一下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void handler(int sig)
{
(void)sig;
while (1)
{
// 这里采用轮巡的等待方式是因为,有可能,
// 在触发信号的子进程有很多
// 但是未决信号表只能表现为一次,
// 所以我们采用waitpid轮巡的去等待
pid_t id = waitpid(-1, NULL, WNOHANG);
if (id > 0)
{
printf("wait success\n");
}
if (id < 0)
{
break;
}
}
}
int main()
{
signal(SIGCHLD, handler); // 用来接受信号并对其进行捕捉
pid_t id = fork();
if (id == 0)
{
printf("i am child\n");
sleep(3);
exit(1);
}
while (1)
{
printf("i am father\n");
sleep(1);
}
return 0;
}
我们来看代码的结果:
我们开始创建father和child进程,当子进程3秒后,退出,触发sigchld信号,程序到捕捉函数中,执行完成后,又回到主线程中。
如有错误,多多指导,谢谢!
上一篇: C# WPF闹钟