信号
信号概念
信号本身是一个通知,信号用于通知某个进程发生了某个事情。
信号都是突发事件,一般不知道什么时候发生,信号是异步发生的,也被称为“软件中断”
信号如何产生?
a)某个进程发送给另外一个进程或发送给自己
b)有内核(操作系统)发送给某个进程,比如键盘输入ctrl+c,kill命令等,内存访问异常,除数为0等硬件会检测到并且通知给内核。
信号名字,都是以SIG开头,比如SIGHUP
UNIX以及类(类似)UNIX类操作系统(linux,freebd,solaris)支持的信号数量各不相同,10~60多个之间。
信号既有名字,但本质上是一些数字,正整数常量,通过宏定义,数字从1开始。
#include <signal.h>
这里我输入
1 | sudo find / -name "signal.h" | xargs grep -in "SIGHUP" |
该条命令的意思是在‘/‘目录下寻找所有signal.h命名的文件,在文件中寻找SIGHUP句子,-i表示查找时忽略大小写,-n显示行号,xargs是用于给grep命令传递参数。
接着打开/usr/include/x86_64-linux-gnu/asm/signal.h
kill与信号
kill 进程id
kill工作是发个信号给进程,一般来说自己写的程序如果没有通过代码来处理信号,操作系统会根据信号默认行为处理进程。
kill能给进程发送多种信号
如上图所示打开了三个终端并且运行nginx程序。
接着直接kill 9162
中止nginx进程
通过strace工具可以看到9162号进程nginx被9141号进程即bash进程发出的SIGTERM信号(15号)所杀死
我们的程序代码如图所示,并没有处理SIGTERM信号。因此操作信号针对SIGTERM信号有一个缺省动作,直接将该进程干掉。
kill -数字 进程id
能发出这个数组对应的信号给进程
kill -1 9191
给9191号进程一个SIGHUP信号。(signal.h头文件定义#define SIGHUP 1)
如图所示,再次运行nigix程序并且strace跟踪,执行kill -1 9191
后运行结果如图。nginx被挂起了。
如果代码没有处理对应信号,那么操作系统对绝大多数的信号的缺省动作都是把进程干掉杀死。
kill的参数 | 该参数发出的信号 | 操作系统缺省动作 |
---|---|---|
-1 | SIGHUP(连接断开) | 终止掉进程(进程没了) |
-2 | SIGINT(终端中断符,比如ctrl+c) | 终止掉进程(进程没了) |
-3 | SIGQUIT(终端退出符,比如ctrl+\) | 终止掉进程(进程没了) |
-9 | SIGKILL(终止) | 终止掉进程(进程没了) |
-15 | SIGTERM(kill pid不指定信号则默认发送该信号) | 终止掉进程(进程没了) |
-18 | SIGCONT(使暂停的进程继续) | 忽略(进程依旧在运行不受影响) |
-19 | SIGSTOP(停止),可用SIGCONT继续,但任务被放到了后台 | 停止进程(不是终止,进程还在) |
-20 | SIGTSTP(终端停止符,比如ctrl+z),但任务被放到了后台,可用SIGCONT继续 | 停止进程(不是终止,进程还在) |
再次运行nginx程序,观察各个进程的state,再对进程进程执行kill -19 9215
后nginx进程暂停,STAT状态由S+变为了T
再执行kill -18 9215
nginx程序继续运行起来了但观察状态变为了S,少了’+‘,表示不再是前台运行的进程。
进程状态
进程状态 | 含义 |
---|---|
D | 不可中断的休眠状态(通常是I/O的进程),可以处理信号,有 延迟 |
R | 可执行状态&运行状态(在运行队列里的状态) |
S | 可中断的休眠状态之中(等待某事件完成),可以处理信号 |
T | 停止或被追踪(被作业控制信号所停止) |
Z | 僵尸进程 |
X | 死掉的进程 |
< | 高优先级的进程 |
N | 低优先级的进程 |
L | 有些页被锁进内存 |
s | Session leader(进程的领导者),在它下面有子进程 |
t | 追踪期间被调试器所停止 |
+ | 位于前台的进程组 |
常用信号
信号名 | 信号含义 |
---|---|
SIGHUP(连接断开) | 是终端断开信号,如果终端接口检测到一个连接断开,发送此信号到该终端所在的会话首进程(前面讲过),缺省动作会导致所有相关的进程退出(上节课也重点讲了这个信号,xshell断开就有这个信号送过来); Kill -1 进程号也能发送此信号给进程; |
SIGALRM(定时器超时) | 一般调用系统函数alarm创建定时器,定时器超时了就会这个信号; |
SIGINT(中断) | 从键盘上输入ctrl+C(中断键)【比如你进程正跑着循环干一个事】,这一ctrl+C就能打断你干的事,终止进程; 但shell会将后台进程对该信号的处理设置为忽略(也就是说该进程若在后台运行则不会收到该信号); |
SIGSEGV(无效内存) | 内存访问异常,除数为0等,硬件会检测到并通知内核;其实这个SEGV代表段违例(segmentation violation),你有的时候运行一个你编译出来的可执行的c程序,如果内存有问题,执行的时候就会出现这个提示; |
SIGIO(异步I/O) | 通用异步I/O信号,咱们以后学通讯的时候,如果通讯套接口上有数据到达,或发生一些异步错误,内核就会通知我们这个信号; |
SIGCHLD(子进程改变) | 一个进程终止或者停止时,这个信号会被发送给父进程;(我们想象下nginx,worker进程终止时 master进程应该会收到内核发出的针对该信号的通知); |
SIGUSR1,SIGUSR2(都是用户定义信号) | 用户定义的信号,可用于应用程序,用到再说; |
SIGTERM(终止) | 一般你通过在命令行上输入kill命令来杀一个进程的时候就会触发这个信号,收到这个信号后,你有机会退出前的处理,实现这种所谓优雅退出的效果; |
SIGKILL(终止) | 不能被忽略,这是杀死任意进程的可靠方法,不能被进程本身捕捉,不能通过代码忽略 |
SIGSTOP(停止) | 不能被忽略,使进程停止运行,可以用SIGCONT继续运行,但进程被放入到了后台 |
SIGQUIT(终端退出符) | 从键盘上按ctrl+\ 但shell会将后台进程对该信号的处理设置为忽略(也就是说该进程若在后台运行则不会收到该信号); |
SIGCONT(使暂停进程继续) | 使暂停的进程继续运行 |
SIGTSTP(终端停止符) | 从键盘上按ctrl+z,进程被停止,并被放入后台,可以用SIGCONT继续运行 |
信号处理动作
当某个信号出现时,我们可以按三种方式之一进行处理,我们称之为信号的处理或者与信号相关的动作;
(1)执行系统默认动作 ,绝大多数信号的默认动作是杀死你这个进程;
(2)忽略此信号(但是不包括SIGKILL和SIGSTOP)
kill -9 进程id,是一定能够把这个进程杀掉的;
(3)捕捉该信号:我写个处理函数,信号来的时候,我就用处理函数来处理;(但是不包括SIGKILL和SIGSTOP)
Unix/Linux操作系统体系结构
Unix/Linux操作系统体系结构分为两个状态(1)用户态(2)内核态
a)操作系统/内核
用来控制计算机的硬件资源,提供应用程序运行的环境
一般而言我们写的程序,要么运行在用户态,要么运行在内核态。一般是运行在用户态。
当程序执行一些特殊代码的时候,程序就可能切换到内核态,这种切换由操作系统控制,不需要人为介入。
b)系统调用
就是一些系统函数,只需调用接口不用管具体细节
c)shell
bash(borne again shell(重新装配的shell))是shell的一种,而linux默认使用bash这种shell。
通俗一点来说,bash是一个可执行程序(/bin/bash),主要作用是把用户输入的命令翻译给操作系统,相当于一个命令解释器
shell可以分割系统调用和应用程序,类似于“胶水”
d)用户态和内核态的切换
运行于用户态的进程可以执行的操作和访问的资源会受到极大限制,而运行于内核态的进程可以执行任何操作并且在资源的使用上没有限制
一个进程执行的时候,大部分时间是处于用户态下的,只有需要内核所提供的服务时 才会切换到内核态,内核态做的事情完成后又转回到用户态;
malloc();printf(); 这种状态在转换是操作系统干的,不需要我们介入;
为什么要区分用户态和内核态?
大概有两个目的:(1)一般情况下,程序都运行在用户态状态,权限小,不至于危害到系统其他部分;当你想干一些危险的事情的时候,系统给你提供接口。(2)既然这些接口是系统提供给你的,那么这些接口也是操作系统统一管理的;资源是有限的, 如果大家都来访问这些资源,如果不加以管理,一个是访问冲突,一个是被访问的资源如果耗尽,那系统还可能崩溃;系统提供这些接口,就是为了减少有限的资源的访问以及使用上冲突;
什么时候用户态切换到内核态?
a)系统调用,比如调用malloc();
b)异常事件,比如来了个信号;
c)外围设备中断:
signal函数范例
信号来了之后我们可以忽略,可以捕捉,可以用signal函数实现。
发现有信号处理函数又会回到用户态,处理完后再回到内核态收尾工作。注意内核态与用户态来回切换是会存储用户态的状态的而不是重头执行。
1 |
|
可重入函数
可重入函数就是我们再信号处理函数中调用它是安全的;在信号处理程序中保证调用安全的函数,这些函数是可重入的函数并且被称为异步信号安全的。
有一些大家周知的函数都是不可重入的,比如malloc(),printf();
比如int errno由系统管理设置,系统函数出错会将errno修改为对应值,但是可能由于信号处理函数里面意外的修改了errno的值,会导致回到main函数中的一些问题。
严格意义:muNEfunc()函数不应该是一个可重入函数。
在写信号处理函数的注意事项
- 尽量使用简单语句做简单的事情,尽量不要调用系统函数以免引起麻烦。
- 如果必须要在信号处理函数中调用一些系统函数,那么要保证在信号处理函数中调用的 系统函数一定要是可重入的;
- 如果必须要在信号处理函数中调用那些可能修改errno值的可重入的系统函数,那么 就得在信号处理函数里事先备份errno值,从信号处理函数返回之前,将errno值恢复;
滥用不可重入函数
在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果有一个函数不幸被设计成为这样:那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数。
一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
在上述代码中将main函数的for循环中调用malloc()函数和信号处理函数中也调用malloc()函数,这将引起一个严重错误。
1 |
|
main函数可能执行到malloc的时候恰好被信号处理函数打断,这个时候malloc又运行了一次导致出现了错误。
一旦在信号处理函数中用了不可重入函数,可能导致程序错乱,不正常。。。。。
signal因为兼容性,可靠性等等一些历史问题;不建议使用(我们的策略,坚决不用),建议用sigaction()函数代替;
高手:我们摸不清楚(有可能有坑)的东西(地方)我们主动回避,不去踩;
信号集
信号集概念
信号处理函数尚未处理完的时候又来了一个信号?一般是不会处理第二个信号,系统会屏蔽或阻塞
进程必须能够记住这个进程当前阻塞了哪些信号,我们需要“信号集”的这么一种数据类型,能够把这60多个信号都表示下,都装下。
linux是用sigset_t结构类型来表示信号集的
1 | typedef struct{ |
信号集的定义:信号集表示一组信号的来(置1)或者没来(置0)
信号集相关函数
void (*signal(int signum, void(* handler)(int)))(int);
函数说明:signal()会依参数signum 指定的信号编号来设置该信号的处理函数. 当指定的信号到达时就会跳转到参数handler 指定的函数执行. 如果参数handler 不是函数指针, 则必须是下列两个常数之一:
1、SIG_IGN 忽略参数signum 指定的信号.
2、SIG_DFL 将参数signum 指定的信号重设为核心预设的信号处理方式.返回值:返回先前的信号处理函数指针, 如果有错误则返回SIG_ERR(-1).
附加说明:在信号发生跳转到自定的 handler 处理函数执行后, 系统会自动将此处理函数换回原来系统预设的处理方式, 如果要改变此操作请改用sigaction().
sigemptyset();把信号集中的所有信号置为0,表示这60多个信号都没有来;
00000000,00000000,00000000…….*int sigemptyset(sigset_t set);
函数说明:sigemptyset()用来将参数set 信号集初始化并清空.
返回值:执行成功则返回0, 如果有错误则返回-1.
sigfillset();把信号集中的所有信号置为1。
11111111,11111111,11111111………int sigfillset(sigset_t * set);
函数说明:sigfillset()用来将参数set 信号集初始化, 然后把所有的信号加入到此信号集里.
返回值:执行成功则返回0, 如果有错误则返回-1。
sigaddset(),sigdelset()就可以往信号机中增加信号(将某一信号从0变为1)或者从信号集中删除特定信号(将某一信号从1变为0)。
*int sigaddset(sigset_t set, int signum);
函数说明:sigaddset()用来将参数signum 代表的信号加入至参数set 信号集里.
返回值:执行成功则返回0, 如果有错误则返回-1.
int sigdelset(sigset_t * set, int signum);
函数说明:sigdelset()用来将参数signum 代表的信号从参数set 信号集里删除。
返回值:执行成功则返回0, 如果有错误则返回-1.
sigprocmask(),sigmember()
每个进程都会有一个信号集,用来记录当前屏蔽了(阻塞)了哪些信号。
如果我们把这个信号集中的某个信号位设置为1,就表示屏蔽了同类信号,此时再来个同类信号,那么同类信号会被屏蔽,不能传递给进程;如果这个信号集中有很多个信号位都被设置为1,那么所有这些被设置为1的信号都是属于当前被阻塞的而不能传递到该进程的信号;
sigprocmask函数能够设置该进程所对应的信号集中的内容。*int sigprocmask(int how, const sigset_t set, sigset_t * oldset);
函数说明:sigprocmask()可以用来改变目前的信号遮罩, 其操作依参数how 来决定:
1、SIG_BLOCK 新的信号遮罩由目前的信号遮罩和参数set 指定的信号遮罩作联集
2、SIG_UNBLOCK 将目前的信号遮罩删除掉参数set 指定的信号遮罩
3、SIG_SETMASK 将目前的信号遮罩设成参数set 指定的信号遮罩. 如果参数oldset 不是NULL 指针, 那么目前的信号遮罩会由此指针返回.返回值:执行成功则返回0, 如果有错误则返回-1.
错误代码:
1、EFAULT 参数set, oldset 指针地址无法存取.
2、EINTR 此调用被中断。*int sigismember(const sigset_t set, int signum);
函数说明:sigismember()用来测试参数signum 代表的信号是否已加入至参数set 信号集里. 如果信号集里已有该信号则返回1, 否则返回0.
返回值:信号集已有该信号则返回1, 没有则返回0.如果有错误则返回-1.
错误代码:
1、EFAULT 参数set 指针地址无法存取。
2、EINVAL 参数signum 非合法的信号编号。
sigprocmask范例
1 |
|
观察上述运行情况执行完30行sigprocmask(SIG_BLOCK,&newmask,&oldmask
后,尽管来了6个SIGQUIT信号,进程依然没有退出仍然在执行,
但是执行到代码57行sigprocmask(SIG_SETMASK,&oldmask,NULL)
将sigset重新置为oldmask(即全0)后,”瞬间地“操作系统将6个SIGQUIT信号六合一直接“抛给进程”,进程立刻调用信号处理函数sig_quit(int signo),只执行1次信号处理函数接着回到58行继续执行。
不再屏蔽SIGQUIT信号后,运行到72行sleep(10)
睡眠,发送SIGQUIT信号后进程将直接退出(信号没有阻塞)。
我们再可以对信号处理函数进行修改,如下所示:
第一次收到SIGQUIT信号就会调用sig_quit信号处理函数,并且在信号处理函数内将给SIGQUIT信号设为核心预设的信号处理方式即缺省处理方式,
那么第二次收到SIGQUIT信号就会执行操作系统的缺省处理方式(即直接终止进程)。
1 | void sig_quit(int signo) |
注意到执行结果有所变化,同样的执行到代码57行sigprocmask(SIG_SETMASK,&oldmask,NULL)
语句时,5个信号合为1个进行处理,这次处理中的signal(SIGQUIT,SIG_DFL)
语句会将SIGQUIT信号设为缺省处理方式,继续运行到72行sleep(10)
再次睡眠的时候再给进程一个SIGQUIT信号,操作系统缺省动作直接将进程终止。
另外sleep()函数能够被打断两种情况:
- 时间到达了
- 来了某个信号,使sleep()提前结束,此时sleep会返回一个值,这个值就是未睡够的时间
信号高级认识范例
1 |
|
用kill 发送 USR1信号给进程
- 执行信号处理函数内sleep(10),被卡住了10秒,这个时候因为流程回不到main(),所以main中的语句无法得到执行;
- 在触发SIGUSR1信号并因此sleep了10秒种期间,就算你多次触发SIGUSR1信号,也不会重新执行SIGUSR1信号对应的信号处理函数,而是会等待上一个SIGUSR1信号处理函数执行完毕才 第二次执行SIGUSR1信号处理函数;
换句话说:在信号处理函数被调用时,操作系统建立的新信号屏蔽字(sigprocmask()),自动包括了正在被递送的信号,因此,保证了在处理一个给定信号的时候,如果这个信号再次发生,那么它会阻塞到对前一个信号处理结束为止; - 在该信号处理函数执行期间,不管你发送了多少次kill -usr1信号,期间收到的所有的SIGUSR1信号统统被归结为一次。比如当前正在执行SIGUSR1信号的处理程序但没有执行完毕,这个时候,你又发送来了5次SIGUSR1信号,那么当SIGUSR1信号处理程序执行完毕(解除阻塞),SIGUSR1信号的处理程序也只会被调用一次(而不会分别调用5次SIGUSR1信号的处理程序)。
kill -usr1,kill -usr2
- 执行usr1信号处理程序,但是没执行完时,是可以继续进入到usr2信号处理程序里边去执行的,这个时候,相当于usr2信号处理程序没执行完毕,usr1信号处理程序也没执行完毕;此时再发送usr1和usr2都不会有任何响应;(类似于上面那种情况)
- 既然是在执行usr1信号处理程序执行的时候来了usr2信号,导致又去执行了usr2信号处理程序,这就意味着,只有usr2信号处理程序执行完毕,才会返回到usr1信号处理程序,只有usr1信号处理程序执行完毕了,才会最终返回到main函数主流程中去继续执行;
思考:如果我希望在我处理SIGUSR1信号,执行usr1信号处理程序的时候,如果来了SIGUSR2信号,我想堵住(屏蔽住),不想让程序流程跳到SIGUSR2信号处理中去执行,可以做到的;
捕获信号可能无线轮回
1 |
|
一直捕获SIGSGEV信号,似乎信号处理函数没达到他的作用?
不,他起了作用,当接收到SIGSGEV信号时,程序自动跳转到信号处理函数处执行,打印出提示信息。然后返回主函数执行非法访问语句(*s) = 1;
再次地出现段错误,抛出信号,继续无限循环。