[ APUE ] 第十章 信号


2. 信号概念

linux 可支持31种信号。不存在编号为0的信号。

不存在编号为0的信号,kill函数对信号编号0有特殊应用,这个信号被称为空信号。

产生信号的条件:

  1. Ctrl+C 产生中断信号(SIGINT)。可以停止程序运行。
  2. 硬件异常产生信号:除数为0,无效的内存引用等。
  3. kill函数可以将任意信号发送给另一个进程或进程组。但有两个限制:接收进程和发送进程的所有者必须相同,或者发送进程的所有者是超级用户。
  4. 检测到某种软件条件发生,并应将其通知有关进程时也产生信号。

某个信号出现时,可以告诉内核按照以下3种方式之一进行处理。

  1. 忽略。 大多数信号都可以忽略,但是SIGKILL和SIGSTOP信号不可忽略。这两种信号不可忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。如果忽略了某些由硬件产生的异常信号,则进程产生的行为使未定义的。

  2. 捕捉。表示 通知内核 在某种信号发生时,调用一个用户函数。

    如键盘产生SIGINT信号,同时希望接收到中断信号时进程退出,则调用用户函数终止当前进程。

    如捕捉到SIGCHLD信号,则表示子进程已终止,调用waitpid以取得进程终止状态。

    如进程创建了临时文件,则可能要为SIGTERM编写一个信号捕捉函数来清除临时文件。(SIGTERM是kill命令的系统默认信号)。

    注意不能捕捉SIGKILL和SIGSTOP。

  3. 执行系统默认动作。

    对于大多数信号,系统的默认动作是终止该进程。

    终止+core :进程在当前工作目录的core文件中复制了该进程的内存映像,大多数UNIX系统调试程序都使用core文件检查进程终止时的状态。

3. signal函数

#include 

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
						返回值:成功返回以前的信号处理函数指针,出错则返回SIG_ERR

handler的值是常量SIG_IGN、SIG_DFL或者信号处理函数。

SIG_IGN表示忽略该信号。SIG_DFL表示接到此信号后的动作是默认动作。

  1. 程序启动

    当exec执行程序时,进程原先设置的信号捕捉函数失效,所有的信号都被设置为默认动作。(因为一个进程原先要捕捉的信号,当其执行exec函数替换进程映像时,原来的信号处理函数地址很可能就被覆盖,无效了)。

  2. 进程创建

    当进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

5.中断的系统调用

6. 可重入函数

进程正常执行的指令序列被信号处理程序临时中断,信号处理程序不能判断进程原来的指令执行至何处。这就会带来一系列问题。

譬如,当malloc执行时中断;当执行修改静态存储单元的函数时中断;

当malloc维护堆内存块链表时,中断可能会破坏刚刚分配的内存。

存入静态存储单元的信息可能在信号处理函数中被覆盖(破坏)。

SUS说明了可以在信号处理程序中保证调用安全的函数。这些函数是可重入的,并被称为是异步信号安全的。这些函数除了可重入,它还会阻止任何引起不一致的信号发送。

(注:可重入函数,即可以被中断的函数,任何时刻都可被中断,返回控制时不会引起什么错误。)

没有列入表中的函数大多数不可重入,它们有以下特征:

  1. 使用了静态数据结构(如果注意了关中断和互斥手段其实也没啥)
  2. 调用malloc或者free
  3. 它们是标准IO函数(标准IO库的很多实现都是通过不可重入的方式使用全局数据结构

即便是对于上表中的函数,每个进程只有一个errno变量,所以信号处理函数可能会修改原来的值。譬如main函数中设置了errno,之后调用上表中函数,之后发生中断信号,信号处理函数中调用了read等函数,它可能更改errno。

因此,我们在调用上表中函数之前要先保存errno,之后恢复errno

(尤其是 SIGCHLD这种常被捕捉的信号,信号处理程序中通常要调用wait,而wait函数通常会修改errno)。

注意longjmp 和 siglongjmp 也是不可重入的。如果在更新数据结构时产生信号,且信号处理程序中调用siglongjmp,那么这次更新可能就半途而废。所以在更新全局数据结构,同时某些信号的信号处理程序可能会执行siglongjmp的时候,要阻塞这些信号。

7.SIGCLD语义

linux上等同于SIGCLD

注意SIGCLD的信号处理函数sig_cld如果先设置signal(SIGCLD,sig_cld); 后wait,则会无限递归调用sig_cld(在某些系统上,linux似乎解决了这个问题)

8. 可靠信号术语和语义

信号的产生和递送之间的时间间隔内,我们称信号是未决的。

进程产生了一个阻塞信号,且对该信号的动作为系统默认动作或者捕捉,则该进程将此信号保持为未决状态,直到该进程对这个信号解除了阻塞,或者对此信号的动作更改为忽略。

* 信号递达(Delivery):实际执行信号的处理动作
* 信号未决(Pending):信号从产生到递达之间的状态
* 信号阻塞(Block):进程可以选择阻塞某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

*** 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作**

内核在递送一个被阻塞的信号给进程时,才决定他的处理方式(而非在产生该信号时)。于是信号在这期间仍然可以改变对这个信号的动作。进程调用 sigpending 函数来判断哪些信号设置为阻塞并处于未决状态。

特殊情况:进程解除对某个信号的阻塞之前,这个信号发生了多次。

Linux下的解决方式:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

9. kill 和 raise函数

kill函数将信号发送给进程或进程组,raise函数允许进程向自身发送信号。

kill返回前该信号就被传送给该进程

#include 
#include 

int kill(pid_t pid, int sig);
   												
#include 

int raise(int sig);
   												成功返回0,失败返回-1

调用 raise (signo), 等价于调用kill(getpid(), signo)

kill 的 pid参数有以下4种不同的情况

  • pid > 0, 将信号发送给进程ID为PID的进程
  • pid == 0, 将信号发送给与发送进程同属同一进程组的所有进程(不包括系统进程集)
  • pid < 0, 将该信号发送给其进程组ID等于pid绝对值的所有进程,而且发送进程具有向这些进程发送信号的权限??(显然不包括系统进程集的进程)?
  • pid == -1, 将该信号发送给发送进程有权限向它们发送信号的所有进程。所有进程不包括系统进程集的进程。

进程将信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。

对于非超级用户,发送进程的实际用户ID或有效用户ID必须等于接受者的实际用户ID或有效用户ID。

10. alarm、pause函数

#include 
unsigned int alarm(unsigned int seconds);
									返回值:0或以前设置的闹钟时间的余留秒数

参数 seconds 的值是产生信号SIGALRM需要经过的时钟秒数, SIGALRM默认动作是终止进程。

若上一个闹钟尚未结束,则该闹钟时间余留值作为本次alarm函数返回的值。以前注册的闹钟时间将被当前闹钟的值代替。

#include 

int pause(void);
											   返回值:-1,errno设置为EINTR

pause将进程挂起直到捕捉到一个信号。

只有执行了一个信号处理程序并从其返回时,pause才返回-1,并设置errno为EINTR。

11.信号集

我们需要一个能表示多个信号的数据类型——信号集。

整型32位 无法用各位表示所有信号。POSIX.1定义了数据类型sigset_t ,用于包含一个信号集。

 #include 

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);
											返回值:成功返回0,失败返回-1
int sigismember(const sigset_t *set, int signum);
											返回值:真,返回1;假,返回0

sigemptyset 函数初始化set指向的信号集,清除其中的所有信号。

sigfillset函数初始化set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对信号集调用sigemptyset或sigfillset函数一次。这是因为C编译程序将不赋初值的外部变量和静态变量都初始化为0,而这是否与给定系统上的信号集的实现相符合,却无法确定。

信号集初始化之后,就可以在该信号集中增删特定信号。sigaddset函数将一个信号添加到已有的信号集中,sigdelset函数从信号集中删除一个信号。

所有以信号集作为参数的函数,总是以指向信号集的指针作为参数。

12.函数 sigprocmask

每个进程都有一个信号屏蔽字,它规定了当前阻塞而不能递送给当前进程的信号集。

sigprocmask函数可以检测或者更改,或同时进行检测和更改进程的信号屏蔽字。

#include 

/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
											返回值:成功返回0,出错返回-1

oldset返回进程当前的信号屏蔽字。

set是用于指示修改的信号屏蔽字。

how用于指示如何修改进程的信号屏蔽字。

how 说明
SIG_BLOCK 当前信号屏蔽字与set做“并”操作(即添加阻塞信号)
SIG_UNBLOCK 当前信号屏蔽字与set的补集做“交”操作(即减少阻塞信号)
SIG_SETMASK 直接将当前信号屏蔽字设置为set

set为空,how无意义。

在调用sigprocmask后如果有任何未决的,不再阻塞的信号,则在sigprocmask返回之前,至少将其中之一递送给该进程。

(sigprocmask函数仅在单线程进程环境下使用,处理多线程进程中的信号屏蔽使用另一个函数。)

13. sigpending函数

sigpending函数返回一个未决信号的信号集。(i.e: 阻塞信号被raise)

#include 

int sigpending(sigset_t *set);

恢复信号阻塞前的状态最好不要简单地使用(sigprocmask函数)SIG_UNBLOCK选项,因为调用该函数的函数可能在之前也阻塞了该信号,所以还是老老实实把 旧mask存下来再恢复。

可以在信号处理函数中更改信号处理方式。

解除信号阻塞后,多个相同的(常规)信号只传一个,没有排队。

14.sigaction函数

sigaction函数是用于 检查或修改或既检查又修改 与指定信号相关联的处理动作。取代了UNIX早期版本的signal函数。

#include 

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);
												成功返回0,失败返回-1

signum 是要检测或者修改其具体动作的信号编号。若act指针非空,则要修改其动作。若oldact指针非空,则oldact指针返回该信号的上一个动作。

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

sa_handler 字段为信号捕捉函数地址时,(而非SIG_DFL或SIG_IGN)sa_mask代表屏蔽字.

  • 该屏蔽字会在调用信号捕捉函数之前被加入到该进程的信号屏蔽字中,仅当从信号捕捉函数返 回时再将信号屏蔽字恢复为原先的值。
  • 信号处理程序被调用时,系统建立的新的信号屏蔽字包含了正在被递送的信号,保证了处理一个信号时,信号到达多次,只会被登记一次。

sa_flags 指定对信号进行处理的各个选项。

sa_sigaction字段是一个替代的信号处理程序,在sa_flag指定了SA_SIGINFO标志时,使用该种信号处理程序。对于sa_handler和sa_sigaction字段,实现可能使用同一存储区,所以只能使用其中一种。

siginfo_t 结构包含了信号产生原因的有关信息。

15. sigsetjmp函数 和 siglongjmp函数

linux3.2.0 下 setjmp和longjmp函数不执行保存和恢复屏蔽字操作。如果在进入信号捕捉函数时longjmp跳出,则当前进程的信号屏蔽字没恢复。(实验证明确实是这样!)

所以有了sigsetjmp和siglongjmp函数。

#include 

int sigsetjmp(sigjmp_buf env, int savesigs);
						返回值:直接调用返回0,从siglongjmp调用返回,返回非0.
void siglongjmp(sigjmp_buf env, int val);

savesigs非0,则env中会保存当前信号屏蔽字,之后siglongjmp返回时会恢复。

16.sigsuspend函数

解除阻塞,等待阻塞信号的错误方法:

sigprocmask(SIG_SETMASK, &oldmask, NULL);
pause();

sigpromask和pause之间发生信号,pause时无信号,导致pause永久阻塞。

需要原子操作!

#include 
int sigsuspend(const sigset_t *mask);
										返回值:-1,并将errno设为EINTR

信号屏蔽字设置为mask指向的值。在捕捉到信号或者发生终止该进程信号之前,该进程挂起。如果捕捉一个信号并且从该信号的信号捕捉函数返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。

如果希望等待信号时进程休眠,则sigsuspend很合适。但若希望等待信号时调用其他函数,则单线程下没有好的解决办法。多线程环境下可以专门安排一个线程处理信号。

17. abort函数

#include 
void abort(void);	

将SIGABRT信号发送给调用进程(进程不应忽略该信号)。调用abort将向主机环境递送一个未成功终止的通知,其方法是调用 raise(SIGABRT) 函数。

19. sleep函数

#include 
unsigned int sleep(unsigned int seconds);
												返回0或未休眠完的秒数
#include 
int nanosleep(const struct timespec *req, struct timespec *rem);
									休眠到要求的时间,返回0;出错返回-1

此函数使得调用进程被挂起直到满足:

  1. 已经过了seconds所指定的墙上时钟时间。

  2. 调用进程捕捉到一个信号并从信号处理程序返回。

相关