【Linux】进程信号

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

文章目录

  • 1. 引入:生活中的信号
  • 2.进程信号
    • 2.1 进程信号的理解
    • 2.2 进程信号的查看
  • 3. 信号产生
    • 3.1 按键产生信号
    • 3.2 系统调用产生信号
      • 3.2.1 kill
      • 3.2.2 raise
      • 3.2.3 abort
    • 3.3 硬件异常产生信号
    • 3.4 软件条件产生信号
    • 3.5 进程退出时的核心转储问题
  • 4. 信号保存/捕捉
    • 4.1 信号保存
      • 4.1.1 信号保存的数据结构
      • 4.1.2 内核数据结构的操作
    • 4.2 信号捕捉
      • 4.2.1 程序执行过程中内核态和用户态
      • 4.2.2 信号捕捉的流程
      • 4.2.3 信号捕捉的操作
  • 5. 可重入函数
  • 6. SIGCHLD信号

1. 引入:生活中的信号

在生活中,我们会有各种各样的信号出现,比如发令枪、红绿灯、闹钟等等。我们拿红绿灯举例:

  • 人为什么能够识别红绿灯信号?

    首先有一个前提条件是识别包括认识行为产生,通俗来说就是能够认识什么是红灯什么是绿灯,行为产生的意思就是知道红灯的时候要做什么,绿灯的时候要做什么。

  • 我们为什么能识别红绿灯呢?

    曾经有人教育过,让我们在大脑中记住了红绿灯的属性和对应的行为

  • 当信号到来的时候,我们不一定立马处理这个信号

    由于信号可以随时产生(异步的),此时我们可能做着更重要的事情

  • 信号到来的时候我们需要记住这个信号(时间窗口),确保将来信号会被处理

  • 处理信号会有一些处理动作,这些动作一般分为三个

    默认动作、忽略动作、自定义动作

2.进程信号

2.1 进程信号的理解

首先明确一个共识:信号是发给进程的

我们在刚接触进程的时候,了解过一个kill命令,可以通过kill命令向指定pid的进程发送9号信号,杀死进程

kill -9 $pid

所以我们知道,信号是发送给进程的

那么,如何将我们在上面讲的生活中的信号的概念对应到进程中?

进程是如何识别信号的? 认识+动作,进程需要知道每个信号是什么,知道每个信号对应的行为是什么

进程本身是由程序员编写的属性和逻辑的集合,是由程序员编码完成的,所以进程对信号和认识和对应的动作都是由程序员编码完成的

进程本身要对信号具有保存能力 在收到信号的时候,进程可能在执行更重要的任务,不一定立刻处理信号,所以进程本身要对信号具有保存能力。**那么信号被保存在哪里呢?**我们知道每个进程都有对应的PCB,在Linux下就是task_struct结构体中。**如何保存?**在task_struct中有位图结构保存了收到的信号,用比特位的位置代表信号的编号,比特位对应的值1,0分别代表是否收到该信号。

进程对信号的处理 进程对信号的处理有三种动作:默认、忽略、自定义 处理信号也可也被称作信号的捕捉

如何理解信号的发送 我们知道信号是保存在task_struct中的信号位图结构中的,所谓的发送信号,实际上就是修改进程task_struct中的信号位图结构的内容,我们知道task_struct是OS内核数据结构,只有OS有修改权限,所以只有OS有向进程发送信号的能力。所以无论我们未来学习多少种发送信号的方式,其本质都是通过OS向目标进程发送信号!

这里说的是只有OS有向进程发送信号的能力,不代表只有OS能够操作信号的发送。可以理解成我附庸的附庸不是我的附庸虽然我们没有办法越过OS直接向进程发送信号,但是我们能控制OS向进程发送信号,这个控制的方式就是系统调用,因此我们可以推测出之前使用的kill命令也是调用了对应的系统调用

2.2 进程信号的查看

我们可以使用kill -l指令查看系统定义的信号列表

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

OS一共定义了61个信号,没有0、32、33号信号

可以使用man 7 signal查看每个信号对应的含义

【Linux】进程信号_第2张图片

Linux提供了61个信号,其中[1,31]被称为普通信号,[34,64]被称为实时信号,其中实时信号这里不做讲解

31个普通信号的具体含义已经做了讲解,具体见博文:

信号的生命周期主要分为三个部分:信号产生、信号保存、信号处理

接下来,将会按照信号的生命周期的顺序来讲解信号

image-20240122231841420

3. 信号产生

3.1 按键产生信号

我们知道,在命令行下,如果一个前台程序出现了问题,可以使用CTRL+C终止掉这个程序,这实际上就是向这个前台程序发送2号信号。

在man 7 signal中的解释

Signal     Value     Action   Comment
──────────────────────────────────────────────────────────────────────
SIGINT        2       Term    Interrupt from keyboard

是由键盘产生的信号,值是2,信号是SIGINT,表示从键盘发送的中断的信号

所以当我们ctrl+c的时候该进程直接进入结束状态

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

键盘是硬件,通过组合键按下给OS识别,OS将组合键解释成信号,向目标进程发信号,目标进程在合适的时候处理这个信号

3.2 系统调用产生信号

我们在上面说到,只有OS有向进程发送信号的权限,所以所有外部向进程发送的信号都要通过系统调用,这里提供三个系统调用

3.2.1 kill

使用kill指令向任意进程发送任意信号

【Linux】进程信号_第4张图片

头文件:
#include 
#include 
函数原型:
int kill(pid_t pid, int sig);
参数解释:
	pid:表示发送信号的对象进程的pid
	sig:表示要发送的信号
返回值:
	调用成功返回0,失败返回-1同时设置错误码

实验:使用kill系统调用模拟实现kill指令

#include 
#include 
#include 

static void Usage(const char* cmd)
{
    std::cout << "\nUsage:" << cmd << " pid signo" << std::endl;
}

// ./mykill pid signo
int main(int argc, char* argv[])
{
    if(argc != 3) // 处理输入参数不对的情况
    {
        Usage(argv[0]);
        exit(1);
    }
    //转换第二个和第三个参数
    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);
    int ret = kill(pid, signo);
    if(ret != 0)
    {
        perror("kill:");
    }
    return 0;
}

【Linux】进程信号_第5张图片

发送了9号信号,进程被杀死

3.2.2 raise

使用raise给进程自己发送任意信号。raise是C语言提供的一个接口

【Linux】进程信号_第6张图片

头文件:
#include 
函数原型:
int raise(int sig);
参数解释:
	sig:表示向自己发送的信号
返回值:
	调用成功返回0,否则返回一个非0的值

可以使用kill模拟实现

// 下面两条代码含义相同
kill(getpid(), signo);
raise(signo);

3.2.3 abort

使用abort给进程自己发送6号信号

【Linux】进程信号_第7张图片

头文件:
#include 
函数原型:
void abort(void);
返回值:由于调用之后就会发送6号信号,中断进程,返回值无意义,所以返回值类型为void

可以使用kill或raise模拟实现

// 下面三条代码含义相同
kill(getpid(), SIGABRT);
raise(SIGABRT);
abort();

关于的行为的理解:有很多的情况进程收到大部分的信号,默认处理动作都是终止进程。

信号的意义:信号的不同代表不同的事件,都是对事件发生之后的处理动作是可以一样的。

3.3 硬件异常产生信号

信号的产生处理用户显示发送之外,在计算机内部也会自动产生,我们看一个例子

【Linux】进程信号_第8张图片

这里出现了除0错误,在CPU的运算器中有一个状态寄存器保存了运算结果的状态,当发生除0运算的时候,运算器产生了一个非常大的结果,这个结果会出现溢出,所以状态寄存器的溢出标记为就会被标记,向OS发送SIGFPE信号,表示浮点数运算异常

signal接口可以捕捉到任意信号,并将这个信号对应的行为自定义

【Linux】进程信号_第9张图片

头文件:
#include 
typedef void(*sighandler_t)(int);
函数原型:
sighandler_t signal(int signum, sighandler_t handler);
参数解释:
	sighandler_t:这是一个被typedef的类型,表示一个函数指针,这个函数的返回值是void,有一个int类型的参数
	signum: 表示要操作的信号编号
	handler:signum信号要绑定的行为的函数指针

捕捉信号的证明:

#include 
#include 
#include 

void catchSig(int signo)
{
    std::cout << "捕捉到一个信号,编号是:" << signo << std::endl;
    exit(-1);
}

int main()
{
    signal(SIGFPE, catchSig); // 把SIGFPE信号
    while(true)
    {
        std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl; 
        sleep(1);
        int a = 10;
        a /= 0;
    }
    return 0;
}

【Linux】进程信号_第10张图片

如果我们定义的catchSig函数中没有进程退出,那么会出现什么情况呢?

【Linux】进程信号_第11张图片

这个情况是因为我们说信号的发送是OS改变进程PCB中的信号位图,那么在我们自己实现的catchSig中没有让OS将这个信号位图恢复,所以OS在调度进程的过程中,会将这个进程重复调度,所以每次都会处理这个信号,就导致了刷屏这个现象


除了除0异常之外,还有空指针解引用等一系列的一场,我们可以再做一个实验来看看

#include 
#include 
#include 

void catchSig(int signo)
{
    std::cout << "捕捉到一个信号,编号是:" << signo << std::endl;
    exit(-1);
}

int main()
{
    signal(SIGSEGV, catchSig); // 把SIGFPE信号
    while(true)
    {
        std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl; 
        sleep(1);
        int* p = nullptr;
        *p = 10;
    }
    return 0;
}

image-20240124142214709

3.4 软件条件产生信号

1. 管道信号——SIGPIPE

我们在之前的文章中说到,进程间通信的管道,如果把所有的读端关闭,那么写端的所有写入都是没有意义的,所以如果所有读端都被关闭,那么OS就会向写端发送信号SIGPIPE,让写端关闭这个对应管道.

2. 定时器——14号信号SIGALRM

我们调用alarm系统调用,可以给OS设定一个闹钟,在时间到了之后,OS会给当前进程发送SIGALRM信号

【Linux】进程信号_第12张图片

头文件:
#include 
函数原型:
unsigned int alarm(unsigned int seconds);
参数解释:
	seconds:表示设定的闹钟时间,单位是秒
返回值:
	若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。如果调用alarm函数前,进程没有设置闹钟,则返回值为0

例如:我们可以使用这个系统调用辅助,来测试我们的机器1s能够进行多少次加法运算

#include 
#include 
#include 

int count = 0;

void catchSig(int signo)
{
    std::cout << "捕捉到一个信号,编号是:" << signo << std::endl;
    std::cout << "运算次数:" << count << std::endl;
    exit(-1);
}

int main()
{
    signal(SIGALRM, catchSig); 
    alarm(1);
    while(true) 
    {
        count++;
    }
    return 0;
}

image-20240124144400518

3.5 进程退出时的核心转储问题

进程在接收到绝大多数信号之后的行为都是终止进程,出现不同信号的原因是为了使对应信号产生不同的行为。在man 7 signal中看到信号对应的Action大多是Term和Core两种,那么这两种的区别是什么?

  • Trem:表示进程正常退出
  • Core:进程异常退出,并产生一些其他行为

但是,在云服务器上,如果进程是core退出的,我们暂时看不到明显现象,如果想看到的话,需要打开一个选项

【Linux】进程信号_第13张图片

可以看到,云服务器默认关闭了core file,可以使用ulimit -c 1024指令,在当前系统中形成最大可以为1024个blocks的数据块,保存core file文件。

在开启之前,我们运行下面一段程序:

int main()
{
    while(true)
    {
        int a[10];
        a[10000] = 10;
    }
    return 0;
}

image-20240124201940137

出现段错误,没有其他任何显示

开启corefile之后:多的文件的文件名为core,后缀名为引起core的进程的pid

【Linux】进程信号_第14张图片

所谓的核心转储就是当进程出现异常的时候,我们在进程对应的时刻,在内存中的有效箱数据转储到磁盘中

为什么要有核心转储?

为了把问题保存下来,支持调试

如何支持调试?

使用gdb

【Linux】进程信号_第15张图片

这种调试方法叫做事后调试

所以core和term的区别就是**通过core结束的进程可以支持生成核心转储文件**

4. 信号保存/捕捉

信号的一些相关概念:

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

4.1 信号保存

4.1.1 信号保存的数据结构

我们知道信号是需要被保存的,根据我们现有知识,可以推测出信号是保存在task_struct中的。实际上,在Linux下,有三个数据结构用于保存信号相关数据。

1. pending位图

pending位图结构的理解:信号被OS发送,被当前进程接收之后不会被立刻处理,所以需要进行保存,这里就使用pending位图保存。

pending位图的结构的定义是unsigned int pending,这个位图结构是一个32位的无符号整型。这个位图结构包括两个部分:1.每个数据位的位置2.每个数据位的内容0/1

其中位置表示的是不同的信号,数据位的内容0表示没有接收到该信号,1表示接收到该信号

2. block位图

block位图的理解:进程可以选择阻塞某个信号,阻塞的实现就是通过block位图来实现的,block的结构和pending的结构是一样的,是一个32位的无符号整型变量unsigned int block。只是对于位图结构的定义不同

1.每个数据的位置:表示的是不同信号

2.每个数据位的内容0/1。0表示该信号没有被阻塞,1表示该信号被阻塞

3. handler_t数组

handler_t数组的理解:每一个信号都需要有一个对应的行为(也就是对应的函数)这个数组有32个元素,从1开始的每个元素都代表了一个回调函数。

handler_t类型:typedef void(*handler_t)(int signo)

handler_t数组的定义:handler_t handler[32] = {0};

当进程准备处理信号的时候,block位图对应位置位0,pending位图对应位置位1,就调用对应的handler数组中的回调函数

【Linux】进程信号_第16张图片

4.1.2 内核数据结构的操作

我们在4.1.1中讲了pendign和block的数据类型是unsigned int,但是实际上他们的类型被封装过,封装后的体现是sigset_t类型,这是因为在Linux中除了我们今天讲到的31个普通信号之外,还有31个实时信号,为了支持这些内容和提供扩展,所以Linux封装了sigset_t类型,这个类型的实现最终是一个结构体

typedef struct
{
    unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef __sigset_t sigset_t;

上文中说到了PCB中保存信号的结构,那么对应的,这些数据结构也需要对应的操作方法。同时,上述的数据结构是内核的数据结构,程序员没办法直接操作这些数据结构,所以OS必定需要提供一些接口/系统调用让程序员能够操作这些数据。

【Linux】进程信号_第17张图片

头文件:
#include 
函数原型:
int sigemptyset(sigset_t *set);                   // 将set位图的所有位置0
int sigfillset(sigset_t *set);                    // 将set位图的所有位置1
int sigaddset(sigset_t *set, int signum);         // 在set位图中吧signum信号对应的比特位置1
int sigdelset(sigset_t *set, int signum);         // 在set位图中吧signum信号对应的比特位置0
int sigismember(const sigset_t *set, int signum); // 判断signum信号对应的比特位内容是0还是1
参数解释:
	set:需要操作的位图
	signum:需要操作的信号编号
返回值:
	调用成功返回0,否则返回-1,对于sigismember函数,若包含signum信号则返回1,不包含则返回0,出错返回-1

【Linux】进程信号_第18张图片

头文件:
#include 
函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数描述:
	读取/更改进程的信号屏蔽字,也就是block位图结构
参数解释:
	how:要如何操作信号屏蔽字,有三个模式:
        SIG_BLOCK,将set中包含的信号添加到当前信号屏蔽字中(block|set)。
        SIG_UNBLOCK,将set中包含的信号从当前信号屏蔽字中移除(block&~set)。
        SIG_SETMASK,将当前信号屏蔽字重置为set。(block=set)
	set:需要操作的信号位图
	oldset:执行操作前的信号屏蔽字结构的保存,是一个输出型参数
返回值:
	操作成功返回0,失败返回-1同时设置错误码

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

【Linux】进程信号_第19张图片

头文件:
#include 
函数原型:
int sigpending(sigset_t *set);
函数描述:
	读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程 序如下
参数解释:
	输出型参数,获取到的pending位图
返回值:
	如果调用成功就返回0,否则返回-1,同时设置错误码

4.2 信号捕捉

4.2.1 程序执行过程中内核态和用户态

在讲解信号捕捉流程之前需要加一点补充知识:这里对内核态和用户态进行一个简单的讲解

内核空间和用户空间

我们在进程地址空间的时候讲过对于任意一个进程,都有一个虚拟的进程地址空间大小为4G(在32位OS下)其中的高地址1G空间(0xC0000000到0xFFFFFFFF)保存内核数据。这个1G大小的空间就是内核空间,低地址的3G称为用户空间

内核级页表:我们之前同样说到,物理内存和进程地址空间通过页表联系起来,实际上页表也分为内核级页表和用户级页表,内核级页表就是进程地址空间的内核空间和物理内存的映射。

内核级页表是一个全局的页表,它用来维护操作系统的代码与进程之间的关系。因此,在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是一样的内容。

【Linux】进程信号_第20张图片

内核态和用户态

我们自己写的代码都是用户级代码,但是用户级代码难免会需要访问OS数据和OS管理的资源,比如getpid访问OS内的task_struct内容,printf访问OS管理的显示器资源,,用户自己写的代码为了访问资源必须直接或间接访问OS提供的接口,必须通过系统调用来完成访问。

系统在执行用户级代码的时候身份就是用户态,在执行内核代码的时候,身份就是内核态

从进程地址空间的角度来看,当执行系统内代码或访问系统内数据的时候,就是在进程地址空间中进行上下文查找。但是此时执行身份时用户态,所以在执行内核代码的时候需要首先进行身份转换,将用户态转变为内核态。

那么当前处于内核态还是用户态是怎么分辨的呢?

在CPU内部有一个CR3寄存器,保存当前运行身份,是用户态(11)还是内核态(00)

如何从用户态切换到内核态?

在x86下通过一个汇编指令:int $0x80,在arm下SWI将执行身份从用户态转换为内核态,这个过程我们称为陷入内核

注意:系统陷入内核的成本非常高

4.2.2 信号捕捉的流程

我们之前说过信号在被发送给进程之后不会被立刻处理,而是由OS在合适的时候进行处理,这个合适的时候实际上就是OS陷入内核之后处于内核态的时候,只有此时处于内核态,才有权力查看当前进程的信号保存的相关数据结构。信号捕捉的流程按照对应的信号处理动作分为两种情况

1. 信号处理动作是默认的或者忽略的

【Linux】进程信号_第21张图片

2. 信号的处理动作是自定义的

【Linux】进程信号_第22张图片

小技巧:可以使用数学中无穷的表达符号来记忆

【Linux】进程信号_第23张图片

为什么执行自定义信号处理函数的时候要转变身份为用户态呢?不是说身份转换成本很大吗?

当前如果处于内核态,当时是有权限执行自定义的信号处理函数的,但是OS不信任任何人,如果自定义的信号函数中存在恶意代码,OS是无法识别的。由于内核态的权限非常高,一旦出现恶意代码将会导致不可预知的后果,所以要从根本上杜绝这种情况的发生,就要让OS在执行用户级代码的时候一定不能是内核态

小实验:

#include 
#include 
#include 
#include 

static std::vector<int> blockSigs = { 2 };
const int MAX_SIGNUM = 31;

void show_pending(sigset_t pending)
{
    for(int signo = MAX_SIGNUM; signo >= 1; --signo)
    {
        if(sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

void myhandler(int signo)
{
    std::cout << signo << "号信号已经被递达" << std::endl;
}

int main()
{
    signal(2, myhandler);
    // 1.先尝试屏蔽指定信号集
    sigset_t block, oblock, pending;
    // 1.1 初始化信号集
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    // 1.2 添加要屏蔽的信号
    for(const auto& sig : blockSigs)
    {
        sigaddset(&block, sig);
    }
    // 1.3 开始屏蔽(设置进内核)
    sigprocmask(SIG_SETMASK, &block, &oblock);
    // sigprocmask(SIG_BLOCK, &block, &oblock);


    // 2. 遍历打印pending信号集
    int cnt = 0;
    while(true)
    {
        cnt++;
        // 2.1 初始化(这里初始化可以不做,因为获取是覆盖式的)
        sigemptyset(&pending);
        // 2.2 获取pending
        sigpending(&pending);
        // 2.3 打印结果
        show_pending(pending);
        sleep(1);
        if(cnt == 10)
        {
            std::cout << "取消屏蔽" << std::endl; // 一旦对特定信号进行屏蔽解除,那么一般OS至少要立马递达一个信号
            sigprocmask(SIG_SETMASK, &oblock, &block);
        }
    }
}

【Linux】进程信号_第24张图片

4.2.3 信号捕捉的操作

1. 使用系统调用signal捕捉任意信号,设置任意的回调函数,当捕捉到设定的信号之后,会调用指定的回调函数

signal函数的使用方法我们在上文的硬件信号的证明的时候讲解使用过,这里就不过多赘述

2. 使用系统调用sigaction捕捉,对特定信号设置特定的回调方法

【Linux】进程信号_第25张图片

头文件:
#include 
函数原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数解释:
	signum:需要设置信号编号
	act:输入型参数,被设置信号对应的相关信息结构体
	oldact:输出型参数,保存之前的信息
返回值:
	调用成功返回0,否则返回-1同时设置错误码

这里出现了一个结构体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); // 跟普通信号无关,我们暂时不关心
};

一些补充:

  • 对于sa_handler参数,可以为SIG_IGN表示忽略该信号,可以为SIG_DFL表示执行系统默认动作,可以是一个函数指针,表示自定义的捕捉行为
  • 对于sa_mask参数:首先需要说明的是,当某个信号的处理函数被调用,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时,自动恢复原来的信号屏蔽字。

例如,下面我们用sigaction函数对2号信号进行了捕捉,将2号信号的处理动作改为了自定义的打印动作,并在执行一次自定义动作后将2号信号的处理动作恢复为原来默认的处理动作。

#include 
#include 
#include 
#include 

struct sigaction act, oact;
void handler(int signo)
{
	printf("get a signal:%d\n", signo);
	sigaction(2, &oact, NULL);
}
int main()
{
	memset(&act, 0, sizeof(act));
	memset(&oact, 0, sizeof(oact));

	act.sa_handler = handler;
	act.sa_flags = 0;
	sigemptyset(&act.sa_mask);

	sigaction(2, &act, &oact);
	while (1){
		printf("I am a process...\n");
		sleep(1);
	}
	return 0;
}

运行代码后,第一次向进程发送2号信号,执行我们自定义的打印动作,当我们再次向进程发送2号信号,就执行该信号的默认处理动作了,即终止进程。

【Linux】进程信号_第26张图片

5. 可重入函数

一般而言,我们认为main执行流和信号捕捉执行流是两个执行流!

如果在main中和在handler中,该函数被重复进入,此时出问题,则该函数(比如insert)称为不可重入函数

如果在main中和在handler中,该函数被重复进入,此时不出问题,则该函数(比如insert)称为可重入函数

我们遇到的绝大多数函数都是不可重入函数

【Linux】进程信号_第27张图片

main函数调用insert,向链表head插入Node1,insert只做了第一步,然后就被中断(或者因为信号原因执行信号捕捉),此时进程挂起,然后唤醒在次回到用户态检查有信号待处理,于是切换到sighandler方法,sighandler也调用了insert函数,要把Node2头插到链表里,Node2的next结点指向下一个结点位置,下一步就是head指向Node2,完成Node2的头插,信号捕捉完之后就成功把Node2插入,接下来回到main执行流,对Node1完成插入的第二步动作,此时把head指向Node1,最后只有Node1真正插入到链表之中,而Node2结点找不到了,发生内存泄漏,出现问题。

不可重入函数的判定方式

如果一个函数符合下列条件之一的,就是不可重入函数

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

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

6. SIGCHLD信号

子进程退出时,会向父进程发送17号信号SIGCHLD

证明:

#include 
#include 
#include 
#include 
void handler(int signo)
{
    printf("pid:%d, %d 号信号,正在被捕捉!\n",getpid(),signo);
}
int main()
{
    signal(SIGCHLD,handler);//17号信号
    printf("我是父进程:%d,ppid:%d\n",getpid(),getppid());
    pid_t id = fork();
    if(id==0)
    {
        printf("我是子进程:%d,ppid:%d,我要退出了\n",getpid(),getppid());
        exit(1);
    }
    while(1)
        sleep(1);
    return 0;
}

image-20240126032347203

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

signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);

注意:虽然SIGCHLD的默认动作就是忽略,但是与手动设置表现的不一样,默认是收到信号就进行处理,该等还得等,而如果我们手动设置了SIG_IGN,子进程退出时发送给父进程的信号会被父进程忽略,但是子进程会被OS回收,这是有所区别的。含义不一样


本节完…

gno)
{
printf(“pid:%d, %d 号信号,正在被捕捉!\n”,getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);//17号信号
printf(“我是父进程:%d,ppid:%d\n”,getpid(),getppid());
pid_t id = fork();
if(id==0)
{
printf(“我是子进程:%d,ppid:%d,我要退出了\n”,getpid(),getppid());
exit(1);
}
while(1)
sleep(1);
return 0;
}


[外链图片转存中...(img-Ml3visS3-1706210780067)]

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

~~~c
signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);

注意:虽然SIGCHLD的默认动作就是忽略,但是与手动设置表现的不一样,默认是收到信号就进行处理,该等还得等,而如果我们手动设置了SIG_IGN,子进程退出时发送给父进程的信号会被父进程忽略,但是子进程会被OS回收,这是有所区别的。含义不一样


本节完…

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