目录
1. 信号的基本认识
- 本质
- 软件中断
- 作用
- 向进程通知事件的发生(打断当前的操作,去处理事件)
- 生命周期
- 产生 -> 注册 -> 注销 -> 处理
2. Linux下信号的种类
信号有不同的种类,每个信号都对应不同的事件
- 查看信号种类:kill -l
- 62种信号
- 1-31号:非可靠信号 / 非实时信号 -> 每个都有各自对应的事件,可丢失(可以不处理;可以不立即处理)
- 34-64号:可靠信号 / 实时信号 -> 后续添加的信号,不可丢失(发出多少次信号,处理多少次;必须立即处理)
3. 信号的产生
-
3.1 通过硬件产生
- ctrl + c -> 中断信号 -> 操作系统解析为一个软件信号SIGIN,发送给前台进程。
- ctrl + | -> 退出信号 -> SIGQUIT
- ctrl + z -> 停止信号 -> SIGTERM
-
3.2 通过软件产生
- kill -signo pid命令
- int kill(pid_t pid,int sig);
- 给指定进程发送指定信号
- int raise(int sig);
- 给调用进程/线程发送指定信号
- void abort(void);
- 给调用进程发送SIGABRT
- unsigned int alarm(unsigned int seconds);
- seconds秒之后,给调用进程发送SIGALRM信号
- seconds == 0,表示取消定时器
- 返回值
- 上一个定时器剩余的时间或0
- 程序异常
- 被动结束
-
3.3 core dumped 核心转储
- 程序异常退出时,保存程序运行信息到core.pid文件中,用于事后调试
- 默认是关闭的 -> 占磁盘资源,安全性考虑
- ulimit -a
- 查看core dump是否开启
- ulimit -c 1024
- 设置核心转储文件大小,开启核心转储
- gdb ./file 进入gdb后 -> core-file core.pid 加载core-file文件 -> bt 查看函数调用栈
4. 信号在进程中注册
- 给一个进程注册信号,实际上就是进程是否在pcb中的sigset_t 结构体做了标记。
- sigset_t 结构体,存放数组,数组大小1024个比特位。
- sigpending 结构体,存放双向链表。
- sigqueue 结构体
-
4.1 非可靠信号
- 判断pending未决信号集合位图相应位是否为1
- 若为0,为信号组织sigqueue节点,添加到链表中,并且pending位图置1
- 若为1(信号已经注册过还未被处理),不处理(相当于丢弃信号)
- 判断pending未决信号集合位图相应位是否为1
-
4.2 可靠信号
- 不管位图是否为1,都组织节点添加到链表中,并且位图置1。
- 信号不会被丢弃
5. 信号在进程中注销
-
5.1 非可靠信号
- ∵ 非可靠信号的信号节点只有一个
- ∴ 删除节点,位图直接置0
- ∵ 非可靠信号的信号节点只有一个
-
5.2 可靠信号
- ∵ 可靠信号的信号节点可能会有多个
- 若还有相同的信号节点,位图依然置1
- 否则,置0
- ∵ 可靠信号的信号节点可能会有多个
6. 信号的处理
-
6.1 处理方式
- 默认处理 -> SIG_DFL
- 忽略处理 -> SIG_IGN 默认处理方式
- 自定义处理
-
6.2 捕捉信号
- 6.2.1 修改信号的处理方式
- 1.
- typedef void (*sighandler_t)(int);
- sighandler_t signal(int signum, sighandler_t handler);.
- signum 信号编号
- handler 函数指针数组,存放每个信号的处理方式;使用handler函数替换signum信号的处理方式
- 有两个宏
- SIG_IGN 信号的忽略处理
- SIG_DFL 信号的默认处理
- 有两个宏
- 2.
- struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}; - int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- act 用户自定义动作,替换原有的处理动作
- oldact 保存原有的处理动作
- int sigemptyset(sigset_t *set);
- 清空信号集合set
- int sigfillset(sigset_t *set);
- 将所有信号添加到set中
- int sigaddset(sigset_t *set, int signum);
- 将指定信号signum添加到set中
- int sigdelset(sigset_t *set, int signum);
- 从集合中移除指定信号
- struct sigaction {
- 1.
- 6.2.2 信号自定义处理方式的捕捉流程
- 6.2.1 修改信号的处理方式
- 注意:
- 若是另外两种处理方式,直接在内核态直接处理完毕。
7. 阻塞信号
暂时阻止信号被递达 -> 信号依然可以注册,只是暂时不处理,解除阻塞之后再处理。
- 信号的递达
- 一个动作 -> 信号的处理
- 信号的未决
- 一种状态 -> 信号从产生到处理之前所处的状态
-
7.1 信号的阻塞过程
- 在pcb的blocked(信号阻塞集合)中 标记暂不处理的信号(将blocked位图集合中对应的位置置1,表示阻塞这个信号)
- 进程返回用户态前,发现有信号需要处理
- 查看该信号在blocked中的标记
- 若标记为1 -> 阻塞,暂不处理
- 若标记为0 -> 处理
- 查看该信号在blocked中的标记
- 注意:
- 9号信号SIGKILL和19号信号SIGSTOP无法被阻塞、无法自定义、无法被忽略
-
7.2 使用接口
- int sigprocmask(int how, const sigset_t *set, sigset_t *old‐set);
- 阻塞set集合中的信号,将原来阻塞的信号保存到oldset中
- how
- SIG_BLOCK 阻塞set中的信号 mask = mask | set
- SIG_UNBLOCK 对set中的信号解除阻塞 mask = mask &(~set)
- SIG_SETMASK 将set中的信号替换原有信号 mask = set
- 返回值
- 成功 -> 0
- 失败 -> -1
- int sigpending(sigset_t *set);
- 获取当前进程的未决信号,置于set
- int sigismember(const sigset_t *set, int signum);
- 判断信号signum是否在信号集合set当中
- int sigsuspend(const sigset_t *mask);
- 临时使用传入的mask替换block阻塞集合,陷入休眠。阻塞的就是mask,被唤醒之后,block阻塞集合还原。
- int sigprocmask(int how, const sigset_t *set, sigset_t *old‐set);
8. 竞态条件
运行时序造成的数据竞争,导致数据二义性。
- 函数的可重入与不可重入:一个函数是否可以在多个运行时序中重复调用而不会出现问题
- 不可重入函数 (malloc / free)
- 如果一个函数中操作了全局数据,并且这个操作既不是原子性操作,又不受保护
- 不能在多个时序的运行中重复调用,重复调用有可能造成数据二义性。
- 可重入函数
- 在多个时序的运行中重复调用,不会造成异常影响(数据二义性问题)。
- 不可重入函数 (malloc / free)
9. volatile关键字
保持内存可见性,每次操作变量都需要重新从内存中获取,防止编译器过度优化
-
优化:将经常使用的变量放入寄存器中,就算在内存中更改了变量的值,也不会重新加载
-
缺点:会造成逻辑紊乱
-
解决:每次对变量访问都要从内存重新获取数据
-
10. SIGCHLD信号
子进程退出,操作系统通知父进程的信号。
- 自定义SIGCHLD信号处理方式sigcb,当子进程退出,操作系统发送信号给父进程,直接出发信号回调sigcb,用户主要在sigcb中去调用 wait / waitpid 就可以处理子进程的退出。
- SIGCHLD是一个非可靠信号,假如有多个子进程同时退出,则有可能导致事件丢失,导致sigcb只被回调一次,只处理一个子进程。
- 因此,在sigcb中用户需要循环非阻塞处理子进程退出,直到没有退出的子进程。
- while(waitpid(-1,NULL,WONOHANG) > 0)(>0表示有子进程退出)
- 必须使用非阻塞,否则没有子进程退出的时候,waitpid将阻塞导致程序无法回到主控流程
- 因此,在sigcb中用户需要循环非阻塞处理子进程退出,直到没有退出的子进程。