Ciallo~(∠・ω< )⌒☆ ~ 今天,我将和大家一起学习 linux 的信号~

❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️
澄岚主页:椎名澄嵐-CSDN博客
Linux专栏:★ Linux ★ _椎名澄嵐的博客-CSDN博客
❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️
目录
[壹 信号基础概念](#壹 信号基础概念)
[贰 信号产生](#贰 信号产生)
[2.1 键盘产生信号](#2.1 键盘产生信号)
[2.2 系统调用产生信号](#2.2 系统调用产生信号)
[2.2.1 kill](#2.2.1 kill)
[2.2.2 raise](#2.2.2 raise)
[2.2.3 abort](#2.2.3 abort)
[2.3 调用系统命令向进程发信号kill](#2.3 调用系统命令向进程发信号kill)
[2.4 异常产生信号](#2.4 异常产生信号)
[2.5 软件条件产生信号](#2.5 软件条件产生信号)
[叁 信号保存](#叁 信号保存)
[3.1 相关概念](#3.1 相关概念)
[3.2 在内核中的表示](#3.2 在内核中的表示)
[3.3 sigset_t](#3.3 sigset_t)
[3.4 信号集操作函数](#3.4 信号集操作函数)
[3.4.1 sigprocmask](#3.4.1 sigprocmask)
[3.4.2 sigpending](#3.4.2 sigpending)
[3.4.3 整合代码](#3.4.3 整合代码)
[肆 信号处理](#肆 信号处理)
[4.1 初步结论](#4.1 初步结论)
[4.2 操作系统是怎么运行的](#4.2 操作系统是怎么运行的)
[4.2.1 硬件中断](#4.2.1 硬件中断)
[4.2.2 时钟中断](#4.2.2 时钟中断)
[4.2.3 软中断](#4.2.3 软中断)
[4.3 内核态和用户态](#4.3 内核态和用户态)
[4.4 sigaction](#4.4 sigaction)
[伍 可重入函数](#伍 可重入函数)
[陆 volatile](#陆 volatile)
[柒 SIGCHLD信号](#柒 SIGCHLD信号)
[~ 完 ~](#~ 完 ~)
壹 信号基础概念
信号是一种给进程发送的,用来进行事件异步通知的机制。
信号的产生相对于进程的运行是异步的。
信号是发送给进程的。
可以类比于闹钟,红绿灯,上课铃,人的脸色等。人就相当于进程。这些东西会中断人正在做的事情。
信号要点:
-
进程在信号还没有产生的时候,就知道如何处理了。
-
处理信号不是立即处理,也可以等一会处理或者等合适的时候处理。
-
OS程序员设计进程的时候,进程已经内置了对于信号的识别和处理方式。
-
给进程产生信号的的信号源非常多。
贰 信号产生
产生信号的方式很多:
2.1 键盘产生信号
1. 键盘怎么产生信号
ctrl+c 就是给目标进程发送2号信号的,相当一部分的信号处理方法是让自己终止。
信号查看:每个信号都有对应的数字和宏。34-64为实时信号(需要立即处理),1-31为普通信号(可以不被立即处理)。

进程收到信号后,会在合适的时候处理信号,处理三选一:默认处理动作,自定义信号处理动作,忽略处理。ctrl+c 发送的2号信号的默认处理动作就是默认处理动作。
更改自定义信号处理:
cpp
#include <signal.h>
typedef void (*sighandler_t)(int); // 函数指针 类型
sighandler_t signal(int signum, sighandler_t handler);
// 第一个参数为1-31 34-64(信号),第二个参数为一个返回值为void的函数指针
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handerSig(int sig)//会把signal函数的第一个参数传过来
{
std::cout << "获得了一个信号: " << sig << std::endl;
}
int main()
{
signal(SIGINT, handerSig);
int cnt = 0;
while(true)
{
std::cout << "Ciallo!~ " << cnt++ << std::endl;
sleep(1);
}
return 0;
}

man 7 signal可以查看具体信号:Term为终止,Ign为忽略...

当所有信号都被自定义时:(9号信号无法被自定义捕捉,仍然可以终止进程)
cpp
for (int i = 1; i < 31; i++)
signal(i, handerSig);

2. 那什么是目标进程呢~
进程有前台进程和后台进程 :
./XXX 为前台进程 键盘产生的信号只能发给前台进程
./XXX & 为后台进程 CTRL+C不做处理
命令行shell进程默认就是前台进程
后台进程无法从标准输入(键盘)中获取内容,前台进程可以(前台进程本质就是要从键盘获取数据,组合键也是键盘输入 )。但都可以向标准输出打印。前台进程只有一个,后台进程可以很多个。屏幕相当于共享资源。
bash
jobs // 查看所有后台进程
fg 任务号 // 后台转前台
ctrl Z // 把进程切换到后台
bg 任务号 // 让后台进程恢复运行
3. 信号不是立即处理的,需要记录下来,等待合适的时候处理。记录在哪,怎么记录:
task_struct 中的 unsigned int sigs;把这个整数当成位图:
比特位位置为信号编号。
比特位内容为是否收到。
比如0000.......0010为收到了一个二号信号。
发送信号的本质就是:向目标进程写信号,就是修改位图,pid和信号编号。
task_struct属于OS内的数据结构对象,修改位图就是修改内核数据,而内核数据只有OS才能改,所以不管信号怎么产生,必须让给OS发送,OS必须提供系统调用,kill就是系统调用。
2.2 系统调用产生信号
系统调用也可以产生信号。
2.2.1 kill
cpp
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
cpp
// mykill.cc
#include <iostream>
#include <sys/types.h>
#include <signal.h>
// ./mykill signumber pid
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "./mykill signumber pid" << std::endl;
}
int signum = std::stoi(argv[1]);
pid_t target = std::stoi(argv[2]);
int n = kill(target, signum);
if (n == 0)
{
std::cout << "send " << signum << " to " << target << "success ~" << std::endl;
return 0;
}
return 0;
}
2.2.2 raise
cpp
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
int raise(int sig);
cpp
int main()
{
for (int i = 1; i < 32; i++)
signal(i, handerSig);
for (int i = 1; i < 32; i++)
{
sleep(1);
if (i == 9 || i == 19)
continue;
raise(i); // 给进程发i号信号
}
return 0;
}

可以验证到9号和19号信号不能被自定义。
2.2.3 abort
cpp
NAME
abort - cause abnormal process termination
SYNOPSIS
#include <stdlib.h>
void abort(void);
cpp
int main()
{
for (int i = 1; i < 32; i++)
signal(i, handerSig);
int cnt = 0;
while(true)
{
std::cout << "Ciallo!~ " << cnt++ << " pid: " << getpid() << std::endl;
abort();
sleep(1);
}
return 0;
}
可以看到使用abort时,已经被自定义的6号信号会被恢复成默认的终止。

2.3 调用系统命令向进程发信号kill
bash
kill -信号号 pid
2.4 异常产生信号
比如当运行的程序发生除零错误时会收到8号信号SIGFPE:
cpp
int main()
{
for (int i = 1; i < 32; i++)
signal(i, handerSig);
int cnt = 0;
while(true)
{
std::cout << "Ciallo!~ " << cnt++ << " pid: " << getpid() << std::endl;
// 发生除零异常
int a = 10;
a /= 0;
sleep(1);
}
return 0;
}

同样,野指针会收到11号信号,SIGSEGV段错误。
在信号的处理动作中,有Core和Term两种终止方式,有什么区别呢~
Term:直接退出
Core :核心,会在当前路径下形成一个文件,进程异常退出时,进程在内存中的核心数据从内存拷贝到磁盘, 形成一个文件,叫做核心转储,支持Debug。云服务器上,core和dump功能默认是被禁止的。
可以 ulimit -c 40960 打开此功能~ 若打开了此功能,遇到异常,会终止进程并core dump,在运行崩溃后,再gdb时输入core-file core就可以查看错误~ 叫做事后调试

cpp
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(2);
printf("Ciallo~\n");
printf("Ciallo~\n");
printf("Ciallo~\n");
printf("Ciallo~\n");
printf("Ciallo~\n");
int a = 10;
a /= 0;
printf("Ciallo~\n");
printf("Ciallo~\n");
printf("Ciallo~\n");
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
printf("signal: %d, exitcode: %d, core dump: %d\n", (status&0x7F), (status>>8)&0xFF, (status>>7)&0x1);
return 0;
}

2.5 软件条件产生信号
比如一个管道文件,把读端关闭时,写端就会自动崩溃,发送信号SIGPIPE ~
管道文件是一种软件,读端关闭就是软件条件不满足,会终止进程~
再举个例子:
alarm:设置闹钟:
alarm(5): 设定时间5s结束,OS就会给进程发信号~~
alarm(0): 取消闹钟~
函数返回值为0或者以前设定的剩余秒数。
cpp
NAME
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
cpp
void handerSig(int sig)//会把signal函数的第一个参数传过来
{
std::cout << "获得了一个信号: " << sig << std::endl;
exit(13);
}
int main()
{
for (int i = 0; i <= 32; i++)
signal(i, handerSig);
alarm(1);//闹钟1秒后响
int cnt = 0;
while (true)
{
std::cout << "count: " << cnt++ << std::endl; // IO打印效率低
cnt++;
}
return 0;
}

会发现程序一秒内循环的次数很少,是因为IO效率很低,可以更改为下代码:
cpp
int cnt = 0;
void handerSig(int sig)
{
std::cout << "获得了一个信号: " << sig << "cnt: " << cnt << std::endl;
exit(13);
}
int main()
{
signal(SIGALRM, handerSig);
alarm(1);//闹钟1秒后响
while (true)
cnt++;
return 0;
}

现在想要实现一个程序,进程一直暂停着,一旦收到一个信号,就唤醒一次,完成一些任务,可以这样实现:
cpp
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
////////////////////func///////////////////////////////
void Sched()
{
std::cout << "进程调度" << std::endl;
}
void MemManger()
{
std::cout << "内存管理" << std::endl;
}
void Fflush()
{
std::cout << "刷新程序" << std::endl;
}
///////////////////////////////////////////////////////
using func_t = std::function<void()>; // 返回值为void的函数 命名为func_t类型
std::vector<func_t> funcs; // 返回值为void的函数列表
// 每隔一秒,完成一些任务
void handerSig(int sig) //会把signal函数的第一个参数传过来
{
std::cout << "##############################################" << std::endl;
for (auto f : funcs) // 执行函数列表内的函数
{
f();
}
std::cout << "##############################################" << std::endl;
int n = alarm(1); // 重新设定一个闹钟,成为循环
}
int main()
{
funcs.push_back(Sched);
funcs.push_back(MemManger);
funcs.push_back(Fflush);
// 遇到SIGALRM信号,执行handerSig,handerSig中又会设置下一个闹钟
signal(SIGALRM, handerSig);
alarm(1); // 初始闹钟
while (true)
{
// 进程一直暂停,一旦收到一个信号,唤醒一次
pause();
}
return 0;
}
操作系统就是这种类型的程序,进程暂停着,以信号作为驱动。
操作系统中又很多闹钟,可以将这些闹钟管理起来,链表结构体内容为:
cpp
struct timer_list {
struct list_head entry; // 闹钟列表起始
unsigned long expires; // 最短的闹钟剩余时间
void (*function)(unsigned long); // 收到信号后执行的任务
unsigned long data;
struct tvec_t_base_s *base;
};
用堆结构将闹钟剩余时间最短的闹钟,成为堆头,保存在结构体中,到时间后给进程发SIGALRM信号。
所以闹钟属于软件条件的一种。
叁 信号保存

3.1 相关概念
实际执行信号的处理动作称为信号递达 (Delivery)
信号递达有三种 :自定义,默认,忽略。
信号从产生到递达之间的状态,称为信号未决(Pending)。信号还在位图中,未被处理。
进程可以选择阻塞 (Block )某个信号 。也叫屏蔽信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达 ,而忽略是 在递达之后 可选的一种处理动
作。
3.2 在内核中的表示

pending表是位图,用来保存收到的信号 ,unsigned int pending有32个比特位对应32种信号,叫做未决表 。比特位位置为信号编号。比特位内容为是否收到。
block表也是一张位图,用来保存要阻塞的信号 ,比特位位置为信号编号。比特位内容为是否阻塞。
所以pending & (~block) 为哪些信号能被递达。
handler表是一个函数指针数组 ,数组下标为信号编号,内容为32种信号对应的函数方法。signal函数就是在修改handler表。把对应的函数地址填入数组。SIG_DFL为默认处理,SIG_IGN为忽略处理。
3张表共同完成了对32个信号的描述。
3.3 sigset_t
每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集 , 这个类型可以表示每个信号的"有效"或"无效"状态, 在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞, 而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。
3.4 信号集操作函数
cpp
#include <signal.h>
// 位图全置0 , 1
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
// 把位图第几号信号置1 ,0
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
// 判断位图第几号信号是否为1
int sigismember(const sigset_t *set, int signo);
3.4.1 sigprocmask
此函数用于更改或设置block表:
cpp
NAME
sigprocmask, rt_sigprocmask - examine and change blocked signals
SYNOPSIS
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
RETURN VALUE
sigprocmask() returns 0 on success and -1 on error.
第一个参数有下三种:

- SIG_BLOCK:想要阻塞哪位就在set中把哪位置1
- SIG_UNBOLOCK:想要取消阻塞哪位就在set中把哪位置1
- SIG_SETMASK:把原BLOCK表替换为set,最方便
第三个参数为输出型参数,返回旧的BLOCK表,以防改错~
3.4.2 sigpending
获取当前pending信号集(不修改)
cpp
NAME
sigpending, rt_sigpending - examine pending signals
SYNOPSIS
#include <signal.h>
int sigpending(sigset_t *set);
RETURN VALUE
sigpending() returns 0 on success and -1 on error.
pending表的修改就是上5种信号产生的方式~
3.4.3 整合代码
cpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void PrintPending(sigset_t pending)
{
printf("我是一个进程( %d ), pending: ", getpid());
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
void handler(int sig)
{
std::cout << "#######################################" << std::endl;
std::cout << "递达" << sig << "信号 ~"<< std::endl;
sigset_t pending;
int m = sigpending(&pending);
PrintPending(pending);
// 若是0000 0010 处理完2号才置0
// 若是0000 0000 处理前2号就置0
std::cout << "#######################################" << std::endl;
}
int main()
{
signal(SIGINT, handler);
// 1. 屏蔽2号信号, block信号集2号位置1
sigset_t block, oldblock;
sigemptyset(&block);
sigemptyset(&oldblock);
sigaddset(&block, SIGINT); // 到此只改了变量, 需要更新到进程中
// for (int i = 0; i <= 31; i++) // 会屏蔽所有信号, 除9外
// {
// sigaddset(&block, i);
// }
int n = sigprocmask(SIG_SETMASK, &block, &oldblock);
(void)n;
// 4. 重复获取打印
int cnt = 0;
while (true)
{
// 2. 获取pending信号集
sigset_t pending;
int m = sigpending(&pending);
// 3. 打印
PrintPending(pending);
if (cnt == 10)
{
// 5. 恢复对2号信号的阻塞, 会直接递达2号信号
// 若不对2号信号做处理, 就会按默认直接终止进程
std::cout << "解除对2号信号的屏蔽" << std::endl;
sigprocmask(SIG_SETMASK, &oldblock, nullptr);
}
sleep(1);
cnt++;
}
return 0;
}
若不改变处理动作:

最后结果:

可以发现在递达前,pending表的对应位置就置0了。
肆 信号处理
到这里会有两个问题:
信号处理有三种动作,是怎么被处理的?
信号在合适的时间被处理,什么是合适的时间?

4.1 初步结论
信号处理的方式如下:

在执行main函数时,会因为系统调用等原因从用户态进入内核态 。在内核处理完后就会用do_signal函数检查是否有待处理的信号 (三个表中pending位为1,block位为0),如果有就会进行处理:
默认处理 :如2号信号,内核直接终止进程。
忽略处理 :返回用户态跳转的位置继续运行。
自定义处理:从内核态返回用户态,以用户身份执行自定义的函数,执行完后调用系统调用sigreturn返回内核态,在内核态执行sys_sigreturn后返回用户态跳转的位置继续运行。
自定义处理的整个流程是一个无限大符号 的样子:
流程中会有四次身份切换 ,在**态只能以**身份执行 ,防止自定义函数中有非法操作,内核权限又很高。中间的交点是检查是否有待处理的信号的时间节点。
所有的进程都会被调度 ,当一个进程的时间片耗尽时,会被操作系统 拿下,这时就会进入内核态被检查信号~
4.2 操作系统是怎么运行的
4.2.1 硬件中断

在硬件视角 ,会有一个中断控制器连接着许多外设,每当外设准备好了,就会发送硬件中断 ,替换中断号n,中断控制器被激活会通知CPU,CPU得知有中断会到中断控制器获取中断号。
在软件方面 ,操作系统中会有一张中断向量表IDT ,其中维护着外设设备对应的函数指针 的数组,数组下标对应中断控制器中的中断号n,CPU得知那个外设设备准备好了,就执行那个中断服务。
操作系统不关心外设设备是否准备好,而是准备好会叫操作系统。
发中断->发信号 保存中断->记录信号
中断号->信号编号 处理中断->处理信号+自定义捕捉
软件中的信号就是在模仿硬件中的中断的思想 ~
当程序发生除零错误时,CPU内部会有一个溢出标志位EFLAGS,此标志位有效时,成为一种CPU内部触发的中断,在中断向量表中选择并执行中断服务处理异常,给目标进程发信号。
其他野指针,重复释放,缺页中断等都会被转化成硬件中断,执行对应的方法。
4.2.2 时钟中断
在操作系统 没有硬件中断要处理的时候,在干嘛呢~
答案是一直暂停着 ,但它还有调度进程等功能,就需要有一个时钟源 ,以固定的频率向CPU发中断~执行对应的shedule函数了。
所以操作系统本质是在硬件驱动下进行调度的 。操作系统是一个基于中断进行工作的软件。
时钟源后来被集成到CPU中,成为了主频。
4.2.3 软中断
CPU内部可以让软件触发中断吗~
在x86_64下CPU的指令集中会有一个int 0x80 或 syscall,可以自动让CPU触发一次中断。

我们以前用的open,fork,umask等系统调用其实不是OS提供的,OS只提供syscall和系统调用号:
在操作系统中有一个全部系统调用的函数指针表 ,每一个系统调用都有唯一的下标,这个下标叫做系统调用号。
cpp
// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};
从用户层面调用open方法时其实干了两件事:
move eax 5 :把函数指针表中的5为下标的sys_open函数地址放到eax寄存器中。
int 0x80或syscall : 把执行地址改到0x80位置。
在内核层面,中断向量表 中就有0x80为地址的函数CallSystem ,此函数也干了两件事:
获取系统调用号n : eax寄存器中已经存了目标函数指针的下标5。
调用系统调用方法 :sys_call_table[5](); _system_call :call [_sys_call_table+eax*4]
这样就完成了open的系统调用,open是被glibc封装的。(其他的也是如此)
CPU内部的软中断,比如int 0x80或者syscall,我们叫做 陷阱
CPU内部的软中断,比如除零/野指针等,我们叫做 异常。
4.3 内核态和用户态

所以系统调用的过程也是在进程地址空间上进行的 。只不过从用户区跳转到内核区。
所有函数调用都是地址空间的跳转。
下图可知,无论进程如何调度,都能找到操作系统。因为所有进程的内核区经过自同一个内核页表。

用户和内核都在同一个地址空间上了,用户能不能直接访问内核代码呢?
不能~ OS有自保功能 ,只允许系统调用方式进行访问。CPU中的cs寄存器的后两位(CPL当前权限级别)(前面是代码段地址)表示在什么区(00为内核,11为用户,int 0x80 syscall就是在改这两位改变身份的)
用户态 :以用户身份,只能访问自己的【0,3GB】
内核态:以内核身份,通过系统调用的方式访问【3,4GB】
4.4 sigaction
cpp
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);
// 第一个参数信号编号,第二个参数输入型,第三个参数输出型
RETURN VALUE
sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.
struct sigaction { // 实时信号也可以,不管
void (*sa_handler)(int); // 和signal第二个参数一样
void (*sa_sigaction)(int, siginfo_t *, void *); // 不管
sigset_t sa_mask; // 信号集类型
int sa_flags; // 不管
void (*sa_restorer)(void); // 不管
};
作用同signal,只不过功能更多。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字 ,当信号处理函数返回时自动恢复原来的信号屏蔽字 ,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
当前处理的信号会被自动屏蔽,那想要处理目标信号的时候屏蔽其他信号就可以使用sa_mask~
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>void handler(int signum)
{
std::cout << "hello signal: " << signum << std::endl;
while (true)
{
// 不断获取pending表
sigset_t pending;
sigpending(&pending);
for (int i = 31; i >= 1; i--)
{
if (sigismember(&pending, i))
std::cout << "1";
else
std::cout << "0";
}
std::cout << std::endl;
sleep(1);
}
exit(0);
}
int main()
{
struct sigaction act, oldact;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
act.sa_flags = 0;
sigaction(SIGINT, &act, &oldact); // 捕捉2号信号,234都被屏蔽
while (true)
{
std::cout << "Ciallo~" << getpid() << std::endl;
sleep(1);
}
return 0;
}
伍 可重入函数

上图是带头单链表的头插,插入第一个节点的函数进行到一半时,收到了信号捕捉处理,刚好信号捕捉处理中又有insert函数,去插入第二个节点了。最终导致了node2丢失,内存泄漏。
insert函数被不同的控制流程 调用,有可能在第一次调用还没返回时就再次进入该函数 ,这称为重入 ,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数 ,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
不可重入函数:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
可重入函数:
函数中只有自己的临时变量。
陆 volatile
以下代码会根据编译的优化等级产生不同的结果:
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>
int flag = 0;
void handler(int signum)
{
std::cout << "更改全局变量 ~ flag: " << flag << "-> 1" << std::endl;
flag = 1;
}
int main()
{
signal(2, handler); // 2号信号处理改成更改flag值~
while (!flag); // flag = 0时死循环
std::cout << "procress quit normal~" << std::endl;
return 0;
}

在O1以上的优化下,flag变量会被优化到CPU的寄存器中,更改时只改变了物理内存中flag的值。导致了flag在物理内存中值为1,CPU中值为0。**寄存器覆盖了进程看到变量的真实情况。**一直在死循环。
cpp
volatile int flag = 0; // 保证内存空间的可见性
在全局变量前加上volatile关键字 ,可以保证内存空间的可见性,每次从内存中检测变量的值。这样优化到满,也可以正常运行。

柒 SIGCHLD信号
sigchld 编号17 ,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略。
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <sys/wait.h>
void WaitAll(int num)
{
while (true)
{
pid_t n = waitpid(-1, nullptr, WNOHANG); // -1为任意一个子进程,WNOHANG非阻塞轮询,防止有子进程没退出卡住
if (n == 0)
{
break;
}
else if (n < 0)
{
std::cout << "waitpid error" << std::endl;
break;
}
}
std::cout << "父进程收到信号: " << num << std::endl;
}
int main()
{
// 父进程
signal(SIGCHLD, WaitAll);
for (int i = 10; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// 子进程
sleep(3);
std::cout << "我是子进程~" << std::endl;
if (i <= 6)
exit(3);
else
pause();
}
while (true)
{
// 父进程
std::cout << "我是父进程~" << std::endl;
sleep(1);
}
}
return 0;
}
WNOHANG 可以使多个子进程退出一些个,还剩几个子进程的时候不在waitpid处卡住,使父进程不能继续运行。(非阻塞轮询)
SIGCHILD的默认处理动作(SIG_DEF)是忽略(ign) ,子进程退出后会僵尸。
若把SIGCHILD的处理动作改成忽略(SIG_IGN),系统会自动回收子进程,父进程拿不到退出码。
cpp
int main()
{
// 父进程
signal(SIGCHLD, SIG_IGN);
for (int i = 10; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// 子进程
sleep(3);
std::cout << "我是子进程~" << std::endl;
exit(3);
}
while (true)
{
// 父进程
std::cout << "我是父进程~" << std::endl;
sleep(1);
}
}
return 0;
}
~ 完 ~
