【Linux】信号机制(非实时信号)

  • Post author:
  • Post category:linux



目录


前言


一.信号的概念以及产生


1.什么是信号


2.信号分为两类


3.查看信号的命令


4.信号如何产生


1).通过软件产生


2).通过硬件产生


3).通过键盘组合键产生


二.信号的发送以及保存


1.信号如何发送


2.信号如何保存


1).概念


2).底层实现结构&&内核中的实现


3).阻塞vs忽略


三.信号的处理以及操作


1.信号被处理的时机(重点)


2.信号的三种处理方式


3.如何对信号自定义捕捉


4.sigset_t类型


5.如何操作pending信号集


6.如何操作block阻塞信号集


7.特殊的9号与19号信号


8.block存在的意义


四.信号的整体流程(总结)


五.补充内容


1.核心转储


2.volatile关键字(补充)


3.kill()/raise()/abort()


1).kill()


2).raise()


3).abort()


4.定时闹钟


5.SIGCHLD信号SIGPIPE信号


1).SIGPIPE


2).SIGCHLD


前言

本篇重点解析普通信号(非实时信号), 以信号概念及产生 — 信号发送及保存 — 信号处理及操作, 三条主线依次进行

有些结论可能会过早的展示, 如有不懂或没有解释的地方就跳过继续向后看

一.信号的概念以及产生

1.什么是信号

信号本质上是一种通信机制, 用户或者操作系统通过以给指定进程发送信号来通知进程, 某件事情已经发生, 等待进程后续进行处理

所以, 进程对于信号要满足以下要求

1.进程需要知道对应的信号, 应该如何被处理(不同信号不同的处理方法)

2.信号是随机产生的, 所以进程对于信号是可以延后处理的(不得不延后的情况下)

3.进程可以存储信号, 以便如果无法及时处理的话对该信号做存储再延后处理

2.信号分为两类

1.

普通信号

: 可以延后处理  2.

实时信号

: 不可以延后处理

例如: 如果同时给同一进程发送了两个相同信号, 这时对于普通信号的处理方式是先处理该信号一次, 处理的同时将同种信号阻塞, 然后处理好之后, 再处理第二次发送的该信号 … , 而实时信号不同, 它则能够同一时间处理该信号两次, 本质上这与普通信号与实时信号的实现有关, 在本篇文章的信号存储部分会详细解释, 注: 本篇文章通篇讨论的是普通信号

3.查看信号的命令

1.指令: kill -l查看全部信号, 注: 一共62个信号, 1~31, 一共31个信号为普通信号, 后31个信号为实时信号

2.man手册: man 7 signal

4.信号如何产生

1).通过软件产生

OS先识别到某种软件/进程触发了某种条件或者不满足某种条件, 此时构建信号再发送给指定进程


例一:

现象: 父子共用一条匿名管道, 当读端关闭之后, 写端进程自动退出

本质: 读端关闭之后, OS向写端发送SIGPIPE信号终止写端进程


例二:

现象: 父进程fork创建子进程, 子进程结束之后, 如果父进程没有调用wait()/waitpid()等待回收子进程, 子进程进入僵尸状态, 则通常会在父进程代码逻辑中, 轮询等待/阻塞等待回收子进程, 另一种也可以通过信号捕捉的方式来等待回收子进程, 虽然本质上还是调用waitpid()

本质: 子进程退出后会给父进程发送SIGCHLD信号, 让父进程自定义捕捉SIGCHLD信号, 在自定义handler方法中去等待回收即可

以上两点会在补充内容中用代码验证

2).通过硬件产生

通过一系列的硬件操作之后, 将操作结果保存或标记在硬件中, 当操作结束OS检测硬件时就会检测出标记的错误, 然后就会给特定进程发送信号


例一: 除0错误

在计算机中进行运算的是cpu, 而cpu是硬件, cpu内部有很多种寄存器, 且很多个寄存器, 当发生除0时, 状态寄存器(以位图的方式), 标记溢出标记位, 当OS进行检测时便会检测出错误


例二: 指针的非法访问, 访问空指针

每个进程都有属于自己的进程地址空间, 我们称之为虚拟内存地址空间, 通过页表映射到物理内存, 那么当出现一个空指针或无效指针, 本质上就是这个指针不指向任何内容, 那么对于这个指针而言, 因为它不指向任何内容或有效内容, 所以页表中就会标记这种指针, 当访问的时候页表+MMU操作的时候会有越界问题, 被OS检测到, 就会给该进程发送信号

3).通过键盘组合键产生

一般通过键盘组合键的方式手动产生信号之后就自动的发送给指定进程了

例如: ctrl+c — 2号信号 — SIGINT

ctrl+\ — 3号信号 — SIGQUIT

二.信号的发送以及保存

1.信号如何发送

1).OS自动检查, 自动构建信号, 自动发送信号

一般的, 信号通过软件/硬件产生, 本质上也是由OS构建信号, 构建好之后也就自动的发送给进程了

2).可以手动发送, 指令: kill -[信号id] 进程pid

3).键盘组合键: ctrl+c, ctrl+\, …

2.信号如何保存

1).概念

实际执行信号的处理动作 — 递达(Delievery) — handler函数指针数组

信号从产生到递达之间的状态(发送中/待处理) — 未决(Pending) — pending位图

进程可与选择阻塞某个信号 — 阻塞(Block) — block位图

2).底层实现结构&&内核中的实现

存储信号的底层数据结构: 位图, 一个整数占4byte->1byte8个bit->32个bit, 刚好用一个整数表示32个普通信号, 通过0/1的方式

整体实现结构: 两个位图+一个函数指针数组

pending位图(信号存储集), block位图(信号阻塞集), handler函数指针数组(信号操作集)

被阻塞的信号产生时并发送到进程后, 会一直保存在pending(未决)位图中(对应的那一位bit为1), 只有当阻塞状态被解除时才会抵达, 也就是执行相应的handler处理动作

信号默认是非阻塞的, 并且默认的处理动作是默认动作或忽略动作, 即SIG_DFL或SIG_IGN, 当捕捉到信号时会修改对应信号的函数指针(操作方法)

3).阻塞vs忽略

阻塞与忽略是不同的, 信号一但被阻塞就不会被递达, 但信号如果被忽略, 仍会被抵达, 只不过执行的动作是忽略, 本质区别就是前者没有被抵达而后者反之

三.信号的处理以及操作

1.信号被处理的时机(重点)

信号相关的数据是存放在PCB中的, 想要修改PCB数据, 必须经过内核也就是必须由OS操作, 而我们在程序中对PCB中内核数据结构的修改也必须是通过系统调用的方式, 本质上还是由OS去执行的, 处理信号意味着改变PCB内核数据结构中的数据, 所以信号操作一定是在内核态的状态下进行的, 由于内核态下执行的代码优先级非常高, 所以OS设计者选择将信号的处理放在, 进程从内核态转变为用户态的前一刻信号被处理

科普一

当进程进行系统调用/出现异常/中断等操作时会进入内核态, 进入内核态的过程对于我们而言是透明的(系统调用本质是通过int 80汇编使当前进程陷入到内核中), 那如果是一个没有系统调用, 也不会出现异常的进程, 也是一定会进入内核态的, 因为进程在OS上跑, 而OS对进程采用类似于轮询检测的方式发生中断, 来检查进程时间片和其他等等, 当切换到用户态的前一刻, OS就会去处理进程中待递达的信号

科普二

进程如何去执行操作系统级别的代码呢

每个进程都有属于自己的4G虚拟内存地址空间, 其中3~4G是内核地址空间, 每一个进程都是如此, 我们平时说的进程去执行操作系统的代码了, 就是进程进入了内核态, 去访问那3~4G的数据, 而3~4G的数据映射的是什么呢, 对于1~3G的用户空间而言, 为了保证进程的独立性, 每个进程的虚拟地址空间都有自己的页表(用户级页表)映射到不同的物理地址, 而在3~4G内存空间有对应的内核级页表, 每个进程中的1G内核空间都通过同一张内核级页表映射到该机器的操作系统中, 去执行OS级别的代码(系统调用…), 内核本质上也是在所有进程的地址空间上下文中执行的, 普通进程是否有权利执行内核代码完全取决于CPU是处于哪一种状态,内核态or用户态?

科普三

什么是进入内核态, 计算机如何区分当前CPU是处在用户态还是内核态

首先明确一个概念, 用户态还是内核态是针对CPU而言的, 并不是进程, 当一个进程可以执行OS代码说明当前的CPU处于内核态, CPU中存在很多寄存器, 一套可见, 一套不可见, 其中CR3寄存器表示当前CPU的执行权限例如1表示内核3表示用户

2.信号的三种处理方式

1 默认 — SIG_DFL — #define SIG_DFL ((sighandler_t)0)

2 忽略 — SIG_IGN — #define SIG_IGN ((sighandler_t)1)

— typedef void(*sighandler_t)(int);

3 自定义捕捉

3.如何对信号自定义捕捉


1.signal

#include <signal.h>

typedef void(*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);


参数:

signum: 要捕捉的信号编号

handler: 函数指针

需要自己定义一个void handler(int signum)函数, 该函数是捕捉到指定信号后对应的自定义处理方法

以回调函数的方式, 向signal传入handler函数指针对handler函数进行回调


返回值:

捕捉成功, 返回旧的handler函数, 即信号操作方法

捕捉失败, 返回SIG_ERR, 并且错误码被设置

代码验证: 使用signal捕捉SIGINT信号

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

using namespace std;

//注册自定义捕捉信号的处理方式
void handler(int signum)
{
  printf("进程[%d],已捕捉到%d信号\n", getpid(), signum);
}

int main()
{
  //自定义捕捉SIGINT信号(2号)
  //注意:这里只是在注册捕捉到的递达信号的处理行为,并不是在这里调用handler,而是注册!
  //handler是在SIGINT信号递达时调用的
  signal(SIGINT, handler);
  
  printf("I am process: %d\n", getpid());

  //不让进程退出, 方便观察信号递达后的自定义捕捉行为
  while(true) sleep(1);

  return 0;
}


2.sigaction

#include <signal.h>

int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);


解释: struct sigaction结构体

struct sigaction

{


void (*sa_handler)(int);                              //关心, 注册自定义信号处理方法

void (*sa_sigaction)(int, siginfo_t *, void *);//不关心, 不用管

sigset_t sa_mask;                                      //关心, 信号屏蔽字

int sa_flags;                                               //不关心, 默认为0

void (*sa_restorer)(void);                           //不关心, 不用管

};


参数:


signo: 信号编号

act: 若为非空, 根据act修改信号处理动作

oact: 若为非空, 通过oact输出旧的信号处理动作


对于act.sa_mask重点介绍

当捕捉一次信号之后, 被捕捉的信号在处理完之前默认在信号屏蔽字中将该信号屏蔽, 处理好之后再接触相应的屏蔽, sa_mask即是去设定在处理当前捕捉到的信号时对于屏蔽字的处理(可以添加一些其他屏蔽的信号), 用sa_mask去覆盖掉当前屏蔽字, 这也是与signal方式捕捉信号的一大区别

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<assert.h>

using namespace std;

//lab3 -- sigaction捕捉信号的同时, 设置处理时屏蔽字

void MyPrint(sigset_t &set)
{
  for(int sig = 1; sig <= 31; ++sig)
  {
    if(sigismember(&set, sig)) cout << 1;
    else cout << 0;
  }
  cout << endl;
}

//捕捉SIGINT信号
void handler(int signum)
{   
  cout << "正在处理信号: " << signum << "(15s)" << endl;
  //当第二次发送2号信号时, 应该观察到pending会存有该信号
  //如果发送3456号信号, pending信号集也会有该对应的未决信号标识
  //因为处理2号信号期间2好信号被自动屏蔽,3456信号被手动屏蔽
  int n = 15;
  while(n--)
  {
    sigset_t set;
    sigemptyset(&set);
    sigpending(&set);
    printf("[%d]pending signal: ", getpid());
    MyPrint(set);
    sleep(1);
  }
}

int main()
{
  cout << "I am process: " << getpid() << endl;
  struct sigaction act, oact;
  act.sa_flags = 0;
  act.sa_handler = handler;
  sigset_t set;
  sigemptyset(&set);
  sigaddset(&set, 3);
  sigaddset(&set, 4);
  sigaddset(&set, 5);
  sigaddset(&set, 6);
  act.sa_mask = set;
  sigaction(SIGINT, &act, &oact);
  while(1)
  {
    sleep(1);
  }
  return 0;
}

4.sigset_t类型

sigset_t是系统级别的变量类型, 是linux定义的类型, sigset_t类型的变量用来表示block位图与pending位图

sigset_t称为信号集, 这个类型可以表示每个信号有效或无效状态, 即1或0

由sigset_t定义出来的变量不可以直接修改与打印, 需要通过以下函数来进行操作

常见操作:sigemptyset, sigfillset, sigaddset, sigdelset, sigismember, sigpending, sigprocmask

#include <signal.h>

int sigemptyset(sigset_t *set);                            // 初始化set所指向的pending信号集, 将其所有bit清零

int sigfillset(sigset_t *set);                                  // 初始化set所指向的pending信号集, 将其所有bit置为1

int sigaddset (sigset_t *set, int signo);               // 添加signo信号到set

int sigdelset(sigset_t *set, int signo);                 // 删除signo信号到set

int sigismember(const sigset_t *set, int signo); // 判断set是否包含signo信号

以上都需要先自定义一个set信号集

5.如何操作pending信号集

int sigpending(sigset_t *set);                             // 将pending信号集获取到set中, 这个set是输出型参数

代码验证: 打印pending信号集

void PrintPending(sigset_t &pending)
{
  for(int sig = 1; sig <= 31; ++sig)
  {
    //判断sig是否存在于pending信号集中
    //存在: 1 不存在: 0
    if(sigismember(&pending, sig))
    {
      cout << 1; 
    }
    else
    {
      cout << 0;
    }
  }
  cout << endl;
}

int main()
{
  sigset_t set;

  //初始化
  sigemptyset(&set);

  //获取pending信号集
  sigpending(&set);

  PrintPending(set);

  return 0;
}

6.如何操作block阻塞信号集

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的屏蔽是指阻塞而并不是忽略!

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);


参数:

how: 传入标记位, 以下的mask代表信号屏蔽字

SIG_BLOCK: 添加要阻塞的信号到信号屏蔽字, 相当于mask = mask | set

SIG_UNBLOCK: 从当前信号屏蔽字解除特定阻塞信号, 相当于mask = mask&~set

SIG_SETMASK: 设置当前信号屏蔽字为set所指向的位图, 相当于mask = set

set与oset:

如果oset是非空指针, 则读取修改之前的信号屏蔽字到oset

如果set是非空指针, 则根据how的修改方式去修改set信号屏蔽字, 然后覆盖掉进程的信号屏蔽字

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

代码验证: 逐一屏蔽1~31号信号, 然后不断向进程发送1~31号信号, 观察pending信号集

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<assert.h>

using namespace std;

//lab1 -- 逐一屏蔽1~31号信号, 然后不断向进程发送1~31号信号, 观察pending信号集
//注: 9号与19号信号不会被屏蔽

//打印pending信号集/block屏蔽字
void MyPrint(sigset_t &set)
{
  for(int sig = 1; sig <= 31; ++sig)
  {
    if(sigismember(&set, sig)) cout << 1;
    else cout << 0;
  }
  cout << endl;
}

//屏蔽signum信号
void MyBlock(int signum)
{
  sigset_t bset, obset;
  sigemptyset(&bset);//初始化
  sigaddset(&bset, signum);//屏蔽signum信号
  int n = sigprocmask(SIG_BLOCK, &bset, &obset);//设置信号屏蔽字并且获取旧的
  assert(n == 0);
  (void)n;

  //打印信号屏蔽字
  printf("[%d]block signal: ", getpid());
  MyPrint(obset);
}

int main()
{
  for(int sig = 1; sig <= 31; ++sig)
  {
    printf("[%d]屏蔽信号%d\n", getpid(), sig);
    MyBlock(sig);
    sleep(1);
  }
  sigset_t set;
  while(true)
  {
    //打印pending信号集
    sigemptyset(&set);
    sigpending(&set);
    printf("[%d]block signal: ", getpid());
    MyPrint(set);
    sleep(1);
  }
  return 0;
}

7.特殊的9号与19号信号

9号信号SIGKILL — 一定会杀掉指定进程

9号信号属于管理员信号, 不会被用户捕捉也不会被用户屏蔽!

19号信号SIGSTOP — 不会被用户屏蔽

8.block存在的意义

如果一个信号在同一时刻被发送了两次, OS如何处理?

回答: 当一个信号第一次被递达, OS在处理信号时, 在处理期间会将该信号屏蔽, 当处理完之后解除该信号的屏蔽, 也就是说上述问题, 同一时刻发送两次, OS也只能一个一个的进行对于信号的处理

为了保证系统安全, 避免信号递归式的处理, OS在同一时刻下同一种信号只能操作1次, 就是通过block屏蔽字实现的

注: 非实时信号(1~31普通信号), pending位图只能标记是否有信号未决, 并不能存储有几个相同信号的数量

所以如果同时发送三个相同信号, 那么也是先处理1次, 处理期间屏蔽该信号, 处理完再处理1次, 因为后两次是第一次屏蔽后发送的所以pending只能标记有该信号未决, 后续也只能处理1次, 所以同时发送了三个相同信号, 最终只会处理两次

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<assert.h>

using namespace std;

//lab2 -- block信号屏蔽字的意义

void MyPrint(sigset_t &set)
{
  for(int sig = 1; sig <= 31; ++sig)
  {
    if(sigismember(&set, sig)) cout << 1;
    else cout << 0;
  }
  cout << endl;
}

void handler(int signum)
{
  cout << "正在处理信号: " << signum << "(15s)" << endl;
  //当第二次发送2号信号时, 应该观察到pending会存有该信号
  //因为处理2号信号期间2好信号被自动屏蔽
  int n = 15;
  while(n--)
  {
    sigset_t set;
    sigemptyset(&set);
    sigpending(&set);
    printf("[%d]pending signal: ", getpid());
    MyPrint(set);
    sleep(1);
  }
}

int main()
{
  cout << "I am process: " << getpid() << endl;
  //用2号信号实验
  signal(SIGINT, handler);
  while(1)
  {
    sleep(1);
  }
  return 0;
}

四.信号的整体流程(总结)

产生信号(软件/硬件/键盘组合键)

—> 发送信号(自动/手动)

—> 保存信号(pending未决)

—> 内核态到用户态的前一刻,OS处理可递达信号

—> 判断是否阻塞(block屏蔽字)

—> 若未阻塞则处理信号(递达并处理, 方式: 默认/忽略/自定义捕捉)

五.补充内容

1.核心转储


关于核心转储:


如何触发核心转储: Core Dump

1

.

SIGQUIT — 默认处理动作为终止进程

2.abort()函数 — 本质发送SIGABRT — 默认处理动作当作异常去终止进程

3.异常 — 例如: 除0, 非法访问等等


表现为:

当进程异常终止时(段错误, 除零错误, 访问空指针等等), 会在当前目录下自动生成一个核心转储文件, 该文件内容全部都是二进制数据, 很显然, 这个文件不是用来直接让用户读取的


关于核心转储文件的使用:

我们可以使用Linux下的gdb调试工具载入core文件, 此时gdb调试工具会自动给我们显示出异常具体出现在哪一行, 该行为叫做Post-mortem Debug(事后调试)


注意:

当然, 在Linux默认是将Core Dump关闭的, 也就是默认不会生成core文件, 因为core文件中可能会包含用户密码等敏感信息, 不安全, 或者某种场景下进程也许会一直生成core文件, 大量的文件会占用磁盘空间


如何打开核心转储:

在开发调试阶段, 可以使用ulimit命令改变这个现实, 允许产生core文件, 使用ulimit命令改变Shell进程的Resource Limit且允许core文件最大为1024K(自定义)


waitpid()函数的第二个参数: status

做为输出型参数, 传入&status; status的倒数第八位用来标记Core Dump, 1 or 0, 即是否发生核心转储


总结/本质:

进程出现异常 or 收到SIGQUIT信号, 可以选择把进程的用户空间数据(内存中的核心数据)全部保存到磁盘上

ulimit -a: 查看

ulimit -c 1024(自定义): 打开核心转储

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

using namespace std;

int main()
{
  sleep(1);
  int a = 10;
  int b = 0;
  cout << a / b << endl;//除零错误
  return 0;
}

除零错误异常–>触发核心转储

发送SIGQUIT信号–>触发核心转储

使用od命令查看core文件

使用gdb操作core文件

1.使用gdb调试进程

2.core-file [core文件名]

2.volatile关键字(补充)

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

using namespace std;

int flag = 1;

void handler(int signum)
{
  cout << "old flag: " << flag;
  flag = 0;
  cout << "->new flag: " << flag << endl;
}

int main()
{
  signal(SIGINT, handler);
  // 验证volatile关键字
  while (flag);
  
  cout << "进程退出flag: " << flag << endl;
  return 0;
}

g++编译代码时不加优化选项, 此时volatile关键字使不使用没有区别

给g++编译命令添加-O3优化选项, 运行结果:

进程执行起来之后即使将flag从1改为0, 也没有跳出while循环

当加了-O3优化选项, CPU在读取flag全局变量时做出了优化, 对于flag而言, CPU不再每次循环都去内存访问flag而是直接将flag存到寄存器内, 因为在CPU看来flag变量是永远不会被修改的, 所以本质上-O3这个优化选项, 对于flag而言无法让CPU在看到内存了

此时, volatile即将登场, volatile关键字的作用: 被修饰的变量无视编译器的优化, 保持了变量的可见性(内存对于CPU而言)

接下来使用volatile修饰flag, 再使用-O3优化, 查看执行结果:

flag从1->0, 直接跳出while循环, 进程结束!

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

using namespace std;

volatile int flag = 1;

void handler(int signum)
{
  cout << "old flag: " << flag;
  flag = 0;
  cout << "->new flag: " << flag << endl;
}

int main()
{
  signal(SIGINT, handler);
  // 验证volatile关键字
  while (flag);

  cout << "进程退出flag: " << flag << endl;
  return 0;
}

3.kill()/raise()/abort()

1).kill()

kill属于指令, 可以向指定进程发送信号

kill命令是调用kill函数实现的, kill函数的功能同理: 可以给指定进程发送指定信号

#include<signal.h>

int kill(pid_t pid, int signo);

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

using namespace std;

int main()
{
  cout << "2s后进程结束" << endl;
  sleep(2);
  kill(getpid(), SIGINT);
  return 0;
}

2).raise()

raise函数可以给当前进程发送指定信号

#include<signal.h>

int raise(int signo);

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

using namespace std;

int main()
{
  cout << "2s后进程结束" << endl;
  sleep(2);
  raise(SIGINT);
  return 0;
}

3).abort()

abort函数 — 可以使当前进程接收到SIGABRT信号(6号)而异常终止

#include<stdlib.h>

void abort();

#include <iostream>
#include<unistd.h>
#include<stdlib.h>

using namespace std;

int main()
{
  cout << "2s后进程结束" << endl;
  sleep(2);
  abort();
  return 0;
}

4.定时闹钟

alarm()函数 — 闹钟函数 — 本质: 向当前进程发送14号信号 — SIGALRM

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

参数:

seconds秒之后, 给当前进程发送SIGALRM信号, 该信号的默认处理动作是终止当前进程(当然你也可以自定义捕捉)

返回值:

0 or 之前设定的闹钟剩余的秒数

假设定一个闹钟30min, 在第20min时再设定闹钟为15min, 此时返回10min即600s

注:

如果seconds为0, 表示取消以前设定的闹钟, 函数的返回值仍是以前设定的闹钟时间剩余的秒数

代码验证:

验证一: 自定义捕捉SIGALRM

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

using namespace std;

void handler(int signum)
{
  cout << "嘀嘀嘀~嘀嘀嘀~" << endl;
}

int main()
{
  signal(SIGALRM, handler);
  alarm(2);
  cout << "已设置闹钟, 2s后" << endl;
  int count = 4;
  while(count--)
  {
    sleep(1);
  }
  return 0;
}

验证二: 验证alarm返回值

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

using namespace std;

void handler(int signum)
{
  cout << "嘀嘀嘀~嘀嘀嘀~" << endl;
  exit(1);
}

int main()
{
  signal(SIGALRM, handler);
  alarm(10);//先设置一个10s闹钟
  int count = 4;
  while(count--)
  {
    sleep(1);
  }
  unsigned int ret = alarm(2);//4s后设置一个2s闹钟
  //ret预期为6
  cout << "alarm返回值: " << ret << endl;

  while(true);
  return 0;
}

实验: 使用定时闹钟达到轮询效果

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

using namespace std;

void handler(int signum)
{
  cout << "嘀嘀嘀~嘀嘀嘀~" << endl;
  alarm(2);
}

int main()
{
  signal(SIGALRM, handler);
  alarm(2);//设置2s闹钟
  while(true);
  return 0;
}

5.SIGCHLD信号SIGPIPE信号

1).SIGPIPE


SIGPIPE信号产生原理:

使用匿名管道让父子进程进行通信, 让某一进程做写端, 某一进程做读端, 当关闭读端, 写端会被OS发送SIGPIPE信号(因为读端关闭了, 写端就没有任何意义)


代码验证: 父写子读

#include<iostream>
#include<unistd.h>
#include<string>
#include<assert.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstring>

using namespace std;

void handler(int signum)
{
  cout << getpid() << " I am father, I receive a signal: " << signum << endl;
  cout << "1s后回收子进程,并终止父进程\n";
  sleep(1);
  waitpid(-1, nullptr, 0);
  exit(2);
}

int main()
{
  //目标: 验证SIGPIPE信号
  
  //父进程一开始就注册好SIGPIPE操作
  signal(SIGPIPE, handler);

  printf("I am father: %d\n", getpid());

  //创建匿名管道
  int pipefd[2];
  int ret = pipe(pipefd);
  assert(ret == 0);
  (void)ret;
  //创建子进程
  pid_t id = fork();
  if(id == 0)
  {
    printf("I am child: %d\n", getpid());
    //执行子进程逻辑,让子进程做读端
    close(pipefd[1]); 
    //进行通信
    //...
    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    while(true)
    {
      ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
      buffer[n] = '\0';
      printf("子进程已接收到数据: %s\n", buffer);
      if(n == 0)
      {
        cout << "写端已退出, 我也即将退出\n";
        break;
      }
      if(strcmp(buffer, "quit") == 0)
      {
        break;
      }
    }
    //读端退出后, 向父进程发送SIGPIPE信号
    printf("子进程(读端)即将退出\n");
    exit(1);
  }
  //父进程做写端
  close(pipefd[0]);
  string msg;
  //不停的写, 即使输入quit将读端退出了, 也一直写
  while(true)
  {
    getline(cin, msg);
    write(pipefd[1], msg.c_str(), msg.size());
  }
  return 0;
}


代码验证: 父读子写

#include <iostream>
#include <unistd.h>
#include <string>
#include <assert.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>

using namespace std;

// 验证SIGPIPE: 父读子写

int main()
{
  // 目标: 验证SIGPIPE信号
  printf("I am father: %d\n", getpid());
  // 创建匿名管道
  int pipefd[2];
  int ret = pipe(pipefd);
  assert(ret == 0);
  (void)ret;
  // 创建子进程
  pid_t id = fork();
  if (id == 0)
  {
    printf("I am child: %d\n", getpid());
    // 执行子进程逻辑,让子进程做写端
    close(pipefd[0]);
    // 进行通信
    //...
    string msg;
    while (true)
    {
      getline(cin, msg);
      write(pipefd[1], msg.c_str(), msg.size());
      // 这里不需要让写端break, 因为读端关闭了写端也就关闭了
    }
    // 读端关闭后, 向子进程发送SIGPIPE信号
    // 继而子进程被SIGPIPE信号结束,等待父进程回收检测
    exit(1);
  }
  // 父进程做读端
  close(pipefd[1]);
  char buffer[1024];
  memset(buffer, 0, sizeof(buffer));
  while (true)
  {
    ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
    buffer[n] = '\0';
    printf("父进程已接收到数据: %s\n", buffer);
    if (n == 0)
    {
      cout << "写端已退出, 我也即将退出\n";
      break;
    }
    if (strcmp(buffer, "quit") == 0)
    {
      break;
    }
  }
  close(pipefd[0]); // 关闭读端
  // 子进程做为写端也会SIGPIPE结束,等待回收子进程
  int status = 0;
  waitpid(-1, &status, 0); // 阻塞回收
  cout << "回收子进程成功, 检测到子进程退出信号: " << (status & 0x7f) << endl;
  return 0;
}

2).SIGCHLD

SIGCHLD信号产生原理: 当父进程创建了子进程, 子进程退出或暂停时, 就会给父进程发送SIGCHLD信号

OS的对SIGCHLD信号的默认处理行为是Ign(忽略)


这有什么意义呢?

如果子进程退出了, 而这时父进程还需要执行自己的逻辑代码, 还没有到达回wait/waitpid逻辑时, 此时就可以采用捕捉SIGCHLD信号的方式, 来在执行父进程逻辑的过程中, 在合适的时机去处理SIGCHLD信号(注意: 信号处理方法代码与进程代码在处理过程中是同一执行流), 也就是说父进程不需要主动关心子进程的回收问题了, 而是当信号递达父进程就处理即可

可以使用signal()捕捉SIGCHLD信号, 然后对子进程进行回收

也可以使用signal()捕捉SIGCHLD信号, 然后执行SIG_IGN(忽略), 这与OS默认的Ign有所区别


Ign与SIG_IGN的区别

Linux规定, 父进程捕捉SIGCHLD处理动作置为SIG_IGN这样子进程在终止退出时OS自动清理回收, 不会产生僵尸进程, 也不会通知父进程, 此方法对于Linux可用, 并不代表在其他UNIX系统上都可用

一般的Term为Ign的信号就是OS直接忽略掉了, 说白了就是什么都不做, 系统默认的忽略动作Ign和用户用signal()/sigaction()函数自定义的忽略通常情况下是相同的, 但这是一个特例!


代码验证:

#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

void handler(int signum)
{
    cout << "父进程接收到信号: " << signum << endl;
    // 轮询检测式回收子进程,WNOHANG非阻塞等待
    // 注: 
    pid_t id = 0;
    while ((id = waitpid(-1, nullptr, WNOHANG)) > 0)
    {
        cout << "成功回收进程: " << id << endl;
    }
    cout << "handler处理结束, 已返回父进程逻辑代码\n";
}

int main()
{
    // 处理SIGCHLD方式一: 自定义捕捉并且回收子进程
    signal(SIGCHLD, handler);
    // 处理SIGCHLD方式二: 忽略
    // signal(SIGCHLD, SIG_IGN);
    cout << "I am father: " << getpid() << endl;
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        cout << "I am child: " << getpid() << ", 我将在2s后退出" << endl;
        sleep(2);
        exit(0);
    }
    while (true)
    {
        sleep(1);
    }
    return 0;
}

方式一:

方式二:



版权声明:本文为Hello_World_213原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。