【Linux】信号的处理

CSDN 2024-07-13 12:07:04 阅读 88

在这里插入图片描述

你很自由

充满了无限可能

这是很棒的事

我衷心祈祷你可以相信自己

无悔地燃烧自己的人生

-- 东野圭吾 《解忧杂货店》


信号的处理

1 信号的处理2 内核态 VS 用户态3 键盘输入数据的过程4 如何理解OS如何正常的运行5 如何进行信号捕捉6 信号处理的总结7 补充知识7.1 可重入函数7.2 volatile关键字7.3 SIGCHID信号

Thanks♪(・ω・)ノ谢谢阅读!!!下一篇文章见

1 信号的处理

处理信号本质就是递达这个信号!首先我们来看如何进行捕捉信号:信号的处理有三种:

<code>signal(2 , handler);//自定义

signal(2 , SIG_IGN);//忽略

signal(2 , SIG_DFL);//默认

注意handler表是函数指针表,传入的参数一定是函数指针类型!!!

我们说过:信号可能不会被立即处理,而是在合适的时候进行处理。那么这个合适的时候到底是什么时候?!

进程从内核态(处于操作系统的状态)返回到用户态(处在用户状态)的时候进行处理!

在这里插入图片描述

首先用户运行一个进程,在执行代码指令时因为中断,异常或者系统调用进如操作系统。进入操作操作系统就变为内核态,操作系统处理完之后,就对进程的三张表进行检查:如果pending中存在,继续判断,如果被block了了就不进行处理,反之执行对应方法!执行对应的方法时,如果是自定义方法,会返回到用户层面的代码,执行对应的方法。然后通过系统调用再次回到内核态。进入内核态之后,再返回到原本的用户指令位置中

注意:

操作系统不能直接转过去执行用户提供的handler方法!因为操作系统权限太高了,必须回到用户权限来执行方法!类似一个∞符号:

在这里插入图片描述

2 内核态 VS 用户态

再谈地址空间

在这里插入图片描述

这样无论进程如何切换,都可以找到OS!!!

所以我们访问OS,其实还是在我们的地址空间进行的,和访问库函数没有区别!OS不相信任何用户,用户访问[3 , 4]地址空间,要受到一定约束(只能通过系统调用!)

3 键盘输入数据的过程

操作系统如何知道我们按下键盘呢?肯定不能是每一时刻都进行检查,这样消耗太大!

在CPU中,键盘按下时会向cpu发送硬件中断,CPU就会读取中断号读到寄存器中,CPU会告诉OS,后续通过软件来读取寄存器。

内存中,操作系统在启动时就会维护一张函数指针数组(中断向量表),数组下标是中断号,数组内容是读磁盘函数,读网卡函数等方法。每个硬件都有自己的中断号,键盘也是。按下键盘时,向CPU发送中断信号,然后调用键盘读取方法,将键盘数据读取到内存中!这样就不需要轮询检查键盘是否输入了!

4 如何理解OS如何正常的运行

根据我们使用电脑的经验,电脑开机到关机的过程中,本质一定是一个死循环。那这死循环是如何工作的呢?那么CPU内部有一个时钟,可以不断向CPU发送中断(例如每隔10纳秒),所以CPU可以被硬件推动下在死循环内部不断执行中断方法。来看Linux内核:

在操作系统的主函数中,首先是进行一些初始化(包括系统调用方法),然后就进入到了死循环!

在这里插入图片描述

<code>操作系统本质是一个死循环 + 时钟中断 (不断调度系统任务)

那么系统调用时什么东西呢?

在操作系统内部,操作系统提供给我们一张表:系统调用函数表

在这里插入图片描述

平时我们用户层使用的<code>fork , getpid , dup2...等都对应到底层的sys_fork , sys_getpid ...。只有我们找到特定数组下标(系统调用号)的方法,就能执行系统调用了!

回到之前的函数指针数组,我们在这里再添加一个新方法,用来调度任何的系统调用。使用系统调用就要有:

系统调用号系统调用函数指针表(操作系统内部)

用户层面如何使用到操作系统中的函数指针表呢?

这就要回到CPU中来谈,CPU中两个寄存器,假设叫做X 和 eax,当用户调用fork时,函数内部有类似

mov 2 eax //将系统调用号放入寄存器中

而所谓的中断不也是让CPU中的寄存器储存一个中断号来进行调用吗!那CPU内部可不可以直接写出数字呢?可以,当eax获取到数字时,寄存器X就会形成对应的数字,来执行操作系统的系统调用。

通过这种方法就可以通过用户的代码跳转到内核,来执行系统调用。但操作系统不是不相信任何用户吗?怎么就直接跳转了呢?用户是无法直接跳转到内存中的内核空间(3~4GB)。那么就有几个问题:

操作系统如何阻止用户直接访问?系统调用最终是可以被调用的,又是如何做到的?

在操作系统中,解决这两种问题是非常复杂的!有很多概念,所以简单单来讲:做到这些需要硬件CPU配合,在CPU中存在一个寄存器code semgent记录代码段的起始与终止地址。就可以通过两个cs寄存器来分别储存用户与操作系统的代码!CS寄存器中单独设置出两个比特位来记录是OS还是用户,这样就要区分了内核态和用户态。运行代码时就会检测当前权限与代码权限是否匹配,进而做到阻止用户直接访问。而当我们调用系统调用(中断,异常)时,会改变状态,变成内核态,此时就可以调用系统调用

5 如何进行信号捕捉

今天我们来认识一个新的系统调用:

NAME

sigaction, rt_sigaction - examine and change a signal action

SYNOPSIS

#include <signal.h>

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

使用方法和signal很像,先介绍struct sigaction

struct sigaction {

void (*sa_handler)(int);

void (*sa_sigaction)(int, siginfo_t *, void *);

sigset_t sa_mask;

int sa_flags;

void (*sa_restorer)(void);

};

在这其中我们只需要注意 void (*sa_handler)(int);,这是个函数指针,就是自定义捕捉的函数方法。这样看来是不是就和signal很类似了

再来看看参数

int signum : 表示要对哪个信号进行捕捉const struct sigaction *act : 输入型参数,表示要执行的结构体方法struct sigaction *oldact: 输出型参数,获取更改前的数据

我们写一段代码来看看:

// 创建一个进行,进入死循环

// 对2号信号进行自定义捕捉

void handler(int signum)

{

std::cout << "get a sig : " << signum << " pid: " << getpid() << std::endl;

}

int main()

{

struct sigaction act, oact;

// 自定义捕捉方法

act.sa_handler = handler;

sigemptyset(&act.sa_mask);

act.sa_flags = 0;

sigaction(2, &act, &oact);

while (true)

{

std::cout << "I am a process... pid: " << getpid() << std::endl;

sleep(1);

}

return 0;

}

我们运行看看:

在这里插入图片描述

这样就成功捕捉了2号信号!用起来和之前的signal很类似!那么我们介绍这个干什么呢?我们慢慢来说:

首先信号处理有一个特性,比如我们在处理二号信号的时候,默认会对二号信号进行屏蔽!对2号信号处理完成的时候,会自动解除对2号信号的屏蔽!也就是操作系统不允许对同一个信号进行递归式的处理!!!

我们来简单验证一下:我们在handler方法中进行休眠,看看传入下一个2号信号是否会进行处理

<code>void handler(int signum)

{

std::cout << "get a sig : " << signum << " pid: " << getpid() << std::endl;

sleep(100);

}

来看:

在这里插入图片描述

可见进程就屏蔽了对2号信号的处理!

我们之前学习过三张表:阻塞,未决和抵达

既然操作系统对信号进行来屏蔽,那么再次传入的信号应该就会被记录到未决表(pending表)中,我们打印这个表来看看:

<code>

void Print(sigset_t &pending)

{

for (int sig = 31; sig > 0; sig--)

{

if (sigismember(&pending, sig))

{

std::cout << 1;

}

else

{

std::cout << 0;

}

}

std::cout << std::endl;

}

void handler(int signum)

{

std::cout << "get a sig : " << signum << " pid: " << getpid() << std::endl;

while (true)

{

// 建立位图

sigset_t pending;

// 获取pending

sigpending(&pending);

Print(pending);

}

}

来看:

在这里插入图片描述

可以看的我们在传入2号信号时就进入到了未决表中!处理信号完毕,就会解除屏蔽!

接下来我们既可以来介绍<code>sa_mask了,上面只是对2号信息进行了屏蔽,当我传入3号新号ctrl + \时就正常退出了,那么怎么可以在处理2号信号时屏蔽其他信号呢?就是通过sa_mask,将想要屏蔽的信号设置到sa_mask中,就会在处理2号信号的时候,屏蔽所设置的信号!

int main()

{

struct sigaction act, oact;

// 自定义捕捉方法

act.sa_handler = handler;

sigemptyset(&act.sa_mask);

//向sa_mask中添加3号信号

sigaddset(&act.sa_mask , 3);

act.sa_flags = 0;

sigaction(2, &act, &oact);

while (true)

{

std::cout << "I am a process... pid: " << getpid() << std::endl;

sleep(1);

}

return 0;

}

这样就也屏蔽了3号信号

在这里插入图片描述

当然如果把所有信号都屏蔽了,肯定是不行的,所以有一部分信号不能被屏蔽,比如9号信号永远都不能屏蔽!!!

6 信号处理的总结

对于信号我们学习了三个阶段:

信号的产生与发送:中断,异常,系统调用。信号的保存:三张表:阻塞,未决和递达信号的处理

7 补充知识

7.1 可重入函数

介绍一个新概念:可重入函数。

我们先来看一个情景:

在这里插入图片描述

这是一个链表,我们的inser函数会进行一个头插,头插会有两行代码:

<code>void insert(node_t* p)

{

p->next = head;

//------在这里接收到信号-----

head = p;

}

我们进行头插时,进行完第一步之后,突然来了一个信号,但是我们之前说过:信号处理时在用户态到内核态进行切换时才进行处理,这链表的头插没有进行状态的切换啊?其实状态的切换不一定只能是系统调用方法,在时间片到了(时钟中断)之后,也进行了状态的切换。

而且恰好,该信号的自定义捕捉方法也是insert这时就导致node2插入到了链表中,信号处理完之后,头指针又被掰到node1了,就造成node2丢失了(内存泄漏了)!!!

这就叫做insert函数被重入了!!!

在重入过程中一旦造成了问题,就叫做不可重入函数!!!(因为一旦重入就造成了问题,那当然不能重入了)

绝大部分函数都是不可重入函数!

7.2 volatile关键字

我们今天在信号的角度再来重温一下:

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

看这样一段代码:

#include <iostream>

#include <signal.h>

int flag = 0;

void changdata(int signo)

{

std::cout << "get a sig : " << signo << " change flag 0->1" << std::endl;

flag = 1;

}

int main()

{

signal(2 , changdata);

while(!flag);

std::cout << "process quit normal" << std::endl;

}

主函数会一直进行死循环,只有接收到了2号信号才会退出!

在这里插入图片描述

但当我们进行编译优化时(因为如果进程不接受到2号信号,那么flag就没有人来修改,编译器就认为没有任何代码对flag进行修改),共同有四级优化<code>00 01 02 03

而while(!flag)是一个逻辑运算,CPU 一般进行两种类别计算:算术运算和逻辑运算!会从内存进行读取,然后进行运算

g++ main main.cc -01

我们再次运行,却发现,进程不会结束了?!这是为什么!因为优化直接将数据优化到寄存中,因为编译器认为后续不会进行修改,所以寄存器中的值不会改变,程序只会读到寄存器中的值。所以就有了volatile关键字解决了这样的问题!!!

7.3 SIGCHID信号

我们之前学习过:父进程创建子进程之后,父进程需要进行轮询(阻塞轮询和非阻塞轮询)等待子进程退出,如果不等待,就会导致子进程没人管,造成僵尸进程,进而内存泄漏。那么子进程退出时是悄咪咪的退出吗?那肯定不是,子进程会向父进程发送SIGCHLD信号!!!

在这里插入图片描述

为什么之前父进程收到信号的时候,怎么父进程没有反应呢?因为SIGCHLD信号的默认操作是<code>IGN忽略而不是Term / Core ,我们来使用信号的自定义捕捉验证一下父进程确实收到了SIGCHLD

#include <iostream>

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

#include <sys/wait.h>

void handler(int signum)

{

// 捕捉

std::cout << "signum: " << signum << " --- pid :"<< getpid() <<" --- 捕捉 Sigchld 成功" << std::endl;

//收到信号才进行等待!

int status;

pid_t rid = waitpid(-1 , &status , 0 );

if(rid > 0)

{

//等待成功

std::cout << "wait childprocess success! pid:" << rid << std::endl;

}

return;

}

int main()

{

//自定义捕捉

signal(SIGCHLD, handler);

pid_t id = fork();

if (id == 0)

{

// child

int cnt = 5;

while (cnt)

{

std::cout << "I am ChildProcess , pid: " << getpid() << " ppid: " << getppid() <<" cnt: " << cnt << std::endl;

sleep(1);

cnt--;

}

exit(1);

}

// parent

// 进行等待子进程退出

// 自定义捕捉sigchld信号

while(true)

{

std::cout << "I am ParentProcess , I am doing other things " << std::endl;

sleep(1);

}

return 0;

}

来看运行效果:

在这里插入图片描述

可以看到父进程确实收到了<code>sigchld信号!!!收到信号父进程才进行等待捕获,这样父子进程就以弱耦合的方式进行联系。

这里有几个问题:

问题1 :上面我们只创建了一个子进程,那要是创建多个子进程呢?比如创建10个子进程,他们同时退出!那么在一个短时间内就会发送多次信号,而信号的保存是在pending位图中记录,那么就会同时收到10个信号!但是pending位图只能保存一个信号,这样就导致无法回收全部的子进程!那么一个如何解决呢?收到信号就循环检查所有退出的子进程:

void handler(int signum)

{

// 捕捉

std::cout << "signum: " << signum << " --- pid :" << getpid() << " --- 捕捉 Sigchld 成功" << std::endl;

// 收到信号才进行等待!

while (true)

{

int status;

pid_t rid = waitpid(-1, &status, 0);

if (rid > 0)

{

// 等待成功

std::cout << "wait childprocess success! pid:" << rid << std::endl;

}

else

{

std::cout << "wait childprocess done" << std::endl;

break;

}

}

return;

}

这样就可以了,可以对多个进程退出时进行回收!

在这里插入图片描述

问题2 :如果我们创建了10个进程,但是只有5个退出了,5个永远不退出呢???这时如果按照上面的方法就会造成父进程阻塞来等待(一直处于handler方法中),因为父进程不知道子进程会不会退出!这时选择非阻塞轮询(<code>WNOHANG)就好

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程!但是好像Sigchld默认操作就是IGN为什么还要单独设置!?其实OS内部的IGN与我们用户设置的IGN是不一样的!如果关心子进程的退出信息,就不要使用这种方式!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。