一些说明:
1 很高兴有这个机会参与到这项活动中来。
2 由于英语语言的习惯,文中有不少复杂长句,按照字面翻译出来并不适合中国人的阅读习惯。我在保留文章原意的基础上作了一些语言组织上的调整,将大部分长句子组织成中文擅长的短句型,所以你会发现翻译品一部分内容不是按照原文字面翻译的。
3 考虑到该书的入门引导作用,在原文中一些比较少见的词后面我添加了少许译注,希望减少入门兄弟的负担。
4 由于2中的语言调整和3中的译注,是我的个人行为,可能会有我对 原文理解模糊,概念不清,或是语言组织不顺畅的地方,还请大家一定多多指出来。谢谢。
个人声明:
本翻译品受控于chinaunix BSD翻译小组。如需单独转载,请保留“翻译:gvim@chinaunix/bsd”和以上几点说明。
1 很高兴有这个机会参与到这项活动中来。
2 由于英语语言的习惯,文中有不少复杂长句,按照字面翻译出来并不适合中国人的阅读习惯。我在保留文章原意的基础上作了一些语言组织上的调整,将大部分长句子组织成中文擅长的短句型,所以你会发现翻译品一部分内容不是按照原文字面翻译的。
3 考虑到该书的入门引导作用,在原文中一些比较少见的词后面我添加了少许译注,希望减少入门兄弟的负担。
4 由于2中的语言调整和3中的译注,是我的个人行为,可能会有我对 原文理解模糊,概念不清,或是语言组织不顺畅的地方,还请大家一定多多指出来。谢谢。
个人声明:
本翻译品受控于chinaunix BSD翻译小组。如需单独转载,请保留“翻译:gvim@chinaunix/bsd”和以上几点说明。
4.1 高级进程控制和信号
信号:4 a:一种对象,用于传输或负载人类声音之外的信息。
到目前为止,我们已经讨论了进程的创建和其他系统调用。现在是讨论下面这些问题的时候了:你要在多个进程间通讯以获得更好的进程控制粒度,或者要其他程序或操作者用信号通知你的程序。例如,你可能希望你的程序重新读取它的配置文件。或者,你的数据库程序需要在退出之前将事务从主存写入后备存储器,然后再退出。这两个例子可能只是使用信号的很小一部分。虽然已经有套接字,先入先出队列,管道,信号量等多种方式来完成类似的任务,但是我们将把讨论的焦点放在信号和其它进程控制机制上。在现实中,信号和进程控制机制可以提供大部分你所需要的特性和功能。
4.2 信号
信号与硬件中断很相似。当设备需要中断服务的时候它可以产生一个硬件中断来通知CPU。与硬件中断类似的,当进程需要将一些事件通知给其他进程的时候可以使用信号来完成。
大多数Unix系统管理员会比较熟悉SIGHUP信号。当你通过kill命令向后台服务进程发出SIGHUP信号后,大多数进程要么重新读入他们的配置文件要么重新启动。这些信号之中,一些与硬件有直接关系,如SIGFPE(浮点异常),SIGILL(非法指令);其它则是与软件相关,如SIGSYS(未实现的系统调用被调用)。
一旦进程接收到信号之后,该信号的行为与信号本身和进程对它的使用目的两个因素有关。一些信号可以被阻塞,忽略,或者捕获,而另外一些则不可以。如果进程需要捕获一个信号并履行一些相关操作,你可以为进程设定这个特定信号的信号处理句柄。处理句柄仅仅是一个函数,在这个信号被进程接收之后调用。或者更确切的说,处理句柄是一个函数调用,你可以对它进行指派(specify)。
当信号没有指定处理句柄时,将会执行操作系统默认的行为。这些缺省行为可以是从终止进程到完全核心转储等不同的操作。注意,有两个信号不能被捕获或忽略:SIGSTOP和SIGKILL,下面会解释。
在BSD系统中定义的有许多信号;我们讨论在/usr/include/sys/signals.h(译注:在我的FB5.2.1中是signal.h)文件中定义的标准信号。注意, NetBSD系统中定义的信号数量稍微多一点,并且我们没有将它们的讨论放在这里。所以如果需要使用某个下面没有涉及到的信号的时候,请查阅你的系统的头文件。
#define SIGHUP 1 /* hangup */
SIGHUP是Unix系统管理员很常用的一个信号。许多后台服务进程在接受到该信号后将会重新读取它们的配置文件。然而,该信号的实际功能是通知进程它的控制终端被断开。缺省行为是终止进程。
#define SIGINT 2 /* interrupt */
对于Unix使用者来说,SIGINT是另外一个常用的信号。许多shell的CTRL-C组合使得这个信号被大家所熟知。该信号的正式名字是中断信号。缺省行为是终止进程。
#define SIGQUIT 3 /* quit */
SIGQUIT信号被用于接收shell的CTRL-/组合。另外,它还用于告知进程退出。这是一个常用信号,用来通知应用程序从容的(译注:即在结束前执行一些退出动作)关闭。缺省行为是终止进程,并且创建一个核心转储。
#define SIGILL 4 /* illegal instr. (not reset when caught) */
如果正在执行的进程中包含非法指令,操作系统将向该进程发送SIGILL信号。如果你的程序使用了线程,或者pointer functions,那么可能的话可以尝试捕获该信号来协助调试。(注意:原文这句为:“If your program makes use of use of threads, or pointer functions, try to catch this signal if possible for aid in debugging.”。中间的两个use of use of,不知是原书排版的瑕疵还是我确实没有明白其意义;另外,偶经常听说functions pointer,对于pointer functions,google了一下,应该是fortran里面的东西,不管怎样,还真不知道,确切含义还请知道的兄弟斧正。)缺省行为是终止进程,并且创建一个核心转储。
#define SIGTRAP 5 /* trace trap (not reset when caught) */
SIGTRAP这个信号是由POSIX标准定义的,用于调试目的。当被调试进程接收到该信号时,就意味着它到达了某一个调试断点。一旦这个信号被交付,被调试的进程就会停止,并且它的父进程将接到通知。缺省行为是终止进程,并且创建一个核心转储。
#define SIGABRT 6 /* abort() */
SIGABRT提供了一种在异常终止(abort)一个进程的同时创建一个核心转储的方法。然而如果该信号被捕获,并且信号处理句柄没有返回,那么进程不会终止。缺省行为是终止进程,并且创建一个核心转储。
#define SIGFPE 8 /* floating point exception */
当进程发生一个浮点错误时,SIGFPE信号被发送给该进程。对于那些处理复杂数学运算的程序,一般会建议你捕获该信号。缺省行为是终止进程,并且创建一个核心转储。
#define SIGKILL 9 /* kill (cannot be caught or ignored) */
SIGKILL是这些信号中最难对付的一个。正如你在它旁边的注释中看到的那样,这个信号不能被捕获或忽略。一旦该信号被交付给一个进程,那么这个进程就会终止。然而,会有一些极少数情况SIGKILL不会终止进程。这些罕见的情形在处理一个“非中断操作”(比如磁盘I/O)的时候发生。虽然这样的情形极少发生,然而一旦发生的话,会造成进程死锁。唯一结束进程的办法就只有重新启动了。缺省行为是终止进程。
#define SIGBUS 10 /* bus error */
如同它的名字暗示的那样,CPU检测到数据总线上的错误时将产生SIGBUS信号。当程序尝试去访问一个没有正确对齐的内存地址时就会产生该信号。缺省行为是终止进程,并且创建一个核心转储。
#define SIGSEGV 11 /* segmentation violation */
SIGSEGV是另一个C/C++程序员很熟悉的信号。当程序没有权利访问一个受保护的内存地址时,或者访问无效的虚拟内存地址(脏指针,dirty pointers,译注:由于没有和后备存储器中内容进行同步而造成。关于野指针,可以参见http://en.wikipedia.org/wiki/Wild_pointer 的解释。)时,会产生这个信号。缺省行为是终止进程,并且创建一个核心转储。
#define SIGSYS 12 /* non-existent system call invoked */
SIGSYS信号会在进程执行一个不存在的系统调用时被交付。操作系统会交付该信号,并且进程会被终止。缺省行为是终止进程,并且创建一个核心转储。
#define SIGPIPE 13 /* write on a pipe with no one to read it */
管道的作用就像电话一样,允许进程之间的通信。如果进程尝试对管道执行写操作,然而管道的另一边却没有回应者时,操作系统会将SIGPIPE信号交付给这个讨厌的进程(这里就是那个打算写入的进程)。缺省行为是终止进程。
#define SIGALRM 14 /* alarm clock */
在进程的计时器到期的时候,SIGALRM信号会被交付(delivered)给进程。这些计时器由本章后面将会提及的setitimer和alarm调用设置。缺省行为是终止进程。
#define SIGTERM 15 /* software termination signal from kill */
SIGTERM信号被发送给进程,通知该进程是时候终止了,并且在终止之前做一些清理活动。SIGTERM信号是Unix的kill命令发送的缺省信号,同时也是操作系统关闭时向进程发送的缺省信号。缺省行为是终止进程。
#define SIGURG 16 /* urgent condition on IO channel */
在进程已打开的套接字上发生某些情况时,SIGURG将被发送给该进程。如果进程不捕获这个信号的话,那么将被丢弃。缺省行为是丢弃这个信号。
#define SIGSTOP 17 /* sendable stop signal not from tty */
本信号不能被捕获或忽略。一旦进程接收到SIGSTOP信号,它会立即停止(stop),直到接收到另一个SIGCONT信号为止。缺省行为是停止进程,直到接收到一个SIGCONT信号为止。
#define SIGTSTP 18 /* stop signal from tty */
SIGSTP与SIGSTOP类似,它们的区别在于SIGSTP信号可以被捕获或忽略。当shell从键盘接收到CTRL-Z的时候就会交付(deliver)这个信号给进程。缺省行为是停止进程,直到接收到一个SIGCONT信号为止。
#define SIGCONT 19 /* continue a stopped process */
SIGCONT也是一个有意思的信号。如前所述,当进程停止的时候,这个信号用来告诉进程恢复运行。该信号的有趣的地方在于:它不能被忽略或阻塞,但可以被捕获。这样做很有意义:因为进程大概不愿意忽略或阻塞SIGCONT信号,否则,如果进程接收到SIGSTOP或SIGSTP的时候该怎么办?缺省行为是丢弃该信号。
#define SIGCHLD 20 /* to parent on child stop or exit */
SIGCHLD是由Berkeley Unix引入的,并且比SRV 4 Unix上的实现有更好的接口。(如果信号是一个没有追溯能力的过程(not a retroactive process),那么BSD的SIGCHID信号实现会比较好。在system V Unix的实现中,如果进程要求捕获该信号,操作系统会检查是否存在有任何未完成的子进程(这些子进程是已经退出(exit)的子进程,并且在等待调用wait的父进程收集它们的状态)。如果子进程退出的时候附带有一些终止信息(terminating information),那么信号处理句柄就会被调用。所以,仅仅要求捕获这个信号会导致信号处理句柄被调用(译注:即是上面说的“信号的追溯能力”),而这是却一种相当混乱的状况。)
一旦一个进程的子进程状态发生改变,SIGCHLD信号就会被发送给该进程。就像我在前面章节提到的,父进程虽然可以fork出子进程,但没有必要等待子进程退出。一般来说这是不太好的,因为这样的话,一旦进程退出就可能会变成一个僵尸进程。可是如果父进程捕获SIGCHLD信号的话,它就可以使用wait系列调用中的某一个去收集子进程状态,或者判断发生了什么事情。当发送SIGSTOP,SIGSTP或SIGCONF信号给子进程时,SIGCHLD信号也会被发送给父进程。缺省行为是丢弃该信号。
#define SIGTTIN 21 /* to readers pgrp upon background tty read */
当一个后台进程尝试进行一个读操作时,SIGTTIN信号被发送给该进程。进程将会阻塞直到接收到SIGCONT信号为止。缺省行为是停止进程,直到接收到SIGCONT信号。
#define SIGTTOU 22 /* like TTIN if (tp->t_local<OSTOP) */
SIGTTOU信号与SIGTTIN很相似,不同之处在于SIGTTOU信号是由于后台进程尝试对一个设置了TOSTOP属性的tty执行写操作时才会产生。然而,如果tty没有设置这个属性,SIGTTOU就不会被发送。缺省行为是停止进程,直到接收到SIGCONT信号。
#define SIGIO 23 /* input/output possible signal */
如果进程在一个文件描述符上有I/O操作的话,SIGIO信号将被发送给这个进程。进程可以通过fcntl调用来设置。缺省行为是丢弃该信号。
#define SIGXCPU 24 /* exceeded CPU time limit */
如果一旦进程超出了它可以使用的CPU限制(CPU limit),SIGXCPU信号就被发送给它。这个限制可以使用随后讨论的setrlimit设置。缺省行为是终止进程。
#define SIGXFSZ 25 /* exceeded file size limit */
如果一旦进程超出了它可以使用的文件大小限制,SIGXFSZ信号就被发送给它。稍后我们会继续讨论这个信号。缺省行为是终止进程。
#define SIGVTALRM 26 /* virtual time alarm */
如果一旦进程超过了它设定的虚拟计时器计数时,SIGVTALRM信号就被发送给它。缺省行为是终止进程。
#define SIGPROF 27 /* profiling time alarm */
当设置了计时器时,SIGPROF是另一个将会发送给进程的信号。缺省行为是终止进程。
#define SIGWINCH 28 /* window size changes */
当进程调整了终端的行或列时(比如增大你的xterm的尺寸),SIGWINCH信号被发送给该进程。缺省行为是丢弃该信号。
#define SIGUSR1 29 /* user defined signal 1 */
#define SIGUSR2 30 /* user defined signal 2 */
#define SIGUSR2 30 /* user defined signal 2 */
SIGUSR1和SIGUSR2这两个信号被设计为用户指定。它们可以被设定来完成你的任何需要。换句话说,操作系统没有任何行为与这两个信号关联。缺省行为是终止进程。(译注:按原文的意思翻译出来似乎这两句话有点矛盾。)
[size=-1]4.3 系统调用
那么,你该如何使用信号呢?有时候甚至拿不准是否应该使用信号。例如,当信号被交付的时候,一方面你可以在行为发生之前,分析当前情况,找出信号发生的原因,或者找到这些信号是从哪里发出来的;另一方面,其他一些时候你也可以只是希望简单的退出程序,并且在清除之后创建一个核心转储文件。参见最后部分的简单代码可以获得这些函数的较详细的例子。
Kill函数
kill函数对于那些经常在命令行使用kill命令杀死进程的人来说是再熟悉不过的。基本语法是:
int kill(pid_t pid, int sig);
Kill函数将指定的信号发送给进程号为pid的进程。只有当进程符合下面几点情况的时候信号才会被交付:
QUOTE:
注意:SIGCONT信号是一个特例,它可以由当前进程发送给任何一个该进程的派生进程。
使用不同的调用参数使得kill函数的行为差别非常大。这些行为如下所述:
(译注:下面的PID应该指的是上面kill函数原型中的那个pid,我在这里做出说明并保留原文)
QUOTE:
• 如果PID大于0,并且发送进程有适当的权限,那么参数sig指定的信号将被交付。
• 如果PID等于0,那么sig信号将被交付给所有那些与发送进程有相同组ID的进程。(发送进程同样需要满足权限需求。)
• 如果PID是 -1,那么信号将被发送给所有那些与发送进程有相同有效用户ID的进程(不包含发送进程在内)。然而,如果发送进程的有效用户ID与超级用户(root)的相同,那么信号被交付给除了系统进程(由它们的proc结构中的p_flag域是否是P_SYSTEM来定义)之外的所有进程。在这个特殊的例子中,如果某些进程不能被发送(could not be sent)sig信号,kill函数并不返回一个错误。
• 如果sig是0,kill函数只检查错误(例如,无效权限,不存在的进程等)。该用法有时候用来检查一个指定进程是否存在。
• 如果成功的话kill函数返回0,否则返回-1。kill调用失败时会在errno全局变量中设置相应的错误值。
• 如果PID等于0,那么sig信号将被交付给所有那些与发送进程有相同组ID的进程。(发送进程同样需要满足权限需求。)
• 如果PID是 -1,那么信号将被发送给所有那些与发送进程有相同有效用户ID的进程(不包含发送进程在内)。然而,如果发送进程的有效用户ID与超级用户(root)的相同,那么信号被交付给除了系统进程(由它们的proc结构中的p_flag域是否是P_SYSTEM来定义)之外的所有进程。在这个特殊的例子中,如果某些进程不能被发送(could not be sent)sig信号,kill函数并不返回一个错误。
• 如果sig是0,kill函数只检查错误(例如,无效权限,不存在的进程等)。该用法有时候用来检查一个指定进程是否存在。
• 如果成功的话kill函数返回0,否则返回-1。kill调用失败时会在errno全局变量中设置相应的错误值。
kill的另一个版本是raise函数:
int raise(int sig);
raise函数会向当前进程发送sig信号。该函数用处不是很大,因为它只能够给当前进程发送信号。raise函数调用成功时返回0,否则返回-1。调用失败时会在errno全局变量中设置相应的错误值,效果和signal函数的返回类似:(译注:原文只有两个单词“as in:”,我并不知道作者把signal列在这里所要表达的意思,所以我按照我的理解+猜测来翻译的。如果大家有什么建议,或是需要纠正的话,请一定告诉我。)
void (*signal(int sig, void (*func)(int)))(int);
4.4 信号处理
现在我们知道何时会产生信号,也知道如何发送信号,那么我们怎么处理它们呢?
signal函数
signal系统函数调用提供了一种最简单的范例。然而,由于C原形声明的缘故使它看起来比实际复杂。signal函数将一个给定的函数和一个特定的信号联系。这里是FreeBSD中的定义(和一个typedef一起):
QUOTE:
typedef void (*sig_t) (int);
sig_t signal(int sig, sig_t func);
sig_t signal(int sig, sig_t func);
第一个参数是目标信号,可以是上面列举的所有信号中的任何一个。func参数是一个指针,指向某个处理该信号的函数。这个处理信号函数带有一个int型参数,并应返回void。signal函数中的func参数也可以设定为下面的一些值:
QUOTE:
SIG_IGN: 如果func参数被设置为SIG_IGN,该信号将被忽略。
SIG_DFL: 如果func参数被设置为SIG_DFL,该信号会按照确定行为处理。
SIG_DFL: 如果func参数被设置为SIG_DFL,该信号会按照确定行为处理。
sigaction函数
sigaction函数是一个比signal更通用的方案。第一个参数是目标信号。下一个名为act的参数(指向)sigaction结构,该结构包含一些用于信号处理的信息。最后一个参数oact是一个指针,指向一个可以存储上一次设置信号处理的信息的地方。
QUOTE:
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
sigaction结构有下面这些个成员:
QUOTE:
void (*sa_handler)(int);
这个结构成员是一个指向函数的指针,该函数带有一个简单的整形参数,并返回(void)。这与signal函数的func参数相同,也可以被设置为SIG_IGN和SIG_DFL,并且与调用signal得到的效果也一样。
QUOTE:
void (*sa_sigaction)(int, siginfo_t *, void *);
该结构成员是一个指向函数的指针,返回(void)并需要三个参数。这些参数依次为:一个整形参数指定信号发送;一个指向siginfo_t结构的指针用来保存关于信号的信息;最后一个也是一个指针,指向信号交付时的特定上下文(context)空间。
QUOTE:
sigset_t sa_mask;
该结构成员是一个位掩码(bitwise mask),用来指示信号交付时哪些信号会被阻塞。阻塞SIGKILL和SIGSTOP信号的做法会被忽略。接下来,被阻塞的信号将被推迟,直到它们被开启(unblock)。参见sigprocmask获得更多关于全局掩码(global masks)的信息。
QUOTE:
int sa_flags;
该数据成员是一个拥有下面这些标志的位掩码:
QUOTE:
SA_NOCLDSTOP: 如果SA_NOCLDSTOP位被置位并且目标信号是SIGCHLD,除非子进程退出,而在子进程停止(stop)时父进程将不会收到通知。
SA_NOCLDWAIT: SA_NOCLDWAIT标志会阻止子进程成为僵尸进程。在目标信号是SIGCHLD的时候使用。如果进程设置了这个标志,接着调用某个wait系统调用,进程将被阻塞直到子进程全部终止,最后返回-1(译注:此处在APUE2ed中的解释是返回1),设置errno全局变量为ECHILD。
SA_ONSTACK: 一些时候需要在特定的堆栈上进行信号的处理。sigaction系统调用提供了这个方式。如果该位被置位,那么信号将会被交付到指定的堆栈上。
SA_NODEFER: 如果SA_NODEFER位被置位,那么当前信号正被处理时,系统不会屏蔽该信号以后的交付。
SA_RESETHAND: 如果SA_RESETHAND被置位,一旦信号被交付,信号处理句柄将被置为SIG_DEF。
SA_SIGINFO: 被置位时,由结构体sigaction 的成员sa_sigaction指向的函数被使用。注意:使用SIG_IGN或SIG_DFL时不应该设置这个标志。成功调用sigaction之后,返回0或-1,并且将error设置成相关错误值。
SA_NOCLDWAIT: SA_NOCLDWAIT标志会阻止子进程成为僵尸进程。在目标信号是SIGCHLD的时候使用。如果进程设置了这个标志,接着调用某个wait系统调用,进程将被阻塞直到子进程全部终止,最后返回-1(译注:此处在APUE2ed中的解释是返回1),设置errno全局变量为ECHILD。
SA_ONSTACK: 一些时候需要在特定的堆栈上进行信号的处理。sigaction系统调用提供了这个方式。如果该位被置位,那么信号将会被交付到指定的堆栈上。
SA_NODEFER: 如果SA_NODEFER位被置位,那么当前信号正被处理时,系统不会屏蔽该信号以后的交付。
SA_RESETHAND: 如果SA_RESETHAND被置位,一旦信号被交付,信号处理句柄将被置为SIG_DEF。
SA_SIGINFO: 被置位时,由结构体sigaction 的成员sa_sigaction指向的函数被使用。注意:使用SIG_IGN或SIG_DFL时不应该设置这个标志。成功调用sigaction之后,返回0或-1,并且将error设置成相关错误值。
4.5信号掩码(阻塞与开启信号)
进程可以阻塞或设置某个信号。一旦该信号被阻塞,关于它的交付将被推迟,直到进程重新开启它。在这样的情况下是非常有用的:进程进入代码中某个部分,不能被中断但仍希望可以接受、处理可能丢失的信号。可靠交付信号的能力直到4.2BSD引入之后(不久被SVR3采用),操作系统才拥有该能力。
随着可靠信号的出现,信号的生命和交付(life and delivery)都有所改变。信号可以在之前产生和交付。现在,一旦信号是挂起的(pending),进程可以在接收它之前决定怎么处理。进程可能会去处理它,也可能设置为缺省行为,或者丢弃信号
注意:如果许多信号都挂起,系统将会首先交付会改变进程状态的信号,例如SIGBUS。
sigprocmask
任何进程可以使用sigprocmask函数来阻塞信号。语法如下:
