Linux中的信号

目录

生活中的信号

Linux中的信号

前台进程与后台进程

信号的产生

核心转储 core dump

​编辑信号的其他相关概念

信号处理的三种方式

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

sigset_t 类型

信号集操作函数

sigprocmask

sigpending

综合练习

用户态与内核态

信号的捕捉过程

sigaction函数

可重入函数

volatile关键字

SIGCHLD信号


生活中的信号

● 信号没有产生时,我们就知道如何处理对应的信号

比如你现在在家里,但你仍然知道红灯停,绿灯行

● 信号的产生是异步的,也就是我们不知道信号什么时候到来

比如你点了外卖后开始打游戏,打游戏期间你也不知道什么时候外卖员会给你打电话

● 信号产生了,可以不立即处理,而是在合适的时候处理

比如电话通知你外卖到了,你可以暂时不取,等打完游戏再取外卖

● 信号产生后可以稍后处理,这就要求你必须要有暂时保存信号的能力

比如电话通知你外卖到了,你可以暂时不取,你的大脑中已经保存了外卖到了的信息,你是可以稍后取外卖的

Linux中的信号

在OS中,上述的"我,你,我们"本质都是进程,信号在OS中就是一种向目标进程发送通知消息的一种机制!

在进程被设计的时候,源代码中就内置了很多对信号的识别和处理动作,因此进程没有收到任何信号之前,进程就认识所有信号,并且知道当信号到来时应该如何处理对应的信号

● kill -l 查看系统中的信号

其中,每个信号都有数字和大写的英文单词,显然是宏定义,数字就是信号的编号!

普通信号从 1-31,  实时信号从34-64, 没有0号信号,可以理解为0表示进程没有收到任何信号,也就是进程代码是正常执行完毕的!本篇博客只介绍普通信号!

● 系统中每个进程都维护了一张函数指针数组表,数组下标和信号编号强相关,数组内容就是相应信号的处理方法,因此进程知道如何处理相应的信号!

前台进程与后台进程

前台进程只能有1个,因为键盘只有1个,能接收键盘输入的必然是前台进程

后台进程可以有多个,一般耗时任务比较长的任务会放到后台执行,比如下载任务

前台进程可以接收键盘输入,可以被ctrl + c 终止掉,此时bash是后台进程,无法执行其他指令

后台进程不能接收键盘输入,无法被ctrl + c 终止掉,此时bash是前台进程,可以执行其他指令

● 当前台进程(bash除外)被杀掉之后, bash会自动把自己提到前台

● 前台进程如果被暂停(ctrl+z),会立刻变为后台进程

启动前台进程:  ./process, 启动后台进程: ./process &

● jobs指令: 查看系统中已经启动的任务

● fg 任务编号,将后台任务提为前台任务

● bg 任务编号,启动后台暂停的任务

信号的产生

OS如何知道键盘有数据了?

● OS是硬件的管理者,硬件有很多,如果OS一直轮询(比如每隔一段时间)检测硬件的状态,时间间隔长了OS不能及时得到有效信息,时间间隔短了OS太忙了,显然不现实!

● OS在设定时,就用到了中断技术: cpu内部除了有运算器,还有控制器,OS不直接和外设在数据层面上打交道,但是要和外设在控制信息层面上打交道!

● cpu上有针脚(有编号), 和主板的硬件电路直接来连接,主板和外设直接连,外设可以向cpu发送光电信号,这种信号就叫做中断信号,而针脚编号叫做中断号!

● 外设向cpu发送的中断信号会被存放在cpu寄存器中,就可以被程序读取了,就把硬件信息转化成了软件信息

● 为了对外部的中断信号做出相应,系统中存在一张中断向量表,本质是函数指针数组,数组下标是中断号,数组内容是相应硬件的读取方法

● 硬件中断信号的发出对于OS是异步的,而信号本质其实就是用软件来模拟中断的行为

如何理解向目标进程发送信号?

● 一个进程可能会收到多个信号,因此进程要对多个信号进行管理

● 管理信号的两个核心问题:是否收到了信号,收到了几号信号

● 显然task_struct中只需要维护一张位图即可,比特位的位置表示信号编号,比特位的内容表示是否收到了对应信号,由于只有31个普通信号,只需要一个32位的整数充当位图即可

● 发送信号本质是将位图对应的比特位由0改1,因此发信号本质是写信号

● OS是进程的管理者,无论是何种方式产生的信号,最终都是OS向目标进程发信号

信号产生的三种方式

1.键盘产生

● 键盘输入的数据分两种,一种是常规输入数据,一种是控制数据(如组合键,表示向进程发信号)

● 组合键表示向进程发送对应的信号,如 ctrl + c == 2号信号,ctrl + z == 19号信号,ctrl + \ == 3号信号

● 我们可以使用signal函数修改信号的默认处理行为,同时可以验证键盘组合键等同于发信号

sighandler_t signal(int signum, sighandler_t handler);

功能: 将编号为signum的信号的默认处理动作改为自定义处理行为(又叫信号捕捉,handler方法)

参数: signum表示信号编号,handler是信号的自定义处理行为

返回值: 返回的是本次修改为自定义处理行为前的信号处理方式

#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;

void func(int signo)
{
    cout << "收到了2号新号" << endl;
    exit(1);
}


int main()
{
    signal(2, func); 
    while(true)
    {
        cout << "I am running..., pid :" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

● 9号信号被称为管理员信号,是不能被自定义捕捉的,否则一个进程对所有信号自定义捕捉后就永远无法被终止了!

#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;

void func(int signo)
{
    cout << "收到了9号信号" << endl;
    exit(1);
}

int main()
{
    signal(9, func); 
    while(true)
    {
        cout << "I am running..., pid :" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

● man 7 signal 查看信号的默认处理动作

2.系统调用/函数产生

kill系统调用:

int kill(pid_t pid, int sig);

功能: 向pid进程发送sig信号

参数: 进程pid, 信号编号sig

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

#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
#include <cstring>
#include <unistd.h>
using namespace std;

static void Usage(const string& proc)
{
    cout << "\nUsage: " << proc << " -sigNumber processId" << endl << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    int sigNumber = stoi(argv[1] + 1);
    int processId = stoi(argv[2]);
    kill(processId, sigNumber);
    return 0;
}

raise函数:

int raise(int sig);

功能: 哪个进程调用raise,就向哪个进程发送sig信号

参数: 信号编号sig

返回值: 成功返回0,失败返回非0

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
}

int main()
{
    signal(2, handler);
    while(1)
    {
        raise(2);
        sleep(1);
    }
    return 0;
}

abort函数:

void abort(void);

功能: 哪个进程调用abort函数,直接收到 SIGABRT 信号,直接终止当前进程

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
}

int main()
{
    signal(6, handler); 
    abort(); //即使6号信号被捕捉了,进程也会被abort直接终止掉!
    while(1)
    {
        cout << "I am running" << endl;
        sleep(1);
    }
    return 0;
}

 

3.异常

● cpu中有一个状态寄存器,当发生除零异常,状态寄存器中的溢出标志位从0变1,cpu显示异常

● os是硬件的管理者,当os发现cpu异常,会立即发信号杀掉引起异常的进程

● 杀掉进程后,状态寄存器的溢出标志位就会从1变0,cpu恢复正常了

验证除零异常会收到8号信号

#include <iostream>
#include <sys/types.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
    sleep(1);
    //exit(0);
}

int main()
{
    signal(8, handler); //捕捉除0异常
    int a = 10;
    a /= 0; //不能直接除0,是数学的规定
    return 0;
}

● 我们对8号信号进行了自定义捕捉,因此发生除0错误时会执行自定义handler方法

● 如果handler中加了exit函数,执行handler方法后进程会立即退出,这是意料之中

● 但如果handler中没有exit函数,最后会死循环执行打印语句

● 当进程发生除0异常,cpu的状态寄存器的溢出标志位从0变1,会通知os自己出异常了,os会处理异常,而我们对信号处理方法进行了自定义捕捉,因此os在屏幕上打印了一句话就认为自己处理完了,实际上cpu的状态寄存器的溢出标志位仍然是1,当调度到该进程时,依然处于异常状态,循环往复

验证段错误异常会收到11号信号

● 段错误本质是一种越界行为,比如空指针解引用,0号虚拟地址空间对应的物理空间并没有分配给用户,当用户对空指针解引用时,会去访问0号虚拟地址空间对应的物理空间,此时MMU硬件单元会转化出错,通知os,os向进程发送信号

#include <iostream>
#include <sys/types.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
    sleep(1);
    //exit(0);
}

int main()
{
    signal(11, handler); //捕捉段错误异常
    int* p = nullptr;
    *p = 2;
    return 0;
}

4.软件条件

● 匿名管道通信时,读端关闭,写端还在写,OS检测到读端文件描述符已经关闭了,就会直接向写端发送SIGPIPE信号杀掉写端进程,此时信号的产生的来源是软件条件!

● 不是所有的信号都是软硬件异常产生的!还有让进程暂停、继续等其他事情,说明还有非异常问题也会产生信号!

● Linux中的闹钟本质就是非异常问题产生的信号

OS中的时间

● 电脑一开机就知道时间,是因为纽扣电池在固定时间间隔给计算机硬件发信号,记录时间戳

● 电脑用上多年,时间可能不太准了,意味着纽扣电池寿命不久了

其他进程都是被OS调度的,那么OS开始是如何跑起来的呢?

● OS的本质就是一个死循环 while(1) { pause() }

● CMOS硬件周期性的,高频率的,向cpu发送时钟中断(也就是cpu的主频),os会去中断向量表中找到对应CMOS中断号的方法,也就是os的调度方法: while(1) { pause() }

● 所以说OS的执行是基于硬件中断的!

OS中的闹钟

● 闹钟在任何OS天然就是要被支持的,而OS内可以设置多个闹钟,  要把闹钟管理起来,先描述,再组织, 定闹钟,就创建一个内核数据结构对象 struct clock { ... }

● 结构体中有什么字段?  比如闹钟响的时间;要能够很好进行时间比对,比如时间戳! 闹钟id, 用户名, 用户id, 什么时间点设置的,闹钟响了向谁发信号等等

● 如何组织设置的闹钟, 如何知道哪些超时了?  只需要按时间为键值建立大/小堆,对闹钟的管理就变成了对数据结构堆的增删改查

alarm系统调用:

unsigned int alarm(unsigned int seconds);

功能: 设置一个闹钟,闹钟响之后,进程会收到SIGALRM信号,进程终止

参数: 闹钟在seconds秒之后响

返回值:  在本次调用alarm之前,没有设置过闹钟,则返回0;如果已经设置过闹钟,则旧的闹钟会被新的闹钟取代,并返回设置过的闹钟的剩余时间;如果参数传0,代表取消历史闹钟,并返回历史闹钟的剩余时间

对比访问外设和访问内存的速度差异

第一份代码访问了外设, 而第二份代码没有访问外设,因此速度差异非常明显!

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;

int cnt = 0;

int main()
{
    alarm(1);
    while(true)
    {
        cout << "alarm : " << cnt++ << endl;
    }
    return 0;
}

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;

int cnt = 0;

void handler(int signo)
{
    cout << "get a signal : " << signo << ", alarm : " << cnt++ << endl;
    exit(0);
}

int main()
{
    signal(14, handler);
    alarm(1);
    while(true)
    {
        cnt++;
    }
    return 0;
}

定一个闹钟只响一次:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;

int n = 0;

void handler(int signo)
{
    n = alarm(0); //设置0就是将历史闹钟取消掉, 历史闹钟如果还没响,就返回历史闹钟剩余时间
    cout << "result: " << n << endl;
    exit(0);
}

int main()
{
    signal(14, handler);
    cout << "pid : " << getpid() << endl;
    alarm(15); //闹钟15秒后响起
    while(true)
    {
        sleep(1);
        cout << "running ..." << endl;
    }
    return 0;
}

定一个闹钟每隔2秒响一次:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
using namespace std;

int n = 0;

void handler(int signo)
{
    alarm(2); //2秒之后响
    cout << "result: " << n << endl;
}

int main()
{
    signal(14, handler);
    cout << "pid : " << getpid() << endl;
    alarm(2); //闹钟2秒后响起
    while(true)
    {
        sleep(1);
        cout << "running ..." << endl;
    }
    return 0;
}

核心转储 core dump

● 当进程收到信号终止运行后,会将当前进程的核心上下文数据写到一个磁盘上的临时文件,这个磁盘上的临时文件就是核心转储文件

● 核心转储文件的名字一般是 进程pid.core 或 core.进程pid

● 云服务器下默认是把core dump选项关闭的,否则进程一旦异常终止就会形成核心转储文件,会占据较大的磁盘空间

● gdb core.pid 可以很好的排查定位出错位置和出错原因

信号的其他相关概念

信号递达: 实际执行信号的处理动作

信号未决: 信号从产生到递达之间的状态

信号阻塞:  信号是允许被阻塞的,阻塞的意思是暂时不进行递达,直到解除对信号的阻塞才能抵达该信号,  信号被阻塞时处于未决状态!

ps1: 注意区分信号忽略和信号阻塞,忽略本身就是对信号做了处理,处理方式就是忽略,也就是忽略信号是递达了的!而信号阻塞是进程主动屏蔽了特定信号,所以信号产生后就无法被递达,直到解除屏蔽才能被递达

ps2: 信号是未决的,不一定被阻塞了,也可能是没来的及处理, 而信号被阻塞了,一定是未决的

信号处理的三种方式

1.默认动作

2.自定义捕捉

3.忽略处理

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

void handler(int signo)
{
    cout << "handler :" << signo << endl; 
}

int main()
{
    cout << "getpid :" << getpid() << endl;
    cout << "自定义行为" << endl;
    signal(2, handler); //自定义
    sleep(5);
    cout << "恢复默认行为" << endl;
    signal(2, SIG_DFL); //恢复默认处理动作

    sleep(10);
    return 0;
}

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void handler(int signo)
{
    cout << "handler " << signo << endl;
} 

int main()
{
    cout << "getpid: " << getpid() << endl;
    signal(2, handler); 
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigaddset(&block, 2); //将2号信号添加到自己定义的block信号集中
    sigprocmask(SIG_BLOCK, &block, &block);
    while(true)
    {
        sleep(1);
    }
    return 0;
}

总结

信号的默认处理和忽略动作,第二个参数都是大写的英文单词,本质就是宏,将0/1强制类型转化成函数指针类型,所以使用signal函数时,系统会先判断参数是否为0/1,如果为0/1,执行默认处理动作/对信号进行忽略,不为0/1,才去执行回调函数,进行自定义捕捉!

#define	SIG_DFL	 ((__sighandler_t)  0)	/* Default action.  */
#define	SIG_IGN	 ((__sighandler_t)  1)	/* Ignore signal.  */

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

每个进程都维护了三张表,block表和pending表是位图表,handler表是函数指针数组,三张表的下标表示信号编号(下标从0开始,信号编号从1开始,对应加1减1即可)

● 三张表的含义:

pending位图表:发信号本质是修改pending位图,对应位置从0改1

handler表: 是一个函数指针数组,保存的是对对应信号的处理方法

block位图表: 是否对特定的信号进行屏蔽(阻塞)

● 进程启动时,pending表和handler表就被设置好了,利用pending表和handler表,我们就可以识别并且处理信号了

● 具体到1个信号,三张表应该横着看,比如:

第一行:  没有收到1号信号,对1号信号没有屏蔽,处理行为是默认动作

第二行:  收到了2号信号,对2号信号屏蔽了,尽管处理动作是忽略,但由于信号阻塞,不会处理

第三行:  没有收到3号信号,对3号信号屏蔽了,即便将来收到了3号信号,也不会进行处理

● 能不能执行handler表中对应的方法,最终还是要看block表中的信号是否被阻塞,而最终执行了handler表中的方法后,pending位图中的比特位会由1置为0;当进程收到了信号,但信号被阻塞了,不能执行方法,但是一旦解除阻塞了,信号就要立即被处理!

● 如果在进程解除对某信号的阻塞之前,这种信号产生过多次,将如何处理?

POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次, 而实时信号在递达之前产生多次可以依次放在一个队列里

● 所有对信号的操作都是围绕三张表展开的,而三张表都是内核数据结构,用户无法直接修改,OS要给我们提供系统调用来访问修改三张表

● 因为三张表都是内核数据结构,因此OS必须提供对应的系统调用来修改三张表, handler数组改直接可以拿着下标改,但是另外两张表要用户自己用位操作修改位图吗? 对用户要求太高了,因此OS不仅提供了系统调用,还提供了特定的数据类型(比如pid_t是OS提供的)

sigset_t 类型

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

● 在内核角度,三张表叫block位图表、pending位图表、handler表,在用户角度,block位图表又叫阻塞信号集(或信号屏蔽字), pending位图表又叫未决信号集

● sigset_t 类型本质就是信号集,该类型可以表示每个信号的"有效"或"无效"状态,比如在阻塞信号集中,“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态

● sigset_t 虽然是OS提供的,但是使用时就当成普通类型来用就行, 在底层本质是位图结构

● 对信号集的操作,我们也不必自己去用位操作修改,直接使用系统提供的信号集操作函数即可

信号集操作函数

sigprocmask

● 调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集), 成功返回0,失败返回-1

● how指的是我们要对信号屏蔽字做哪种操作,可以传递以下三个字段

● oldset是一个输出型参数,无论对block表进行何种操作,都是对block表的修改,把block表修改了,如果后续想恢复呢? 传入oldset参数就可以把旧的信号屏蔽字的内容带出来!

● 代码演示(屏蔽2号信号)

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void handler(int signo)
{
    cout << "handler " << signo << endl;
} 

int main()
{
    cout << "getpid: " << getpid() << endl;
    signal(2, handler);
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigaddset(&block, 2); //将2号信号添加到自己定义的block信号集中
    sigprocmask(SIG_BLOCK, &block, &block);
    while(true)
    {
        sleep(1);
    }
    return 0;
}

● 结论: 9号信号是管理员信号,无法被屏蔽

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void handler(int signo)
{
    cout << "handler " << signo << endl;
} 

int main()
{
    cout << "getpid: " << getpid() << endl;
    signal(2, handler);
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);
    for(int signo = 1; signo <= 31; signo++)
    {
        sigaddset(&block, signo);
    }
    sigprocmask(SIG_SETMASK, &block, &block);
    while(true)
    {
        cout << "我已经屏蔽了所有的信号,来打我呀!" << endl;
        sleep(1);
    }
    return 0;
}

sigpending

sigpending用于获取三张表中的pending表,以输出型参数的方式带出来!

综合练习

1. 屏蔽2号信号,每隔1s循环打印pending位图

2. 10s之后解除对2号信号的屏蔽,每隔1s循环打印pending位图

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void handler(int signo)
{
    cout << "handler " << signo << endl;
} 

void PrintPending(const sigset_t& pending)
{
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }      
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

int main()
{
    signal(2, handler);
    cout << "getpid: " << getpid() << endl;
    //1.屏蔽2号信号
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set, 2); 
    sigprocmask(SIG_BLOCK, &set, &oset);
    //2.让进程不断获取当前进程的pending表
    int cnt = 0;
    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        PrintPending(pending);     
        sleep(1);
        cnt++;
        if(cnt == 10)
        {
            cout << "解除对2号信号的屏蔽, 2号信号准备递达" << endl;
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }
    }
    return 0;
}

问题: 信号被递达,pending位图的对应位置会从1改为0,这个过程发生在什么时候? 是处理完信号之前还是处理完信号后呢??

结论: 信号在要被递达时,pending位图对应位置就已经从1改0了!

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }      
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{    
    sigset_t pending;
    cout << "############################" << endl;
    sigpending(&pending);
    PrintPending(pending);     
    cout << "############################" << endl;
    cout << "handler " << signo << endl;
} 

int main()
{
    signal(2, handler);
    while(true)
    {
        sleep(1);
    }
    return 0;
}

用户态与内核态

● 用户态是一种受控的状态, 比如操作上有权限约束,代码上会出现越界访问或野指针问题, 能够访问的资源是有限的,不能直接访问硬件资源,必须借助系统调用

● 内核态是一种OS的工作状态,能够访问大部分系统资源,系统调用背后,就包含了从用户态到内核态的身份转变

● 进程地址空间中, 用户态只能访问自己的[0, 3GB], 而内核态可以让用户以OS的身份访问[3, 4GB]

● 每一个进程都会有对应的进程地址空间,页表,而此处的页表指的是内存级页表,有n个进程就有n个用户级页表

● OS也是需要加载到内存的,OS代码和数据到内存的映射靠的是内核级页表,无论有多少个进程,内核级页表只有1个,因为OS的代码和数据只有1份

● 无论进程如何切换调度,cpu都能找到OS的代码运行,因为有内核级页表的存在

● 进程所有代码的执行,都可以在自己的地址空间内通过跳转的方式,进行调用和返回

● Linux操作系统中: cpu内部存在cs寄存器,有2个比特位,组合有4种,但是只使用2种,1表示内核态,3表示用户态,  用户态和内核态的切换都会修改两个比特位

 ● cpu内还有一套寄存器:CR寄存器,比如CR1(保存引发缺页中断/异常,比如野指针,越界访问的虚拟地址), CR3寄存器(保存当前进程页表信息, 物理地址!)

信号的捕捉过程

● 用户代码中调用了系统调用,需要从用户态转变为内核态,才有权限调用系统调用

● 调用完系统调用之后,会检测进程是否收到了信号,如果没有收到信号,会直接回到用户态;如果收到了信号,且处理动作为忽略,那么pending信号集对应位置由1改0,回到用户态如果处理动作为默认,则去执行默认行为,如直接杀掉进程,回到用户态

● 如果处理动作是自定义捕捉信号,则需要去回调处于用户态的代码,内核态当然可以直接执行用户态代码,但代码中可能会有恶意访问os的行为,因此回调用户态代码,需要从内核态转变到用户态

● 自定义捕捉完信号后再回到用户态调用系统调用的地方,继续向下执行代码

sigaction函数

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

功能: 对信号自定义捕捉

参数:  signum是信号编号, act和oldact都是输出型参数,可以修改对信号处理的自定义行为以及获取上次对信号处理的动作

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

其中,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);     
};

我们只用到了其中两个参数:

sa_handler:自定义的信号捕捉函数

sa_mask:希望屏蔽的信号添加到该信号集中

注:

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

2.如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

使用sigaction函数达到和signal函数一样的效果,比如对2号信号进行自定义捕捉

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
}

int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigaction(2, &act, &act); //对2号信号进行自定义捕捉
    while(true)
    {
        sleep(1);
    }
    return 0;
} 

验证正在处理当前信号时,会自动屏蔽掉OS不断发来的相同的信号

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
    while(true)
    {
        sigset_t pending;
        sigpending(&pending);
        PrintPending(pending);
        sleep(1);
    }
}

int main()
{
    cout << "getpid : " << getpid() << endl;
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigaction(2, &act, &oact); //对2号信号进行自定义捕捉
    while(true) sleep(1);
    return 0;
}

使用sigaction函数屏蔽指定的一堆信号

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
    while(true)
    {
        sigset_t pending;
        sigpending(&pending);
        PrintPending(pending);
        sleep(1);
    }
}

int main()
{
    cout << "getpid : " << getpid() << endl;
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3); //将3号信号也屏蔽掉
    sigaction(2, &act, &oact); //对2号信号进行自定义捕捉
    while(true) sleep(1);
    return 0;
}

研究进程同时收到多个信号的处理情况

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
    for(int signo = 31; signo > 0; signo--)
    {
        if(sigismember(&pending, signo))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
    sleep(1);
}

int main()
{
    signal(2, handler);
    signal(3, handler);
    signal(4, handler);
    signal(5, handler);
    sigset_t mask, omask;
    sigemptyset(&mask);
    sigemptyset(&omask);
    sigaddset(&mask, 2);
    sigaddset(&mask, 3);
    sigaddset(&mask, 4);
    sigaddset(&mask, 5);
    sigprocmask(SIG_SETMASK, &mask, &omask);

    cout << "getpid :" << getpid() << endl;
    int cnt = 20;
    while(true)
    {
        sigset_t pending;
        sigpending(&pending);
        PrintPending(pending);
        cnt--;
        sleep(1);
        if(cnt == 0)
        {
            sigprocmask(SIG_SETMASK, &omask, nullptr);
            cout << "cancel 2,3,4,5 block" << endl;
        }
    }
    return 0;
}

结论: 当信号从内核态返回到用户态时,会检测是否还有信号没有处理,如果还有信号没有处理,则会将剩余信号处理完之后返回,而处理其余信号的顺序是不固定的,主要依据的是信号优先级

可重入函数

● 一个函数被多个执行流同时进入而不会出现任何问题,则该函数称为可重入函数;否则称为不可重入函数

● 函数中用到了全局变量或全局数据,一般是不可重入的

● 可重入函数或不可重入函数描述的只是函数的特征,并不代表函数的好坏

● 大多数函数都是不可重入函数

volatile关键字

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

#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

int flag = 0;

void handler(int signo)
{
    cout << "signo : " << signo << endl;
    flag = 1;
    cout << "change flag to " << flag << endl;
}

int main()
{
    signal(2, handler);
    cout << "getpid : " << getpid() << endl;
    while(!flag);
    cout << "quit nomal!" << endl;
    return 0;
}

当编译代码时如果不优化以及 O0级别 的优化结果都在预期之内,但是当采用 O1 级别优化时,发现卡住了,分析代码就知道卡在了while(!flag)循环处,但是flag不是改成1了吗,为啥还在循环??

稍微修改了一下代码,在定义flag时使用volatile关键字进行修饰,此时尽管O1级别 优化也可以正常跑完代码了!

经过O1级别优化,形成的汇编代码会有所变化,因为main函数中flag是只读的,因此以后cpu检测flag值时,只有第一次会从内存中读取数据到cpu寄存器中,以后就只在cpu内部做检测即可,因此你发送2号信号将flag值改了,改的是物理内存的flag值,不会影响到cpu的判断了!

而volatile的作用就是保持内存的可见性, 意思是所有使用flag的操作,都要从内存中拿数据!

SIGCHLD信号

子进程在退出的时候,不是静悄悄退出的,会给父进程发送17号信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

void handler(int signo)
{
    cout << "子进程给父进程说他要退了!!!" << endl;
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if(0 == id)
    {
        int cnt = 5;
        while(cnt--)
        {
            cout << "I am child process" << endl;
            sleep(1);
        }
        exit(0);
    }
    sleep(10);
    return 0;
}

既然子进程退出时会向父进程发信号,而父进程因为一些客观原因(比如网络服务)一直在运行,因此父进程可以直接在handler函数中等待回收子进程

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;

    //回收子进程
    waitpid(-1, nullptr, 0); //阻塞等待
}

int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if(id == 0)
    {
        //child
        cout << "child is running" << endl;
        sleep(5);
        exit(10);
    }
    while(1)
    {
        sleep(1); //父进程一直在进行某种服务
    }
    return 0;
}

● fork可能创建了多个子进程,有可能这些子进程同时退出了,都同时向父进程发送17号信号,而根据上文提到的,父进程收到第一个17号信号之后就阻塞17号信号了,也就意味着其他子进程发送的17号信号就被丢弃了,父进程只能处理一个子进程,其他子进程都是僵尸了!因此我们要在handler中循环式等待处理子进程

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
    pid_t id = 0;
    while(id = waitpid(-1, nullptr, 0))
    {
        if(id < 0) break; //没有需要回收的子进程了!
        cout << "回收子进程 : " << id << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // child
            cout << "child is running" << endl;
            sleep(5);
            exit(10);
        }
    }

    while(true)
    {
        sleep(1); // 父进程一直在进行某种服务
    }
    return 0;
}

● 不一定所有的进程都会退出,比如在有6个进程退出了,有4个进程不退出,handler函数中等待回收子进程就会阻塞住,父进程就做不了其他事情了! 因此在等待子进程时必须是非阻塞等待!

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signo : " << signo << endl;
    pid_t id = 0;
    while(id = waitpid(-1, nullptr, WNOHANG))
    {
        if(id <= 0) break; //没有需要回收的子进程了!
        cout << "回收子进程 : " << id << endl;
    }
}

int main()
{
    signal(SIGCHLD, handler);
    for (int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // child
            cout << "child is running" << endl;
            sleep(5);
            exit(10);
        }
    }

    while(true)
    {
        sleep(1); // 父进程一直在进行某种服务
    }
    return 0;
}

● 如果要获取子进程的退出信息,那么父进程必须等待子进程,但如果只是为了回收子进程的僵尸状态,则等待不是必须的, Linux支持对SIGCHLD信号进行忽略处理,所有的子进程都不需要父进程进行等待了,自动回收!

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;

int main()
{
    signal(SIGCHLD, SIG_IGN); 
    for(int i = 0; i < 10; i++)
    {
        pid_t id = fork();
        if(0 == id)
        {
            cout << "child is running" << endl;
            sleep(5);
            exit(10);
        }
    }
    while(true) sleep(1);
    return 0; 
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/929593.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于STM32F4实现步进电机闭环控制实现(无PID)

文章目录 概要整体流程代码实现TIM8 PWM控制TIM5 编码器计数TIM13 闭环控制 效果展示小结 概要 因客户外部负载较大&#xff0c;步进电机出现丢步现象&#xff0c;所以需要进行闭环控制&#xff0c;保证最后走到相应的位置即可&#xff0c;所以我采用的是电机停止后与编码器值…

第4章:颜色和背景 --[CSS零基础入门]

在 CSS 中&#xff0c;颜色和背景属性是用于美化网页元素的重要工具。你可以通过多种方式定义颜色&#xff0c;并且可以设置元素的背景颜色、图像、渐变等。以下是关于如何在 CSS 中使用颜色和背景的一些关键点和示例。 1.颜色表示法 当然&#xff01;以下是使用不同颜色表示…

二叉树概述

目录 一、二叉树的基本结构 二、二叉树的遍历 1.前序 2.中序 3.后序 4.层序遍历 三.计算二叉树的相关参数 1.计算节点总个数 2.计算叶子节点的个数 3.计算树的高度 4.计算第k层的子树个数 5.查找树中val为x的节点 四.刷题 1.单值二叉树 2.检查两棵树是否相同 3.一…

04 创建一个属于爬虫的主虚拟环境

文章目录 回顾conda常用指令创建一个爬虫虚拟主环境Win R 调出终端查看当前conda的虚拟环境创建 spider_base 的虚拟环境安装完成查看环境是否存在 为 pycharm 配置创建的爬虫主虚拟环境选一个盘符来存储之后学习所写的爬虫文件用 pycharm 打开创建的文件夹pycharm 配置解释器…

weblogic开启https

JSK证书生成 生成密钥库和证书 使用Java的keytool命令来生成一个Java密钥库&#xff08;Keystore&#xff09;和证书。keytool是Java开发工具包&#xff08;JDK&#xff09;中用于管理密钥库和证书的命令行工具。 #创建证书存放目录 [weblogicosb1 jksHL]$ mkdir -p /home/w…

学习记录,正则表达式, 隐式转换

正则表达式 \\&#xff1a;表示正则表达式 W: 表示一个非字&#xff08;不是一个字&#xff0c;例如&#xff1a;空格&#xff0c;逗号&#xff0c;句号&#xff09; W: 多个非字 基本组成部分 1.字符字面量&#xff1a; 普通字符&#xff1a;在正则表达式中&#xff0c;大…

防火墙有什么作用

防火墙的作用&#xff1a;1. 提供网络安全防护&#xff1b;2. 实施访问控制和流量过滤&#xff1b;3. 检测和阻止恶意攻击&#xff1b;4. 保护内部网络免受未经授权的访问&#xff1b;5. 监控网络流量和安全事件&#xff1b;6. 支持虚拟专用网络&#xff08;VPN&#xff09;。防…

linux中启动oracle19c操作过程及详解

1.登录Oracle用户 su - oracle2.启动监听程序 监听器&#xff08;Listener&#xff09;是Oracle数据库与客户端通信的桥梁。使用以下命令启动监听器&#xff1a; lsnrctl start如图情况监控程序启动成功。 3.启动数据库实例 使用 sqlplus 工具以 SYSDBA 权限连接到数据库&a…

ainiworth 在分布式目标的方程中 与正常互易性可以形成的方程不同 多引入了协方差元素未知 但可解,因为此时只有一个串扰参数且已经解出来了

这个散射互易性&#xff0c;在不考虑AB时 方程应该只剩两个即 HVHV VHVH 和VHHV相位(虚部) 0 但是这一组方程却可以解4个参数未知数。C元素是观测的已知。 β表示真实协方差矩阵&#xff0c;Σ是恢复的协方差&#xff08;也可以认为是真实协方差元素&#xff09; 1、首先把方…

10a大电流稳压芯片_24v转3.3v稳压芯片,高效率DC-DC变换器10A输出电流芯片-AH1514

### AH1514——高性能的大电流稳压芯片 在现代电子电路设计中&#xff0c;对于能够满足大电流、高效率转换以及稳定电压输出的芯片需求日益增长。AH1514芯片作为一款出色的DC-DC变换器&#xff0c;以其独特的性能特点&#xff0c;在众多应用场景中展现出了卓越的优势. ### 一…

【网络篇】HTTP知识

键入网址到网页显示&#xff0c;期间发生了什么&#xff1f; 浏览器第一步是解析URL&#xff0c;这样就得到了服务器名称和文件的路径名&#xff0c;然后根据这些信息生成http请求&#xff0c;通过DNS查询得到我们要请求的服务器地址&#xff0c;然后添加TCP头、IP头以及MAC头&…

pdf转word/markdown等格式——MinerU的部署:2024最新的智能数据提取工具

一、简介 MinerU是开源、高质量的数据提取工具&#xff0c;支持多源数据、深度挖掘、自定义规则、快速提取等。含数据采集、处理、存储模块及用户界面&#xff0c;适用于学术、商业、金融、法律等多领域&#xff0c;提高数据获取效率。一站式、开源、高质量的数据提取工具&…

github使用SSH进行克隆仓库

SSH 密钥拉取git 查询密钥是否存在 s -al ~/.ssh这个文件夹下 known_hosts 就是存在的密钥文件 创建密钥文件 ssh-keygen -t rsa -b 4096 -C "testtt.com"-t rsa 是 rsa 算法加密 -b 是指定密钥的长度&#xff08;以位为单位&#xff09;。 -C 是用于给密钥添加注…

【MARL】MAT论文阅读笔记

文章目录 前言一、如何产生这个想法(TRPO -> ) PPO -> MAPPO -> HAPPO -> MAT 二、多智能体优势值分解定理三、transformer 在MAT的应用四、伪代码简述五、实验效果 前言 正好有节课让我们调研最新的自己的方向的新论文&#xff0c;找到一篇自己觉得比较可行&…

代码随想录32 动态规划理论基础,509. 斐波那契数,70. 爬楼梯,746. 使用最小花费爬楼梯。

1.动态规划理论基础 动态规划刷题大纲 什么是动态规划 动态规划&#xff0c;英文&#xff1a;Dynamic Programming&#xff0c;简称DP&#xff0c;如果某一问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。 所以动态规划中每一个状态一定是由上一个状态推导出来的…

基于SpringBoot的社区医院管理系统(代码+论文)

&#x1f389;博主介绍&#xff1a;Java领域优质创作者&#xff0c;阿里云博客专家&#xff0c;计算机毕设实战导师。专注Java项目实战、毕设定制/协助 &#x1f4e2;主要服务内容&#xff1a;选题定题、开题报告、任务书、程序开发、项目定制、论文辅导 &#x1f496;精彩专栏…

Leetcode 每日一题 49.字母异位词分组

目录 问题描述 示例 示例 1 示例 2 示例 3 约束条件 解决方案 思路 算法步骤 过题图片 代码实现 复杂度分析 题目链接 结论 问题描述 给定一个字符串数组&#xff0c;需要将其中的字母异位词分组。字母异位词是指通过重新排列源单词的所有字母得到的新单词。要求…

进程控制(下)

进程控制&#xff08;下&#xff09; 进程程序替换 fork() 之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢&#xff1f;进程的程序 替换来完成这个功能&#xff01; 程序替换是通过特定的接⼝&#xff0c;加载磁盘上的⼀个全新的程序(代码和数据)&am…

安全关系型数据库查询新选择:Rust 语言的 rust-query 库深度解析

在当今这个数据驱动的时代&#xff0c;数据库作为信息存储和检索的核心组件&#xff0c;其重要性不言而喻。然而&#xff0c;对于开发者而言&#xff0c;如何在保证数据安全的前提下&#xff0c;高效地进行数据库操作却是一项挑战。传统的 SQL 查询虽然强大&#xff0c;但存在诸…

读取电视剧MP4视频的每一帧,检测出现的每一个人脸并保存

检测效果还不错,就是追踪有点难做 import cv2 import mediapipe as mp import os from collections import defaultdict# pip install msvc-runtime# 初始化OpenCV的MultiTracker # multi_tracker = cv2.MultiTracker_create() # multi_tracker = cv2.legacy.MultiTracker_cre…