信号:软件中断
用于通知一个事件的发生,会打断当前进程的操作去处理这个事件;前提:必须识别这个信号
信号有生命周期:
- 信号的产生
- 信号在进程中的注册
- 信号在进程中注销
- 信号的处理
信号种类有很多,每个都代表不同事件:
使用 kill -l 命令查看:62种
1~31:继承 unix 而来,非可靠信号(信号有可能会丢失,丢失事件),也叫 非实时信号
34~64:可靠信号(信号不会丢失),也叫 实时信号
信号的产生
硬件产生信号:
- ctrl + c(中断)
- ctrl + |(退出)
- ctrl + z(停止)
软件产生信号:
- 向进程发送一个signum信号
kill -signum pid
- 给指定 pid 进程发送指定 sig 信号
int kill(pid_t pid, int sig);
- 给进程自己发送 sig 信号
int raise(int sig);
- 给进程自己发送SIGABRT信号
void abort(void);
- 在 seconds 秒之后给进程自己发送一个 SIGALRM 信号,若 seconds = 0, 则作用是取消上一个定时器
unsigned int alarm(unsigned int seconds);
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
// int kill(pid_t pid, int sig);
// 向pid进程发送sig信号
//kill(getpid(), SIGKILL);
// int raise(int sig);
// 向自己发送sig信号
//raise(SIGTERM);
// void abort(void);
// 给自己发送SIGABRT信号
//abort();
// unsigned int alarm(unsigned int seconds);
// 经过seconds秒之后,给自己发送一个SIGALRM信号-->定时器
alarm(3);
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
信号在进程中的注册:在进程pcb中做标记,标记进程收到了哪些信号
- sigset_t 结构体的认识:
unsigned long int_val[_SIGSET_NWORDS];
是一个128位的位图,用于对信号是否到来做标记 - 非可靠信号注册:判断pcb的 pending 位图中相应信号是否已经注册(位图是否已经置1)
若未注册,则位图修改为1,向 sigqueue 链表中添加一个信号节点
若注册,则不做任何操作(事件丢失) - 可靠信号注册:不管信号是否已经注册,都会向链表中添加一个新的信号节点(事件不会丢失)
- 未决:是一个信号从产生到信号处理之间所处的状态
- 未决信号:注册了但是没有被处理的信号
信号在进程中的注销
非可靠信号:节点只有一个,注销就是删除节点,位图置0
可靠信号:节点可能有多个,注销就是删除一个节点,判断链表中是否还有相同信号的节点;若没有则位图置0;否则位图不变,依然需要标记进程有这个信号待处理
信号的处理
- 信号的处理并不是立即被处理;而是选择一个合适的时机去处理信号
- 信号处理有多种方式:
默认处理方式:SIG_DFL
忽略处理方式:SIG_IGN
自定义处理方式:用户自己写一个事件处理函数 - 如何修改信号处理方式:
第一种:
signum:信号编号,用户传入的处理函数替换 signum 这个信号的处理
handler:函数指针,用户传入的处理函数typedef void(*sighandler_t)(int);
第二种:
使用 act 动作替换 signum 信号的处理方式,将原来的处理方式放到 oldact 中
signum:信号编号
act:信号新的处理动作
oldact:用于保存信号原来的处理动作
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigcb(int signo)
{
printf("receive a signo:%d\n", signo);
}
int main()
{
//将SIGINT信号设置为忽略处理
signal(SIGINT, SIG_IGN);
struct sigaction new_act, old_act;
new_act.sa_flags = 0;
new_act.sa_handler = sigcb;
// int sigemptyset(sigset_t *set);
// 清空set信号集合
sigemptyset(&new_act.sa_mask);
// sigaction使用newact替换SIGINT信号的处理动作,将原有动作保存到oldact中
sigaction(SIGINT, &new_act, &old_act);
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
信号的阻塞:阻止信号被递达(暂时不处理信号)
递达:一个动作,即信号的处理
在pcb中还有一个集合(阻塞信号集合,标记哪些信号暂时不被处理)
在所有的信号中,9号信号 SIGKILL 和19号信号 SIGSTOP,无法被阻塞,无法被自定义,无法被忽略
操作方式:
how:
- SIG_BLOCK:向阻塞集合中加入 set 集合中的信号 block = mask | set
- SIG_UNBLOCK:从阻塞集合中移除 set 集合中的信号 block = mask & (~set)
- SIG_SETMASK:将 set 集合的信号设置为阻塞集合 block = set
set:要阻塞/解除阻塞的信号集合
oldset:用于保存修改前阻塞集合中的信号
关于set的接口操作:
操作步骤(demo):
- 将一些信号的处理函数自定义
- 将所有的信号都给阻塞
- 在解除阻塞之前给进程发送信号
- 解除阻塞,查看信号的处理情况
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigcb(int signo)
{
printf("receive signo:%d\n", signo);
}
int main()
{
//自定义函数处理SIGINT信号,非可靠信号
signal(SIGINT, sigcb);
//自定义函数处理SIGRTMIN+4信号,可靠信号
signal(SIGRTMIN+4, sigcb);
sigset_t set;
sigemptyset(&set); //清空set集合
sigfillset(&set); //将所有信号都添加到set集合中
sigprocmask(SIG_BLOCK, &set, NULL); //阻塞set集合中的信号
printf("press enter continue\n");
getchar(); //按下回车键后继续向下运行
sigprocmask(SIG_UNBLOCK, &set, NULL); //将set集合中的信号解除阻塞
while(1)
{
sleep(1);
}
return 0;
}
可重入函数与不可重入函数
- 可重入:多个执行流程同时执行进入相同的函数,不会造成数据二义性以及代码逻辑混乱
- 不可重入:多个执行流程同时执行进入相同的函数,有可能会造成数据二义性以及代码逻辑混乱
- 当用户设计一个函数或使用一个函数时在多个执行流中,就需要考虑函数是否可重入情况
- 函数可重入与不可重入的关键点:
这个函数是否对临界资源(全局数据)进行了非原子操作
//这个demo来演示函数的可重入与不可重入
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int a = 1;
int b = 1;
int sum(int* a, int* b)
{
(*a)++;
sleep(3);
(*b)++;
return *a + *b;
}
void sigcb(int signo)
{
(void)signo;
printf("signal---------%d\n", sum(&a, &b));
}
int main()
{
//当用户输入ctrl + c时,会用sigcb函数替换当前信号的执行
signal(SIGINT, sigcb);
printf("main---------%d\n", sum(&a, &b));
return 0;
}
SIGCHLD:17号信号
因为子进程退出后,操作系统通过 SIGCHLD 信号通知父进程,由于 SIGCHLD 信号处理方式默认是忽略,因此父进程不知道子进程何时退出,因此调用 wait 死等,浪费了父进程。
现在有了信号,那么我们就可以自定义 SIGCHLD 信号的处理方式,在信号回调函数中调用 wait 。子进程退出,向父进程发送 SIGCHLD 信号,触发信号回调函数,在函数中执行wait。
因为SIGCHLD是非可靠信号,如果同时有多个进程退出,有可能信号会丢失,导致 sigcb 只会被回调一次,只处理了一个子进程退出;其它退出的子进程成为僵尸进程;因此在一次回调中要将所有的僵尸进程全部处理。