《Unix环境高级编程》读书笔记 第10章-信号

1.引言

  • 信号是软件中断
  • 信号提供了一种处理异步事件的方法。

2. 信号概念

  • 信号的名字都是以3个字符SIG开头。
  • Linux3.2.0支持31种信号。FreeBSD、Linux和Solaris作为实时扩展都支持另外的应用程序定义的信号。
  • 在头文件signal.h(其中include的bits/signum.h)中,信号名都被定义为正整数常量,不存在编号为0的信号。kill函数对信号编号0有特殊的应用。

  • 很多条件可以产生信号:

    1. 用户按下某些终端键时:Ctrl+C、Ctrl+\、Ctrl+Z
    2. 硬件异常产生信号:除数为0、无效的内存引用
    3. 进程调用kill函数可将任意信号发送给另一个进程或进程组
    4. 当检测到某些软件条件已经发生,并应将其通知有关进程时产生信号。如:SIGURG(网络连接上传来带外数据)、SIGPIPE(在管道的读进程已经终止后,一个进程写此管道)、SIGALRM(进程所设置的定时器超时)
  • 信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(如errno)来判断是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列操作”。

  • 当某个信号出现时,可以告诉内核按下列3种方式之一进行处理,称之为信号的处理

    1. 忽略此信号。SIG_IGN。只有两种信号不能被忽略:SIGKILL和SIGSTOP。原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(如非法内存引用或除以0),则进程的运行行为是未定义的。
    2. 捕捉信号。即通知内核在某种信号发生后,调用一个用户函数。
    3. 执行系统默认动作。对大多数信号的默认动作是终止该进程。
  • 终止+core。大多数Unix系统调试程序都使用core文件检查进程终止时的状态。

  • 在下列条件下不产生core文件:

    1. 进程是设置用户ID的,而且当前用户并非程序文件的所有者
    2. 进程是设置组ID的,而且当前用户并非程序文件的组所有者
    3. 用户没有写当前工作目录的权限
    4. 文件已存在,而且用户对该文件没有写权限
  • 4个平台对各种signal的支持及默认处理方式
    《Unix环境高级编程》读书笔记 第10章-信号_第1张图片

  • 主要信号简要说明:
    • SIGABRT。调用abort函数时产生此信号。
    • SIGALRM。当用alarm函数设置的定时器超时时,产生此信号。
    • SIGCHLD。在一个进程终止或停止时,该信号被送给其父进程。按系统默认,将忽略此信号。
    • SIGFPE。表示算术运算异常,如除以0、浮点溢出等。
    • SIGHUP。如果终端接口检测到一个连接断开,则将此信号送给与该终端相关的控制进程(会话首进程)。通常使用此信号通知守护进程再次读取它们的配置文件。选用此信号的理由是:守护进程不会有控制终端,通常决不会接收到这种信号。
    • SIGILL。表示进程执行一条非法硬件指令。
    • SIGINT。当用户按下中断键Ctrl+C时,终端驱动程序产生此信号并发送至前台进程组的每一个进程。
    • SIGIO。指示一个异步I/O事件。
    • SIGTERM。由kill命令发送的系统默认终止信号。
    • SIGKILL。不能捕获或忽略。它向管理员提供了一红杀死任一进程的可靠方法。
    • SIGPIPE。如果在管道的读进程已终止时写管道,则产生此信号。
    • SIGQUIT。当用户在终端上按下退出键Ctrl+\时,终端驱动程序产生此信号并发送给前台进程组中的所有进程。此信号除了终止前台进程组(和SIGINT一样),同时产生一个core文件。
    • SIGSEGV。指示进程进行了一次无效的内存引用。
    • SIGTSTP。交互停止信号。当用户在终端上按下挂起键Ctrl+Z时,终端驱动程序产生此信号,并发送至前台进程组的所有进程。
    • SIGSTOP。类似于交互停止信号(SIGTSTP),但它不能被捕获或忽略。
    • SIGCONT。此作业控制信号发送给需要继续运行,但当前处于停止状态的进程。
    • SIGTTIN。当一个后台进程组进程试图读其控制终端时,终端驱动程序产生此信号。下列情况例外:1. 读进程忽略或阻塞此信号;2. 读进程所属的进程组是孤儿进程组,此时读操作返回出错,errno设置为EIO
    • SIGTTOU。当一个后台进程组进程试图写其控制终端时,终端驱动程序产生此信号。
    • SIGURG。通知进程已经发生一个紧急情况。如带外数据到达。
    • SIGUSR1、SIGUSR2。用户定义的信号,可用于应用程序。

3. 函数signal

  
  
  1. #include <signal.h>
  2. void (*signal(int signo, void (*func)(int)))(int);
  3. Returns: previous disposition of signal (see following) if OK, SIG_ERR on error
  • signal函数由ISO C定义。不涉及多进程、进程组以及终端I/O等,所以它对信号的定义非常含糊,以致于对Unix系统而言几乎毫无用处。
  • 因为signal的语义与实现有关,所以最好使用sigaction函数代替signal函数。
  • 本书中的所有实例均使用图10-18中给出的signal函数,该函数使用sigaction函数是一个平台无关、语义一致的实现。
  • signo参数是上面的信号名。func参数可以是常量SIG_IGN、SIG_DFL或接收到该信号后要调用的函数的地址,即信号处理程序的地址。signal函数的返回值是指向在此之前的信号处理程序的指针。
  
  
  1. typedef void Sigfunc(int);
  2. Sigfunc* signal(int, Sigfunc*);

  3. #define SIG_ERR (void (*)())-1
  4. #define SIG_DFL (void (*)())0
  5. #define SIG_IGN (void (*)())1
  • exec,程序启动

    当exec执行一个程序时,所有信号都被设置为它们的默认动作,除非调用exec的进程忽略该信号(则继续保持忽略)。也就是说,exec函数将原先设置为要捕获的信号都更改为默认动作,其他保持不变。因为当exec一个新程序时,信号处理程序的地址很可能在新程序中已无意义。

  • fork,进程创建

    当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为信号处理程序的地址在子进程中是有意义的。

4. 不可靠的信号

  • 早期的Unix版本中,信号是不可靠的。不可靠指的是,信号可能会丢失:一个信号发生了,当进程却可能一直不知道。同时,进程对信号的控制能力很差,它能捕获或忽略它,但不能阻塞。
  • 早期版本的另一个问题是:在进程每次接到信号对其进行处理时,随即将该信号动作重置为默认值。故需要再次建立对该信号的捕获,但在此期间有一个时间窗口。
  • 早期版本的另一个问题是:在进程不希望某种信号发生时,它不能关闭该信号,只能忽略它。

5. 中断的系统调用

  • 早期Unix系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续运行。该系统调用返回出错,其errno设置为EINTR。
  • 为了支持这种特性,将系统调用分为两类:低速系统调用和其他系统调用。
  • 低速系统调用是可能会使进程永远阻塞的一类系统调用。
  • 与被中断的系统调用相关的问题是必须显式地处理出错返回。典型的代码序列如下:
  
  
  1. again:
  2. if ((n = read(fd, buf, BUFFSIZE)) < 0) {
  3. if (errno == EINTR)
  4. goto again; /* just an interrupted system call */
  5. /* handle other errors */
  6. }
  • 4.2 BSD引进了某些被中断系统调用的自动重启动,包括ioctl、read、readv、write、writev、wait、waitpid。但是这种自动重启动的处理方式也会带来问题,某些应用程序并不希望这些函数被中断后重启动。为此,4.3 BSD运行进程基于每个信号禁用此功能。
  • POSIX.1要求只有中断信号的SA_RESTART标志有效时,实现才重启动系统调用。
  • 历史上,使用signal函数建立信号处理程序时,对于如何处理被中断的系统调用,各种实现的做法各不相同。
    《Unix环境高级编程》读书笔记 第10章-信号_第2张图片

6. 可重入函数

  • 进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。但是,在信号处理程序中,不能判断捕捉到信号时进程执行到何处:
    1. 如果进程正在执行malloc,而在信号处理程序中又再次调用malloc,这时会?
    2. 如果进程正在执行getpwnam,这是将其结果存放在静态存储单元中的函数,而在信号处理程序中又再次调用getpwnam,这时会?
  • SUS说明了在信号处理程序中保证调用安全的函数。这些函数是可重入的,并被称为异步信号安全的。
    《Unix环境高级编程》读书笔记 第10章-信号_第3张图片
  • 没有列入上图的大多数函数是不可重入的,因为:
    1. 它们使用静态数据结构
    2. 它们调用malloc或free
    3. 它们是标准的I/O函数。标准I/O库的很多实现都以不可重入方式使用全局数据结构。
  • 应当了解,即使信号处理程序调用的是上图中的函数,但是由于每个线程只有一个errno变量,所以信号处理程序可能会修改其原先值。故作为一个通用的规则,先保存,后恢复。

7. SIGCLD语义

8. 可靠信号术语和语义

  • 首先,当造成信号的事件发生时,向进程发送一个信号。
  • 当对信号采取了某种动作时,我们说向进程递送了一个信号。在信号产生和递送之间的时间间隔内,称信号是未决的。
  • 进程可以选用“阻塞信号递送”。内核在递送一个原来被阻塞的信号给进程时(而不是在产生该信号时),才决定对它的处理方式。因此,进程在信号递送给它之前仍可改变对该信号的动作。
  • 每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。进程可以调用sigprocmask函数来检测和更改其当前信号屏蔽字。
  • 进程调用sigpending函数来判定哪些信号是设置为阻塞并处于未决状态的。
  • 如果在进程解除对某个信号的阻塞之前,该信号发生了多次,那么?如果递送该信号多次,则称这些信号进行了排队。除非支持POSIX.1实时扩展,否则大多数Unix并不对信号排队,而只递送一次。

9. 函数kill和raise

  • kill函数将信号发送给进程或进程组
  • raise函数则允许进程向自身发送信号。
  
  
  1. #include <signal.h>
  2. int kill(pid_t pid, int signo);
  3. int raise(int signo);
  4. Both return: 0 if OK, 1 on error
  • raise(signo); 等价于 kill(getpid(), signo);
  • kill的pid参数有以下4种情况:
    • pid > 0,发送给进程ID为pid的进程
    • pid == 0,发送给与发送进程属于同一进程组的所有进程
    • pid < 0,发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程
    • pid == -1,发送给发送进程具有权限向它们发送信号的所有进程
  • 关于发送信号的权限

    1. 超级用户可将信号发送给任一进程。
    2. 非超级用户,其基本规则是发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID。

      特例:如果被发送的信号是SIGCONT,则进程可以将它发送给属于同一会话的任一其他进程。

  • POSIX.1 将信号编号为0定义为空信号。如果signo参数为0,则kill仍执行正常的错误检查,当不发送信号。这常被用来确定一个特定进程是否仍然存在。但是,在返回测试结果时,原来存在的被测试进程可能已经终止,所以这种测试并无多大意义。

10. 函数alarm、pause

  • 当定时器超时时,产生SIGALRM信号。信号由内核产生。
  
  
  1. #include <unistd.h>
  2. unsigned int alarm(unsigned int seconds);
  3. Returns: 0 or number of seconds until previously set alarm
  • 每个进程只能有一个闹钟时间。多次调用alarm以新值代替旧值,并返回旧值的余留值。参数为0,则取消以前的闹钟。

  • pause函数使调用进程挂起直到捕捉到一个信号

  
  
  1. #include <unistd.h>
  2. int pause(void);
  3. Returns: 1 with errno set to EINTR
  • 只有执行了一个信号处理程序并从其中返回时,pause才返回。返回-1,errno设置为EINTR。

11. 信号集

  • POSIX.1定义数据类型sigset_t包含一个信号集,并定义以下5个处理信号集的函数
  
  
  1. #include <signal.h>
  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. All four return: 0 if OK, 1 on error

  7. int sigismember(const sigset_t *set, int signo);
  8. Returns: 1 if true, 0 if false, 1 on error

12. 函数sigprocmask

  • 调用函数sigprocmask可以检测或更改,或同时检测和更改进程的信号屏蔽字
  
  
  1. #include <signal.h>
  2. int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
  3. Returns: 0 if OK, 1 on error
  • how参数:SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK
  • 在调用sigprocmask后如果有任何未决的、不再阻塞的信号,则在其返回之前,至少将其中之一递送给该进程。

13. 函数sigpending

  • sigpending函数返回一信号集,它对于调用进程而言,其中的各信号是阻塞不能递送的
  
  
  1. #include <signal.h>
  2. int sigpending(sigset_t *set);
  3. Returns: 0 if OK, 1 on error

14. 函数sigaction

  • sigaction函数的功能是检查或修改与指定信号相关联的处理动作
  
  
  1. #include <signal.h>
  2. int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
  3. Returns: 0 if OK, 1 on error
  
  
  1. struct sigaction {
  2. void (*sa_handler)(int); /* addr of signal handler, or SIG_IGN, or SIG_DFL */
  3. sigset_t sa_mask; /* additional signals to block */
  4. int sa_flags; /* signal options, Figure 10.16 */

  5. void (*sa_sigaction)(int, siginfo_t *, void *); /* alternate handler */
  6. };

《Unix环境高级编程》读书笔记 第10章-信号_第4张图片

     原创文章,转载请声明出处:http://www.cnblogs.com/DayByDay/p/3948397.html

你可能感兴趣的:(unix)