信号
基本概念及机制
信号的共性:
- 简单
- 不能携带大量信息
- 满足特性条件才能发送
特质:A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行去处理信号,处理完毕之后再继续执行。与硬件中断类似——异步模式。但信号是软件层面上的实现的中断,早期被称为”软中断”。
信号的特质:由于信号通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
所有信号的产生和处理, 都是由内核完成的。
与信号相关的事件和状态
产生信号:
- 按键产生: - Ctrl+c, Ctrl+z, Ctrl+\
- 系统调用产生: - kill, raise, abort
- 软件条件产生:定时器 - alarm
- 硬件异常产生:非法访问内存(段错误), 除0(浮点数例外), 内存对齐错误(总线错误); 
- 命令产生: - kill命令
递达: 内核发出的信号递送并且到达进程
未决: 产生和递达之间的状态, 主要由于阻塞(屏蔽)导致该状态
信号的处理方式:
- 执行默认动作 
- 丢弃(忽略) 
- 捕捉(调用户处理函数) 
信号屏蔽字和未决信号集
Linux内核的进程控制块PCB是一个结构体,task_struct除了包含进程id,状态,工作目录,用户id,组id,文件描述符,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞态:用阻塞信号集(信号屏蔽字)来描述
PCB中阻塞信号集影响未决信号集
阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽字后)
未决信号集:
- 信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态,当信号被处理后,对应位翻转回为0,这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号4要素
- 编号:信号有自己的编号,不存在为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号)。34-64为实时信号,驱动编程与硬件相关,名字上区别不大。而前32个名字各不相同。 
- 名称 
- 事件 
- 默认处理动作 - Term:终止进程
- Ign:忽略信号(默认即时对该种信号忽略操作)
- Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试)
- Stop:停止(暂停)进程
- Cont:继续运行进程
 
man 7 signal可以查看帮助文档
特别强调:9)SIGKILL和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其其设置为阻塞。
只有每个信号所对应的事件发生了, 该信号才会被递送(但不一定递达), 不应该乱发信号
kill函数
给指定进程发送指定信号(不一定杀死)
| 1 | int kill(pid_t pid, int sig);//成功:0, 失败:-1(ID非法,普通用户杀init进程等权级问题),设置errno | 
进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,它们互相关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。kill -9 (root用户的pid)是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号,普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID。
raise和abort函数
raise函数:给当前进程发送指定信号(自己给自己发)
| 1 | raise(signo) == kill(getpid(), signo); | 
abort函数:给自己发送异常终止信号。SIGABRT信号,终止并产生core文件。
| 1 | void abort(void); //该函数无返回 | 
软件条件产生信号(定时产生信号)
alarm函数:设置定时器(闹钟),在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。
| 1 | unsigned int alarm(unsigned int seconds);//返回0或剩余的秒数,无失败 | 
常用:取消定时器alarm(0),返回旧闹钟余下秒数
定时,与进程无关(自然定时法)!无论进程处于何种状态(就绪、运行、挂起、终止、僵尸…),alarm都计时。
| 1 | //测试一秒钟数多少个数 | 
time ./alarm可以统计alarm的运算时间。
使用time命令查看程序执行的时间。程序运行的瓶颈在IO,优化程序,首先优化IO
实际执行时间 = 系统时间+用户时间+等待时间
setitimer函数:设置定时器(闹钟),可以替代alarm函数,精度微秒(us),可以实现周期定时
| 1 | 
 | 
提示:
- it_interval :用来设定两次定时任务之间间隔的时间
- it_value:定时的时长
- 两个参数都设置为0,即清0操作 
signal捕捉信号:
| 1 | 
 | 
信号集操作函数
内核通过读取未决信号集来判断信号是否应该被处理,信号屏蔽字mask可以影响未决信集。可以在应用程序中自定义set来改变mask以达到屏蔽指定信号的目的。
操作信号集的若干步骤
| 1 | /*创建一个自定义信号集*/ | 
信号集设定
sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用下列函数,保证跨系统操作有效。
| 1 | sigset_t set; //typedef unsigned long sigset_t | 
sigprocmask函数
用来屏蔽信号、解除屏蔽也使用该函数。其本质为读取或修改进程的信号屏蔽字(PCB中).
注意:屏蔽信号只是将信号处理延后执行(延至解除屏蔽),而忽略表示将信号丢弃处理
| 1 | int sigprocmask(int how, const sigset *set, sigset_t *oldset);//成功,0,失败-1,设置errno | 
sigpending函数
读取当前进程的未决信号集
| 1 | int sigpending(sigset *set) ;//set传出参数。 | 
打印未决信号集:
| 1 | 
 | 
简易信号捕捉
| 1 | 
 | 
sigaction函数注册捕捉
sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)
| 1 | int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact); | 
| 1 | 
 | 
信号捕捉特性
- 进程正常运行时,默认PCB中有一个信号屏蔽字,假定为 - x,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号之后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不能由- x来指定。而是用- sa_mask来指定。调用完信号处理函数,再次恢复为- x(捕捉函数执行期间, 信号屏蔽字由- mask变为- sigaction结构体中的- sa_mask, 捕捉函数执行结束后, 恢复回- mask)。
- xxx信号捕捉函数执行期间,- xxx信号自动被屏蔽(捕捉函数执行期间, 本信号自动被屏蔽(- sa_flags=0);)
- 阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)(捕捉函数执行期间, 若被屏蔽信号多次发送, 解除屏蔽后只响应一次)
| 1 | 
 | 
内核实现信号捕捉过程:

为什么执行完信号处理函数后要再次进入内核?因为信号处理函数是内核调用的, 函数执行完毕后要返回给调用者。
竞态条件
pause函数
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu)直到信号递达将其唤醒。
| 1 | int pause(void) ;//返回值:-1并设置errno为EINTR | 
使用pause和alarm来实现sleep函数:
| 1 | 
 | 
时序竞态
时序问题分析:
借助pause和alarm实现的mysleep函数,设想如下时序:
- 注册SIGALRM信号处理函数(sigaction...)
- 调用alarm(1)函数设定闹钟1秒
- 函数调用刚结束,开始倒计时1秒,当前进程失去cpu,内核调度优先级高的进程(多个)取代当前进程,当前进程无法获得cpu,进入就绪态等待cpu
- 1秒后,闹钟超时,内核向当前进程发送SIGALARM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
- 优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数catch_sigalrm
- 信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)
- SIGALRM信号已经处理完毕,- pause不会等到。
解决时序问题
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”,sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。
| 1 | int sigsuspend(const sigset_t *mask); //挂起等待信号 | 
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
程序执行过程的信号屏蔽字由sigaction.sa_mask决定,但在执行sigsuspend期间由传入的mask决定。
可将某个信号(如SIGALRM)从临时屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsubpend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号字。
| 1 | unsigned int mysleep1(unsigned int seconds) | 
总结
竞态条件跟系统负载有很紧密的的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理导致。信号是通过软件方式实现的(与内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理结束后,需要扫描PCB中的未决信号集来判断是否应该处理某个信号,当系统负载过重时,会出现时序混乱。
这种意外情况只能出现在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补,且由于该错误不具规律性,后期捕捉和重现十分困难。
可重入函数,不可重入函数
一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称为“重入”,根据函数实现的方法可分为”可重入函数”和“不可重入函数”。
注意事项:
- 定义可重入函数,函数内部不能含有全局变量及static变量,不能使用malloc,free
- 信号捕捉函数应设计为可重入函数
- 信号处理程序可以调用的可重入函数可参阅man 7 signal
SIGCHLD信号
产生条件
- 子进程终止时
- 子进程收到SIGSTOP信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
借助SIGCHLD信号回收子进程
子进程结束运行,其父进程会收到SIGCHLD信号,该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
| 1 | 
 | 
中断系统调用
系统调用可以分为两种:慢速系统调用和其他系统调用。
- 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就会被中断,不再继续执行(早期)。也可以设定系统调用是否重启。如read、write、pause、wait...
- 其他系统调用:getpid、getppid、fork
结合pause,回顾慢速系统调用:
慢速系统调用被中断的相关行为。实际上就是pause的行为,如read:
- 想中断pause,信号不能被屏蔽
- 信号的处理方式必须是捕捉(默认、忽略都不可以)
- 中断后返回-1,设置errno为EINTR(表示被信号中断)
可以修改sa_flags参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启,SA_RESTART重启。
sa_flags还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉信号期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号。
终端
所有输入输出设备总称。
终端启动流程:init->fork->exec->getty->用户输入帐号->login->输入密码->exec->bash