信号的基本概念


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

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

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

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

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

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

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

Linux之信号第一谈_第1张图片    


信号产生的条件

     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;
}

Linux之信号第一谈_第2张图片


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;
}

Linux之信号第一谈_第3张图片

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;
}

Linux之信号第一谈_第4张图片


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;
}

Linux之信号第一谈_第5张图片

    然后呢,这里就要提到上面的一个函数,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;
}

Linux之信号第一谈_第6张图片    、

    我们可以发现,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),打破了默认的退出当前进程的行为。

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

https://github.com/muhuizz/Linux/tree/master/Linux%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B/%E4%BF%A1%E5%8F%B7/code

    --------muhuizz整理