Unix系统编程 信号部分学习笔记

Posted 狱典司

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unix系统编程 信号部分学习笔记相关的知识,希望对你有一定的参考价值。

一、 基础概念

信号是对事件(小事件)相关进程的软件层面通知;

  1. 有发出者和接收者(都是进程)–> 进程间通信
  2. 信号实际类型:int

信号的产生、递送、挂起:

  1. 产生:导致信号发生的事件
  2. 递送:传递/签收,目标进程执行与信号绑定的操作,即根据信号采取行动(看到还不算完,还要执行)
  3. 挂起:产生了但是还没递送,即以及生成但还没传递(1.时间片没轮到2.信号的屏蔽)

接信号的收者要安装信号处理程序(绑定信号–>操作)
如果在传递信号时进程执行了“信号处理程序”,那么进程就捕捉到了这个信号。

程序以用户编写的函数(处理程序)名作为参数调用sigaction来安装信号处理程序
也可以用SIG_DFL或SIG_IGN而不用处理程序来调用sigaction函数。
 SIG_DFL表示采用默认的动作
 SIG_IGN表示忽略信号
上面这两种动作都不是在捕捉信号。

信号生成时采取的动作取决于:

  1. 该信号当前使用的信号处理程序
  2. 进程信号掩码(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划分出的四种情况:

  1. Pid > 0 : 向相应进程发送信号
  2. Pid = 0 : 向调用程序的进程组成员发送信号
  3. Pid = -1 : kill向所有它有权发送信息的进程发送信号
  4. Pid < -1: kill将信号发送到组ID等于| pid |(取绝对值,即进程组的领头进程pid)的进程组中去


  • 注意:用户只能向其拥有的进程发送信号

对于大多数信号来说,kill通过比较调用进程和目标进程的用户ID来确定它是否有权限发送信号。
SIGCONT是个例外 —— 如果kill被发送到与其在同一个会话中的进程中去,就不对用户ID进行检查了。

进程可以通过raise信号向自己发送一个信号
raise函数只有一个参数,即信号码:

在键盘上按下Ctrl+C来中断一个进程是怎么回事?
由按键动作引起了一个硬件中断,这种硬件中断是由键盘的设备驱动程序来处理(对键盘的输入进行缓冲和编辑)的。

有两个特殊的字符(信号生成字符):

  1. INTR —— 对应于信号SIGINT
  2. QUIT —— 对应于信号SIGQUIT
    这两个特殊字符会使得设备驱动程序向前台进程组(而不是会话或后台进程组)发送对应的信号。

可以使用stty -a来查看于标准输入相关的设备特性,其中就包括对信号生成字符的设置:
函数alarm

三、对信号掩码和信号集进行操作

进程可以通过阻塞信号暂时地阻止信号的传递。在这个阻塞信号被传递之前,它不会影响进程的行为。

  • 信号掩码(signal mask)
    类型:sigset_t
    作用:进程的信号掩码给出了当前被阻塞的信号的集合。
    可以使用类型为sigset_t的信号即来指定对信号的操作(例如阻塞或解除阻塞的操作)

  • 对信号集(sigset_t)进行操作的五个函数:

注意:下列函数的两个重要参数
(1) sigset_t *set:是指向信号集的指针
(2) int signo:是信号编号或在signal.h中定义的大写信号名(宏)

五个重要函数讲解

  1. int sigaddset ( sigset_t *set, int signo )
    作用:将signo信号加入信号集set

  2. int sigdelset ( sigset_t *set, int signo )
    作用:将信号signo从信号集set删除

  3. int sigemptyset ( sigset_t *set )
    作用:将信号集set清空

  4. int sigfillset ( sigset_t *set )
    作用:将所有的信号加入信号集set中

  5. 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 )

参数解析:

  1. set:被操作的信号集;如果为NULL,则说明不需要任何修改。
  2. oset:如果不为NULL,sigprocmask将会把修改之前的信号集放在*oset中返回
  3. 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 )

参数讲解

  1. sig:待绑定的信号码
  2. act:要绑定的动作,是一个指向struct sigaction结构的指针
    如果act为NULL,则不改变与sig相绑定的动作(而不是清空sig绑定的动作)
  3. 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语句的用法做类比(注意:只是表面的类比,内部实现有很大不同):

  1. sigsetjmp函数类似于goto语句的标签;
  2. 而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的返回值:

  1. 若是被程序直接调用,则返回0;
  2. 若是通过信号被siglongjmp跳转到则返回siglongjmp的参数val。

看一个例子来理解:



该篇笔记中PDF版书籍截图图片均来自于《UNIX系统编程》一书

以上是关于Unix系统编程 信号部分学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

Unix系统编程()信号处理器简介

Linux Unix shell 编程指南学习笔记(第四部分)

进一步学习的书籍

《Linux/Unix系统编程》第七八章学习笔记

ucos实时操作系统学习笔记——任务间通信(信号量)

unix环境高级编程学习笔记