• [Linux](14)信号



    kill -l 查看所有信号

    [CegghnnoR@VM-4-13-centos 2022_11_6]$ kill -l
     1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
     6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
    11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
    16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
    21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
    26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
    31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
    38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
    43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
    48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
    53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
    58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
    63) SIGRTMAX-1  64) SIGRTMAX
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    一共有62个信号,没有0号信号,32号信号和33号信号。1~31号信号称为普通信号,34~64号信号称为实时信号

    因为信号产生是异步的,当信号产生的时候,对应的进程可能正在处理更重要的事情,那么进程可以暂时不处理这个信号。

    signal 信号处理

    信号可以由键盘产生,在命令行中我们要想终止一个正在运行的前台进程可以按 ctrl+c,这一指令实际上是向进程发送了2号信号SIGINT

    此外,在命令行中使用 ctrl+\ 可以产生3号信号 SIGQUIT

    我们可以使用 signal 来设置对信号的处理方式

    NAME
           signal - ANSI C signal handling
    
    SYNOPSIS
           #include 
    
           typedef void (*sighandler_t)(int);
    
           sighandler_t signal(int signum, sighandler_t handler);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    signum 表示我们要对哪个信号设置捕捉动作。

    handler 函数指针,允许用户自定义对信号的处理动作。

    例子

    #include 
    #include 
    #include 
    using namespace std;
    
    void handler(int signo)
    {
        cout << "成功获取信号" << signo << endl;
    }
    
    int main()
    {
        // 设置对信号的处理方式
        signal(SIGINT, handler);
        signal(SIGQUIT, handler);
    
        while (true)
        {
            cout << "正在运行中..." << endl;
            sleep(1);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行

    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ./myproc
    正在运行中...
    ^C成功获取信号2
    正在运行中...
    ^C成功获取信号2
    正在运行中...
    正在运行中...
    ^\成功获取信号3
    正在运行中...
    ^\成功获取信号3
    正在运行中...
    正在运行中...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以发现 ctrl+c 和 ctrl+\ 都无法终止进程了,而是改为打印一句提示语。

    要想终止这个进程,可以使用 kill -9

    注意:9号信号无法被重新设置


    通过系统调用发送信号

    NAME
           kill - send signal to a process
    
    SYNOPSIS
           #include 
           #include 
    
           int kill(pid_t pid, int sig);
    // 返回值:成功(至少发送了一个信号):返回0。出错:返回-1,并设置errno。
    
       Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
    
           kill(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    例子

    模拟 kill -9 杀掉进程:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    void handler(int signo)
    {
        cout << "成功获取信号" << signo << endl;
    }
    
    static void Usage(const string& proc)
    {
        cerr << "Usage:\n\t" << proc << " signo pid" << endl;
    }
    
    int main(int argc, char* argv[])
    {
        if (argc != 3)
        {
            Usage(argv[0]);
            exit(1);
        }
        if (kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == -1)
        {
            cerr << "kill: " << strerror(errno) << endl;
            exit(2);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    img


    raise 函数

    NAME
           raise - send a signal to the caller
    
    SYNOPSIS
           #include 
    
           int raise(int sig);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个函数就是给自己发信号

    例子

    void handler(int signo)
    {
        cout << "成功获取信号" << signo << endl;
    }
    
    static void Usage(const string& proc)
    {
        cerr << "Usage:\n\t" << proc << " signo pid" << endl;
    }
    
    int main()
    {
        signal(2, handler);
    
        while (1)
        {
            sleep(1);
            raise(2);
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
    成功获取信号2
    成功获取信号2
    成功获取信号2
    
    • 1
    • 2
    • 3
    • 4

    abort 函数

    NAME
           abort - cause abnormal process termination
    
    SYNOPSIS
           #include 
    
           void abort(void);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    向自己发送 SIGABRT 信号

    使用它也可以终止进程,和 9 号信号不同的是,它可以被 signal 捕获并处理,但是依然无法被重新设置。

    void handler(int signo)
    {
        cout << "成功获取信号" << signo << endl;
    }
    
    static void Usage(const string& proc)
    {
        cerr << "Usage:\n\t" << proc << " signo pid" << endl;
    }
    
    int main()
    {
        signal(6, handler);
    
        abort();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
    成功获取信号6
    Aborted
    
    • 1
    • 2
    • 3

    可以看到它成功被捕获并处理了,但是程序依然通过 abort 退出了

    由软件条件来产生信号

    alarm 函数

    NAME
           alarm - set an alarm clock for delivery of a signal
    
    SYNOPSIS
           #include 
    
           unsigned int alarm(unsigned int seconds);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    设置发送信号的闹钟

    该函数会令进程在 seconds 秒后收到 4 号信号 SIGALRM 终止进程。

    硬件异常产生信号

    硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号,例如当前进程执行了除以 0 的进程,CPU 的运算单元会产生异常,内核将这个异常解释为 SIGFPE 信号发送给进程。又比如,当前进程访问了非法内存得知,MMU 会产生异常,内核将这个异常解释为 SIGSEGV 信号发送给进程。

    总结

    1. 上面所说的所有型号产生,最终都要由 OS 来进行执行,因为 OS 是进程的管理者
    2. 信号的处理不一定是立即处理的,而是在合适的时候。
    3. 信号如果不是被立即处理,那么信号需要暂时被进程记录在 PCB 中。
    4. 一个进程在没有收到信号的时候,就应该知道自己应该对合法信号作何处理。
    5. OS 通过更新目的进程上下文中的某个状态,来发送一个信号给目的进程。

    内存转储(core dump)

    [Linux](9)进程控制:进程创建,进程终止,进程等待,进程程序替换_世真的博客-CSDN博客中我们提到了进程的退出状态

    img

    其中的 core dump 标志表示是否进行了内存转储,”内存转储“是一个历史术语,意思是把代码和数据内存段的映像写到磁盘上。说人话就是,将进程在运行中出现的异常上下文数据,存储到磁盘上,方便调试。

    使用 man 7 signal 我们可以看到这样一张表格:

    Signal     Value     Action   Comment
    ──────────────────────────────────────────────────────────────────────
    SIGHUP        1       Term    Hangup detected on controlling terminal
    or death of controlling process
    SIGINT        2       Term    Interrupt from keyboard
    SIGQUIT       3       Core    Quit from keyboard
    SIGILL        4       Core    Illegal Instruction
    SIGABRT       6       Core    Abort signal from abort(3)
    SIGFPE        8       Core    Floating point exception
    SIGKILL       9       Term    Kill signal
    SIGSEGV      11       Core    Invalid memory reference
    SIGPIPE      13       Term    Broken pipe: write to pipe with no
    readers
    SIGALRM      14       Term    Timer signal from alarm(2)
    SIGTERM      15       Term    Termination signal
    SIGUSR1   30,10,16    Term    User-defined signal 1
    SIGUSR2   31,12,17    Term    User-defined signal 2
    SIGCHLD   20,17,18    Ign     Child stopped or terminated
    SIGCONT   19,18,25    Cont    Continue if stopped
    SIGSTOP   17,19,23    Stop    Stop process
    SIGTSTP   18,20,24    Stop    Stop typed at terminal
    SIGTTIN   21,21,26    Stop    Terminal input for background process
    SIGTTOU   22,22,27    Stop    Terminal output for background process
    
    The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在 Action 一列,Term 表示终止,Core 表示终止并进行 core dump

    当进程接受到 Action 为 Core 的信号后,将会进行 core dump。通过观察可以发现,这些信号的原因大概是程序代码出了问题。

    对于以下程序,我们创建一个子进程,让它发生野指针问题,最后将退出信息发送个父进程进行解析。

    int main()
    {
        pid_t id = fork();
        if (id == 0)
        {
            int* p = nullptr;
            *p = 10;
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
        printf("exitcode: %d, signo: %d, core dump flag: %d\n", (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果

    exitcode: 0, signo: 11, core dump flag: 0
    
    • 1

    可以发现,终止信号是11,core dump flag 是 0,也就是没有发生 core dump。

    为什么呢?

    因为在云服务器中,默认是不允许 core dump 的。

    使用指令 ulimit -a

    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ulimit -a
    core file size          (blocks, -c) 0
    data seg size           (kbytes, -d) unlimited
    scheduling priority             (-e) 0
    file size               (blocks, -f) unlimited
    pending signals                 (-i) 7904
    max locked memory       (kbytes, -l) unlimited
    max memory size         (kbytes, -m) unlimited
    open files                      (-n) 100002
    pipe size            (512 bytes, -p) 8
    POSIX message queues     (bytes, -q) 819200
    real-time priority              (-r) 0
    stack size              (kbytes, -s) 8192
    cpu time               (seconds, -t) unlimited
    max user processes              (-u) 7904
    virtual memory          (kbytes, -v) unlimited
    file locks                      (-x) unlimited
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    第一列的 core file size 默认被设置为 0,也就是禁止发生 core dump

    使用 ulimit -c 1000000 可以将它的大小设置为 1000000,这样就允许生成 core file 了

    接着再次运行上一个程序,即可成功生成 core file。

    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
    exitcode: 0, signo: 11, core dump flag: 1
    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ll
    total 296
    -rw------- 1 CegghnnoR CegghnnoR 593920 Nov  7 16:24 core.13458
    -rw-rw-r-- 1 CegghnnoR CegghnnoR    151 Nov  7 12:37 makefile
    -rwxrwxr-x 1 CegghnnoR CegghnnoR   8896 Nov  7 16:11 mykill
    -rw-rw-r-- 1 CegghnnoR CegghnnoR    477 Nov  7 16:11 mykill.cpp
    -rwxrwxr-x 1 CegghnnoR CegghnnoR   9176 Nov  7 12:37 myproc
    -rw-rw-r-- 1 CegghnnoR CegghnnoR    184 Nov  7 12:37 myproc.cpp
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    core 文件一般比较大。

    编译时使用 -g 选项,编译后使用 gdb 调试:

    然后使用 core-file [core file] 指令来指定刚才生成的 core 文件,这样就能迅速定位到出现错误的一行。

    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ls
    core.15976  makefile  mykill  mykill.cpp  myproc.cpp
    [CegghnnoR@VM-4-13-centos 2022_11_6]$ gdb mykill
    GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
    Copyright (C) 2013 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later 
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "x86_64-redhat-linux-gnu".
    For bug reporting instructions, please see:
    ...
    Reading symbols from /home/CegghnnoR/code/2022_11_6/mykill...done.
    (gdb) core-file core.15976
    [New LWP 15976]
    Core was generated by `./mykill'.
    Program terminated with signal 11, Segmentation fault.
    #0  0x000000000040077f in main () at mykill.cpp:17
    17              *p = 10;
    Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64
    (gdb) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    阻塞信号

    概念

    • 实际执行信号的处理动作称为信号递达(Delivery)
    • 信号从产生到递达之前的状态,称为信号未决(Pending)
    • 进程可以选择阻塞(Block)某个信号。
    • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
    • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

    img

    • 如上图,block 和 pending 都是位图,handler 是函数指针数组。
    • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending), 还有一个函数指针表示处理动作。信号产生即内核在进程控制块中设置该型号的 pending 标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP 信号未阻塞也未产生过,当它递达是执行默认处理动作。
    • SIGINT 信号产生过,但正在被阻塞,所以暂时不能递达,一旦阻塞被解除,进程就会在适当的时机进行递达。
    • SIGQUIT 信号未产生过,一旦产生 SIGQUIT 信号将被阻塞,它的处理动作是用户自定义函数 sighandler

    信号集

    sigset_t

    • sigset_t 是一个结构体类型,用于表示位图
    • sigset_t 称为信号集
    • 阻塞信号集也叫做信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

    信号集操作函数

    sigset_t 类型对于每种信号用一个 bit 表示 有效 或 无效 状态,至于这个类型内部是如何实现的,使用者不必关心,使用者只能使用以下函数来操作 sigset_t 变量。

    #include 
    int sigemptyset(sigset_t *set);	// 清空信号集
    int sigfillset(sigset_t *set);	// 信号集全置1
    int sigaddset (sigset_t *set, int signo);	// 加入信号
    int sigdelset(sigset_t *set, int signo);	// 删除信号
    int sigismember(const sigset_t *set, int signo);	// 判断信号是否在信号集中
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    sigprocmask

    调用函数 sigprocmask 以读取或更改进程的信号屏蔽字(阻塞信号集)

    NAME
           sigprocmask - examine and change blocked signals
    
    SYNOPSIS
           #include 
    
           int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    // 返回值:成功:返回0,失败:返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    how 可以传入三个选项(假设当前的信号屏蔽字为 mask):

    SIG_BLOCKset 包含了我们希望添加到当前信号屏蔽字的信号,相当于 mask = mask | set
    SIG_UNBLOCKset 包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于 mask = mask & ~set
    SIG_SETMASK设置当前信号屏蔽字为 set 所指向的值,相当于 mask = set

    set 是输入型参数,oldset 是输出型参数

    • 如果 oldset 是非空指针,则读取进程的当前信号屏蔽字通过 oldset 参数传出,如果 set 是非空指针,则更改进程的信号屏蔽字。参数 how 指示了如何更改。如果 oldsetset 都是非空指针,则先将原来的信号屏蔽字备份到 oldset 里,然后根据 sethow 参数更改信号屏蔽字。

    如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递达。

    注意:9 号信号无法被屏蔽

    sigpending

    NAME
           sigpending - examine pending signals
    
    SYNOPSIS
           #include 
    
           int sigpending(sigset_t *set);
    // 返回值:成功:返回0,失败:返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    set 为输出型参数,该函数可以直接将 pending 信息通过 set 传出

    例子

    每隔一秒打印一次当前进程的 pending 信号集,在程序运行中我们给它发送 2 号信号,由于发完立刻就会被递达,所以我们先把 2 号信号屏蔽掉,方便查看发送信号后的 pending 信号集的变化

    static void showPending(sigset_t* pendings)
    {
        // 遍历31个信号
        for (int sig = 1; sig <= 31; ++sig)
        {
            if (sigismember(pendings, sig))
            {
                cout << "1";
            }
            else
            {
                cout << "0";
            }
        }
    }
    
    int main()
    {
        sigset_t bsig, obsig;
        sigemptyset(&bsig);
        sigemptyset(&obsig);
        // 添加2号信号到信号屏蔽字中
        sigaddset(&bsig, 2);
        // 让当前进程屏蔽2号信号
        sigprocmask(SIG_SETMASK, &bsig, &obsig);
    
        sigset_t pendings;
        while (true)
        {
            sigemptyset(&pendings);
            if (sigpending(&pendings) == 0)
            {
                showPending(&pendings);
            }
            sleep(1);
            cout << endl;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    [CegghnnoR@VM-4-13-centos 2022_11_6]$ ./mykill
    0000000000000000000000000000000
    0000000000000000000000000000000
    ^C0000000000000000000000000000000
    0100000000000000000000000000000
    0100000000000000000000000000000
    0100000000000000000000000000000
    0100000000000000000000000000000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到在发送了 2 号信号后,第 2 位变成 1 了。

    何时进行信号处理?信号捕捉

    上面我们说到,进程收到信号不是立即就进行处理的,而是在合适的时候。那么什么是合适的时候?

    当当前进程从内核态,切换回用户态的时候,进行信号的检测与处理。

    在讲虚拟进程地址空间中:[Linux](8)进程地址空间_世真的博客-CSDN博客,我们见过下面这张图:

    img

    我们知道,在虚拟进程地址空间与物理内存之间有一级页表,负责映射位置关系。其实页表分为两种:

    • 用户级页表:每个进程都有一份,负责 0 ~ 3G 的用户空间的映射。
    • 内核级页表:只有一份,所有进程共享,负责 3 ~ 4G 的内核空间的映射。

    这两个页表的访问权限是不一样的,当前进程要想访问内核级页表,需要进行身份切换。

    • 进程如果是用户态的——只能访问用户级页表
    • 进程如果是内核态的——可以访问内核级和用户级页表

    CPU 内部有对应的状态寄存器 CR3,有 bit 位表示当前进程的状态:0——内核态,3——用户态

    一般在系统调用和进程时间片到了,进行进程切换的时候会从用户态切换到内核态,在执行完系统调用或进程切换后,由内核态切换回用户态的时候,就会进行信号的检测与处理。


    如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

    如下图:

    信号捕捉与状态切换的过程。

    img

    1. 遇到中断、故障、系统调用,则让进程进入内核态
    2. 执行完内核代码,需要返回用户态,返回之前检测信号
    3. 如果遇到自定义信号处理动作,由于自定义动作时用户写的,需要防止恶意代码,所以回到用户态执行
    4. 执行信号处理方法,回到内核态
    5. 将陷入内核态的代码执行完毕(比如完成返回值的返回)然后回到用户态,恢复 main 函数的上下文继续执行后续代码。

    sigaction

    sigaction 函数可以读取和修改与指定信号相关联的处理动作。

    NAME
           sigaction - examine and change a signal action
    
    SYNOPSIS
           #include 
    
           int sigaction(int signum, const struct sigaction *act,
                         struct sigaction *oldact);
    // 返回值:成功:返回0,失败:返回-1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • signo 是指定信号的编号。

    • act 指针非空,则根据 act 修改该信号的处理动作。若 oldact 非空,则通过 oldact 传出信号原来的处理动作。

    actoldact 两个参数是结构体指针,其结构体被定义为:

    struct sigaction
    {
        void (*sa_handler)(int);
        void (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t sa_mask;
        int sa_flags;
        void (*sa_restorer)(void);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们只研究该结构体的 void (*sa_handler)(int); sigset_t sa_mask; 两个成员

    • sa_handler 可以赋值为我们自己定义的处理方式,也可以赋值为 SIG_IGN 表示忽略信号。赋值为 SIG_DFL 表示执行系统默认动作。

    例子

    void handler(int signo)
    {
        cout << "获取到一个信号,信号编号:" << signo << endl;
    }
    
    int main()
    {
        struct sigaction act, oact;
        act.sa_handler = handler;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
    
        sigaction(2, &act, &oact);
    
        while (true)
        {
            sleep(1);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,如果这种信号再次产生,那么它就会被阻塞到当前的处理结束为止。

    例子

    设置对2号信号的处理函数,处理方式为一个死循环,不停打印 pending 位图。

    #include 
    #include 
    #include 
    
    using namespace std;
    
    void handler(int signo)
    {
        cout << "获取到一个信号,信号编号:" << signo << endl;
        sigset_t pending;
        while (true)
        {
            cout << "." << endl;
            sigpending(&pending);
            for (int i = 1; i <= 31; ++i)
            {
                if (sigismember(&pending, i))
                    cout << '1';
                else cout << '0';
            }
            cout << endl;
            sleep(1);
        }
    }
    
    int main()
    {
        struct sigaction act, oact;
        act.sa_handler = handler;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
    
        sigaction(2, &act, &oact);
    
        while (true)
        {
            cout << "main running" << endl;
            sleep(1);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    [CegghnnoR@VM-4-13-centos 2022_11_9]$ ./mysignal
    main running
    main running
    main running
    ^C获取到一个信号,信号编号:2
    .
    0000000000000000000000000000000
    .
    0000000000000000000000000000000
    .
    0000000000000000000000000000000
    ^C.
    0100000000000000000000000000000
    .
    0100000000000000000000000000000
    ^C.
    0100000000000000000000000000000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    当我们第一次发送 2 号信号时,进程成功开始处理这个信号。之后再多次发送 2 号信号,我们发现进程只会把 2 号信号设为未决状态,不会继续去处理新的信号,因为内核自动将当前信号加入进程的信号屏蔽了。

    • sa_masksigaction 结构体中的 sigset_t 类型成员。设置该变量可以让进程在处理信号的时候不仅屏蔽当前信号,也可以同时屏蔽其他的由用户指定的信号。

    例子

    在如上代码基础上对 sa_mask 进行设置,屏蔽 3 号信号。

    //...
    	act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask, 3); // 设置 sa_mask
        sigaction(2, &act, &oact);
    //...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    [CegghnnoR@VM-4-13-centos 2022_11_9]$ ./mysignal
    main running
    main running
    main running
    ^C获取到一个信号,信号编号:2
    .
    0000000000000000000000000000000
    .
    0000000000000000000000000000000
    ^C.
    0100000000000000000000000000000
    .
    0100000000000000000000000000000
    .
    0100000000000000000000000000000
    ^\.
    0110000000000000000000000000000
    .
    0110000000000000000000000000000
    .
    0110000000000000000000000000000
    .
    0110000000000000000000000000000
    .
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在处理 2 号信号的时候,2 号信号和 3 号信号都被屏蔽了。

    可重入函数

    以链表的插入为例:

    • main 函数调用 insert 函数向一个链表 head 中插入结点 node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核态,再次回到用户态之前检查到有信号待处理,于是切换到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入结点 node2,插入操作的两步都做完之后从 sighandler 返回内核态,再次回到用户态就从 main 函数调用 insert 函数调用的 insert 函数中继续往下执行,先前做一步之后被打断,现在继续做完第二步。结果是,main 函数和 sighandler 先后向链表中插入两个结点,而最后只有第一个结点真正插入链表中了。
    • 像上例这样,insert 函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入
    • insert 函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。

    具有以下条件之一的函数是不可重入的:

    • 调用了 mallocfree,因为 malloc 也是用全局链表来管理堆的。
    • 调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。

    volatile

    volatile 是 C 语言中的关键字,这里我们站在信号的角度来重新理解一下。

    例子

    我们定义一个全局变量 flags = 0,然后死循环,直到发送 2 号信号,通过信号处理函数将 flags 改为 1,结束循环

    #include 
    #include 
    
    int flags = 0;
    
    void handler(int signo)
    {
        printf("更改flags: 0->1\n");
        flags = 1;
    }
    
    int main()
    {
        signal(2, handler);
    
        while (!flags);
        printf("进程是正常退出的\n");
        
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    [CegghnnoR@VM-4-13-centos volatile]$ ./mysignal
    ^C更改flags: 0->1
    进程是正常退出的
    
    • 1
    • 2
    • 3

    运行结果是符合预期的!

    但是在编译器高优化级别下,结果就不一定了,

    如下,开启 O2 优化后的运行结果:

    [CegghnnoR@VM-4-13-centos volatile]$ make
    gcc -o mysignal mysignal.c -std=c99 -O2
    [CegghnnoR@VM-4-13-centos volatile]$ ./mysignal
    ^C更改flags: 0->1
    ^C
    
    • 1
    • 2
    • 3
    • 4
    • 5

    编译器为了提高运行速度,可能会把 flags 放到寄存器中,while 循环判断的时候就直接从寄存器中读取,而我们的信号处理函数只是修改了内存中的 flags,最后导致 while 循环无法退出。

    使用 volatile 关键字,可以告诉编译器不准对 flags 进行优化,每次 CPU计 算的时候,要从内存中读取数据。

    volatile int flags = 0;
    
    • 1

    这样运行结果又符合预期了。

    SIGCHLD 信号

    子进程退出、暂停、继续的时候,都会自动给父进程发送 SIGCHLD 信号。

    那么这个信号有什么用呢?

    我们以前是让父进程回收子进程是通过阻塞等待或者轮询等待,现在我们可以基于 SIGCHLD 信号,完成一种新的非阻塞等待:SIGCHLD 信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD 信号的处理处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,当子进程终止时会发信号通知父进程,父进程在信号处理中调用 waitpid 回收子进程即可

    例子

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    void FreeChld(int signo)
    {
        assert(signo == SIGCHLD);
        while (true)
        {
            pid_t id = waitpid(-1, nullptr, WNOHANG);
            if (id > 0)
            {
                cout << "父进程等待成功, chld pid: " << id << endl;
            }
            else if (id == 0)
            {
                // 还有子进程,但是现在没有退出
                cout << "还有子进程,但是没有退出,父进程要继续忙自己的事情了" << endl;
                break;
            }
            else
            {
                // waitpid 调用失败,即没有子进程了
                break;
            }
        }
    }
    
    int main()
    {
        signal(SIGCHLD, FreeChld);
        pid_t id = fork();
        if (id == 0)
        {
            int cnt = 5;
            while (cnt)
            {
                cout << "子进程pid: " << getpid() << "当前cnt: " << cnt-- << endl;
                sleep(1);
            }
            cout << "子进程退出,进入僵尸状态" << endl;
            exit(0);
        }
    
        while (true)
        {
            cout << "父进程正在运行: " << getpid() << endl;
            sleep(1);
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    注意:如果多个子进程同时退出,则会同时向父进程发送多次 SIGCHLD 信号,其中必然会有一部分信号无法递达(信号屏蔽以及pending 位图只能存储 01 的原因),所以在处理函数中不能只等待一次,而是需要循环等待,将所有已经退出的子进程都回收完再结束等待。如果有子进程,但这些子进程还未退出,那么也要结束等待,让父进程去做其他事,当这些子进程也退出的时候,会重新发送 SIGCHLD 信号让父进程去回收。


    事实上,由于 UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程将 SIGCHLD 的处理动作置为 SIG_IGN,这样 fork 出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

    通常系统默认的忽略动作和用户自定义的忽略是没有区别的,但是这里是一个特例。此方法对于 Linux 可用,但不保证在其他 UNIX 系统上可用。

  • 相关阅读:
    AQS介绍
    前端食堂技术周刊第 47 期:Docusaurus 2.0 、7 月登陆网络平台的新内容 、Nuxt.js 团队的轮子库
    jdbc的API详解
    必看:阿里云99元服务器原价续费,你肯定不知道!
    企业为何要挖掘专利和专利布局,如何做?
    CSS 实现动态显示隐藏(:checked 和 :target 的妙用)
    灯光秀如何打造夜间经济新增长点
    函数式编程概述
    WPF 属性触发器示例
    【算法训练-二叉树 六】【路径和计算】路径总和I、路径总和II、路径总和III、二叉树的最大路径和
  • 原文地址:https://blog.csdn.net/CegghnnoR/article/details/127811660