Linux信号

来源:互联网 发布:上交所网络投票系统 编辑:程序博客网 时间:2024/06/05 08:21

当我们在终端运行这样的程序时

#include<stdio.h>int main(){    int count = 0;    while(1)    {    sleep(1);    printf("%d\n", count++);    }}

我们可以看到每隔一秒会将count++然后输出到显示器,但是这个时候要结束掉这个进程该怎样做呢?很简单,我们可以使用Ctrl+c组合键来直接结束掉这个进程。

那么为什么这样做会让进程直接结束运行呢?

当按下Ctrl+c时,会产生一个硬件中断,这时cpu如果在执行这个代码,则会暂停执行,cpu由用户态切换到内核态处理这个中断。这时操作系统会将这个中断解释成一个SIGINT信号,记录在进程的pcb中的特定位置,当从内核态返回到用户态继续执行进程时,会处理进程中记录的信号,而这个信号的默认动作是终止进程,进而进程被终止运行。


综上我们可以得到这样的结论:

1.进程可以接收到信号,并有特定的位置保存信号。

2.对于不同的信号都有自己的默认处理方式,所以说进程尽管没有收到信号时,但是他仍然知道遇到信号时该怎样去处理。

3.进程在收到信号时,先将信号保存,并不是立刻就去进行处理,而是等到合适的时间在去处理。


注意:

虽然说是给进程发送信号,但是期间的实际过程是操作系统自己在进程的pcb中的特定位置写入。


信号的种类:

可以使用 kill -l 查看系统中所有的信号

注意:没有32和33号信号
前32个信号是一般信号,后32个信号是实时信号。

信号的产生:
1.用户在终端按下某些键时,终端程序会发送信号给前台进程。
注意:这样发送只能给前台进程发送信号,不能给后台进程发送信号。

2.硬件异常产生信号,这些条件有硬件检测到并通知操作系统,然后操作系统向当前进程发送适当的信号。

3.通过调用系统函数向进程发送信号
#include<signal.h>
int kill(pid_t pid, int signo);
函数说明:
可以发送signo信号给进程号为pid的进程,kill指令就是通过调用kill()函数实现的。
int raise(int signo);
这两个函数成功返回0,失败返回-1。
函数说明:
raise()可以给当前进程发送signo信号,也就是自己给自己发送信号。
#include<stdlib.h>
void abort(void);
调用这个函数,进程将给自发送一个信号SIGABRT,而这个信号的默认动作就是结束进程,这个信号虽然可以捕捉但是进程仍然会退出。

4.由软件条件产生
SIGPIPE是一种由软件条件产生的信号,关闭管道的读端时,写端进程在往管道里写入时进程就会收到这个信号。
alarm函数也会产生一个同样性质的信号,SIGALRM信号。
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,在seconds秒后会给当前进程发送SIGALRM信号,该信号默认动作是终止当前进程。这个函数的返回值是0或者是以前是定闹钟时间还剩下的秒数。

模拟实现kill指令
#include<stdio.h>#include<signal.h>void usage(const char *arr){    printf("usage : %s sig pid", arr);}int main(int argc, char *argv[]){    if(argc !=  3)    {    usage(argv[0]);    return -1;    }    //获取到pid    int pid = atoi(argv[2]);    //获取到要发送的信号    int sig = atoi(argv[1]);    //调用kill函数实现    kill(pid, sig);    return 0;}

信号的处理:
当一个进程收到信号时会有三种处理方式:
1.忽略该信号
2.执行默认动作,每一个信号都对应了自己默认的动作
3.自定义捕捉信号,大部分信号都可以进行捕捉,但是有个别信号不能对其捕捉
信号的捕捉函数:
参数描述;
signum:标识要捕捉的信号
handler:用来替代默认动作的函数

信号在内核中的储存:
要了解信号是如何在内存中存储的,先来了解几个概念:
信号递达(Delivery):实际执行信号的处理动作。
信号未决(Pending):信号在产生到递达之间的状态。
信号阻塞(Block):信号可以被阻塞,一旦有信号被阻塞,那么该信号就会一直处于未决状态,直到该信号被解除阻塞,才会执行递达的动作。
在pdb中信号会对应三张表,Block、pending和Handler,Block用来标记对应信号是否被阻塞,pending标记对应信号是否被进程接收或者递达,Handler用来存储对应信号的执行操作。
在pcb中会有对应的位置存储各信号的状态,当信号被写入时会将pending表中对应的位置标记为1,当信号递达时pending表中对应的位置就会被清0,如果信号被阻塞block表的对应位置就会标记为1。

注意:Block和Pending底层是用位图来实现的。

信号集操作函数:
#include<signal.h>
信号集的初始化:
//将所有信号对应的bit清0,表示该信号集不包含任何有效的信号
int sigemptyset(sigset_t *set);
//将所有信号对应的bit置1,表示该信号集的有效信号包括系统支持的所有信号。
int sigfillset(sigset_t *set);

//信号的添加
int sigaddset(sigset_t *set, int signo);
//信号的删除
int sigdelset(sigset_t *set, int signo);
//判断信号集的有效信号中是否包含某种信号,若包含返回1,不包含返回0
int sigismember(const sigset_t *set, int signo);

读取、更改进程的信号屏蔽字(阻塞信号集
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:成功返回0,出错返回-1
how:
SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号mask=mask&~set
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask = set

读取当前进程的未决信号集
int sigpending(sigset_t *set);
将进程的未决信号集通过set参数传出。成功返回,出错返回-1。

使用上面的信号集操作函数,实现,将2号信号阻塞,然后每隔一秒打印每个信号的未决状态,在向进程发送2号信号,会发现该信号一直处于未决状态。
#include<stdio.h>#include<signal.h>#include<stdlib.h>void showpending(sigset_t *pending){    int i = 1;    for(; i <= 31; i++)    {    //如果信号集中的有效信号包含信号i,打印1,否则打印0    if(sigismember(pending, i))    {        printf("1");    }    else    {        printf("0");    }    }    printf("\n");}int main(){    //定义信号集    sigset_t set, oset;    //初始化信号集    sigemptyset(&set);    sigemptyset(&oset);    //将2号信号加进信号集set中    sigaddset(&set, 2);    //将mask设置为set指向的值,用oset接收之前信号集的内容    //因为sigprocmask的属性为SIG_SETMASK所以会将添加set中的所有信号阻塞    sigprocmask(SIG_SETMASK, &set, &oset);    //程序运行到这里时,当进程收到2号信号时不会递达,因为2号信号被阻塞    sigset_t pending;    while(1)    {    sleep(1);    sigpending(&pending);    showpending(&pending);    }    return 0;}
运行结果:
因为阻塞了2号信号,所以当发送2号信号时,会一直处于未决状态,而其他信号没有被阻塞,可以递达