当我们在终端运行这样的程序时
#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号信号时,会一直处于未决状态,而其他信号没有被阻塞,可以递达