信号的基本概念

    在Linux环境下,有一重要概念信号(signal),说它重要是因为它在进程管理中占有着相当重要的低位。整个进程之间的管理切换,都是通过信号的方式实现的。首先举个简单例子。

    生活中,当我们过马路时,总是可以看到交通灯,从小耳熟能详的一局童谣“红灯停,绿灯行,黄灯亮了等一等”,遇到红灯,我们都会下意识地等待,绿灯亮起,我们会选择穿过马路,这就是一种信号。暂且不讨论闯红灯的行为。考虑一下,为什么我们会这么做?红绿灯给我们的只有颜色上的信号,我们却做出了一系列的反应,并不是因为红灯有多么大的能力,而是在我们脑海中有这样的一种意识,看到它之后我们就理所当然的要这样。

    信号也是这样,在Linux操作系统中,进程的控制大都需要信号来实现,并不是信号指使进程进行某一项动作,而是进程在信号来临之前就已经知道了,如果遇到某个信号该怎么做。有了这个概念之后,我们从操作系统的角度来看看,信号是如何工作的。

    首先要得到一个信号,这由终端的命令或程序代码可以实现,不是我们讨论的重点,这些命令或函数被执行之后,根据冯诺依曼体系结构,首先输入的信息要传递给内存,这时CPU从原来的用户态切换为内核态,分析解释该信息,得到信号,接下来这些信号会保存在该进程的PCB当中(注意,这个很重要!!!)。等待CPU从内核态转换为用户态,重新处理该进程时,会首先处理PCB中记录的信号,这个和线程切换有点类似,发现待处理的信号,接下来就会去执行它,从而使进程产生相应的行为。

    了解了这些东西之后,我们需要从宏观角度来认识一下信号,信号是一种通知机制,告诉进程某些事情发生了,进程针对信号产生特定的行为。(进程对这些信号会产生的默认行为是已知的)同时信号的产生是异步产生的,完全随机,可能在进程运行过程中的任何时间  

    之前谈进程的时候,说过kill命令,这个后面会用到

kill -l        # 查看当前系统所有可用信号

信号产生的条件

     1、终端组合键,只能产生少量信号,仅适用于前台进程

     2、硬件异常,硬件检测并通知内核

     3、软件方式,指令或函数接口。kill命令(例闹钟超时SIGALRM信号)

    关于条件2,要多说一点的是,硬件异常产生的信号,由操作系统解释,例如除0操作产生SIGFPE,访问非法内存地址;还有就是MMU内存管理单元异常。MMU是用来结合页表进行虚拟地址到物理地址转化,与MMU搭配使用的还有TLB, 做后备缓存,用来缓存映射之后的硬件缓存结果。

    那当进程得到信号之后会怎么处理呢?还是当我们遇到交通灯一样,遇到红灯,每个人都知道要停下来,默认的动作应该是在原地等待,但依旧会有些人闯红灯,同时,有些人没有老老实实地在等,而是在打电话,进程也一样,有三种处理方式:

信号处理方式:

     1、忽略信号

     2、执行信号默认动作

     3、自定义动作, 提供一个信号处理函数,也叫作信号捕捉(catch)【使用signal函数,后面会提到】  

    关于如何去实现,后面会提到。

信号的产生

    

    在Linux下信号的产生主要有三种方式。

    1、终端通过按键组合键产生

        常见的组合键有以下几个:

ctrl+c:SIGINT(2)        终止前台进程ctrl+z:SIGSTOP(19)     停止前台进程,同时将该进程放到后台ctrl+\:SIGQUIT(3)       终止进程并Core Dump

    这里多说一点关于core dump的东西。Core Dump, 即核心转储,当一个进程被异常终止时,可以选择性的将用户空间的内存数据全部保存到磁盘上,默认在当前路径下生成一个文件名为 core.****的文件,****通常为进程ID,在运行结束之后,可以使用gdb进行调试,找到异常所在。默认情况下是不会产生core文件的,原因有两个,一是容易将用户的私人信息也保存到了磁盘上,造成信息的不安全,二就是如果在用户不知情的情况下,产生core文件,会占用磁盘大量空间,因此即使是在实际开发中,core dump的使用也很少见。接下来给出两天命令,关于查看和调整core文件的属性

# 查看系统资源上限:[root@localhost mySemaphore]# ulimit -a# 更改core文件大小上限:[root@localhost mySemaphore]# ulimit -c 1024     # 以block为基本单位     使用gdb调试,-g编译选项         (gdb) core-file core文件

     使用ulimit命令改变了shell的ResourceLimit,而当前进程的PCB有shell复制而来,所以也就具有了和shell相同的Resource Limit值,从而产生了core dump。

core测试代码:

//test.c#include 
#include 
int main(){    int count = 0;    while(1)    {        printf("hello world\n");        if(count == 5){            int c = 3/0;        }        count++;        sleep(1);    }    return 0;}

2、 调用系统函数想进程发送信号

    这里我们会提到三个函数 kill,raise, bort

#include 
#include 
       int kill(pid_t pid, int sig);               # 给一个指定的进程发送指定信号(成功返回0, 失败返回-1)              #include 
     int raise(int sig);             # 给自己发送指定信号,用父进程wait验证(成功返回0, 失败返回-1)             #include 
     void bort(void);               # 使当前进程异常终止,abort函数总是成功的,所以没有返回值

关于kill 函数中的进程ID,和信号,我们可以通过命令行参数的方式获得,代码测试如下:

kill测试代码:

// 终端1// mysignal.c#include 
#include 
int main(int argc, char* argv[]){    if(argc != 3){        printf("Isn't form: ./file pid signo \n");        return 1;    }    else{        int pid = atoi(argv[1]);        int sig = atoi(argv[2]);        kill(pid, sig);    }    return 0;}//终端2// test.c#include 
int main(){    while(1);    return 0;}// 终端3[muhui@bogon ~]$ ps aux | grep test// 执行顺序先执行test,终端3使用命令查看test进程id然后执行mysignal,     [muhui@bogon mysignal]$ ./mysignal 3773 9观察终端2的显示结果,并再次运行终端3的执行

raise测试代码:

// mysignal.c#include 
#include 
#include 
int main(int argc, char* argv[]){    int count = 0;    while(1){        printf("hello world\n");        if(count == 5)            raise(9);        sleep(1);        count++;    }    return 0;}

abort测试代码:

// mysignal.c#include 
#include 
#include 
#include 
int main(int argc, char* argv[]){    int count = 0;    while(1){        printf("hello world\n");        if(count == 5)            abort();        sleep(1);        count++;    }    return 0;}

3、软件条件产生信号

    关于这种信号的产生方式,我们以SIGALRM信号为例。

    SIGALRM是14号信号,也叫闹钟信号,软件通过alarm函数产生,可以以秒为单位进行定时,当时间到达之后,终止进程,alarm函数定义如下:

#include 
     unsigned int alarm(unsigned int seconds);               # 在seconds秒之后给当前进程发送SIGALRM信号               # 传入0,停止闹钟               # 返回值为0,或剩余秒数               # 默认处理动作是终止当前进程

alarm测试代码:   

// myalarm.c// 测试一秒钟能打印多少次#include 
#include 
int main(){    int count = 0;    alarm(1);    while(1){        printf("count = %d\n", count++);    }    return 0;}

    然后呢,这里就要提到上面的一个函数,signal函数,用来自定义进程对信号的动作,网上很多人都把这个函数当做一个单独的模块来讲,这里我用一种比较简单的方式,尽量最容易地说清楚这个函数。

    首先看函数的定义:

#include 
     typedef void (*sighandler_t)(int);     sighandler_t signal(int signum, sighandler_t handler);     # 接收信号之后自定义动作           # 参数1,要处理的信号           # 参数2,通常有三种形式                SIG_ING,表示忽略前面的信号,即没有动作。                SIG_DFL,表示执行该信号的默认动作,换句话说,如果使用这个选项,完全可以 不适用这个函数,因为本身就是执行默认动作                自定义函数名,即函数指针

接下来看一段基于上面alarm函数的测试代码。

// my_alarm.c#include 
#include 
#include 
int count = 0;void my_sig(int){    alarm(1);    printf("count = %d\n", count);}int main(){    signal(SIGALRM, my_sig);    // 首先要声明该信号的处理方式,这是使用自定义函数    alarm(1);    while(1){        count++;    }    return 0;}

    、

    我们可以发现,alarm是一次性定时闹钟,一次结束之后,如果没有特殊声明,进程直接终止。

对比两次输出的count,会发现相差数万被,这里就体现出了I/O速度和内存的速度之间的差距。

    我们可以试着将上面自定义函数中的alarm(1);去掉,代码如下:

#include 
#include 
#include 
int count = 0;void my_sig(int i){    printf("count = %d\n", count);}int main(){    signal(SIGALRM, my_sig);    alarm(1);    while(1)    {        count++;    }    return 0;}

    运行结果我们会发现,打印一次之后,进程会卡住不再运行,当我们在另一个终端下执行ps命令,会看到

[muhui@bogon my_alarm]$ ps aux | grep myalarm

muhui     3024 87.0  0.0   1872   376 pts/0    R+   09:03   0:04 ./myalarm

muhui     3026  0.0  0.0   5984   728 pts/1    S+   09:03   0:00 grep myalarm

    进程一直在运行!!

    不要想的太多,这里自定义行为之后,并不是就卡在了自定义函数中,而是执行完毕自定义函数,就又调回原来程序跳出的地方,继续执行原来的while(1),打破了默认的退出当前进程的行为。

    关于本文中所有的源码,全部打包上传,下载连接:

    --------muhuizz整理