信号

一.概述

       信号是一种软件中断,它提供了一种处理异步事件的方法。LInux中定有了31种不同的信号,且支持应用程序定义信号。

1.产生信号的几种情况 

       有很多条件可以产生信号,以下便是常见的几种。

  • 当用户按下某些中断按键时,会引发终端产生信号。比如在终端上使用ctrl + c 通常会产生中断信号(SIGINT),这可以停止一个程序
  • 硬件异常产生信号。比如:除数为0(SIGFPE),无效的内存引用(SIGSEGV,如:访问了一个未初始化的指针)等。上述两种信号的默认处理都是终止+吐核。这些异常通常由硬件检测到,并通知内核。
  • 进程调用kill(2)函数发送任意信号至另一进程,但接收信号的进程与发送信号的进程的所有者必须相同,或者发送进程的所有者是超级用户,即超级用户可以将信号发送给任一进程,而非超级用户的规则是发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID。但若定义了_POSIX_SAVED_IDS则不比较有效用户ID,而比较保存的设置用户ID
  • 当检测到某些软件条件已经发生时,且需要通知相关进程,此时也会产生信号。比如:SIGURG(带外数据,参博文《TCP/IP实现(九) 插口I/O》),SIGPIPE(某个通道以终止,但仍有进程写该通道,如写一个已关闭写的socket)以及SIGALRM。

2.信号的处理方式

  1. 忽略该信号。但SIGKILL和SIGSTOP信号既不可被忽略,也不可被捕捉,因为它们向内核和超级用户提供了可靠的终止进程或停止进程的方法。若忽略了某些硬件产生的信号,那么进程的行为也将是未定义的,如:除数为0(SIGFPE),无效的内存引用(SIGSEGV)等
  2. 捕捉信号。SIGKILL和SIGSTOP信号不可被捕捉
  3. 执行系统默认动作。大多数系统默认信号的动作是终止该进程。

3.信号说明  

     关于各个信号作用的详细说明请参照APUE p251 ~ p256。

4.编号为0的信号

       编号为0的信号被定义为空信号,常用于作为kill的参数,kill仍将进行错误检测但不真正发送信号,若目标进程存在则返回0,否则返回-1并将errno设置为ESRCH,因此常常用于检查进程是否依然存在

【注】:用编号为0的信号测试进程是否存在的操作并不是原子操作,即当kill向调用者返回测试结果时,原来已存在的被测试进程可能已经终止,这个有些像TCP中的保活,但并不能保证实时性。

二.信号处理

1.signal函数

       该函数的定义形式比较特别,它返回的是是一个函数指针,指向旧的信号处理函数,关于返回函数指针的写法可以参考博文《STL 空间分配器(二)》,其中设置内存申请失败的回调函数的函数也是这种写法。signal的第二个参数是用于处理信号的函数,其值可以是一个函数地址,也可以是SIG_IGN(忽略该信号),也可以是SIG_DFL(执行系统默认处理),SIG_IGN与SIG_DFL的定义如下:

typedef	void (*__p_sig_fn_t)(int);
#define SIG_DFL (__p_sig_fn_t)0
#define SIG_IGN (__p_sig_fn_t)1
#define SIG_ERR (__p_sig_fn_t)-1

若signal返回SIG_ERR则说明出现错误。

2.exec与fork时信号处理的继承问题

       exec函数将原先设置为要捕获的信号都更改为默认动作,其它信号的状态则不变,这是因为信号捕获函数的地址很可能在所执行的新程序文件种已无意义了(因为exec是在进程的地址空间中装入新的数据)。而当调用fork时,其子进程继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

3.自动重启被中断的系统调用

       当我们调用某个系统API时,内核会转而进行系统调用。当在执行一个慢系统调用期间捕获到一个信号,则会造成系统调用中断。此时会发生什么呢?对于signal函数会自动重启系统调用,会自动重启的几种函数一般为:ioctl,read,readv,write,writev,wait和waitpid(sendmsg,readmsg应该也会吧,因为在socket函数上调用时最后都会调用sosend函数,参TCP/IP卷2 p385)。关于connect函数被中断可以参考博文《TCP/IP实现(八) 插口层》。sigaction可以通过是否设置SA_RESTART来进行选择是否自动重启。

【注】:磁盘I/O一般不同于网络I/O,不会长时间阻塞

4.信号处理函数中应该只调用可异步信号安全的函数

        可重入函数的实现方式分为两种:1.在函数中只能修改局部变量,而不能修改全局数据结构与变量。2.使用锁,保证一次只有一个进程(线程)在执行该函数,但这种情况在信号处理函数中可能会造成死锁,是否可以用try_lock解决或是加锁前屏蔽信号来进行解决,就向内核中屏蔽中断一样。可重入函数不一定是异步信号安全的函数

       在信号处理函数中应该只调用异步信号安全的函数,这是由于中断的处理方式导致的,一个进程可能正在执行一个正常的指令序列就被信号处理程序临时中断,比如一个进程正在调用malloc函数进程内存分配(malloc执行到一半,即对其中维护的链表修改到了一半,这部分可参考《STL 空间分配器(二)》),但是此时由于捕获信号而开始执行信号处理函数,而在信号处理函数中再次调用了malloc函数,此时便会出现不可预料的错误。

      因此为了保证信号处理函数执行的安全性,应该只调用异步信号安全的函数。Linux内核是可重入的,标准I/O一般都是不可重入的,fwrite不是异步信号安全的,也不是可重入的,其内部维护了一个缓冲。

      特别要注意有的函数间接调用了free,特别是那些在内部读取系统文件的函数,比如struct passwd* getpwname(const char *),因为在其中读取/etc/passwd口令文件(参博文《系统数据文件和系统信息》)时一般使用了标准I/O流,而标准I/O流中的缓冲使用了malloc与free函数,这两个函数是不可重入的。

【注】:关于哪些函数可重入,哪些不可重入可参考APUE p262。

5.信号处理函数与errno

      每个线程只有一个errno变量,而信号处理函数可能会修改其值,因此为了防止调用errno之前的errno值丢失,应该在调用前先保存er'r'no的值,并在调用后恢复errno的值。

三.信号的递交与阻塞

1.信号的产生到递交

       当造成信号的事件发生时,会为进程产生一个信号,之后内核更新目标进程的数据结构标识信号已递送。而在信号产生和递送之间的时间间隔内,称信号是未决的。(在讨论Linux内核时还会在相关博文中进行更深入的探讨)

2.阻塞信号递送

       如果为进程产生了一个信号,但进程已阻塞了该信号(由信号屏蔽字指出,每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号屏蔽字,用函数sigprocmask设置),而且进程对信号的处理是系统默认或者捕获,则暂不递交给进程(即不改变进程数据结构中的某标志位),内核为进程将此信号保持未决状态,直到进程解除了对该信号的阻塞(即从信号屏蔽字中移除)或将该信号的处理方式改为忽略(那么将不递交)。

      sigaction函数可以在第二个参数struct sigaction{}中的sa_mask指定一个信号集,在调用相应信号的处理函数之前先将该屏蔽字加入到进程的信号屏蔽字当中,仅当从信号捕获函数返回时再将进程的信号屏蔽字恢复为原先值。

     此外在信号处理函数被调用时,操作系统建立的新信号屏蔽字包括正被递送的信号(自动加入),这保证了在处理一个给定信号时,如果该信号再次发生,那么它会被阻塞,直到前一个信号的处理结束为止。(【问:】当一个进程执行一个信号处理函数时,通常会屏蔽该信号,可是整个进程屏蔽该信号还是仅仅这个线程屏蔽该信号,待测试

3.产生信号的函数

1)kill与raise

       kill函数将信号发送给进程或进程组,而raise函数则用于向进程自身发送信号,当然kill也可以发给自己。

       kill函数根据参数pid的值不同而分为4种不同情况:   

pid > 0将该信号发送给进程ID为pid的进程,但需要有发送的权限
pid = 0

将信号发送给与发送进程(本进程)属于同一进程组的且有发

送权限的所有进程

pid < 0将信号发送给其进程组ID等于pid绝对值且具有发送权限的所有进程
pid = -1发送给发送进程有权发送的所有进程

       如过kill是为调用进程发信号,而且信号是不被阻塞的,则kill在signo(即本次调用kill发送的信号)或某个其它未决的信号被传送至该进程后返回。

2)alarm与pause

        alarm可以设置一个定时器,在定时器超时后,产生SIGALRM信号,如果不捕获此信号,则其默认行为是终止该进程,每个进程只能用alarm设置一个闹钟时间,如果多次调用alarm则将闹钟超时时间替换为最新值,并返回旧值。

        pause函数使调用进程挂起直至捕捉到一个信号,并执行完信号处理程序后才返回。

3)信号屏蔽字

       前面提到每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号,用函数sigprocmask设置,信号屏蔽字是以信号集sigset_t的方式来存储的,在设置信号集之前,应先用相应函数将信号集初始化,因为C编译器将不赋初值的外部变量与静态变量初始化为0,但这不一定与给定系统上的信号集的实现相一致。 

4)sigsuspend原子的设置屏蔽字与等待信号

       若我们先调用sigprocmask函数解除了对某个信号的屏蔽,之后调用pause函数等待信号递交,那么在这两者之间可能信号就会被递交,那么之后在调用pause将永远阻塞。因此需要一个原子的设置屏蔽字并等待的函数,即sigsuspend函数。可以利用该函数与kill函数实现父子进程之间的同步。

5)about函数的特点

       调用该函数会将SIGABRT发送至调用进程,标准要求若进程捕获了此信号,那么即使从相应的信号处理函数返回,仍不从about函数返回到调用进程,且about不理会进程对此信号的阻塞和忽略。

       捕获SIGABRT信号的意图为:在进程终止之前,由其执行所需的清理操作。,若进程不在信号处理函数结束进程,则在信号处理程序返回后由about终止进程

6)sleep与alarm之间的不确定关系

       sleep可以使用alarm实现,但这又不是唯一办法,而标准中又未给出明确的实现方法,因此有的实现中sleep可能与alarm相互影响。

在linux中使用函数nanosleep(提供了纳秒级的精度,具体依靠系统支持)实现sleep,因此不会与alarm相互影响

7)设置信号排队

       使用排队信号必须做以下几步操作:

  1. 使用sigaction函数安装信号时指定SA_SIGINFO标志,当指定了该标志后,会额外提供附加信息到用户的信号处理程序:一个指向siginfo结构(包含了信号产生的原因及有关信息,比如发送信号的进程ID等等)的指针,以及一个指向进程上下文标识符的指针(进程描述符??)。
  2. 在sigaction结构中(用做sigaction函数的参数)的sa_sigaction成员中提供信号处理程序(而不是sa_handler,实现可能允许用户使用sa_hander字段来设置,但是不能获取sigque发出的额外信息)。
  3. 使用sigqueue发送信号。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值