【Linux】25.进程信号(2)
文章目录
- 4.捕捉信号
- 4.1 重谈地址空间
- 4.2 内核如何实现信号的捕捉
- 4.3 sigaction
- 4.4 可重入函数
- 4.5 volatile
- 4.6 SIGCHLD信号(了解)
4.捕捉信号
4.1 重谈地址空间
用户页表有几份?
有几个进程,就有几份用户级页表–进程具有独立性
内核页表有几份?
1份
每一个进程看到的3~4GB的东西都是一样的。整个系统中进程再怎么切换,3,4GB的空间的内容是不变的。
进程视角:我们调用系统中的方法,就是在我自己的地址空间中进行执行的。
操作系统视角:任何一个时刻,都有有进程执行。我们想执行操作系统的代码,就可以随时执行。
操作系统的本质:基于时钟中断的一个死循环。
计算机硬件中,有一个时钟芯片,每个很短的时间,向计算机发送时钟中断
4.2 内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了
SIGQUIT
信号的处理函数sighandler
。 当前正在执行main
函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main
函数之前检查到有信号SIGQUIT
递达。 内核决定返回用户态后不是恢复main
函数的上下文继续执行,而是执行sighandler
函 数,sighandler
和main
函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。sighandler
函数返回后自动执行特殊的系统调用sigreturn
再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main
函数的上下文继续执行了。
4.3 sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction
函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0
,出错则返回- 1
。signo
是指定信号的编号。若act
指针非空,则根据act修改该信号的处理动作。若oact
指针非空,则通过oact
传出该信号原来的处理动作。act
和oact
指向sigaction
结构体。将
sa_handler
赋值为常数SIG_IGN
,传给sigaction
表示忽略信号。赋值为常数SIG_DFL
,表示执行系统默认动作。赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void
,可以带一个int
参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main
函数调用,而是被系统所调用。
sigaction()
函数中的两个指针参数*act
和*oldact
有不同的用途:
*act
(新动作):
指向要设置的新的信号处理方式
如果不为
NULL
,系统会按照这个结构体设置新的信号处理方式用于指定我们"想要"的信号处理方式
*oldact
(旧动作):
用于保存信号的原有处理方式
如果不为
NULL
,系统会将原来的信号处理方式保存在这个结构体中常用于之后恢复原有的信号处理方式
示例:
struct sigaction new_action, old_action;
// 设置新的处理方式
new_action.sa_handler = my_handler; // 设置处理函数
sigemptyset(&new_action.sa_mask); // 清空信号掩码
new_action.sa_flags = 0; // 设置标志
// 设置SIGINT的处理方式,同时保存原有设置
sigaction(SIGINT, &new_action, &old_action);
// ... 一段时间后 ...
// 恢复原有的处理方式
sigaction(SIGINT, &old_action, NULL);
注意:
- 如果只想设置新的处理方式,可以将
oldact
设为NULL
- 如果只想查询当前的处理方式,可以将
act
设为NULL
,oldact
指向一个结构体
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用
sa_mask
字段说明这些需要额外屏蔽的信号,信号处理函数返回时,这些额外屏蔽的信号也会自动解除屏蔽。
代码:
#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
// 信号处理函数(被注释掉的版本)
// 不是必须调用wait,但建议调用以避免僵尸进程
// void handler(int signo)
// {
// sleep(5); // 模拟信号处理需要一定时间
// pid_t rid;
// // WNOHANG: 非阻塞等待,如果没有子进程退出立即返回0
// while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
// {
// cout << "I am proccess: " << getpid() << " catch a signo: " << signo
// << "child process quit: " << rid << endl;
// }
// }
int main()
{
// 忽略SIGCHLD信号(17),避免产生僵尸进程
// SIG_DFL是默认处理方式,SIG_IGN是忽略信号
signal(17, SIG_IGN);
// 创建10个子进程
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0) // 子进程
{
while (true)
{
cout << "I am child process: " << getpid()
<< ", ppid: " << getppid() << endl;
sleep(5);
break;
}
cout << "child quit!!!" << endl;
exit(0); // 子进程退出
}
sleep(1); // 父进程每隔1秒创建一个子进程
}
// 父进程循环
while (true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
// 以下是被注释的其他示例代码:
// volatile关键字示例
// volatile int flag = 0; // volatile防止编译器优化
// void handler(int signo)
// {
// cout << "catch a signal: " << signo << endl;
// flag = 1;
// }
// int main()
// {
// signal(2, handler); // 设置SIGINT(Ctrl+C)的处理函数
// // flag可能被优化到CPU寄存器中,volatile防止这种优化
// while(!flag); // flag为0时循环继续
// cout << "process quit normal" << endl;
// return 0;
// }
// 信号pending示例
// pending位图从1变为0的时机:在执行信号处理函数之前清零
// 处理信号时会将该信号添加到block表中,防止信号处理函数被重入
// 打印当前进程的pending信号集
// void PrintPending()
// {
// sigset_t set;
// sigpending(&set); // 获取当前pending的信号集
// // 打印1-31号信号的pending状态
// for (int signo = 1; signo <= 31; signo++)
// {
// if (sigismember(&set, signo))
// cout << "1";
// else
// cout << "0";
// }
// cout << "\n";
// }
// void handler(int signo)
// {
// cout << "catch a signal, signal number : " << signo << endl;
// while (true)
// {
// PrintPending();
// sleep(1);
// }
// }
// sigaction使用示例
// int main()
// {
// // struct sigaction act, oact;
// // memset(&act, 0, sizeof(act));
// // memset(&oact, 0, sizeof(oact));
// // sigemptyset(&act.sa_mask); // 清空信号屏蔽字
// // sigaddset(&act.sa_mask, 1); // 添加要屏蔽的信号
// // sigaddset(&act.sa_mask, 3);
// // sigaddset(&act.sa_mask, 4);
// // act.sa_handler = handler; // 设置信号处理函数
// // sigaction(2, &act, &oact); // 设置SIGINT的处理方式
// // while (true)
// // {
// // cout << "I am a process: " << getpid() << endl;
// // sleep(1);
// // }
// return 0;
// }
这段代码主要演示了:
- 信号处理和僵尸进程避免
- 进程创建和父子进程通信
volatile
关键字的使用- 信号的
pending
机制sigaction
的使用方法
4.4 可重入函数
main
函数调用insert
函数向一个链表head
中插入节点node1
,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler
函数,sighandler
也调用insert
函数向同一个链表head
中插入节点node2
,插入操作的 两步都做完之后从sighandler
返回内核态,再次回到用户态就从main
函数调用的insert
函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main
函数和sighandler
先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。像上例这样,
insert
函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert
函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的:
调用了
malloc
或free
,因为malloc
也是用全局链表来管理堆的。调用了标准
I/O
库函数。标准I/O
库的很多实现都以不可重入的方式使用全局数据结构。
不可重入函数特征:
- 使用静态或全局变量
- 返回指向静态变量的指针
- 调用
malloc/free
- 调用不可重入的系统函数
可重入函数特征:
- 仅使用局部变量
- 数据通过参数传递
- 不调用不可重入函数
- 不依赖共享资源
4.5 volatile
- 基本作用:
// 没有volatile的问题
int flag = 0;
while (!flag) {
// 编译器可能优化为死循环
// 因为编译器认为没人修改flag
}
// 使用volatile解决
volatile int flag = 0;
while (!flag) {
// 每次都会从内存重新读取flag
// 而不是使用寄存器中的值
}
volatile
作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
- 常见场景:
// 1. 信号处理
volatile sig_atomic_t signal_flag = 0;
void signal_handler(int signo) {
signal_flag = 1; // 修改共享变量
}
int main() {
signal(SIGINT, signal_handler);
while (!signal_flag) {
// 等待信号处理程序修改flag
}
}
// 2. 硬件寄存器访问
volatile uint32_t* hardware_reg = (uint32_t*)0x20000000;
*hardware_reg = 0x1; // 每次都直接写入硬件
- volatile的特性:
volatile int counter = 0;
void example() {
// 1. 防止优化删除
counter++; // 不会被优化掉
// 2. 保证读写顺序
int temp = counter; // 确保在counter++之后读取
// 3. 每次都访问内存
for (int i = 0; i < 10; i++) {
counter++; // 每次都读写内存
}
}
- volatile的局限:
// volatile不能保证原子性
volatile int shared = 0;
// 多线程访问时仍需要互斥锁
mutex mtx;
void thread_func() {
lock_guard<mutex> lock(mtx);
shared++;
}
主要作用:
- 防止编译器优化
- 保证每次都从内存读取
- 保证代码执行顺序
- 用于多线程共享或硬件访问
不能做到:
- 不保证原子性
- 不保证线程安全
- 不是线程同步工具
4.6 SIGCHLD信号(了解)
之前讲过用wait
和waitpid
函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD
信号,该信号的默认处理动作是忽略。父进程可以自定义SIGCHLD
信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait
清理子进程即可。