• linux篇【10】:进程信号


    目录

    一.信号入门

    1.信号是操作系统内一个内置机制

    2.前后台进程的几条命令与ctrl+c,Ctrl+z

    3.信号分类

    4.信号产生是异步的

    5.进程是如何记住这个信号

    (3)存储方式:位图

    二.signal ——对某信号设置自定义行为(捕捉)的函数

    (1)证明 ctrl + c 是2号信号

    (2)ctrl+\:3号信号 ——默认也是终止自己

    9号信号:管理员信号,该信号不可被自定义设置(捕捉)。除了D状态进程,其他进程都可杀。SIGSTOP(19号信号)/SIGKILL(9号信号)信号无法被阻塞,无法被自定义,无法被忽

     (3) 一个进程无法被kill杀死的可能有哪些

    三.用户层产生信号的方式(信号产生前)

    1.敲键盘的信号发送过程

    2.通过 系统接口 完成对进程发送信号

     (1)kill 

    自己写一个mykill命令:

     (2)raise

     (3)abort

    ① abort()   =   exit()

     ②9号信号不可被捕捉,6号信号是可以被捕捉,但捕捉完还是要退出

    3. 软件条件,定闹钟alarm

     ①1秒IO只能不到2w次

     ②但实际CPU 1秒能跑5亿次,证明IO效率低

    4.硬件异常产生信号

    (1)除零错误 与 越界&&野指针

    (2)崩溃了,一定会导致进程终止吗?——答:不一定!

    四.core dump 核心转储

    1.status中的core dump标记位

    (1)core dump 核心转储 详细解释:

    (2)观察core dump标记位

    (3) core可以用gdb定位错误

    五.阻塞信号(信号产生中)

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

    2. 在内核中的表示

    信号忽略和信号阻塞区别:

    3.sigset_t(位图)

    4.信号集操作函数

    5.sigprocmask

    6.sigpending

    六.信号的捕捉(信号处理后)

    1.用户态和内核态

    2.自定义捕捉信号的处理过程

    3.第二个捕捉函数 sigaction

    (1)sigaction介绍

     killall mysignal——根据进程名杀进程

     (2)sa_mask 解释

    (3)handler可以配合使用switch语句:

    4.可重入函数

    5.volatile

    (1)常规情况(无-O2优化,无volatile)

    (2)优化情况(有-O2优化,无volatile)

     (3)优化+volatile 情况(有-O2优化,有volatile)

    (4)volatile关键字可以保证并发编程中的可见性,有序性。

    七.SIGCHLD信号

    1.子进程在 终止/暂停(stop)/继续运行时 会给父进程发SIGCHLD信号(ctrl+z是19号信号-暂停进程)

    (1)进程退出会发信号

     (2)进程 暂停(stop)/继续运行 会发信号(ctrl+z是19号信号-暂停进程)

    2.等待的方案

    (1)单个子进程退出

     (2)多个子进程同时退出时的等待方案

    (3)多个子进程不同时退出时的等待方案

    (4)将SIGCHLD的处理动作置为SIG_IGN——自动释放子进程


    一.信号入门

    1.信号是操作系统内一个内置机制

    信号是给进程发送的,进程要具备处理信号的能力。
    1.该能力一定是预先已经早就有了的(程序员——>写OS的代码,OS帮我们提供)
    2.进程能够识别对应的信号
    3.进程能够处理对应信号
    对于进程来讲,即便是信号还没有产生,我们进程已经具有识别和处理这个信号的能力了,因为信号是操作系统内一个内置的机制

    2.前后台进程的几条命令与ctrl+c,Ctrl+z

    • 前台进程:占有控制终端的进程,其它称为后台进程。前台进程可以ctrl+c杀掉,后台进程不可杀,只能把后台进程转成前台进程再ctrl+c杀掉。

    Ctrl+c是强制中断程序的执行,杀死程序的进程;
    Ctrl+z是将任务中断,挂起的状态,进程还存在,任务还没有结束,用户可以通过使用fg命令将中断的命令再次启动运行;

    ctrl+c:硬件行为被解释成信号,发送给进程。作用:杀死进程

    jobs:查看后台进程

    ./proc &:把proc这个前台进程放入后台

    fg (任务号):把作业号对应的后台进程放入前台 

    3.信号分类

    1~31:普通信号(我们要学习的信号)也叫不可靠信号。 34~64:实时信号(带RM) 【34】SIGRTMIN(实时信号也叫可靠信号)

    普通信号介绍:1) SIGHUP ——>1是信号的编号,SIGHUP是信号名称。信号就是宏,SIGHUP的值就是1

    查看详细信号命令

    man 7 signal
    

    4.信号产生是异步的

    因为信号产生是异步的(互不干扰,同步反义词),当信号产生的时候,对应的进程可能正在做更重要的事情,我们进程可以暂时不处理这个信号!——进程可能不需要立即处理这个信号。不代表这个信号不会被处理
    未来要处理,所以你必须记住这个信号已经来了(要记住:①是否有信号。②什么信号。 )(你在玩游戏,外卖员敲门,你说等一会儿)
    ①默认动作(吃外卖)
    ②忽略(把外面放在外卖,不吃)
    ③自定义动作(把外卖给弟弟吃)
    这就是类比了信号的处理,信号的捕捉,递达处理动作。

    5.进程是如何记住这个信号

    (1)要存储的内容

    ① 有没有产生【位图比特位的内容1,0】
    ② 什么信号产生【位图比特位的位置】

    (2)存储在哪里:信号内容记录在 进程的PCB中的 

    (3)存储方式:位图
     

    1. task_ struct {
    2. uint32_ _t sig; 位图,0000 0010
    3. }

    进程的task_ struct是内核的数据结构

    只有OS有这个权利,能直接修改这个task_struct内的数据位图
    OS是进程的管理者,进程的所有属性的获取和设置,只能由OS来。
    无论信号怎么产生,最终一定只能是OS帮我们进行信号的设置!

    二.signal ——对某信号设置自定义行为(捕捉)的函数

    man 2 signal        

                    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    signum:对哪个信号设置捕捉动作。 handler:自定义对信号的捕捉动作的函数

    (1)证明 ctrl + c 是2号信号

    这里不是调用hander方法,这里只是设置了一个回调(注册这个方法),让SIGINT(2)产生的时候,该方法才会被调用,如果不产生SIGINT(2),该方法不会被调用!

    ctrl + c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己

    今天更改了对2号信号的处理,设置了用户自定义处理方法

    1. void handler(int signo)
    2. {
    3. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    4. }
    5. int main()
    6. {
    7. //这里不是调用hander方法,这里只是设置了一个回调,让SIGINT(2)产生的时候,该方法才会被调用
    8. //如果不产生SIGINT(2),该方法不会被调用!
    9. //ctrl + c : 本质就是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己
    10. //今天更改了对2号信号的处理,设置了用户自定义处理方法
    11. signal(SIGINT, handler); // 设置所有的信号的处理动作,都是自定义动作
    12. while (true)
    13. {
    14. cout << "我是一个正在运行中的进程: " << getpid() << endl;
    15. sleep(1);
    16. }
    17. return 0;
    18. }

    (2)ctrl+\:3号信号 ——默认也是终止自己

    1. void handler(int signo)
    2. {
    3. cout << "我是一个进程,刚刚获取了一个信号: " << signo << endl;
    4. }
    5. int main()
    6. {
    7. signal(SIGINT, handler); // 设置所有的信号的处理动作,都是自定义动作
    8. signal(3, handler);
    9. while (true)
    10. {
    11. cout << "我是一个正在运行中的进程: " << getpid() << endl;
    12. sleep(1);
    13. }
    14. return 0;
    15. }

    这个进程里面自定义这些终止信号后,就只能用9号信号杀死。

    9号信号:管理员信号,该信号不可被自定义设置(捕捉)。除了D状态进程,其他进程都可杀。SIGSTOP(19号信号)/SIGKILL(9号信号)信号无法被阻塞,无法被自定义,无法被忽

     

     (3) 一个进程无法被kill杀死的可能有哪些

    A①这个进程阻塞了信号

    B②用户有可能自定义了信号的处理方式

    C③这个进程有可能是僵尸进程

    D④这个进程当前状态是停止状态

    A正确 信号被阻塞,则暂时不被处理(SIGKILL/SIGSTOP除外,因为无法被阻塞,这里说的是可能性,因此不做太多纠结)

      B正确 自定义处理之后,信号的处理方式有可能不再是进程退出

      C正确 僵尸进程因为已经退出,因此不做任何处理

      D正确 进程停止运行,则将不再处理信号

    三.用户层产生信号的方式(信号产生前)

    1.敲键盘的信号发送过程

    键盘产生信号,OS给进程发送的信号——发送信号(写入信号):就是OS在位图中把对应信号的位图由0置1,即可完成发送信号(发送信号不如说成写入信号)。详解ctrl+v的信号发送过程:

    敲键盘ctrl+v,键盘产生信号后,通过中断的方式告诉OS,CPU直接调用中断向量表中的方法,把ctrl+v这个组合按键读取进来,OS识别此组合按键,OS对组合按键进行解释,解释成信号,OS找到对应进程,在位图中把对应信号的位图由0置1。

    2.通过 系统接口 完成对进程发送信号

     (1)kill 

    man 2 kill

      int kill(pid_t pid, int sig);

    向任意进程pid发送任意信号sig

    返回值:成功返回0;失败返回-1

    自己写一个mykill命令

     

     (2)raise

    man 3 raise

    给自己这个进程发送任意信号

     

     (3)abort

    man 3 abort

    向自己这个进程发送 SIGABRT 这个6号信号

    ① abort()   =   exit()

     

     ②9号信号不可被捕捉,6号信号是可以被捕捉,但捕捉完还是要退出

    3. 软件条件,定闹钟alarm

    alarm

    man 2 alarm

    定闹钟:seconds秒以后给自己这个进程发送信号 14 SIGALRM

     ①1秒IO只能不到2w次

     ②但实际CPU 1秒能跑5亿次,证明IO效率低

    4.硬件异常产生信号

    (1)除零错误 与 越界&&野指针

    进程崩溃的本质,是该进程收到了异常信号。

    因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象!

    除零错误: CPU内部,状态寄存器,当我们除0的时候,CPU内的状态寄存器会被由0置1,设置成为 有报错:浮点数越界。CPU的内部寄存器(硬件)会记录报错,OS就会识别到CPU内有报错啦 -> 识别之后OS会确认 ①谁干的?② 是什么报错,OS结合报错会构建信号,并向目标进程发送信号->目标进程在合适的时候处理信号,处理一般信号就是终止进程

    越界&&野指针: 我们在语言层面使用的地址(指针), 其实都是虚拟地址,虚拟地址要转成物理地址->然后才能访问物理内存->才可以读取对应的数据和代码

    如果虚拟地址有问题,因为地址转化的工作是由(MMU(memory maneger unit 内存管理单元,是硬件)+页表(软件)), 转化过程就会引起问题->表现在硬件MMU上->OS发现硬件出现了问题会确认:①谁干的?② 是什么报错(OS->构建信号) -> 向目标进程发送信号->目标进程在合适的时候->处理信号->终止进程

    (2)崩溃了,一定会导致进程终止吗?——答:不一定!

     例如:我们把信号都重新注册了,但是注册的方法里面没有终止程序(exit),那么当发生除0错误时,OS给进程发送8号信号,但是由于没有中断进程,这个异常一直存在,OS会持续不断的发送8号进程,此时崩溃了,但是进程也并没有终止

     小总结:

    上面所说的所有信号产生,最终都要由OS来进行执行,为什么?
    ——OS是进程的管理者
    信号的处理是否是立即处理的?
    ——在合适的时候
    信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
    ——会被进程记录下来,记录在进程PCB中。
    一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
    ——知道
    如何理解OS向进程发送信号?能否描述一下完整的发送处理过程? 

    ——OS将进程PCB中信号位图的比特位由0置1

    四.core dump 核心转储

    1.status中的core dump标记位

    man 7 signal

    (1)core dump 核心转储 详细解释:

    像3,4,6,8,11这些信号属于代码内部出现问题导致的进程终止(比如4:非法指令。6:abort终止。8:浮点数错误。11:越界。),这些异常可以调试。如果父进程获取子进程退出信息时,若被这几种信号终止,status中的core dump标记位会被置1,并且会生成一个叫 core.22357 的大文件,22357 叫引起core文件(崩溃)的进程pid。

     即:core dump 核心转储定义: core dump 会把进程在运行中,对应的异常上下文数据,core dump(转而存储)到磁盘上,方便调试,同时会把status中的 status- >core dump -> 置1

    (2)观察core dump标记位

    ulimit -a 打开 core dump文件

     

    (3) core可以用gdb定位错误

    五.阻塞信号(信号产生中)

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

    实际执行信号的处理动作称为信号递达(Delivery)
    信号从产生到递达之间的状态,称为信号未决(Pending)。
    进程可以选择阻塞 (Block )某个信号。
    被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
    注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

    2. 在内核中的表示

    信号在内核中的表示示意图

    横着看——block—pending—hander横着每一格对应每一格

    pending:未决信号集。(用途:代表是否收到信号)就是一个32bit的位图,存信号标记位,比特位位置是信号编号(第一个比特位代表1号信号),比特位内容代表 是否收到信号。(信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志

    【非可靠信号在进行注册时,会查看是否已经有相同信号添加到未决集合中,如果有则什么都不做,因此非可靠信号只会添加一次,因此处理完毕后会直接移除(准确来说是先移除,后处理)。而可靠信号会重复添加信号信息到sigqueue链表中,相当于可靠信号可以重复添加,处理完毕后,因为有可能还有相同的信号信息待处理,因此并不会直接移除,而是检测没有相同信号信息后才会从pending集合中移除】

    hander:函数指针数组。里面存的是处理pending表里面信号的处理方法的函数地址。(signal注册信号方法就是通过改这个数组实现的)补充:多次发送信号时,因为一个信号只有一个比特位去记录,所以(一段时间内)多次发送普通信号时,只能递达一次去执行方法,其他全部丢弃。实时信号是可以记录多次的,就成为队列了。

    block:信号屏蔽字(阻塞信号集)。和pending一样的是 比特位位置是信号编号,不同的是 比特位内容代表 是否阻塞该信号——为1:拦截对应信号执行对应的方法,举例:即使pending[0]=1,block[0]=1,1号信号也无法执行hander里面对应的方法( 阻塞信号集也叫做当前进程的 信号屏蔽字(Signal Mask),这里的屏蔽应该理解为阻塞而不是忽略。

    ①每个信号都有两个标志位分别表示阻塞 (block) 和未决 (pending), 还有一个函数指针表示处理动作。信号产生时, 内核在进程控制块中设置该信号的未决标志, 直到信号递达才清除该标志在上图的例子中,SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作。
    ②SIGINT 信号产生过 , 但正在被阻塞 , 所以暂时不能递达。虽然它的处理动作是忽略SIG_IGN , 但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。即:block是1时一直是阻塞,pending对应比特位是1就一直保持1,除非未来解除block的阻塞,否则pending对应比特位永远保持1
    ③SIGQUIT 信号未产生过 , 一旦产生 SIGQUIT 信号将被阻塞 , 它的处理动作是用户自定义函数 sighandler 。如果在进程解除对某信号的阻塞之前这种信号产生过多次, 将如何处理 ? Linux 是这样实现的 : 常规信号在递达之前产生多次只计一次 , 而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号

    信号忽略和信号阻塞区别:

            信号忽略是处理信号的方式,处理方式是忽略他;信号阻塞是拦截信号的递达,不让信号被处理

    3.sigset_t(位图)

    sigset_t就是一个位图

    从上图来看 , 每个信号只有一个 bit 的未决标志 , 0 1, 不记录该信号产生了多少次 , 阻塞标志也是这样表示的。 因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集 , 这个类型可以表示每个信号的“ 有效 无效 状态 , 在阻塞信号集中 有效 无效 的含义是该信号是否被阻塞 , 而在未决信号集中 有效” 无效 的含义是该信号是否处于未决状态。
    1. # define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
    2. typedef struct
    3. {
    4. unsigned long int __val[_SIGSET_NWORDS];
    5. } __sigset_t;

    4.信号集操作函数

    sigset_t 类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t变量 , 而不应该对它的内部数据做任何解释, 比如用 printf 直接打印 sigset_t 变量是没有意义的
    #include
    int sigemptyset(sigset_t *set);                                ——对信号集set做清空
    int sigfillset(sigset_t *set);                                     ——对信号集set全部置1
    int sigaddset (sigset_t *set, int signo);                  ——在特定信号集set 中加上特定信号signo
    int sigdelset(sigset_t *set, int signo);                    ——在特定信号集set 中删除特定信号signo
    int sigismember (const sigset_t *set, int signo);  ——在特定信号集set 中判断是否有特定信号signo
    这四个函数都是成功返回 0, 出错返回 -1 sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种 信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1

    5.sigprocmask

    调用函数sigprocmask 可以读取或更改进程的信号屏蔽字 (阻塞信号集),即修改block表
    #include
    int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
    返回值:若成功则为0,若出错则为-1
    假设当前的信号屏蔽字为mask,下表说明了how参数的选项:

    const sigset_t *set 是输入型参数,set传入一个信号集,

    如果how是 SIG_BLOCK :则把set中包含的信号添加到当前进程的信号屏蔽字block表中(block对应标记位由0置1),当前进程的信号屏蔽字中已经屏蔽的信号不会改变。
    如果how是 SIG_UNBLOCK:则把set中包含的信号从到当前进程的信号屏蔽字block表中解除屏蔽block对应标记位由1置0),当前进程的信号屏蔽字中其他已经屏蔽的信号不会改变。
    如果how是 SIG_SETMASK:直接把用set这个信号集把 当前进程的信号屏蔽字block表 覆盖

    sigset_t *oset 是输出型参数:把原本老的信屏蔽字返回出来,以便恢复,如果不想返回就传nullptr

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

    使用示例:

    6.sigpending

    #include
    sigpending
    读取当前进程的未决信号集 , 通过 set 参数传出。调用成功则返回 0, 出错则返回 -1

     总结:sigprocmask修改block表;sigpending读取 pending表;signal修改hander表

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. using namespace std;
    10. int cnt = 0;
    11. void handler(int signo)
    12. {
    13. cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl;
    14. // exit(1);
    15. }
    16. static void showPending(sigset_t *pendings)
    17. {
    18. for (int sig = 1; sig <= 31; sig++)
    19. {
    20. if (sigismember(pendings, sig))
    21. {
    22. cout << "1";
    23. }
    24. else
    25. {
    26. cout << "0";
    27. }
    28. }
    29. cout << endl;
    30. }
    31. int main(int argc, char *argv[])
    32. {
    33. cout << "pid: " << getpid() << endl;
    34. sigset_t bsig, obsig;
    35. sigemptyset(&bsig);
    36. sigemptyset(&obsig);
    37. // sigfillset();
    38. for (int sig = 1; sig <= 31; sig++) //将当前进程阻塞所有信号,并注册所有信号
    39. {
    40. sigaddset(&bsig, sig);
    41. signal(sig, handler);
    42. }
    43. // 设置用户级的信号屏蔽字到内核中
    44. sigprocmask(SIG_SETMASK, &bsig, &obsig);
    45. // 1. 不断的获取当前进程的pending信号集
    46. sigset_t pendings;
    47. int cnt = 0;
    48. while (true)
    49. {
    50. // 1.1 清空信号集
    51. sigemptyset(&pendings);
    52. // 1.2 获取当前进程(谁调用,获取谁)的pending信号集
    53. if (sigpending(&pendings) == 0)
    54. {
    55. // 1.3 打印一下刚刚获取到的当前进程的pengding信号集
    56. showPending(&pendings);
    57. }
    58. sleep(1);
    59. cnt++;
    60. if(cnt == 20) //第20秒时解除2号信号的阻塞
    61. {
    62. cout << "解除对2号信号的block...." << endl;
    63. sigset_t sigs;
    64. sigemptyset(&sigs);
    65. sigaddset(&sigs, 2);
    66. sigprocmask(SIG_UNBLOCK, &sigs, nullptr);
    67. }
    68. }

     

    六.信号的捕捉(信号处理后)

    进程处理信号,不是立即处理的。
    合适的时候,是什么时候呢? ?——当当前进程从内核态,切换回用户态的时候,进行信号的检测与处理! 先解释用户态和内核态:

    1.用户态和内核态


    ①OS在不在内存中被加载呢? ?——在
    无论进程怎么切换,我们都可以找到内核的代码和数据,前提是你只要能够有权利访问!

    ②当前进程如何具备权利 访问这个内核页表乃至访问内核数据呢?——要进行身份切换。
            进程如果是用户态的——>只能访问用户级页表 0~3G
            进程如果是内核态的——>访问内核级和用户级的页表 3~4G
    ③我怎么知道我是用户态的还是内核态的呢?
    CPU内部有对应的状态寄存器CR3, CR3有比特位标识当前进程的状态 0:内核态,3:用户态

    ④0—>3 用户态切到内核态的情况:1.系统调用的时候。2.时间片到了,进程间切换。3.其他等等。执行完毕就继续切回用户态。即:程序运行从用户态切换到内核态的操作:中断/异常/系统调用,例如

            <1> 整数除以零操作会导致用户态—>内核态:因为会导致程序异常(分母不能为0)

            <2> sin()函数调用操作不会切换状态,因为库函数并不会引起运行态的切换

            <3> read 系统调用操作会导致用户态—>内核态:符合系统调用接口

    ⑤内核态vs用户态
    内核态可以访问所有的代码和数据——内核态具备更高权限
    用户态只能访问自己的

    ⑥我们的程序,会无数次直接或者间接的访问系统级软硬件资源(管理者是OS),本质上,你并没有自己去操作这些软硬件资源,而是必须通过OS- >无数次的陷入内核(1.切换身份3->0 2.切换页表,切到内核级页表)->调用内核的代码->完成访问的动作->结果返回给用户(1.切换身份0->3 2. 切换页表,切到用户级页表)->用户得到结果

    ⑦while(1);死循环进程普通程序会身份切换吗? —>也会陷入内核,来回切换身份 —>你也有自己的时间片 —>时间片到了的时候->OS收到中断信息,把进程从cpu移走,进程切到内核态,更换内核级页表 —>保护上下文,执行调度算法 —>选择了新的进程 —>恢复新进程的上下文 —>用户态,更换成用户级页表 —>CPU执行的就是新进程的代码!
     

    2.自定义捕捉信号的处理过程

    ①因为一些系统调用例如open(),用户态—>内核态,处理完open的代码后

    ②本来可以返回代码继续执行了,但是正好处于内核态,就顺便去检测信号,并处理信号。

    ③处理信号:若是阻塞或无信号(忽略/默认)就直接进行对应简答操作就 内核态—>用户态 返回即可 / 若是非阻塞并有信号的自定义捕捉,就 内核态—>用户态 执行对应的handler方法。(执行handler方法为什么只能是用户态?解释:内核态是什么都可以做的,如果让内核态做用户自定义代码,万一用户写的是一段恶意代码呢? ? ?比如rm根目录等等,这样内核态身份就被恶意利用了,所以内核为了保护自己,就只能用户态执行用户的代码

    ④执行完信号对应方法后再由 用户态—>内核态 回到内核。此时所有任务都完成:系统接口调用完成,信号捕捉完成。

    ⑤完成所有任务后,由  内核态—>用户态 通过特殊系统调用返回到当时用户跳出的代码中。

    结论:进程的生命周期中,会有很多次机会去陷入内核(中断,陷阱,系统调用,异常...),一定会存在很多次的机会进行内核态返回用户态。进程的信号在被合适的时候处理,合适的时候:从内核态返回到用户态的时候,返回之前顺便检测信号,并处理信号

    过程图,跟上面一样,可以忽略:

    记忆图 :无穷大画法

    无穷大中间交点要在横线下方,则有多少个交点,就证明有多少个状态切换;方向决定了 是内核到用户,还是用户到内核

     

    3.第二个捕捉函数 sigaction

    (1)sigaction介绍

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

     signum:要对哪个信号自定义捕捉。act:设置成什么动作。oldact:老动作,输出型参数,不要就设置nullptr

    只用填sa_handler,sa_mask,2个 ,sa_flags 默认为0不考虑,其他是实时信号的也不考虑、

    sa_handler:注册信号的函数处理方法

    sa_ mask ——作用:执行某①信号处理函数时,信号集位图sa_ mask中有某②信号,阻塞①信号同时,阻塞 sa_ mask 传入的②信号——(2)是详细解释

     

     例1

     

    例2

     killall mysignal——根据进程名杀进程

     (2)sa_mask 解释

    sa_ mask ——作用:执行某①信号处理函数时,信号集位图sa_ mask中有某②信号,阻塞①信号同时,阻塞 sa_ mask 传入的②信号

    当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止

      ctrl c后再ctrl c发现2号信号为1,即:阻塞了2号信号。sigaddset(&act.sa_ mask,3) ; 信号集位图中有3号信号,作用是执行2号信号时阻塞了2号信号同时也阻塞3号信号

    (3)handler可以配合使用switch语句:

    1. void Handler2()
    2. {
    3. cout << "hello 2" << endl;
    4. }
    5. void Handler3()
    6. {
    7. cout << "hello 3" << endl;
    8. }
    9. void Handler4()
    10. {
    11. cout << "hello 4" << endl;
    12. }
    13. void Handler5()
    14. {
    15. cout << "hello 5" << endl;
    16. }
    17. void Handler(int signo)
    18. {
    19. switch (signo)
    20. {
    21. case 2:
    22. Handler2();
    23. break;
    24. case 3:
    25. Handler3();
    26. break;
    27. case 4:
    28. Handler4();
    29. break;
    30. case 5:
    31. Handler5();
    32. break;
    33. default:
    34. break;
    35. }
    36. }
    37. int main()
    38. {
    39. signal(2, Handler);
    40. signal(3, Handler);
    41. signal(4, Handler);
    42. signal(5, Handler);
    43. while (1)
    44. {
    45. sleep(1);
    46. }
    47. }

    4.可重入函数

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

    像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,即:不能多个执行流调用,被像这样的函数称为 不可重入函数(大部分都是)。反之, 如果一个函数只访问自己的局部变量或参数在重复被多个执行流调用时不会出现问题,则称为可重入(Reentrant) 函数。(或者此函数中对全局变量进行了原子操作,也是可重入函数<原子操作指的是一次完成,中间不会被打断的操作,表示操作过程是安全的>)想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

    如果一个函数符合以下条件之一则是不可重入的:
            调用了malloc free, 因为 malloc 也是用全局链表来管理堆的。
            调用了标准I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。

    5.volatile

    该关键字在C 当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下

    作用:保持内存的可见性,读取必须从内存中读

    (1)常规情况(无-O2优化,无volatile)

    正常makefile中 不加-O2 也不加volatile 是和(2)一样的结果,都能把flags修改而终止进程。

    (2)优化情况(有-O2优化,无volatile)

    但是makefile加了-O2,表明让编译器优化:因为while(!flags)是位运算,只能CPU处理,而且flags一直在被使用,此时CPU自作主张把内存中的flags放入寄存器中,当我们修改flags=1时,只是修改了内存中的flags,没有影响寄存器中的flags,则ctrl c 时执行处理方法,但是寄存器中的flags不会改变,永远不会终止进程。

     (3)优化+volatile 情况(有-O2优化,有volatile)

    volatile:保持内存的可见性,读取必须从内存中读

    此时要求CPU每次使用flags只能从内存中拿flags放入寄存器中,这样修改flags就可以终止进程。

    (4)volatile关键字可以保证并发编程中的可见性,有序性。

    并发编程中通常会遇到三个问题 原子性问题,可见性问题,有序性问题,java/C/C++中volatile关键字可以保证并发编程中的可见性,有序性。

    原子性:一个操作不会被打断,要么一次完成,要么不做。

    可见性:一个资源被修改后,是否对其他线程是立即可见的(一个变量的修改存在一个过程,将数据从内存加载的cpu寄存器,进行运算,完毕后交还内存,但是这个过程在代码优化中可能会被编译器优化,将数据放入寄存器,则后续运算只从寄存器取数据,就节省了从内存获取数据的时间)

    有序性:简单理解,程序按照写代码的先后顺序执行,就是有序的。(编译器有时候会为了提高程序效率进行代码优化,进行指令重排,来提高效率,而有序性就是禁止指令重排)

    而volatile关键字的作用是,防止编译器过度优化,因此具备 可见性与有序性 功能

    七.SIGCHLD信号

    进程一章讲过用 wait waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻塞地查询是否有子进程结束等待清理( 也就是轮询的方式 )。采用第一种waitpid方式, 父进程阻塞了就不能处理自己的工作了 ; 采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。
    其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用waitpid清理子进程即可。
    请编写一个程序完成以下功能 : 父进程 fork 出子进程 , 子进程调用 exit(2) 终止 , 父进程自定 义 SIGCHLD 信号的处理函数 , 在其中调用wait 获得子进程的退出状态并打印。事实上, 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调用 sigaction SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不 会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略 通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用 , 但不保证
    在其它 UNIX 系统上都可 用。请编写程序验证这样做不会产生僵尸进程

    1.子进程在 终止/暂停(stop)/继续运行时 会给父进程发SIGCHLD信号(ctrl+z是19号信号-暂停进程)

    子进程在 终止/暂停(stop)/继续运行时 会给父进程发SIGCHLD信号 , 该信号的默认处理动作是忽略。

     测试:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. void handler(int signo)
    6. {
    7. cout << "子进程退出啦,我确实收到了信号: " << signo << " 我是: " << getpid() << endl;
    8. }
    9. int main()
    10. {
    11. signal(SIGCHLD, handler);
    12. pid_t id = fork();
    13. if (id == 0)
    14. {
    15. while (true)
    16. {
    17. cout << "我是子进程: " << getpid() << endl;
    18. sleep(1);
    19. }
    20. exit(0);
    21. }
    22. // parent
    23. while (true)
    24. {
    25. cout << "我是父进程: " << getpid() << endl;
    26. sleep(1);
    27. }
    28. }

    (1)进程退出发信号

     (2)进程 暂停(stop)/继续运行 发信号(ctrl+z是19号信号-暂停进程)

    进程暂停也就是进程停止状态,进程停止运行,此时进程无法被kill杀死

    2.等待的方案

    (1)单个子进程退出

    等一个进程waitpid的第一个参数就传-1,可以等待任意进程退出就回收 

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChld(int signo)
    9. {
    10. assert(signo==SIGCHLD);
    11. pid_t id=waitpid(-1,nullptr,0);
    12. if(id>0)
    13. {
    14. cout<<"父进程等待成功,chld id:"<
    15. }
    16. }
    17. int main()
    18. {
    19. signal(SIGCHLD,FreeChld);
    20. pid_t id=fork();
    21. if(id==0)
    22. {
    23. int cnt=10;
    24. while(cnt--)
    25. {
    26. cout<<"我是子进程,pid:"<<getpid()<<"当前cnt:"<
    27. sleep(1);
    28. }
    29. cout<<"子进程退出,进入僵尸状态"<
    30. exit(0);
    31. }
    32. while(1)
    33. {
    34. cout<<"我是父进程,pid:"<<getpid()<
    35. sleep(1);
    36. }
    37. return 0;
    38. }

     (2)多个子进程同时退出时的等待方案

    如果10个进程同时退出,在执行方法时会阻塞该信号的多次发出,所以只能捕捉一个信号,导致其他信号丢失,则其他僵尸进程无法捕捉。所以应该给waitpid套一层循环,并加上等待失败的情况。

    错误的示例:

    这样只等待了一个子进程退出,其他9个全僵尸泄露了

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChld(int signo)
    9. {
    10. assert(signo == SIGCHLD);
    11. pid_t id = waitpid(-1, nullptr, 0);
    12. if (id > 0)
    13. {
    14. cout << "父进程等待成功,chld id:" << id << endl;
    15. }
    16. }
    17. int main()
    18. {
    19. signal(SIGCHLD, FreeChld);
    20. for (int i = 0; i < 10; i++)
    21. {
    22. pid_t id = fork();
    23. if (id == 0)
    24. {
    25. int cnt = 10;
    26. while (cnt--)
    27. {
    28. cout << "我是子进程,pid:" << getpid() << "当前cnt:" << cnt-- << endl;
    29. sleep(1);
    30. }
    31. cout << "子进程退出,进入僵尸状态" << endl;
    32. exit(0);
    33. }
    34. }
    35. while (1)
    36. {
    37. cout << "我是父进程,pid:" << getpid() << endl;
    38. sleep(1);
    39. }
    40. return 0;
    41. }

     

    处理后的示例:用给waitpid套一层循环即可等待所有子进程退出。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChld(int signo)
    9. {
    10. assert(signo == SIGCHLD);
    11. while (true)
    12. {
    13. pid_t id = waitpid(-1, nullptr, 0);
    14. if (id > 0)
    15. {
    16. cout << "父进程等待成功,chld id:" << id << endl;
    17. }
    18. else
    19. {
    20. cout<<"所有子进程已退出"<
    21. break;
    22. }
    23. }
    24. }
    25. int main()
    26. {
    27. signal(SIGCHLD, FreeChld);
    28. for (int i = 0; i < 10; i++)
    29. {
    30. pid_t id = fork();
    31. if (id == 0)
    32. {
    33. int cnt = 5;
    34. while (cnt)
    35. {
    36. cout << "我是子进程,pid:" << getpid() << "当前cnt:" << cnt-- << endl;
    37. sleep(1);
    38. }
    39. cout << "子进程退出,进入僵尸状态" << endl;
    40. exit(0);
    41. }
    42. }
    43. while (1)
    44. {
    45. cout << "我是父进程,pid:" << getpid() << endl;
    46. sleep(1);
    47. }
    48. return 0;
    49. }

    (3)多个子进程不同时退出时的等待方案

    子进程退出会给父进程发送SIGCILD,这里10个进程7个子进程5秒后退出,后3个子进程10秒再退出。

    若是waitpid(-1,nullptr,0):此时是父进程阻塞等待,那么当前7个子进程退出后,父进程等待成功前7个子进程,此时父进程已经陷入了FreeChld中的循环中了,所以会继续等待,但是后3个仍会运行5秒再退出,这5秒内,父进程会一直阻塞等待,知道这3个子进程退出才会执行waitpid等待成功,这个期间,父进程什么也干不了,一直阻塞着。

    若是waitpid(-1,nullptr,WNOHANG):此时是父进程非阻塞等待,那么当前7个子进程退出后,父进程等待成功前7个子进程,此时父进程进入FreeChld中的循环中,所以会继续等待,后3个仍会运行5秒再退出,这三个进程运行期间,父进程是非阻塞等待会通过id==0(等待成功,但子进程没有退出 )这个条件跳出循环去做父进程的事情,直到这3个子进程退出又会发出信号,父进程就会回收这3个子进程。这个期间,父进程可以执行自己的代码,并不阻塞,效率高。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. using namespace std;
    8. void FreeChld(int signo)
    9. {
    10. assert(signo == SIGCHLD);
    11. while (true)
    12. {
    13. // -1: 等待任意一个子进程
    14. //当这里是 waitpid(-1, nullptr, 0)时是第一种情况
    15. pid_t id = waitpid(-1, nullptr, WNOHANG);
    16. if (id > 0)
    17. {
    18. cout << "父进程等待成功,chld id:" << id << endl;
    19. }
    20. else if (id == 0) //等待成功,但子进程没有退出
    21. {
    22. cout << "还有子进程没有退出,父进程要忙自己的事了" << endl;
    23. break;
    24. }
    25. else
    26. { // waitpid 已经没有子进程了调用失败,id<0
    27. cout << "所有子进程已退出" << endl;
    28. break;
    29. }
    30. }
    31. }
    32. int main()
    33. {
    34. signal(SIGCHLD, FreeChld);
    35. for (int i = 0; i < 10; i++)
    36. {
    37. pid_t id = fork();
    38. if (id == 0)
    39. {
    40. int cnt = 0;
    41. if (i < 7)
    42. cnt = 5;
    43. else
    44. cnt = 10;
    45. while (cnt)
    46. {
    47. cout << "我是子进程,pid:" << getpid() << "当前cnt:" << cnt-- << endl;
    48. sleep(1);
    49. }
    50. cout << "子进程退出,进入僵尸状态" << endl;
    51. exit(0);
    52. }
    53. }
    54. while (1)
    55. {
    56. cout << "我是父进程,pid:" << getpid() << endl;
    57. sleep(1);
    58. }
    59. return 0;
    60. }

    (4)SIGCHLD的处理动作置为SIG_IGN——自动释放子进程

    下面这个代码:父进程调用sigactionSIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

    signal(SIGCHLD, FreeChld);
    子进程退出的时候,默认的信号处理就是忽略,那调用signal/sigaction手动设置SIG_IGN, 意义在哪里呢?
    答:SIG_IGN手动设置:操作系统自动设置:让子进程退出不要给父进程发送信号了,并且自动释放,子进程不会僵尸

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. int main()
    10. {
    11. // signal(SIGCHLD, FreeChld);
    12. // 子进程退出的时候,默认的信号处理就是忽略吗?
    13. // 调用signal/sigaction SIG_IGN, 意义在哪里呢?
    14. // SIG_IGN手动设置,让子进程退出,不要给父进程发送信号了,并且自动释放
    15. signal(SIGCHLD, SIG_IGN);
    16. for (int i = 0; i < 10; i++)
    17. {
    18. pid_t id = fork();
    19. if (id == 0)
    20. {
    21. //子进程
    22. int cnt = 3;
    23. // if (i < 7)
    24. // cnt = 5;
    25. // else
    26. // cnt = 20;
    27. while (cnt)
    28. {
    29. cout << "我是子进程, pid: " << getpid() << " 当前的cnt: " << cnt-- << endl;
    30. sleep(1);
    31. }
    32. cout << "子进程退出,进入僵尸状态" << endl;
    33. exit(0);
    34. }
    35. // sleep(1);
    36. }
    37. while (true)
    38. {
    39. cout << "我是父进程,我正在运行: " << getpid() << endl;
    40. sleep(1);
    41. }
    42. // //父进程,都是要自己主动等待
    43. // if(waitpid(id, nullptr, 0) > 0)
    44. // {
    45. // cout << "父进程等待子进程成功" << endl;
    46. // }
    47. return 0;
    48. }

  • 相关阅读:
    Spring Boot 教程:使用 Spring Boot 整合 JdbcTemplate
    推荐算法效果不佳时的检查清单
    正则表达式
    MySQL数据库 | 手把手教你如何去下载安装MySQL数据库
    离线数仓 (四) --------- 用户行为数据采集模块
    JavaScript 函数原理 +各种案例 + 使用场景
    【XGBoost】第 2 章:深度决策树
    2022下半年《软考-系统架构设计师》备考经验分享
    linux安装配置MongoDB并设置开机启动
    在word、ppt、excel编辑软件标题栏顶部左上角加入自定义功能:另存为、导出PDF
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/127816111