Linux——进程信号(下)

简介: Linux——进程信号(下)

信号的处理

那么,从内核态返回用户态的时候,才会进行信号处理,也就是说很可能进行了系统调用或者是进程切换(进程切换需要进程切换到内核态,因为进程被切换的时候一定没有被执行完,放在运行或者是等待队列的时候一定就要切换到内核态,然后再继续调度下面代码的时候就要切换回用户态)

sigset_t

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

pengding位图和block位图的统一类型就是sigset_t,是为了更方便用户,定义的用级数据结构的类型。

一般将block信号集叫做信号屏蔽字

信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

对于信号集位图不要私自修改,要用对应的接口。

#include <signal.h>

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);//判断一个信号是否在这个信号集中

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>

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

返回值:若成功则为0,若出错则为-1

第一个参数是下面这些选项。

第三个选项是重置信号屏蔽字。

第二个参数是你要修改的位图结构,也就是信号集。

第三个参数是第二个参数修改之前的信号集。(输出行参数)

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

sigpending

这个函数参数是一个输出型参数,在哪个进程调用就返回哪个进程的pengding位图。

返回成功0,失败-1。

对于信号保存更深入的理解

这里用起来上面介绍的接口,然后来写一段程序。

条件:

先屏蔽2号信号,发送一个信号2,在发生2号信号之前打印出pengding位图,发送之后再次打出pengding位图

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;
#define BLOCK_STGNAL 2
void show_pending(const sigset_t& pengding)
{
    for(int i = 31; i >= 1; i--)
    {
        if(sigismember(&pengding, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << "\n";
}
int main()
{
    //1.屏蔽指定信号
    sigset_t block, oblock, pengding;
    //初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pengding);
    //屏蔽
    sigaddset(&block, BLOCK_STGNAL);//添加屏蔽的信号
    sigprocmask(SIG_BLOCK, &block, &oblock);//正式屏蔽,这里才是真正通过OS设置进当前进程的PCB中
    //2.打印pengding信号集
    int count = 5;
    while(true)
    {
        sigpending(&pengding);//获取他
        show_pending(pengding);
        sleep(1);
        //3.解除信号的屏蔽
        if(count-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block);
            cout << "不屏蔽信号" << endl;
        }
    }
    return 0;
}

我们发现,如果一旦解除信号屏蔽,进程立刻就会退出,后续的代码不会被执行。

因为一旦信号屏蔽解除,一般OS要立马递达一个信号。(处理完一个信号,该比特位立刻清零)

sigaction

这个函数和signal函数差不多,第一个参数是对于该信号进行捕捉,第二个参数是一个结构体对象指针,传入的就是结构体的对象;

第一个成员是对于处理这个信号的方法。

第三个成员是信号集。

也就是说第二个参数是要对于该信号做一些列结构体中内容的设置的,是一个输入性参数。

第三个参数是一个输出型参数,获取对应信号老的处理方法。

成功返回0,失败返回-1。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;
void handler(int sig)
{
    cout << "get a signo" << sig << endl;
    sleep(10);
}
int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT, &act, &oact);
    while(true) sleep(1);
    return 0;
}

第一次立刻打印,第二次和第三次只打印了一次,两次我一起按的,但是打印出来的结果只有一个,这是为什么呢?

当我们进行正在递达第一个信号期间,同类型信号无法被递达,因为当前信号正在被捕捉,系统会自动将当前信号加入到该进程的信号屏蔽字。

当信号完成捕捉动作时,OS又会自动解除对该信号的屏蔽。

上面的现象可以这样解释,2号比特位被第一次置为1的时候,相对应的block位图2号也被置为了1,那么处理这个2号信号的时候,pengding位图对应的比特位又被置为0了,但是紧接着又来了一个2号信号,该比特位又变成了1,最后又来了一个2号信号,这个时候就不会再让pengding为途中2号信号中的比特位继续改变了,因为已经没有能力保存了。

在一个信号被解除屏蔽的时候,会自动递达当前屏蔽信号,没有就不做任何动作。

也就是说我们进程处理信号的原则是串行的处理同类型的信号,不允许递归。

那么,刚才这段代码这里:

当我们正在处理某一种信号的时候,我们也想顺便屏蔽其他信号,就可以添加到这个sa_mask中。

让上面的也屏蔽3号信号试一下。

这里退出的原因是什么呢?

因为是同时屏蔽2,3信号,第一次发送的也是2号信号,在处理2号信号的时候会同时屏蔽2号和3号信号,所以3号不会被立刻递达,因为是先发的2号信号,3号信号先不会处理,处理完前面两个2号信号之后才会解除对2号和3号的屏蔽,因为3号默认动作是退出,所以3号递达程序也就退出了。

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

可重入函数

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

这就会导致一个结果,node2就会数据丢失。

1.一般来说,mina执行流和信号捕捉执行流是两个执行流。

2.如果在main中和handler中,该函数被重复进入,出问题,insert函数就是不可重入函数。

3.如果在main中和handler中,该函数被重复进入,没出问题,insert函数就是可重入函数。

上面的例子,insert就是不可重入函数。

其实大部分函数都是不可重入的,这是一个特性。

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

调用了malloc或free,因为malloc也是用全局链表来管理堆的。

调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile

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

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int quit = 0;
void handler(int signo)
{
    printf("信号捕捉成功->\n");
    printf("quit:%d\n",quit);
    quit = 1;
    printf("quit变化之后->%d\n",quit);
}
int main()
{
    signal(2, handler);
    while(!quit);
    printf("正常退出\n");
    return 0;
}

在gcc编译器有个优化的选项是O3,再来看一下优化之后的效果:

这里进程并没有正常退出,这是为什么呢?

这里和优化是有关系的:

在循环这里,CPU从内存当中拿数据进行分析,但是并没有写回去。

上面说过,mian执行流和信号捕捉执行流是两个执行流,在没有进行优化的时候,捕捉到信号执行信号的动作就到了捕捉信号的执行流,将quit变成1之后返回到了main的执行流。然后CPU做出处理判断循环条件为假就跳出了循环。

那么优化之后,因为quit在main执行流没有被改变,所以编译器就认为quit没必要进行后续的判断,所以就将quit的值放进了编译器的内存里面,也就是说它的值已经无法被用户去改变了。所以这里判断的是CPU中寄存器最开始储存的那个值,就算信号捕捉执行流去改变,但是也不会影响CPU中寄存器的值。

那么这个时候怎么办呢?又想优化又不想出现这种情况,这个时候就需要加volatile关键字了。

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

SIGCHLD信号

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

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

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdlib>
using namespace std;
void handler(int sig)
{
    cout << "捕捉到信号" << endl;
    //以下是伪代码
    /*while(1)
    {
        pid_t ret = waitpid(-1, NULL, WNOHANG);//这里不能阻塞,万一只有一部分子进程退出就不好办了,这就是阻塞式调用了
        if(ret == 0) break;
    }*/
}
int main()
{
    signal(SIGCHLD, handler);
    cout << "父进程:" << getpid() << endl;
    pid_t id = fork();
    if(id == 0)
    {
        cout << "子进程:" << getpid() << "父进程:" << getppid() <<endl;
        sleep(5);
        exit(1);
    }
    while(true) sleep(1);
    return 0;
}

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

这里子进程退出也没留下任何痕迹。

还有一个细节:

明明对于17号信号处理就是”忽略“嘛?

但其实我们默认设置和手动设置的是不一样的。

因为OS会识别,如果是手动设置的,就会修改未来创建子进程的时候的退出的属性等等。

相关文章
|
8月前
|
安全 Linux
【Linux】阻塞信号|信号原理
本教程从信号的基本概念入手,逐步讲解了阻塞信号的实现方法及其应用场景。通过对这些技术的掌握,您可以更好地控制进程在处理信号时的行为,确保应用程序在复杂的多任务环境中正常运行。
288 84
|
7月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
279 67
|
6月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
188 16
|
6月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
135 20
|
5月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
119 0
|
5月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
169 0
|
5月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
118 0
|
5月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
139 0
|
8月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
310 4
|
8月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。

热门文章

最新文章