Linux信号概述

20 篇文章 0 订阅
4 篇文章 0 订阅


Linux信号(signal)允许进程和内核中断其它进程,例如 Ctrl+C会向终端中正在运行的进程发送 SIGINT信号,一个进程试图除0时内核会发送给它一个 SIGFPE信号,进程执行一条非法指令时内核会发送 SIGILL信号,进程非法内存引用时内核会发送给它一个 SIGSEGV信号,一个进程可以通过向另一个进程发送 SIGKILL信号强制终止它,当一个子进程终止时,内核会发送 SIGCHLD信号给其父进程。下表展示了Linux系统支持的30多种不同类型的信号。

SignalStandardActionComment
SIGABRTP1990终止并转储内存Abort signal from abort(3)
SIGALRMP1990终止Timer signal from alarm(2)
SIGBUSP2001终止并转储内存Bus error (bad memory access)
SIGCHLDP1990忽略Child stopped or terminated
SIGCLD-忽略A synonym for SIGCHLD
SIGCONTP1990ContContinue if stopped
SIGEMT-终止Emulator trap
SIGFPEP1990终止并转储内存Floating-point exception
SIGHUPP1990终止Hangup detected on controlling terminal or death of controlling processss
SIGILLP1990终止并转储内存Illegal Instruction
SIGINFO-A synonym for SIGPWR
SIGINTP1990终止Interrupt from keyboard
SIGIO-终止I/O now possible (4.2BSD)
SIGIOT-终止并转储内存IOT trap. A synonym for SIGABRT
SIGKILLP1990终止Kill signal
SIGLOST-终止File lock lost (unused)
SIGPIPEP1990终止Broken pipe: write to pipe with no readers; see pipe(7)
SIGPOLLP2001终止Pollable event (Sys V). Synonym for SIGIO
SIGPROFP2001终止Profiling timer expired
SIGPWR-终止Power failure (System V)
SIGQUITP1990终止并转储内存Quit from keyboard
SIGSEGVP1990终止并转储内存Invalid memory reference
SIGSTKFLT-终止Stack fault on coprocessor (unused)
SIGSTOPP1990停止直到下一个SIGCONTStop process
SIGTSTPP1990停止直到下一个SIGCONTStop typed at terminal
SIGSYSP2001终止并转储内存Bad system call (SVr4); see also seccomp(2)
SIGTERMP1990终止Termination signal
SIGTRAPP2001终止并转储内存Trace/breakpoint trap
SIGTTINP1990停止直到下一个SIGCONTTerminal input for background process
SIGTTOUP1990停止直到下一个SIGCONTTerminal output for background process
SIGUNUSED-终止并转储内存Synonymous with SIGSYS
SIGURGP2001忽略Urgent condition on socket (4.2BSD)
SIGUSR1P1990终止User-defined signal 1
SIGUSR2P1990终止User-defined signal 2
SIGVTALRMP2001终止Virtual alarm clock (4.2BSD)
SIGXCPUP2001终止并转储内存CPU time limit exceeded (4.2BSD); see setrlimit(2)
SIGXFSZP2001终止并转储内存File size limit exceeded (4.2BSD); see setrlimit(2)
SIGWINCH-忽略Window resize signal (4.3BSD, Sun)

:上述信号中SIGSTOPSIGKILL既不能被捕获也不能被忽略。

术语

  • 发送信号。内核通过更新目的进程上下文的某个状态发送一个信号给目的进程。内核可能因为:1)检测到一个系统事件。2)一个进程调用了kill显示地要求内核发送一个信号给目的进程。进程可以给自己发信号。
  • 接收信号。目的进程对信号做出反应时,它就接收了信号。进程可以忽略这个信号,或者通过执行信号处理程序捕获这个信号。
  • 待处理信号(pending signal)。发出而没有被接收的信号称之为待处理信号。在任何时刻,一个进程的某种类型的待处理信号至多有一个。
  • 阻塞信号(blocked signal)。进程可以阻塞接收某种信号,这种情况下该信号仍然是待处理信号,但直到进程取消阻塞前,它都不会被进程接收。

进程组

每个进程都只属于一个进程组,下面函数可以用来查看当前进程的进程组以及改变自己或其它进程的进程组:

#include <unistd.h>

pid_t getpgrp();
                              // 返回:调用进程的进程组ID
pid_t setpgid(pid_t pid, pid_t pgid);
                              // 返回:若成功则为0,若错误则为-1

对于setpgid来说,若pid为0,那么就是用当前进程的PID,如果pgid为0,那么就用pid指定的进程的PID作为进程组的ID。

发送信号

  1. 使用/bin/kill命令发送信号
    例如 kill -9 15213 会发送信号9SIGKILL终止该进程
  2. 通过键盘发送信号
    Ctrl+C发送SIGINT信号,默认情况下终止进程;Ctrl+Z发送SIGTSTP信号,默认情况下挂起当前进程
  3. 使用kill函数发送信号
    #include <sys/types.h>
    #include <signal.h>
    
    int kill(pid_t pid, int sig);
                                 // 返回:若成功则为0,若错误则为-1
    
    • pid > 0,发送信号sig给进程pid
    • pid == 0,发送信号sig给调用进程所在进程组的每个进程,包括调用进程
    • pid < 0,发送信号sig给进程组|pid|(绝对值)中的每个进程。
  4. 使用alarm函数发送信号
    #include <unistd.h>
    
    unsigned int alarm(unsigned int secs);
                                 // 返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0
    

接收信号

每种信号类型都有下面四种行为中的一个默认行为:

  • 进程终止
  • 进程终止并转储内存
  • 进程挂起直到被SIGCONT信号重启
  • 忽略该信号
    进程可以通过signal函数设置收到信号的行为(SIGSTOPSIGKILL除外):
#include <signal.h>

typedef void (*sighandler_t)(int); // 函数指针

sighandler_t signal(int signum, sighandler_t handler);
                                // 返回:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR (不设置errno)

对于第二个参数handler来说:

  • SIG_IGN,忽略signum类型的信号
  • SIG_DFL,恢复signum类型信号的默认行为
  • 函数指针,这个函数即为signum信号的处理程序,进程收到该信号时会执行处理程序(即handler函数);处理程序返回(return语句)时,指令通常会回到程序被信号中断的位置。

阻塞及解除阻塞信号

  • 隐式阻塞机制
    默认阻塞与正在处理信号同类型的信号
  • 显式阻塞机制
    进程可以使用sigprocmask及其辅助函数阻塞多个信号:
    #include <signal.h>
    
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);    // 使用set设置要阻塞的信号,how指明行为,oldset存放旧的set
    int sigemptyset(sigset_t *set);                                     // 置set为空
    int sigfillset(sigset_t *set);                                      // 将每个信号都加入set中
    int sigaddset(sigset_t *set, int signum);                           // 将信号signum加入set
    int sigdelset(sigset_t *set, int signum);                           // 从set中删除signum
                                  // 返回:若成功则为0,若错误则为-1
    
    int sigismember(const sigset_t *set, int signum);
                                  // 返回:若signum是set的一员,则为1,如果不是则为0,若出错则为-1
    
    对于sigprocmask来说,其行为依赖于第一个参数how的值:
    • SIG_BLOCK: 把set中的信号添加到阻塞信号集blocked中
    • SIG_UNBLOCK: 从阻塞信号集blocked中删除set中的信号
    • SIG_SETMASK: 设置阻塞信号集为set
      如下程序展示了使用方法:
    sigset_t mask, prev;
    
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    
    sigprocmask(SIG_BLOCK, &mask, &prev);   // 阻塞mask中的信号SIGINT
    //...程序不会被 SIGINT 信号打断
    sigprocmask(SIG_SETMASK, &prev, NULL);  // 恢复程序的阻塞信号集
    

:阻塞信号时,信号仍能发送到进程并成为待处理信号,但会阻塞而不是被立即处理。

等待信号

可以使用sigsuspend函数挂起当前进程并等待某一个信号:

#include <signal.h>

int sigsuspend(const sigset_t *mask);
                                // 返回:-1

该函数首先使用mask替换当前的阻塞集合,然后挂起进程直到进程收到一个信号。该函数可以用来避免一些多进程并发中的同步问题,例如对于如下程序:

void handler(int sig) {
    printf("signal SIGUSR1 got.\n");
}
int main(int argc, char *argv[]) {
    printf("here we go...\n");
    signal(SIGUSR1, handler);
    pid_t pid = fork();
    if (pid == 0) {
        printf("child: I will do something.\n");
        kill(getppid(), SIGUSR1);
        printf("child: I will do some other things and exit.\n");
        exit(0);
    }
    // 希望在子进程发送SIGUSR1信号后执行的任务
    printf("parent: I will do something after child send SIGUSR1\n");
    wait(NULL); // recycle child
    return 0;
}

上述程序,父进程希望在子进程发送SIGUSR1信号后执行一些任务,由于父子进程并发执行,因此无法断定何时子进程发送信号。虽然可以使用sleeppause函数使父进程休眠,但这种方式并不妥当,并且子进程可能在父进程调用pause前就发送信号。比如上述程序的输出:

here we go...
parent: I will do something after child send SIGUSR1
child: I will do something.
child: I will do some other things and exit.
signal SIGUSR1 got.

解决方法是使用sigprocmasksigsuspend函数,如下为修改后的main函数:

int main(int argc, char *argv[]) {
    printf("here we go...\n");
    signal(SIGUSR1, handler);

    sigset_t mask, prev;
    sigemptyset(&mask);
    sigaddset(&mask, SIGUSR1);                  // 阻塞SIGUSR1信号
    sigprocmask(SIG_BLOCK, &mask, &prev);

    pid_t pid = fork();
    if (pid == 0) {
        sigprocmask(SIG_SETMASK, &prev, NULL);  // 子进程继承父进程阻塞集,因此要恢复子进程的阻塞集合
        printf("child: I will do something.\n");
        kill(getppid(), SIGUSR1);
        printf("child: I will do some other things and exit.\n");
        exit(0);
    }

    sigsuspend(&prev);                          // 取消阻塞,挂起进程并等待信号
    printf("parent: I will do something after child send SIGUSR1\n");
    sigprocmask(SIG_SETMASK, &prev, NULL);

    wait(NULL); // recycle child
    return 0;
}

可以看到,父进程首先阻塞了SIGUSR1信号,之后运行子进程,此时即便子进程先调度并给父进程发送SIGUSR1信号,也会加入父进程的待处理信号集合中。之后父进程调用sigsuspend函数,取消阻塞、挂起进程并等待信号,便可以保证在子进程发送SIGUSR1后再执行接下来的任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值