ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

Linux进程信号

2021-05-01 11:32:46  阅读:117  来源: 互联网

标签:sigqueue 函数 int 信号 Linux 进程 include


信号

1. 信号概念

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

  • 信号的种类:

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

2. 信号的产生

2.1 硬件产生

通过终端按键产生信号

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

  • 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 文件中并使之生效。
      在这里插入图片描述
2.2 软件产生
2.2.1 kill命令

kill -[信号] 进程pid
例:在后台运行一个死循环进程,然后在另一个终端发送终止信号
在这里插入图片描述
可以看到进程也被终止了。

2.2.2 kill函数

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

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

代码示例:

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

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)。

例:
在这里插入图片描述

3. 信号的注册

3.1 位图加sigqueue队列

位图:
在这里插入图片描述
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

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;
}

运行结果:
在这里插入图片描述
可以看到,在wait success之后,子进程没有成为僵尸进程

5.2 SIG_IGN:忽略处理

收到信号,不去处理

5.3 自定义信号处理函数

自定义处理函数是对9号信号没有用的
图示:
在这里插入图片描述

5.3.1 signal函数

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

代码示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/signal.h>

//改变了原有的处理方式
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;
}

运行结果:
在这里插入图片描述

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 <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>

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;
}

运行结果:
在这里插入图片描述

6. 信号捕捉

调用系统调用函数的时候,或者调用库函数的时候(库函数大多数都是封装系统调用的函数的),会进入到内核空间
流程图示:
在这里插入图片描述

  • 如果没有信号处理,那就是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 <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

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;
}

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

  • 对比非可靠信号和可靠信号的处理
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

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;
}

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

8. volatile关键字

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

代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

//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关键字

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

标签:sigqueue,函数,int,信号,Linux,进程,include
来源: https://blog.csdn.net/weixin_46078890/article/details/116243866

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有