Linux编程–信号

1、信号的概念:共性

  • 简单
  • 不能携带大量信息
  • 满足某个特设条件

2、与信号相关的事件与状态

  • 键盘产生:Ctrl+C、Ctrl+Z等;
  • 系统调用:kill、raise、abort
  • 软件条件:定时器
  • 硬件异常:非法访问、除0等;
  • 命令产生:kill命令

3、信号的处理方式

  • 执行默认动作
    • Term:终止进程
    • Ign:忽略信号
    • Core:终止进程,生成Core文件(查看死亡原因,用于gdb调试)
    • Stop:停止(暂停)进程
    • Cont:继续停止(暂停)进程
  • 忽略(丢弃)
  • 捕捉(调用户处理函数)

4、信号四要素

  • 编号
  • 名称
  • 事件
  • 默认处理动作

5、常用信号

~$ 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 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 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

下面我们对编号小于SIGRTMIN的信号进行讨论。

  1. SIGHUP 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也 能继续下载。此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

  2. SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

  3. SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

  4. SIGILL 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

  5. SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用。

  6. SIGABRT 调用abort函数生成的信号。

  7. SIGBUS 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

  8. SIGFPE 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

  9. SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

  10. SIGUSR1 留给用户使用

  11. SIGSEGV 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

  12. SIGUSR2 留给用户使用

  13. SIGPIPE 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

  14. SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

  15. SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

  16. SIGCHLD 子进程结束时, 父进程会收到这个信号。如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

  17. SIGCONT 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

  18. SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

  19. SIGTSTP 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

  20. SIGTTIN 后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

  21. SIGTTO 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

  22. SIGURG 有”紧急”数据或out-of-band数据到达socket时产生.

  23. SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

  24. SIGXFSZ 当进程企图扩大文件以至于超过文件大小资源限制。

  25. SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

  26. SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

  27. SIGWINCH 窗口大小改变时发出.

  28. SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作.

  29. SIGPWR Power failure

  30. SIGSYS 非法的系统调用

6、信号的产生

  • 终端按键产生
    • Ctrl + c —> 2)SIGINT (终止/中断) “INT” — Interrupt
    • Ctrl + z —> 20)SIGTSTP (暂停/停止) “T” — Terminal 终端
    • Ctrl + \ —> 3)SIGQUIT (退出)
  • 硬件异常产生信号
    • 除0操作 —> 8)SIGFPE(浮点数例外)”F” — float 浮点数
    • 非法访问内存 —> 11)SIGSEGV(段错误)
    • 总线错误 —> 7)SIGBUS
  • kill函数/命令产生信号

    • kill -SIGKILL pid
    • kill函数
    • int kill (pid_t pid, int sig);
    • raise函数,给当前进程发送信号
    • int raise(int sig)
    • abort函数,给当前进程发送SIGBRT信号
    • void abort(void)
    #include <stdlib.h>
    #include <signal.h>
    #include <stdio.h>
    #include <unistd.h>
    
    int main(){
      int ret = kill(getpid(), SIGKILL);
      if (ret == -1){
          perror("kill error");
          exit(1);
      }
      printf("if i am alived?!\n");
      return 0;
    }
    

    执行结果

    Killed
    

    多进程是杀死指定进程

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    
    int main(){
      pid_t kill_pid;
      int i = 0;
      for (i; i < 5; i++){
          pid_t pid = fork();
          if (i == 2) kill_pid = pid;
          if (pid < 0){
              perror("fork error");
              exit(1);
          }else if (pid > 0){
              // 父进程
          }else {
              // 子进程
              break;
          }
      }
      // 只有父进程能进来
      if (i == 5){
          // kill指定进程id,也可以传0(同一进程组的所有进程)、-1(进程权限中的所有进程)、<0(对应的组)
          // kill中pid与waitpid函数中的pid类似
          kill(kill_pid, SIGKILL);
      }
      while(1){
          sleep(1);
      }
      return 0;
    }
    
  • 软件条件产生信号
    • alarm函数
    • 每个进程有且只有一个定时器
    • unsigned int alarm(unsigned int seconds); // 返回0或剩余秒数
    • alarm返回的是上一次定时所剩余的时间,alarm(0)可以取消闹钟
    • alarm只能定时一次,并且是秒级精度,自然计时
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(){
      int i = 0;
      alarm(1);
      for (i;;i++){
          printf("%d\n", i);
      }
      return 0;
    }
    

    time ./alarm.out 可以查看当前执行所消耗的时间
    ./alarm.out > out 可以大大提高程序的执行次数,I/O操作非常消耗系统资源

  • setitimer函数:可代替alarm函数,精度微秒us,可以实现周期定时

    • int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value)
    • 参数which
    • ITIMER_REAL:自然定时,计算自然时间,与alarm相同
    • ITIMER_VIRTUAL:虚拟空间计时,只计算进程占用CPU的时间(用户空间)
    • ITIMER_PROF:运行时计时,计算占用cpu及执行系统调用的时间(用户空间+系统空间)
    • 参数new_value:定时时长
    • 参数old_vlaue:上一次定时剩余时间,与alarm的返回值意义相同
    • 计时开始时,有效数据是new_value
    • it_value为延时开始时间
    • it_interval为间隔时间
    • 延时it_value时间,发送信号,再将it_interval赋值给it_value,时间到后发送信号,重复执行
    • it_value为0则不发送信号
    • it_interval为0,则不重复
    struct itimerval{
      struct timerval it_interval;     // 间隔时间
      struct timerval it_value;        // 延时时间
    }
    struct timerval{
      time_t      tv_sec;        // 秒
      suseconds_t tv_usec;       // 微秒
    }
    
  • 使用setitimer函数实现alarm函数功能
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/time.h>
    
    unsigned int my_alarm(unsigned int second){
      struct itimerval new_value, old_value;
      new_value.it_interval.tv_sec = 0;
      new_value.it_interval.tv_usec = 0;
      new_value.it_value.tv_sec = second;
      new_value.it_value.tv_usec = 0;
      int ret = setitimer(ITIMER_REAL, &new_value, &old_value);
      if (ret == -1){
          perror("setitimer error");
          exit(1);
      }
      return old_value.it_interval.tv_sec;
    }
    
    int main(){
      my_alarm(1);
      int i;
      for (i = 0; ;i++){
          printf("%d\n", i);
      }
      return 0;
    }
    
  • signal函数
    • signal函数可以捕捉信号
    • typedef void (*sighandler_t)(int);
    • sighandler_t signal(int signum, sighandler_t handler);
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/time.h>
    #include <signal.h>
    
    unsigned int my_alarm(unsigned int seconds){
      struct itimerval new_value, old_value;
      new_value.it_value.tv_sec = seconds;
      new_value.it_value.tv_usec = 0;
      new_value.it_interval.tv_sec = 2;
      new_value.it_interval.tv_usec = 0;
      int ret = setitimer(ITIMER_REAL, &new_value, &old_value);
      if (ret == -1){
          perror("setitimer error");
          exit(1);
      }
      return old_value.it_value.tv_sec;
    }
    void func(int sign){
      printf("sign:%d\n", sign);
    }
    int main(){
      my_alarm(1);
      signal(SIGALRM, func);
      int i = 0;
      for(i;;i++){
          printf("%d\n", i);
          sleep(1);
      }
      return 0;
    }
    

7、信号集

  • 信号集设置数据类型:sigset_t
    // 清空集合
    int sigemptyset(sigset_t *set);
    // 填充集合
    int sigfillset(sigset_t *set);
    // 添加信号
    int sigaddset(sigset_t *set, int signum);
    // 删除信号
    int sigdelset(sigset_t *set, int signum);
    // 信号是否在集合中
    int sigismember(const sigset_t *set, int signum);
    
  • 阻塞信号集(信号屏蔽字)
    • sigprocmask函数
    • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    • how参数
      • SIG_BLOCK:将当前set做为mask添加信号
      • SIG_UNBLOCK:将当前set做为mask删除信号
      • SIG_SETMASK:将当前set做为信号集直接覆盖
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    int main(){
      // 定义两个信号集
      sigset_t set, old_set;
      // 在set信号集中添加SIGALRM信号
      int ret = sigaddset(&set, SIGALRM);
      if (ret == -1){
          perror("sigaddet error");
          exit(1);
      }
      // 将信号集添加到阻塞信号集中,阻塞set中的信号
      ret = sigprocmask(SIG_BLOCK, &set, &old_set);
      if (ret == -1){
          perror("sigprocmask error");
          exit(1);
      }
      // 1秒后发送SIGALRM信号,进程无法正常接收到该信号
      alarm(1);
      int i = 0;
      while(i++){
          printf("-----------%d\n", i);
          sleep(1);
      }
      return 0;
    }
    
  • 未决信号集
    • int sigpending(sigset_t *set);
    • 传出参数set,可获取未决信号集
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <unistd.h>
    
    // 打印1~32号信号集状态
    void print_sig(sigset_t sigset){
      int i;
      for(i=1;i<32;i++){
          // 循环遍历信号集中各个信号,并以0、1形式输出
          int ismb = sigismember(&sigset, i);
          putchar(ismb == 0 ? '0':'1');
      }
      putchar('\n');
    }
    
    int main(){
      sigset_t mask, set, old_set;
      // 创建信号屏蔽字mask,并将相关信号加入集合
      sigemptyset(&mask);
      sigaddset(&mask, SIGINT);
      sigaddset(&mask, SIGTSTP);
      sigaddset(&mask, SIGQUIT);
      // 将mask合并到阻塞信号集
      sigprocmask(SIG_BLOCK, &mask, &old_set);
      while(1){
          // 循环打印未决信号集,被阻塞的信号会停留在未决信号集中
          sigpending(&set);
          print_sig(set);
          sleep(1);
      }
      return 0;
    }
    

8、信号捕捉

  • signal函数
    • typedef void (*sighandler_t)(int);
    • sighandler_t signal(int signum, sighandler_t handler);
    #include <stdio.h>
    #include <stdlib.h>
    #include <signal.h>
    
    // 信号捕捉函数返回值为函数指针
    typedef void (*sighandler_t)(int);
    void sigcatch(int signal){
      printf("catch signal : %d\n", signal);
    }
    int main(){
      // 捕捉成功返回传入的捕捉函数指针,失败则返回宏SIG_ERR
      sighandler_t handler = signal(SIGINT, sigcatch);
      if (handler == SIG_ERR){
          perror("signal error");
          exit(1);
      }
      while(1);
      return 0;
    }
    
  • sigaction函数
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    struct sigaction {
      void     (*sa_handler)(int);
      void     (*sa_sigaction)(int, siginfo_t *, void *);
      sigset_t   sa_mask;    // 信号屏蔽字,在捕捉函数执行期间,临时代替阻塞信号集
      int        sa_flags;
      void     (*sa_restorer)(void);
    };
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    
    // 信号捕捉回调函数
    void sigcatch(int signal){
      printf("%d signal is catched ...\n", signal);
      sleep(5);// 睡眠10秒,在此期间sigaction中的sa_mask生效
      printf("----finish--------------\n");
    }
    
    int main(){
      // 定义一个sigaction结构体
      struct sigaction sa;
      // 重点关注3个变量:1、回调函数
      sa.sa_handler = sigcatch;
      // 重点关注3个变量:2、信号集合
      sigemptyset(&sa.sa_mask);
      sigaddset(&sa.sa_mask, SIGINT);
      // 重点关注3个变量:3、标记默认传0即可
      sa.sa_flags = 0;
      // 捕捉Ctrl+Z信号
      int ret = sigaction(SIGTSTP, &sa, NULL);
      if (ret == -1){
          perror("sigaction error");
          exit(1);
      }
      // 运行后,Ctrl+Z会被捕捉,其他信号执行默认行为
      // Ctrl+Z被捕捉sigcatch函数执行期间,Ctrl+C会被阻塞,Ctrl+Z被阻塞放进未决信号集,捕捉函数结束后立即触发
      while(1);
      return 0;
    }
    
  • 信号捕捉特性
    • 进程正常运行时,默认pcb有一个信号屏蔽字,它决定了进程屏蔽哪些信号,当某个信号被捕捉进入捕捉函数时,信号屏蔽暂时由sa_mask来指定,函数执行结束后,恢复为原信号屏蔽字;
    • 函数执行期间,被捕捉的信号自动屏蔽(处于阻塞状态,函数执行结束后会被立即执行);
    • 阻塞的常规信号不支持排队,产生多次只记录一次(后32个实时信号支持排队)
  • 内核实现信号捕捉的过程
    1. 执行主控制流程时,因为中断或异常,进行内核;
    2. 内核处理当前进程可以递送的信号。
    3. 执行默认动作回用户模式,如果被捕捉执行用户捕捉函数;
    4. 执行用户捕捉函数,结束后执行特殊的系统调用sigreturng回到内核;
    5. 内核返回用户模式,从上次被中断的地方继续向下执行;

Leave a Reply