该系列文章总纲链接:专题分纲目录 LinuxC 系统编程
本章节思维导图如下所示(思维导图会持续迭代):
第一层:
第二层:
信号与信号处理
信号是一种典型的异步通讯方式,也是linux下常用的进程通讯方式之一,也称做软中断。利用kill -l命令可以查看系统所支持的信号列表。一般有:
1 信号基础
1.1 信号的产生
linux下有5种方式可以产生信号。
注意:无论上述5种方法哪一种发送了信号,接收信号的进程都会暂停执行程序,转而处理接收到的信号。如果进程处于就绪状态/阻塞状态,那么一旦得到CPU的时间片将首先处理信号。如果进程处于挂起状态,那么接收信号将唤醒挂起进程,进程将首先处理信号。总之就是优先处理信号的原则,哪怕程序是死循环。
1.2 信号的处理
linux下进程有3种处理方式:
1.3 常用信号的使用方法
系统设置的向进程发送信号的快捷键。
ctrl+C:结束当前的进程。
ctrl+/:程序运行结束,并且产生了core文件。core文件用于程序的调试,可以用gdb工具对其进行调试。
使用kill命令向一个指定的进程发送指定信号。
2 信号的影响
不论当前进程执行到何处,都会跳到信号处理函数中执行,所以信号也会产生副作用,这是避免不了的。例如,在一个进程中,main函数、其他线程以及信号处理函数都是各自独立的执行流程,它们是并行的。如果一个进程有多个执行流程并且这些流程访问相同的资源,就有可能出现冲突。
2.1 重入
基本概念:
如果一个函数满足以下条件之一则是不可重入的:
总之,使用了具有全局作用域的数据/函数都是不可重入的,这种函数/代码被称为非纯代码。
2.2 原子操作
2.3 中断系统调用
3 信号处理函数
3.1 设置信号处理函数
linux下允许用户提供自己的信号处理函数,使用signal函数将处理函数加载,并且通知系统:
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
详细见linux函数参考手册。实际上在signal.h文件中是这样的:
#define SIG_IGN ((void *) (*)()) 1
#define SIG_DFL ((void *) (*)()) 0
#define SIG_ERR ((void *) (*)()) -1
系统中信号处理程序的整体框架是这样的:
void signal_handler(int signo)
{
switch(signo){
case SIG1: /*处理信号1*/
...
case SIG2: /*处理信号2*/
...
case SIGn: /*处理信号n*/
...
}
}
linux系统下不允许用户创建新的信号,但是提供两个信号SIGUSR1和SIGUSR2专门用于应用程序之间进行信号通讯。这两个信号没有特殊的意义,系统默认的方式是忽略。
3.2 发送信号与向进程本身发送信号
@1 信号可以很好地应用到进程间的通信。linux下使用kill函数向进程/进程组发送信号,函数的原型如下:
#include
#include
int kill(pid_t pid, int sig);
详细见linux函数参考手册。一个进程向另一个进程发送信号时必须注意以下几点:
@2 linux下单独提供了向进程本身发送信号的函数,该函数可以替代上述形式。函数原型如下:
#include
int raise(int sig);
详细见linux函数参考手册。利用raise函数可以实现exit函数的功能,所不同的是它不做任何善后处理(比如冲洗流、关闭文件等)。因此,如果信号的处理方式是终结进程,那么应该编写一个处理所有善后事宜的函数作为此信号的信号处理程序,使用signal函数设置后使用。
3.3 设置linux定时器
有些场合需要设置一个定时器,经过若干时间后通知设置定时器的进程。linux下使用alarm函数设置一个定时器,函数原型如下:
#include
unsigned int alarm(unsigned int seconds);
详细见linux函数参考手册。alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。当然,有些点还是需要注意的:
3.4 挂起进程
当一个进程的运行条件已经具备时仍然需要使进程阻塞(例如期望进程延迟执行,sleep函数经常起到这个作用),这种由进程资源进入阻塞状态的1情况称为进程挂起。linux下使用pause函数实现挂起一个进程,函数原型如下:
#include
int pause(void);
/*pause函数执行成功返回-1,同时将errno设置为EINTR。
pause函数使调用该函数的进程进入挂起状态,直到有一个终止进程的信号/一个信号处理程序从其返回后,pause函数才返回。*/
详细见linux函数参考手册。
3.5 进程休眠
pause函数是使进程无时间限制的挂起,若想使进程在一定时间内恢复运行则使用sleep函数,函数原型如下:
#include
unsigned int sleep(unsigned int seconds);
int usleep(useconds_t usec);
详细见linux函数参考手册。
4 信号集与屏蔽信号
4.1 信号集和信号集处理函数
屏蔽信号是linux系统下一个重要的功能,允许用户进程有选择的接收并处理信号。需要屏蔽的信号使用信号集表示。这种集合是一种位向量,其中每一位对应一个信号。这种数据类型是sigset_t。对信号进行操作不可以直接进行移位,需要系统函数来实现相关功能。在linux下,提供了一组信号集处理函数:
#include
int sigemptyset(sigset_t *set); //将信号集清空
int sigfillset(sigset_t *set); //将所有信号添加到信号集
int sigaddset(sigset_t *set, int signum); //将信号signum添加到信号集set中
int sigdelset(sigset_t *set, int signum); //将信号signum从信号集set中删除
int sigismember(const sigset_t *set, int signum); //在信号集set中查看signum信号是否被设置,被设置函数返回1,没被设置函数返回0,查看失败返回-1。
详细见linux函数参考手册。
注意:信号的编号是从1开始的,而信号处理函数中的位编号是从0开始的,因此有:
signum==信号编码-1
4.2 屏蔽信号
程序中需要对信号进行处理,自然就有一些情况不需要对信号进行处理。所以需要屏蔽信号的函数。每个进程都有一个信号屏蔽字,标记被屏蔽的信号。信号屏蔽字的本质是同信号集一样的,是一个位向量,信号编码对应的位是1表示屏蔽该信号,对应的位是0表示处理该信号。linux下使用sigpromask函数实现检测或更改进程的信号屏蔽字。sigpromask函数的原型如下:
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
详细见linux函数参考手册。(注意:SIG_KILL和SIG_STOP这两个信号不能被屏蔽。如果这两个信号被屏蔽,恶意进程就不用担心被根用户的kill命令杀死而可以肆意破坏了)
4.3 处理未决信号
信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间;可以通过 int sigpending (sigset_t *set) 和 int sigismember (const sigset_t *set, int signum) 查看某个信号是未决的。linux下使用sigpending函数来检查未决信号,函数的原型如下:
#include
int sigpending(sigset_t *set);
详细见linux函数参考手册。