APUE第10章 信号

信号是异步中的重要概念,进程执行过程中可能会收到各种各样的信号。

信号概念

信号一般SIG作开头,头文件一般在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> If sig is 0, then no signal is sent, but error checking is still performed; 
this can be used to check for the existence of a process ID or process
group ID.
### 不能捕捉或者忽略的信号
SIGKILL、SIGSTOP这两个信号是不能被忽略或者捕捉的,因为是内核提供的可靠终止的方法。但是SIGTERM可以。SIGTERM可以让程序检测kill命令发出的信号,从而做好清理工作而退出

## signal函数
Unix系统信号机制最简单的接口
```cpp
#include <signal.h>
void (*signal(int signo, v函数(*func)(int)))(int);
//signal函数第一个参数是信号,第二个是回调函数,无返回值,参数为整数
//signal返回值是一个函数指针,就是之前的处理指针,一般会有这样的宏定义
#define SIG_ERR (void (*)())-1
#define SIG_DFL (void (*)())0
#define SIG_IGN (void (*)())1

早期的信号是不可靠的,可能会丢失。

  1. 早期的信号处理是有时间窗口的,这个窗口内遇到第二个信号就会造成执行默认工作
  2. 早期如果进程不希望某种信号发生时,不可以关闭该信号

系统中断

当捕捉到某个信号的时候,系统中断中断的是内核中执行的系统调用,而不是函数。

信号处理函数可重入

背景:在信号处理程序中,是无法判断捕捉到信号时进程执行到了哪里,所以要保证在处理程序中只能调用异步信号安全(可重入)的函数。

可重入函数见APUE的P262页
另外由于每个线程只有一个errno,所以就算调用异步信号安全的函数也有可能会改变errno,所以应当在调用前保存errno,调用后恢复errno

可靠信号的一些术语和语义

  1. 当一个信号产生的时候,内核通常再进程表中以某种形式设置一个标志。
  2. 信号再产生和递送过程中之间的间隔内,信号是未决的。
  3. 进程可以阻塞信号传送,如果产生了一个阻塞的信号,且对信号的动作是系统默认动作或捕捉信号,那么该进程讲此信号保持为未决状态,知道该进程解除此信号的阻塞,或设置为忽略。
  4. 信号在递送信号的时候才决定信号的处理方式,所以再递送前还是可以修改处理动作。
  5. 进程调用sigpending函数来判定那些信号设置为阻塞切是pending状态(未决状态)
  6. 如果进程解除阻塞之前,信号发生了多次,POSIX.1允许系统递送信号一次或者多次,但大多数UNIX并不对信号排队,而是只传递一次。
  7. 如果有多个信号要传递给进程,则传递顺序是不可知的。
  8. 进程可调用sigprocmas来检测和更改当前信号屏蔽字

kill和raise

  1. kill将信号发给进程或进程组,而raise允许发给自身
  2. 发送0信号给其他进程的话,可以用来确定一个特定的进程是否存在,实际不发送信号,如果不存在返回-1,errno为ESRCH。但是由于UNIX会重用进程ID,或当返回时该进程已结束,所以这种测试并没多大意义。

alarm和pause

1
2
3
4
5
6
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
int paused(void);
//example
alarm(seconds);//return 0
alarm(0);//turn off timer and return remain times
  1. 每个进程只能有一个闹钟时间,如果给了新的值,返回值会是上次剩余的时间
  2. SIGALARM的默认动作是终止进程。如果想捕捉alarm,应该先注册处理函数,再设置alarm,否则窗口时间可能让进程直接终止。
  3. pause使进程挂起知道捕捉到一个信号,并执行了信号处理程序且返回了,pause才返回。这种情况下pause返回-1,errno为EINTR。
  4. 使用alarm和pause以及他们的组合,可以实现sleep或其他需要超时的机制,但有很多需要考虑的问题
    1. alarm的处理与阻塞点的竞争条件,如果系统繁忙,在alarm返回后才执行到阻塞点,那么alarm机制就失效了
    2. 如果系统调用是自动重启的,如read这种操作并不会被中断
  5. 如果使用setjmp和longjmp来处理的话虽然可以达到预期效果,但是有可能会跟其他的信号交互出现问题

信号集及其处理函数

1
2
3
4
5
6
7
8
9
#include <signal.h>
//sigset_t 信号集结构
int sigemptyset(sigset_t *set);//清空信号集
int sigfillset(sigset_t *set);//初始化信号集
int sigaddset(sigset_t *set, int signo);//设置信号集
int sigdelset(sigset_t *set, int signo);//删除信号集
//上述四个函数返回值,若成功为为0,出错为-1
int sigismember(const sigset_t *set, int signo);
//为真1,为假0

使用信号集前要清空或者初始化,否则信号集的状态不可知

1
2
3
4
//宏实现,若为32位整数表示31个信号的情况下
#define sigemptyset(ptr) (*(ptr) = 0)
#define sigfillset(ptr) (*(ptr)) = ~(sigset_t)0, 0)
//上面第二个利用了逗号优先级最低,从而让0作为返回值返回

sigprocmask

用于规定阻塞的信号字

1
2
3
4
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
//return 0 if success, else -1
//how: SIG_BLOCK SIG_UNBLOCK SIG_SETMASK

如果set是空指针的话,则不改变进程的信号屏蔽字,how的值也就无意义,这时候单纯用来获取当前信号值
如果oset是非空指针的话,当前信号屏蔽字由oset返回

sigpending

用于返回当前pending的信号集

1
2
3
#include <signal.h>
int sigpending(sigset_t *set);
//return 0 if success, else -1

sigaction

取代UNIX的signal函数,用于检查或修改对应信号的处理动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <signal.h>
int sigaction(int signo, const struct sigaction *restrict act,struct sigaction *restrict oact);
struct sigaction{
void (*sa_handler)(int); //addr of signal handler, or SIG_IGN, SIG_DFL
sigset_t sa_mask; //additional singals to block
int sa_flags; //signal options: SA_INTERRUPT,SA_RESTART,SIGINFO... //《APUE》P350
void (*sa_sigaction)(int, siginfo_t *, void *);
};
struct siginfo{
int si_signo; //signal number
int si_errno; //if nonzero, errno value from <errno.h>
int si_code; //additional info (depends on signal)
pid_t si_pid; //sending process id
uid_t si_uid; //sending process real user id
void *si_addr; //address that caused the fault
int si_status; //exit value or singal number
union sigval si_value; //application-specific value
};
union sigval{
int sival_int;
void *sival_ptr;
};

sigsetjmp和siglongjmp

由于setjmp和longjmp并不保存和恢复信号屏蔽字(FreeBSD8.0和Mac OS X10.6.8中会保存和回复),所以有另外两个函数转门用来做这个事情

1
2
3
4
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
//如果直接调用返回0,从siglongjmp调用返回非0
void siglongjmp(sigjmp_buf env, int val);

当savemask不为0的时候,会保存信号屏蔽字

sigsuspend

原子操作的要求而诞生的,如果想要解除某个信号的阻塞然后pause等待信号发生,由于不是原子操作,有可能会在解除阻塞和pause之间发生,这样的话pause就会永远阻塞了。所以产生了这个sigsuspend的原子操作。

1
2
3
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
//只要收到信号后返回,返回值一定为-1,并将errno设置为EINTR

信号应用

一般用于:

  1. 获取退出信号,进行工作保存,日志记录等
  2. 父子间的进程间同步

sigqueue

1
2
#include <signal.h>
int siggueue(pid_t oid, int signo, const union sigval value);

只能发给单个进程,可以使用value参数向信号处理程序传递整数和指针值。

将信号从id转为str

1
2
3
4
5
external char *sys_siglist[];//下表就是信号,对应char*的信号名称
#include <signal.h>
void psignal(int signo, const char *msg);//类似perror,msg输出错误信息
void psiginfo(const siginfo_t *info, const char *msg);//跟上一个类似,不过提供更多信息
char *strsignal(int signo);//返回字符串