跳转至

信号

  • 信号: 通知进程系统中发生了一个某种类型的事件
    • 每种信号类型都对应于某种系统事件. 低层的硬件异常时由内核异常处理程序处理的, 正常情况下, 对用户进程而言是不可见的. 信号提供了一种机制, 通知用户进程发生了这些异常 image.png

信号术语

  • 传送一个信号到目标进程的过程:
    • 发送信号: 内核通过更新目的进程上下文中的某个状态, 发送一个信号给目的进程
      • 发送信号的两种原因:
        • 内核检测到一个系统事件
        • 一个进程调用了kill函数, 显式的要求内核发送一个信号到目的进程
    • 接收信号: 当目的进程被内核强迫以某种方式对信号的发送做出反应时, 即接收到了信号
      • 进程可以忽略这个信号, 终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号
      • 信号处理程序: image.png
  • 待处理信号: 发出但是没有被接收
    • 在任何时刻, 一种类型至多只会有一个待处理信号
    • 后续到来的该类型信号不会进行排队等待, 而是会被丢弃
  • 一个进程可以有选择性的阻塞接收某种信号
    • 当一种信号被阻塞时候, 仍然可以被发送, 但是产生的待处理信号不会被接收, 直到进程取消对该信号的阻塞
  • 一个待处理信号最多只能被接收一次

发送信号

  • UNIX提供了大量想进程发送信号的机制, 而这些机制都是基于进程组(process group)

进程组

  • 每个进程都只属于一个进程组
  • 使用getpgrp函数返回当前进程的进程组ID
    #include <unistd.h>
    
    // 返回调用进程的进程组ID
    pid_t getpgrp(void);
    
  • 一个进程可以使用setpgid函数来改变自己或者其他进程的进程组:
    • 如果pid是0, 则使用当前进程的PID
    • 如果pgid是0, 则使用pid指定的进程PID作为进程组ID
      • setpgid(0, 0): 创建一个新的进程组, 进程组ID为我自己
        #include <unistd.h>
        
        // success: 0, else -1
        int setpgid(pid_t pid, pid_t pgid);
        

使用/bin/kill程序发送信号

从键盘发送信号

  • 在任何时刻, 至多只有一个前台作业0个或多个后台作业:
  • ls | sort
    • 这个命令会创建一个由两个进程组成的前台作业, 这两个进程是通过unix管道连接的:image.png
      • lssort
  • Ctrl+C: SIGINT, 终止前台作业
  • Ctrl+Z: SIGSTP, 挂起前台作业

使用kill函数发送信号

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

使用alarm函数发送信号

#include <unistd.h>

// return: 前一次闹钟剩余的描述, 若以前没有设定闹钟, 则为0
unsigned int alarm(unsigned int secs);

接收信号

  • 当内核把进程p从内核模式切换到用户模式时, 它会检查进程p的未被阻塞的待处理信号集合
    • 如果这个集合为, 则内核将控制传递给p的逻辑控制流的下一条指令(正常执行)
    • 非空: 内核选择集合中的某个信号k, 并且强制p接收信号k, 执行信号处理程序
  • 信号处理程序的行为:
    • 进程终止
    • 进程终止并转储内存
    • 进程停止直到被SIGCONT信号重启
    • 进程忽略该信号
  • 进程可以通过使用signal函数修改和信号相关联的默认行为
    • 例外: SIGSTOPSIGKILL的默认行为是不可修改的
      #include <signal.h>
      
      typedef void (*sighandler_t)(int);
      
      // return: 成功则为指向前次处理程序的指针, 出错则为SIG_ERR
      sighandler_t signal(int signum, sighandler_t handler);
      
    • signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
      1. handler==SIG_IGN: 忽略类型为signum的信号
      2. handler==SIG_DFL: 类型为signum的信号行为恢复为默认行为
      3. handler为用户定义的函数地址

image.png - 解析: - 按下Ctrl+C时, sleep被强制中断, sleep函数返回剩余的秒数(因为我们定义的信号处理, 仅仅是返回, 而不是结束程序), 接下来打印出剩余的秒数即可, 并且程序会正常的结束

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

unsigned int snooze(unsigned int secs) {
// 按下Ctrl+C时, sleep被强制中断
  unsigned int secs_left = sleep(secs);
  printf("Slept for %d of %d secs\n", secs - secs_left, secs);
  return secs_left;
}

// 信号处理的方法, 直接返回
void sign_handler(int sig) { return; }

int main(int argc, char *argv[]) {
  unsigned int secs;
  // 设置信号处理程序
  if (signal(SIGINT, sign_handler) == SIG_ERR) {
    perror("signal error");
  }
  if (argc > 1) {
    secs = atoi(argv[1]);
  }
  unsigned int rc = snooze(secs);
  printf("rc");
  return 0;
}


阻塞和解除阻塞信号

  • 两种阻塞信号的机制:

    • 隐式阻塞机制: 内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号
    • 显式阻塞机制: 应用程序可以使用sigprocmask函数和它的辅助函数, 明确的阻塞和接触阻塞选定的信号
      #include <signal.h>
      
      int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
      int sigemptyset(sigset_t *set);
      int sigfillset(sigset_t *set);
      int sigaddset(sigset_t *set, int signum);
      // 返回: 如果成功则为0, 出错则为-1
      int sigdelset(sigset_t *set, int signum);
      // 返回: 若signum是set的成员则为1, 如果不是则为0, 出错则为-1
      int sigismember(const sigset_t *set, int signum);
      
  • sigprocmask:

    • how:
      • SIG_BLOCK: 将set中的信号添加到blocked
      • SIG_UNBLOCK: 从blocked中删除set中的信号
      • SIG_SETMASK: block=set
    • 如果oldset非空, 则blocked位向量之前的值保存在oldset
  • set信号集合进行操作的函数
    • sigemptyset: 初始化set集合为空集合
    • sigfillset: 将每个信号都添加进set
    • sigaddset: 把signum添加到set
    • sigdelset: 从set中删除signum