信号都以SIG开头,信号名都被定义为正整数常量,不存在编号为0的信号,kill函数对信号编号0有特殊的应用,这种信号称为空信号。很多条件可以产生信号:
- 当用户按某些终端键,引发终端产生的信号
- 硬件产生的信号:除数为0,无效内存引用
- 进程调用kill函数可将任意信号发送给另一个进程或进程组
- 用户可调用kill命令将信号发送给其他进程
- 当检测到某种软件条件已经发生,并将其通知有关的进程时也产生信号
在某个信号出现时,可以告诉内核按照下列三种方式之一处理
- 忽略,大多数信号可以使用这种方式进行处理,但时SIGKILL和SIGSTOP不能被忽略,因为他们向内核和超级用户提供了使进程终止的可靠方法。
- 捕捉,做到这一点的前提是要通知内核在某种信号发生时,调用一个用户函数,在用户函数中对这种事件进行处理
- 执行系统默认动作,大多数信号的系统默认动作是终止进程
在系统默认动作中,有的默认动作会在进程当前工作目录中生成core文件(中间复制了该进程的内存映像)下面几种条件不产生core文件:
- 进程是设置用户id的,而且当前用户并非程序文件的所有者
- 进程是设置组id的
- 用户没有写当前工作目录的权限的
- 文件已经存在,而且用户对该文件有写权限
- 文件太大
信号名 | 说明 |
---|---|
SIGABRT | 调用abort函数时产生此信号 |
SIGALRM | 用alarm函数设置定时器超时时产生此信号,setitimer函数也产生此信号 |
SIGBUG | 硬件故障产生 |
SIGCANCEL | 线程库内部使用的信号 |
SIGCHLD | 当一个进程终止或停止,SIGCHLD信号被送给父进程。父进程可以使用wait取得子进程id和终止状态,系统默认忽略此信号 |
SIGFPE | 算术异常(除以0,浮点溢出) |
SIGHUP | 如果终端检测到一个连接断开,则将此信号送给与该终端相关的控制进程 |
SIGILL | 非法硬件指令 |
SIGINFO | 键盘状态建(Ctrl+T)请求 |
SIGINT | 终端中断符(DELETE/Ctrl+C) |
SIGIO | 异步io事件,SIGPOLL |
SIGIOT | 实现定义的硬件故障 |
SIGKILL | 这是两个不能被捕捉或忽略信号中的一个,直接杀死进程 |
SIGPIPE | 如果管道的读进程已终止写管道,产生该信号 |
SIGQUIT | 退出键时触发(Ctrl+) |
SIGSEGV | 进程进行了一次无效的内存引用 |
SIGTERM | 这是kill命令发送的的系统默认终止信号。该信号由应用程序捕获,可以让程序有机会在退出前做好清理工作。 |
SIGUSR1(SIGUSR2) | 用户定义的信号,可用于应用程序 |
信号函数
1 |
|
2 | void(*signal(int signo,void (*func)(int)))(int) ; |
3 | //成功返回以前的信号处理配置,出错返回SIG_ERR |
4 |
|
5 |
|
6 |
|
该函数的signal参数是信号名,func值是常量SIG_IGN,常量SIG_DFL或当接收到此信号后要调用的函数的地址。
- 当指定SIG_IGN,则向内核表示忽略此信号
- 当指定SIG_DFL,则表示接收到此信号后的动作是系统默认动作
- 当指定函数地址时,则信号发生时,调用该函数。我们称这种函数为信号处理程序或信号捕捉函数
1 |
|
2 | |
3 | static void sig_usr(int); /* one handler for both signals */ |
4 | |
5 | int main(void) |
6 | { |
7 | if (signal(SIGUSR1, sig_usr) == SIG_ERR) |
8 | err_sys("can't catch SIGUSR1"); |
9 | if (signal(SIGUSR2, sig_usr) == SIG_ERR) |
10 | err_sys("can't catch SIGUSR2"); |
11 | for ( ; ; ) |
12 | pause(); |
13 | } |
14 | |
15 | static void sig_usr(int signo) /* argument is signal number */ |
16 | { |
17 | if (signo == SIGUSR1) |
18 | printf("received SIGUSR1\n"); |
19 | else if (signo == SIGUSR2) |
20 | printf("received SIGUSR2\n"); |
21 | else |
22 | err_dump("received signal %d\n", signo); |
23 | } |
24 | |
25 | output: |
26 | $ ./a.out & start process in background |
27 | [1] 7216 job-control shell prints job number and process ID |
28 | $ kill -USR1 7216 send it SIGUSR1 |
29 | received SIGUSR1 |
30 | $ kill -USR2 7216 send it SIGUSR2 |
31 | received SIGUSR2 |
32 | $ kill 7216 now send it SIGTERM |
33 | [1]+ Terminated ./a.out |
程序启动时:
- 当执行一个程序时,所有信号状态都是系统默认或忽略。
- shell自动将后台进程对终端和退出信号的处理方式设置为忽略
进程创建时:
- 当一个进程调用fork时,其子进程继承父进程的信号处理方式
不可靠信号
信号不可靠指的是信号可能会丢失:一个信号发生了,但进程可能不知道这一点。同时,进程对信号的控制能力也很差,他能捕获信号或或忽略它。有时用户希望通知内核阻塞某个信号:不要忽略该信号,在其发生时记住它,然后在进程做好了准备时再通知它。这种阻塞信号的能力当时并不具备。
中断的系统调用
早期unix系统的特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不在继续执行。
当捕捉到某个信号时,被中断的是内核中执行的系统调用,而非函数。
为了支持该特性,系统调用分为两类:低速系统调用和其它系统调用,低速系统调用是可能会使进程永远阻塞的的一类系统调用。包括:
- 某些类型文件不存在(读管道,终端和网络设备),则读操作坑会永远阻塞
- 如果这些数据不能被相同类型文件立即接受,则可操作可能会使调用者永远阻塞
- 在某种条件发生之前被相同类型文件立即接受,则可能会发生阻塞(例如打开一个终端设备,需要先等待与之连接的调制解调器应答)
- pause函数(它使调用进程休眠直至捕捉到一个信号)和wait函数
- 某些ioctl操作
- 某些进程间通信函数
- 其他系统调用
与被中断的系统调用相关的问题是必须显示的处理出错返回。某些操作系统为了使程序不必处理被中断的系统调用,引入了自动重启动的系统调用。
可重入函数
进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果在信号处理的过程中,由于出现了新的信号,则会导致信息的丢失或被覆盖,因而引入了可重入的函数。
可重入是一种异步信号安全的(async-signal safe)函数。在信号处理操作期间,它会阻塞任何会引起信号不一致的信号发送。
SIGCLD语义
SIGCHLD信号产生于子进程状态改变后,父进程需要调用一个wait函数以检测发生了什么。
SIGCLD处理方式:
- 如果进程明确地将该信号的配置设置为SIG_IGN,则调用进程的子进程将不产生僵尸进程。子进程再终止时,将其状态丢弃。如果调用进程随后调用一个wait函数,那么它将阻塞直到所有子进程都终止,然后wait会返回-1,并将其errno设置为ECHILD。(此信号的默认配置是忽略,与SIG_IGN不同)
- 如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序。
可靠信号术语和语义
当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。事件可以是硬件异常(除0),软件条件(如alarm定时器超时),终端产生的信号或调用kill函数。
当一个信号产生的时候,内核通常在进程表中以某种形式设置一个标志,当对信号采取了这种的动作的时候,我们说向进程递送了一个信号。
在信号产生(generation)和递送(delivery)之间的时间间隔,信号是未决的。
进程可以选用“阻塞信号递送”,如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程为对此信号解除了阻塞,或者将该信号的动作改为忽略。
内核在递送一个原来被阻塞的信号给进程时(而不是在产生该信号时)才决定对它的处理方式。于是进程在信号递送给它之前仍可改变对该信号的动作。进程调用sigpending
函数来判断哪些信号是设置为阻塞并处于未决状态的。
如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,POSIX.1允许系统递送该该信号一次或多次。如果递送该信号多次,则称这些信号进行了排队。除非支持POSIX.1实时扩展,否则大多数UNIX并不对信号排队,而是只递送这种信号一次。
每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用sigprocmask
函数来检测和更改当前信号屏蔽字。
1 |
|
2 | int kill(pid_t pid, int signo); |
3 | int raise(int signo); |
4 | //成功返回0,出错返回-1 |
kill
函数将信号发送给进程或进程组。raise
函数则允许进程向自身发送信号。raise(signo); == kill(getpid(), signo);
kill
的pid参数有以下4种不同的情况:
pid > 0
将该信号发送给进程ID为pid的进程。pid == 0
将该信号发送给与发送进程属于同一进程组的所有进程,而且发送进程具有权限向这些进程发送信号。pid < 0
将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。pid == -1
将该信号发送给进程有权限向他们发送信号的所有进程。(不包括系统进程集种的进程)
进程将信号发送给其它进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户:发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID。
使用alarm
函数可以设置一个定时器,在将来某个时刻该定时器会超时。当定时器超时时,产生SIGALRM
信号。如果忽略或不捕捉此信号,则其默认动作时终止调用该alarm
函数的进程。
1 |
|
2 | unsigned int alarm(unsigned int seconds); |
3 | //返回值为0或以前设置的闹钟时间的余留秒数 |
4 | int pause(void); |
5 | //返回值-1,errno设置为EINTR |
每个进程只能有一个闹钟时间。
pause
函数使调用进程挂起直至捕捉一个信号。
信号集
1 |
|
2 | int sigemptyset(sigset_t *set); |
3 | int sigfillset(sigset_t *set); |
4 | int sigaddset(sigset_t *set, int signo); //信号集中添加一个信号 |
5 | int sigdelset(sigset_t *set, int signo); //信号集中删除一个信号 |
6 | //以上4个函数成功返回0出错返回-1 |
7 | int sigismember(const sigset_t *set, int signo); |
8 | //若真返回1,若假返回0 |
函数sigemptyset
初始化由set指向的信号集,清除其中所有的信号。函数sigfillset
初始化由set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用sigemptyset
或sigfillset
一次。
1 |
|
2 | //检测或更改信号屏蔽字,成功返回0,出错返回-1 |
3 | int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); |
- 若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。
- 若set是非空指针,则参数how指示如何修改当前信号屏蔽字。
how | 说明 |
---|---|
SIG_BLOCK | 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含期望阻塞的附加信号。 |
SIG_UNBLOCK | 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集补集的交集。set包含了希望解除阻塞的信号。 |
SIG_SETMASK | 该进程新的信号屏蔽字是set指向的值。 |
sigpending
函数返回一信号集,对于调用进程而言,其中的信号是阻塞不能递送的,因而也一定是当前未决的。
1 |
|
2 | //成功返回0,出错返回-1 |
3 | int sigpending(sigset_t *set); |
sigaction
函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。此函数取代unix早期版本使用的signal函数。参数signo是要检测或修改其具体动作的信号编号。若act指针非空,则要修改其动作。若oact指针非空,则系统经由oact指针返回该信号的上一个动作。
1 |
|
2 | |
3 | struct sigaction { |
4 | void (*sa_handler)(int); /* addr of signal handler, */ |
5 | /* or SIG_IGN, or SIG_DFL */ |
6 | sigset_t sa_mask; /* additional signals to block */ |
7 | int sa_flags; /* signal options, Figure 10.16 */ |
8 | /* alternate handler */ |
9 | void (*sa_sigaction)(int, siginfo_t *, void *); |
10 | }; |
11 | |
12 | int sigaction(int signo, const struct sigaction *restrict act, |
13 | struct sigaction *restrict oact); |
sigsuspend
函数原子的恢复信号屏蔽字,然后使进程休眠。
1 |
|
2 | int sigsuspend(const sigset_t *sigmask); |
abort
函数使程序异常终止。此函数将SIGABRT信号发送给调用进程(经常不应忽略此信号)。
1 |
|
2 | void abort(void); |
1 |
|
2 | unsigned int sleep(unsigned int seconds); |
3 | int nanosleep(const struct timespec *reqtp, struct timespec *remtp); |
4 | int clock_nanosleep(clockid_t clock_id, int flags, |
5 | const struct timespec *reqtp, struct timespec *remtp); |
sleep
函数使调用进程被挂起直到满足:
- 已经经过seconds所指定的墙上时钟时间。
- 调用进程捕捉到一个信号并从信号处理程序返回。当由于捕获到某个信号sleep提早返回时,返回值是未休眠的描述。
nanosleep
函数挂起调用进程,直到要求的时间已经超过或者某个信号中断了该函数。reqtp参数用秒和纳秒指定了需要休眠的时间长度。如果某个信号中断了休眠间隔,进程并没有终止,remtp参数指向的timespec结构就会被设置为未休眠的时间长度。
clock_nanosleep
函数用于特定时钟的延迟时间调用。