from : https://segmentfault.com/a/1190000004506065


信号概念

从前面的文章和实际使用中,大家应该也对信号有一些模糊的认识了。比如,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);
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。


SIGCLD信号

SIGCLD和SIGCHLD是两个很相似的信号,SIGCLD是SystemV的一个信号名字,而SIGCHLD是BSD信号,但是POSIX.1标准使用了BSD的SIGCHLD信号名称。

BSD的SIGCHLD信号是很普通的意思,就是子进程状态改变就会产生这个信号,父进程则是调用wait函数查看子进程的状态,而SystemV的SIGCLD信号则不同,基于SVR4的系统都会继承这个情况。

  1. 如果进程明确配置SIGCLD信号为SIG_IGN,则调用进程的子进程不产生僵尸进程,在前面说过,如果子进程退出,但是没有父进程进行清理,则会产生僵尸进程。而如果配置成SIG_IGN,则子进程在终止的时候,就会丢弃其退出状态,那么父进程使用wait函数就不会接收到任何的信息,直到所有的子进程终止。
  2. 如果SIGCLD被设置为捕捉,则内核就会立刻检查是否有子进程准备好了等待。

基本就是这些,实际上这个信号可看可不看。因为在实际开发中是不可能使用这个信号的,不少平台都不支持此信号。


可靠信号术语和语义

信号是事件发生时,为进程产生一个信号或者发送一个信号,当信号产生时,内核会在进程表中设置一个标志。在信号产生和传递中间,信号是阻塞的(pending)。

进程可以设置阻塞一个信号传送,如果对这个进程发送了一个已经设置为阻塞的信号,并且该信号的动作是系统默认动作或者捕捉该信号,换言之,就是不忽略该信号的处理,则为该进程将此信号设置为pending状态,直到该进程对此信号接触阻塞,或者设置该信号的动作为忽略。

int sigpending(sigset_t *set);

实际上,每个进程都有一个信号屏蔽字(signal mask),就和权限屏蔽字一样,这是用于记录当前要阻塞传递的信号集。


发送信号

发送信号有两种函数

int kill(pid_t pid, int sig);
int raise(int sig);

两个函数的区别就是一个是系统函数库,一个是ISO C函数库,并且raise函数允许进程向自身发送信号。

kill函数很常用,所以在这里讲一下kill函数的参数

  1. pid大于0,信号发送给当前为pid的进程
  2. pid等于0,信号发送给当前进程同一进程组的所有进程
  3. pid等于-1,信号发送给发送进程有权限发送信号的所有进程

为了保持SystemV的兼容,当pid小于0且不等于-1的时候,信号发送给所有进程组ID等于当前进程绝对值的进程。


alarm和pause函数

unsigned alarm(unsigned seconds);

alarm函数就是设置一个定时器,当超时后会产生一个SIGALRM信号,如果忽略或者系统默认动作,就是进程终止,当然,一般情况下,进程都捕捉该信号。

int pause(void);

pause函数会强制进程暂停直到从kill函数或者setitimer函数收到一个信号。所以当看到这两个函数,基本想法就是这两个函数能让进程休眠

#include <signal.h>
#include <unistd.h>

static void sig_alrm(int signo)
{
}

unsigned int sleep1(unsigned int seconds)
{
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        return seconds;
    alarm(seconds);
    pause();
    return(alarm(0));
}

当然,这样子写肯定是有问题的,比如

  1. 在sleep1之前,已经存在定时器了,那么sleep函数中第一次alarm调用将会擦除之前的闹钟。
  2. 程序修改了SIGALRM信号的配置
  3. 在第一课次调用alarm和puse之间有一个竞争条件。

信号集

为了能够表示多个信号,系统提供了信号集的数据类型。在通常的开发中,经常会使用到二进制位来表示状态,二进制的每一位代表一种信号,但是实际上,由于信号的编号肯定会超过一个整形量的位数,所以一般都不是用一个整形量表示信号集。POSIX.1定义了数据类型sigset_t用以表示一个信号集,苹果系统下实际上是这么表示的

typedef __uint32_t    __darwin_sigset_t;
typedef __darwin_sigset_t sigset_t;

除此以外,系统还定一款了下列5个处理信号集的函数

int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigismember(const sigset_t *set, int signo);

sigemptyset函数初始化set指向的信号集,清除所有信号,sigfillset初始化set指向的信号集,被设置为包含所有信号,在使用之前,sigemptyset或者sigfillset必须被调用。sigaddset和sigdelset则是添加删除一个信号,sigismember函数则返回是否一个指定的signo信号被包含在这个信号集中。


sigprocmask函数

在前文中提及了信号屏蔽字指定了当前进程阻塞不能传递的信号集。而sigprocmask函数就是用来检测修改信号屏蔽字的函数

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

如果set不是null,sigprocmask函数的行为依赖how参数。

  1. SIG_BLOCK,进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集的并集运算

  2. SIG_UNBLOCK,信号新的屏蔽字是当前信号屏蔽字和set指向信号集的补集的交集

  3. SIG_SETMASK,新的信号屏蔽字是set指向信号集

如果oset不是null,那么当前的信号屏蔽字将会被设置给oset,如果set参数为null,则不改变信号屏蔽字,how参数也没有意义。


sigpending和sigaction函数

sigpending函数返回当前进程阻塞不能传递信号的信号集。

int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);

sigaction函数用于检查或修改与制定信号相关联的处理动作,如果act指针非空,则修改其动作,如果oact非空,则通过oact指针返回该信号的上一个动作,是不是觉得很熟悉,这个函数很像sigprocmask函数,实际上这个函数就是替代了signal函数,在上面的参数中也可以看出实际上多了新结构体,手册上也写了,所以这里也列了出来。

struct  sigaction {
        union __sigaction_u __sigaction_u;  /* signal handler */
        sigset_t sa_mask;               /* signal mask to apply */
        int     sa_flags;               /* see signal options below */
};

union __sigaction_u {
        void    (*__sa_handler)(int);
        void    (*__sa_sigaction)(int, siginfo_t *,
                      void *);
};

#define sa_handler      __sigaction_u.__sa_handler
#define sa_sigaction    __sigaction_u.__sa_sigaction

看起来和原著上面不一样啊,其实就是一个道理,原著中有两种handler,所以用了两个成员存储,而这里实际上使用一个union来存储,因为handler实际上只有一个的。当更改信号的时候,如果sa_handler包含一个信号捕捉函数地址,则sa_mask字段说明了一个信号集,在调用信号捕捉函数之前,信号集要加入到进程信号屏蔽字中。

siginfo_t结构体包含了信号产生原因的有关信息,这里就不在继续列出。


sigsetjmp和siglongjmp函数

前面讲过setjmp和longjmp函数,主要用于非局部转移。信号处理程序经常会调用longjmp函数返回到main函数,但是,当使用longjmp函数的时候,信号会自动的加到进程屏蔽字中,如果使用longjmp跳出,则会在一些平台上导致信号屏蔽字无法恢复,所以Unix系统提供了两个新函数用于信号处理函数的非局部转移。这两个函数只在此介绍一下,不详细讲述了。


sigsuspend函数

sigsuspend函数临时改变当前的阻塞信号屏蔽字为sigmask参数指定的信号集,然后等待一个信号到来,返回的时候,先前的信号屏蔽字将被恢复。可能有朋友想问,这个函数到底是干嘛的,如果按照我们之前的知识,完全可以使用sig信号集函数去除被阻塞信号,然后使用pause等待信号发生。实际上,我们需要考虑到这是两步操作,很有可能在pause之前,就已经有信号传递了,所以这个函数只是执行了原子操作的封装。


abort函数

abort函数也没有什么好说的,这个函数从名字就知道是程序终止,并且是异常终止。

void abort(void);

这是一个ISO C库函数,函数就是发送了SIGABRT信号给调用进程。abort函数会导致不正常的程序终止,除非信号SIGABRT被捕捉并且信号处理函数没有返回。实际上,信号处理函数不能返回的唯一方法是它调用exit、_exit、_Exit、longjmp或者siglongjmp。当执行此函数的时候,所有的stream都会被冲洗并且被关闭。其实结合上面的信息,很容易就知道了,abort函数肯定是会终止程序的,但是捕捉SIGABRT的唯一意图就是——在程序终止前由其执行所需的清理操作。


System函数

int system(const char *command);

system函数把command参数提交给命令解释器sh,调用的进程等待shell执行命令结束,忽略SIGINT、SIGQUIT,并且阻塞SIGCHLD信号,如果command参数是null指针,system函数将会返回非0当sh解释器可用,如果不可用,则返回0。

为什么system函数需要考虑信号处理,实际上原著讲述足够详细了,一句话,system函数创建的子进程不应当使用wait函数获得退出状态而导致system函数阻塞。


sleep、nanosleep和clock_nanosleep函数

unsigned int sleep(unsigned int seconds);

sleep函数会导致调用进程挂起,当进程超过了seconds指定的时间或者收到一个信号并且从信号处理程序返回,那么进程将会恢复。实际上,很容易就把sleep函数和alarm函数对比,而且alarm函数的信号是否会导致sleep函数的失败,所以sleep函数并不是那么好用。

int nanosleep(const struct timespec *rqtp, struct timespec *rmtp);

nanosleep和sleep函数差不多,但是提供了纳秒级精度,而且非常尴尬的是,系统很有可能不支持纳秒级精度,这就会导致时间取整。由于现代化Unix操作系统有多个系统时钟,sleep函数也衍生出了clock_nanosleep函数用于相对特定时钟挂起,但是在苹果系统中,好像并没有存在这个函数,手册和头文件都没有找到,所以这里就不在提及。


sigqueue函数

int sigqueue(pid_t pid,int sig,const union sigval value);

sigqueue在队列中向指定进程发送一个信号和数据。但是好像苹果系统也没有提供这个函数,或者说是提供了自有函数,所以很遗憾,无法讲解,只能请各位朋友自行阅读原著。


小结

其实本章最后应该还有两节,但是这两节只是将作业控制信号和信号名编号转换稍稍讲解一下,实际上没有什么价值,信号在大多数的应用程序中都是非常重要的手段,所以应当掌握。

results matching ""

    No results matching ""