1

目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]

信号概念

从前面的文章和实际使用中,大家应该也对信号有一些模糊的认识了。比如,Nginx使用信号来管理进程的启动关闭,UNIX进程信号是经典的操作异步事件机制。在早期,Unix信号是每个实现都不同的,但是随着Unix标准化的进行,标准提出了统一的信号机制模型。除了共有的信号以外,各自的实现还提供了扩展信号,当然,这个不是重点。
在头文件<signal.h>中定义了所有的信号,我们可以通过编辑这个头文件来查看当前Unix实现提供的信号。

#define SIGHUP  1       /* hangup */
#define SIGINT  2       /* interrupt */
#define SIGQUIT 3       /* quit */
#define SIGILL  4       /* illegal instruction (not reset when caught) */
#define SIGTRAP 5       /* trace trap (not reset when caught) */
#define SIGABRT 6       /* abort() */
...

就是这样列出来的,从上面可以看到,信号不存在为0的编号,在最初的几章里,提到过产生信号的条件

  1. 终端按键产生信号,例如:Ctrl+C、Ctrl+\这种形式

  2. 硬件产生信号,比如除以0错误等,都是通过中断通知内核,然后产生信号

  3. 进程调用 kill 函数发送信号,在前面权限部分就提到过,想要发送信号,则所有者权限需要检查或者说有效用户组是root权限

  4. 用户使用 kill 命令,实际上就是调用了 kill 函数

  5. 系统产生的软中断

信号是进程交互和异步事件的方式,由于信号出现是不可知的,所以不能说像检查返回值,检查 errno 一样判断是否有信号,系统提供了一套完善的机制来实现,开发者只需要让进程注册函数来处理信号。
进程收到信号后,可以选择3种方式处理行为:

  1. 忽略信号

  2. 捕捉信号

  3. 执行系统默认行为

下面列出了通常的信号

No    Name         Default Action       Description
1     SIGHUP       terminate process    terminal line hangup
2     SIGINT       terminate process    interrupt program
3     SIGQUIT      create core image    quit program
4     SIGILL       create core image    illegal instruction
5     SIGTRAP      create core image    trace trap
6     SIGABRT      create core image    abort program (formerly SIGIOT)
7     SIGEMT       create core image    emulate instruction executed
8     SIGFPE       create core image    floating-point exception
9     SIGKILL      terminate process    kill program
10    SIGBUS       create core image    bus error
11    SIGSEGV      create core image    segmentation violation
12    SIGSYS       create core image    non-existent system call invoked
13    SIGPIPE      terminate process    write on a pipe with no reader
14    SIGALRM      terminate process    real-time timer expired
15    SIGTERM      terminate process    software termination signal
16    SIGURG       discard signal       urgent condition present on socket
17    SIGSTOP      stop process         stop (cannot be caught or ignored)
18    SIGTSTP      stop process         stop signal generated from keyboard
19    SIGCONT      discard signal       continue after stop
20    SIGCHLD      discard signal       child status has changed
21    SIGTTIN      stop process         background read attempted from control terminal
22    SIGTTOU      stop process         background write attempted to control terminal
23    SIGIO        discard signal       I/O is possible on a descriptor (see fcntl(2))
24    SIGXCPU      terminate process    cpu time limit exceeded (see setrlimit(2))
25    SIGXFSZ      terminate process    file size limit exceeded (see setrlimit(2))
26    SIGVTALRM    terminate process    virtual time alarm (see setitimer(2))
27    SIGPROF      terminate process    profiling timer alarm (see setitimer(2))
28    SIGWINCH     discard signal       Window size change
29    SIGINFO      discard signal       status request from keyboard
30    SIGUSR1      terminate process    User defined signal 1
31    SIGUSR2      terminate process    User defined signal 2

函数signal

void (*signal(int sig, void (*func)(int)))(int);

or in the equivalent but easier to read typedef'd version:

typedef void (*sig_t) (int);

sig_t signal(int sig, sig_t func);

Unix使用信号的接口就是signal函数。在前面的章节中提到过signal函数的定义和具体意义。signal函数实际上是ISO C定义的,但是由于这种跨平台函数具有实现的不同,所以windows还是Unix各个实现,实际上都有其特殊的方式。对于函数本身来说,带有两个参数,一个是signo信号名,func的值是一个常亮

1     SIGHUP       terminate process    terminal line hangup
2     SIGINT       terminate process    interrupt program
3     SIGQUIT      create core image    quit program
4     SIGILL       create core image    illegal instruction
5     SIGTRAP      create core image    trace trap
6     SIGABRT      create core image    abort program (formerly SIGIOT)
7     SIGEMT       create core image    emulate instruction executed
8     SIGFPE       create core image    floating-point exception
9     SIGKILL      terminate process    kill program
10    SIGBUS       create core image    bus error
11    SIGSEGV      create core image    segmentation violation
12    SIGSYS       create core image    non-existent system call invoked
13    SIGPIPE      terminate process    write on a pipe with no reader
14    SIGALRM      terminate process    real-time timer expired
15    SIGTERM      terminate process    software termination signal
16    SIGURG       discard signal       urgent condition present on socket
17    SIGSTOP      stop process         stop (cannot be caught or ignored)
18    SIGTSTP      stop process         stop signal generated from keyboard
19    SIGCONT      discard signal       continue after stop
20    SIGCHLD      discard signal       child status has changed
21    SIGTTIN      stop process         background read attempted from control terminal
22    SIGTTOU      stop process         background write attempted to control terminal
23    SIGIO        discard signal       I/O is possible on a descriptor (see fcntl(2))
24    SIGXCPU      terminate process    cpu time limit exceeded (see setrlimit(2))
25    SIGXFSZ      terminate process    file size limit exceeded (see setrlimit(2))
26    SIGVTALRM    terminate process    virtual time alarm (see setitimer(2))
27    SIGPROF      terminate process    profiling timer alarm (see setitimer(2))
28    SIGWINCH     discard signal       Window size change
29    SIGINFO      discard signal       status request from keyboard
30    SIGUSR1      terminate process    User defined signal 1
31    SIGUSR2      terminate process    User defined signal 2

If a process explicitly specifies SIG_IGN as the action for the signal SIGCHLD, the system will not create zombie processes when children of the callingprocess exit.  As a consequence, the system  will discard the exit status from the child processes.  If the calling process subsequently issues a call to wait(2) or equivalent, it will block until all of the calling process's children terminate, and then return a value of -1 with errno set to ECHILD.

上面这些是苹果系统列出的可使用的信号,如果func参数可以指定3种,分别是常量SIG_IGN、常量SIG_DFL或者一个函数指针。常量SIG_IGN表示内核忽略此信号,常量SIG_DFL表示执行内核默认动作,函数指针则是当信号发生时,调用该函数,一般叫做信号处理函数。
结合前文中对signal函数的理解,实际上signal函数要求两个参数,只有一个返回值,signal函数第一个参数是signo信号名,第二个参数是一个函数指针,一般是信号处理函数,最后返回这个信号处理函数指针。由于函数原型非常复杂,一般都是使用typedef将其简化。

#define SIG_DFL         (void (*)(int))0
#define SIG_IGN         (void (*)(int))1
#define SIG_HOLD        (void (*)(int))5
#define SIG_ERR         ((void (*)(int))-1)

一般声明就是像上面一样。下面是一个例程

#include "include/apue.h"

static void sig_usr(int);

int main(int argc, char *argv[])
{
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
        err_sys("can't catch SIGUSR1");
    if (signal(SIGUSR2, sig_usr) == SIG_ERR)
        err_sys("can't catch SIGUSR2");
    for ( ; ; )
        pause();
}

static void sig_usr(int signo)
{
    if (signo == SIGUSR1)
        printf("received SIGUSR1\n");
    else if (signo == SIGUSR2)
        printf("received SIGUSR2\n");
    else
        err_dump("received signal %d\n", signo);
}

然后编译运行

> ./a.out &
[1] 21588
> kill -USR1 21588
received SIGUSR1
> kill -USR2 21588
received SIGUSR2
> kill 21588
[1]  + 21588 terminated  ./a.out

我们知道,所有的进程都是内核启动的,当启动一个进程的时候,所有的信号都会被设置为系统默认或者忽略,在Unix环境中最典型的一个例子就是shell启动其他进程,为了保证前台和后台对中断信号和退出信号的捕捉,一般都这样写

void sig_int(int), sig_quit(int);
if (signal(SIGINT, SIG_IGN) != SIG_IGN)
    signal(SIGINT, sig_int);
if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)
    signal(SIGQUIT, sig_quit);

因为所有都是被子进程继承的,所以在exec时,父进程可以控制子进程的忽略状态,所以需要这样判断。
在上面可以看出,signal函数除了改变当前信号的处理方式以外,还会返回之前的状态,这就非常的繁琐,所以在实际中很少使用了。

可重入函数

信号是异步调用,进程自身执行各种正常指令序列,当接受到信号的时候,内核就会通知进程处理,而此时正常的指令序列就会被中断,很容易造成进程空间的破坏,所以现在的Unix实现都会提供在信号处理程序中保证安全调用的函数,通俗的说,就是异步信号安全。至于具体列表,则需要查看具体Unix实现。
在前面学习errno错误信息的时候讲到过,每个进程都维护了一个errno变量,当需要获得的时候就通过extern int errno的形式获取。当然实际不是这样的,因为有多线程的模型存在,所以实际情况更加复杂。当进程执行正常代码,然后调用的系统函数修改了errno值,如果一个信号被发送给进程,进程的信号处理函数调用了另一个系统函数修改errno值,结果最终返回的时候就是信号处理程序内调用得到的errno,所以在实际开发中,如果需要调用系统函数,需要先保存errno值,然后调用后恢复errno


山河永寂
2.4k 声望159 粉丝