Linux——进程信号(二)

引言

在进程信号(一)中我们已经讲到了信号的保存,那么接下来要讲信号的处理了。

信号的处理主要要回答3个问题:

1.信号什么时候被处理的?

2.信号如何被处理的?

3.捕捉信号还有其他方式吗?

首先回答问题一:信号在合适的时候被处理,那什么是合适的时候呢?

这就要先看什么是内核态和用户态了

一、内核态和用户态

在这里先回答到底是什么时候:

进程从内核态切换回用户态的时候**(CPU中有寄存器来专门标识执行状态)****,信号会被检测并处理**

OS是一个进行软硬件资源管理的软件,它很容易就能获取到CPU中CR3寄存器中是0还是3,从而知道当前是用户态还是内核态

1.1内核态和用户态的概念

用户态:一种用来执行普通用户代码的状态,是一种受监管的状态

内核态:通常用来执行OS的代码,是一种权限非常高的状态

执行系统调用时,是由操作系统来执行的,而不是用户。那么有引出了一个问题:一个进程又是怎么跑到OS中执行代码的呢?

之前我们说地址空间[0,3]GB的用户空间,[3,4]GB是内核空间

用户所写的代码和数据位于用户空间为了保证进程的独立性,每个进程都有自己的进程地址空间,都有一个用户级页表

还有一份所有进程共用的内核级页表

进程地址空间的 3~4GB是不允许用户访问的,因为这1GB空间的代码和数据通过内核级页表和内存中的OS相映射。内存中只存在一份内核,所以所有进程的虚拟地址空间的那1G内核空间都通过同一份内核级页表映射到同一个内核

所以回答一下刚才的问题:进程又是怎么跑到OS中执行代码的呢?

1.进程使用系统调用或者访问系统数据,其实还是在进程的地址空间内进行跳转的

2.进程无论如何切换总能找到操作系统,我们访问操作系统本质就是通过进程的地址空间的[3,4]GB来访问

3.不同的进程指向同一张内核级页表,这样就都能进行系统调用

1.2内核态和用户态转化

用过系统调用->陷入内核->OS执行系统调用->结果给用户

在处理信号的过程(捕捉) 中,一共会有4次状态切换(内核/用户态)

  1. 执行 到用户代码中的系统调用函数时 ,会跳转到虚拟地址空间中的内核空间去执行对应的方法,此时陷入内核 ,状态从用户态变成内核态
  2. 当系统系统调用被执行完后,再返回之前,OS会检查task_struct中的两张位图表,然后根据handler表中的处理方式去处理相应的信号,如果此时是自定义的处理方式, 那么OS会用这handler表中此自定义函数的地址去执行用户自定义的处理方式 ,此时状态从内核态转化为用户态
  3. 在执行完信号处理函数 后,返回时会执行特殊的系统调用 ,再次进入内核,此时状态从用户态变成内核态
  4. 特殊系统调用执行完返回用户模式,返回用户代码原来的位置继续向下执行,此时状态从内核态转化为用户态

为什么我们在信号捕捉的时候,执行我们写的方法(自定义),还要从内核态切换到用户态?(用户态执行方法)

OS不信任(用户),还是让用户去(在外面)用系统调用安全些(万一你的方法时一些越权的非法操作呢,操作系统还要继续执行吗)

二、处理信号sigaction

信号的捕捉除了前面用过的signal函数之外,我们还可以使用sigation函数对信号进行捕捉

man sigaction

sigaction函数可以读取和修改与指定信号相关联的处理动作,该函数调用成功返回0,出错返回-1

signum:指定信号的编号

若act指针非空,根据act修改该信号的处理动作

若oldact指针非空,则通过oldact传出该信号原来的处理动作

act和oact指向sigaction结构体:

cpp 复制代码
struct sigaction
{
    void(*sa_handler)(int);
    void(*sa_sigaction)(int,siginfo_t *,void *);
    sigset_t sa_mask;
    int sa_flags;
    void(*sa_restorer)(void);
};

对于sigaction结构体第一个成员来说:

将sa_handler赋值为

SIG_IGN传给sigaction函数,表示忽略此信号

SIG_DFL,表示执行系统默认动作

一个函数指针,表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数

第二个成员是实时信号的处理函数,我们不讲

第三个:sa_mask

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

第四、五个不讲

sigaction的使用:

将2号信号自定义捕捉,打印信号编号,设置sa_mask当2号递达后会被阻塞

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

void Print(sigset_t &pending)
{
    cout<<"Pending bitmap: ";
    for (int signub = 31; signub > 0; signub--)
    {
        if (sigismember(&pending, signub))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

void sigcb(int signo)
{
    cout<<"signal:"<<signo<<endl;
    sigset_t pending;
    sigemptyset(&pending);
    while(1)
    {
        sigpending(&pending);
        Print(pending);
        sleep(1);
    }
}
int main()
{
    struct sigaction act,oact;
    act.sa_handler = sigcb;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(2,&act,&oact);
    while(true)
    {
        sleep(1);
    }    


    return 0;
}

三、可重入函数

可重入函数:被不同执行流重复进入不会产生问题的函数

以链表的插入为例,当main函数(主执行流)的insert插入执行一半时,又来一个新信号自定义处理方法中又调用了insert函数->该函数被重复进入了(main执行流、信号捕捉执行流)->导致产生了问题:该函数叫做不可重入函数(我们用到的大部分函数都是不可重入的)

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

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

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

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

volatile关键字

volatile是C语言中的一个关键字,它的作用是保持内存的可见性

cpp 复制代码
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int signo)
{
    cout<<"change flag:"<<flag;
    flag = 1;
    cout<<"->"<<flag<<endl;
}
int main()
{
    signal(2, handler);
    while(!flag);
    printf("process quit normal\n");
    return 0;
}

在接收到2号信号后,quit从0变成1,main函数正常结束,不在循环

编译器又是会做很多优化,g++编译器可以指定优化级别 -O3是最高的优化级别

cpp 复制代码
mytest:sigaction.cc
	g++ -o $@ $^ -std=c++11 -O3
.PHONY:clean
clean:
	rm -f mytest

这时重新编译

发送2好信号后进程没有退出。flag改为1,但是主执行流还在循环

当flag添加volatile关键字后

cpp 复制代码
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int signo)
{
    cout<<"change flag:"<<flag;
    flag = 1;
    cout<<"->"<<flag<<endl;
}
int main()
{
    signal(2, handler);
    while(!flag);
    printf("process quit normal\n");
    return 0;
}

进程正常结束了

SIGCHLD信号

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

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

请编写一个程序完成以下功能:父进程fork出子进程,子进程调用exit(2)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用wait获得子进程的退出状态并打印。

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

相关推荐
小糖学代码7 小时前
LLM系列:1.python入门:3.布尔型对象
linux·开发语言·python
shizhan_cloud7 小时前
Shell 函数的知识与实践
linux·运维
Deng8723473487 小时前
代码语法检查工具
linux·服务器·windows
霍夫曼9 小时前
UTC时间与本地时间转换问题
java·linux·服务器·前端·javascript
月熊10 小时前
在root无法通过登录界面进去时,通过原本的普通用户qiujian如何把它修改为自己指定的用户名
linux·运维·服务器
大江东去浪淘尽千古风流人物11 小时前
【DSP】向量化操作的误差来源分析及其经典解决方案
linux·运维·人工智能·算法·vr·dsp开发·mr
打码人的日常分享11 小时前
智慧城市一网统管建设方案,新型城市整体建设方案(PPT)
大数据·运维·服务器·人工智能·信息可视化·智慧城市
赖small强11 小时前
【Linux驱动开发】NOR Flash 技术原理与 Linux 系统应用全解析
linux·驱动开发·nor flash·芯片内执行
风掣长空12 小时前
Google Test (gtest) 新手完全指南:从入门到精通
运维·服务器·网络
IT运维爱好者13 小时前
【Linux】LVM理论介绍、实战操作
linux·磁盘扩容·lvm