对信号的认识

本文详细介绍了操作系统中的信号机制,包括信号的产生、阻塞、信号集操作以及信号的捕捉过程。通过实例展示了如何使用系统调用如`kill`、`raise`以及`alarm`来发送信号,并解释了信号在内核中如何表示和处理。此外,还讨论了`sigaction`函数用于自定义信号处理动作,并提到了`SIGCHILD`信号及其在处理子进程结束时的作用。
摘要由CSDN通过智能技术生成

认识信号

生活中的信号:
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能 “识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你在忙学习,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成 “在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了。
而处理快递一般方式有三种:

  1. 执行默认动作(幸福的打开快递,使用商品)
  2. 执行自定义动作(快递是零食,你要送给你你的女朋友)
  3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)

快递到来的整个过程,对你来讲是异步的,快递员可能在送快递的时候你在做别的事情

计算机中的信号:

kill -l查看,【1-31】普通信号 【34,64】实时信号

在这里插入图片描述

进程是如何识别信号的?认识+动作 进程本身是被程序员编写的属性和逻辑的集合——程序员编码完成
当进程收到信号的时候,进程可能在执行更重要的代码,所以信号不一定会被立即处理 进程本身必须要有对信号的保存能力
进程在处理信号的时候,一般有三个动作(默认、自定义、忽略)—进程处理的专业名词【信号被捕捉】

如果一个信号发送给进程,信号要被进程保存在pcb里面

struct task_struct
{
   ......
   unsigned int signal;
   ......
}

发送信号的本质:修改PCB中的信号的位图

PCB的管理者是OS,任何一种发送信号的方式,本质都是通过OS向目标进程发送的信号,例如:kill命令——底层一定调用了对应的系统调用,所以OS必须要提供发送信号/处理信号的相关系统调用

系统调用

以前我们所使用的ctrl+c终止进程本质是向进程发送了2号信号

1.signal——对一个信号注册特定的处理动作(注册一个对信号的捕捉方式)

不过9号进程是无法被修改的!

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:
signum: 要注册的信号
handler: 处理动作,有三种:SIG_DFL(默认) 、SIG_IGN(忽略) 和 自定义(函数指针)
其中函数指针指向的函数有一个int类型的参数,无返回值,这个函数指针就是用户给信号自定义的处理动作,通过函数实现

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
void handler(int signo)
{
    cout << "进程捕捉到了信号,编号是" << signo << endl;
}
int main()
{
    //这里是signal的调用,不是handler的调用
    //仅仅是设置了对2号信号的捕捉方法,并不代表该方法被调用了
    //一般这个方法不会被执行,除非收到对应的信号
    signal(2,handler);
    while(1)
    {
        cout << "我是一个进程:" << getpid() << endl;
        sleep(1);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ohi6imlS-1680794945950)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/dfc3be46-85d7-4c64-9650-dec174722350/Untitled.png)]

在正式进入话题前,我们先谈一谈整个信号的生命周期:预备工作(例如:信号产生的时候,进程早已知道该如何处理,信号在产生之时,并不能立即处理)→信号发送(通过键盘产生,通过软件产生等)→信号保存→信号递达处理

信号的产生

1.通过按键产生

除了之前的ctrl+c,还有ctrl+\,发送3号进程

我们使用man 7 signal可以查看到信号的详细信息

在这里插入图片描述

我们发现2号信号和3号信号都是退出,但是2号信号是Term,3号信号是Core,这有什么区别呢?

Core Dump(核心转储)

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常
core,这叫做Core Dump。我们可以通过使用gdb调试查看core文件查看进程退出的原因,这也叫
后调试

不过在云服务器上,默认进程如果是core退出的,我们暂时看不到现象,我们可以使用ulimit -a查看OS给用户设置的各种资源的上限:

在这里插入图片描述

我们可以使用ulimit -c [大小]去更改core file的大小

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main()
{
    int arr[10];
    arr[10000]=1;
    return 0;
}

这样一串代码运行起来

在这里插入图片描述

文本编译器打开是乱码,在gdb中可以调式,带上**-g选项才可以调式 gcc -o test test.cpp -g**

在这里插入图片描述

2.通过系统调用

1.kill——给任意进程发送任意信号

疑问:写成如下这样为什么对别人的进程发送信号没有自定义操作

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
void handler(int signo)
{
  printf("catch a signal : %d\n", signo);
}
int main(int argc,char* argv[])
{
    signal(2, handler);
    while(1)
    {
        kill(atoi(argv[1]),2);
    }
    return 0;
}
#include <signal.h>
int kill(pid_t pid, int sig);

参数:
pid:进程pid
sig:要发送的信号
返回值:
成功返回0,失败返回-1

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{

    if(argc==3)
    {
        kill(atoi(argv[1]),atoi(argv[2]));
    }
    return 0;
}

在这里插入图片描述

  1. raise——给进程自己发送信号
#include <signal.h>  
int raise(int sig);

参数:
sig:要发送的信号
返回值:
成功返回0,失败返回-1
和kill比较:
raise函数相当于kill(getpid(), sig)

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
void handler(int signo)
{
  printf("catch a signal : %d\n", signo);
}

int main()
{
  signal(2, handler);
  while(1){
     raise(2);
     sleep(1);
  }
  return 0;
}

在这里插入图片描述

3.abort——给自己发送指定的信号(发送6号信号)

#include <stdlib.h>  
void abort(void);
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main()
{
  while(1){
     abort();
  }
  return 0;
}

在这里插入图片描述

3.通过软件条件产生

管道如果读端不读了,存储系统会发生SIGPIPE 信号给写端进程,终止进程。这个信号就是由一种软件条件产生的,这里再介绍一种由软件条件产生的信号SIGALRM(时钟信号)

1.alarm——设定一个闹钟,操作系统会在闹钟到了时送SIGALRM信号给进程,默认处理动作是终止进程

#include <unistd.h> 
unsigned alarm(unsigned seconds);

参数:
second:设置时间,单位是s
返回值:
0或者此前设定的闹钟时间还余下的秒数

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
int main()
{
  alarm(1);
  int cnt=0;
  while(1)
  {
    cnt++;
    cout << cnt << endl;
  }
  
  return 0;
}

在这里插入图片描述

“闹钟“就是一个软件实现的,任意一个进程都可以通过alarm系统调用在内核中设置闹钟,OS内可能存在很多的闹钟,操作系统需要管理这些闹钟,即先描述,再组织

在这里插入图片描述

4.通过硬件产生的异常

这里介绍CPU异常和MMU异常

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

int main()
{
  
  // 由软件条件产生信号  alarm函数和SIGPIPE
  // CPU运算单元产生异常,内核将这个异常处理为SIGFPE信号发送给进程
  int a = 10;
  int b = 0;
  printf("%d", a/b); 
  return 0;
}

在这里插入图片描述

CPU产生异常:发生除零错误,CPU运行单元会产生异常,内核将这个异常解释为信号,最后OS发送SIGFPE信号给进程

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
  // MMU硬件产生异常,内核将这个异常处理为SIGSEGV信号发送给进程
  int* p = NULL;
  printf("%d\n", *p);
  return 0;
}

在这里插入图片描述

**MMU产生异常:**当进程访问非法地址时,mmu想通过页表映射来将虚拟转换为物理地址,此时发现页表中不存在该虚拟地址,此时会产生异常,然后OS将异常解释为SIGSEGV信号,然后发送给进程

在这里插入图片描述

阻塞信号

概念

  • 实际执行信号的处理动作称为信号递达
  • 信号递达的三种方式:默认、忽略和自定义捕捉
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

**注意:**阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

信号在内核图中的表示

OS发生信号给一个进程,此信号不是立即被处理的,那么这个时间窗口中,信号就需要被记录保存下来,那么信号是如何在内核中保存和表示的呢?

在这里插入图片描述

**说明:**上面有三种表,分别是信号阻塞位图block表,信号未决位图pending表,信号处理动作handler表

  • block表:每个信号对应1位,如果该位为1,那么代表该信号被阻塞,为0代表不被阻塞
  • pending表:如果该位为1,代表收到该信号,处于未决状态,为0代表还没收到该信号或者收到信号已经被递达了
  • handler表:代表对该信号处理动作,前面说过有三种,默认、忽略和自定义捕捉,其中自定义捕捉就是用户自定义的函数。handler表本质其实是函数指针数组,存放的是用户自定义函数的指针

图片分析:

  • 1号信号未被阻塞,递达是才用默认动作
  • 2号信号产生了,但是被阻塞了,如果抵达才用的也是默认动作
  • 3号信号没有产生,但是先被阻塞了,它的默认动作是自定义函数

总结:

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?

POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

几个疑问:

1.所有信号的产生都要由OS来进行执行,这是为什么?

信号的产生涉及到软硬件,且OS是软硬件资源的管理者,还是进程的管理者。

2.进程在没有收到信号的时候,能否知道自己应该如何对合法信号进行处理呢?

答案是能知道的。每个进程都可以通过task_struct找到表示信号的三张表。此时该进程的pending表中哪些信号对应的那一位比特位是为0的,且进程能够查看block表知道如果收到该信号是否需要阻塞,可以查看handler表知道对该信号的处理动作。

3.OS如何发生信号?

OS给某一个进程发送了某一个信号后,OS会找到信号在进程中pending表对应的那一位比特位,然后把那一位比特位由0置1,这样OS就完成了信号发送的过程。

信号集及信号集操作函数

sigset_t: 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,也被定义

一种数据类型。这个类型可以表示每个信号状态处于何种状态(是否被阻塞,是否处于未决状态)

阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略

信号集操作函数: sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内

如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操

sigset_ t变量,而不应该对它的内部数据做任何解释

注意: 对应sigset类型的变量,我们不可以直接使用位操作来进行操作,而是一个严格实现系统给

们提供的库函数来对这个类型的变量进行操作

信号集操作函数的原型

#include <signal.h>
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);
  • sigemptyset: 初始化set指向的信号集,将所有比特位置0
  • sigfillset: 初始化set指向的信号集,将所有比特位置1
  • sigaddset: 把set指向的信号集中signum信号对应的比特位置1
  • sigdelset: 把set指向的信号集中signum信号对应的比特位置0
  • sigismember: 判断signum信号是否存在set指向的信号集中(本质是信号判断对应位是否为1)

注意:在实现这些函数之前,需要使用sigemptysetsigfillset对信号集进行初始化。前四个函数的返回值是成功返回0,失败返回-1。最后一个函数的返回值是真返回0,假返回-1

1.sigprocmask——阻塞信号集操作函数

#include <signal.h> 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

功能:
读取或更改进程的信号屏蔽字

参数:
how:三个选项
SIG_BLOCK:把set中的信号屏蔽字添加到进程的信号屏蔽字中,mask = mask|set
SIG_UNBLOCK:把set中的信号屏蔽字在进程信号屏蔽字的那些去掉,mask = mask&~set
SIG_SETMASK:设置当前进程的信号屏蔽字为set,mask = set
set:如果为非空指针,则根据how参数更改进程的信号屏蔽字
oset:如果为非空指针,将进程原来的信号屏蔽字备份六种oset中
返回值:
成功返回0,失败返回-1

2.sigpending——未决信号集操作函数

#include <signal.h> 
int sigpending(sigset_t *set);

功能:
读取进程的未决信号集
参数:
set:读取当前进程的信号屏蔽字到set指向的信号屏蔽中
返回值:
成功返回0,失败返回-1

实例演示:

1.把进程中信号屏蔽字2号信号进行阻塞,然后隔1s对未决信号集进行打印,观察现象

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <iostream>
using namespace std;
static void show_pending(const sigset_t& pending)
{
  for(int signo=31;signo>=1;--signo)
  {
    //判断signum信号是否存在set指向的信号集中(本质是信号判断对应位是否为1)
    if(sigismember(&pending,signo))
    {
      cout << "1";
    }
    else
    {
      cout << "0";
    }
  }
  cout << endl;
}
int main()
{
  // 1. 先尝试屏蔽指定的信号
  sigset_t set,oset;
  sigset_t pending;
  //1.1使用系统函数对信号集进行初始化
  sigemptyset(&set);
  sigemptyset(&oset);
  sigemptyset(&pending);
  //阻塞2号信号
  //1.2添加要屏蔽的信号
  sigaddset(&set,2);
  //1.3开始屏蔽,设置进内核, 前面的代码没有影响当前进程,从这段开始影响
  //oset保存原来的信号
  sigprocmask(SIG_BLOCK,&set,&oset);
  //2.遍历打印pending信号集
  while(1)
  {
    //2.1初始化
    sigemptyset(&pending);
    //2.2获取它
    sigpending(&pending);
    //2.3打印
    show_pending(pending);
    sleep(1);
  }
}

**代码运行结果如下:**可以看到,进程收到2号信号时,且该信号被阻塞,处于未决状态,没有被递达,未决信号集中2号信号对应的比特位由0置1,所以代码一直运行

在这里插入图片描述

2.进行运行10s后,我们将信号屏蔽字中2号信号解除屏蔽

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <iostream>
using namespace std;
static void show_pending(const sigset_t& pending)
{
  for(int signo=31;signo>=1;--signo)
  {
    //判断signum信号是否存在set指向的信号集中(本质是信号判断对应位是否为1)
    if(sigismember(&pending,signo))
    {
      cout << "1";
    }
    else
    {
      cout << "0";
    }
  }
  cout << endl;
}
int main()
{
  // 1. 先尝试屏蔽指定的信号
  sigset_t set,oset;
  sigset_t pending;
  //1.1使用系统函数对信号集进行初始化
  sigemptyset(&set);
  sigemptyset(&oset);
  sigemptyset(&pending);
  //阻塞2号信号
  //1.2添加要屏蔽的信号
  sigaddset(&set,2);
  //1.3开始屏蔽,设置进内核, 前面的代码没有影响当前进程,从这段开始影响
  //oset保存原来的信号
  sigprocmask(SIG_BLOCK,&set,&oset);
  //2.遍历打印pending信号集
  int cnt=10;
  while(1)
  {
    //2.1初始化
    sigemptyset(&pending);
    //2.2获取它
    sigpending(&pending);
    //2.3打印
    show_pending(pending);
    sleep(1);
    cnt--;
    if(cnt==0)
    {
      cout << "屏蔽信号解除" << endl;
      sigprocmask(SIG_UNBLOCK,&set,&oset);
    }
  }
}

**代码运行结果如下:**2号信号解除阻塞后,信号被递达了,进程终止

在这里插入图片描述

捕捉信号

捕捉过程介绍

问题:信号什么时候被处理的?

首先,不是立即被处理的。而是在合适的时候,这个合适的时候,具体指的是进程从用户态切换回内核态时进行处理

何为用户态?内核态?

  • 用户态: 处于⽤户态的 CPU 只能受限的访问内存,用户的代码,并且不允许访问外围设备,权限比较低
  • 内核态: 处于内核态的 CPU 可以访问任意的数据,包括外围设备,⽐如⽹卡、硬盘等,权限比较高

操作系统怎么知道你处于什么状态?

  • 操作系统中有一个cr寄存器来记录当前进程处于何种状态,0表示内核态,3表示用户态

进程空间分为用户空间和内核空间。此前我们介绍的页表都是用户级页表,其实还有内核级页表。

程的用户空间是通过用户级页表映射到物理内存上,内核空间是通过内核级页表映射到物理内存上

如下面简图所示:

在这里插入图片描述

进程有不同的用户空间,但是只有一个内核空间,不同进程的用户空间的代码和数据是不一样的,但是内核空间的代码和数据是一样的。

上面的图主要说明:进程处于用户态访问的是用户空间的代码和数据,进程处于内核态,访问的是内核空间的代码和数据。

下面给演示信号捕捉的整个过程:

在这里插入图片描述

从上面的图可以看出,进程是在返回用户态之前对信号进行检测,检测pending位图,根据信号处

动作,来对信号进行处理。这个处理动作是在内核态返回用户态后进行执行的。

如果信号的处理动作是用户的自定义函数,可以画成如下的图,便于理解:

在这里插入图片描述

其中4个绿点是4次状态切换,4个红色的点对应上图的4个执行步骤

之前我们介绍过信号捕捉函数signal,这里在介绍另一个

1.sigaction——一个对指定信号的执行动作进行特殊处理的函数

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

功能:
可以读取和修改与指定信号相关联的处理动作
参数
signum: 要操作的信号
act:一个结构体
sa_handler:SIG_DFT、SIG_IGN和handler(用户自定义处理函数)
sa_sigaction:实时信号处理的函数,我们不关心
sa_mask:一个信号屏蔽字,里面有需要额外屏蔽的的信号
sa_flags:包含一下选项,这里我们给0
sa_restorer:我们这里不使用
act:如果不为空,根据act修改信号处理动作
oact: 如果不为空,备份原来的信号处理动作给oact
返回值:
成功返回0,失败返回-1

act结构体如下:

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 <signal.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
	cout << "get a signo: " << signo << endl;
}
int main()
{
	struct sigaction act,oact;
	act.sa_handler=handler;
	// 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
	sigemptyset(&act.sa_mask); 
	sigaddset(&act.sa_mask,3);
	act.sa_flags=0;
	sigaction(2,&act,&oact);
	while(1) sleep(1);
	return 0;
}

代码运行结果如下:

在这里插入图片描述

再做一个实验,自定义动作睡眠20秒,会有什么现象呢?

#include <stdio.h>
#include <signal.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
	cout << "get a signo: " << signo << endl;
	sleep(20);
}
int main()
{
	struct sigaction act,oact;
	act.sa_handler=handler;
	// 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
	sigemptyset(&act.sa_mask); 
	act.sa_flags=0;
	sigaction(2,&act,&oact);
	while(1) sleep(1);
	return 0;
}

运行结果如下:

在这里插入图片描述

原因:当我们进行正在递达某一个信号期间,同类型的信号无法递达!当当前信号正在被捕捉,系统会自动将当前信号加入到进程的信号屏蔽字(block),当信号完成捕捉,系统又会自动解除对该信号的屏蔽,一般一个信号被解除屏蔽的时候,会自动进行递达当前屏蔽信号,如果该信号已经被pending的话,没有就不会做任何动作!

再做一个实验:在sa_mask中在添加一个信号

#include <stdio.h>
#include <signal.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
	cout << "get a signo: " << signo << endl;
	sleep(20);
}
int main()
{
	struct sigaction act,oact;
	act.sa_handler=handler;
	// 当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中
	sigemptyset(&act.sa_mask); 
	// 这个sa_mask只是屏蔽!不会加入到handler动作!
	sigaddset(&act.sa_mask,2);
	act.sa_flags=0;
	sigaction(3,&act,&oact);
	while(1) sleep(1);
	return 0;
}

运行结果如下:

在这里插入图片描述

可重入代码

在这里插入图片描述

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile关键字

先看一段代码:

#include <stdio.h>
#include <signal.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
int quit=0;
void handler(int signo)
{
	cout << signo << "号信号,正在被捕捉!" << endl;
	cout << "quit:" << quit ;
	quit=1;
	cout << "->" << quit << endl ;
}
int main()
{
	signal(2,handler);
	while(!quit) ;
	cout << "注意,我是正常退出的" << endl;
	return 0;
}

运行结果如下:我们输入ctrl+c,会正常退出

在这里插入图片描述

但如果编译的时候带上O3级别的优化呢?g++ -o test test.cpp -O3

运行结果如下:

在这里插入图片描述

改变了也不会退出,这是为什么呢?quit放在内存中

在这里插入图片描述

使用volatile可以保存内存的可见性,加上volatile就正常执行了

SIGCHILD信号

子进程在死亡的时候,会向父进程发送SIGCHILD信号,不过父进程默认是忽略的,使用man 7 signal查看

在这里插入图片描述

用以上知识检查看看是不是17号信号

#include <stdio.h>
#include <signal.h>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
	printf("pid: %d, %d 号信号,正在被捕捉!\n", getpid(), signo);
}
void Count(int cnt)
{
    while (cnt)
    {
        printf("cnt: %2d\r", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
int main()
{
	signal(17,handler);
	printf("我是父进程, %d, ppid: %d\n", getpid(), getppid());
	pid_t id=fork();
	if(id==0)
	{
		printf("我是子进程, %d, ppid: %d,我要退出啦\n", getpid(), getppid());
		sleep(20);
        exit(1);
	}
	//保持父进程在运行
	while (1)
    sleep(1);
	return 0;
}

运行结果如下:

在这里插入图片描述

这样的意义在于,以前父进程被动式的等待,例如阻塞等待子进程,或者主动去“问问子进程”,即非阻塞式等待,现在我们可以让子进程叫我们了!

所以我们可以把handler写成如下形式

void handler(int signo)
{
	// 1. 我有非常多的子进程,在同一个时刻退出了 【只需要循环处理】
	// 2. 我有非常多的子进程,在同一个时刻只有一部分退出了 【必须非阻塞式等待,因为操作系统不知道你有多少个子进程要退出
	//如果你没退出,在这里就会造成死循环】
	//waitpid第一个参数是pid,这里是多个子进程,所以设置-1,意思是会等待任意一个子进程
	while(1)
    {
        pid_t ret = waitpid(-1, NULL, WNOHANG);
        if(ret == 0) break;
    }
}

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程

signal(17,SIG_IGN);

这里的手动设置的IGN和之前默认的IGN是不一样的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值