【Linux】信号三部曲——产生、保存、处理

信号

  • 1. 信号的概念
  • 2. 进程如何看待信号
  • 3. 信号的产生
    • 3.1. kill命令
    • 3.2. 终端按键
      • 3.2.1. 核心转储core dump
      • 3.2.2. OS如何知道键盘在输入数据
    • 3.3. 系统调用
      • 3.3.1. kill
      • 3.3.2. raise
      • 3.3.3. abort
    • 3.4. 软件条件
      • 3.4.1. SIGPIPE信号
      • 3.4.2. SIGALRM信号
    • 3.5. 硬件异常
      • 3.5.1. 除零异常
      • 3.5.2. 野指针异常
  • 4. 信号的保存
    • 4.1. 信号常见概念
    • 4.2. 在内核中的表示
    • 4.3. 信号集sigset_t
    • 4.4. sigprocmask
    • 4.5. sigpending
  • 5. 信号的处理
    • 5.1. 信号处理常见三种方式
    • 5.2. 信号捕捉的方式
      • 5.2.1. signal
      • 5.2.2. sigaction
    • 5.3. 信号什么时候被处理的
      • 5.3.1. 用户态、内核态
      • 5.3.2. 重谈地址空间
    • 5.4. 内核如何实现信号的捕捉
    • 5.5 可重入函数
    • 5.6. volatile关键字
    • 5.7. SIGCHLD信号

1. 信号的概念

  1. 信号的概念:是一种软件中断机制,是linux系统提供的让用户(进程)给其他进程发送异步事件通知的一种方式,用于进程间通信或通知进程发生了特定的事件(如:硬件异常、软件条件等)。

  2. 每个信号都有一个编号和宏定义名称,在#include<signal.h>头文件中进行宏定义,编号132为普通信号、编号3464为实时信号(此处不作讨论)。

kill -l 、man 7 signal

  • 功能:用户查看linux系统定义的所有信号列表,如:信号在什么条件下产生、信号默认处理动作是什么等。

  1. 信号产生的原因:系统需要确保进程能够随时响应外部事件,并及时作出响应的处理。

2. 进程如何看待信号

  1. 进程具有识别并处理不同信号的能力。

进程在启动时,就已经设置好了识别特定信号的方式,且在信号产生之前,进程就知道如何处理这些信号,因为进程在启动时,OS会为其分配一个信号处理表(handle表,即:函数指针表),用来记录每个信号对应信号的处理函数,且信号的识别方式也是通过它。

  1. 当信号产生时,如果进程正在处理更重要的事情(如:处于临界区或执行不可中断操作),而暂时不能处理到来的信号,为了确保信号不会丢失,OS会将这个信号暂时保存起来。

这种暂时保存是通过进程控制块(task_struct)中的pending位图来记录的,pending位图用来记录哪些信号已经被发送但尚未处理,位图中的每一位对应一个信号,如果该位为1,则表示该信号已经被发生但尚未处理,一旦这个信号被处理完毕,OS会将此信号从位图中清除(由1置为0)。

  1. 信号产生时,如果进程正在内核态执行,一般不会立即处理,而是等从内核态切回到用户态之前进行检查是否有未处理的信号,如果有未处理的信号且满足信号处理条件,就会在此时处理信号。

  2. 信号的产生具有不确定性和临时性,我们无法准确预料何时会有信号产生,因此信号是异步发送的。

信号的产生是由外部事件触发的(如:用户输入、硬件异常等),不是进程主动请求的,这体现了异步发送的不可预测性。

信号的产生与接收信号的进程的执行流是不同步的。当信号产生时,进程可能正在执行其他任务或处于某种状态,而信号的到达不会中断或阻塞当前进程的执行流,而是在合适的时候处理,这体现了异步发送的独立性。

由于信号是异步发送的,进程可以根据自身需求选择在何时以及如何处理信号(如:设置信号函数或忽略某些信号等),这体现了异步发送的灵活性。

3. 信号的产生

3.1. kill命令

kill -信号编号 进程的PID

3.2. 终端按键

  1. 用户可以通过键盘输入特定的组合键来产生信号,如:Ctrl+c会产生SIGINT信号(2号信号,终止进程),Ctrl+\会产生SIGQUIT信号(3号信号,终止进程)、Ctrl+z会产生SIGSTP信号(20号信号,暂停进程的执行)等。
#include<iostream>
#include<cstdio>
#include<csignal>
#include<unistd.h>
#include<sys/types.h>using namespace std;void handle(int signo)
{cout << "handing signao " << signo << endl;
}int main()
{for(int signo = 1; signo <= 32; signo++){signal(signo, handle);}while(true){printf("I am a process, pid: %d\n", getpid());sleep(3);}return 0;
}

3.2.1. 核心转储core dump

  1. 进程接收到信号后默认处理动作为终止时两种常见的方式,分别为term、core,两者主要区别在与core具有核心转储功能,即:生成核心转储文件。

term:进程在接收到信号后,有机会执行清理操作并优雅地退出,通常是通过SIGTERM(信号编号15)来实现的。

  1. 核心转储(core dump):是指在进程因收到了特定信号而异常终止时,OS将进程在内存的核心数据(如:地址空间、与调试有关的信息等)转储到磁盘中,形成一个核心转储文件core(Ubuntu系统)或core.pid(Centos系统)文件。

问1:为什么要存在核心转储?

核心存储文件包含了进程异常终止时的内存状态、寄存器值、调用栈等调试信息,有助于协助程序调试,从而快速定位到错误原因(如:进程为什么退出、进程执行到哪行代码退出)。

  1. 事后调试:可以通过核心存储文件对已经异常终止的进程,使用调试器进行调试,以便定位到错误原因,因为进程异常终止通常是有Bug(如:非法访问内存导致的段错误等)。

  2. 云服务器为了节省磁盘空间、避免资源浪费或出于安全考虑,对以core方式终止的进程进行了特定的设定,默认关闭core文件的生成。

ulimit -a

  • 功能:显示当前用户的所有资源限制。

ulimit -c

  • 功能:打开核心转储功能,并设置或查询core文件的大小限制。

  1. 确定子进程是否发生核心转储,需要检查这两个条件:a. 系统是否开启了核心转储功能,b.子进程异常退出的默认处理动作是生成core文件(Action : core)。

如果以上两个条件都满足,但在当前目录下未生成core文件,解决方法如下:

sudo bash -c “echo core.%p > /proc/sys/kernel/core_pattern”

  • 功能:设置核心转储文件的生成路径和格式。

当进程异常终止时,OS会根据core_pattern文件中的设置生成核心转储文件。

core.%p表示核心转储文件名为core.pid,为了防止未知的core dump一直运行,导致服务器磁盘被打满,因为程序每次运行都是全新的进程,pid均不同,因此通常将其设置为core,表示核心转储文件名为core,其大小是固定的;

3.2.2. OS如何知道键盘在输入数据

一、OS如何检测和处理键盘输入

  1. 效率问题:通过硬件中断机制,OS无需定期检测键盘是否被按下,大大提高了系统的效率和响应速度。这是因为硬件中断是异步发生的,当键盘被按键按下时,会立即触发中断,CPU会立即响应并处理该中断。

  2. 中断向量表:OS在启动时,会初始化一张中断向量表,这张表实际上是一个函数指针数组,每个下标对应一个中断编号,每个元素对应对应一个具体的中断处理函数。

  3. 硬件中断机制:当键盘上某个键被按下时,键盘控制器会向CPU发送一个硬件中断信号,这个信号通常是通过主板上一个固定的针脚发送的,该针脚与CPU某个特定中断输入引脚相连。

  4. OS响应中断:CPU收到中断信号后,会将中断编号保存在寄存器中,并且会要求OS根据中断编号查找中断向量表中的中断处理函数,并执行该函数。

对于键盘的输入,OS提供了读取键盘数据的方法,通过这个函数,OS就可获取键盘中输入的数据。

  1. 数据处理:OS读取数据后,会对数据进行判定,如果是字符,将其放入缓冲区中,供后续的程序通过read系统调用读取;如果是控制命令(组合键,如 : ctrl c),OS会将其解释为信号,并发送给当前正在运行的、与信号相关联的进程。

二、发送信号的本质

  1. 给进程发送信号的本质是将信号写入进程的PCB中,而PCB是内核数据结构,只有OS才有资格写入,用户只能通过调用OS提供的系统调用来写入信号。

  2. 无论信号的产生有多少种,最终都是OS负责将信号写入到目标进程的PCB中,并触发相应的信号处理机制。

3.3. 系统调用

3.3.1. kill

int kill(pid_t pid, int sig) ;

  • 功能:对任意进程发送任意信号。
  • 返回值:成功返回0,失败返回-1。

Tisp:kill命令底层封装了系统调用kill函数。

#include<iostream>
#include<cstdio>
#include<sys/types.h>
#include<signal.h>
#include<errno.h>
#include<cstring>using namespace std;void Usage(char* argv[]) 
{cout << argv[0] << " -signumber PID" << endl; 
}int main(int argc, char* argv[]) //模拟实现kill命令
{if(argc != 3) Usage(argv); //用法错误int pid = stoi(argv[2]), signo = stoi(argv[1] + 1);int n = kill(pid, signo); //底层封装了系统调用killif(n < 0) cerr << "kill error: " << strerror(errno) << endl;return 0;
}

3.3.2. raise

int raise(int sig);

  • 功能:给当前进程发送任意信号。
  • 返回值:成功返回0,失败返回非0值。

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>using namespace std;void handle(int signo) 
{cout << "get a signal, number is " << signo << endl;
}int main()
{signal(2, handle); //设定信号捕捉的方法int cnt = 4;while(cnt--){cout << "I am a process, pid: " << getpid() << endl;if(cnt == 2) raise(2); //给当前进程发送任意信号 —— 给自己发送2号信号sleep(1);}return 0;
}

3.3.3. abort

void abort(void);

  • 功能:终止进程,且给当前进程发送SIGABRT信号(6号信号)。

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>using namespace std;void handle(int signo) 
{cout << "get a signal, number is " << signo << endl;
}int main()
{signal(6, handle); //设定信号捕捉的方法int cnt = 4;while(cnt--){cout << "I am a process, pid: " << getpid() << endl;if(cnt == 2) abort(); //给当前进程发送指定信号(6号信号)sleep(1);}return 0;
}


abort后续行为:即使捕捉了SIGABRT并返回了处理函数,abort函数仍然会尝试执行其标准的终止流程,这包括调用raise(SIGABRT),然后执行一些清理操作,并最终调用_exit(1)来终止程序,,因此它不关心循环中是否还有未执行的代码。

3.4. 软件条件

3.4.1. SIGPIPE信号

  1. SIGPIPE信号(13号信号)是由操作系统内核检测到的管道写端已关闭这一软件条件触发的信号。

  2. 产生条件:当一个进程向已经关闭写端的管道中写入数据时,OS会向该进程发送SIGPIPE信号,其默认行为是终止进程。

  3. 为什么向已关闭写端的管道写入数据,被视为软件条件?

这一过程涉及操作系统内核中软件代码来管理管道的状态、检查写入条件以及产生和处理信号。这与硬件条件(如:物理设备的状态变化)不同,是由物理设备的物理特性决定的。

管道是通过OS中特定的数据结构来实现的,但这些数据结构及其操作逻辑都是由OS软件来管理的;当一个进程进行写入时,OS会检查管道的状态,包括写端是否关闭,OS会检测到写端已关闭这一错误条件,会进行响应产生SIGPIPE信号,以通知进程发生了错误,这一过程是由操作系统内核中的软件代码来实现的,因此它属于软件条件范畴。

3.4.2. SIGALRM信号

一、SIGALRM信号

  1. SIGALRM信号(14号信号)是由软件条件触发的信号。

  2. 产生条件:通过调用alarm函数来设置一个定时器,在second秒后定时器到期,OS会给当前进程发送SIGALRM信号,其默认行为是终止进程。

二、系统调用接口alarm

unsigned int alarm(unsigned int seconds);

  1. 功能:设置一个定时器,定时器在seconds秒后到期,OS会向当前进程发送SIGALRM信号,这个信号可以被进程捕捉处理(signal函数),或执行默认处理动作(终止进程)。

  2. 返回值:调用失败:返回UINT_MAX,并设置错误码以指示错误的原因。

    调用成功:a. 如果调用alarm函数前,已经设置了一个全新的定时器且在运行,则alarm函数会取消之前的定时器,用新的定时器代替,此时,alarm函数返回值。b.如果调用alarm函数前,没有设置全新的定时器,则alarm返回值为0。

在调用alarm函数前,设置了alarm(0),表示取消之前设置的定时器,返回值为离之前设置的定时器剩余的时间。

#include<unistd.h>
#include<signal.h>using namespace std;void handle(int signo) 
{cout << "get a signal, number is " << signo << endl;
}int main()
{signal(14, handle); //对SIGALAM信号进行捕捉alarm(50); int cnt = 5;while(cnt--){cout << "I am a process, pid: " << getpid() << endl;sleep(1);if(cnt == 3)  {size_t n = alarm(0); //取消之前设定的定时器cout << "alarm(0) retval " << n << endl; //返回值为离之前设置的定时器剩余的时间}}return 0;
}

  1. alarm只会执行一次,如果想让其在某段时间内每隔指定秒数触发SIGALRM信号,就需要在信号处理函数(signal)中捕捉SIGALRM信号,在此处理函数中设置一个全新的定时器。
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>using namespace std;int cnt;/*如果在信号处理函数中设置了alarm(2),并且接着是一个死循环,
现象:在2秒过后OS尝试发送SIGALRM信号,由于进入了死循环,
意味着处理函数不会返回,导致新的信号无法被处理,因为无法进入该信号处理函数*/
void handle(int signo) 
{cout << "catching..." << endl;int n = alarm(2); //在上一个定时器调用前,设置一个全新的定时器cout << "alarm(2) retval" << n << endl; //离之前设置的定时器剩余的时间
}int main()
{signal(SIGALRM, handle); //设置SIGALRM信号的捕捉方法alarm(10); //设置一个定时器while(true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);}return 0;
}

  1. OS需要管理大量的定时任务,如:定期将数据从内核缓冲区刷新到外设,或执行其他需要定时控制的任务。alarm函数是一个系统调用接口,是用户空间和操作系统内核交互设置定时任务的一种方式,而在OS内部,必然存在大量的定时器,OS需要对它们进行管理,只需要判断当前时间是否超过了定时器的超时时间。

创建描述定时器的结构体,该结构体通常包含以下信息:设置定时器进程的PID、定时器的超时时间、触发时要发送给进程的信号等。

组织定时器结构体,使用最小堆,堆顶始终表示最近的一个超时的闹钟。

  1. 为什么SIGALRM信号被视为一个软件条件?它是由OS内部的软件逻辑所控制的,这个逻辑涉及到定时器的管理、检查和超时处理,最终导致SIGALRM信号的发送,而不是由外部硬件事件直接触发的。

3.5. 硬件异常

  1. 硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。

eg1:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。

eg2:当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

3.5.1. 除零异常

int main()
{int a = 0, b = 3;int c = b / a; //除数为0,会导致除零错误,接收到SIGFPE信号(8号信号)return 0;
}

  1. 除零异常

当CPU执行除法运算,如果除数为0,会导致除零错误,会触发一个除0异常;

CPU进行计算时会出现溢出的情况,这会导致CPU会更新EFLAGS寄存器中相应的标志位(如:OF、ZF等);

OS会识别到这些标志位的变化,则OS的异常处理机制会捕获这个异常,并发送SIGFPE信号(8号信号)给进程;

如果进程设置了该信号的自定义捕捉handle方法,那么该方法会被调用。

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>using namespace std;void handle(int signo) //未终止进程,导致异常一直存在
{cout << "get signumber: " << signo << endl; sleep(1); 
}int main()
{signal(SIGFPE, handle);int a = 0, b = 3;int c = b / a; //除数为0,会导致除零错误while(true)sleep(1);return 0;
}

问:发生除零错误,触发除0异常,收到SIGFPE信号,为什么会一直执行自定义捕捉handler方法?

  • 进程中设置了对SIGFPE信号的自定义捕捉handle方法,发生除零错误时,会收到SIGFPE信号,那么该方法会被调用,因为在此方法中未终止进程,导致异常状态仍然保存在进程的上下文数据中,OS会将你这个进程切走,进程上下文会保存在PCB中,当此进程被OS再次调度时,上下文会进行恢复,OS识别到错误仍然存在,就会再次发送信号。

3.5.2. 野指针异常

  1. 野指针异常

野指针是指向无效内存地址的指针,是虚拟地址,当一个进程尝试通过野指针访问内存时,MMU会尝试将虚拟地址转化为物理地址,如果找不到对应的页表项或权限不匹配,则转化失败,MMU会触发页面错误异常。

CPU会捕获这个异常,并将导致错误的虚拟地址存储在cr2寄存器中,并设置EFLAGS寄存器中相应的标志位。

OS会识别到这些标志位的变化,并发送SIGSEGV信号(11号信号)给进程。

如果进程设置了该信号的自定义捕捉handle方法,那么该方法会被调用,若handle方法中未设置终止进程,那么每次上下文恢复时,OS识别到错误仍然存在,就会再次发送信号。

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>using namespace std;void handle(int signo) //未终止进程,导致异常一直存在
{cout << "get signumber: " << signo << endl; sleep(1); 
}int main()
{signal(SIGSEGV, handle);int* p = NULL; //非法访问野指针,会导致野指针异常*p = 10;while(true)sleep(1);return 0;
}

  1. 总结:除零和野指针异常最终都会表现为硬件级别的异常(程序出现的错误最终都会在硬件层面上有所表现),CPU会触发相应的异常处理机制,OS通过捕获这些异常,并向进程发送信号来通知它,如果进程设置了信号的自定义捕捉方法,需要在该方法内采取适当的措施来清除异常状态或终止进程,否则异常会被重复触发。

4. 信号的保存

当信号产生时,如果进程正在处理更重要的事情(如:处于临界区或执行不可中断操作),而暂时不能处理到来的信号,为了确保信号不会丢失,OS会将这个信号暂时保存起来,那我们就来看看内核中如何保存信号。

4.1. 信号常见概念

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

  2. 信号未决:信号产生到递达之间的状态。

  3. 进程可以选择阻塞某个信号。

  4. 被阻塞的信号产生时,将一直保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。

Tips:注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是抵达之后可选的一种处理动作,形象来说忽略是视而不见,阻塞是看不到。

4.2. 在内核中的表示

  1. task_struct是Linux内核用于描述进程的数据结构,与信号相关的信息主要存储在task_struc的signal字段中。
struct task_struct {// ...struct signal_struct *signal;  // 指向信号结构体// ...
};struct signal_struct {// ...sigset_t blocked;  // 被阻塞的信号集合sigset_t pending;  // 待处理的信号集合struct sigaction *actions;  // 信号处理函数指针数组// ...
};
  1. blocked位图:存储当前进程阻塞(屏蔽)的信号集合。

当一个信号被阻塞时,即使该信号被发送给进程,进程也不会立即处理它,而是将其暂时保存起来,直到进程解除对此信号的阻塞。

在block位图中,比特位的位置表示信号编号,比特位的内容表示信号是否被阻塞,如果某一个位为1,则表示该信号被阻塞。

  1. pending位图:存储当前进程待处理的信号集合。

当一个信号被发送时,它会被添加到这个集合中,对应比特位的内容由0变为1,直到信号递达,OS才会将此信号从pending位图中清除,对应比特位由1置为0(细节:pending位图先被清0,在递达)。

在pending位图:比特位的位置表示信号编号,比特位的内容表示是否收到信号,如果某一位为1,则表示该信号已被发送但尚未处理。

  1. handle函数指针数组:存储信号处理函数的地址。

当一个信号被发送,且未被阻塞,在合适的时候,此信号需要被处理,OS会根据信号编号来在这个数组中找到对应的处理函数,并调用该函数来处理信号。

数组下标表示信号编号,数组的内容表示信号递达时的处理动作,包括默认SIG_DEF、忽略SIG_IGN、自定义捕捉函数(handle)。

Tips:POSIX.1允许信号在递达之前产生多次,在Linux中是这样实现的,普通信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列中。

4.3. 信号集sigset_t

一、sigset_t

  1. sigset_t:在Linux系统被称为信号集,是用于表示一组信号集合的数据类型,实际上是一个位图,每个位对应一个信号。

sigset_t用于表示当前进程的阻塞信号集时,它被称为阻塞信号集或信号屏蔽字。

sigset_t用于表示当前进程已发送但尚未处理的信号集合时,它被称为未决信号集。

  1. 每个信号通常只有一个未决标志和阻塞标志,且这两个标志都只有两种状态(“有效"或"无效”,非0即1),不记录该信号产生了多少次,因此未决标志和阻塞标志可以使用相同的数据类型sigset_t来存储,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,在未决信号集中"有效"和"无效"的含义是信号是否处于未决状态。

  2. sigset_t类型对于每个信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit依赖于OS的实现,且不同的OS或OS实现采用内部的存储方式不尽相同,从使用者的角度不关心底层的具体的存储方式,不直接访问或解释sigset_t内部的数据(如:printf直接打印sigset_t变量无意义),只能依赖于系统提供的标准函数来对sigset_t进行操作。

二、信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo); 

int sigemptyset(sigset_t* set);

  • 功能:初始化set所指向的信号集,使其中所有信号的对应bit清0,表示该信号集不包含任何有效信号。
  • 返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。

int sigfillset(sigset_t* set);

  • 功能:初始化set所指向的信号集,使其中所有信号的对应bit全部置为1,表示该信号集的有效信号为系统支持的所有信号。
  • 返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。

int sigaddset(sigset_t* set, int signum)、int sigdelset(sigset_t* set, int signum);

  • 功能:用于向/从信号集中添加/删除一个信号,signum是添加/删除的信号编号。
  • 返回值:成功时返回 0,失败时返回 -1,并设置errno以指示错误的原因。

int sigismember(const sigset_t* set,int signum);

  • 功能:检查一个信号是否存在于信号集中,signum是要检查的信号编号。
  • 返回值:如果signum是set中的一个成员,则返回非零值(通常为 1);如果signum不是set的成员,则返回 0;出现错误时返回 -1 并设置 errno以指示错误的原因。

Tips:注意,在使用sigset_t类型的变量之前,一定要先调用sigemptyset、sigfillset做初始化,使信号集处于确定的状态,之后就可以调用sigaddset、sigdelset在该信号集中添加或删除某个有效信号。

问:调用信号集操作函数后,有没有将数据(信号的设置)写入到内核中,从而修改PCB中有关信号的字段呢?

  • 没有,因为此处的sigset_t类型的变量,是在用户空间中开辟的一块空间,它用于在用户态表示一个信号集,这些函数允许用户空间的程序对信号集进行操作,这些操作实际上是对用户空间的信号集进行修改,若想要将这些修改后的信号集写入到进程内核的PCB中,还需要通过特定的系统调用。

4.4. sigprocmask

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

  1. 功能:读取或更改当前进程的信号屏蔽字。

  2. 返回值:成功返回0,失败返回-1,并设置errno以指示错误类型。

  3. 参数:set参数:指向信号集的指针,它指定了新的信号集; oldset参数:指向信号集的指针,用于存储修改前的信号屏蔽字。how参数指定如何修改当前的信号屏蔽字。

a. 如果只想读取当前的信号屏蔽字,将set设置为NULL,oset为非空指针,则读取到的信号屏蔽字通过oset传出。
b. 如果set为非空指针,修改当前的信号屏蔽字,how指示如何修改,如果不需要保存旧的信号屏蔽字,则oset可以被设置为NULL。
c. 如果oset和set都为非空指针,先会把原来的信号屏蔽字保存在oset中,再根据set和how来修改信号屏蔽字。

4.5. sigpending

int sigpending(sigset_t* set);

  1. 功能:获取当前进程的未决信号集。

  2. 返回值:成功返回0,失败返回-1,并设置errno以指示错误类型。

  3. set参数:指向信号集的指针,用于存储当前进程的未决信号集,它会将当前进程的未决信号集复制到set指向的信号集中。

应用场景1实现:屏蔽2号信号 -> 获取当前进程的pending位图,并打印 -> 给进程发送2号信号 -> 获取当前进程的pending位图,并打印 -> 解除2号信号的屏蔽。

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>using namespace std;void PrintSig(sigset_t pending) 
{cout << "pending bitmap: ";for(int i = 31; i > 0; i--){if(sigismember(&pending, i))cout << "1";else cout << "0";}cout << endl;sleep(1);
}void handle(int signo) 
{cout << "2 signo 递达中" << endl;sigset_t pending;sigemptyset(&pending);int n3 = sigpending(&pending);PrintSig(pending);  //细节:先清空pengding位图,在递达cout << "2 signo 递达完毕" << endl;
}int main()
{signal(2, handle);  //设置2号信号捕捉的方法//1.阻塞2号信号sigset_t set, oldset;sigemptyset(&set);sigaddset(&set, 2);sigemptyset(&oldset);int n1 = sigprocmask(SIG_SETMASK, &set, &oldset);assert(n1 == 0);cout << "block 2 signo success! pid: " << getpid() << endl;sleep(1);int cnt = 0;while(true){//2.获取penging位图sigset_t pending;sigemptyset(&pending);int n2 = sigpending(&pending);assert(n2 == 0);//3.打印pending位图PrintSig(pending);cnt++;//4.解除对2号信号的阻塞if(cnt == 8) {cout << "release 2 signo block!" << endl;n2 = sigprocmask(SIG_UNBLOCK, &set, &oldset); //解除完后,立即递达assert(n2 == 0);}}return 0;
}

应用场景2:对所有信号进行阻塞,观察收到所有信号时是否都未被递达,进程只能正常退出吗

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>using namespace std;void PrintSig(sigset_t pending) {cout << "pending bitmap: ";for(int i = 31; i > 0; i--)     {if(sigismember(&pending, i))cout << "1";else cout << "0";}cout << endl;sleep(1);}int main()
{sigset_t set, oldset;sigemptyset(&set);sigemptyset(&oldset);cout << "begin block all signo! pid: " << getpid() << endl;for(int i = 31; i > 0; i--) //将1~31号阻塞{sigaddset(&set, i);int n1 = sigprocmask(SIG_SETMASK, &set, &oldset);assert(n1 == 0);}while(true){sigset_t pending;sigemptyset(&pending);int n2 = sigpending(&pending); //获取pending位图assert(n2 == 0);PrintSig(pending); //打印pending位图}return 0; 
}

5. 信号的处理

5.1. 信号处理常见三种方式

  1. 执行默认处理动作。

  2. 忽略。

  3. 自定义捕捉信号函数handle。

这个处理函数的执行要求内核切换到用户态,这种方式称为捕捉一个信号。

5.2. 信号捕捉的方式

信号捕捉:信号的处理动作是用户的自定义函数,信号递达时就调用这个函数,这称为信号捕捉。

5.2.1. signal

sighandler_t sigal(int signum,sighandler_t handler);

功能:设置信号处理函数。

参数:signum参数:要处理的信号编号、handler参数:信号处理函数指针,handler可以是用户自定义的函数指针,也可以是预定义的常量(SIG_IGN,忽略信号,或SIG_DFL,默认动作)。

5.2.2. sigaction

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

  1. 功能:读取或修改与指定信号相关联的处理动作。

  2. 返回值:成功返回0,失败返回-1,并设置errno以指示错误原因。

  3. 参数:signum参数:指定要捕捉或处理的信号编号; oldact参数:输出型参数,如果为非空指针,保存了信号原来的处理动作; act参数:指定了信号新的处理动作。

sa_handler:指向信号处理函数的指针。 如果sa_handler被赋值为常数SIG_IGN,表示忽略信号、如果sa_handler被赋值为常数SIG_DFL,表示执行系统的默认处理动作、如果sa_handler被赋值为一个函数指针,表示自定义函数捕捉信号。

sig_flags:此处设置为0即可。 sa_sigaction:是实时信号的处理函数,此处不做解释。

sa_restorer:此处不管。

  1. 当某个信号的处理函数被调用时,OS自动将当前信号加入到进程的信号屏蔽字中,直到信号处理函数返回时,解除对当前信号的屏蔽,这样防止了信号被嵌套式的捕捉处理,如果此信号再次被产生时,它会被阻塞到当前处理结束为止。 如果调用信号处理函数时,除了屏蔽当前信号之外,还希望自动屏蔽其他信号,就可以用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
struct sigaction {void (*sa_handler)(int);  // 信号处理器函数指针void (*sa_sigaction)(int, siginfo_t *, void *);  // 异步安全信号处理器函数指针sigset_t sa_mask;  // 信号掩码int sa_flags;  // 标志位void (*sa_restorer)(void);  // 用于恢复上下文
};
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>using namespace std;void PrintSig(sigset_t pending) {cout << "pending bitmap: ";for(int i = 31; i > 0; i--)     {if(sigismember(&pending, i))cout << "1";else cout << "0";}cout << endl;sleep(1);}void handler(int signo) 
{cout << "get signo: " << signo << endl;sigset_t pending;sigemptyset(&pending);while(true){int n3 = sigpending(&pending);assert(n3 == 0);PrintSig(pending);  }
}int main()
{struct sigaction act, oact;act.sa_handler = handler;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);sigaddset(&act.sa_mask, 5);int n = sigaction(2, &act, &oact);assert(n == 0);cout << "I am a process! pid: " << getpid() << endl;while(true)sleep(1);return 0;
}

5.3. 信号什么时候被处理的

5.3.1. 用户态、内核态

问:信号什么时候被处理的?

  • 进程从内核态切换回用户态的时候,信号会被检测并处理。
  1. 用户态:当进程执行自己编写的代码或调用库函数时,通常处于用户态。

在用户态下进程的权限受到限制,不能直接访问内核数据和代码或执行特权指令,只能访问自己地址空间。

用户态下的代码通常是应用程序或库函数代码,它们通过系统调用等方式向OS请求服务,以完成各种功能。

  1. 内核态:进程进行系统调用或陷入到OS内部执行特定的任务,通常处于内核态。

在内核态下进程具有最高的权限,可以直接访问内核所有资源(如:硬件设备、内核数据结构、内核代码和数据等),并且可以执行一些特权指令。

内核状态下的代码通常是操作系统内核代码。

  1. 在计算机体系结构中,CPU特权级别是用来区分当前进程的运行模式(如:内核模式、用户模式等)的一种机制,从而保护了系统的安全性和稳定性,存放在CS段寄存器中。 在x86架构中,特权级别通常分为4个级别,分别为0、1、2、3。

内核态:CPU处于最高特权级别,0级。

用户态:CPU处于最低特权级别,3级。

  1. 系统调用(内核态下执行)、库函数以及用户提供自定义函数(用户态下执行),不仅是调用一个函数,还涉及身份切换。

当进程执行系统调用函数时,OS会自动将进程的身份由用户态变为内核态,让内核执行系统调用对应的任务,完成任务后,再将进程的身份由内核态转变为用户态,以便进程在用户态执行后续的代码。

5.3.2. 重谈地址空间

  1. 用户空间存储用户程序的代码和数据,用户空间通过用户级页表与物理内存建立映射关系,用户空间通常占据地址空间范围[0, 3]GB。

  2. 内核空间存储OS的代码和数据,内核空间通过内核级页表与物理内存建立映射关系,内核空间通常占据地址空间范围[3, 4]GB。

  3. 每个进程都有一个用户级页表,用户级空间确保了每个进程地址空间相互隔离。

  4. 内核级页表在整个OS中只有一份,使得所有进程共享相同的内核级页表,指向相同的OS代码和数据。

Tips:a. 进程无论如何切换,都能找到OS;b. 指向系统调用或访问系统数据,其实还是在自己的地址空间内进行跳转。

5.4. 内核如何实现信号的捕捉

       ![](https://cdn.nlark.com/yuque/0/2024/png/42574816/1728637030076-60509ee1-4d6d-458d-8920-c2ceb2bf3d34.png)

  1. 在执行主控制流程(main函数)的某条指令时,发生中断、异常或系统调用,使得进程需要从用户态切换到内核态。

  2. 当内核处理完毕后准备返回用户态的main函数时,内核会检查penging位图中是否有可以递达的信号。

  3. 如果可以递达的信号处理动作是默认或忽略,在执行完信号的处理动作后,就在pending位图中清除信号对应的标志位,如果没有其他信号需要递达,就直接返回到用户态,从主控制流程中上次被中断的地方继续向下执行。

如果可以递达的信号处理动作是执行自定义捕捉函数,因为自定义捕捉函数的代码是在用户空间的,就得要切换到用户态执行对应的捕捉函数。

  1. 执行完后通过特殊的系统调用sigreturn再次切换到内核态,如果没有其他可以递达的信号,就直接返回到用户态,从主控制流程中上次被中断的地方继续向下执行。

5.5 可重入函数

  1. 可重入函数:指一个函数可以被多个任务(线程、进程)并发执行,而不会出现错误或不一致的结果。不可重入函数概念与之相反。

这意味着函数可以被不同的控制执行流调用,函数在执行过程中可以被中断,然后再另一个执行流中再次被调用,而不会破坏函数的正确性和数据的完整性。

重入:像上例,insert函数被不同的控制流调用,在第一次调用还未返回时就再次进入该函数,这称为重入。

insert函数访问一个全局链表,有可能因为重入而造成错乱(node2节点丢失),这样的函数就成为不可重入函数。

如果一个函数只访问自己的局部变量或参数,则称为可重入函数。

为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

独立的栈空间:每个控制流程都有自己独立的运行环境,其中包括独立的栈空间。意味着每个控制流程在调用函数时,都会在自己的栈上创建该函数局部变量和参数的副本。

操作的独立性:每个控制流程都在操作自己的局部变量副本,它们的操作不会影响其他控制流程的局部变量副本。

函数执行的独立性:每个控制流程对函数的执行也是独立的,只在自己的上下文中进行,不会影响其他控制流程。

  1. 如果一个函数符合以下条件之一,则是不可重入的:a. 调用了malloc和free,因为malloc也是使用全局链接来管理堆的; b. 调用了标准I/O库函数,因为标准库的很多实现都以不可重入的方式使用全局数据结构。

5.6. volatile关键字

  1. 编译器(gcc、g++)提供了多个级别的优化,-O0不启用优化、-O1启用基本的优化、-O2启用更高级别的优化,-O. . . ,数字越大,优化级别越高。

  2. 当编译器对代码进行优化时,它会减少访问内存的次数,以提高程序的运行速度。

eg:在循环中频繁访问某个变量g_val,编译器可能会将存储在内存中的g_val的值,拷贝到CPU的寄存器中,因为访问寄存器的速度比访问内存的速度快的多,如果修改了g_val值,则是对内存中的g_val进行了修改,这个修改不会自动反映到存储在寄存器中的g_val值。

int g_val;void handler(int signo)
{(void)signo; //骗过编译器,不要警告,此变量不光光是定义,在后面还使用了它g_val = 1;  //对存放在内存中的g_val值进行修改,不会自动更新寄存器的值cout << "g_val由0变为1" << endl;
}int main()
{signal(2, handler); /*编译器会作优化,将存放在内存中的g_val值拷贝到CPU寄存器中,之后直接从寄存器中读取此变量的值*/while(!g_val) ; printf("process normal quit!\n");return 0;
}

  1. volatile关键字的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许作优化,对该变量的任何操作,都必须在真实的内存中进行操作。
volatile int g_val;void handler(int signo)
{(void)signo; //骗过编译器,不要警告,此变量不光光是定义,在后面还使用了它g_val = 1;  //对存放在内存中的g_val值进行修改,不会自动更新寄存器的值cout << "g_val由0变为1" << endl;
}int main()
{signal(2, handler); /*编译器会作优化,将存放在内存中的g_val值拷贝到CPU寄存器中,之后直接从寄存器中读取此变量的值*/while(!g_val) ; printf("process normal quit!\n");return 0;
}

5.7. SIGCHLD信号

  1. 子进程在终止时会给父进程发SIGCHLD信号(17号信号),该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程只需要在信号处理函数中调用wait清理子进程即可。
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<signal.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;void clean(int signo)
{if(signo == SIGCHLD){/*在同一时刻多个子进程退出,会同时向父进程发送SIGCHLD信号,但pending位图只会记录一次,即只清理了一个子进程的资源,所以使用while循环*/while(true) {/*若为阻塞等待,只有部分子进程退出,由于while循环,会再次调用waitpid,就会一直在这阻塞,所以使用非阻塞轮询等待*/pid_t rid = waitpid(-1, nullptr, WNOHANG);if(rid > 0)cout << "child wait success!" << endl;else break;}}
}int main()
{signal(SIGCHLD, clean);for(int i = 0; i < 100; i++) //父进程创建100个子进程{pid_t id = fork();if(id == 0){cout << "I am child process, pid: " << getpid() << endl;exit(0); //100个子进程全部退出}sleep(1);}return 0;
}
  1. 要想不产生僵尸的另一种方法:将SIGCHLD信号的自定义捕捉函数的捕捉方法handler设置为忽略SIG_IGN,这样fork出来的子进程在终止时自动清理掉,不会产生僵尸,也不会通知父进程。

与系统默认处理动作中的忽略无区别,但这是个特例,这只适用于父进程不需要知道子进程的执行情况,反之,还得在自定义捕捉函数中设置waitpid获取子进程的退出码、退出信号。

int main()
{//子进程在终止时自动清理掉,不会产生僵尸,也不会通知父进程signal(SIGCHLD, SIG_IGN); for(int i = 0; i < 100; i++) //父进程创建100个子进程{pid_t id = fork();if(id == 0){cout << "I am child process, pid: " << getpid() << endl;exit(0); //100个子进程全部退出}sleep(1);}return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue 计算属性和监听器

文章目录 一、计算属性1. 计算属性定义2. computed 比较 methods3. 计算属性完整写法 二、监听器1. 普通监听2. 添加额外配置项 一、计算属性 1. 计算属性定义 概念&#xff1a;基于现有的数据&#xff0c;计算出来的新属性&#xff0c;依赖的数据变化&#xff0c;自动重新计…

【计网】实现reactor反应堆模型 --- 框架搭建

没有一颗星&#xff0c; 会因为追求梦想而受伤&#xff0c; 当你真心渴望某样东西时&#xff0c; 整个宇宙都会来帮忙。 --- 保罗・戈埃罗 《牧羊少年奇幻之旅》--- 实现Reactor反应堆模型 1 前言2 框架搭建3 准备工作4 Reactor类的设计5 Connection连接接口6 回调方法 1 …

外包干了2年,快要废了。。。

先说一下自己的情况&#xff0c;普通本科毕业&#xff0c;在外包干了2年多的功能测试&#xff0c;这几年因为大环境不好&#xff0c;我整个人心惊胆战的&#xff0c;怕自己卷铺盖走人了&#xff0c;我感觉自己不能够在这样蹉跎下去了&#xff0c;长时间呆在一个舒适的环境真的会…

linux驱动-i2c子系统框架学习(2)

linux驱动-i2c子系统框架学习(1) 在这篇博客里面已经交代了i2c设备驱动层&#xff0c;主要的功能就是编写具体i2c的外设驱动&#xff0c;和创建设备接点给上层使用 &#xff0c;按之前学习的字符设备&#xff0c;有了设备节点&#xff0c;就可以对硬件操作了&#xff0c;在i2c…

Webserver(4.6)poll和epoll

目录 pollclient.cpoll.c epollepoll.cclient.c epoll的两种工作模式水平触发边沿触发 poll poll是对select的一个改进 select的缺点在于每次都需要将fd集合从用户态拷贝到内核态&#xff0c;开销很大。每次调用select都需要在内核遍历传递进来的所有fd&#xff0c;这个开销也…

Stable Diffusion的解读(一)

Stable Diffusion的解读&#xff08;一&#xff09; 文章目录 Stable Diffusion的解读&#xff08;一&#xff09;摘要Abstract一、机器学习部分1. Stable Diffusion的早期工作1.1 从编码器谈起1.2 第一条路线&#xff1a;VAE和DDPM1.3 第二条路线&#xff1a;VQVAE1.4 路线的交…

计算机网络——TCP篇

TCP篇 基本认知 TCP和UDP的区别? TCP 和 UDP 可以使用同一个端口吗&#xff1f; 可以的 传输层中 TCP 和 UDP在内核中是两个完全独立的软件模块。可以根据协议字段来选择不同的模块来处理。 TCP 连接建立 TCP 三次握手过程是怎样的&#xff1f; 一次握手:客户端发送带有 …

ROS话题通信机制理论模型的学习

话题通信是ROS&#xff08;Robot Operating System&#xff0c;机器人操作系统&#xff09;中使用频率最高的一种通信模式&#xff0c;其实现模型主要基于发布/订阅模式。 一、基本概念 话题通信模型中涉及三个主要角色&#xff1a; ROS Master&#xff08;管理者&#xff0…

【Android】名不符实的Window类

1.“名不符实”的Window类 Window 是一个窗口的概念&#xff0c;是所有视图的载体&#xff0c;不管是 Activity&#xff0c;Dialog&#xff0c;还是 Toast&#xff0c;他们的视图都是附加在 Window 上面的。例如在桌面显示一个悬浮窗&#xff0c;就需要用到 Window 来实现。Wi…

后端java——如何为你的网页设置一个验证码

目录 1、工具的准备 2.基本方法 3.实现类 4.实践 HTML文件&#xff1a; Java文件1:创建验证码 Java文件2:验证验证码 本文通过HUTOOL实现&#xff1a;Hutool参考文档Hutool&#xff0c;Java工具集https://hutool.cn/docs/#/ 1、工具的准备 如果我们通过hutool来实现这个…

【go从零单排】Strings and Runes 字符串和字符

Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 概念 在Go语言中&#xff0c;rune 是一个内置的数据类型&#xff0c;用于表示一个Unicode字符。它实际上是一个别名…

如何在本地Linux服务器搭建WordPress网站结合内网穿透随时随地可访问

文章目录 前言1. 安装WordPress2. 创建WordPress数据库3. 安装相对URL插件4. 安装内网穿透发布网站4.1 命令行方式&#xff1a;4.2. 配置wordpress公网地址 5. 配置WordPress固定公网地址 前言 本文主要介绍如何在Linux Ubuntu系统上使用WordPress搭建一个本地网站&#xff0c…

vue data变量之间相互赋值或进行数据联动

摘要&#xff1a; 使用vue时开发会用到data中是数据是相互驱动&#xff0c;经常会想到watch,computed&#xff0c;总结一下&#xff01; 直接赋值&#xff1a; 在 data 函数中定义的变量可以直接在方法中进行赋值。 export default {data() {return {a: 1,b: 2};},methods: {u…

在 Java 中使用脚本语言

在 Java 中使用脚本语言&#xff0c;特别是在 Java 平台上集成如 Python、JavaScript 或 Ruby 等语言&#xff0c;通常可以通过 Java 的 Scripting API 来实现。这个 API 基于 JSR 223&#xff08;“Scripting for the Java Platform”&#xff09;&#xff0c;提供了一种标准方…

大数据-212 数据挖掘 机器学习理论 - 无监督学习算法 KMeans 基本原理 簇内误差平方和

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

微服务系列六:分布式事务与seata

目录 实验环境说明 前言 一、分布式事务问题与策略 1.1 分布式事务介绍 1.2 分布式事务解决策略分析 二、分布式事务解决方案 Seata 2.1 认识Seata 2.2 Seata的工作原理 2.3 部署Seata微服务 2.3.1 准备数据库表 2.3.2 准备配置文件 2.3.3 docker部署 2.4 微服务集…

Java 上机实践2(基础数据类型与数组)

&#xff08;大家好&#xff0c;今天分享的是Java的相关知识&#xff0c;大家可以在评论区进行互动答疑哦~加油&#xff01;&#x1f495;&#xff09; 目录 实验一&#xff1a;输出希腊字母表 一、实验目的 二、实验要求 三、程序代码 四、实验结果 实验二&#xff1a;…

w024基于SpringBoot的企业客户管理系统的设计与实现

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0…

并发编程volatile精解

多线程下变量的不可见性 在多线程并发执行的情况下&#xff0c;多个线程修改共享的成员变量&#xff0c;会出现一个线程修改了共享变量的值后&#xff0c;另一个线程不能直接看到该线程修改后的变量最新值。(多线程下修改共享变量会出现变量修改值后的不可见性) 可见性问题…

十款外贸软件盘点,专注企业订单业务管理

在当今全球化的市场环境中&#xff0c;外贸企业的发展面临着诸多挑战与机遇。如何高效管理企业业务&#xff0c;提升运营效率&#xff0c;成为外贸企业在激烈竞争中脱颖而出的关键。外贸业务管理ERP软件作为一种强大的工具&#xff0c;能够整合企业资源、优化管理流程、实现数据…