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

【Linux】-- 进程信号(处理、内核)

上篇:【Linux】-- 进程信号(认识、应用)_川入的博客-CSDN博客


目录

信号其他相关常见概念

pending

handler

block

信号处理的过程

sigset_t

sigset_t使用

系统接口

sigpending

sigprocmask

捕捉方法

sigaction

struct sigactio

sa_mask

补充

可重入函数

volatile

SIGCHLD信号


信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
    • 信号递达:可能是默认、可能是忽略、可能是自定义捕捉。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
    • 信号产生,进程不是立即处理这个信号,不代表其不会处理。意味着未来会处理,于是从收到信号到未来准备处理时:信号存在但是没有被处理 —— 信号被临时保存(PCB的位图中),此时就被称为信号未决 —— Pending位图。

融汇贯通的理解:

        所以,前面所提的临时存储概念是不准确的,应该称作为信号未决(Pending)

  • 进程可以选择阻塞 (Block)某个信号。
    • 进程是可以将处于未决的信号就是不递达其,屏蔽某些信号 —— 阻塞 (Block)。

忽略和阻塞的区别:

        忽略:已经递达了,已经处理该信号了,只不过处理动作是忽略。

        阻塞:根本就不会进行抵达,不进入信号处理流程。(阻塞即永远不递达)

  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

        为了支持递达、未决、阻塞三个概念,其在内核中有对应的表现 —— 内核当中有对应的三张表

pending

        pending就是之前所提的位图操作系统就是修改pending位图中指定的位置,来完成信号的发送过程。

handler

        当收到了一个信号,操作系统会在pending中修改对应的位图。处理信号的时候就会根据为1的信号,拿着信号去handler数值中索引。对应调用handler中的函数指针的方法,去完成信号捕捉就可以了。

        所以,signal是两个参数的原因也在此,是根据第一个元素signum找到handler数组对应的位置,将第二个参数handler作为数据存入。

但是需要注意,进程对于信号的处理方式有三种

  • 默认(进程自带的,程序员写好的逻辑)
  • 忽略(也是信号的一种处理方式)
  • 自定义动作(捕捉信号)

        所以,此处的handler中并不只是signal函数这么简单。

(路径:/usr/include/bits/signum.h

block

        block位图,结构和pending一摸一样。位图中的内容,代表的含义是对应的信号是否被阻塞。

信号处理的过程

信号处理的大致流程图可以画为:

1. 向操作系统向pending中发送信号。

2. 处理信号,遍历pending找到为1的信号。

3. 找到之后,去对应的block中查看是否为1。block为1,该信号永远不递达。block为0,该信号合适的时候递达。

4. block为0且需要抵达,根据handler数组中的数据处理信号。

sigset_t

        为了支持我们更好的编程,信号在内核当中是一个位图,不可能让我们直接操作其,也不可能让我们操作。操作系统为我们提供了一个类型sigset_t

        因为,操作系统提供的类型,需要与操作系统提供的.h文件相对应,也就是与系统调用接口相对应。因为有的接口不允许用户传语言层的参数,需要传一个结构体、一个位图等。于是操作系统必须提供对应的类型。

融汇贯通的理解:

        其实语言级的.h、.hpp也一样,因为只要涉及硬件级别的操作,就必须通过操作系统,那么就需要使用操作系统提供的.h以及操作系统提供的类型 —— 语言级的.h、.hpp一定对操作系统提供的.h与类型有着包含。最典型的就是文件操作。

# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct{unsigned long int __val[_SIGSET_NWORDS];} __sigset_t;#endif

        sigset_t是位图结构,操作系统提供的类型。

        每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来表示,sigset_t 称为信号集,这个类型可以表示每个信号的 " 有效 " 或 " 无效 " 状态,在阻塞信号集中 " 有效 " 和 " 无效 " 的含义是该信号是否被阻塞,而在未决信号集中 " 有效 " 和 " 无效 " 的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的 " 屏蔽 " 应该理解为阻塞而不是忽略。

sigset_t使用

1. sigset_t —— 不允许用户自己进行位操作 —— 操作系统给我们提供了对应的操作位图的方法。

#include <signal.h>
系统接口意义返回值
int sigemptyset(sigset_t *set);
初始化set所指向的信号集,所有信号位清0,表示该信号集不包含任何有效信号。

成功时返回0

错误时返回-1

int sigfillset(sigset_t *set);
初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
int sigaddset (sigset_t *set, int signo);
向set所指向的信号位,添加某种有效信号。
int sigdelset(sigset_t *set, int signo);
向set所指向的信号位,删除某种有效信号。
int sigismember(const sigset_t *set, int signo);

判断在set所指向的信号集中是否包含某种信号.

包含返回1

不包含返回0

错误返回-1

2.  sigset_t —— user是可以直接使用该类型 —— 和内置类型 && 自定义类型没有任何差别。

3.  sigset_t —— 一定需要对应的系统接口,来完成对应的功能,其中系统接口需要的参数,可能就包含了sigset_t定义的变量或者对象。

系统接口

sigpending

#include <signal.h>

-

int sigpending(sigset_t *set);

获取pending位图。

-

参数:

        set:类型为sigset_t的位图。

返回值:(如果发生错误,将设置errno以指示原因)

  • 成功时返回0。
  • 错误时返回-1。

为什么pending只有获取,为什么没有设置?因为其实前面讲的信号产生就是在设置。

sigprocmask

#include <signal.h>

-

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-

参数:

        how:参数更改信号屏蔽字。

        set:类型为sigset_t的位图。

        oldset:将原来的信号屏蔽字备份到oldset里。

返回值:(如果发生错误,将设置errno以指示原因)

  • 成功时返回0。
  • 错误时返回-1。

how参数(宏):下列宏没有任何交集,不自持按位|、按位&。

选项含义
SIG_BLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask|~set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask|~set
SIG_SETMASK    设置当前信号屏蔽字为set所指向的值,相当于mask=set

#问:如果我们对所有的信号都进行了自定义捕捉,是不是就写了一个不会被异常或者用户杀掉的进程?

        不是,也不可能,操作系统的设计者也考虑到了。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>static void handler(int signum)
{std::cout << "捕捉 信号: " << signum << std::endl;// 不要终止进程,exit
}int main()
{// 将block全部设置for(int sig = 1; sig <= 31; sig++){signal(sig, handler);}return 0;
}

        9号信号与19号信号属于管理员信号,我们是无法设定自定义捕捉动作的。为的就是防止我们将所有的31个信号全部捕捉。

9号信号:

19号信号:

#问:如果我们将对所有的信号都进行了block,是不是就写了一个不会被异常或者用户杀掉的进程?

        不是,也不可能,操作系统的设计者也考虑到了。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cassert>// 打印pending
static void showPending(sigset_t &pending)
{for (int sig = 1; sig <= 31; sig++){if (sigismember(&pending, sig))std::cout << "1";elsestd::cout << "0";}std::cout << std::endl;
}// 设置block
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()
{// 将block全部设置for(int sig = 1; sig <= 31; sig++){blockSig(sig);}// 循环打印pendingsigset_t pending;while(true){sigpending(&pending);showPending(pending);sleep(1);}return 0;
}

        9号信号与19号信号属于管理员信号,我们是无法设定block。为的就是防止我们将所有的31个信号全部block。

 19号信号:

Note:

        9号信号与19号信号,永远不会被阻塞、永远不会被捕捉。

捕捉方法

1. 信号在合适的时候处理(什么时候?)

       合适的时候:内核态返回到用户态的时候处理。

#问:什么叫做用户态?什么叫做内核态?

        我们调用某些系统调用(或者是时间片到了、或者是我们主导的调用了陷入内核的汇编指令)。以此,进入操作系统,在操作系统内,执行操作系统的代码。其中执行操作系统底层的代码的状态,就称作为内核态 —— 同理:执行用户层代码时称作为用户态。 

  • 用户态:是一个受管控的状态。
  • 内核态:是一个操作系统执行自己代码的一个状态,具备非常高的优先级。

#问:从内核态返回到用户态,即必须要进入过内核态。为什么要进入内核态?如何进入的内核态?

        在操作系统或者是在硬件CPU上执行代码的时候,执行的一大批代码都在内存之中保存着(二进制)。CPU执行的时候区分是用户的代码还是内核的代码,即执行用户的代码就是用户态,执行内核的代码就是内核态。

内核范畴:

  • 相关数据在操作系统内部,普通用户没有权利去进行检测。

内核状态:系统调用接口。

用户状态:hello world。

我们大部分执行的是我们的代码,所以我们是用户态。

进入内核态最典型的方式:

  • 进行系统调用。
  • 而有时候缺陷、陷阱、异常等,也可能进入内核态。

#问:我们怎么进入内核态和用户态?(我们不用担心)

        在汇编语言中,有一个中断编号80,有一个汇编指令int。以int 80可以让我们陷入内核,即:代码的执行权限,由我下达给操作系统,让操作系统去执行。

(int 80内置在系统调用函数中,我们不用管)

        用户态是一个受管控的状态:即不管是哪一个用户,都叫做普通用户。其启动的进程或者是任务,都是需要以用户态来运行的。受管控:受访问权限的约束、资源限制等如果,基本不受任何资源的约束,也就是不受权限的管控。

页表分为:

  • 用户级页表:用于映射用户所写的代码和数据所对应的物理内存位于用户空间:0 ~ 3G
  • 内核级页表:用于映射操作系统的代码和数据所对应的物理内存(位于内核空间:3 ~ 4G )

        内核级页表不同于用户级页表。对于用户级页表而言,因为不同进程的代码和数据是不相同的,所以需要每一个进程有属于自己的用户级页表(进程的独立性的体现)对于内核级页表而言,因为所有的进程都是在一个操作系统上跑的(只有一份操作系统),所以内核级页表只有一份

        当我们的代码中有类似open的系统调用时,根本不用担心,因为操作系统也在进程地址空间里。所以,无非就是跳转到open对应的内核地址空间里。

        即:操作系统的代码可以找到,因为整个操作系统被映射进了所有进程的3~4G中。所以,所有进程想调系统调用,只是在自己的地址空间上,通过函数调用跳转到系统里就可以了。


融会贯通的理解:

#问:进程切换的过程是什么?

        操作系统内有一个switch process对应的函数,然后当我们对应的进程时间片到了,操作系统底层硬件给我们发时钟中断。操作系统就直接,在CPU找当执行的进程,然后通过其的地址空间,找到对应的进程切换的函数,因为在当前进程中切换,所以可以将CPU内所有的临时数据压到PCB当中,即切换成功。(下面一个进程,运用其进程地址空间中3~4G,与内核级页表,找到恢复上下文的代码和数据。)


        时钟中断执行的频率很高:100次/秒,时钟中断的主要工作是处理和时间有关的所有信息、决定是否执行调度程序以及处理下半部分。 和时间有关的所有信息包括系统时间、进程的时间片、延时、使用CPU的时间、各种定时器,进程更新后的时间片为进程调度提供依据,然后在时钟中断返回时决定是否要执行调度程序。

#问:我们凭什么有权利执行操作系统的代码?

        凭的是我们是处于内核态还是用户态。因为CPU里面的寄存器一般分为两类:一套可见(程序员可用),一套不可见(权限、控制等,CPU自己用)。其中有一个寄存器,叫做CR3寄存器 —— 表示当前CPU的执行权限。

        只有内核态才可以访问操作系统里,结合前面所提:其实执行open的时候,就会执行其本身自带的指令int 80,该指令第一件事就会将CR3寄存器由用户态变为内核态,于是后面权限检查发现是内核态,于是就可以访问操作系统(使用内核级页表),于是跳转到操作系统执行open的代码。

#问:为什么从用户态 -> 内核态?

        因为有时候有一些功能,大部分情况下无法在用户态无法去执行的。因为操作系统是软硬件资源的管理者,换句话说,就是任何普通用户无法(不能、不可以)绕过操作系统去访问对应的软硬件资源。

  • 用户需要通过访问软硬件资源,达到自身的目的。

        用户需要,操作系统不允许,所以便有了:先变成内核,然后用户通过其访问 —— 系统调用接口

#问:怎么从用户态 -> 内核态?

        目前最常见就是,系统调用接口。通过特定的接口陷入内核。

#问:为什么从内核态 -> 用户态?

  1. 当前用户的代码还没有执行完
  2. 当前用户层还有若干个进程没有被调度完。

        计算机当中,操作系统是为了给用户提供服务的,所以执行用户的代码为主执行操作系统的代码为辅,以此协同完成。所以是必定要从内核态 -> 用户态,不反回用户态就无法给用户提供完整的服务 —— 可以说:进入内核态是一种临时的状态。

#问:CPU如何知晓的当前其执行的代码是用户的还是内核的?

硬件上:

        CPU寄存器分为两类:一套可见,一套不可见。执行内核代码中,其第一件事就是自行自带的指令int 80,该指令第一件事就会将CR3寄存器由用户态变为内核态,于是后面权限检查发现是内核态。

软件上:

        除用户级页表(将虚拟的用户地址空间进行物理内存的实质化,同时每一个进程由独立的用户级页表,用以保证进程的独立性)之外,还有内核级页表,用于将进程地址空间中的内核地址空间映射到物理内存中存储的操作系统(开机启动电脑,无非就是将操作系统加载到物理内存,用以运行)。以此达到进程对于操作系统的代码和数据的访问,由于操作系统只有一份(只需要一份:Windows、Linux)。所以,所有进程只需要看见同一份资源,即同一份内核级页表即可。

融会贯通的理解:

        所有进程都在一个操作系统下跑 —— 必须看见同一份操作系统 —— 同一份内核级页表。

        CPU执行代码的时候都是进程的代码进程,而如何切换软硬件体系都可以找到操作系统,并且所有的操作系统执行都是在地址空间中 —— 执行系统调用 —— 在当前进程地址空间内执行跳转 —— 与动态库类似(区别:更改实行级别、状态、权限等)

2. 信号处理的整个流程:

        内核态处理默认、忽略,是水到渠成的事情。

融会贯通的理解:

        进程由内核态 -> 用户态的时,遍历检查pending中处于1的信号,然后其block中为0,即进行handler中的递达。

  • 忽略:即pengding中的1置0,而后直接返回上层的用户代码处继续执行。
  • 默认:大部分是终止,直接将当前进程进入终止逻辑(不调度该进程了),将进程对应的PCB、地址空间、页表释放,无需返回用户态继续运行。
    • 进程终止_exit():就是直接终止,说白了就是这种终止无需返回到用户态。
    • 进程终止exit():上层用户层由刷新行为(特殊处理),操作系统需要返回到用户态,将缓冲区数据进行刷新,然后再进行终止。
      • 终止了如何还能执行用户层代码?—— 操作系统提供有一些相关接口。(进程终止之前,帮我们返回到用户态,执行特定的方法)
    • 进程暂停:当前进程在内核态,将进程PCB的状态由运行态改为T状态,然后无需返回用户态,直接执行调度算法,将进程放入等待队列里。然后从新选择进程调度。

        默认和忽略都是处于内核态。

捕捉动作是最难的,下图以捕捉动作为例:

#问:执行我们所写的捕捉动作时,处于什么状态?

        此时,处于信号检测,信号处理 -> 所以是处于内核态的。

#问:当前的状态,能不能执行user handler方法的?

        能执行,但是操作系统不想执行。因为不要认为操作系统内核态不能访问用户层的代码、数据。只要操作系统愿意,想访问就访问。

融汇贯通的理解:

以文件操作中为例:

        用户通过read读取文件中的内容,而文件中的内容就是属于用户层的数据,但是read是系统接口,是属于内核层

        所以:我们获取文件内容的方式,就是通过我们自己写的用户级缓冲区buffer,获取数据,而缓冲区的数据就是操作系统拷贝进的

        文件数据读取,操作系统愿意做,因为数据只是拷贝没有什么问题,但是如果以操作系统内核态的身份,如果user handler有非法的操作,那就完了 —> 操作系统是不会相信普通用户任何人的 —> 不能用内核态执行用户的代码。

        进行user handler方法时,从内核态切回用户态 —— 以用户态的身份执行我们所写的方法,如此,所有行为我们自己负责。

        在完成之后,在用户的层面上,没有能力跳转回执行内核层的代码,然后继续向后执行。

        因为递达完成,需要将pending置0,此操作需要以内核态身份执行。并且当时在哪被中断进入内核,这个位置只有操作系统知道。

图像简易化

  • 1 -> 2 -> 3 -> 4 -> 5
  • 1次的信号检测
  • 4次的状态的切换

sigaction

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

检查并更改信号动作。

参数:

        signum:信号的编号

        act:输入型参数

  • 一个结构体,至少包含对于信号的处理 —— 回调函数

        oldact:输出型参数

  • 一个结构体,曾经对于这个信号的老的处理方法。

返回值:

  • 成功时,返回0
  • 出现错误时,返回-1,并设置errno(错误码)以指示错误。

struct sigactio

        结构体,操作系统提供的数据类型。(结构体名称能与函数相同 —— 不建议)

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 <iostream>
#include <signal.h>
#include <unistd.h>void handler(int signum)
{std::cout << "获取了一个信号: " << signum << std::endl;
}int main()
{std::cout << "getpid: " << getpid() << std::endl; // 内核数据类型,用户栈定义的struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask); // 清空原理后面讲解act.sa_handler = handler;// 设置进当前调用进程的pcb中sigaction(2, &act, &oact);std::cout << "default action : " << oact.sa_handler << std::endl;while(true) sleep(1);return 0;
}

        Linux的设计方案:在任意时刻,只能处理一层信号 —— 不允许信号正在被处理时又来信号需要处理 —— 信号什么时候来挡不住,但是可以挡得住信号什么时候被处理。

sa_mask

        当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

#include <iostream>
#include <signal.h>
#include <unistd.h>void handler(int signum)
{std::cout << "获取了一个信号: " << signum << std::endl;sleep(10);
}int main()
{std::cout << "getpid: " << getpid() << std::endl; // 内核数据类型,用户栈定义的struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);  // 保证初始状态下,信号屏蔽字全为0act.sa_handler = handler;// 设置进当前调用进程的pcb中sigaction(2, &act, &oact);std::cout << "default action : " << oact.sa_handler << std::endl;while(true) sleep(1);return 0;
}

         实验在信号处理期间,会屏蔽该信号。

#include <iostream>
#include <signal.h>
#include <unistd.h>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;
}void handler(int signum)
{std::cout << "获取了一个信号: " << signum << std::endl;sigset_t pending;int c = 7;while(true){sigpending(&pending);showPending(&pending);c--;if(!c) break;sleep(1);}
}int main()
{std::cout << "getpid: " << getpid() << std::endl; // 内核数据类型,用户栈定义的struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);  // 保证初始状态下,信号屏蔽字全为0act.sa_handler = handler;// 设置进当前调用进程的pcb中sigaction(2, &act, &oact);std::cout << "default action : " << oact.sa_handler << std::endl;while(true) sleep(1);return 0;
}

        这也就是为什么会有block或者信号屏蔽字,这样的字段。也就是为了支持操作系统内处理普通信号,防止其进行递归式的处理。让其在自己的处理周期内只会调用一层,不出现太多的调用层次。

在处理2号信号的期间,捎带的屏蔽一下信号:3、4、5、6

#include <iostream>
#include <signal.h>
#include <unistd.h>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;
}void handler(int signum)
{std::cout << "获取了一个信号: " << signum << std::endl;sigset_t pending;int c = 20;while(true){sigpending(&pending);showPending(&pending);c--;if(!c) break;sleep(1);}
}int main()
{std::cout << "getpid: " << getpid() << std::endl; // 内核数据类型,用户栈定义的struct sigaction act, oact;act.sa_flags = 0;sigemptyset(&act.sa_mask);  // 保证初始状态下,信号屏蔽字全为0act.sa_handler = handler;// 在处理2号信号的期间,捎带的屏蔽一下信号:3、4、5、6sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);sigaddset(&act.sa_mask, 6);// 设置进当前调用进程的pcb中sigaction(2, &act, &oact);std::cout << "default action : " << oact.sa_handler << std::endl;while(true) sleep(1);return 0;
}

        因为,我们只是将2号信号设置为自定义捕捉,其他信号是默认。所以在执行完2号信号的自定义捕捉后,处于被block状态的信号,才会被递达。(此处最后递达的是:4号信号)

Note:

        信号捕捉,并没有创建新的进程或者线程。信号的处理整个流程都是单进程的,就是这一个进程处理信号时,处于此进程的上下文中处理。

补充

可重入函数

        main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。于是便出现了经典的内存泄漏问题

        排查代码的时候,可以发现,我们的代码写的没有任何问题。对应的main函数、单链表头插insert、信号捕捉sighandler、函数调用都没有问题 —— 这个问题的产生严格来说并不是代码问题,而是因为操作系统调度导致的进程时序的变化 —— 时序问题。

        此问题存在且,非常不容易排查。

        像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

可重入函数 VS 不可重入函数

        是函数的一种特征,目前我们用的90%函数,都是不可重入的。

  • 不可重入函数:好编写。
  • 可重入函数:不好编写,书写成本高。

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

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

可重入函数:需要保证函数其是独立的,没有访问任何的全局数据。

volatile

        编译做优化的时候,自作聪明,它一看flag是一个全局的数据,发现在main函数里没有任何一个语句是改flag的。其认为每一次检测flag都需要访问内存,并将数据拷贝进CPU的寄存器中,于是自作聪明,将第一次的flag的值一直放在edx中,后面的检测就不去内存拿了,继续看edx中的数据,就是0,于是一直检测不过。

        所以为了解决这个问题,一些可能被优化的字段,我们显性的告诉编译器,不要这么优化。

SIGCHLD信号

(只有Linux采用了这样的方案)

        在进程等待中,用 wait 和 waitpid 函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
        本质上也就是:子进程通过操作系统来给父进程写入信号,逻辑同文上。
  • 验证子进程在终止时会给父进程发SIGCHLD信号:
#include <cstdio>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>void handler(int sig)
{printf("子进程退出:%d\n", sig);
}// 证明:子进程退出,会向父进程发送信号
int main()
{signal(SIGCHLD, handler);pid_t cid;if ((cid = fork()) == 0){ // childsleep(1);exit(0);}while(true) sleep(1);return 0;
}

  • 父进程在信号处理函数中调用 wait / waitpid 清理子进程:

Note:

        我们需要注意,如果我们有10个子进程,而如果其中5个子进程在一个时刻退出。由于普通信号只会有与没有,没有个数的表达,而对于第6个子进程,也是需要进行判断是否退出的(我们知道5个子进程退出是我们站在上帝的视角)。此时我们使用常规的 wait / waitpid 去使用,信号处理就会阻塞在那里。


解决方法:

  • 使用全局变量记录所有子进程的pid,用以遍历的非阻塞等待。
  • 使用waitpid第一个参数为-1(等待任意一个子进程),并非阻塞等待。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.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);pid_t cid;if ((cid = fork()) == 0){// childprintf("child : %d\n", getpid());sleep(3);exit(1);}//fatherwhile (1){printf("father proc is doing some thing!\n");sleep(1);}return 0;
}
  • 父进程不关心子进程的任何推出信息:
#include <iostream>
#include <unistd.h>
#include <signal.h>// 如果我们不想等待子进程,并且我们还想让子进程退出之后,自动释放僵尸子进程
int main()
{// OS 默认就是忽略的signal(SIGCHLD, SIG_IGN); // 手动设置对子进程进行忽略if(fork() == 0){std::cout << "child: " << getpid() << std::endl;sleep(5);exit(0);}while(true){std::cout << "parent: " << getpid() << " 执行我自己的任务!" << std::endl;sleep(1);}
}

#问:操作系统默认就是忽略的,在此处我们还自己写一个忽略?

        也看得出来差别是很大的。忽略的概念是不一样的,可以理解为对信号的处理有第四方案:操作系统级别的忽略。可以理解为我们的忽略为1,操作系统的忽略为2,是不同级别的忽略。

相关文章:

【Linux】-- 进程信号(处理、内核)

上篇&#xff1a;【Linux】-- 进程信号&#xff08;认识、应用&#xff09;_川入的博客-CSDN博客 目录 信号其他相关常见概念 pending handler block 信号处理的过程 sigset_t sigset_t使用 系统接口 sigpending sigprocmask 捕捉方法 sigaction struct sigactio …...

C/【静态通讯录】

&#x1f331;博客主页&#xff1a;大寄一场. &#x1f331;系列专栏&#xff1a;C语言学习笔记 &#x1f618;博客制作不易欢迎各位&#x1f44d;点赞⭐收藏➕关注 前言 往期回顾&#xff1a; C/扫雷 C/N子棋 通讯录作为通讯录地址的书本&#xff0c;当今的通讯录可以涵盖多项…...

万卷书 - 让孩子对自己负责 [The Self-Driven Child]

让孩子对自己负责 The Self-Driven Child - 让你的孩子更加科学合理的掌控自己的生活 简介 《The Self-Driven Child》(2018)解释了我们对孩子的习惯性控制欲,它导致了孩子压力过大、难以合作,以及主观能动性差。本书不提倡这种做法,而是认为我们应该帮助孩子自己做出合适…...

Postman中cookie的操作

在接口测试中&#xff0c;某些接口的调用&#xff0c;需要带入已有Cookie&#xff0c;比如有些接口需要登陆后才能访问。 Postman接口请求使用Cookie有如下两种方式&#xff1a; 1、直接在头域中添加Cookie头域&#xff0c;适用于已经知道请求所用Cookie数据的情况。 2、使用…...

torch.grid_sample

参考&#xff1a; 双线性插值的理论Pytorch grid_sample解析PyTorch中grid_sample的使用方法pytorch中的grid_sample()使用 查阅官方文档&#xff0c;TORCH.NN.FUNCTIONAL.GRID_SAMPLE grid_sample的函数签名如下所示&#xff0c;torch.nn.functional.grid_sample(input, gr…...

前端基于 Docker 的 SSR 持续开发集成环境实践

项目收益 整体开发效率提升20%。加快首屏渲染速度&#xff0c;减少白屏时间&#xff0c;弱网环境下页面打开速度提升40%。 权衡 在选择使用SSR之前&#xff0c;需要考虑以下事项&#xff01; SSR需要可以运行Node.js的服务器&#xff0c;学习成本相对较高。对于服务器而言&a…...

ARM交叉编译入门及交叉编译第三方库常见问题解析

1. 交叉编译是什么&#xff1f; 交叉编译简单说来&#xff0c;就是编译成果物的地儿不是你运行这个成果物的地儿。最常见的场景&#xff0c;就是我们要编译一个 ARM版本 的可执行程序&#xff0c;但我们编译这个 ARM版本 可执行程序的地方&#xff0c;是在一个 x86_x64 的平台…...

Ruby Web Service 应用 - SOAP4R

什么是 SOAP&#xff1f; 简单对象访问协议(SOAP,全写为Simple Object Access Protocol)是交换数据的一种协议规范。 SOAP 是一种简单的基于 XML 的协议&#xff0c;它使应用程序通过 HTTP 来交换信息。 简单对象访问协议是交换数据的一种协议规范&#xff0c;是一种轻量的、…...

HashMap底层实现原理概述

原文https://blog.csdn.net/fedorafrog/article/details/115478407 hashMap结构 常见问题 在理解了HashMap的整体架构的基础上&#xff0c;我们可以试着回答一下下面的几个问题&#xff0c;如果对其中的某几个问题还有疑惑&#xff0c;那就说明我们还需要深入代码&#xff0c…...

Linux驱动学习环境搭建

背景常识 一、程序分类 程序按其运行环境分为&#xff1a; 1. 裸机程序&#xff1a;直接运行在对应硬件上的程序 2. 应用程序&#xff1a;只能运行在对应操作系统上的程序 二、计算机系统的层次结构 所有智能设备其实都是计算机&#xff0c;机顶盒、路由器、冰箱、洗衣机、汽…...

Java基础之异常

目录1 异常1.1 异常的概述1.2 常见异常类型1.3 JVM的默认处理方案1.4 编译时异常的处理方式1.4.1 异常处理之 try ... catch ... [ktʃ]&#xff08;捕获异常&#xff09;1.4.2 异常处理之 throws&#xff08;抛出异常&#xff09;1.5 Throwable 的成员方法1.6 编译时异常和运行…...

感慨:大三了,未来该何去何从呢

笔者曾在十一月份通过了字节跳动的三次面试&#xff0c; 但是最终因为疫情原因不能满足公司的入职时间要求&#xff0c; 没有拿到offer。近期也是投递了大量大厂的实习岗&#xff0c; 但是要么已读不回&#xff0c; 要么明确告诉我学历至少要985硕士(天天被阿里cpu)。 说实话一…...

分账系统逻辑

一、说明 主体与业务关系方进行相关利益和支出的分配过程 使用场景&#xff1a; 在分销业务中&#xff0c;主营商户收到用户购买分销商品所支付的款项后&#xff0c;可以通过分账逻辑&#xff0c;与分销商进行佣金结算。在零售、餐饮等行业中&#xff0c;当销售人员完零售等…...

SpringCloud篇——什么是SpringCloud、有什么优缺点、学习顺序是什么

文章目录一、首先看官方解释二、Spring Cloud 的项目的位置三、Spring Cloud的子项目四、Spring Cloud 现状五、spring cloud 优缺点六、Spring Cloud 和 Dubbo 对比七、Spring Cloud 学习路线一、首先看官方解释 Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式…...

TCP核心机制之连接管理详解(三次握手,四次挥手)

目录 前言&#xff1a; 建立连接 建立连接主要两个TCP状态&#xff1a; 断开连接 断开连接的两个重要状态 小结&#xff1a; 前言&#xff1a; TCP是如何建立对端连接&#xff0c;如何断开连接&#xff0c;这篇文章会详细介绍。 建立连接 首先明确连接的概念&#xff1a…...

前端—环境配置

前端开发建议用 Google Chrome 浏览器 vscode https://code.visualstudio.com 1、open in browser 插件&#xff1a;可以在 vscode 中直接运行查看浏览器效果 2、Live Server 插件&#xff1a;可以使代码修改浏览器页面实时刷新。 用户代码片段 … JavaScript 与 TypeScri…...

大学生常用python变量和简单的数据类型、可迭代对象、for循环的3用法

文章目录变量和简单的数据类型下划线开头的对象删除内存中的对象列表与元组debug三酷猫钓鱼记录实际POS机小条打印使用循环找乌龟可迭代对象&#x1f4d7;理解一&#x1f4d8;理解二2️⃣什么是迭代器✔️注意3️⃣迭代器对象4️⃣有关迭代的函数for循环的3用法&#x1f338;I …...

Java集合:Map的使用

1.Map框架l----Map:双列数据&#xff0c;存储key-value对的数据 ---类似于高中的函数: y f(x)|----HashMap:作为Map的主要实现类&#xff0c; 线程不安全的&#xff0c;效率高&#xff1b;可以存储null的key和value|----LinkedHashMap:保证在遍历map元素时&#xff0c;可以按照…...

【Datawhale图机器学习】第一章图机器学习导论

图机器学习导论 学习路径与必读论文清单 斯坦福CS224W&#xff08;子豪兄中文精讲&#xff09;知识图谱实战DeepwalkNode2vecPageRankGNNGCNGragh-SAGEGINGATTrans-ETrans-R 图无处不在 图是描述关联数据的通用语言 举例 计算机网络新冠肺炎流行病学调查传播链食物链地铁图…...

window 配置深度学习环境GPU

CUDA 11.6 CUDNN Anaconda pytorch 参考网址&#xff1a;https://zhuanlan.zhihu.com/p/460806048 阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 (aliyun.com) 电脑信息 RTX 2060 GPU0 1. CUDA 11.6 1.1 确认信息 C:\Users\thzn>nvidia-smi &#xff08;CUDA Versi…...

VS Code 用作嵌入式开发编辑器

使用 Keil MDK 进行嵌入式开发时&#xff0c;Keil 的编辑器相对于主流编辑器而言有些不方便&#xff0c;比如缺少暗色主题、缺少智能悬停感知&#xff08;鼠标停在一个宏上&#xff0c;能自动展开最终的宏结果&#xff09;、代码补全不好用等等&#xff0c;所以推荐使用 VS Cod…...

【Python】网络爬虫经验之谈

爬虫经验之谈对爬虫的认识网站分析技术选型JS逆向反爬机制结语近段时间&#xff0c;因为工作需要做一些爬虫的开发&#xff0c;分享一下走过的坑和实战的经验吧&#xff01;对爬虫的认识 F12查看的网络请求&#xff0c;找到相应的接口查看一下json数据来源和构造。我爬取的网站…...

数学建模美赛【LaTeX】公式、表格、图片

数学建模美赛【LaTeX】公式、表格、图片 1 宏包 \package{ } 就是在调用宏包&#xff0c;对计算机实在外行的同学姑且可以理解为工具箱。 每一个宏包里都定义了一些专门的命令&#xff0c;通过这些命令可以实现对于一类对象&#xff08;如数学公式等&#xff09;的统一排版&a…...

【大数据】YARN节点标签Node Label特性

简介 YARN 的 Node-label 特性能够将不同的机器类型进行分组调度&#xff0c;也可以根据不同的资源要求进行分区调度。运维人员可以根据节点的特性将其分为不同的分区来满足业务多维度的使用需求。YARN的Node-label功能将很好的试用于异构集群中&#xff0c;可以更好地管理和调…...

C# SolidWorks二次开发 API-命令标签页的切换与按钮错乱问题

这是一个网友咨询的问题&#xff0c;说他想控制默认打开文件之后solidworks上方工具栏的当前激活标签页。 之前我们提到过,制作Solidworks的插件也会在上面增加一个标签页&#xff0c;用来放自己开发的命令&#xff0c;经常开发的人肯定会遇到有时候更新版本&#xff0c;或者标…...

ElasticSearch 7.6.1

疑问 ES为什么这么快&#xff1f; 全文检索 听过一个程序扫描文本的每一个单词&#xff0c;针对单词建立索引&#xff0c;并保存该单词在文本中的位置&#xff0c;以及出现的次数。在检索查询时候&#xff0c;通过建立好的索引进行查询&#xff0c;将索引中单词对应的文本位…...

Linux系列 操作系统安装及服务控制(笔记)

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.操作系统 1.Linux系统三大类 &#xff08;1&#xff09;ubu…...

Linux基础 - NTP时间同步

‍‍&#x1f3e1;博客主页&#xff1a; Passerby_Wang的博客_CSDN博客-系统运维,云计算,Linux基础领域博主 &#x1f310;所属专栏&#xff1a;『Linux基础』 &#x1f30c;上期文章&#xff1a; Linux基础 - DNS服务进阶 &#x1f4f0;如觉得博主文章写的不错或对你有所帮助…...

golang 入门教程:迷你 Twitter 后端

请记住&#xff0c;这个项目主要是为了稍微熟悉下Golang&#xff0c;您可以复制架构&#xff0c;但该项目缺少适当的 ORM&#xff0c;没有适当的身份验证或授权&#xff0c;我完全无视中间件&#xff0c;也没有测试。 我将在其自己的部分中讨论所有这些问题&#xff0c;但重要的…...

CPP2022-30-期末模拟测试03

6-1 引用作函数形参交换两个整数 分数 5 全屏浏览题目 切换布局 作者 李廷元 单位 中国民用航空飞行学院 设计一个void类型的函数Swap&#xff0c;该函数有两个引用类型的参数&#xff0c;函数功能为实现两个整数交换的操作。 裁判测试程序样例&#xff1a; #include <…...

温州网站建设小公司/广告投放公司

DaoCloud获光速安振数百万美元投资&#xff0c;将推出容器技术云平台 新浪网以Docker为代表的容器技术是2014年最受关注的云计算开源项目。这项技术为开发云平台原生应用提供了便利的手段&#xff0c;在开发测试领域和云计算平台运维 ...2 days ago新闻 SDN将帮助Docker克服网络…...

生产企业做网站有用吗/百度搜索优化怎么做

遇到这种情况&#xff0c;现有项目的数据库已经建好&#xff0c;数据表也已经创建完成。问题来的&#xff0c;数据库不能插入中文&#xff0c;调试时候发现中文数据从发送请求到最后请求处理完成这些步骤&#xff0c;中文还没有发生乱码。只有在存储到数据库后查询数据并显示才…...

web前端导师小何/宝鸡百度seo

NET 连接池救生员 防止可淹没应用程序的池溢出 William Vaughn 大多数 ADO.NET 数据提供程序使用连接池&#xff0c;以提高围绕 Microsoft 断开连接的 .NET 结构构建的应用程序的性能。应用程序首先打开一个连接&#xff08;或从连接池获得一个连接句柄&#xff09;&#xff0c…...

自己开网店没有货源怎么办/百度seo通科

JDBC客户端操作步骤 转载于:https://www.cnblogs.com/ggzhangxiaochao/p/9222944.html...

电商网站设计公司有哪些/网上如何推广自己的产品

Axure RP Pro - 相关问题 - 修改生成的prototype原型的css样式表 使用Axure RP Pro生成prototype原型时&#xff0c;会按照线框中的设计生成所对应的元素。因此为了调整生成原型的效果&#xff0c;就需要调整线框中的设计&#xff0c;但是Axure RP Pro中并不是HTML/CSS设计工具…...

淘宝网上做美国签证的网站可靠吗/seo排名工具有哪些

首先&#xff0c;对于动态规划&#xff0c;我来做一个简短的介绍&#xff0c;相信各位都看得懂。动态规划(dynamic programming)是运筹学的一个分支&#xff0c;是求解决策过程(decision process)最优化的数学方法。先给一道最简单的例题&#xff08;小学奥数水平&#xff09;&…...