TLPI读书笔记第20章-信号:基本概念2

20.6 检查进程的存在

kill()系统调用还有另一重功用。若将参数 sig 指定为 0(即所谓空信号),则无信号发送。相反, kill()仅会去执行错误检查,查看是否可以向目标进程发送信号。从另一角度来看,这意味着,可以使用空信号来检测具有特定进程 ID 的进程是否存在。若发送空信号失败,且 errno为 ESRCH,则表明目标进程不存在。如果调用失败,且 errno 为 EPERM(表示进程存在,但无权向目标进程发送信号)或者调用成功(有权向进程发送信号),那么就表示进程存在。 验证一个特定进程 ID 的存在并不能保证特定程序仍在运行。因为内核会随着进程的生灭而循环使用进程 ID。而一段时间之后,同一进程 ID 所指恐怕是另一进程了。此外,特定进程ID 可能存在,但是一个僵尸(亦即,进程已死,但其父进程尚未执行 wait()来获取其终止状态,如 26.2 节所述)。 还可使用各种其他技术来检查某一特定进程是否正在运行,其中包括如下技术。

1.wait()系统调用:第 26 章将描述这些调用。这些调用仅用于监控调用者的子进程。

2.信号量和排他文件锁:如果进程持续持有某一信号量或文件锁,并且一直处于被监控状态,那么如能获取到信号量或锁时,即表明该进程已经终止。

3.诸如管道和 FIFO 之类的 IPC 通道:可对监控目标进程进行设置,令其在自身生命周期内持有对通道进行写操作的打开文件描述符。同时,令监控进程持有针对通道进行读操作的打开文件描述符,且当通道写入端关闭时,即可获知监控目标进程已经终止。监控进程对此情况的判定,既可借助于对自身文件描述符的读取,也可采用第 63 章所述的描述符监控技术之一。

4./proc/PID 接口:例如,如果进程 ID 为 12345 的进程存在,那么目录/proc/12345 将存在,可以发起诸如 stat()之类的调用来进行检查。 除去最后一项之外,循环使用进程 ID 不会影响上述所有技术。 程序清单 20-3 展示了 kill()的用法。该程序接受两个命令行参数,分别为信号编号和进程ID,并使用 kill()将该信号发送给指定进程。如果指定了信号 0(空信号),那么程序将报告目标进程是否存在

20.7 发送信号的其他方式: raise()和 killpg()

有时,进程需要向自身发送信号( 34.7.3 节就有此一例)。 raise()函数就执行了这一任务。

#include<signal.h>
int raise(int sig);

在单线程程序中,调用 raise()相当于对 kill()的如下调用:kill(getpid(),sig) 支持线程的系统会将 raise(sig)实现为:pthread_kill(pthread_self(),sig) 33.2.3 节描述了 pthread_kill()函数,但目前仅需了解一点就已足够,该实现意味着将信号 传递给调用 raise()的特定线程。相比之下, kill(getpid(), sig)调用会发送一个信号给调用进程, 并可将该信号传递给该进程的任一线程。

当进程使用 raise()(或者 kill())向自身发送信号时,信号将立即传递(即,在 raise()返回调用者之前)。 注意, raise()出错将返回非 0 值(不一定为–1)。调用 raise()唯一可能发生的错误为 EINVAL,即 sig 无效。因此,在任何指定了某一 SIGxxxx 常量的位置,都未检查该函数的返回状态。

#include<signal.h>
int killpg(pid_t pgrp,int sig);

killpg()函数向某一进程组的所有成员发送一个信号。 killpg()调用相当于对 kill()的如下调用:kill(-pgrp,sig) 如果指定 pgrp 的值为 0,那么会向调用者所属进程组的所有进程发送此信号。 SUSv3 对此未作规范,但大多数 UNIX 实现对该情况的处理方式与 Linux 相同

20.8 显示信号描述

每个信号都有一串与之相关的可打印说明。这些描述位于数组 sys_siglist 中。例如,可以用 sys_siglist[SIGPIPE]来获取对 SIGPIPE 信号(管道断开)的描述。然而,较之于直接引用sys_siglist 数组,还是推荐使用 strsignal()函数。

#include<signal.h>
extern const char *const sys_siglist[];
#include<string.h>
char *strsigal(int sig);
int psignal(int sig,const char *msg);

strsignal()函数对 sig 参数进行边界检查,然后返回一枚指针,指向针对该信号的可打印描述字符串,或者是当信号编号无效时指向错误字符串。(在其他一些 UNIX 实现中, strsignal()函数会在 sig 无效时返回空值。 ) 除去边界检查之外, strsignal()函数较之于直接引用 sys_siglist 数组的另一优势是对本地 ( locale)设置敏感( 10.4 节),所以显示信号描述时会使用本地语言。 程序清单 20-4 中所示为使用 strsignal()的例子之一。 psignal()函数(在标准错误设备上)所示为 msg 参数所给定的字符串,后面跟有一个冒号,随后是对应于 sig 的信号描述。和 strsignal()一样, psignal()函数也对本地设置敏感。 尽管 SUSv3 并未将 psignal()、 strsignal()和 sys_siglist 纳入标准,但还是有许多 UNIX 实现支持它们。 ( SUSv4 中加入了对 psignal()和 strsignal()的规范。 )

20.9 信号集

许多信号相关的系统调用都需要能表示一组不同的信号。例如, sigaction()和 sigprocmask()允许程序指定一组将由进程阻塞的信号,而 sigpending()则返回一组目前正在等待送达给一进程的信号。 (稍后将描述这些系统调用。 ) 多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t.。 SUSv3规定了一系列函数来操纵信号集,现在将描述这些函数

sigemptyset()函数初始化一个未包含任何成员的信号集。 sigfillset()函数则初始化一个信号集,使其包含所有信号(包括所有实时信号)。

#include<signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set ,int sig);
int sigdelset(sigset_t *set,int sig);
int sigismember(sigset_t *set,int sig);1=true,other=false
/*补充函数*/
int sigandset(sigset_t *set,sigset_t *left,sigset_t *right);
int sigandset(sigset_t *set,sigset_t *left,sigset_t *right);
int sigisemptyset(sigset_t *set);

必须使用 sigemptyset()或者 sigfillset()来初始化信号集。这是因为 C 语言不会对自动变量进行初始化,并且,借助于将静态变量初始化为 0 的机制来表示空信号集的作法在可移植性上存在问题,因为有可能使用位掩码之外的结构来实现信号集。(出于同一原因,为将信号集标记为空而使用 memset(3)函数将其内容清零的做法也不正确。 ) 信号集初始化后,可以分别使用 sigaddset()和 sigdelset()函数向一个集合中添加或者移除单个信号

在 sigaddset()和 sigdelset()中, sig 参数均表示信号编号。sigismember()函数用来测试信号 sig 是否是信号集 set 的成员

如果 sig 是 set 的一个成员,那么 sigismember()函数将返回 1(true),否则返回 0(false)。 GNU C 库还实现了 3 个非标准函数,是对上述信号集标准函数的补充。

这些函数执行了如下任务。 1.sigandset()将 left 集和 right 集的交集置于 dest 集。 2.sigorset()将 left 集和 right 集的并集置于 dest 集。 3.若 set 集内未包含信号,则 sigisemptyset()返回 true。

示例程序 程序清单 20-4 所示为使用本节介绍的函数来编写的函数,供本书后续各程序调用。第一个函数 printSigset()显示了指定信号集的成员信号。该函数使用了定义于<signal.h>文件中的NSIG 常量,其值等于信号最大编号加 1。当获取信号集成员时,会在测试所有信号编号的循环中将该值作为循环上限。 利用 printSigset()函数, printSigMask()和 printPendingSigs()函数分别用于显示进程的信号掩码和当前处于等待状态的信号集。 这两个函数还分别使用了sigprocmask()和sigpending()系统调用。 sigprocmask()和 sigpending()系统调用将分别在 20.10 节和 20.11 节中予以描述。

20.10 信号掩码(阻塞信号传递)

内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程的传递。

(是不是类似黑名单,进程不接受也不处理这类信号?)

如果将遭阻塞的信号发送给某进程,那么对该信号的传递将延后,直至从进程信号掩码中移除该信号,从而解除阻塞为止。 向信号掩码中添加一个信号,有如下几种方式。 1.当调用信号处理器程序时,可将引发调用的信号自动添加到信号掩码中。是否发生这一情况,要视 sigaction()函数在安装信号处理器程序时所使用的标志而定。 2.使用 sigaction()函数建立信号处理器程序时,可以指定一组额外信号,当调用该处理器程序时会将其阻塞。 3.使用 sigprocmask()系统调用,随时可以显式向信号掩码中添加或移除信号。 对前两种情况的讨论将推迟到 20.13 节对 sigaction()函数的介绍之后,现在先来讨论sigprocmask()函数

#include<signal.h>
int sigprocmask(int how,sigset_t *set,sigset_t *oldset);

使用 sigprocmask()函数既可修改进程的信号掩码,又可获取现有掩码,或者两重功效兼具。 how 参数指定了 sigprocmask()函数想给信号掩码带来的变化。

SIG_BLOCK 将 set 指向信号集内的指定信号添加到信号掩码中。换言之,将信号掩码设置为其当前值和 set 的并集。

SIG_UNBLOCK 将 set 指向信号集中的信号从信号掩码中移除。即使要解除阻塞的信号当前并未处于阻塞状态,也不会返回错误。

SIG_SETMASK 将 set 指向的信号集赋给信号掩码。 上述各种情况下,若 oldset 参数不为空,则其指向一个 sigset_t 结构缓冲区,用于返回之前的信号掩码。 如果想获取信号掩码而又对其不作改动,那么可将 set 参数指定为空,这时将忽略 how 参数。 要想暂时阻止信号的传递,可以使用程序清单 20-5 中所示的一系列调用来阻塞信号,然后再将信号掩码重置为先前的状态以解除对信号的锁定。

USv3 规定,如果有任何等待信号因对 sigprocmask()的调用而解除了锁定,那么在此调用返回前至少会传递一个信号。换言之,如果解除了对某个等待信号的锁定,那么会立刻将该信号传递给进程。 系统将忽略试图阻塞 SIGKILL 和 SIGSTOP 信号的请求。如果试图阻塞这些信号,sigprocmask()函数既不会予以关注,也不会产生错误。这意味着,可以使用如下代码来阻塞除SIGKILL 和 SIGSTOP 之外的所有信号:

20.11 处于等待状态的信号

如果某进程接受了一个该进程正在阻塞的信号,那么会将该信号填加到进程的等待信号集中。当(且如果)之后解除了对该信号的锁定时,会随之将信号传递给此进程。为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()。

#include<signal.h>
int sigpending(sigset_t *set);

sigpending()系统调用为调用进程返回处于等待状态的信号集,并将其置于 set 指向的sigset_t 结构中。随后可以使用 20.9 节描述的 sigismember()函数来检查 set。 如果修改了对等待信号的处置,那么当后来解除对信号的锁定时,将根据新的处置来处理信号。这项技术虽然不经常使用,但还是存在一个应用场景,即将对信号的处置置为SIG_IGN, 或者 SIG_DFL(如果信号的默认行为是忽略),从而阻止传递处于等待状态的信号。 因此,会将信号从进程的等待信号集中移除,从而不传递该信号。

20.12 不对信号进行排队处理

等待信号集只是一个掩码,仅表明一个信号是否发生,而未表明其发生的次数。换言之,如果同一信号在阻塞状态下产生多次,那么会将该信号记录在等待信号集中,并在稍后仅传递一次。(标准信号和实时信号之间的差异之一在于,如 22.8 节所述,对实时信号进行了排队 处理。 ) 程序清单 20-6 和程序清单 20-7 显示了两个程序,可用于观察未作排队处理的信号。清单 20-6的程序可接受多达四个命令行参数,如下所示:

第一个参数是程序发送信号的目标进程 ID。

第二个参数则指定发送给目标进程的信号数量。

第三个参数指定发往目标进程的信号编号。如果还提供了一个信号编号作为第四个参数,那么当程序发送完之前参数所指定的信号之后,将发送该信号的一个实例。在如下 shell 会话示例中,就使用了最后一个参数向目标进程发送一个 SIGINT 信号,发送该信号的目的将在稍后揭晓。

程序清单 20-7 中程序则被设计为去捕获程序清单 20-6 程序所发送的信号并汇总其统计数据。该程序执行了以下步骤。

1.该程序建立了单个处理器程序来捕获所有信号。 (捕获 SIGKILL 和 SIGSTOP 信号是不可能的,不过将忽略在尝试为这些信号建立处理器时所发生的错误。 )对于大多数类型的信号,处理器程序只是简单地使用一个数组来对信号计数。如果收到的信号为SIGINT,那么处理器程序将对标志( gotSigint)置位,从而使程序退出主循环(下面所描述的 while 循环)。(至于 volatile 修饰符以及声明 gotSigint 变量的 sig_atomic_t 数据 类型,将在 21.1.3 节中解释其用途。 )

2.如果提供有一个命令行参数给程序,那么程序对所有信号的阻塞秒数将由该参数指 定,并且在解除阻塞之前会显示待处理的信号集,从而使用户在进程执行下面的步骤前向其发送信号。 3.程序执行 while 循环以消耗 CPU 时间,直至将 gotSigint 标志置位。 ( 20.14 节和 22.9节描述了 pause()和 sigsuspend()的用法,二者在等待信号到来期间对 CPU 的使用方式都颇为高效。 ) 4.退出 while 循环后,程序显示对所有接收信号的计数。 首先使用这两个程序来展示的是遭阻塞的信号无论产生了多少次,仅会传递一次。这里 为接收者指定了一个睡眠间隔,并在醒来之前发送所有信号。 发送程序的命令行参数指定了 SIGUSR1 和 SIGINT 信号,其在 Linux/x86 中的编号分别为 10 和 2。 从以上输出可知,即使一个信号发送了一百万次,但仅会传递一次给接收者。即使进程没有阻塞信号,其所收到的信号也可能比发送给它的要少得多。如果信号发送速度如此之快,以至于在内核考虑将执行权调度给接收进程之前,这些信号就已经到达,这时就会发生上述情况,从而导致多次发送的信号在进程等待信号集中只记录了一次。如果不带任何命令行参数来执行程序清单 20-7 中程序(因此,进程没有阻塞信号,也没有睡眠),那么将看到如下情况

在所发送的一百万次信号之中,接收进程仅捕获到 52 次.之所以如此,原因在于,发送程序会在每次获得调度而运行时发送多个信号给接收者。然而,当接收进程得以运行时,传递来的信号只有一个,因为只会将这些信号中的一个标记为等待状态。

20.13 改变信号处置: sigaction ()

除去 signal()之外, sigaction()系统调用是设置信号处置的另一选择。虽然 sigaction()的用法比之 signal()更为复杂,但作为回报,也更具灵活性。尤其是, sigaction()允许在获取信号处置的同时无需将其改变,并且,还可设置各种属性对调用信号处理器程序时的行为施以更加精准的控制。此外,如 22.7 节所述,在建立信号处理器程序时, sigaction()较之 signal()函数可移植性更佳。

#include<signal.h>
int sigaction(int sig,const struct sigaction *act,struct sigaction *oldact)

sig 参数标识想要获取或改变的信号编号。该参数可以是除去 SIGKILL 和 SIGSTOP 之外的任何信号。 act 参数是一枚指针,指向描述信号新处置的数据结构。如果仅对信号的现有处置感兴趣,那么可将该参数指定为 NULL。 oldact 参数是指向同一结构类型的指针,用来返回之前信号处置的相关信息。如果无意获取此类信息,那么可将该参数指定为 NULL。 act 和 oldact 所指向的结构类型如下所示:

struct sigaction{
   void (*sa_handler)(int);
   sigset_t sa_mask;
   int sa_flags;
   void (*sa_restorer)(void);
};

sa_handler 字段对应于 signal()的 handler 参数。其所指定的值为信号处理器函数的地址,亦或是常量SIG_IGN、 SIG_DFL 之一。

仅当 sa_handler 是信号处理程序的地址时,亦即 sa_handler的取值在 SIG_IGN 和 SIG_DFL 之外,才会对 sa_mask 和 sa_flags 字段(稍后讨论)加以处理。 余下的字段 sa_restorer,则不适用于应用程序( SUSv3 未予规定)。

sa_mask 字段定义了一组信号,在调用由 sa_handler 所定义的处理器程序时将阻塞该组信号。当调用信号处理器程序时,会在调用信号处理器之前,将该组信号中当前未处于进程掩码之列的任何信号自动添加到进程掩码中。这些信号将保留在进程掩码中,直至信号处理器函数返回,届时将自动删除这些信号。利用 sa_mask 字段可指定一组信号,不允许它们中断此处理器程序的执行。此外,引发对处理器程序调用的信号将自动添加到进程信号掩码中。 这意味着,当正在执行处理器程序时,如果同一个信号实例第二次抵达,信号处理器程序将不会递归中断自己。由于不会对遭阻塞的信号进行排队处理,如果在处理器程序执行过程中重复产生这些信号中的任何信号, (稍后)对信号的传递将是一次性的。 sa_flags 字段是一个位掩码,指定用于控制信号处理过程的各种选项。该字段包含的位如下(可以相或( |))。

SA_NOCLDSTOP 若 sig 为 SIGCHLD 信号,则当因接受一信号而停止或恢复某一子进程时,将不会产生此信 号。参见 26.3.2 节。

SA_NOCLDWAIT (始于 Linux 2.6)若 sig 为 SIGCHLD 信号,则当子进程终止时不会将其转化为僵尸。更多细节参见 26.3.3 节。

SA_NODEFER 捕获该信号时, 不会在执行处理器程序时将该信号自动添加到进程掩码中。 SA_NOMASK历史上曾是 SA_NODEFER 的代名词。之所以建议使用后者,是因为 SUSv3 将其纳入规范。

SA_ONSTACK 针对此信号调用处理器函数时,使用了由 sigaltstack()安装的备选栈。参见 21.3 节。

SA_RESETHAND 当捕获该信号时,会在调用处理器函数之前将信号处置重置为默认值(即 SIG_DFL)(默认情况下,信号处理器函数保持建立状态,直至进一步调用 sigaction()将其显式解除。 )

SA_ONESHOT 历史上曾是 SA_RESETHAND 的代名词,之所以建议使用后者,是因为 SUSv3 将其纳入规范。

SA_RESTART 自动重启由信号处理器程序中断的系统调用。参见 21.5 节。

SA_SIGINFO 调用信号处理器程序时携带了额外参数,其中提供了关于信号的深入信息。对该标志的描述参见 21.4 节。 SUSv3 定义了上述所有选项。 程序清单 21-1 展现了对 sigaction()的使用。

20.14 等待信号: pause()

调用 pause()将暂停进程的执行,直至信号处理器函数中断该调用为止(或者直至一个未处理信号终止进程为止)。

#include<unistd.h>
int pause(void);

处理信号时, pause()遭到中断,并总是返回-1,并将 errno 置为 EINTR。 ( 21.5 节描述了关于 EINTR 错误的更多信息。 ) 程序清单 20-2 提供了应用 pause()的一例子。 在 22.9 节、 22.10 节及 22.11 节中,可以看到程序等待信号时暂停执行的各种其他方式

原文地址:https://www.cnblogs.com/wangbin2188/p/14691062.html