5.信号概述

什么是信号?

生活中有很多信号,比如交通信号灯的红灯亮了,或绿灯亮了。交通信号灯本身不是信号,它是信号的来源。“红”、“绿”才是信号。另外,信号本身并不包括行为。通常来说,一个人看到红灯信号的默认行为是停止,但我们也无法阻止一个法外之徒闯红灯。看到了红灯信号,行和停取决于人的反应。Unix/Linux中的进程接收到一个信号后,也有一个默认的行为,那就是死。

在Unix/Linux系统中,信号(signal)是通知进程发生了异步事件的一种机制。在Unix/Linux系统中,信号用一个整数表示,为了便于记忆,每个整数也有相应的宏定义:如2号信号的宏定义为SIGINT(signal of interrupt)。信号是一种软中断,当一个进程收到了信号后,一般它有三个选择:

  1. 默认处理方式:进程终止
  2. 忽略
  3. 调用一个自定义信号处理函数。

信号来源于内核,生成信号的请求来自三个地方:

  1. 用户终端:比如“ctrl+c”
  2. 内核:当进程执行出错时,内核给进程发送一个信号,比如:浮点数溢出、非法段读取等
  3. 进程:一个进程可以通过系统调用kill给其他进程发送信号,进程间可以通过信号通信。

信号可以用于:(1)处理异常,(2)协调进程的执行。

信号可以用来杀死进程

由于进程对信号的默认反应就是死,因此信号可以用来杀死进程。

当我们在前台运行一个进程,但希望终止其运行时,可以在终端按下ctrl+c。按下ctrl+c时,就通过终端是向该进程发送了一个SIGINT,即2号信号。

我们用stty -a查看一下终端设置,^表示ctrl键。

image-20211203192108914

有一个命令专门用来发送信号给进程,它就是kill。kill的基本用法是:

kill -signum pid

除了kill,如果系统通过程序名杀死多个进程,可以使用killall命令。例如,想杀死所有的vim进程,可以用

killall -9 vim

发送信号的系统调用也是kill。我们可以通过man 2 kill查看一下该系统调用。

kill系统调用简单易懂。很显然,kill命令是用kill系统调用编写的。

查看所有的信号列表

可以使用kill -l查看所有的信号。

image-20211203200214147

可以用man 7 signal查看关于信号的详细知识。

查看/usr/include/bits/signum.h文件

C语言系统调用和库函数的头文件都存放于/etc/include/下面。关于信号数的文件为/usr/include/bits/signum.h,可以用vi编辑器直接打开。

信号处理

一个进程对某一个信号的处理有三种方式:

  1. 默认:进程消亡;
  2. 忽略:就像没有收到信号一样;
  3. 用信号处理函数进行处理。

信号对于进程来说,是一种软中断——对于进程来说,软中断与终端类似,即无论当前进程在执行什么指令,软中断都会使得当前进程暂停当前指令,而不得不去处理该信号。也正因为不知道信号是否到达、何时到达,因而对信号如何处理,一般要在程序开始就加以声明为好。

对某一信号处理方式的声明,使用signal系统调用完成。先用man 2 signal查看下文档。

通过signal的文档(man 7 signal/usr/include/bits/signum.h也同样写了)我们可知,SIGKILL(9号信号)SIGSTOP(19号信号)是不可以被捕捉和忽略的——目的是防止出现杀不死的进程。

例1:忽略2号、3号信号,试图忽略9号信号。

#include <stdio.h>
#include <signal.h>

int main(){
	signal(2, SIG_IGN);
	signal(3, SIG_IGN);
	signal(9, SIG_IGN);

	while(1){
		printf("You cannot kill me\n");
		sleep(1);
	}
}
image-20211203202110261

这是一个死循环程序,我们试图用ctrl+c杀死该进程,但是没有任何效果,因为该进程已设置忽略SIGINT。因此我们通过ctrl+z挂起该进程,之后通过9号信号(SIGKILL)杀死了该进程。我们注意到,尽管我们在程序中设置了对9号信号忽略,但仍不能忽略9号信号——因为9号和15号信号不可被捕捉和忽略

例2:

#include <stdio.h>
#include <signal.h>

void f(int signum){
	printf("%d comes. Ouch!\n", signum);
}

main(){
	signal(2, f);
	signal(3, f);

	while(1){
		printf("You cannot kill me\n");
		sleep(1);
	}
}

我们通过在终端输入“ctrl+c”和“ctrl+\”向进程发送2号和3号信号,进程成功地将其捕捉,并调用信号处理函数进行处理。因为进程会“实时地“处理信号——因此我们称之为软中断。信号处理函数的参数可以获得调用该函数的来源信号是谁,因此我们可以用同一个信号处理函数处理不同的信号。

sleep()函数是如何实现的?

sleep()是我们在编写示例程序时常用的库函数.通过观察易于发现进程在sleep()后cpu使用率为0,因而其一定不是自旋锁。它的神奇之处在于能挂起n秒后能继续接着执行。因而必定有其他机制将其唤醒。

sleep()的本质与我们每晚睡眠前设定闹钟,待第二日清晨闹钟产生闹铃信号,唤醒我们没有什么不同。示例代码演示了sleep()的本质。

#include <stdio.h>
#include <signal.h>

void f(int signum){
}

main(){
    signal(SIGALRM, f);
    printf("about to sleep 5 seconds\n");
    alarm(5);
    pause();
    
    printf("wake up\n");
}

每个进程都有一个私有计时器,可以设定其在一定秒数之后产生一个SIGALRM信号。当然,如果没有设定对SIGALRM信号的处理,该信号会杀死进程。系统调用pause会挂起一个进程直到有一个信号被处理。

间隔计时器

计时器系统是基于信号的,是软中断。借助计时器系统,能实现类似于“多线程”的效果,只不过计时器系统出现得更早,实时性更高。alarm()只是一个秒级的计时器,在Unix很早就有,很多情况下精度不够。后来Unix引入了一个更复杂、更高精度的计时器系统——间隔计时器(interval timer)。

首先,间隔计时器提供了3种可选的计时器,分别为ITIMER_REALITIMER_VIRTUAL,和ITIMER_PROF

  • ITIMER_REAL(真实)跟我们正常的时钟一样,是基于客观的、人类直觉的时间,因而也是最经常使用的。
  • ITIMER_VIRTUAL (虚拟)是进程在用户态运行时才计时的时间。有点像篮球比赛中裁判的那个表——明明比赛一分钟就要结束,结果半个小时也没打完。这里的一分钟就是虚拟时间,而半个小时是真实时间。
  • ITIMER_PROF在进程运行于用户态或由于系统调用进入内核态时都会进行计时。

其次,间隔计时器提供了初始时间和间隔时间,且每个时间都是双精度的(秒和微秒)。

man 2 setitimer写得非常清楚。

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>

void f(int signum){
    printf("Alarm!\n");
}

main(){
    struct itimerval time;
    signal(SIGALRM, f);

    time.it_value.tv_sec=3;
    time.it_value.tv_usec=500000;
    time.it_interval.tv_sec=0;
    time.it_interval.tv_usec=500000;

    setitimer(ITIMER_REAL, &time, NULL);

    while(1){
        printf("haha\n");
        usleep(100000);
    }
}

while循环会每0.1秒打印一个“haha”。当程序运行3.5秒后,间隔计时器产生第一个SIGALRM信号(信号处理导致打印出一个“Alarm”),之后每0.5秒会产生一个SIGALRM信号(打印出一个“Alarm”)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值