信号
基本概念及机制
信号的共性:
- 简单
- 不能携带大量信息
- 满足特性条件才能发送
特质: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_interva
l :用来设定两次定时任务之间间隔的时间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