【Linux】进程通信

目录:

  • 信号完整生命周期:信号产生 → 信号保存 → 信号处理
  • 信号产生:
    • 7.进程通信#一、信号是什么
    • 7.进程通信#二、产生信号
  • 信号保存:
    • 7.进程通信#三、阻塞信号
  • 信号处理:
    • 7.进程通信#四、捕捉信号
  • 其他相关:
    • 7.进程通信#五、可重入函数
    • 7.进程通信#六、volatile关键字
    • 7.进程通信#七、SIGCHLD信号(选学了解)

一、信号是什么

联系实际

  • 区分信号和信号量:

    • 信号与[[6.进程间通信(IPC)#3 信号量(只说原理)]]无任何联系,属于两套不同体系。
    • 信号:属于通信范畴
    • 信号量:属于用于互斥和同步通信体系的。
  • 生活中的信号:红绿灯;烽火;线上购物商品;外卖等

  • 信号:本质是一种通知机制,用户或操作系统通过发送一定的信号,来通知进程某些事件已经发生,你可以在后续进行处理

信号特点

  • ①进程要处理信号,必须具备“识别”信号的能力(看到+处理动作)
  • ②程序员让进程能够“识别”信号==>进程内部提前规定了这个信号应该如何被处理。
    • kill -9杀死一个进程,本质就是对进程发送了9号信号来杀死进程,此处“9”就是一个信号
  • ③信号产生是随机的,产生时进程可能在忙其他事==>故信号的后续处理,可能并非立即处理
  • ④会临时记录对应的信号,方便后续进行处理
  • ⑤会在合适的时候处理信号
  • ⑥一般而言,信号的产生相对于进程是异步

信号如何产生

  • 本质:通过键盘组合键向目标进程发送信号
    • ctrl+c本质就是通过键盘组合键向目标进程发送2号信号 --> 进程退出
  • 信号处理的常见方式:
    • a.默认(每一种信号都有默认的处理动作,进程自带的、程序员写好的逻辑)
    • b.忽略(将计算机中记住的信号忘掉)
      • 如:闹钟响了,但是仍不想醒来。
    • c.自定义动作(捕捉信号)
      • 如:闹钟响了,默认是起床,但是可以自定义动作来想跳广播体操

常见信号

  • kill -lLinux中对应的所有的信号:普通信号和实时信号(标绿为相对常见)

    • 共62个信号(没有32,33,没有0号信号):[1:31]信号被称为普通信号 ;[34:64]信号中带有RT的称为实时信号

    • 【Linux】进程通信_第1张图片

    • 实时操作系统:有严格的时序,需要立马严格地响应、处理完成。

    • 分时操作系统:基于时间片的轮转调度算法(一个时间片上的时间乘以用户数,前者一定,后者越多,响应时间就越长)的操作系统

      • 目前所用操作系统,分时操作系统居多
    • 如:汽车中的车载操作系统,有些操作系统是Linux,也会参与我们的汽车的操作(如刹车)。若是分时操作系统,刹车进程没货的处理机的处理,就不会马上刹车;若是实时操作系统,刹车就会立即执行。
      1.SIGHUP挂起
      2.SIGINT:中断/终止进程
      3.SIGQUIT:退出
      5.SIGTRAP和6.SIGTRAP:终止
      8.SIGFPE:浮点不透?
      9.SIGKILL:捕捉信号9号信号
      10.SIGUSR1:用户自定义信号
      11.SIGSEGV:段错误对应的错误
      13.SIGPIPE:向已关闭的管道中写入可能会出错误
      14.SIGALRM:闹钟

  • $ kill 7 signal【Linux】进程通信_第2张图片

    • value:认识信号的编号
    • action:默认处理行为 ==> 信号对应的动作
      • Term:终止。 70%上的信号,被进程收到就是默认终止该进程
      • Core:终止且发生核心转储
        • 回顾核心转储介绍:[[4.Linux进程控制#core dump 标志:是否发生了核心转储。]]
      • Ign:忽略
      • Cont:继续

组合键如何变信号?

  • 键盘的工作方式:中断 ==> 故能识别组合键
  • 过程:OS解释组合键 ==> 查找进程列表 ==> 找到前台运行的进程 ==> OS写入对应的信号到进程内部的位图结构

信号如何被进程保存?

  • 采用位图的方式:
    • 信号编号? ==> 用unsigned int中的第几位表示;
    • 是否产生? ==> 用对应比特位0/1表示
    • 0000 0100表示第三个信号产生
  • 信号位图字段是保存在进程PCB内部task_struct)的,而task_struct是内核数据结构(操作系统内的数据结构),故只有操作系统才有资格修改内核数据结构。==> 即,信号产生方式多种,但都是操作系统发的(仅有OS有资格修改PCB内部的相关字段)
  • 信号发送的本质:OS向目标进程写信号,即OS直接修改PCB中指定的位图结构,完成“发送”信号过程。

中断概念

计算机组成学科中相关中断概念
  • 硬件:
    • ①中断设备
    • ②硬件单元(如8259)
    • ③CPU的针脚
  • 软件:
    • ①中断向量表
    • ②上下文线程保存和线程恢复
信号如何从硬件到软件?
  • 前提:每一个设备都有对应的中断编号(简称中断号),中断号可理解作数组下标。==> 整个过程类似函数指针数组这样的概念。
  • ①中断表和软件的对应,即用函数指针数组的方式和一张表来对应。当对应的某设备一旦触发了中断,就会通过硬件的方式,向计算机中(如CPU某些针脚上)发送对应的中断信息。
  • ②中断信息对应的中断号就以硬件方式写入到特定的寄存器当中。
  • ③操作系统就能够识别到对应寄存器内部的中断号,就可根据中断编号查找内置相关的中断处理方法。
  • ④调用中断处理方法,完成从硬件到软件的行为
中断的分类
  • ①网卡中断
    • #待补充 学习网络时候,会再把中断拿出来,帮助同学们做一个有完整性认知
    • 网卡当中发送、接收消息时,硬件上先收到消息,软件上操作系统是如何得知有数据了吗?其实也是中断去处理的
  • ②时钟中断
    • 在计算机中,除了外设(包括网卡、键盘等)设备会触发中断,还有一类中断叫做时钟中断
      • 它的频率非常高频(基本上是纳秒级别),每隔一定时间就会向对应的计算机触发时钟中断,一旦触发,操作系统就必须得先处理时钟中断。
      • 相当于计算机中有个天然的机制,每几纳秒就向操作系统发送对应的输入中断,操作系统就必须先执行软件的一个处理,此时即可进行诸如进程调度、调度时间检查、上下文切换、识别标准输入、识别网络相关的数据等等用户行为。
    • 操作系统即是采取此驱动方式–时钟中断。
      • 有的程序跑一遍就结束;有的程序它永远不退出,即主逻辑是个死循环( #待补充 往后学习网络时,网络服务器就是死循环,永不退出,除非挂了)。
      • 操作系统启动之后永远也不退出的,其也属软件,操作系统的本质就是个死循环地执行运作进程,它能周而复始的去做一件事情;
    • 例子:
      • ①刷新:“文件从应用层把数据刷新到操作系统里,操作系统会自动去将数据刷新到外设” 当中“刷新到外设”,正是由于操作系统内部的内核级相关执行流,这些执行流被操作系统定期根据时钟中断来调度(执行对应函数指针数组里定的方法来调度。调度的可能是用户进程,也可能是内部线程或者其他执行流),最终定期完成刷新行为。但若触发后检测时发现进程的时间片没有到达,它就什么都不干。
      • ②CPU的主频:每秒内可以进行响应指令的一个条数。主频越高,响应中断的时间单元节点那么越短。正因此,操作系统自动会直接进行各方面调度切换、各种内存管理、文件管理,包括有些文件定期被清理等。

二、产生信号

1. 通过终端按键产生信号 →a.键盘

  • signal命令
#include 
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
  • 用途:通过回调的方式,修改对应的信号捕捉方法

  • signum:对哪一个信号进行捕捉

  • handler:函数指针类型的参数

    • 对函数中传入函数指针==> [[指针进阶#8、回调函数]]
  • void(*sighandler_t)(int):返回值是void,参数是int的函数指针

  • $ ulimit -a:查看当前环境中相关资源配置的问题。

  • mysignal相关使用:

    • 分析点①:signal函数仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作。如果后续没有任何SIGINT信号产生,catchSig永远也不会被调用。每个特定信号的处理动作,一般只有一个,修改后默认的就会被覆盖。
#include 
#include 
#include 

using namespace std;

void catchSig()
{
    cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl
}

int main()
{
	//signal一般是写在main函数开头
    //“ctrl+c”是2号信号SIGINT是键盘产生的
    //signal(SIGINT, fun);//a.可直接写名(推荐,可读性好)
     signal(2, fun); //b.也可写编号
     signal(SIGINT, catchSig); //分析点①
     signal(SIGQUIT, catchSig);//“ctrl+\”是3号信号SIGQUIT

     while(true)
     {
         cout << "我是一个进程,我正在运行..., Pid: " << getpid() << endl;
         sleep(1);

         int a = 100;
         a /= 0;//除0后会触发8号信号

         cout << "run here ...." << endl;
     }
     return 0;
}
  • core文件形成后的调试(事后调试)==> 主要仅展示core的应用
步骤1:查看是否已核心转储形成core文件
$ ll 
	core.11077
	……
步骤2:
$ gdb mysignal # 可执行文件
(gdb) core-file core.11077
步骤3:
# 会自动定位到终止的错误代码位置

2. 调用系统函数向进程发信号 → b.系统调用接口

  • 理解:用户调用系统接口–>执行OS对应的系统调用代码–>OS提取参数,或者设置特定的数值–>OS向目标进程写信号–>修改对应进程的信号标记位–>进程后续会处理信号–>执行对应的处理动作
kill接口
  • 用途:向指定进程发送指定信号
#include 
#include 
int kill(pid_t pid, int sig);//pid指定进程;sig指定信号
  • 实现mykill
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;
static void Usage(string proc)
{
	cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}

// ./mykill 2 pid ==>三个参数
int main(int argc, char* argv[])
{
	if(argc != 3) //命令行参数argc不为3,就是传参错误
	 {
		 Usage(argv[0]);
		 exit(1);
	 }

     int signumber = atoi(argv[1]);//①发几号的信号:0是可执行程序名字,1是第一个选项(即"./mykill 2 pid"中的第二个参数)
     int procid = atoi(argv[2]);//②给哪个进程发信号

     kill(procid, signumber);//给谁发什么信号
     return 0;
}
raise接口
  • 自己向自己发送指定信号
#include 
int raise(int sig);
  • raise的应用
#include 
#include 
#include 
#include 
#include 

using namespace std;

int main(int argc, char* argv[])
{
	cout << "我开始运行了" << endl;
    //调用方式①
    //sleep(1);
    //raise(8); // = raise(6) = kill(getpid(), 6)
    //调用方式②
    abort(); // 自己给自己发6号信号,通常用来进行终止进程(终止进程:exit/return/abort)
    
    return 0;
}

3. 由软件条件产生信号 → c.软件条件

  • 信号的产生可能无关用户操作,有可能是系统内部一些特定条件不满足,操作系统就会自动识别,从而发送信号
管道读关,写继续
  • 对于管道的读端不光不读,而且还关闭了,写端一直在写==>会导致写没有意义,OS会自动终止对应的写端进程,通过发送13号信号SIGPIPE的方式
    • 验证流程:①创建匿名管道 ②让父进程进行读取,子进程进行写入;③父子可以通信一段时间(非必需);④让父进程关闭读端,并且进入waitpid()等待子进程,子进程只要一直写入即可 ⑤结果必然,子进程退出,则父进程waitpid即可拿到子进程的退出status ⑥提取退出信号
    • 为什么发送信号? > 操作系统识别到管道(管道是通过文件在内存级的实现)读端关闭了却还在写这个问题>此情况为软件条件不满足==>故OS直接发送13号信号SIGPIPE
  • 理解:
    • a. OS先识别到某种软件条件触发或者不满足
    • b. OS构建信号,发送给指定的进程
alarm函数
  • 用途:设定一个秒级别的闹钟,告诉内核在seconds秒后给当前进程发送SIGALRM信号, 该信号默认处理动作是终止当前进程。
#include   
unsigned int alarm(unsigned int second
测试CPU的计算能力:验证1s之内,共会进行多少次count++
  • 实现基本定时器功能
#include 
#include 
#include 
#include 
#include 

using namespace std;

uint64_t count = 0;
// 实现基本定时器功能
void catchSig(int signum)
{
	cout<<"final count : "< 这俩个都是阻塞式IO
    /*int count = 0; 
    while (true)
    {
        cout << "count:" << count++ << endl;
    }*/
    
    // 2. 单纯计算算力
    signal(SIGALRM, catchSig);
    while(true) count++;//纯计算,无IO

    return 0;
}
  • 定时自动处理(如打印日志、刷新数据等)
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

typedef function func;
vector callbacks;

uint64_t count = 0;

void showCount()
{
    cout << "final count : " << count << endl;
}

void showLog()
{
    cout << "这个是日志功能" << endl;
}

void logUser()//定期查看用户是谁
{
    if(fork() == 0)//若是子进程
    {
        execl("/usr/bin/who", "who", nullptr);
        exit(1);
    }
    wait(nullptr);
}

// 此就可以用于处理数据自动定时刷新等任务
// void flushdata()
// {
// }

void catchSig(int signum)
{
    for(auto &f : callbacks)
    {
        f();
    }
    alarm(1);
}

int main(int argc, char* argv[])
{
	alarm(1); 
    signal(SIGALRM, catchSig);
    callbacks.push_back(showCount);
	callbacks.push_back(showLog);
	callbacks.push_back(logUser);
    while(true) count++;

    return 0;
}

4. 硬件异常产生信号 → d.硬件异常

理解除0问题()
  • ①进行计算的是CPU硬件
  • ②CPU内部是有多种寄存器,其中有个状态寄存器(软硬件的耦合方式)
    • 状态寄存器(可看作位图),其不进行数值保存,仅单纯保存本次计算状态。
    • 状态寄存器有对应的状态标记位,如记录有无进位、有无溢出、结果正负、是否为零等等。
    • 各标记位由硬件层面CPU设置,再由软件层面操作系统软件识别、自动进行计算完毕之后的检测。如果溢出标记位是1,OS立马就能识别到有溢出问题,并找到当前运行进程,提取其PID。而后OS完成信号发送的过程,进程会在合适的时候进行处理。
      • 为什么OS能立马找到?==>因为CPU内部的数据都是当前正在运行进程的上下文数据;而操作系统内核中有个指针test_struct*,该指针定义了一个current指向的当前正在运行的进程pcb。当进程被调度的时候,current指针内的内容也会load到CPU内其他相关的寄存器
    • 因此除0错误属于硬件异常
  • ③一旦出现硬件异常,进程不一定会退出(虽然一般默认是退出的),但我们即便不退出,也做不了什么
  • ④死循环的原因:寄存器中的异常一直没有被解决。
    • 最好办法:打完日志后终止进程,自动释放上下文信息
理解野指针或越界问题
  • ①越界、野指针问题在Linux中都可称段错误,无论哪种都必须通过地址,找到目标位置
    • 读取未必报错,写入都会报错
  • ②语言上面的“地址”,全都是虚拟地址
  • ③在进行指针等访问时,需将虚拟地址转换成物理地址
    • 虚拟→物理转化对效率要求很高,故采用硬件:页表(其中字段是二进制的,通过硬件形式对应到物理内存中)+内存管理单元MMU(Memory Manager Unit,属硬件)
  • ④“野指针、越界”定是非法地址 --> 则MMU转化的时候,一定会报错 --> 操作系统就把MMU的报错转换成信号发送给进程
    • 不是仅CPU存在寄存器,几乎任何外设和常见的硬件都可能存在寄存器。故MMU内部也有相关的寄存器或其他电路,从而能被CPU读取
using namespace std;

void handler(int signum)
{
    sleep(1);
    cout << "获得了一个信号: " << signum << endl;
    // exit(1);
} 


int main(int argc, char* argv[])
{
    //①除0异常
    signal(SIGFPE, handler);
    int a = 100;
    a /= 0;

    //②越界、野指针问题在Linux中都可称段错误==>对应11号SIGSEGV信号
    signal(SIGSEGV, handler);//捕捉到SIGSEGV就执行handler()

    int* p = nullptr;
    *p = 100;//p解引用后是指向0号地址,往没有写权限的0号地址写属于段错误

    while (true) sleep(1);

    return 0;
}

5. 对信号的总结:

  • 所有的信号都有它的来源,但最终全部都是被OS识别、解释并发送

    • 以后再有新的信号来源,思考方向:它的场景产生的问题最终是如何被OS识别并解释的?
  • 问题:

    1. 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
      • 答:因为OS是进程的管理者
    2. 信号的处理是否是立即处理的?在合适的时候
    3. 信号如果不是被立即处理,信号需要暂时被进程记录下来,记录在PCB对应的信号位图当中
    4. 一个进程在没有收到信号的时候,能知道应该对合法信号作何处理:程序员提前编写好了
    5. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
      • 答: 操作系统根据信号编号,修改对应位图中其特定的比特位,由0置1完成信号发送。

三、阻塞信号

1.信号其他相关常见概念

  • 实际执行信号的处理动作,称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞 (Block )某个信号。
    • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
    • 区别忽略和阻塞
      • 忽略:已经递达并处理了这个动作,只是处理方式是忽略
        • 信号递达方式有3种:默认;忽略;自定义
      • 阻塞:未进行递达(根本不会进入信号处理流程)。若某信号被阻塞且永远不解除阻塞,那它永远都无法递达

2.信号在内核中的表示

Pending位图(Pending信号集) --> 整数
  • 与信号未决概念有关
  • 操作系统修改Pending位的信息:
    • 比特位的位置代表信号编号;
    • 比特位的内容(0和1)代表是否收到信号 ==> 0没有收到;1收到信号
    • 0000 0001:代表收到了1号信号;0000 0000:代表没有收到1号信号
handler处理方法 --> 函数指针数组
  • 与信号递达概念有关
typedef(*handler_t)(int);
handler_t handler[32];
  • handler数组属于函数指针数组(起映射表的作用),数组的下标就是信号的编号
  • signal(signum,handler);:调用signal时,并非直接调用handler方法,而仅是将自定义的捕捉动作方法填入到handler数组下标里,真正的执行调用在后续才被实现。识别信号并根据对应动作执行的过程如下:
取到信号编号signal
handler[signal];
(int)handler[signal]==0;//执行默认动作,done
(int)handler[signal]==1;//执行忽略动作,done
handler[signal]();//当前两个动作都不满足时,才回去调用对应的方法
block位图(信号屏蔽字)
  • 与阻塞有关
  • block位图与pending位图的异同:
    • 【同】两者结构一样
    • 【异】block位图中的内容代表:是否阻塞/屏蔽对应的信号;pending位图中的内容代表:是否收到信号
总结:一个信号被处理的过程

【Linux】进程通信_第3张图片

  • ①OS向目标进程发送信号:即修改pending位图

  • ②进程在合适时间处理信号:遍历检测pending位图,当找到为1的比特位时 ==> 先去block表查看当前信号是否被阻塞,若为1被阻塞则不处理当前信号;若为0,才进入handler表调用处理对应信号的方法

  • 提高计算机学习认知

    • ①结构②方案③算法
      • ==> 结构决定算法 ==> 如对于链表:头插、尾插、遍历、删除;而对于队列和栈具体算法的处理方式又有不同,但思想层面是相似的(结构决定了算法也就出来了)
    • 明白学习一门学科,其究竟扮演着什么样的角色?
      • 数据结构扮演形成结构的过程(无论是结构体/指针/位图,都是结构)==> 语言层形成先描述的能力,而数据结构这门学科提供了再组织的能力,还需要编写匹配的算法(算法绝大部分都是基于某种结构的算法)
      • 为什么要构建如此的结构/对象?为什么以这种方式去构建呢?为什么要编写匹配的算法呢?
        • 一定是出于某种需求,需要解决某种问题(如进程排队问题、文件组织、编写内核级文件系统问题、编写某某文件管理系统等)
      • 学习要知道自己在学什么,知道所学的内容扮演的是什么样的角色,而不是单纯的把知识背记住==>提高计算机学习的认知,知识难度不降,但降低了学习成本

3.sigset_t

  • 回顾以前学的操作系统类型:pid_t

    • [[3.Linux进程概念#通过系统调用创建进程 – fork初始]]
  • 基本上,语言会提供.h, .hpp和语言的自定义类型; 同时OS也会提供.h和OS自定义的类型

    • 为什么OS要提供自定义的类型呢?
      • 因为要和OS提供的.h内部接口相对应(相当于系统调用接口). 有的接口不允许用户去传语言层的参数, 而是需要传结构体/位图等,而既然OS提供了对应接口,就必须也提供对应的类型
    • 若对应的语言层某方法接口,涉及到硬件访问(如外设等),那么它底层100%必须要访问操作系统 ==> 语言层内部封装了系统调用 ==> 因此语言类提供的头文件(.h && .hpp)一定会包含OS提供的头文件(.h)以及OS自定义的类型
  • sigset_t属于位图结构, 也属于OS提供的一种数据类型

    • 不必关心其细节,仅需知道它是个位图即可
    • sigset_t不允许用户自己进行位操作, OS有提供对应的操作位图的方法 ==> [[7.进程通信#4 信号集操作函数]]
    • ② user可以直接使用sigset_t类型,和用内置类型&&自定义类型没有任何差别
    • sigset_t一定需要对应的系统接口来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象
  • 由[[7.进程通信#2 信号在内核中的表示]]可见,每个信号只有一个bit的未决标志(0/1)和一个bit的阻塞标志,且并不记录该信号产生了多少次。

    • ==> 未决和阻塞标志可以用相同的数据类型sigset_t来表示(并非是“存储”,存储的问题是由内核考虑的)
    • sigset_t称为信号集(sigset_t位图结构,把所有信号放一起为集合的形式),这个类型可以表示每个信号的“有效”或“无效”状态。
      • 在阻塞信号集(即block信号集==>也有教材称其为信号屏蔽字Signal Mask)中“有效”和“无效”的含义是:该信号是否被阻塞;
      • 在未决信号集(即pending信号集)中“有效”和“无效”的含义是:该信号是否处于未决状态。

4.信号集操作函数

#include   
int sigemptyset(sigset_t *set);  //清空:全置0
int sigfillset(sigset_t *set);   //全置1
int sigaddset (sigset_t *set, int signo);  //将特定信号添加至信号集中(设置信号集中比特位)
int sigdelset(sigset_t *set, int signo);  //从信号集当中删除对应信号
int sigismember(const sigset_t *set, int signo);  //判定某个信号是否在信号集中
sigpending接口
#include
int sigpending(sigset_t *set);
  • 用处:获取当前进程pending信号集中的所有位图结构 ==> 将操作系统OS内的数据通过sigpending接口拿给用户user
  • 返回值:成功0;失败-1
sigprocmask接口
#include
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
  • 作用:检查并更改block信号字
  • 返回值:成功0;失败-1
  • 各参数说明
    • int how可选值如下:
      • SIG_BLOCK:set包含要添加到当前信号屏蔽字的新信号 ==> mask = mask|set
      • SIG_UNBLOCK:set包含了要从当前信号屏蔽字中解除阻塞的信号 ==> mask=mask&~set
      • SIG_SETMASK:设置当前信号屏蔽字为set所指向的值 ==> mask = set
    • sigset_t *oldset:输出型参数,用于返回旧的信号屏蔽字。
      • 若没需求,可直接设为nullptr
      • how参数的三种可选值,均是对原始进程的信号屏蔽字做更改,而*oldset参数是给原信号屏蔽字做备份,以备对旧信号的不时之需

5.编码验证

验证思路:
//makefile文件
mysignal:mysignal.cc
	g++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:
	rm -f mysignal
  • ①若我们对所有的信号都进行了自定义捕捉,是否旧写了一个不会被异常或者用户杀掉的进程?
    • 不是,OS的设计者也考虑了。9号信号是无法被自定义捕捉的,就是用于避免该情况。
//mysignal.cc文件
#include 
#include 
#include 

void catchSig(int signum)
{
    std::cout << "获取一个信号: " << signum << std::endl;
}

int main()
{
    for(int sig = 1; sig <= 31; sig++) signal(sig, catchSig);

    while(true) sleep(1);
    return 0;
}
  • ②若我们将2号信号block,并且不断地获取并打印当前进程的pending信号集,而后突然发送一个2号信号 ==> 预计:可看到pending信号集中,有个比特位0→1
    • 分析点①:默认情况下,恢复对于2号信号的block的时候,确会进行递达,但是2号信号的默认处理动作是终止进程,故需要对2号信号进行捕捉
    • 分析点②:打印解除block和捕捉的顺序 --> 仅是打印语句放前放后的问题
    • 我们可以同个sigpending获取pending 没有接口用于设置pending位图(所有的信号发送方式,都是修改pending位图的过程)
    • 分析点③(void)n;:此语句是为了消除编译器的警告的习惯,比如定义了一个变量后面没有使用,编译器会提醒没使用。而将变量n强制转换为没有返回值的表达式,可以抑制编译器对变量的警告信息。
      • 此仅是个无意义的语句,写不写都一样。
#include 
#include 
#include 
#include 

static void showPending(sigset_t& pending)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(&pending, sig))//在pending集合里
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << std::endl;
}

static void blockSig(int sig)
{
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, sig);
    int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
    assert(n == 0);
    (void)n;
}

int main(){
	//0~4步实现对2号进程进行阻塞
	// 0. 方便测试,捕捉2号信号,不要退出 -- 分析点①
	signal(2, handler);
	// 1. 定义信号集对象
	sigset_t bset, obset;
	sigset_t pending;
	// 2. 初始化
	sigemptyset(&bset);
	sigemptyset(&obset);
	sigemptyset(&pending);
	// 3. 添加要进行屏蔽的信号
	sigaddset(&bset, 2 /*SIGINT*/);
	// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]
	int n = sigprocmask(SIG_BLOCK, &bset, &obset);
	assert(n == 0);//在release下不可用assert
	(void)n;//分析点③
	
	std::cout << "block 2 号信号成功... , pid: " << getpid() << std::endl;
	// 5. 重复打印当前进程的pending信号集
	int count = 0;
	while (true)
	{
	    // 5.1 获取当前进程的pending信号集
	    sigpending(&pending);
	    // 5.2 显示pending信号集中的没有被递达的信号
	    showPending(pending);
	    sleep(1);
	    count++;
	    if (count == 20)
	    {
	        //分析点①
	        std::cout << "解除对于2号信号的block" << std::endl;//分析点②
	        int n = sigprocmask(SIG_SETMASK, &obset, nullptr);
	        assert(n == 0);
	        (void)n;
	    }
	}
	
	return 0;
}
  • ③ 若我们对所有的信号都block,是否就写了个不会被异常或者用户杀掉的进程?
    • 9号信号无法被屏蔽,也无法被阻塞
#include 
#include 
#include 
#include 

static void handler(int signum)
{
    std::cout << "捕捉 信号: " << signum << std::endl;
    // 不要终止进程,exit
}
static void showPending(sigset_t& pending)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(&pending, sig))
            std::cout << "1";
        else
            std::cout << "0";
    }
    std::cout << std::endl;
}

static void blockSig(int sig)
{
    sigset_t bset;
    sigemptyset(&bset);
    sigaddset(&bset, sig);
    int n = sigprocmask(SIG_BLOCK, &bset, nullptr);
    assert(n == 0);
    (void)n;
}

int main()
{
    for (int sig = 1; sig <= 31; sig++)
    {
        blockSig(sig);
    }
    sigset_t pending;
    while (true)
    {
        sigpending(&pending);
        showPending(pending);
        sleep(1);
    }
    return 0;
}

四、捕捉信号

  • 信号产生后,信号可能无法被立即处理,在合适的时候
    • ①什么时候是合适的
    • ②信号处理的整个流程

1. 合适的时候

  • 信号相关的数据字段都是在进程PCB内部,而PCB内部属于内核范畴

    • 普通用户没有权力作检测,故若想检测当前信号是否被屏蔽,一定要处于内核状态
    • 只执行自身代码时的状态,称作用户态
    • ==> 故,合适时候:在内核态中,从内核态返回用户态的时候,进行信号检测和处理
  • 为什么会进入内核态?

    • 当进行系统调用、缺陷、陷阱、异常等
  • 如何进入内核态?

    • 在汇编语言层面上,有个终端编号int(汇编指令,取“中断interrupt”的前三个字母) 80,将代码的执行权限由我下达给操作系统,让操作系统去执行
    • int 80:内置在系统调用函数中(可理解做系统调用函数内部就有int 80
用户态和内核态
  • 用户态是一个受管控的状态(如受访问权限的约束、资源限制)
  • 内核态是一个操作系统执行自己代码的一个状态,具备非常高的优先级
  • 回顾:进程地址空间[[3.Linux进程概念#五、程序地址空间]]
    • 【Linux】进程通信_第4张图片

    • 每个进程都有各自3~4G的进程地址空间,但所有进程都可看到同一个内核级页表,当需要执行系统调用的代码时,就会在进程地址空间中的内核地址空间找到对应系统调用的代码(过程如上图①)

      • [[3.Linux进程概念#地址空间]]
    • 动态库的代码也是加载到各进程的进程地址空间里,当想执行动态库时,由于调用库也属于函数,则其也是在地址空间里调用,只不过库的代码用的是用户级页表(过程如上图②)

    • 由①②可知:内核和进程切换的代码都是在所有进程的地址空间上下文中跑的

进程从硬件到软件的整体原理
  • 凭什么有权力执行OS的代码?==> 凭当前处于内核态,还是用户态。
    • CPU寄存器有2套:1套可见;1套CPU不可见(自用的)
      • CPU中有一个寄存器为CR3 --> 表示当前CPU的执行权限:1表内核态,3表用户态
    • 汇编指令int 80,将CPU中寄存器中3用户态改变至1内核态 ;而每个系统调用函数内部都存在int 80指令
  • 故,进程从硬件到软件的整体原理如下:
    • 当执行系统调用时(如调用open),它会先执行open中的int 80指令,将3用户态转换成1内核态,内核态允许访问,从而通过OS的权限审查,而后跳转到操作系统的内核地址空间,使用内核级页表,因此可以访问操作系统内的所有资源。
    • 当系统调用结束后,系统调用结尾会将CPU内状态寄存器CR3中由1内核态再转换成3用户态后,跳转回代码继续执行后续的内容。
总结:内核态和用户态切换
  • 为什么要从用户态切换到内核态?

    • 用户需要通过软硬件资源达到自己的目的,但是访问软硬件资源必须通过操作系统(不允许跨层访问,因为内核态中执行的代码是属于优先级更高的代码,更重要的事情需要先被处理),也就必须从用户态转变成内核态
  • 内核态与用户态的切换是如何做到的呢?

    • 通过系统调用接口,访问内核。==> 系统调用接口又是通过调度,调度背后的逻辑是通过硬件层面上的时钟中断,在极短(每隔几纳秒)时间内就触发硬件来督促操作系统自行调度
    • 具体的切换:CPU内存在寄存器,寄存器保存了当前的页表地址、当前进程PCB的地址,因此很快可以确认当前进程相关的信息,也就可以通过地址空间所表征的用户及内核的所有代码,直接访问即可。
      • [[7.进程通信#进程从硬件到软件的整体原理]]
  • 为什么要从内核态切换到用户态?

    • ①当前用户的代码仍未执行完(本进程未调度结束)
    • ②当前用户层仍有若干进程未被调度(其余进程未调度结束)
    • 计算机OS是为了给用户服务的,执行用户代码应为主,进入内核态仅是临时的状态,必须返回用户态继续执行用户的代码,从而给用户提供完整服务
  • 为什么是从内核态返回用户态时进行信号检测处理,而不是从用户态刚进内核态就处理呢?

    • 操作系统就是这么设计的。
    • “ 接收到一个信号时,当前可能在操作优先级别更高、更重要的事情。所以信号还会被先pending ”==> 如何体现在处理优先级更高的事呢? ==> 通过内核态返回用户态才进行信号检测处理体现。因为“从内核态返回用户态”时,说明已经把内核态所要做的事(优先级更高的事)处理完了,处理完优先级更高的事后才处理被pending的信号

2. 信号的捕捉

  • 能否因为我们不系统调用,从而实现无法陷入内核呢?
    • 不能,即便我们不系统调用,进程依旧会周期性地陷入内核
    • 就如OS发现时间片到了,就会触发进程调度,即在当前进程的上下文执行调度函数(由于Linux底层使用C语言写的,故调度本质就是函数)。而调度该函数、执行这个方法之前,也必须要陷入,因此不论进程愿意与否,都必须陷入
handler的三种处理
  • ①忽略:将pending清0(由1置0)即处理结束,而后无需返回用户态,并继续向后执行即可
  • ②默认:大部分都是终止(将当前进程进入终止逻辑,不调度该进程,将PCB对应页表都释放了,也不需要返回用户态,而是继续向后执行)
    • “终止不需要返回用户态”的话,若有的需求需要返回用户态执行呢?
      • 也有一些系统调用(如atexit),会用于在进程终止之前,帮你返回用户态,而后执行特定的方法
    • 对于进程终止,那核心转储该怎么办呢?
      • 核心转储,即将内存中当前进程的相关数据转移到磁盘上(即IO),而在内核态是能读取当前进程的相关数据的。
      • 因此可以在退出之前,OS自动地把当前进程的核心数据,以当前用户身份来down到磁盘里,并且把进程pid以pid的方式来命名,最后再退出
    • 可有时候也会遇到不退出的信号,是怎么回事呢?
      • 不退出的信号,比如“暂停”,对暂停的处理即,在内核态将当前进程的进程pcb状态由运行态改为T状态。==> 而后也不用恢复用户态,OS紧接着执行调度算法,将当前进程放入等待队列里,再重新选择进程去调度,因此进程就进入了暂停状态,就不回去了 ==> 直到下次由T状态变回R状态时,才能将该进程投入到运行队列里,重新调度
  • (handler处理中,在内核态里处理“忽略”和“默认”是容易理解、水到渠成的,“捕捉动作”相对不好理解)
  • ③捕捉:捕捉意味着曾经已经设置了对该信号的回调处理,即在用户层代码中已经设置过了一个函数,该函数就是处理此信号的动作,执行完该函数,即完成了捕捉。
    • 当在内核态转换成用户态之前,处理到一个执行handler捕捉方法的信号时,就要跳转到用户层,去执行对应的handler方法。
      • ①当前的状态? 是内核kernal的身份。
      • ②以内核态,是否有能力执行用户层自定义的方法?==> 有
        • 操作系统若是愿意,它是能以内核态的身份去访问3~4G进程地址空间的代码、访问用户级页表,都是有能力做到的。证明有能力做到的例子如下:
        • a. 数据类型sigset_t传参时,是需要将用户层数据拷贝到内核的,若内核无法访问用户,它是无法完成拷贝的;
        • b. 对于很多系统调用,是需要将操作系统内的数据获取到,才能让我们在磁盘中读取到(如进行IO读取,调用文件read、write接口,其中数据都是操作系统先从外设磁盘读到自己的缓冲区中,而后再把数据从OS的缓冲区中拷贝到我们用户自定义的缓冲区。)
      • ③OS有能力帮用户执行对应的handler方法,但它并不愿意、也并不会执行。
        • 原因:若用户层方法中存在非法操作(如rm、scp…),此时以内核态的身份去执行,就会导致问题 > 操作系统不相信任何人(OS不相信任何user,才能保证自己的安全性,从而才能为user提供更好的服务)。> OS不以内核身份执行用户层方法,正是由于用户层方法是用户写的,无法保证方法的安全性,故不能用内核态执行用户层代码
        • 操作系统不相信任何人的体现:
          • a. 任何人想用操作系统的功能,必须通过系统调用,这是受管控的
          • b. 操作系统不会帮任何用户,执行任何一个用户层代码
    • 故当处于内核态检测到信号要被捕捉时,从内核态返回用户层后,要执行用户层方法时,需要将当前状态从内核态转变成用户态,所有行为当前用户负责。
    • 当以用户态身份执行完代码,即完成捕捉动作后,没有系统调用接口,用户态是无法返回内核态的。因此要通过执行特殊的系统调用sigreturn,从而再次进入内核态。再在内核态中,完成将pending比特位由1置0、恢复函数的栈帧结构等,而后返回main函数,继续向后执行,至此完成完整的信号捕捉动作。
完整的信号处理过程
  • 关键“∞”图【Linux】进程通信_第5张图片

  • 1、信号捕捉调度变化的逻辑理解链

    • ①当前正在执行的用户代码出于一些原因导致陷入内核
    • ②在内核中执行完操作系统里的代码,准备返回用户态时做信号检测
      • 若当前有信号检测被判定没有被block,且handle是自定义的,就从内核态切换回用户态,执行用户对应的handler方法。
    • ③当把handle方法执行完毕后,再通过特定的系统调用重新陷入内核,在内核中做一些比如收尾性工作
    • ④而后恢复到曾经对应main函数,或者用户态仍继续向后执行
  • 2、信号捕捉身份转换的理解链

    • ①从用户态进入内核态(从上到下)
    • ②为了处理捕获动作,内核态返回用户态(从下到上)
    • ③用户态处理完捕获动作后,再恢复到内核,执行收尾工作(从上到下)
    • ④完成收尾后,再返回用户态继续执行后面的代码(从下到上)

3.信号的操作

sigaction函数
  • 检查和更改系统动作的一个系统调用 ==> 捕捉信号
    • sigaction函数在普通信号中能使用,在实时信号中也可使用
  • 参数介绍
    • int signo: 对应信号的编号
    • const struct sigaction *act: 一种结构体[[7.进程通信#sigaction 结构体介绍]],操作系统提供的数据类型.在C/C++中结构体类型名称和函数的名称允许是同样的(但并不建议这么处理).sigaction类型是一种输入型参数,此参数(sigaction类型的结构体)中包含你对该信号要怎么处理的回调函数
    • struct sigaction *oldact: 返回对信号的旧处理方法,属于输出型参数.
  • 返回值:成功返回0;失败返回-1及其错误码
#include   
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
sigaction结构体介绍
struct sigaction{
    void (*sa_handler)(int); //sa_handler方法即对应信号捕捉的回调函数
    void (*sa_sigaction)(int, siginfo_t *, void *);//×暂时不考虑
	sigset_t sa_mask; //√重点谈论
	int sa_flags; //×暂时不考虑
	void (*sa_restorer)(void);//×暂时不考虑
};
  • void (*sa_handler)(int);
    • sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号;赋值为常数SIG_DFL表示执行系统默认动作;赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,
    • 该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。
    • 显然,这也是个回调函数,并非被main函数调用,而是被系统所调用。
  • 处理信号执行自定义动作时,如果在处理信号期间,又来了同样的信号,OS如何处理? ( 本质: 为什么要有block/信号屏蔽字? ==> 为了支持操作系统内处理普通信号,防止进行递归式处理;而我们主要让其在自己周期内只会调用一层,不会出现过多的调用层次)
    • 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。
    • 这样就保证了在处理某个信号时,如果这种信号再次产生,那它会被阻塞到当前处理结束为止。

五、可重入函数

  • 信号捕捉,并没有创建新的进程或线程

  • 重入函数:一个函数在一个特定时间段内被多个执行流重复进入,此函数称为“重入函数”

    • 若重入并不导致函数出现问题,称之为“可重入函数”
    • 若重入导致函数出现问题,称之为“不可重入函数”
  • 可重入函数和不可重入函数:属于函数的一种特征,并无好坏之分。

  • 目前我们使用的90%函数,都是不可重入的,如果一个函数符合以下条件之一则是不可重入的:

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

六、volatile关键字

  • 我们无法假设别人的编译场景,编译器有时候会自动进行代码优化:

    • VS2019用户很难看出其中的优化;
    • 而gcc/g++有特定的优化级别,其有明显的优化选项可供设置。一般默认使用的是-O1-O3是优化级别最高的。
    • 编译器的优化是在编译阶段就优化好了
  • volatile关键字作用演示

    • 结果说明:即使signal将flag从0置1,但程序依旧无法退出
    • 原因说明:在下例中flag属于全局变量,内存中存储变量flag值(默认情况为0)。
      • 当代码正常执行时:CPU会从内存中读取flag数据(最初为0)到cpu寄存器(CPU中有很多寄存器,此处以edx为例)中,而后对其进行检测(即判断!flag
      • 编译器优化情况:由于在main函数中仅有对flag做检测的语句(!flag),并无修改flag的语句。 ==> 于是在编译器优化里(-O3优化),编译器第一次将flag的默认值0放入edx后,再做while循环检测时,它自作主张仅在cpu里重复对第一次读取到的flag做检测。 > 可当信号来时(signal(2,changeFlag);),即使修改了内存中flag=1,cpu也并不再访问内存里flag,依旧检测的是首次读到的flag值,导致了数组二异性。> 故优化的存在,让cpu无法看到内存了
    • 对问题的改进:对代码中可能被优化的字段(如下例中的volatile int flag =0;),就需要程序员显性地告诉编译器不要给它优化(如对于下例中的while(!flag);,必须去内存中访问 ==> 保持内存的可见性
int flag =0;//volatile int flag =0;
void changeFlag(int signum)
{
	(void)signum;
	cout << "change flag:" << flag;
	flag = 1;
	cout<<"->"<
  • volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量(如上例中的:volatile int flag =0;),不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

七、SIGCHLD信号(选学了解)

  • 回顾:[[4.Linux进程控制#1、进程等待的必要性 功能]]子进程在退出后,父进程需要对其wait,否则会造成以下两点问题

    • ① 产生僵尸进程,进而导致内存泄漏
    • ② 父进程无法得知子进程的退出结果,即无法拿到进程的退出码、退出信号
  • 而父进程处理子进程结束有两种方式:

    • 阻塞:[[7.进程通信#1 父进程 阻塞 等待子进程结束]]
    • 非阻塞:[[7.进程通信#2 父进程 非阻塞 查询是否有子进程结束等待清理 → 即轮询]]

1.父进程阻塞等待子进程结束

  • 回顾:[[4.Linux进程控制#2、 进程等待的方法]]
  • 阻塞处理的不足:子进程若识别作未结束的状态,就会导致父进程阻塞等待子进程,就不能处理父进程自己的工作了。

2. 父进程非阻塞查询是否有子进程结束等待清理 → 即轮询

  • 轮询过程:子进程在退出或暂停时,会主动地通过OS,向父进程发送17号SIGCHLD信号。

    • 既然子进程在退出是主动发送了信号,为什么还会导致僵尸进程的存在(即为什么父进程还是没有释放处理它)呢?
      • 父进程没有处理子进程退出信号,是由于17号SIGCHLD信号默认动作是忽略(应用层忽略是什么都不做;在内核中,OS可能还会对忽略再做些工作)。
      • 区分应用层和OS级的“忽略”:[[7.进程通信#5、应用层和系统默认的“忽略”]]
    • 只有Linux采用了 “子进程在退出或暂停时,会主动地通过OS,向父进程发送17号SIGCHLD信号” 的方案
  • 非阻塞处理的不足及优势

    • 不足:父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
    • 优势:检测到退出子进程就回收,检测到没退出就下一个,不会被阻塞卡住
  • 回收退出子进程的新思路:既然Linux采用了发送SIGCHLD信号的方式通知父进程,父进程就不必主动去等待子进程了,而是当子进程退出时,通过设置捕捉信号,在捕捉方法里对信号做回收,就能完成一套基于信号版的子进程回收

3. 验证子进程向父进程发送SIGCHLD信号

//证明子进程退出,会向父进程发送17号SIGCHLD信号
void handler(int signum)//父进程执行信号捕捉handler
{
	cout <<"子进程退出: "<< signum << endl;
	cout << "father pid: " << getpid() << endl;//若输出17号新号编码,则证明确实子进程退出发出了17号SIGCHLD信号

	//父进程进行信号捕捉,故可在此回收退出的子进程
	wait();//若有10个子进程都退出,则采用while(wait())
}

int main()
{ 
	signal(SIGCHLD, handler);//捕捉子进程退出发出的SIGCHLD信号
	if(fork() == 0)//子进程退出
	{
		cout<< "child pid: " << getpid()< 取决于是否需要父进程退出 ==> 取决后面的代码需求
	……
}

//最后输出
// child pid: 27251
// 子进程退出: 17
// father pid: 27250

4. 信号版的子进程回收代码

为什么要遍历所有子进程?
  • 子进程退出,父进程是会接收到子进程退出发送的SIGCHLD信号,但:

    • ①父进程无法得知是哪个子进程退出;
    • ②父进程无法得知所有进程里共有多少个子进程退出
    • ==>故必须采用while去遍历所有的子进程
  • 若一共10个进程,其中有5个子进程退出,当while循环出前5个都退出发送了SIGCHLD信号,那还要继续while循环第6个子进程吗?

    • ①父进程无法站在上帝视角,得知共有几个子进程退出
      • 同时向父进程发送多个SIGCHLD信号,仅会接收到1个,其余可认为均丢失,因为:
        • a. SIGCHLD信号的bite位仅有1个,它并没有传达信号数量的信息。
        • b. pending位图里只有1个比特位表示收到某一个信号,故即使同一个时刻收到了多个SIGCHLD信号,实际上只收到了1个信号(操作系统只会保留一个),而剩下的4个信号可认为已丢失了
        • ▲但注意这是普通信号的特征,而对于实时信号是会把所有信号保留起来的
      • 故作为信号的处理者,即使此时收到了1个SIGCHLD信号,但其实并不能确定有几个进程退出,所以只能用while循环不断去读取。
    • ②while循环的功能:回收+检测
      • 对于第6个进程仍需要继续检测,并非为了回收,而是检测
        • waitpid和wait本身除了回收,还可以检测子进程是否退出。==> 因为我们无法确定进程退出的数量是5个还是6个,故仍需通过while循环继续检测
      • 若6号进程检测到并没有退出,则此时的信号处理就会被阻塞在那,阻塞式地等待6号进程,导致主进程(相当于main执行流)的事就无法执行了==> 因此,考虑非阻塞遍历子进程
如何非阻塞遍历所有子进程?
  • 方式①while循环+vector pids数组:在fork时,采用vector pids数组记录所有子进程pid,而后去非阻塞遍历所有进程,再在对应代码里对其进行捕捉检测,从而回收退出的子进程
  • 方式②while( (id = waitpid(-1, NULL, WNOHANG)):waitpid中传入-1,效果等价于wait。作用是:我们可以等待任何一个退出的进程,不关心是哪个,但若是-1,操作系统就会去识别哪个进程退出了;若没有任何一个进程退出,调用-1也会导致阻塞

5. 应用层和系统默认的“忽略”

  • 代码最终实现的目的:父进程又不想等子进程(即不关心子进程退出码,不在意其退出是否异常或其他),并且还想让子进程退出之后,系统直接自动释放、回收僵尸子进程
  • 分析点①:应用层和系统默认的“忽略”程度是不同的
    • 【Linux】进程通信_第6张图片

    • 对于应用层中手动设置的“忽略”:属于用户明确告知内核了是直接忽略,并且遇到僵尸进程直接释放掉

    • 操作系统级别的“忽略”:属于默认的动作,什么也不做,进程该僵尸就僵尸(操作系统拿不准是否要回收)

#include
#include
#include
using namespace std;

int main()
{
	//手动设置对子进程进行忽略(应用层的忽略)
	signal(SIGCHLD,SIG_IGN);//分析点①

	if(fork()==0)
	{
		cout<<"child: "<

你可能感兴趣的:(Linux学习,linux,服务器,运维)