tlpi_hdr.h头文件使用及设置
liao__ran
于 2020-09-29 15:12:01 发布
阅读量1.6k
收藏 5
点赞数 1
分类专栏: linux系统编程手册
版权
linux系统编程手册 专栏收录该内容
7 篇文章 1 订阅
订阅专栏
使用的头文件,主要如下:
ename.c.inc
error_functions.h
get_num.h
tlpi_hdr.h
其中ename.c.inc文件定义了一个字符串数组,用于对应错误码的名称。
error_functions.h文件声明了本书自定义的错误处理函数。
get_num.h文件声明了本书自定义的数值提取函数。
tlpi_hdr.h文件则包含了后续需用到的系统调用头文件。
将头文件复制到默认的编译器寻找目录下,并将实现打包成静态库,然后使用别名来默认链接静态库。
具体操作如下:
第一步:下载所需源码文件,若无法下载,则可将网址放置于浏览器中下载,然后再传入主机中
[root@localhost ~]# wget http://man7.org/tlpi/code/download/tlpi-161214-dist.tar.gz
第二步:解压后,make编译
[root@localhost ~]# tar -zxvf tlpi-161214-dist.tar.gz
[root@localhost ~]# cd tlpi-dist/
[root@localhost ~]# make -j
第三步:拷贝头文件至系统目录
[root@localhost ~]# cd lib/
[root@localhost ~]# cp tlpi_hdr.h /usr/local/include/
[root@localhost ~]# cp get_num.h /usr/local/include/
[root@localhost ~]# cp error_functions.h /usr/local/include/
[root@localhost ~]# cp ename.c.inc /usr/local/include/
第四步:制作静态库文件
[root@localhost ~]# g++ -c get_num.c error_functions.c
[root@localhost ~]# ar -crv libtlpi.a get_num.o error_functions.o
[root@localhost ~]# cp libtlpi.a /usr/local/lib
第五步:简化编译命令,设置别名
[root@localhost ~]# vi /root/.bashrc
alias gl++='new() { g++ $1 -ltlpi;}; new'
[root@localhost ~]# source /root/.bashrc
编译文件格式示例:
[root@localhost ~]# gl++ test.c
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/liao__ran/article/details/108868892
20.1 概念和概述
信号是事件发生时对进程的通知机制,软件中断。
信号与硬件中断的相似之处在于打断了程序执行的正常流程,无法预测信号到达的精确时间。
一个具有权限的进程能够向另一进程发送信号。信号,一种同步技术,甚至是进程间通信( IPC)的原始形式。进程也可以向自身发送信号。然而,发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下。
1.硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。
2.用户键入了能够产生信号的终端特殊字符。包括中断(通常是 Control-C)、暂停(通常是 Control-Z)。
3.发生了软件事件。例如,
(1)针对文件描述符的输出变为有效,
(2)调整了终端窗口大小,
(3)定时器到期,
(4)进程执行的 CPU 时间超限,
(5)该进程的某个子进程退出。
针对每个信号,都定义了一个唯一的(小)整数,从 1 开始顺序展开。
<signal.h>以 SIGxxxx形式的符号名对这些整数做了定义。
由于每个信号的实际编号随系统不同而不同,所以在程序中总是使用这些符号名。
信号分为两大类。
第一组用于内核向进程通知事件,构成所谓传统或者标准信号。
Linux 中标准信号的编号范围为 1~31。本章将描述这些标准信号。
另一组信号由实时信号构成,其与标准信号的差异将在 22.8 节中描述。
信号因某些事件而产生。
(1)信号产生后,会于稍后被传递给某一进程,而进程也会采取某些措施来响应信号。
(2)在产生和到达期间,信号处于等待( pending)状态。
(3)(内核)接下来要调度该进程运行,等待信号会马上送达,或者如果进程正在运行,则会立即传递信号。然而,有时需要确保一段代码不为传递来的信号所中断。
(4)将信号添加到进程的信号掩码中-----目前会阻塞该组信号的到达。如果所产生的信号属于阻塞之列,那么信号将保持等待状态,直至稍后对其解除阻塞(从信号掩码中移除)。
(5)进程可使用各种系统调用对其信号掩码添加和移除信号。信号到达后,进程视具体信号执行如下默认操作之一。
①忽略信号:也就是说,内核将信号丢弃,信号对进程没有产生任何影响
②终止(杀死)进程:这有时是指进程异常终止,而不是进程因调用 exit()而发生的正常终止。
③产生核心转储文件,同时进程终止:核心转储文件包含对进程虚拟内存的镜像,可将其加载到调试器中以检查进程终止时的状态。
④停止进程:暂停进程的执行。
⑤于之前暂停后再度恢复进程的执行。
除了根据特定信号而采取默认行为之外,程序也能改变信号到达时的响应行为。也称之为对信号的处置( disposition)设置。程序可以将对信号的处置设置为如下之一。
1.采取默认行为。这适用于撤销之前对信号处置的修改、恢复其默认处置的场景。
2.忽略信号。这适用于默认行为为终止进程的信号。
3.执行信号处理器程序。 信号处理器程序是由程序员编写的函数,用于为响应传递来的信号而执行适当任务。
通知内核应当去调用某一处理器程序的行为,通常称之为安装或者建立信号处理器程序。
调用信号处理器程序以响应传递来的信号,则称之为信号已处理(handled),或者已捕获(caught)。
请注意,无法将信号处置设置为终止进程或者转储核心。 效果最为近似的是为信号安装一个处理器程序,并于其中调用 exit()或者 abort()。
20.2 信号类型和默认行为
Linux 于 signal(7)手册页中列出的信号名称却超出了 31 个。介绍了各种信号。
SIGABRT 当进程调用 abort()函数(21.2.2 节)时,系统向进程发送该信号。默认情况下,该信号会终止进程,并产生核心转储文件。这实现了调用 abort()的预期目标,产生核心转储文件用于调试。
SIGALRM 经调用 alarm()或 setitimer()而设置的实时定时器一旦到期,内核将产生该信号。实时定时器是根据挂钟时间进行计时的(即人类对逝去时间的概念)。
SIGBUS 产生该信号(总线错误, bus error)即表示发生了某种内存访问错误。如 49.4.3 节所述,当使用由 mmap()所创建的内存映射时,如果试图访问的地址超出了底层内存映射文件的结尾,那么将产生该错误。
SIGCHLD 当父进程的某一子进程终止(或者因为调用了 exit(),或者因为被信号杀死)时, (内核)将向父进程发送该信号。当父进程的某一子进程因收到信号而停止或恢复时,也可能会向父进程发送该信号。
SIGCLD 与 SIGCHLD 信号同义。
SIGCONT 将该信号发送给已停止的进程,进程将会恢复运行(即在之后某个时间点重新获得调度)。当接收信号的进程当前不处于停止状态时,默认情况下将忽略该信号。进程可以捕获该信号,以便在恢复运行时可以执行某些操作。
SIGEMT UNIX 系统通常用该信号来标识一个依赖于实现的硬件错误。 Linux 系统仅在 Sun SPARC 实现中使用了该信号。后缀 EMT 源自仿真器陷阱( emulator trap), Digital PDP-11 的汇编程序助记符之一。
SIGFPE 该信号因特定类型的算术错误而产生,比如除以 0。后缀 FPE 是浮点异常的缩写,不过整型算术错误也能产生该信号。该信号于何时产生的精确细节取决于硬件架构和对 CPU 控制寄存器的设置。
SIGHUP 当终端断开(挂机)时,将发送该信号给终端控制进程。 SIGHUP 信号还可用于守护进程(比如, init、 httpd 和 inetd)。许多守护进程会在收到 SIGHUP 信号时重新进行初始化并重读配置文件。借助于显式执行kill命令或者运行同等功效的程序或脚本,系统管理员可向守护进程手工发送 SIGHUP 信号来触发这些行为。
SIGILL 如果进程试图执行非法的机器语言指令,系统将向进程发送该信号。
SIGINFO 在 Linux 中,该信号名与 SIGPWR 信号名同义。在 BSD 系统中,键入 Control-T 可产生SIGINFO 信号,用于获取前台进程组的状态信息。
SIGINT 当用户键入终端中断字符(通常为 Control-C)时,终端驱动程序将发送该信号给前台进程组。该信号的默认行为是终止进程。
SIGIO 利用 fcntl()系统调用,即可于特定类型(诸如终端和套接字)的打开文件描述符发生 I/O事件时产生该信号。
SIGIOT 在 Linux 中,该信号名与 SIGABRT 信号同义。在其他一些 UNIX 实现中,该信号表示发生了由实现定义的硬件错误。
SIGKILL 此信号为“必杀( sure kill)”信号,处理器程序无法将其阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。
SIGLOST Linux 中存在该信号名,但并未加以使用。在其他一些 UNIX 实现中,如果远端 NFS 服务器在崩溃之后重新恢复,而 NFS 客户端却未能重新获得由本地进程所持有的锁,那么NFS客户端将向这些进程发送此信号。
SIGPIPE 当某一进程试图向管道、 FIFO 或套接字写入信息时,如果这些设备并无相应的阅读进程,那么系统将产生该信号。之所以如此,通常是因为阅读进程已经关闭了其作为 IPC 通道的文件描述符。
SIGPOLL 该信号从 System V 派生而来,与 Linux 中的 SIGIO 信号同义。
SIGPROF 由 setitimer()调用所设置的性能分析定时器刚一过期,内核就将产生该信号。性能分析定时器用于记录进程所使用的 CPU 时间。与虚拟定时器不同,性能分析定时器在对 CPU 时间计数时会将用户态与内核态都包含在内。
SIGPWR 这是电源故障信号。当系统配备有不间断电源( UPS)时,可以设置守护进程来监控电源发生故障时备用电池的剩余电量。如果电池电量行将耗尽(长时间停电之后),那么监控进程会将该信号发往 init 进程,而后者则将其解读为快速、有序关闭系统的一个请求。
SIGQUIT 当用户在键盘上键入退出字符(通常为 Control-\)时,该信号将发往前台进程组。默认情况下,该信号终止进程,并生成可用于调试的核心转储文件。进程如果陷入无限循环,或者不再响应时,使用 SIGQUIT 信号就很合适。键入 Control-\,再调用 gdb 调试器加载刚才生成的核心转储文件,接着用 backtrace 命令来获取堆栈跟踪信息,就能发现正在执行的是程序的哪部分代码。
SIGSEGV 这一信号非常常见,当应用程序对内存的引用无效时,就会产生该信号。引起对内存无效引用的原因很多,可能是因为要引用的页不存在(例如,该页位于堆和栈之间的未映射区域),或者进程试图更新只读内存(比如,程序文本段或者标记为只读的一块映射内存区域)中某一位置的内容,又或者进程企图在用户态(参见 2.1 节)去访问内核的部分内存。 C 语言中引发这些事件的往往是解引用的指针里包含了错误地址(例如,未初始化的指针),或者传递了一个无效参数供函数调用。该信号的命名源于术语“段违例”。
SIGSTKFLT signal(7)手册页中将其记载为“协处理器栈错误”, Linux 对该信号作了定义,但并未加以使用。
SIGSTOP 这是一个必停( sure stop)信号,处理器程序无法将其阻塞、忽略或者捕获,故而总是能停止进程。
SIGSYS 如果进程发起的系统调用有误,那么将产生该信号。这意味着系统将进程执行的指令视为一个系统调用陷阱( trap),但相关的系统调用编号却是无效的(参见 3.1 节)。
SIGTERM 这是用来终止进程的标准信号,也是 kill 和 killall 命令所发送的默认信号。用户有时会使用 kill-KILL 或者 kill-9 显式向进程发送 SIGKILL 信号。然而,这一做法通常是错误的。精心设计的应用程序应当为 SIGTERM 信号设置处理器程序,以便于其能够预先清除临时文件和释放其他资源,从而全身而退。发送 SIGKILL 信号可以杀掉某个进程,从而绕开了 SIGTERM信号的处理器程序。因此,总是应该首先尝试使用 SIGTERM 信号来终止进程,而把 SIGKILL 信号作为最后手段,去对付那些不响应 SIGTERM 信号的失控进程。
SIGTRAP 该信号用来实现断点调试功能以及 strace(1)命令所执行的跟踪系统调用功能。
SIGTSTP 这是作业控制的停止信号,当用户在键盘上输入挂起字符(通常是 Control-Z)时,将发送该信号给前台进程组,使其停止运行。
SIGTTIN 在作业控制 shell 下运行时,若后台进程组试图对终端进行 read()操作,终端驱动程序则将向该进程组发送此信号。该信号默认将停止进程。
SIGTTOU 该信号的目的与 SIGTTIN 信号类似,但所针对的是后台作业的终端输出。在作业控制 shell下运行时,如果对终端启用了 TOSTOP(终端输出停止)选项(可能是通过 stty tostop 命令),而某一后台进程组试图对终端进行write()操作,那么终端驱动程序将向该进程组发送 SIGTTOU 信号。该信号默认将停止进程。
SIGUNUSED 顾名思义,该信号没有使用。在 Linux 2.4 及其后续版本中,该信号名在很多架构中与SIGSYS 信号同义。换言之,尽管信号名还保持向后兼容,但信号编号在这些架构中不再处于未使用状态。
SIGURG 系统发送该信号给一个进程,表示套接字上存在紧急数据。
SIGUSR1 该信号和 SIGUSR2 信号供程序员自定义使用。内核绝不会为进程产生这些信号。进程可以使用这些信号来相互通知事件的发生,或是彼此同步。在早期的 UNIX 实现中,这是可供应用随意使用的仅有的两个信号。(实际上,进程间可以相互发送任何信号,但如果内核也为进程产生了同类信号,这两种情况就有可能产生混淆。 )现代 UNIX 实现则提供了很多实时信号,也可用于程序员自定义的目的。
SIGUSR2 参见对 SIGUSR1 信号的描述。
SIGVTALRM 调用 setitimer()(参见 23.1 节)设置的虚拟定时器刚一到期,内核就会产生该信号。虚拟定时器计录的是进程在用户态所使用的 CPU 时间。
SIGWINCH 在窗口环境中,当终端窗口尺寸发生变化时,会向前台进程组发送该信号。借助于为该信号安装的处理器程序,诸如 vi 和 less 之类的程序会在窗口尺寸调整后重新绘制输出。
SIGXCPU 当进程的 CPU 时间超出对应的资源限制时,将发送此信号给进程。
SIGXFSZ 如果进程因试图增大文件(调用 write()或 truncate())而突破对进程文件大小的资源限制时,那么将发送此信号给进程。
20.3 改变信号处置:signal
UNIX 系统提供了两种方法来改变信号处置: signal()和 sigaction()。
signal()系统调用,是设置信号处置的原始 API,所提供的接口比 sigaction()简单。signal()的行为在不同 UNIX 实现间存在差异,这也意味着对可移植性有所追求的程序绝不能使用此调用来建立信号处理器函数。
sigaction()提供了 signal()所不具备的功能。sigaction()是建立信号处理器的首选 API(强力推荐)。自 20.13 节介绍了 sigaction()调用的用法之后,本书示例将一律采用该调用来建立信号处理器程序。
#include<signal.h>
void (*signal(int sig,void (*handler)(int)))(int)
这里需要对 signal()函数的原型做一些解释。第一个参数 sig,标识希望修改处置的信号编号,第二个参数 handler,则标识信号抵达时所调用函数的地址。该函数无返回值(void),并接收一个整型参数。因此,信号处理器函数一般具有以下形式:
void handler(int sig){
/**/
}
signal()的返回值是之前的信号处置。像 handler 参数一样,这是一枚指针,所指向的是带有一个整型参数且无返回值的函数。换言之,编写如下代码,可以暂时为信号建立一个处理器函数,然后再将信号处置重置为其本来面目
void (*oldhandler)(int);
oldhandler=signal(SIGINT,newHandler);
if(oldhandler==SIGERR)
errExit(“signal”)
/**/
if(signal(SIGINT,newHandler)==SIG_ERR)
errExit(“signal”)
在为 signal()指定 handler 参数时,可以以如下值来代替函数地址:
SIG_DFL 将信号处置重置为默认值(表 20-1)。这适用于将之前 signal()调用所改变的信号处置还原。
SIG_IGN 忽略该信号。如果信号专为此进程而生,那么内核会默默将其丢弃。进程甚至从未知道曾经产生了该信号。 调用 signal()成功将返回先前的信号处置,有可能是先前安装的处理器函数地址,也可能是常量 SIG_DFL 和 SIG_IGN 之一。如果调用失败, signal()将返回 SIG_ERR
20.4 信号处理器简介
信号处理器程序(也称为信号捕捉器)是当指定信号传递给进程时将会调用的一个函数。本节描述了信号处理器的基本原理。 调用信号处理器程序,可能会随时打断主程序流程;内核代表进程来调用处理器程序,当处理器返回时,主程序会在处理器打断的位置恢复执行。这一工作序列可用图 20-1 来加以说明。
虽然信号处理器程序几乎可以为所欲为,但一般而言,设计应力求简单。
#include <signal.h>
#include "tlpi_hdr.h"
static void
sigHandler(int sig){
printf("Ouch!\n"); /* UNSAFE (see Section 21.1.2) */
}
int
main(int argc, char *argv[]){
int j;
if (signal(SIGINT, sigHandler) == SIG_ERR)
errExit("signal");
/* Loop continuously waiting for signals to be delivered */
for (j = 0; ; j++) {
printf("%d\n", j);
sleep(3); /* Loop slowly... */
}
}
(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/signals$ ./ouch
0
1
2
^COuch!
3
4
5
^COuch!
6
7
8
9
10
^Z
为一个简单的信号处理器函数,由主程序为 SIGINT 信号而建立。当键入中断字符(通常为 Control-C)时,终端驱动程序将产生该信号。处理器只是简单打印一条消息,随即返回。 主程序会持续循环。每次迭代,程序都将递增计数器值并将其打印出来,然后休眠几秒钟。
#include <signal.h>
#include "tlpi_hdr.h"
static void
sigHandler(int sig)
{
static int count = 0;
if (sig == SIGINT) {
count++;
printf("Caught SIGINT (%d)\n", count);
return; /* Resume execution at point of interruption */
}
/* Must be SIGQUIT - print a message and terminate the process */
printf("Caught SIGQUIT - that's all folks!\n");
exit(EXIT_SUCCESS);
}
int
main(int argc, char *argv[])
{
if (signal(SIGINT, sigHandler) == SIG_ERR)
errExit("signal");
if (signal(SIGQUIT, sigHandler) == SIG_ERR)
errExit("signal");
for (;;) /* Loop forever, waiting for signals */
pause(); /* Block until a signal is caught */
}
(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/signals$ ./intquit
^CCaught SIGINT (1)
^CCaught SIGINT (2)
^CCaught SIGINT (3)
^\Caught SIGQUIT - that’s all folks!
内核在调用信号处理器程序时,会将引发调用的信号编号作为一个整型参数传递给处理器函数。如果信号处理器程序只捕获一种类型的信号,那么这个参数几乎无用。然而,如果安装相同的处理器来捕获不同类型的信号,那么就可以利用此参数来判定引发对处理器调用的是何种信号。 程序清单 20-2 中程序展示了这一思路,为 SIGINT 和 SIGQUIT 信号建立了同一处理器程序。 (当键入终端退出字符时,通常为 Control-\,终端驱动程序将产生 SIGQUIT 信号。 )处理器程序代码通过检查 sig 参数来区分这两种信号,并为每种信号采取不同措施。 main()函数则使用 pause()函数(参见 20.14 节的描述)来阻塞进程,直至捕获到信号。
20.5 发送信号: kill()
与 shell 的 kill 命令相类似,一个进程能够使用 kill()系统调用向另一进程发送信号。 (之所以选择 kill 作为术语,是因为早期 UNIX 实现中大多数信号的默认行为是终止进程。 )
#include<signal.h>
int kill(pid_t pid,int sig);
pid 参数标识一个或多个目标进程,而 sig 则指定了要发送的信号。
如何解释 pid,要视以下 4 种情况而定。
1.如果 pid 大于 0,那么会发送信号给由 pid 指定的进程。
2.如果 pid 等于 0,那么会发送信号给与调用进程同组的每个进程,包括调用进程自身。
3.如果 pid 小于-1,那么会向组 ID 等于该 pid 绝对值的进程组内所有下属进程发送信号。向一个进程组的所有进程发送信号在 shell 作业控制中有特殊用途
4.如果 pid 等于-1,那么信号的发送范围是: 调用进程有权将信号发往的每个目标进程,除去 init(进程 ID 为 1)和调用进程自身。如果特权级进程发起这一调用,那么会发送信号给系统中的所有进程,上述两个进程除外。显而易见,有时也将这种信号发送方式称之为广播信号。 如果并无进程与指定的 pid 相匹配,那么 kill()调用失败,同时将 errno 置为 ESRCH(“查无此进程”)。 进程要发送信号给另一进程,还需要适当的权限,其权限规则如下。
5.特权级( CAP_KILL)进程可以向任何进程发送信号。
6.以 root 用户和组运行的 init 进程(进程号为 1),是一种特例,仅能接收已安装了处理器函数的信号。这可以防止系统管理员意外杀死 init 进程—这一系统运作的基石。
7.如果发送者的实际或有效用户 ID 匹配于接受者的实际用户 ID 或者保存设置用户 ID(saved set-user-id),那么非特权进程也可以向另一进程发送信号。利用这一规则,用户可以向由他们启动的 set-user-ID 程序发送信号,而无需考虑目标进程有效用户 ID 的当前设置。将目标进程有效用户 ID 排除在检查范围之外,这一举措的辅助作用在于防止用户某甲向用户某乙的进程发送信号,而该进程正在执行的 set-user-ID程序又属于用户某甲。
8.SIGCONT 信号需要特殊处理。无论对用户 ID 的检查结果如何,非特权进程可以向同一会话中的任何其他进程发送这一信号。利用这一规则,运行作业控制的 shell 可以重启已停止的作业(进程组),即使作业进程已经修改了它们的用户 ID。 (亦即,使用9.7 节所述系统调用来改变其凭据,进而成为特权级进程。 )
如果进程无权发送信号给所请求的 pid,那么 kill()调用将失败,且将 errno 置为 EPERM。若 pid所指为一系列进程(即 pid 是负值)时,只要可以向其中之一发送信号,则 kill()调用成功。 程序清单 20-3 中展示了 kill()的用法
20.6 检查进程的存在
kill()系统调用还有另一重功用。
若将参数 sig 指定为 0(即所谓空信号),则无信号发送。
kill()仅会去执行错误检查,查看是否可以向目标进程发送信号。
可以使用空信号来检测具有特定进程 ID 的进程是否存在。
1.若发送空信号失败,且 errno为 ESRCH,则表明目标进程不存在。
2.如果调用失败,且 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)调用会发送一个信号给调用进程, 并可将该信号传递给该进程的任一线程。
#include <signal.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int s, sig;
if (argc != 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s pid sig-num\n", argv[0]);
sig = getInt(argv[2], 0, "sig-num");
s = kill(getLong(argv[1], 0, "pid"), sig);
if (sig != 0) {
if (s == -1)
errExit("kill");
} else { /* Null signal: process existence check */
if (s == 0) {
printf("Process exists and we can send it a signal\n");
} else {
if (errno == EPERM)
printf("Process exists, but we don't have "
"permission to send it a signal\n");
else if (errno == ESRCH)
printf("Process does not exist\n");
else
errExit("kill");
}
}
exit(EXIT_SUCCESS);
}
(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/signals$ ./t_kill
Usage: ./t_kill pid sig-num
当进程使用 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()函数也对本地设置敏感。
20.9 信号集
许多信号相关的系统调用都需要能表示一组不同的信号。
sigaction()和 sigprocmask()允许程序指定一组将由进程阻塞的信号,
sigpending()则返回一组目前正在等待送达给一进程的信号。
多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
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 的机制来表示空信号集的作法在可移植性上存在问题,因为有可能使用位掩码之外的结构来实现信号集。
信号集初始化后,可以分别使用 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 不对信号进行排队处理
等待信号集只是一个掩码,仅表明一个信号是否发生,而未表明其发生的次数。换言之,如果同一信号在阻塞状态下产生多次,那么会将该信号记录在等待信号集中,并在稍后仅传递一次。
程序可接受多达四个命令行参数,如下所示:
第一个参数是程序发送信号的目标进程 ID。
第二个参数则指定发送给目标进程的信号数量。
第三个参数指定发往目标进程的信号编号。
如果还提供了一个信号编号作为第四个参数,那么当程序发送完之前参数所指定的信号之后,将发送该信号的一个实例。在如下 shell 会话示例中,就使用了最后一个参数向目标进程发送一个 SIGINT 信号,发送该信号的目的将在稍后揭晓。
#include <signal.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[]){
int numSigs, sig, j;
pid_t pid;
if (argc < 4 || strcmp(argv[1], "--help") == 0)
usageErr("%s pid num-sigs sig-num [sig-num-2]\n", argv[0]);
pid = getLong(argv[1], 0, "PID");
numSigs = getInt(argv[2], GN_GT_0, "num-sigs");
sig = getInt(argv[3], 0, "sig-num");
/* Send signals to receiver */
printf("%s: sending signal %d to process %ld %d times\n",
argv[0], sig, (long) pid, numSigs);
for (j = 0; j < numSigs; j++)
if (kill(pid, sig) == -1)
errExit("kill");
/* If a fourth command-line argument was specified, send that signal */
if (argc > 4)
if (kill(pid, getInt(argv[4], 0, "sig-num-2")) == -1)
errExit("kill");
printf("%s: exiting\n", argv[0]);
exit(EXIT_SUCCESS);
}
(base) wannian07@wannian07-PC:~/Desktop/std/linux_prog_interface$ gcc sig_sender.c -o sig_sender error_functions.c get_num.c
(base) wannian07@wannian07-PC:~/Desktop/std/linux_prog_interface$ ./sig_sender
Usage: ./sig_sender pid num-sigs sig-num [sig-num-2]
sig_receivce.c
#define _GNU_SOURCE
#include <signal.h>
#include "signal_functions.h" /* Declaration of printSigset() */
#include "tlpi_hdr.h"
static int sigCnt[NSIG]; /* Counts deliveries of each signal */
static volatile sig_atomic_t gotSigint = 0;
/* Set nonzero if SIGINT is delivered */
static void
handler(int sig){
if (sig == SIGINT)
gotSigint = 1;
else
sigCnt[sig]++;
}
int
main(int argc, char *argv[]){
int n, numSecs;
sigset_t pendingMask, blockingMask, emptyMask;
printf("%s: PID is %ld\n", argv[0], (long) getpid());
for (n = 1; n < NSIG; n++) /* Same handler for all signals */
(void) signal(n, handler); /* Ignore errors */
if (argc > 1) {
numSecs = getInt(argv[1], GN_GT_0, NULL);
sigfillset(&blockingMask);
if (sigprocmask(SIG_SETMASK, &blockingMask, NULL) == -1)
errExit("sigprocmask");
printf("%s: sleeping for %d seconds\n", argv[0], numSecs);
sleep(numSecs);
if (sigpending(&pendingMask) == -1)
errExit("sigpending");
printf("%s: pending signals are: \n", argv[0]);
printSigset(stdout, "\t\t", &pendingMask);
sigemptyset(&emptyMask); /* Unblock all signals */
if (sigprocmask(SIG_SETMASK, &emptyMask, NULL) == -1)
errExit("sigprocmask");
}
while (!gotSigint) /* Loop until SIGINT caught */
continue;
for (n = 1; n < NSIG; n++) /* Display number of signals received */
if (sigCnt[n] != 0)
printf("%s: signal %d caught %d time%s\n", argv[0], n,
sigCnt[n], (sigCnt[n] == 1) ? "" : "s");
exit(EXIT_SUCCESS);
}
(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/signals$ ./sig_receiver 15 &
[2] 5608
(base) wannian07@wannian07-PC:~/Desktop/std/tlpi-dist/signals$ ./sig_receiver: PID is 5608
./sig_receiver: sleeping for 15 seconds
./sig_receiver: pending signals are:
./sig_sender 5608 10000000 10 2
./sig_sender: sending signal 10 to process 5608 10000000 times
./sig_sender: exiting
./sig_receiver: signal 10 caught 1633011 times
[2]- 已完成 ./sig_receiver 15
程序清单 20-7 中程序则被设计为去捕获程序清单 20-6 程序所发送的信号并汇总其统计数据。该程序执行了以下步骤。
1.该程序建立了单个处理器程序来捕获所有信号。 (捕获 SIGKILL 和 SIGSTOP 信号是不可能的,不过将忽略在尝试为这些信号建立处理器时所发生的错误。 )对于大多数类型的信号,处理器程序只是简单地使用一个数组来对信号计数。如果收到的信号为SIGINT,那么处理器程序将对标志( gotSigint)置位,从而使程序退出主循环。
2.如果提供有一个命令行参数给程序,那么程序对所有信号的阻塞秒数将由该参数指 定,并且在解除阻塞之前会显示待处理的信号集,从而使用户在进程执行下面的步骤前向其发送信号。
3.程序执行 while 循环以消耗 CPU 时间,直至将 gotSigint 标志置位。 ( 20.14 节和 22.9节描述了 pause()和 sigsuspend()的用法,二者在等待信号到来期间对 CPU 的使用方式都颇为高效。 )
4.退出 while 循环后,程序显示对所有接收信号的计数。
首先使用这两个程序来展示的是遭阻塞的信号无论产生了多少次,仅会传递一次。
这里 为接收者指定了一个睡眠间隔,并在醒来之前发送所有信号。 发送程序的命令行参数指定了 SIGUSR1 和 SIGINT 信号,其在 Linux/x86 中的编号分别为 10 和 2。
从以上输出可知,即使一个信号发送了一百万次,但仅会传递一次给接收者。即使进程没有阻塞信号,其所收到的信号也可能比发送给它的要少得多。如果信号发送速度如此之快,以至于在内核考虑将执行权调度给接收进程之前,这些信号就已经到达,这时就会发生上述情况,从而导致多次发送的信号在进程等待信号集中只记录了一次。如果不带任何命令行参数来执行程序清单 20-7 中程序(因此,进程没有阻塞信号,也没有睡眠),那么将看到如下情况
在所发送的一百万次信号之中,接收进程仅捕获到 52 次.
(发送的一千万次信号之中,接收进程捕获到 5608 次.) 之所以如此,原因在于,发送程序会在每次获得调度而运行时发送多个信号给接收者。然而,当接收进程得以运行时,传递来的信号只有一个,因为只会将这些信号中的一个标记为等待状态。
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。