当前位置: 首页 > news >正文

【Linux】信号 —— 信号的产生 | 信号的保存 | 信号的处理 | 可重入函数 | volalite关键字 | SIGCHLD

信号

  • 1. 信号的产生 - 信号发送前
    • 1.1 键盘产生
    • 1.2 异常
    • 1.3 系统调用
    • 1.4 软件条件
    • 1.5 理解发送信号
  • 2. 信号的保存 - 信号发送中
    • 2.1 相关概念 & 内核结构
    • 2.2 sigset_t 及一系列系统调用函数
  • 3. 信号的处理 - 信号发送后
    • 3.1 内核如何实现信号捕捉
    • 3.2 sigaction
  • 4. 可重入函数
  • 5. volalite关键字
  • 6. SIGCHLD信号

people change

前排声明:以后更新文章会慢一些吧,如上文所言,people change… (哈哈哈距离我刚开始写下这句话时候已过去一个月了,果然,人生总是在不经意的想法中出现转折,曾经认为理所当然的事儿… 所以顺其自然…

“而且我们都是学计算机的,我们知道如果把宇宙所有的原子结构保存,配上超强的算法,一切都是必然的,我们决定不了的”

“你一上升到这个高度我就讨论不了了”

“我只是想说顺其自然”

a zing never lies @小边小边别发愁

生活中有许多关于信号的场景:比如红绿灯,你daddy的脸色。。。

这些场景触发时,我立马就知道我接下来该做什么,这来自于天生or后天习得,并不是它们真的发生时我知道该怎么做。同理,进程具有识别并处理信号的能力是远远早于信号产生的,这是由那些编写操作系统的工程师写好的。

在生活中,我们收到信号时,不一定会立即处理,因为当前我可能做着更重要的事儿。同理,信号随时都可能产生(异步),进程收到某种信号时,不一定是立即处理的,而是在合适的时候处理

比如有一小伙儿跟你表白了,你虽然没有立即回复(此时你正向闺蜜群组求助哈哈哈),但你也记着要给个答复。既然有时信号不能被立即处理,进程就需要先将信号保存起来,以供合适时机处理。那应该保存在哪里呢?struct task_struct,信号的发送即向进程控制块写入信号数据。task_struct是一个内核数据结构,内核不相信任何人,只相信自己,无论以何种方式发送信号,本质都是通过OS发送的,那一定是操作系统向task_struct写入信号数据。

kill -l 查看系统支持的信号列表

前31个(1 ~ 31)是普通信号,后31个(34 ~ 64)是实时信号 ——

image-20220830193845696

一般进程收到信号的处理方案有三种情况 ——

  1. 默认动作:终止自己、暂停等

  2. 忽略动作:是一种信号处理的方式,只不过就是什么也不干

  3. 自定义动作(信号的捕捉):比如如下的signal方法,修改处理信号的动作,由默认动作变为自定义动作

1. 信号的产生 - 信号发送前

信号产生的方式有哪些呢?

1.1 键盘产生

写一段死循环函数,可以ctrl+c终止程序。我们键盘ctrl+C时,本质是向指定进程发送2号信号SIGINT。那你怎么证明?可以通过signal函数为信号指定处理函数:

signal - ANSI C signal handling

#include <signal.h>

typedef void (*sighandler_t)(int); //函数指针 - 返回值为void,参数为int

sighandler_t signal(int signum, sighandler_t handler);

修改进程对信号的默认处理动作 ——

  • signal:实际上这些大写的信号都是#define 整数定义出来的
  • handler:注册函数。注册时不是调用这个函数,只有信号到来时,函数才会被调用。

代码如下 ——

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

void handler(int signo)    
{    
  printf("get a signal: %d, pid: %d\n", signo, getpid());    
  exit(1);
}    
    
int main()    
{ 
  int sig;
  for(sig = 1; sig<=31; sig++)
  {
    //通过signal注册对1~31号信号的处理动作,改成我们自定义的动作    
    signal(sig, handler);                                                           
  }
  
  while(1)    
  {    
    printf("people change...pid: %d\n", getpid());    
    sleep(1);    
  }    
  return 0;    
} 

发送信号时,执行自定义方法 ——

image-20220830191947376

信号的产生方式的其中一种是通过键盘产生,键盘产生的信号只能终止前台进程,杀掉后台进程&得用kill命令 ——

kill -9 [pid]

我们还可以设置不同信号的处理方式 ——

#include<stdio.h>    
#include<unistd.h>    
#include<signal.h>    
#include<stdlib.h>    
    
void handler(int signo)    
{    
  switch(signo)    
  {    
    case 2:                                                                     
      printf("Ayayaya~\n");    
      break;    
    case 3:    
      printf("people change a lot\n");    
      break;    
    case 9:    
      printf("I am tired\n");    
      break;    
    default:
      printf("signo: %d\n", signo);
      break;    
  }    
  //exit(1);
}    
    
int main()    
{    
  int sig;    
  for(sig = 1; sig<=31; sig++)    
  { 
    //通过signal注册对1~31号信号的处理动作,改成我们自定义的动作    
    signal(sig, handler);    
  }    
  while(1)    
  {    
    printf("people change...pid: %d\n", getpid());
    sleep(1);
  }
  return 0;
} 

发现9号信号不可被捕捉(自定义) ——

072801

为什么呢?we will talk about it later…

1.2 异常

信号产生,也可能由于程序中存在异常问题,使进程收到信号并退出。

写一段野指针解引用代码 ——

  int* p = NULL;    
  *p = 100;

进程为什么会崩溃呢?本质是因为进程收到了11号信号SIGSEGV。而且如果不exit会刷屏,说明不断收到11号信号。

image-20220830192208739

除0 ——

  int a = 10;
  a /= 0;

除0收到的是8号信号SIGFPE,浮点数异常 ——

image-20220830192225461

综上,在win或Linux环境下,进程崩溃的本质是进程收到了对应的信号,并执行默认处理动作(杀死进程)。那为什么会收到信号呢?

软件上的错误通常会体现在硬件或其它软件上,而操作系统是硬件管理者,就应该对硬件的健康负责,找到有问题的进程并发送信号反馈,终止进程。我们在C++中的捕捉异常,实际上就是在处理信号。

进程崩溃时,你最关心的是崩溃的原因,这是通过waitpid()的status参数的低7位来获取退出信号。(忘了的宝子去复习进程控制吧~)

image-20220830192240158

从前从前我们就知道,在Linux中,进程正常退出时,它的退出码和退出信号都会被设置;进程异常退出时,它的退出信号会被设置,表明进程退出的原因。另外我还想知道是在哪一行崩溃的喂?

从前从前我们挖了这样一个坑:如有必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘中,方便我们后期调试。

在云服务器上,core dump被关掉了,我们把它打开~

ulimit -a	查看系统资源
ulimit -c 10240
image-20220830192326805

这样就允许你core dump了,(但也不是所有的信号都会core dumped),此时再运行,还生成了core文件 ——

image-20220830192533926image-20220830192548240

我们打开生成的core.10225,发现是人类看不懂的乱码,因为是把磁盘内容直接拷下来的,我们也不关心啦~

我们需要小小修改一下Makefile文件,编译时带上-g选项,允许用gdb调试。用core-file命令,得知错误原因及错误行数 ——

image-20220830192533926image-20220830192548240

这种方案叫做事后调试。曾经我们程序崩溃,我们只能注释掉定位或者一行一行的单步调试。

若进程出现异常时被core dump,core dump会被设置为1,我们来通过位操作获取退出信息 ——

#include<stdio.h>    
#include<unistd.h>                                                            
#include<sys/types.h>    
#include<sys/wait.h>    

int main()    
{    
    if(fork() == 0)    
    {    
      while(1)    
      {    
        printf("I am child...\n");    
        int a = 10;    
        a /= 0;    
      }    
    }    

    int status = 0;    
    waitpid(-1, &status, 0);    
    printf("exit code: %d, exit sig:%d, core dump flag: %d\n", (status>>8)&0xFF, status&0x7F, (status>>7)&1); return 0;    
}   
image-20220830192638108

1.3 系统调用

通过系统调用产生信号 ——

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig); //给任意进程发送任意信号
int raise(int sig); //给自己发任意信号
#include <stdlib.h>

void abort(void); //给自己发送SIGABRT信号

kill命令就是调用kill函数实现的 ——

#include<stdio.h>    
#include<sys/types.h>    
#include<signal.h>    
#include<stdlib.h>    
    
static void Usage(const char* proc)    
{    
  printf("Usage:\n\t%s signo who", proc);    
}    
 
// ./mytest signo who
int main(int argc,char* argv[])    
{    
  if(argc != 3)    
  {    
    Usage(argv[0]);    
    return 1;    
  }    
      
  int signo = atoi(argv[1]);    
  int who = atoi(argv[2]);    
      
  kill(who, signo);    
      
  return 0;                                                                                     
}  

image-20220830192655262

1.4 软件条件

通过某种软件(OS),来触发信号的发送,在系统层面设置定时器,或某种操作导致条件不就绪等这样的场景下,触发的信号。

在进程间通信匿名管道的4种场景中,写端疯狂写,若此时读端关闭了读fd,则写端会**收到13号信号SIGPIPE**退出,这就是一种软件条件触发的信号发送。今天我们再介绍 ——

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

设置一个计时器/闹钟,延迟n秒后发送14号信号SIGALRM ——

  • 返回值:可以认为alarm被正常触发时,返回值就是0;或者是距离设定时间余下的秒数。
#include<stdio.h>    
#include<sys/types.h>    
#include<signal.h>    
#include<stdlib.h>    
#include<unistd.h>    
    
void handler(int signo)    
{    
  printf("well, I recieved %d signal\n", signo);    
}    
    
int main()    
{    
  int sig;    
  for(sig = 1; sig <= 31;sig++)    
  {    
    signal(sig, handler);    
  }    
    
  int ret = alarm(20);    
    
  sleep(5);    
  ret = alarm(0); //取消闹钟                                                                                                                                        
  printf("autually, it still left %d seconds\n", ret);    
    
  return 0;    
}

image-20220830192713126image-20220830192731743

我们不断打印count,统计1s中server对count可以递增到三万多次;然后屏蔽掉printf,而只是在捕捉信号处打印count,发现count达到了5亿次 ——

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

int count = 0;    

//没有设置alarm信号(14)的捕捉/自定义动作, 执行默认动作:终止进程
//void handler(int signo)    
//{    
//	printf("after 1s... count: %d\n", count);                                                               //  exit(1);    
//}    

int main()    
{    
    signal(SIGALRM, handler);    
    alarm(1); /    
    while(1)    
    {    
      //printf("people change...%d\n", count);    
      count++;    
    }    
    return 0;    
}
image-20220830192731743

因为IO是非常慢的。。

1.5 理解发送信号

信号产生方式的种类非常多,但就算信号产生的方式千差万别,最终一定是通过OS向目标进程发送信号。

如何理解OS给进程发送信号?朴素的理解就是OS发送信号数据给task_struct,我们要进一步来谈 ——

我们发现信号的编号是有规律的**[1, 31],进程内部一定要有对应的数据变量,来保存记录是否收到了对应的信号。那用什么数据类型,来标识进程是否收到信号**呢?唔唔唔!位图

struct task_struct{
    //进程的各种属性
    uint32_t sigs; 
};

比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号 ——

//比如这个,表示收到3号信号
00000000 00000000 00000000 00000100 

综上,“信号的发送”更准确的说是信号的写入,即OS向指定进程的task_struct的信号位图(pending位图)比特位 置为1。

2. 信号的保存 - 信号发送中

信号随时都可能产生(异步),进程收到某种信号时不一定是立即处理的,因为当前我可能做着更重要的事儿,而是把它暂存起来在合适的时候处理。怎么保存信号?合适又是什么时候?

2.1 相关概念 & 内核结构

信号有三张表:pendingblockhandler,我们姑且不关心它们底层的数据结构,其中pending表就是用来写入接收到的信号的位图 ——

  • 实际执行信号的处理动作称为信号递达(Delivery) —— 自定义捕捉、默认、忽略
  • 信号从产生到递达之间的状态,称为信号未决(Pending) —— 本质是这个信号被暂存在task_struct的信号位图pending中。
  • 进程可以选择阻塞信号(Block) —— 本质是OS允许进程暂时屏蔽指定信号,表示该信号仍然是未决的,该信号不会被递达,直到解除阻塞方可执行递达的动作

注意:阻塞和递达中的忽略有区别吗?yes,忽略是递达的一种方式,而阻塞是递达前的独立状态。

grep -ER 'SIG_DFL | SIG_IGN' /usr/include	递归查找一下这两个宏
    
#define SIG_DFL	((__sighandler_t)0)	/* Default action */
#define SIG_IGN	((__sighandler_t)1)	/* ignore signal */
  • block表:信号屏蔽字,阻塞位图,表示是否阻塞,不影响接收信号但影响递达信号。是一种状态位图,同样的也是uint32_t block;无符号整数类型:比特位的位置,代表信号的编号;比特位的内容,代表信号是否被阻塞(屏蔽)。这些信号不会被递达直到解除阻塞。
  • pending表:未决位图,保存的是已经收到但没有被递达的信号。比特位的位置(第几个),代表的是哪一个信号;比特位的内容(0/1),代表是否收到信号。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。所以你看,发送信号时都要给pid和信号数。
  • handler表:函数指针数组,每个信号的编号就是该数组的下标。实际上,我们执行signal方法,对特定的信号注册自定义方法的本质就是,把handler函数地址填入到信号编号对应下标的 handler表中,这样执行的就是自定义方法。

内核中对信号会做类似如下检测 ——

int ishandled(int signo)
{
    if(block & signo) { //该信号被block
        //根本就不看是否收到该信号
	}
    else if(pending & signo) { //该信号没被block,且已经收到了
        handler_array[signo](signo); /*回调函数*/
        return 0;
	}
    return 1;
}

综上,这张表应该横着看,进程内置了识别信号的方式,[三元组] 是否被屏蔽→是否被递达(→handler),即你是谁?你现在能否处理?怎么处理?

2.2 sigset_t 及一系列系统调用函数

从上图来看,每个信号只有一个1bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,这种类型无法直接对其进行位操作,因为这是极不安全的,就必须通过系统调用接口。因为不同OS对于sigset_t位图结构的实现是不一样的,因此不能让用户直接修改。它定义的变量和我们之前的int、double没有任何区别,都是在用户栈上。

💛 ​下面是信号集的各种操作。

#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

但这只是对用户层定义的set进行的操作,要想设置进对应进程,还需要如下函数。

💛 ​sigpromask

读取或更改进程的信号屏蔽字(阻塞信号集),本质是修改block表

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1
  • set:输入型参数。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改;
  • oset:输出型参数。如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出;如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
  • how ——
    • SIG_BLOCK:把set中的信号屏蔽字添加到进程的信号屏蔽字中,mask = mask|set
    • SIG_UNBLOCK:把set中的信号屏蔽字在进程信号屏蔽字的那些抹掉,mask = mask&~set,和权限掩码类似
    • SIG_SETMASK:设置当前进程的信号屏蔽字为set,mask = set

来~ 用起来 ——

#include<stdio.h>    
#include<signal.h>    
#include<unistd.h>    
    
int main()    
{    
  sigset_t set, oset;    
  sigemptyset(&set);    
  sigemptyset(&oset);    
    
  //屏蔽2号信号    
  sigaddset(&set, 2);    
    
  // 设置当前进程屏蔽字,获取当前进程老的屏蔽字    
  sigprocmask(SIG_SETMASK, &set, &oset);    
    
  while(1)    
  {    
    printf("people change..\n");                                                                        
    sleep(1);    
  }    
  return 0;    
}

发现ctrl+c不能再终止进程 ——

image-20220830192839061

但是就算设置屏蔽9号信号,也没法把它屏蔽,因为9号信号被称为管理员信号,不能被屏蔽,也不能被捕捉,只能执行默认行为。

💛 sigpending

不对pending位图做修改,因为修改一定是OS做的,这儿只是单纯的获取进程的pending表

#include <signal.h>

int sigpending(sigset_t *set);
  • sigprocmask:输出型参数

还有signal()是用来修改handler表的~

来用一下吧 ——

① 我们不断的获取并打印当前进程的pending位图00000000 00000000 00000000 00000000;如果我的进程预先屏蔽掉2号信号,因为2号信号不会被递达,所以预期打印的pending位图是01000000 00000000 00000000 00000000 (注意打印位图不能直接通过位操作,必须通过信号集操作函数)。

② 当然也可以恢复2号信号的接收,把老的屏蔽字再传回去就行了;由于2号信号的默认动作是终止进程,我们看不到现象,所以我们signal()指定处理程序。

#include<stdio.h>                                                       
#include<signal.h>
#include<unistd.h>                                 
                                            
void show_pending(sigset_t* pending)
{                   
  printf("current process pending:");
  int i;      
  for(i = 1; i<=31; i++)
  {                           
    if(sigismember(pending, i))
    {                   
      printf("1");                                       
    }                                            
    else               
    {                                              
      printf("0");
    }                      
  }                                                    
  printf("\n");                                 
}                                             
                                                                       
void handler(int signo)                                              
{                                                             
  printf("signal %d has been recieved and handled...\n", signo);
}                                                         
                                                  
int main()             
{                                                   
  signal(2, handler);     
  sigset_t set, oset;   
  sigemptyset(&set);    
  sigemptyset(&oset);
 
  //屏蔽2号信号
  sigaddset(&set, 2);
                                                                        
  // 设置当前进程屏蔽字,获取当前进程老的屏蔽字
  sigprocmask(SIG_SETMASK, &set, &oset);
  
  int count = 0;
  sigset_t pending;
  while(1)
  {
    sigemptyset(&pending);
    sigpending(&pending);
    //打印pending表
    show_pending(&pending);
    sleep(1);
    
    count++;
    if(count == 8)
    {
      //10s后解除对2号信号的屏蔽,处理2号信号(1->0)
      sigprocmask(SIG_SETMASK, &oset, NULL);
      printf("SIGINT can be delivered\n"); 
    }
  }
  
  return 0;
}
image-20220830192857646

3. 信号的处理 - 信号发送后

3.1 内核如何实现信号捕捉

因为信号的产生是异步的,当前进程可能正在做更重要的事儿,需要暂存并在合适的时对信号延时处理 (取决于OS和进程)。

**什么是合适,信号何时被处理?**信号是保存在进程的PCB的pending位图中,要经历处理(检测->递达[默认、忽略、自定义]),当进程从内核态 返回到 用户态时,会进行如上的检测并处理。

执行和访问用户代码和数据时所处的状态叫做用户态,我们自己写的代码全都是在用户态执行的;执行OS的代码和数据时,计算机所处的状态是内核态。它们的主要区别在于权限

用户调用system call函数,除了进入函数,身份也会发生切换,从用户身份变为内核身份。


用户的身份是以进程为代表的。用户的数据和代码一定要被加载到内存;OS的数据和代码也是要加载到内存的。OS的代码是怎样被执行到的呢?是通过系统级页表映射到物理内存的代码和数据,用户级页表一个进程一个;而内核页表由所有进程共享。

这样以来,一个进程就可以通过地址空间看到用户&内核的所有内容,那如何确定到底以什么身份执行程序呢?是通过CPU内的CR3寄存器保存当前进程的状态:用户态使用的是用户级页表,只能访问用户的数据和代码;内核态使用的是内核级页表,只能访问内核的数据和代码。

无论进程之间如何切换,也能执行的是一份OS代码,因为都是通过3~4G的地址空间,通过同一张内核页表映射而来。

所谓的系统调用,本质就是进程的切化成为内核身份,根据内核页表找到系统函数,再执行。大部分情况下,OS是可以在进程的上下文直接运行的。

image-20220830192950123

为什么一定要切换为用户态执行信号捕捉方法?OS理论上是可以执行用户的代码,但是OS身份特殊,不能轻易执行用户代码,因为可能执行恶意代码。

3.2 sigaction

作用和signal()完全类似,它修改的就是handler函数指针数组(但是这个函数还考虑了实时函数,可能更复杂,但是我们暂时不考虑啦~) ——

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum:注册一个信号。

  • act:输入型参数,调用方法。我们发现这个结构体类型和函数名一致,虽然我们自己写并不建议,但是便于记忆。

    image-20220830193009497
    • sa_handler:将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。

    • sa_mask:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号过程中,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止,即信号不会多次捕捉,不会排队!也就是说普通信号可能会被丢弃,但是有些信号不能被丢失 —— 实时信号,内核中是用链表组织起来的。

      如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

  • oldact:输出型参数,带回老的信号catch方法,当然不想获取设为NULL就行啦~

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

void handler(int signo)
{
  printf("get a signo: %d\n", signo);
}

int main()
{
  struct sigaction act;
  memset(&act, 0, sizeof(act)); 

  act.sa_handler = handler;
  // act.sa_handler = SIG_IGN; //忽略信号
  // act.sa_handler = SIG_DFL; //默认 - 一般以接收2号信号为结束
  
  //本质是修改当前进程的handler函数指针数组的特定内容
  sigaction(2, &act, NULL);

  while(1)
  {
    printf("I am missing you~\n");
    sleep(1);
  }
  return 0;
}
image-20220830193105171

测试sa_mask,它是signet_t类型,无法直接进行位操作,必须使用信号集操作函数。

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

void handler(int signo)
{
  while(1)
  {
    //正在处理信号... 现实中你不要这样干哦
    printf("get a signo: %d\n", signo);
  	sleep(1);
  }
}

int main()
{
  struct sigaction act;
  memset(&act, 0, sizeof(act)); 

  act.sa_handler = handler;
  sigemptyset(&act.sa_mask);
  sigaddset(&act.sa_mask, 3);

  //本质是修改当前进程的handler函数指针数组的特定内容
  sigaction(2, &act, NULL);

  while(1)
  {
    printf("I am missing you~\n");
    sleep(1);
  }
  return 0;
}

观察发现处理2号信号的过程中,3号信号被忽略了。

4. 可重入函数

  • insert函数一旦重入,有可能出现问题 —— 该函数不可被重入
  • insert函数一旦重入,不会出现问题 —— 可重入函数
image-20220830193125649

我们所学到的大部分函数,STL、boast库中的函数,大部分都是不可重入的。

5. volalite关键字

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

int flag = 0;

void handler(int signo)
{
    flag = 1;
    printf("change flag 0 → 1\n");
}

int main()
{
    signal(2, handler);

    while(!flag);

    printf("这个进程是正常退出的!\n");
    return 0;
}

带优化选项后,编译器在main函数中没有检测到对flag的修改,于是把flag优化到寄存器中,但是这儿会出问题,屏蔽了内存的修改 ——

image-20220830193146611

如何解决呢?在flag前添加关键字volatile ——

image-20220830193201051

为什么呢?volatile的作用是,告诉编译器,不要对我这个变量做任何优化,读取必须贯穿式的读取内存,而不要读取中间缓冲区、寄存器 —— 保持内存的可见性。

还有一个作用 ——防止指令重排。嗯嗯有意思。

6. SIGCHLD信号

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地轮巡检测查询是否有子进程结束等待清理。采用第一种方,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

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

void get_child(int signo)
{
    //waitpid() 
    printf("get a signal: %d, pid: %d\n", signo, getpid());
}

int main()
{
    pid_t id = fork();
    signal(SIGCHLD, get_child);
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程: %d\n", getpid());
            sleep(1);
            cnt--;
        }
        exit(0);
    }
    //parent
    while(1);
    return 0;
}

但是会出现僵尸进程 ——

image-20220830193332413

如果在信号捕捉函数处waitpid()回收子进程,这样就不用父进程主动等待了。

如果我不回收子进程呢?可以显式设置忽略17号信号,当进程退出后,会自动释放僵尸进程。

	signal(SIGCHLD, SIG_IGN);

这种方式,只在Linux平台下有效。

处理多个子进程,并且WNOHANG防止有子进程,了解即可 ——

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}

int main()
{
    signal(SIGCHLD, handler);
    if (fork() == 0)
    { // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}

相关文章:

  • A Survey of Deep Learning-based Object Detection
  • Java新手小白入门篇 API -Socket网络编程
  • kafka如何保证消息不丢失?半分钟的答案和半个小时的答案有点不一样。
  • Java学习----集合1
  • PBR概念及PBR核心理论和渲染原理
  • 5.5如何去除有序数组的重复元素
  • PBR标准化工作流程
  • Vue学习第17天——netTick()的原理及使用
  • 英语语法精讲合集
  • 如何用数据采集网关快速采集工业现场数据,怎么搭建MQTT服务器?
  • Vue中的样式绑定
  • 大学网课答案公众号题库搭建
  • torch.utils.data
  • torch.torchvision
  • Git GitHub VSCode 简单使用
  • Django 博客开发教程 16 - 统计文章阅读量
  • MySQL主从复制读写分离及奇怪的问题
  • PaddlePaddle-GitHub的正确打开姿势
  • pdf文件如何在线转换为jpg图片
  • PHP变量
  • Spring框架之我见(三)——IOC、AOP
  • STAR法则
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • Vue源码解析(二)Vue的双向绑定讲解及实现
  • 对JS继承的一点思考
  • 前端学习笔记之原型——一张图说明`prototype`和`__proto__`的区别
  • 写代码的正确姿势
  • 原创:新手布局福音!微信小程序使用flex的一些基础样式属性(一)
  • AI又要和人类“对打”,Deepmind宣布《星战Ⅱ》即将开始 ...
  • const的用法,特别是用在函数前面与后面的区别
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • #传输# #传输数据判断#
  • #我与Java虚拟机的故事#连载15:完整阅读的第一本技术书籍
  • (1/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类
  • (附源码)ssm高校社团管理系统 毕业设计 234162
  • (汇总)os模块以及shutil模块对文件的操作
  • (六)激光线扫描-三维重建
  • (全注解开发)学习Spring-MVC的第三天
  • (一)spring cloud微服务分布式云架构 - Spring Cloud简介
  • (一)SpringBoot3---尚硅谷总结
  • .htaccess 强制https 单独排除某个目录
  • .NET 3.0 Framework已经被添加到WindowUpdate
  • .Net 8.0 新的变化
  • .NET CORE 第一节 创建基本的 asp.net core
  • .net程序集学习心得
  • .NET简谈设计模式之(单件模式)
  • .NET轻量级ORM组件Dapper葵花宝典
  • @transaction 提交事务_【读源码】剖析TCCTransaction事务提交实现细节
  • [ vulhub漏洞复现篇 ] AppWeb认证绕过漏洞(CVE-2018-8715)
  • [16/N]论得趣
  • [2013][note]通过石墨烯调谐用于开关、传感的动态可重构Fano超——
  • [20150629]简单的加密连接.txt
  • [20160807][系统设计的三次迭代]