信号
信号在我们的生活中随处可见,他们都有共性:
- 简单。
- 不能携带大量信息。
- 满足某个特设条件才发送。
信号的机制
A 给 B 发送信号,B 收到信号之前执行自己的代码,**收到信号后,不管执行到程度的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。**与硬件中断类似 – 异步模式。但信号似软件层面上实现的中断,早期常被称为“软中断”。
**信号的特质:**由于信号是通过软件方式实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延时时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
与信号相关的事件和状态
产生信号:
- 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
- 系统调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器 alarm
- 硬件异常产生,如:非法访问内存(段错误)、除 0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:kill 命令
**递达:**递送并且到达进程。
**未决:**产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号的处理方式:
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用户处理函数)
Linux 内核的进程控制块 PCB 是一个结构体,task_struct,除了包含进程 id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
**阻塞信号集(信号屏蔽字):**将某些信号加入集合,对他们设置屏蔽,当屏蔽 x 信号后,再收到该信号,该信号的处理将推后(接触屏蔽后)
未决信号集:
- 信号产生,未决信号集中描述该信号的位立即翻转位 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号的编号
可以使用 kill -l 命令查看当前系统可使用的信号有那些。
不存在编号为 0 的信号。其中 1-31 号信号称之为常规信号(也叫普通信号或标准信号),34-64 称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前 32 个名字各不相同。
信号 4 要素
- 编号 2) 名称 3) 事件 4) 默认处理动作
可通过 man 7 signal 查看帮助文档获取。
默认动作:
-
Term: 终止进程
-
Ign: 忽略信号(默认即使对这种信号忽略操作)
-
Core: 终止进程,生成 Core 文件。(查验进程死亡原因,用于 gdb 调试)
-
Stop: 停止(暂停)进程
-
Cont: 继续运行进程
9)sigkill 和 19)sigstop 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞
Linux 常规信号一览表
-
- SIGHUP(hang up): 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程。
-
- SIGINT(Interrupt): 当用户按下<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作终止进程。
-
- SIGQUIT(quit): 当用户按下<ctrl + \ >组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作终止进程。
-
- SIGILL(illegal): CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件。
-
- SIGTRAP: 该信号由断点指令或其他 trap 指令产生。默认动作为终止里程并产生 core 文件。
-
- SIGABRT(abort): 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。
-
- SIGBUS: 非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。
-
- SIGFPE: 在发生致命的运算错误时发出。不仅包括浮点运行错误,还包括溢出及除数为 0 等所有的算法错误。默认动作为终止进程并产生 core 文件。
-
- SIGKILL: 无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
-
- SIGUSE1: 用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
-
- SIGSEGV: 指明进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。
-
- SIGUSR2: 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
-
- SIGPIPE: broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。
-
- SIGALRM: 定时器超时,超时的时间由系统调用 alarm 设置。默认动作为终止进程。
-
- SIGTERM: 程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。
-
- SIGSTKFLT: Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
-
- SIGCHLD: 子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
-
- SIGCONT: 如果进程已停止,则使其继续运行。默认动作为继续/忽略。
-
- SIGSTOP: 停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
-
- SIGTSTP: 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
-
- SIGTTIN: 后台进程读终端控制台。默认动作为暂停进程。
-
- SIGTTOU: 该信号类似于 SIGTTIN, 在后台进程要向终端输出数据时发生。默认动作为暂停进程。
-
- SIGURG: 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
-
- SIGXCPU: 进程执行时间超过了分配给该进程的 CPU 时间,系统产生该信号并发送给该进程。默认动作为终止进程。
-
- SIGXFSZ: 超过文件的最大长度设置。默认动作为终止进程。
-
- SIGVTALRM: 虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间。默认动作为终止进程。
-
- SGIPROF: 类似于 SIGVTALRM,它不但包括该进程占用 CPU 时间还包括执行系统调用时间。默认动作为终止进程。
-
- SIGWINCH: 窗口变化大小时发出。默认动作为忽略该信号。
-
- SIGIO: 此信号向进程指示发出了一个异步 IO 时间。默认动作为忽略
-
- SIGPWR: 关机。默认动作为终止进程。
-
- SIGSYS: 无效的系统调用。默认动作为终止进程并产生 core 文件。
-
- SIGRTMIN~(64) SIGRTMAX: LINUX 的实时信号,它没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
信号产生:
终端按键产生信号:
-
Ctrl+c -> 2) SIGINT(终止/中断) “INT” – interrupt
-
Ctrl+z -> 20) SIGTSTP(暂停/停止) “T” – Terminal
- fg – 恢复暂停的进程
-
Ctrl+\ -> 3) SIGQUIT(退出)
硬件异常产生信号
-
除 0 操作 -> 8)SIGFPE (浮点数例外) “FPE” – Floating point exception
-
非法访问内存 -> 11)SIGSEGV (段错误)
-
总线错误 -> 7)SIGBUS
kill函数/命令产生信号
kill 命令产生信号:kill -SIGKILL pid
**kill 函数:**给指定进程发送指定信号(不一定杀死)
-
int kill(pid_t pid, int sig); 成功: 0; 失败: -1(ID 非法,信号非法,普通用户杀 init 进程等权级问题),设置 errno
-
sig: 不推荐直接使用数字,应使用宏名。
-
pid>0: 发送信号给指定的进程。
-
pid==0: 发送信号给与调用 kill 函数进程属于同一进程组的所有进程。
-
pid==-1: 发送给进程有权限发送的系统中所有进程。
-
pid<-1: 取|pid|发给对应的进程组。
**进程组:**每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组 ID 和进程组长 ID 相同。
**权限保护:**super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。kill -9(root 用户的 pid) 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户 ID == 接收者实际或有效用户 ID
raise 和 abort 函数:
**raise 函数:**给当前进程发送指定信号(自己给自己发)raise(signo) == kill(getpid(), signo);
- int raise(int sig);
- 成功 0。
- 失败非 0 值。
**abort 函数:**给自己发送异常终止信号 6)SIGABRT 信号,终止并产生 core 文件
- **void abort(void); **该函数无返回
软件条件产生信号:
alarm 函数:
设定定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 14) SIGALRM 信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。
- unsigned int alarm(unsigned int seconds);
- 返回 0 或 上一次剩余的秒数,无失败。
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸 … 无论进程处于何种状态 alarm 都计时。
练习:编写程序,测试你使用的计算机 1 秒钟能数多少个数。
#include <cstdio>
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
alarm(1);
for(int i = 1; ; i++) {
if(i >= 400000000)
cout << i << endl;
}
}
使用 time 命令查看进程执行的时间。程序运行的瓶颈在于 IO.
实际执行时间 = 系统时间 + 用户时间 + 等待时间。
setitimer 函数
设置定时器(闹钟)。可代替 alarm 函数。精度微妙 us,可以实现周期定时。
- **int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); **
- 成功:0;
- 失败:-1,设置 errno.
参数:which:指定定时方式
-
自然定时:ITIMER_REAL -> 14) SIGLARM 计算自然时间
-
虚拟空间计时(用户空间):ITIMER_VIRTUAL -> 26) SIGVTALRM 只计算进程占用 cpu 的时间
-
运行时计时(用户+内核):ITIMER_PROF -> 27) SIGPROF 计算占用 cpu 及执行系统调用的时间。
练习:结果 man page 编写程序,测试 it_interval、it_value 这两个参数的作用。
#include <cstdio>
#include <cstdlib>
#include <sys/time.h>
#include <signal.h>
#include <iostream>
using namespace std;
void myf(int signo) {
cout << "hello world" << endl;
}
int main(void)
{
signal(SIGALRM, myf);
struct itimerval it, oldit;
it.it_value.tv_sec = 5;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 3;//周期间隔时间.
it.it_interval.tv_usec = 0;
if(setitimer(ITIMER_REAL, &it, &oldit) == -1) {
perror("alarm error");
exit(1);
}
while(1);
return 0;
}
-
it_interval: 用来设定两次定时任务之间间隔的时间。
-
it_value: 定时的时长
-
两个参数都设置为 0,即清 0 操作。
信号集操作函数
内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。而我们可以在应用程序中自定义 set 来改变 mask。已达到屏蔽指定信号的目的。
信号集设定
-
sigset_t set; //typedef unsigned long sigset_t; 位图
-
int sigemptyset(sigset_t *set);
- 将某个信号集清 0
- 成功: 0; 失败: -1
-
int sigfillset(sigset_t *set);
- 将某个信号集置 1
- 成功: 0; 失败: -1
-
int sigaddset(sigset_t *set, int signum);
- 将某个信号加入信号集
- 成功: 0; 失败: -1
-
int sigdelset(sigset_t *set, int signum);
- 将某个信号清出信号集
- 成功: 0; 失败: -1
-
int sigismember(const sigset_t *set, int signum);
- 判断某个信号是否在信号集中
- 在集合: 1; 不在: 0;
sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
sigprocmask 函数
用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)
严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃。
-
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 成功: 0; 失败: -1, 设置 errno
-
set: 传入参数,是一个位图,set 中哪位置 1,就表示当前进程屏蔽哪个信号。
-
oldset: 传入参数,保存旧的信号屏蔽集。
-
how 参数取值:假设当前信号屏蔽字为 mask
- SIG_BLOCK: 当 how 设置为此值,set 表示需要屏蔽的信号。相当于 mask = mask | set
- SIG_UNBLOCK: 当 how 设置为此,set 表示需要解除屏蔽的信号。相当于 mask = mask & ~set
- SIG_SETMASK: 当 how 设置为此,set 表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set 若,调用 sigprocmask 解除了对当前若干个信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递送。
sigpending 函数
读取当前进程的未决信号集
- int sigpending(sigset_t *set);
- 成功:0;失败 -1,设置 error.
- set 传出参数。
练习:编写程序。把所有常规信号的未决状态打印至屏幕。
#include <cstdio>
#include <signal.h>
#include <iostream>
#include <cstdlib>
#include <unistd.h>
using namespace std;
void show_ped(sigset_t *ped) {
for(int i = 1; i < 31; i++) {
if(sigismember(ped, i)) {
cout << 1;
}
else cout << 0;
}
cout << endl;
}
int main(void)
{
sigset_t set, old_set, ped;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
sigaddset(&set, SIGTSTP);
int ret = sigprocmask(SIG_BLOCK, &set, &old_set);
if(ret == -1) {
perror("procmask error");
exit(1);
}
while(1) {
sigpending(&ped);
show_ped(&ped);
sleep(1);
}
return 0;
}
signal 函数
注册一个信号捕捉函数:
-
typedef void (*sighandler_t)(int);
-
sighandler_t signal(int signum, sighandler_t handler);
该函数由 ANSI 定义,不同版本的 UNIX 可能由不同的行为。因此避免使用它,取而代之使用 sigaction 函数。
void( signal(int signum), void(sighandler_t)(int)))(int);
能看出这个函数代表什么意思吗?注意多在复杂结构中使用 typedef.
sigaction 函数
修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)
-
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 成功:0; 失败:-1, 设置 errno.
-
**act: **传入参数,新的处理方式。
-
**oldact: **传出参数,旧的处理方式。
-
stuct 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); };
-
siginfo_t { int si_signo; /* Signal number / int si_errno; / An errno value / int si_code; / Signal code / int si_trapno; / Trap number that caused hardware-generated signal (unused on most architectures) / pid_t si_pid; / Sending process ID / uid_t si_uid; / Real user ID of sending process / int si_status; / Exit value or signal / clock_t si_utime; / User time consumed / clock_t si_stime; / System time consumed / sigval_t si_value; / Signal value / int si_int; / POSIX.1b signal */ void si_ptr; / POSIX.1b signal / int si_overrun; / Timer overrun count; POSIX.1b timers / int si_timerid; / Timer ID; POSIX.1b timers */ void si_addr; / Memory location which caused fault / long si_band; / Band event (was int in glibc 2.3.2 and earlier) / int si_fd; / File descriptor / short si_addr_lsb; / Least significant bit of address (since Linux 2.6.32) */ };
-
**sa_restorer: **该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
-
**sa_sigaction: **当 sa_flags 被指定为 SA_SIGINFO 标志时,使用该信号处理程序。(很少使用)
-
重点掌握:
- **sa_handler: **指定信号捕捉后的处理函数名(既注册函数)。也可赋值为 SIG_IGN 表忽略或 SIG_DFL 表执行默认动作
- **sa_mask: **调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
- **sa_flags: **
- 通常设置为 0,表示用默认属性。信号捕捉函数执行期间,自动屏蔽本信号
- SA_SIGINFO: 选用 sa_sigaction 来指定捕捉函数
- SA_INTERRURT: 被信号中断后系统调用不重启
- SA_RESTART: 被信号中断后系统调用自动重启
- SA_DEFER: 不自动屏蔽本信号
-
信号捕捉特性:
- 进程正常运行时,默认 PCB 中有一个信号屏蔽字,假定为 X,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由 X 来指定。而是用 sa_mask 来指定。调用完信号处理函数,再恢复 X.
- XXX 信号捕捉函数执行期间,XXX 信号自动屏蔽
- 阻塞的常规信号不支持排队,产生多次只记录一次。(后 32 个实时信号支持排队)
练习 1:为某个信号设置捕捉函数
#include <cstdio>
#include <signal.h>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
using namespace std;
void cat(int signo) {
sleep(4);
cout << "--------cat" << endl;
}
int main(void) {
struct sigaction now;
now.sa_handler = cat;
sigemptyset(&now.sa_mask);
sigaddset(&now.sa_mask, SIGINT);
sigaddset(&now.sa_mask, SIGTSTP);
sigaddset(&now.sa_mask, SIGQUIT);
now.sa_flags = 0;
int ret = sigaction(SIGINT, &now, NULL);
if(ret == -1) {
perror("car error");
exit(1);
}
while(1);
return 0;
}
内核实现信号捕捉过程
用户 -> 内核 -> (用户)执行捕捉函数 -> 内核 -> 用户
pause 函数
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程处于阻塞状态(主动放弃 CPU)直到由信号递达将其唤醒。
- int pause(void);
- 返回值:-1 并设置 errno 为 EINTR.
返回值:
-
(1) 如果信号的默认处理动作是终止进程,则进程终止,pause 函数么有返回值机会
-
(2) 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回
-
(3) 如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause 返回-1】errno 设置为 EINTR,表示“被信号中断”。想想我们还有哪个函数只有出错返回值。
-
(4) pause 收到的信号不能被屏蔽,如果被屏蔽,那么 pause 就不能唤醒。
注意,unslept = alarm(0) 的用法
时序竞态
时序问题分析:
回顾,借助 pause 和 alarm 实现的 mysleep 函数。设想如下时序:
-
注册 SIGALRM 信号处理函数 (sigaction…)
-
调用 alarm(1) 函数设定闹钟 1 秒。
-
函数调用刚结束,开始倒计时 1 秒。当前进程失去 cpu,内核调度优先级高的进程(有多个)取代当前进程。
-
1秒后,闹钟超时,内核向当前进程发送 SIGALRM 信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
-
优先级高的进程执行完,当前进程获得 cpu 资源,内核调度回当前进程执行。
-
SIGALRM 信号递达,信号设置捕捉,执行处理函数 sig_alarm。
-
信号处理函数执行结束,返回当前进程主控进程,pause()被调用挂起等待。(预等待 alarm 函数发送的 SIGALRM 信号将自己唤醒)
-
SIGALRM 信号已经处理完毕,pause 不会等到。
解决时序问题
可以通过设置屏蔽 SIGALRM 的方法来控制程序执行逻辑,**但无论如何设置,程序都有可能在"解除信号屏蔽"与挂起等待信号"这个两个操作间隙失去 cpu 资源。除非将这两步骤合并成一个"原子操作"。sigsuspend 函数具备这个功能。**在对时序要求严格的场合下都应该使用 sigsuspend 替换 pause。
-
int sigsuspend(const sigset_t *mask) 挂起等待信号。
-
sigsuspend 函数调用期间,进程信号屏蔽字有其参数 mask 指定。
**可将某个信号 (如 SIGALRM) 从临时信号屏蔽字 mask 中删除,这样在调用 sigsuspend 时将解除对该信号的屏蔽,然后挂起等待,**当 sigsuspend 返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend 函数返回后仍然屏蔽该信号。
练习:使用 sigsuspend 和 alarm 来实现 sleep 函数
#include <cstdio>
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <errno.h>
using namespace std;
void cat(int signo)
{
cout << "5s arrived" << endl;
}
unsigned int mysleep(unsigned int seconds)
{
struct sigaction act, old_act;
act.sa_handler = cat;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
int ret = sigaction(SIGALRM, &act, &old_act);
if(ret == -1) {
perror("sigaction error");
exit(1);
}
//屏蔽 SIGALRM 信号
sigset_t set, old_set, sus_set;
sigemptyset(&set);
sigaddset(&set, SIGALRM);
sigprocmask(SIG_BLOCK, &set, &old_set);
//计时
alarm(seconds);
//解决了:如果这时失去 cpu, 直至 seconds 后回来,优先执行信号,以至于在 pause 后没法收到信号.
sus_set = old_set;
sigdelset(&sus_set, SIGALRM);
//调用解除屏蔽 SIGALRM 信号的 sigsuspend
ret = sigsuspend(&sus_set);// 原子操作
if(ret == -1 && errno == EINTR) {
printf("pause sucess\n");
}
sigprocmask(SIG_SETMASK, &old_set, NULL);
sigaction(SIGALRM, &old_act, NULL);
ret = alarm(0);
return ret;
}
int main(void) {
cout << "start sleep" << endl;
mysleep(5);
//sleep();
cout << "end sleep" << endl;
return 0;
}
总结
竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
这种意外情况只能在编写程序过程重,提早预见,主动规避,而无法通过 gdb 程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。
全局变量异步 I/O
分析如下父子进程交替数数程序。
#include <cstdio>
#include <signal.h>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int n, flag;
pid_t pid;
void son(int signo) {
printf("I is son, pid = %d, n = %d\n", getpid(), n);
n += 2;
flag = 1;
}
void parent(int signo) {
printf("I is parent, pid = %d, n = %d\n", getpid(), n);
n += 2;
flag = 1;
}
int main(void) {
pid = fork();
if(pid == -1) {
perror("fork error");
exit(1);
} else if(pid == 0) {
n = 2;
struct sigaction act, old_act;
act.sa_flags = 0;
act.sa_handler = son;
sigemptyset(&act.sa_mask);
sigaction(SIGUSR1, &act, &old_act);
while(1) {
if(flag == 1) {
flag = 0;//flag = 0,不能在发送信号下面
kill(getppid(), SIGUSR2);
//失去了 cpu, 收到信号, 执行信号, flag == 1, 接下来 flag == 0, 导致无法在发信号给父亲
//flag = 0;
}
}
} else {
sleep(1);
n = 1;
struct sigaction act, old_act;
act.sa_flags = 0;
act.sa_handler = parent;
sigemptyset(&act.sa_mask);
sigaction(SIGUSR2, &act, &old_act);
parent(0);
while(1) {
if(flag == 1) { //代表刚刚数完数,需要给对方发信号
flag = 0;
kill(pid, SIGUSR1);
//flag = 0;
}
}
}
return 0;
}
如何解决该问题呢?也可以用“锁”机制。当操作全局变量的时候,通过加锁,解锁来解决该问题。
现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步 IO 可能造成的问题
可/不可重入函数
可重入函数:递归调用不会导致最终结果变化
不可重入函数:递归调用会导致最终结果变化
注意事项:
-
函数内不能含有全局变量及 static 变量,不能使用malloc、free
-
信号捕捉函数应设计为可重入函数
-
信号处理程序可以调用的可重入函数可参阅 man 7 signal
-
没有包含在上述列表中的函数大多是不可重入的,其原因为:
-
使用静态数据结构
-
调用了 malloc 或 free
-
是标准 I/O 函数
-
SIGCHLD信号
SIGCHLD的产生条件
-
子进程终止时
-
子进程接收到 SIGSTOP 信号停止时
-
子进程处于停止态,接收到 SIGCONT 后唤醒时
-
总结:子进程状态发送变化,都会向父亲发送 SIGCHID
借助 SIGCHLD 信号回收子进程
练习:借助 sigchld 信号回收子进程
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void re_son(int signo) {
pid_t pid;
int status;
while((pid = waitpid(0, &status, WNOHANG)) > 0) {//这里不能用 if,多个子进程同时发送 sigchld 信号,相当于也就收到了一个。所以不能说收到一个信号,回收一个子进程。
if(WIFEXITED(status)) {
printf("------child %d exit %d\n", pid, WEXITSTATUS(status));
}
else if(WIFSIGNALED(status))
printf("------child %d cancel signal %d\n", pid, WTERMSIG(status));
}
}
int main(void) {
int n = 10, i;
for(i = 1; i <= n; i++) {
pid_t pid = fork();
if(pid == -1) {
perror("fork error");
exit(1);
} else if(pid == 0) {
break;
}
}
if(i <= n) {
//sleep(i);
return i;
}
else {
struct sigaction act;
act.sa_handler = re_son;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
int ret = sigaction(SIGCHLD, &act, NULL);
if(ret == -1) {
perror("sigaction error");
exit(1);
}
while(1);
}
return 0;
}
信号传参
发送信号传参
sigqueue 函数对应 kill 函数,但可在向指定进程发送信号的同时携带参数
-
int sigqueue(pid_t pid, int sig, const union sigval value);
- 成功: 0; 失败 -1, 设置 errno
-
union sigval{
int sival_int;
void *sival_ptr;
}
向指定进程发送指定信号的同时,携带数据。但,如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一个进程没有实际意义。
捕捉函数传参
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
中断系统调用
系统调用可分为两类:慢速系统调用和其他系统调用
-
**慢速系统调用:**可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如, read, write, pause, wait…
-
**其他系统调用:**getpid、getppid、fork…
慢速系统调用被中断的相关行为,实际上就是 pause 的行为:如,read
-
想中断 pause,信号不能被屏蔽
-
信号的处理方式必须是捕捉(默认、忽略都不可以)
-
中断后返回 -1,设置 errno 为 EINTR(表“被信号中断”)
可修改 sa_flags 参数来设置被信号中断后系统调用是否重启。SA_INTERRURT 不重启。SA_RESTART 重启。
扩展了解:
- sa_flags 还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将 sa_flags 设置为 SA_NODEFER,除非 sa_mask 中包含该信号。