美文网首页
Linux信号, since 2020-11-18

Linux信号, since 2020-11-18

作者: Mc杰夫 | 来源:发表于2020-11-18 21:33 被阅读0次

    (2020.11.18 Wed)
    信号是内核向进程传递信息的方式。

    按键信号

    在Shell中可通过Ctrl+c来中断正在运行的进程,或Ctrl+Z来中止进程。按下这些键,Shell都向进程发出了信号。进程捕捉到信号,根据其含义来执行特定动作。

    使用Shell时,在Shell中输入一个命令,等待命令完成。完成后执行其他命令。在进程运行期间,Shell的命令行输入会阻塞(block),此时Shell不再接受新的命令。

    $ping localhost > log
    

    命令ping的进程占据了舞台,称为前台进程每个Shell最多有一个前台进程,该进程会阻塞Shell。如果Shell启动前台进程,那么在Shell中的输入就重新定向到前台进程的stdin。Shell的按键信号,也就是发给前台进程的。

    除了一个前台进程,一个Shell还可以有多个后台进程,后台进程不会阻塞Shell的命令行

    $ping localhost > log &
    

    有&符号的命令被置于后台,按键信号不会发送给后台进程。

    Linux使用特定短语来指代信号,可以用按键发出的信号有三个,都是传递给Shell的前台进程的。

    • SIGINT:按快捷键Ctrl+C,中断(interrupt)前台进程
    • SIGTSTP:按快捷键Ctrl+Z,暂停(stop)前台进程
    • SIGQUIT:按快捷键Ctrl+\,退出(quit)前台进程

    Kill命令

    按键是向前台进程发出信号的快捷方式,也可以用kill命令向特定进程发出信号。首先在一个Shell中运行一个前台进程

    $ping localhost > log
    

    另一个Shell中,找到ping命令对应进程的PID:

    $ps aux | grep 'ping localhost'
    

    返回进程的PID,如1111,就可以用kill命令中断程序

    $kill 1111
    

    事实上,kill命令可以向系统的任何一个进程发信号,无论这个进程是前台进程还是后台进程,以一个后台进程为例

    $ping localhost > log & 
    

    后台进程会在Shell上打印出PID,比如8848,有了这个PID,可以用kill命令向进程发信号

    $kill -s SIGTSTP 8848 //format: $kill -s xxxx PID, where xxxx is SIGCONT/SIGALRM
    $kill -s SIGCONT 8848 //让暂停的进程继续
    
    • SIGCONT: 通知暂停的进程继续
    • SIGALRM: 定时信号
    $kill -l //用下面命令查询完整的信号列表
    $man 7 signal //查询更详细的文档
    

    信号机制

    信号本质上很简单,是内核传递给进程的一个整数。

    整数 信号
    2 SIGINT
    3 SIGQUIT
    14 SIGALRM

    用下面指令查询信号对应的整数

    $kill -l
    

    在内核的内存空间里,系统给每个进程预留了一块用于存放信号的空间,这个空间是内核和进程之间沟通的信箱,信箱里存放的就是对应信号的整数。

    内核自身也可能产生信号,比如出现分母为0的出发运算这样的错误,内核会用SIGFPE信号来通知进行该运算的进程。

    其他进程也可以产生信号,比如用户用快捷键发出的中断信号。无论信号是谁发出,最终都由内核进入目标进程的邮箱,也就是专用内存空间,也就产生了信号。

    进程何时查看信号

    信号产生后, 就躺在目标进程对应的内存里,直到这个进程来看。信号总是在特定时机下被查看。每次进程完成系统调用、即将退出内核的时间,就是查看信号的最好时机。原因简单,信号保存在内核空间,而进程执行系统调用时正好处于内核模式,查看内核空间的代价最小。如果有信号,进程会执行对应该信号的操作,称作信号处理(signal disposition)。从信号生成到信号处理这段时间,信号处于等待(pending)状态。

    信号处理signal disposition

    信号没有操纵进程的魔力,只是通知进程某种情况的发生。在ping命令的例子中,ping收到信号受,根据Linux系统的管理,针对每种信号都采取了对应了默认操作,但并不绝对。进程信号处理的三种可能

    1. 无视信号(ignore signal):信号被清除,进城本身不采取任何特殊的操作
    2. 默认操作(default action):每个信号对应有一定的默认操作,比如SIGCONT用于继续进程
    3. 捕获信号(catch signal):根据信号,执行程序中自定义的操作

    进程收到信号后,根据信号的种类和程序设计,决定采取哪种行动。以bash脚本为例。脚本运行后成为一个进程,这个进程可以处理信号,某脚本test_signal.bash

    #!/bin/bash
    while true
    do
        echo hello
    done
    

    运行脚本

    $./test_signal.bash > log &
    

    获得脚本的PID,如10412,向脚本发出信号

    $kill -s SIGINT 10412
    

    此时进程对信号的处理,采用的是默认操作。可以利用trap命令,让一个bash脚本捕获信号。

    #!/bin/bash
    
    trap "echo 'interrupted'; exit 1; " SIGINT
    
    while true
    do
        echo hello
    done
    

    命令trap后面跟的参数,一个是捕获信号后想要进行的操作,一个是用来说明想要捕捉信号的名称。除了用信号名,也可以用信号编号。根据脚本中的trap命令可知,这个脚本会捕获SIGINT信号,针对该信号进行自定义处理:先打印interrupt,再以状态1退出。

    可以配合bash函数,在捕获信号后进行更复杂的操作

    #!/bin/bash
    
    WANNA_QUIT=false
    
    function handle_exit {
        if [ "$WANNA_QUIT" = true ]; then
            echo "bye."
            exit 0
        else
            WANNA_QUIT=true
            echo "press Ctrl+c again to quit."
        fi
    }
    
    trap "handle_exit;" SIGINT
    
    while true
    do
        echo working...
        sleep 1
    done
    

    该脚本中定义了一个叫做handle_exit函数,该函数在第一次被调用时不会直接退出,而是提示用户如果退出,需要再次按下快捷键Ctrl+C,即打印提示。而第二次收到SIGINT信号后,将会输出bye.,并退出程序。

    脚本一开始就用trap说明了信号捕获的相关操作,但脚本不会停在trap那一行等待信号。脚本继续执行下去,进行循环操作。只有等到信号出现,脚本才会回到trap说明的操作。对于trap命令来说,语句执行的时机和语句发生作用的时机是分离的。回忆ls,echo,which等语句,当语句执行时,语句规定的操作就进行了,这种工作方式是同步(synchronous),而trap的工作方式是异步(asynchronous)。异步编程用于处理像信号这样出现时机不确定的事件。

    C程序中的信号

    C语言编写的程序也可以捕获信号,C语言信号相关的内容在头文件signal.h里。捕获信号的语句格式

    signal(SIGINT, sigint_handler);
    

    signal函数跟着两个参数,第一个是信号标识,在这里捕捉的是SIGINT,第二个是回调函数,这里是sigint_handler。SIGINT常量是在signal.h中定义的,如果打开头文件,那么可以看到有下面这一行

    #define SIGINT 2 /*interrupt*/
    

    回调函数和一般的C语言函数类似,例如定义下面这个函数

    void sigint_handler() 
    {
         printf('Received SIGINT');
    }
    

    这个C语言程序运行时,如果遇到SIGINT信号,会打印received SIGINT。下面是完成的C程序。

    #include <signal.h>
    #include <stdio.h>
    #include <unistd.h>
    void sigint_handler() 
    {
         printf('Received SIGINT');
    }
    int main()
    {
        signal(SIGINT, sigint_handler);
        while (1)
        {
            printf('waiting...\n');
        }
        return 0;
    }
    

    程序不断打印waiting,此时发出信号,即按下Ctrl+C,将触发程序一开顶的信号处理函数sigint_handler。程序将执行该处理函数中的语句,即打印Received SIGINT。

    Reference

    1 Vamei,周梓昕著,树莓派开始玩转Linux,中国工信出版集团,电子工业出版社

    相关文章

      网友评论

          本文标题:Linux信号, since 2020-11-18

          本文链接:https://www.haomeiwen.com/subject/jdqhiktx.html