Unix系统编程 信号部分学习笔记
Posted 狱典司
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unix系统编程 信号部分学习笔记相关的知识,希望对你有一定的参考价值。
《Unix系统编程》Unix信号学习笔记
一、 基础概念
信号是对事件(小事件)相关进程的软件层面通知;
- 有发出者和接收者(都是进程)–> 进程间通信
- 信号实际类型:int
信号的产生、递送、挂起:
- 产生:导致信号发生的事件
- 递送:传递/签收,目标进程执行与信号绑定的操作,即根据信号采取行动(看到还不算完,还要执行)
- 挂起:产生了但是还没递送,即以及生成但还没传递(1.时间片没轮到2.信号的屏蔽)
接信号的收者要安装信号处理程序(绑定信号–>操作)
如果在传递信号时进程执行了“信号处理程序”,那么进程就捕捉到了这个信号。
程序以用户编写的函数(处理程序)名作为参数调用sigaction来安装信号处理程序
也可以用SIG_DFL或SIG_IGN而不用处理程序来调用sigaction函数。
SIG_DFL表示采用默认的动作
SIG_IGN表示忽略信号
上面这两种动作都不是在捕捉信号。
信号生成时采取的动作取决于:
- 该信号当前使用的信号处理程序
- 进程信号掩码(process signal mask)
注意:信号掩码中包含一个当前被阻塞信号的列表。
注意区分阻塞一个信号和忽略一个信号的区别
1.忽略:直接将被忽略的信号丢弃
实现方式:程序通过调用sigaction将信号处理程序设置为SIG_IGN来忽略一个信号。
2.阻塞:信号不会被丢弃;如果一个挂起信号被阻塞了,当进程解除对该信号的阻塞时,信号会被传递出去。
实现方式:程序通过调用sigprocmask来改变它的进程信号掩码来阻塞一个信号。
signal函数测试代码
#include"apue.h"
static void sig_usr(int signo) /* signal handler OR signal-catching function */
{
if (signo==SIGUSR1){
printf("recieve SIGUSR1\\n");
}
else if(signo==SIGUSR2){
printf("receive SIGUSR2\\n");
}
else{
printf("receive sinal %d\\n",signo);
}
}
int main(void){
//printf("%p\\n",sig_usr); /* check the address of function by the name of it */
if(signal(SIGUSR1,sig_usr)==SIG_ERR){ //注意第二个参数使用的是存储函数地址的指针
fprintf(stderr,"cannot catch SIGUSR1\\n");
}
if(signal(SIGUSR2,sig_usr)==SIG_ERR){
fprintf(stderr,"cannot catch SIGUSR2\\n");
}
while(1)
pause();//使得调用的进程在收到某一信号前挂起
//进程调用pause函数使得自己休眠,直到捕捉到一个信号
}
Shell上执行结果:
[root@localhost Q10]# gcc signal_10-2.c
[root@localhost Q10]# ./a.out & ( & 让程序在后台运行 )
[6] 4218 (作业控制shell打印作业编号和进程ID )
[root@localhost Q10]# kill -USR1 4218
recieve SIGUSR1
[root@localhost Q10]# kill -USR2 4218
receive SIGUSR2
[root@localhost Q10]#
二、 信号的产生
在shell上使用kill命令
注意:kill的最后一种格式只支持如下signal_number的值:
0为信号0,1为信号SIGHUP,2为信号SIGINT,3为信号SIGQUIT,6为新信号SIGABRT,9为信号SIGKILL,14为信号SIGALARM,15为信号SIGTERM。
- 查看信号编号:
看到9号信号是SIGKILL,可以用这个来杀死进程
在程序内调用kill函数:
Kill函数针对pid划分出的四种情况:
- Pid > 0 : 向相应进程发送信号
- Pid = 0 : 向调用程序的进程组成员发送信号
- Pid = -1 : kill向所有它有权发送信息的进程发送信号
- Pid < -1: kill将信号发送到组ID等于| pid |(取绝对值,即进程组的领头进程pid)的进程组中去
- 注意:用户只能向其拥有的进程发送信号
对于大多数信号来说,kill通过比较调用进程和目标进程的用户ID来确定它是否有权限发送信号。
SIGCONT是个例外 —— 如果kill被发送到与其在同一个会话中的进程中去,就不对用户ID进行检查了。
进程可以通过raise信号向自己发送一个信号
raise函数只有一个参数,即信号码:
在键盘上按下Ctrl+C来中断一个进程是怎么回事?
由按键动作引起了一个硬件中断,这种硬件中断是由键盘的设备驱动程序来处理(对键盘的输入进行缓冲和编辑)的。
有两个特殊的字符(信号生成字符):
- INTR —— 对应于信号SIGINT
- QUIT —— 对应于信号SIGQUIT
这两个特殊字符会使得设备驱动程序向前台进程组(而不是会话或后台进程组)发送对应的信号。
可以使用stty -a来查看于标准输入相关的设备特性,其中就包括对信号生成字符的设置:
函数alarm
三、对信号掩码和信号集进行操作
进程可以通过阻塞信号暂时地阻止信号的传递。在这个阻塞信号被传递之前,它不会影响进程的行为。
-
信号掩码(signal mask)
类型:sigset_t
作用:进程的信号掩码给出了当前被阻塞的信号的集合。
可以使用类型为sigset_t的信号即来指定对信号的操作(例如阻塞或解除阻塞的操作) -
对信号集(sigset_t)进行操作的五个函数:
注意:下列函数的两个重要参数
(1) sigset_t *set:是指向信号集的指针
(2) int signo:是信号编号或在signal.h中定义的大写信号名(宏)
五个重要函数讲解:
int sigaddset ( sigset_t *set, int signo )
作用:将signo信号加入信号集setint sigdelset ( sigset_t *set, int signo )
作用:将信号signo从信号集set删除int sigemptyset ( sigset_t *set )
作用:将信号集set清空int sigfillset ( sigset_t *set )
作用:将所有的信号加入信号集set中int sigismember(const sigset *set, int signo)
作用:报告signo是否在信号集set中
返回值:在则返回1,不在则返回0;注意:除了sigismember函数,其他函数若成功都返回0,不成功返回-1并设置errno。
函数sigprocmask —— 将信号集设置为阻塞/解除阻塞
函数原型:
#include<signal.h>
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset )
参数解析:
- set:被操作的信号集;如果为NULL,则说明不需要任何修改。
- oset:如果不为NULL,sigprocmask将会把修改之前的信号集放在*oset中返回
- how:用于说明sigprocmask修改信号掩码的方式
(1) SIG_BLOCK:向当前被阻塞的信号(信号掩码)中添加一个信号集
(2) SIG_UNBLOCK:从当前被阻塞的信号中删除一个信号集
(3) SIG_SETMASK:将指定的信号集设置为阻塞信号
四、捕捉与忽略信号 —— sigaction
sigaction函数讲解
函数作用:
(1)绑定信号与该信号相关的动作
(2)检查与某信号绑定的动作
函数原型
#include<signal.h>
sigaction( int sig, const struct sigaction *restrict act, struct sigaction *restrict oact )
参数讲解
- sig:待绑定的信号码
- act:要绑定的动作,是一个指向struct sigaction结构的指针
如果act为NULL,则不改变与sig相绑定的动作(而不是清空sig绑定的动作) - oact:用于接收与信号相关的前一个动作,也是一个指向struct sigaction结构的指针
如果oact为NULL,则对sigaction的调用不会返回sig前一个绑定的动作
返回值:
成功返回0,不成功返回-1并设置errno。
struct sigaction结构体:
struct sigaction{
void (*sa_handler)(int); /* SIG_DFL、SIG_IGN或指向函数(信号处理程序)的指针 */
sigset_t sa_mask; /* 处理程序的执行过程中需要阻塞的额外信号 */
int sa_flags; /* 特殊的标识符和选项 */
void(*sa_sigaction)(int, siginfo_t *, void *) /* 实时处理程序 */
};
注意:POSIX标准中,信号处理程序(sa_handler)就是一个普通的函数,返回void,并有一个int型参数
需要注意的是,在绑定函数sigaction的参数中的act并不是真正的信号处理程序!act是一个结构体(struct sigaction),该结构体中的sa_handler才是(常用的)信号处理程序!
例子8-13很好,是个模板
为什么8-13在处理程序中没有使用fprintf或者是strlen呢?(有关异步信号安全)
另外一些关于信号安全的问题
关于sig_atomic_t作为一个小到可以实现原子访问的变量(了解程序8-5)
另外还需要注意一下用volatile声明的变量,这些变量不存储在寄存器中,直接存储在内存中!
五、等待信号 —— pause、sigsuspend和sigwait
信号提供了一种不需要忙等(busy waiting)来等待事件发生的方法。
注意:忙等是指连续使用CPU周期来检测事件(通常是一个变量)的发生。
POSIX中的函数接口pause、sigsuspend、sigwait提供了三种机制:
将进程挂起,直到信号发生为止。
函数pause
注意:pause的核心问题在于:在pause被启动之前信号可能以及被传递了,之后再执行pause的话进程则会一直被阻塞(产生死锁)。
可以把pause的缺陷简记为:在pause之前传递了信号。
函数sigsuspend (信号安全函数,将sigpromask和purse变成’原子’操作)
对于pause的缺陷,解决的思路是:
必须再解除对信号的阻塞之后立即启动pause,这两个操作的过程应该被合并成一个原子操作。
sigsuspend函数提供了一种实现原子操作的办法。
这样的写法并没有解决pause的缺陷,信号还是有可能在sigdelset之后和sigsuspend之前就发出去了。
所以要想改善这种缺陷,使用例8-16的代码思想来使进程阻塞等待某个(注意是单个)信号 :
例8-16的代码在设计屏蔽集(信号掩码)的时候使用了sigfillset,即把所有的信号都阻塞了,故只能接收到一个信号。
使用例8-17的代码思想可以使进程在被suspend阻塞的情况下等待多个信号:
注意sigprocmask函数:第二个参数为NULL且第三个参数不为NULL的时候就相当于是观察原来的信号掩码,这时候第一个参数应该也没意义了。
例8-18在设置完屏蔽集之后再对信号集做sigdelset来删除要等待的信号,之后用这个改动过的信号集来suspend,减少了一个sigset_t变量的开销,达到了和8-17一样的效果:
sigwait函数
注意一下sigwait和sigsuspend在参数和功能上的区别!
六、信号处理:错误和异步信号安全
七、用siglongjmp和sigsetjmp进行程序控制
siglongjmp和sigsetjmp提供的功能:
信号处理程序直接跳转到期望的终止点。这个跳转过程需要解开程序栈。
将这两个函数与goto语句的用法做类比(注意:只是表面的类比,内部实现有很大不同):
- sigsetjmp函数类似于goto语句的标签;
- 而siglongjump函数类似于一条goto语句
最主要的区别是这对函数在跳转时 —— 清除了栈和信号的状态
函数讲解:
#include<setjmp.h>
(1) void siglongjmp(sigjmp_buf env, int val);
参数分析
1. env:调用程序时必须提供的缓冲区,其类型是sigjmp_buf;
2. val:携带的值,有可能被sigsetjmp返回(在sigsetjmp被信号调用而不是直接调用的时候)
(2) int sigsetjmp(sigjmp_buf env, int savemask);
参数分析
1. 跳转所需的信息集合,与siglongjump里的是同一个;
2. savemask是一个标志位,非零的时候(一般置1),信号掩码的当前状态就被保存在缓冲器env中
关于sigsetjmp的返回值:
- 若是被程序直接调用,则返回0;
- 若是通过信号被siglongjmp跳转到则返回siglongjmp的参数val。
看一个例子来理解:
该篇笔记中PDF版书籍截图图片均来自于《UNIX系统编程》一书
以上是关于Unix系统编程 信号部分学习笔记的主要内容,如果未能解决你的问题,请参考以下文章