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

  • 相关阅读:
    php安全编程&python测试实例编写
    MySQL注入技巧性研究
    第一届“百度杯”信息安全攻防总决赛
    不想在315“中奖”?你得躲过这些坑!
    这些故事你尽管听,不奇葩算我输!
    str2-045漏洞事件,你想要的这里都有
    python多线程在渗透测试中的应用
    【ZCTF】easy reverse 详解
    UVA
    用Thinphp发送电子邮件的方法
  • 原文地址:https://www.cnblogs.com/w-x-me/p/6399350.html
Copyright © 2011-2022 走看看