Linux进程信号

信号

        • 1. 信号概念
        • 2. 信号的产生
          • 2.1 硬件产生
            • 2.1.1 ctrl+c:SIGINT(2)
            • 2.1.2 ctrl+z:SIGTSTP(20)
            • 2.1.3 ctrl+\:SIGQUIT(3)
          • 2.2 软件产生
            • 2.2.1 kill命令
            • 2.2.2 kill函数
            • 2.2.3 abort函数
        • 3. 信号的注册
          • 3.1 位图加sigqueue队列
          • 3.2 非可靠信号的注册
          • 3.3 可靠信号的注册
        • 4. 信号的注销
          • 4.1 非可靠信号的注销
          • 4.2 可靠信号的注销
        • 5. 信号的处理
          • 5.1 SIG_DFL:默认处理方式
            • SIGCHILD信号
          • 5.2 SIG_IGN:忽略处理
          • 5.3 自定义信号处理函数
            • 5.3.1 signal函数
            • 5.3.2 sigaction函数
        • 6. 信号捕捉
        • 7. 信号阻塞
          • 7.1 概念
          • 7.2 阻塞函数sigprocmask
        • 8. volatile关键字

1. 信号概念

  • 信号是进程之间事件异步通知的一种方式,属于软件中断

  • 信号的种类:

    • 1~31:非可靠信号,信号有可能会丢失
    • 34~64:可靠信号,信号是不可能丢失的
  • 用 kill -l 命令可以查看系统定义的信号列表
    Linux进程信号_第1张图片
    每个信号都有一个编号和一个宏定义,这些信号各自在什么条件下产生,默认的处理动作,可以使用命令man 7 signal 查看。
    Linux进程信号_第2张图片

2. 信号的产生

2.1 硬件产生

通过终端按键产生信号

2.1.1 ctrl+c:SIGINT(2)
  • 默认处理动作是终止进程
  • ctrl+c 产生的信号只能发给前台进程。一个命令后面加个&可以将进程放到后台运行,使用fg可以将刚刚放到后台的进程重新放到前台来运行,这样shell不必等待进程结束就可以接受新的命令,启动新的进程。
    Linux进程信号_第3张图片
    可以看到将进程放到后台运行,是接收不到 ctrl+c 信号的,但是终端可以接受其他的命令,再将进程调到前台运行时,就可以接受到信号了。
  • shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接收到ctrl+c 这种信号。
2.1.2 ctrl+z:SIGTSTP(20)
  • 默认处理动作是停止前台进程,只能手动唤醒,尽量不要使用
2.1.3 ctrl+\:SIGQUIT(3)
  • 默认处理动作是终止进程并且Core Dump
    Linux进程信号_第4张图片

  • Core Dump:

    • 当一个进程异常终止时,可以选择把进程的用户空间内存数据全部保存在磁盘上,文件名通常是core,这就叫Core Dump。
    • 进程异常终止通常是有bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件已查看错误原因,这叫做事后调试。
    • 一个进程允许产生多大的core文件,取决于进程的Resource Limit(这个信息保存在PCB中)。默认不允许产生core文件,因为core文件中可能包含用户密码等敏感信息,不安全。
    • 解引用空指针、内存访问越界,进程就会收到11号信号(SIGSEGV),导致进程coredump,或者double free(多次释放同一块内存),进程会收到6号信号(SIGABRT),导致当前进程退出,产生coredump。
    • 可以使用命令 ulimit -c 1024 改变core文件大小,这只是临时生效的,想要永久生效,将命令写进 ~/.bash_profile 文件中并使之生效。
      Linux进程信号_第5张图片
2.2 软件产生
2.2.1 kill命令

kill -[信号] 进程pid
例:在后台运行一个死循环进程,然后在另一个终端发送终止信号
Linux进程信号_第6张图片
可以看到进程也被终止了。

2.2.2 kill函数

int kill(pid_t pid, int sig)
函数说明:

  • pid:要给哪一个进程发送信号
  • sig:要发送的具体信号值(可以直接传数字,也可以传宏定义)

代码示例:

#include 
#include 
#include 

int main()
{
  //给进程发送2号信号
  kill(getpid(),2);
 // kill(getpid(),SIGINT); 可以直接使用数字,也可以传宏定义
 // raise(2); //这两个函数都是成功返回0,失败返回-1
  while(1)
  {
    printf("linux\n");
    sleep(1);
  }
  return 0;
}

运行结果:

[test@localhost signal_test1]$ ./signal_test2

[test@localhost signal_test1]$

可以看到并没有打印,而是直接被终止了。
同样的除了kill函数给指定进程发送信号外,raise函数可以给当前进程发送指定的信号(即自己给自己发送信号)

2.2.3 abort函数

void abort(void)
说明:

  • abort函数是当前进程收到信号而异常终止
  • abort函数如同exit函数,总是会成功
  • 其实是封装了kill函数,相当于kill(getpid(),6)。

例:
Linux进程信号_第7张图片

3. 信号的注册

3.1 位图加sigqueue队列

位图:
Linux进程信号_第8张图片
sigqueue队列:

  • 用于添加sigqueue节点到sigqueue队列中,或者将节点出队
3.2 非可靠信号的注册

当进程收到一个非可靠信号:

  • 将非可靠信号对应的比特位更改为1
  • 添加sigqueue节点到sigqueue队列当中
    但是,如果在添加sigqueue节点时,队列中已经有了该信号的sigqueue节点,则不添加。
3.3 可靠信号的注册

当进程收到一个可靠信号:

  • 在sig位图中将该信号对应的比特位更改为1
  • 不论之前sigqueue队列当中是否存在该信号的sigqueue节点,都再次添加sigqueue节点到sigqueue队列当中。

4. 信号的注销

4.1 非可靠信号的注销
  • 将该信号的sigqueue节点从sigqueue队列当中进行出队操作
  • 信号在sig位图中对应的比特位由1置为0
4.2 可靠信号的注销
  • 将该信号的sigqueue节点从sigqueue队列当中进行出队操作

  • 需要判断sigqueue队列当中是否还有相同的sigqueue节点

    • 没有:将信号在sig位图中对应的比特位由1置为0
    • 还存在:不会将sig位图中对应的比特位由1置为0

5. 信号的处理

5.1 SIG_DFL:默认处理方式

收到信号,采取默认的处理方式

SIGCHILD信号
  • 在进程控制中,子进程先于父进程退出,会想父进程发送一个SIGCHILD信号,该信号默认处理动作是忽略,没有处理导致子进程成为僵尸进程。
  • 使用wait 和 waitpid 函数可以等待子进程退出,采用wait,父进程会阻塞等待子进程退出,使用waitpid需要循环处理,所以需要自定义SIGCHILD信号的处理函数。

代码示例:

#include 
#include 
#include 
#include 
#include 

void sigcb(int signo)
{
  //等待子进程退出
  wait(NULL);
  printf("wait success\n");
}
int main()
{

  signal(SIGCHLD,sigcb);  

  pid_t pid = fork();
  if( pid < 0 )
  {
    perror("fork");
    return -1;
  }else if(pid == 0)
  {
    //child
    sleep(5);
  }else{
    //parent    
    while(1)
    {
      printf("I am parent\n");
      sleep(1);
    }
  }
  return 0;
}

运行结果:
Linux进程信号_第9张图片
可以看到,在wait success之后,子进程没有成为僵尸进程

5.2 SIG_IGN:忽略处理

收到信号,不去处理

5.3 自定义信号处理函数

自定义处理函数是对9号信号没有用的
图示:
Linux进程信号_第10张图片

5.3.1 signal函数

可以更改信号的处理动作
函数:
typedef void (*sighandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler)

代码示例:

#include 
#include 
#include 

//改变了原有的处理方式
void sigcallback(int signo)
{
  printf("signo:%d\n",signo);
}
int main()
{
  //自定义信号捕捉函数
  signal(2,sigcallback); //当捕捉到2号信号时,调用sigcallback函数
  signal(20,sigcallback); //当捕捉到20号信号时,调用sigcallback函数

  while(1)
  {
    printf("linux\n");
    sleep(1);
  }
  return 0;
}

运行结果:
Linux进程信号_第11张图片

5.3.2 sigaction函数

可以更改信号的处理动作
函数:

  • int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact)
  • int sigemptyset(sigset_t *set):将比特位清零

函数说明:

  • signum:待更改的信号的值

  • struct sigaction:

    • void (*sa_handler)(int):函数指针,保存了内核对信号的处理方式
    • void (* sa_sigaction) (int,siginfo_t*,void*)
    • sigset_t sa_mask:保存的是当进程在处理信号的时候,收到的信号
    • int sa_flags:SA_SIGINFO,操作系统在处理信号的时候,调用的就是sa_sigaction函数指针当中保存的值0,在处理信号的时候,调用sa_handler保存的函数
    • void (*sa_restorer)(void):预留信息
  • act:将信号处理之前改变为act

  • oldact:信号之前的处理方式

代码示例:

#include 
#include 
#include 
#include 

void sigcallback(int signo)
{
  printf("signo:%d\n",signo);
}
int main()
{

  //sigaction
  struct sigaction act;
  //将位图比特位清零
  sigemptyset(&act.sa_mask);

  act.sa_flags = 0;
  //捕捉到信号需要调用的函数
  act.sa_handler = sigcallback;
  
  struct sigaction oldact;
 // sigaction(2,&act,NULL);
  //oldact是一个出参,保存的是信号原来的处理方式
  sigaction(3,&act,&oldact);

  getchar();
  // 又将信号处理方式改回去
  sigaction(3,&oldact,NULL);
  while(1)
  {
    printf("linux\n");
    sleep(1);
  }

  return 0;
}

运行结果:
Linux进程信号_第12张图片

6. 信号捕捉

调用系统调用函数的时候,或者调用库函数的时候(库函数大多数都是封装系统调用的函数的),会进入到内核空间
流程图示:
Linux进程信号_第13张图片

  • 如果没有信号处理,那就是1、2、6
  • 如果有信号处理,就是1、2、3、4、5、6

7. 信号阻塞

7.1 概念

相较于sig位图,阻塞是一个block位图

  • 信号的阻塞,并不会干扰信号的注册,信号该注册还是注册,只不过当前的进程不能立即处理
  • 当我们将block位图当中对应信号比特位置为1,表示当前进程阻塞该信号,当进程收到该信号,进程还是一如既往的对该信号进行注册
  • 当进程进入到内核空间,准备返回用户空间的时候,调用do_signal函数,不会立即去处理信号,一定不是之后不处理。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
7.2 阻塞函数sigprocmask

函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
函数说明:

  • how:告诉sigprocmask函数应该做什么操作

    • SIG_BLOCK:设置某个信号为阻塞
    • SIG_UNBLOCK:解除对某个信号的阻塞
    • SIG_SETMASK:替换阻塞位图
  • set:用来设置阻塞位图

    • SIG_BLOCK:设置某个信号为阻塞
      block(new) = block(old) | set
    • SIG_UNBLOCK:解除对某个信号的阻塞
      block(new) = block(old) & (~set)
    • SIG_SETMASK:替换阻塞位图
      block(new) = set
  • oldset:原来的阻塞位图

代码示例:

  • 对2号信号阻塞:
#include 
#include 
#include 
#include 
#include 

int main()
{
  sigset_t set;
  sigset_t oldset;
  //将所有信号比特位清零
  sigemptyset(&set);
  //将2号信号的比特位置为1
  sigaddset(&set,2);
  //阻塞2号信号
  sigprocmask(SIG_BLOCK,&set,&oldset);
  getchar();
  //解除阻塞
  sigprocmask(SIG_UNBLOCK,&set,NULL);

  while(1)
  {
    printf("linux\n");
    sleep(1);
  }
  return 0;
}

运行结果:
Linux进程信号_第14张图片
可以看到,在其他终端发送了2号信号后,信号被阻塞了,进程没有被终止,在按回车后,进程立即终止了,也就是说,阻塞并没有干扰信号的注册。

  • 对比非可靠信号和可靠信号的处理
#include 
#include 
#include 
#include 
#include 

void sigcallback(int signo)
{
  printf("signo:%d\n",signo);
}
int main()
{

  signal(3,sigcallback);
  signal(40,sigcallback);
  sigset_t set;
  //将所有信号阻塞 将比特位全部置为1
  sigfillset(&set);
  
  sigset_t oldset;
  sigprocmask(SIG_SETMASK,&set,&oldset);
  
  getchar();

  sigprocmask(SIG_SETMASK,&oldset,NULL);

  while(1)
  {
    printf("linux\n");
    sleep(1);
  }
  return 0;
}

运行结果:
Linux进程信号_第15张图片
可以看到分别发送可靠信号和非可靠信号各5次,可靠信号被处理了5次,而非可靠信号只处理了1次。这也就验证了不论之前sigqueue队列当中是否存在该信号的sigqueue节点,都再次添加sigqueue节点到sigqueue队列当中

8. volatile关键字

  • 保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

代码示例:

#include 
#include 
#include 
#include 
#include 

//volatile关键字保证每次取值都从内存中取值
//volatile int g_val = 1;
int g_val = 1;
void sigcallback(int signo)
{
  g_val=0;
  printf("signo:%d\n",signo);
}
int main()
{
  signal(2,sigcallback);
 //优化后一直从寄存器中取值,而不是内存中最新的值
  while(g_val)
  {
  }
  return 0;
}

  • 不使用volatile关键字

    • 正常情况下:
      在这里插入图片描述
    • 编译优化后:
      Linux进程信号_第16张图片
  • 使用volatile关键字
    在这里插入图片描述
    即不允许被优化,保持内存的可见性。

你可能感兴趣的:(Linux,linux)