APUE笔记之10章_信号

版权声明:本文为博主原创文章,可以转载但必须注明出处。 https://blog.csdn.net/nirendao/article/details/88219526

第10章 信号

介绍

信号是软件中断。信号提供异步事件处理方法。

早期的信号模型 不可靠,信号可能丢失。POSIX.1对可靠信号例程进行了标准化。

调用kill(2)函数可将任意信号发送给另一个进程或进程组,但有限制:

  • 信号的发送进程和接收进程的所有者必须相同,或者,
  • 发送信号进程的所有者是超级用户

当某个信号出现时,内核有3种处理方式:

  1. 忽略:有2种信号不能被忽略 - SIGKILL 和 SIGSTOP , 原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法;
  2.  捕捉:比如,若捕捉到SIGCHLD信号,则表示一个子进程已经终止,所以SIGCHLD信号的捕捉函数可调用 waitpid 以取得该子进程的进程ID和它的终止状态。又比如,若进程创建了临时文件,那么要为 SIGTERM 信号编写一个信号捕捉函数以清除临时文件(SIGTERM是终止信号)
  3. 执行默认动作:图10-1给出了每一种信号的系统默认动作(P251)。大多数信号的系统默认动作是终止该进程。

常见信号及发生原因:

SIGABRT 

调用abort函数时产生此信号;进程异常终止。

SIGALRM 

进程所设置的定时器超时

SIGCHLD

当一个进程终止或停止时,SIGCHLD被送给其父进程;系统默认忽略此信号;若父进程希望被告知,则其应该捕捉此信号,在信号处理函数中调用一种wait函数以取得子进程的ID和终止状态。

SIGCONT

这是作业控制信号,发送给需要继续运行,但当前处于停止状态的进程。若进程没有处于停止状态,则默认动作是忽略此信号。

SIGFPE

算术运算异常,如除以0、浮点移出

SIGHUP

终端接口检测到一个连接断开,则将此信号发送给与该终端相关的控制进程(即会话首进程)

SIGINT

中断信号,由Ctrl+C产生,用来停止程序;

SIGIO

一个异步I/O事件产生了

SIGKILL

用来杀死进程,不能被忽略

SIGPIPE 

在管道的读进程已终止后,一个进程写此管道,则产生此信号;

SIGQUIT

Ctrl+\ ,不仅终止前台进程组,而且产生一个core文件

SIGSEGV

 无效的内存访问

SIGSTOP

这是一个作业控制信号,停止一个进程;不能被忽略

SIGTERM

一般让该信号的捕捉函数在程序退出之前做好清理工作,从而优雅地终止

SIGTSTP

交互停止信号;当用户在终端上按 Ctrl+Z 后,终端驱动程序产生此信号。该信号发送给前台进程组的所有进程。

函数 signal 

#include <signal.h>

void (*signal(int signo, void(*func)(int)))(int);
  • signo是信号名
  • func的值是3种之一: SIG_IGNSIG_DFL(default)、以及信号处理函数的地址

这个原型太复杂了,使用下面的typedef会简单一些:

typedef void Sigfunc(int);

Sigfunc *signal(int, Sigfunc *); 

简单来讲,signal函数原型是:

  • 有2个参数,返回1个函数指针;
  • 第1个参数signo是一个整型数,代表信号;
  • 第2个参数是一个函数指针,它所指向的函数带1个int型的参数而无返回值;
  • 返回值是1个函数指针,它所指向的函数带1个int型的参数而无返回值;

查看系统的头文件signal.h,可以看到下列形式的声明:

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

这些常量用于表示“指向函数的指针,该函数要求一个整型参数,而无返回值”。这些常量所用的3个值不一定是-1、0、1,但它们必须是3个值而决不能是任一函数的地址。

程序示例:

#include <signal.h>


// one handler for 2 signals
static void sig_user(int)
{
    if (signo == SIGUSR1) 
        printf("received SIG_USR1\n");
    else if (signo == SIGUSR2)
        printf("received SIG_USR2\n");
    else 
        printf("received signal %d\n", signo);
}

int main()
{
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
        printf("cannot catch SIGUSR1\n");
    if (signal(SIGUSR2, sig_usr) == SIG_ERR)
        printf("cannot catch SIGUSR2\n");
    while(1) {
        pause();
    }
    return 0;
}

程序的启动

exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址一般在所执行的新程序文件中已无意义)。

当进程调用fork后,子进程复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

很多捕捉到SIG_INT和SIG_IGN的程序具有下列形式的代码:
 

void sig_int(int);
void 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); 

不可靠的信号

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

有时候用户希望通知内核阻塞某个信号,即:不忽略该信号,在其发生时记住它,在进程做好了准备时再通知它。这种阻塞信号的能力,在早期并不具备。

中断的系统调用

早期的UNIX系统:如果进程在执行一个低速系统调用而阻塞时,捕捉到一个信号,则该系统调用就被中断而不再执行。

后来的UNIX系统:捕捉到信号并处理后,会返回已读或已写的部分,从而视该系统调用为成功。

4.2BSD引入了一些被中断后可以自动重启动的系统调用:ioctl、read、readv、write、writev、wait、waitpid. 

前5个函数只有对低速设备操作时才会被信号中断,而wait和waitpid在捕捉到信号时则总是被中断。

4.3BSD允许进程基于每个信号禁用此自动重启动的功能。

系统调用可分为2类:低速系统调用和其他系统调用。

低速系统调用是可能会使进程永远阻塞的一类系统调用。如,读管道、终端、网络设备时,数据不存在。

可重入函数

下列是一些不可重入函数的特征:

  1. 使用了静态的数据结构
  2. 调用了malloc或free
  3. 是标准I/O函数,因为标准I/O库的很多实现都以不可重入的方式使用全局数据结构

作为一种通用的规则,当在信号处理程序中调用图10-4中的这些可重入函数时,应当在调用前保存errno,在调用后恢复errno. 

在信号处理程序中,调用一个不可重入函数,其结果是不可预知的。举个栗子,当主程序中调用free时被信号中断,而信号处理程序中也调用了free,那么malloc和free维护的数据结构就遭到了破坏,从而程序会运行出错。

SIGCLD语义

SIGCLD是SystemV的一个信号名,其语义与名为SIGCHLD的BSD信号不同。POSIX.1采用BSD的SIGCHLD信号。

BSD的SIGCHLD的语义是,子进程改变状态后产生此信号,父进程需要调用一个wait函数以检测发生了什么。

对于SIGCLD的处理方式(略)

注意,Linux3.2.0和Solaris 10定义了SIGCLD,其等同于SIGCHLD. 

可靠信号术语与语义

当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。当内核做这个动作时,我们称为“向进程递送了一个信号”。

在信号产生(generation)和递送(delivery)之间的时间间隔,称信号是未决的(pending)

进程可以“阻塞信号递送”。如果为进程产生了一个阻塞的信号,而且信号处理动作是系统默认动作(SIG_DFL)或捕捉该信号,那么该进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞或对此信号的动作更改为忽略。

进程调用 sigpending 函数来判定哪些信号是设置为阻塞并处于未决状态的。

如果在进程解除对某个信号的阻塞之前,该信号发生了多次,将如何呢?

POSIX.1允许系统递送该信号一次或多次。若递送多次,则称这些信号排队了。但除非支持POSIX.1的实时扩展,否则大多数UNIX并不对信号排队,而只递送一次。

若有多个信号要递送给一个进程,POSIX.1并没有规定这些信号的递送顺序。但POSIX.1基础部分建议:在其他信号之前递送与进程当前状态有关的信号,如SIGSEGV. 

每个进程都有一个信号屏蔽字(signal mask),它规定了当前要阻塞而不递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已经设置,则它当前是被阻塞的。进程可以调用 sigprocmask 函数来检测和更改其当前的信号屏蔽字。

信号编号可能会超过一个整型的二进制位数,因此POSIX.1定义了一个新数据类型 sigset_t , 它可以容纳一个信号集。

信号集

数据类型信号集(signal_set)被函数sigprocmask用于告诉内核不允许发生在该信号集中的信号。

下面是处理信号集的函数

#include <signal.h>

int sigemptyset(sigset_t *set);  // 清除信号集中的所有信号
int sigfillset(sigset_t *set);  // 初始化由set指向的信号集,使其包括所有信号
int sigaddset(sigset_t *set, int signo);  // 讲一个信号signo添加到信号集set中
int sigdelset(sigset_t *set, int signo);  // 从信号集中删除一个信号
// 以上4个函数,若成功,返回0,失败返回-1

int sigismember(const sigset_t * set, int signo);  // 判断信号signo是否在信号集set内

信号相关函数

  • kill(): 将信号发送给进程或进程组。
  • raise(): 允许进程向自身发信号。  raise(signo); = kill(getpid(), signo);
  • alarm(): 可利用此函数设置一个定时器,将来超时的时候,产生SIGALRM信号。如果忽略或不捕捉该信号,则默认动作是终止调用该alarm函数的进程。
  • pause():使调用进程挂起,直至捕捉到一个信号。
  • abort():  将SIGABRT信号发送给调用进程,使其异常终止。让进程捕捉SIGABRT信号的意图是,在进程终止之前执行所需的清理工作。
  • sigprocmask(): 检测或更改进程的信号屏蔽字
  • sigpending(): 返回一个信号集,对于调用进程而言, 其中各信号是阻塞而不能递送的,因而也一定是当前未决的。
  • sigaction(): 常被用来实现signal函数,功能是检查或修改与指定信号相关联的处理动作。此函数取代了UNIX早期版本的 signal 函数。
  • sigsuspend(): 在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号的处理程序返回了,则sigsuspend()返回,并且该进程的信号屏蔽字设置为调用sigsuspend()之前的值。
  • sigqueue(): 大部分UNIX系统不对信号进行排队,而POSIX.1的实时扩展中,有些系统开始增加对信号排队的支持。
  • sigsetjmp()和siglongjmp(): 从多重嵌套的信号处理程序中一下返回到主程序中。

作业控制信号

POSIX.1认为有以下6个与作业控制有关的信号:

  • SIGCHLD: 子进程已经停止或终止
  • SIGCONT: 如果进程停止,则使其继续运行
  • SIGSTOP: 停止信号(不能被捕捉或忽略)
  • SIGTSTP: 交互式停止信号
  • SIGTTIN: 后台进程组成员读控制终端
  • SIGTTOU: 后台进程组成员写控制终端

其他略

信号名和编号

(完)

猜你喜欢

转载自blog.csdn.net/nirendao/article/details/88219526
今日推荐