zoukankan      html  css  js  c++  java
  • (已转)Linux基础第六章 信号

    6.1 前言

    本章简单描述信号。信号是Linux系统中,内核和进程通信的一种方式。如果内核希望打断进程的顺序执行,那么内核会在进程的PCB中记录信号。而当这个进程被分配到CPU,进入执行状态时,首先会检查是否有信号,如果有信号,那么进程会先尝试执行信号处理函数。

    内核需要打断进程运行的时机:

    • 进程无法继续了

    int* p = NULL;
    *p = 100;
    // 此时代码无法再继续运行,内核会发送SIGSEGV信号给进程,这是我们常见的段错误
    int a = 0;
    int b = 1/a;
    // 除0操作,发送SIGFPE给进程
    • 按下ctrl+c
      ctrl+c其实是bash向前台进程组发送SIGINT

    int main()
    {
        fork();
        fork(); // 四个进程
        while(1)
        {
            sleep(1);
        }
    }

    运行该程序后,再按Ctrl+c,结果是四个进程全部退出

    int main()
    {
        signal(SIGINT, SIG_IGN);
        fork();
        fork(); // 四个进程
        while(1)
        {
            sleep(1);
        }
    }

    有了signal的处理之后,ctrl+c发送的SIGINT不会导致进程退出。

    6.2 信号类型

    通过kill -l命令可以看到系统定义的信号类型,信号值小于32的是传统的信号,称之为非实时信号,而大于32的称之为实时信号。这里只讨论非实时信号。

    6.3 信号的处理

    可以通过signal函数,注册信号处理函数。如果没有注册信号处理函数,那么按照默认方式处理。
    也可以通过signal设置忽略信号。

    信号默认处理动作发出信号的原因
    SIGHUP A 进程session leader退出时,同session的其他进程会收到这个信号
    SIGINT A Ctrl+C
    SIGQUIT C Ctrl+D
    SIGILL C 非法指令
    SIGABRT C 调用abort函数产生的信号
    SIGFPE C 浮点异常
    SIGKILL AEF Kill信号
    SIGSEGV C 无效的内存引用
    SIGPIPE A 管道破裂: 写一个没有读端口的管道
    SIGALRM A 由alarm(2)发出的信号
    SIGTERM A 终止信号
    SIGUSR1 A 用户自定义信号1
    SIGUSR2 A 用户自定义信号2
    SIGCHLD B 子进程状态变化会给父进程发送SIGCHLD信号
    SIGCONT   进程继续(曾被停止的进程)
    SIGSTOP DEF 暂停进程
    SIGTSTP D 控制终端(tty)上按下停止键
    SIGTTIN D 后台进程企图从控制终端读
    SIGTTOU D 后台进程企图从控制终端写

    A 缺省的动作是终止进程
    B 缺省的动作是忽略此信号
    C 缺省的动作是终止进程并进行内核映像转储(dump core)
    D 缺省的动作是停止进程
    E 信号不能被捕获
    F 信号不能被忽略

    因为SIGKILL不能被捕获,那么以下代码是不正常
    signal(SIGKILL, handle) //xxxx
    #include <stdio.h>
    #include <signal.h>
    #include <stdlib.h>
    
    void signal_handle(int a)
    {
        if(a == SIGINT)
            printf("signal_handle
    ");
        else if(a == SIGABRT)
            printf("abrt
    ");
        else if(a == SIGALRM)
            printf("alarm
    ");
        else if(a == SIGCHLD)
            printf("child
    ");
        else if(a == SIGUSR1)
            printf("usr1 signal
    ");
    }
    
    int main()
    {
        // SIGINT 2
        signal(SIGINT, signal_handle);
        signal(SIGABRT, signal_handle);
        signal(SIGALRM, signal_handle);
        signal(SIGCHLD, signal_handle);
        signal(SIGUSR1, signal_handle);
    
        pid_t pid  = fork();
        if(pid == 0)
            return 0;
    
    
        // 给自己发送一个abrt信号
        //abort();
        alarm(1);
    
        while(1)
        {
            sleep(1);
        }
    }

    6.4 不可靠信号

    信号值小于32的都是不可靠信号,假如进程收到一个信号来不及处理,这时候又收到一个同样的信号,那么这两个信号会合并成一个信号,这个原因是因为进程保存该信号的值只有1位。

    6.5 中断系统调用(中断阻塞)

    假如一个进程调用了某系统调用导致该进行处于挂起状态,而此时该进程接收到一个信号,那么该系统调用被唤醒。通常该系统调用会返回-1,错误码是EINTR

    也有些系统调用,可以设置打断后重启,这样就不会被信号打断,具体参考man 7 signal

    如果使用signal函数注册信号处理函数,默认被中断的系统调用是自动重启的。

    // 阻塞
    int ret = read(0, buf, sizeof(buf));
    printf("read data ");
    #include <signal.h>
    #include <stdio.h>
    #include <errno.h>
    void handle(int v){
        printf("ctrl+c
    ");
    }
    
    int main()
    {
        signal(SIGINT, handle);
        
        char buf;
        int ret = read(0, buf, sizeof(buf));  // read被中断打断了
        printf("ret = %d, errno=%d, EINTR=%d
    ", ret, errno, EINTR); // EINTR
    }

    6.6 可重入问题

    信号会导致可重入问题,比如一个全局链表。

    std::vector<int> v;
    void handler(int sig)
    {
      v.push_back(0);
    }
    int main()
    {
      signal(SIGINT, handler);
        signal(SIGUSR1, handler);
      while(1)
      {
            // 先屏蔽所有信号
        v.push_back(0);
            // 再去掉屏蔽
      }
    }

    以上代码在一定情况下会崩溃,在main函数中不停调用push_back,如果在push_back执行一半时,被中断打断,然后去执行中断处理函数时,那么该中断处理函数的push_back会崩溃。

    有些系统调用本身带有局部静态变量,因此那些函数不能在信号处理函数中调用,比如strtokreaddir等,对应的可重入的函数是strtok_rreaddir_r

    6.7 发送信号

    可以通过kill函数发送信号。

    调用kill
    发送信号
    进程1
    内核
    进程2

    kill也可以进程组发送信号

    #include <sys/types.h>
    #include <signal.h>
    
    int main()
    {
        kill(27054, SIGUSR1);    
    }

    补充:掩盖信号

    //掩盖不可靠信号
    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    // 掩盖SIGINT
    // 掩盖一个可靠信号
    void handle(int v)
    {
        printf("sigint 
    ");
    }
    
    int main()
    {
        signal(SIGINT, handle);
        sigset_t set;
    
        // 将set集合设置为空
        sigemptyset(&set);
        // 将SIGINT信号加入集合
        sigaddset(&set, SIGINT);
        // 把这个集合代表的信号,加入信号掩码
        sigprocmask(SIG_BLOCK, &set, NULL);
    
        // 从此,该进程收到SIGINT,不会被处理
        
        sleep(5);
        printf("remove SIGINT from mask
    ");
    
        // 去掉SIGINT的掩码
        sigprocmask(SIG_UNBLOCK, &set, NULL);
    
    
        while(1)
        {
            sleep(1);
        }
    }
    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    // 掩盖34
    // 掩盖一个可靠信号
    void handle(int v)
    {
        printf("hahahaha 
    ");
    }
    
    int main()
    {
        signal(34, handle);
        sigset_t set;
    
        // 将set集合设置为空
        sigemptyset(&set);
        // 将34信号加入集合
        sigaddset(&set, 34);
        // 把这个集合代表的信号,加入信号掩码
        sigprocmask(SIG_BLOCK, &set, NULL);
    
        // 从此,该进程收到34,不会被处理
        
        kill(getpid(), 34);
        kill(getpid(), 34);
        kill(getpid(), 34);
        kill(getpid(), 34);
        kill(getpid(), 34);
    
        sleep(5);
        printf("remove 34 from mask
    ");
    
        // 去掉34的掩码
        sigprocmask(SIG_UNBLOCK, &set, NULL);
    
    
        while(1)
        {
            sleep(1);
        }
    }

    6.8 忽略信号

    signal(SIGPIPE, SIG_IGN);
    #include <signal.h>
    
    void handle(int v)
    {
    
    }
    
    int main()
    {
        // 表示忽略SIGINT
        // 忽略信号和掩盖信号是有区别:
        //
        // 未决的信号:已经发出但是没有被处理的信号叫未决的信号
        signal(SIGINT, SIG_IGN);
    
        // 从这里开始就不忽略
        signal(SIGINT, handle); // 设置一个处理函数
        signal(SIGINT, SIG_DFL); // 恢复成默认处理
        while(1)
        {
            sleep(1);
        }
    }

    以上例子,忽略SIGPIPE信号,那么进程收到SIGPIPE后,不会有任何反应。

    6.9 屏蔽信号

    屏蔽和忽略不同,忽略意味着在忽略期间,接收的信号就被忽略了。而屏蔽的意思,是暂时屏蔽,屏蔽期间收到的信号依旧在,如果某一时刻该信号不再忽略时,该信号的处理程序会被调用。

    设置屏蔽集合,使用sigprocmask

    6.10 SIGCHLD

    SIGCHLD信号产生于子进程退出和状态变化,父进程通常在该信号的处理函数中,调用wait来回收子进程的PCB,这样可以避免阻塞。

    #include <signal.h>
    
    void chld_handle(int v)
    {
        // 有子进程退出了
        // wait(NULL);
        //
        while(1) // 使用while(1)是避免有多个子进程同时退出,由于SIGCHLD是不可靠信号,函数只会调用一次
        {
            int ret = waitpid(-1, NULL, WNOHANG); // 每次回收都应该用非阻塞方式去回收
            if(ret == -1)  // 没有僵尸进程了,之前僵尸进程已经被回收了
                break;
        }
    }
    
    int main()
    {
        // 等待子进程的结束,问题是:ddddd
        // 它阻塞主进程的执行,影响效率
        // wait(NULL);
        //
        signal(SIGCHLD, chld_handle);
    
        pid_t pid = fork();
        if(pid == 0) return 0;
    
        while(1)
        {
            sleep(1);
        }
    }

    6.11 sigaction和sigqueue

    sigaction和signal一样用来注册信号处理函数,siqqueue和kill一样,用来发送信号,但是sigaction比signal功能强大,signal比较简单。

    强大:

    1. 可以传递参数

    2. 可以获得发送信号的进程信息

    3. 可以设置SA_RESTART

      #include <signal.h>
      #include <stdio.h>
      #include <errno.h>
      //void handle(int v){}
      //
      // 新的信号处理函数
      void handle(int v, siginfo_t* info, void* p)
      {
          printf("ctrl+c
      ");
      }
      int main()
      {
          // 默认的signal已经有SA_RESTART这个flag了
          //signal(SIGINT, handle);
          
          struct sigaction sig;
          sig.sa_handler = NULL;
          sig.sa_sigaction = handle;
          sigemptyset(&sig.sa_mask);
       //   sig.sa_flags = 0;
          sig.sa_flags = SA_RESTART; // 让阻塞的系统调用,被这个信号打断之后,要重启
          sig.sa_restorer = NULL; // 在Linux下没用,直接填NULL就可以了
          sigaction(SIGINT, &sig, NULL);
      
          char buf;
          int ret = read(0, buf, sizeof(buf));  // read被中断打断了
          printf("ret = %d, errno=%d, EINTR=%d
      ", ret, errno, EINTR); // EINTR
      }
      #include <stdio.h>
      #include <stdlib.h>
      #include <signal.h>
      
      char buf[1024];
      
      void handle_data()
      {
          printf("user input is %s
      ", buf);
      }
      
      void handle_chld1(int v)
      {
          while(1)
          {
              int ret = waitpid(-1, NULL, WNOHANG);
              if(ret < 0) break;
          }
      }
      
      void handle_chld(int v, siginfo_t* info, void* p)
      {
          handle_chld1(v);
      }
      
      int main()
      {
      //   signal(SIGCHLD, handle_chld);
      
          struct sigaction act;
          act.sa_handler = NULL;
          act.sa_sigaction = handle_chld;
          sigemptyset(&act.sa_mask);
          act.sa_flags = 0;
          act.sa_restorer = NULL;
          sigaction(SIGCHLD, &act, NULL);
      
          while(1)
          {
          //    char* ret = fgets(buf, sizeof(buf), stdin);
          //    if(ret == NULL) break;
              
              int ret = read(0, buf, sizeof(buf));
              if(ret < 0)
              {
                  // 说明read出错,不是真正的出错,而是被中断打扰了
                  // 那此时,应该重新调用read函数,去获取信息
                  if(errno == EINTR)
                  {
                      continue;
                  }
                  // 如果是其他错误原因,就break
                  break;
              }
      
              pid_t pid = fork();
              if(pid == 0)
              {
                  handle_data();  // 创建一个子进程去处理数据
                  exit(0);
              }
          }
      }
      #include <signal.h>
      #include <stdio.h>
      void handle(int v, siginfo_t* info, void* p)
      {
          printf("recv SIGINT, arg=%d
      ", info->si_value.sival_int);
      }
      
      int main()
      {
          struct sigaction act;
          act.sa_handler = NULL;
          act.sa_sigaction = handle;
          sigemptyset(&act.sa_mask);
          act.sa_flags = SA_SIGINFO|SA_RESTART;
          act.sa_restorer = NULL;
      
          // 注册信号处理函数
          sigaction(SIGINT, &act, NULL);
      
          union sigval v;
          v.sival_int = 99;
          // 相当于kill,但是它可以传递一个参数
          sigqueue(getpid(), SIGINT, v);
      
          getchar();
      }
      #include <signal.h>
      #include <stdio.h>
      
      int main()
      {
          char* p = malloc(100);
          union sigval v;
        //  v.sival_int = 98;
          v.sival_ptr = p;
          // 相当于kill,但是它可以传递一个参数
          sigqueue(28360, SIGINT, v);
      
          getchar();
      }

    6.12 函数命令

    signal:注册信号处理函数
    kill:发送信号
    sigprocmask:设置信号掩码
    sigemptyset:清空信号集
    sigfillset:设满信号集
    sigaddset:往信号集增加一个信号
    sigdelset:从信号集删除一个信号
    sigismember:判断信号否则在信号集

    sigaction:注册更加强大的处理函数
    sigqueue:发送信号

    abort
    alarm
    pause

  • 相关阅读:
    LeetCode偶尔一题 —— 617. 合并二叉树
    《剑指offer》 —— 链表中倒数第k个节点
    《剑指offer》 —— 青蛙跳台阶问题
    《剑指offer》—— 二维数组中的查找
    《剑指offer》—— 替换空格
    《剑指offer》—— 合并两个排序的链表
    《剑指offer》—— 礼物的最大价值
    生成Nuget 源代码包来重用你的Asp.net MVC代码
    Pro ASP.Net Core MVC 6th 第四章
    Pro ASP.NET Core MVC 6th 第三章
  • 原文地址:https://www.cnblogs.com/w-x-me/p/6399350.html
Copyright © 2011-2022 走看看