信号通过软件方法实现(软中断), 会导致延时性. 每个进程接收到的信号都是由内核发送处理的, 内核作为中转.
未决: 产生和递达之间的状态, 主要由于阻塞(屏蔽)导致.
未决信号集和阻塞信号集(信号屏蔽字)都存放在PCB中, 数据结构是集合, 不重复但无序, 内部存放0/1值. 正常传递时, 未决信号集中从0变到1(未决)再从1变到0(被处理)的过程可以看作是瞬时的. 但当阻塞信号集某编号因为某种原因为1时, 当内核发送信号后, 未决信号集中对应的该编号也会变为1(不发送就仍为0), 所以说阻塞信号集影响未决信号集.
信号的处理方式:
- 执行默认动作
- 忽略(丢弃), 也是一种处理
- 捕捉, 调用户处理函数.
信号四要素:
- 名字 2. 编号 3. 默认动作 4. 对应触发事件
默认动作有5种:
- Term,终止进程
- Core,终止进程并产生core文件
- Ign,忽略
- Stop,暂停
- Cont,继续
man 7 signal查看信号, 当一个名字对应多个编号时, 取中间那列的, 左右两列为其他平台的编号.
9)SIGKILL和19)SIGSTOP比较特殊, 不允许忽略和捕捉, 甚至不允许阻塞, 只能执行默认动作
信号的产生
1. 终端按键产生信号
ctrl + c 2)SIGINT 中断信号interrupt, 终止进程(Term)
ctrl + z 20)SIGTSTP 暂停与终端交互进程, 放到后台(Stop). 注意与19)SIGSTOP区分, SIGSTOP可以暂停任何进程(Stop).
ctrl + \ 3)SIGQUIT 退出进程(Core)
2. 硬件异常产生信号
除0操作 8)SIGFPE(浮点数例外)
非法访问内存 11)SIGSEGV(段错误)
总线错误 7)SIGBUS
3. kill命令/函数产生信号
int kill(pid_t pid, int sig)可以调用kill函数
对于管道连接的进程, 当写端进程终止后, 写端都关闭, 读端也会跟着关闭, 所以只kill写端进程就会把管道两端的进程都杀死.

4. 自身调用raise()和abort()函数产生信号

5. 软件条件产生信号:alarm()和setitimer()

alarm()传新的时间进去后会覆盖旧的, 所以返回的上一次剩下的秒数没什么用, 如果alarm(0)就是关闭定时器, 直接终止进程. 注意终止进程只是默认行为, 可以设置一个handler函数如signal(SIGALRM, handler)来注册信号的捕捉函数(真正捕捉信号的是内核), 这样就可以在时间到了之后不终止进程. 信号捕捉函数是一个典型的回调函数(封装了一个函数, 但是没有直接调用).
可以使用time命令查看程序运行的时间, 总时间=用户空间时间+内核空间时间+等待时间, 等待时间I/O占了大部分.

new_value是传入参数, 表示要定时的时间; old_value是传出参数, 表示上次剩的时间, 相当于alarm()函数的返回值. setitimer成功返回0, 失败返回-1.
需要先把结构体成员赋初值, 再把结构体指针传到函数里, 分别指定val和interval的秒值/毫秒值:
signal(SIGALRM, signalHandler);
struct itimerval new_value, old_value;
new_value.it_value.tv_sec = 5;
new_value.it_value.tv_usec = 0;
new_value.it_interval.tv_sec = 3;
new_value.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &new_value, &old_value);
只有第一次设定闹钟后5秒会接收到SIGALRM信号, 之后每3秒接收到一个SIGALRM信号.
信号集操作函数

sigset_t myset;自己的信号集, 即"位图"
影响阻塞信号集3号位的方法:
1.先sigemptyset(&myset)再sigaddset(&myset, 3)把自己的信号集3号位设置为1(注意信号集编号从1开始, 长度为64位unsigned long int, 对应kill -l列出来的信号宏, 一般操作前31个).
2.如果是设置阻塞, 调用sigprocmask(SIG_BLOCK, &myset, &oldset)把阻塞集mask的3号位置为1, 其中sigset_t oldset是传出参数, 用来保存原来的阻塞集状态, 可以传NULL. 之后当内核发送3号信号(SIGQUIT)时, 未决信号集的第3位也根据阻塞集变为1.
3.如果是解除阻塞, 调用sigprocmask(SIG_UNBLOCK, &myset, &oldset)把阻塞集mask的3号位置为0, 则未决集的3号位也会从1变0. 也就是说无论是设置阻塞还是解除阻塞, 位图myset的对应号位都要置为1.
类似sigaddset()的的还有sigfillset(&myset)全置1, sigdelset(&myset, 3)把3号位改为0(若已为0则无动作), sigismember(&myset, 5)判断5号位是否为1.
阻塞信号集不可读, 但可以使用int sigpending(sigset_t *set)来读取未决信号集, set是传出参数. 打印信号集要使用sigismember函数.
sigaction()捕捉信号
信号捕捉函数由内核调用, 即所谓的回调模式, 只有在确认信号已抵达后才会被调用, 在用户空间执行, 执行完通过sigreturn返回内核进行报道, 再从上次被中断的地方继续执行.

注册信号捕捉函数除了signal(signum, handler)之外, 还可以使用int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)函数, 用结构体的sigset_t sa_mask成员设置信号处理期间要被屏蔽的信号集合(使用sigemptyset()和sigaddset()设置), 防止调用sa_handler(函数指针型成员变量)期间被打断, sa_mask会覆盖内核的阻塞集mask. 另外sa_flags也要设置为0, 表示sa_handler处理某信号期间忽略相同信号.
捕获完信号后最好恢复成信号的默认动作sigaction(signum, &oldact, NULL), 且sigaction()调用前后都要对要接收的信号进行屏蔽(设置mask), 以防止注册信号捕捉函数的过程中信号到来.

时序竞态(竞态条件)
pause()函数可以让进程主动被挂起, 当捕获并处理完信号后返回-1, 设置errno为EINTR. 需和信号捕捉函数配合使用
如果alarm()后失去CPU, 且过了定时的时间, 当重获CPU时会先处理已发送并阻塞的SIGALRM信号, 这样pause()后就无法接收到本应等待的SIGALRM, 从而一直挂起, 导致时序竞态问题. 使用sigsuspend可解决该问题.

sigsuspend()函数实质上就是在内核阻塞信号集mask上把SIGALRM屏蔽, 当执行sigsuspend()时再使用临时阻塞集解除屏蔽. 这样即使alarm后失去CPU, SIGALRM信号被阻塞, 等恢复执行后也是先调用sigsuspend()并用捕捉函数处理信号, 而不是先处理SIGALRM再pause()导致一直挂起.
全局变量异步I/O
对于进程间通信, 全局变量存在异步I/O的情况, 应尽量少用全局变量(使用锁的机制可以避免这种情况). 即可能主函数信号发出后失去CPU, 当恢复CPU后, 另一端已发送的信号会优先被处理(信号捕捉函数里可以改变全局变量), 使得对全局变量的修改顺序出现错误, 导致进程挂起.
使用全局变量的函数容易变成不可重入函数, 一旦被信号打断容易发生错误, 执行结果会和预期的不同(如递归操作). 所以信号捕捉函数要设计成可重入函数, 避免使用全局变量和static变量, 避免使用malloc/free.
捕捉SIGCHLD回收子进程
当多个子进程同时死亡时, 虽然信号捕捉函数一次只能处理一个信号, 但如果waitpid()第一个参数设为回收全部子进程, 在这一次调用的过程中waitpid()就会把当前剩下死亡的子进程都回收掉, 此处信号的作用只是激活waitpid()函数, 回收几个子进程由waitpid自己决定.
中断慢速系统调用

read在读文件时不会阻塞, 在读网络/设备/管道时可能发生阻塞, 此时收到信号就会被中断.
中断的慢速系统调用要么重启要么执行默认动作, 如果重启需要again和goto参数, 同时判断EINTR信号
