目录
一、前言
二、Signals
1、Signal Handling(ctrlc.c)
2、Sending Signals
(1)alarm.c
(2)A Robust Signals Interface
(3)ctrlc2.c
3、Signal Sets
(1)sigaction Flags
(2)Common Signal Reference
一、前言
进程和信号是 Linux 操作环境的基本组成部分。它们控制 Linux 和所有其他类 unix 计算机系统执行的几乎所有活动。了解 Linux 和 UNIX 如何管理进程对任何系统程序员、应用程序程序员或系统管理员都有好处。
在本中,我们将学习如何在 Linux 环境中处理进程,以及如何找出计算机在任何给定时间正在做什么。我们还将了解如何从我们自己的程序中启动和停止其他进程,如何使进程发送和接收消息,以及如何避免僵尸进程。特别是,我们将了解:
(1)进程的结构、类型和调度;
(2)以不同的方式启动新进程;
(3)父进程、子进程和僵尸进程;
(4)什么是信号以及如何使用它们。
二、Signals
信号是 UNIX 和 Linux 系统为响应某些条件而生成的事件,进程在接收到这些条件后可能反过来采取一些行动。我们使用术语 raise 表示信号的生成,使用术语 catch 表示信号的接收。信号由一些错误条件引发,例如内存段违规、浮点处理器错误或非法指令。它们是由 shell 和终端处理程序生成的,用于引起中断,也可以从一个进程显式地发送到另一个进程,作为传递信息或修改行为的一种方式。在所有这些情况下,编程接口是相同的。信号可以被提出、捕捉和采取行动,或者 (至少对一些人来说) 被忽略。
信号名称是通过包含头文件 signal.h 来定义的,他们均以 SIG 开头,下表列出常见信号。
如果一个进程接收到其中一个信号而没有先安排捕获它,该进程将立即终止。通常会创建一个核心转储文件。这个文件称为 core,放在当前目录中,是进程的映像,在调试中很有用。
附加信号包括下表中的信号。
SIGCHLD 对于管理子进程很有用。默认情况下它会被忽略。其余的信号会导致接收它们的进程停止,SIGCONT 除外,它会导致进程恢复。它们被 shell 程序用于作业控制,很少被用户程序使用。
我们稍后会更详细地讨论第一组信号。现在,只要知道如果 shell 和终端驱动程序配置正常,在键盘上输入中断字符 (通常是Ctrl+C) 将导致 SIGINT 信号被发送到前台进程,即当前正在运行的程序就足够了。这将导致程序终止,除非它已安排捕捉信号。
如果要向当前前台任务以外的进程发送信号,可以使用 kill 命令。它接受一个可选的信号或名称,以及要将信号发送到的 PID (通常使用ps命令找到)。例如,要向运行在另一个PID为 512 的终端上的shell发送“挂起”信号,可以使用该命令:
$ kill –HUP 512
kill 命令的一个有用的变体是 killall,它允许向运行指定命令的所有进程发送信号。并非所有版本的 UNIX 都支持它,但 Linux 通常支持。当我们不知道 PID 时,或者当我们想向执行相同命令的多个不同进程发送信号时,这非常有用。一个常见的用法是告诉 inetd 程序重新读取它的配置选项。为此,我们可以使用命令:
$ killall –HUP inetd
程序可以使用信号库函数处理信号。
这个相当复杂的声明说明,signal 是一个接受两个形参的函数,sig 和 func。要捕获或忽略的信号作为参数 sig 给出。接收到指定信号时要调用的函数以 func 的形式给出。此函数必须接受单个int参数 (接收到的信号) ,且类型为 void。信号函数本身返回一个相同类型的函数,这是为处理该信号而设置的函数的前一个值,或者是以下两个特殊值之一:
举个例子就能说明问题。在下面的程序中,编写一个程序 ctrlc.c,它对键入Ctrl+C 做出反应,打印适当的消息而不是终止。第二次按 Ctrl+C 将结束程序。
1、Signal Handling(ctrlc.c)
函数 ouch 对传入参数 sig 中的信号作出反应,当信号出现时将调用这个函数。它打印一条消息,然后将 SIGINT 的信号处理 (默认情况下,通过键入 Ctrl+C 生成) 重置为默认行为。
main 函数必须拦截当我们键入 Ctrl+C 时生成的 SIGINT 信号。在其余的时间里,它只是处于一个无限循环中,每秒打印一条消息。
#include<signal.h>
#include<stdio.h>
#include<unistd.h>void ouch(int sig)
{printf("OUCH! - I got signal %d\n",sig);(void) signal(SIGINT,SIG_DFL);
}int main()
{(void) signal(SIGINT,SIG_DFL);while(1){printf("Hello World!\n");sleep(1);}//exit(0);
}
第一次输入 Ctrl+C (如下面的输出中显示为 ^C) 会导致程序做出反应,然后继续。当你再次键入Ctrl+C时,程序将结束,因为SIGINT的行为已经返回到导致程序退出的默认行为。
从这个示例中可以看到,信号处理函数接受一个整数参数,即导致调用函数的信号。如果相同的函数用于处理多个信号,这将非常有用。在这里打印出 SIGINT 的值,在这个系统中它的值恰好是 2。你不应该依赖于传统的数值来获取信号;在新程序中始终使用信号名称。
从信号处理程序内部调用所有函数 (如 printf ) 是不安全的。一种有用的技术是使用信号处理程序设置一个标志,然后从主程序中检查该标志,并在需要时打印一条消息。最后,你将发现可以在信号处理程序内部安全地执行的调用列表。
How It Works:
当你通过键入 Ctrl+C 给出 SIGINT 信号时,程序安排函数 ouch 被调用。在中断函数 ouch 完成后,程序继续执行,但信号动作被恢复到默认值。(不同版本的 UNIX,特别是源自 Berkeley UNIX 的 UNIX,在历史上具有微妙的不同信号行为。如果你想在一个信号发生后恢复它的默认动作,最好是专门这样编码。)当它接收到第二个 SIGINT 信号时,程序采取默认操作,即终止程序。
如果希望保留信号处理程序并继续对 Ctrl+C 作出反应,则需要通过再次调用信号来重新建立它。这将导致从中断函数开始到重新建立信号处理程序之前的一小段时间内没有处理信号。有可能在这个时候接收到第二个信号并违背人的意愿终止程序。
我们不建议使用信号接口来捕获信号。我们在这里包含它,因为我们会在许多较老的程序中发现它。稍后我们将看到 sigaction,这是一个定义更清晰、更可靠的接口,我们应该在所有新程序中使用它。
如果有,signal 函数返回指定信号的信号处理程序的前一个值,否则返回 SIG_ERR,在这种情况下,errno 将被设置为正数。如果指定了无效的信号,或者试图处理无法捕获或忽略的信号 (如SIGKILL) ,则 errno 将设置为 EINVAL。
2、Sending Signals
一个进程可以通过调用 kill 向其他进程 (包括它自己) 发送信号。如果程序没有发送信号的权限,调用将失败,通常是因为目标进程属于另一个用户。这是等价于同名 shell 命令的程序。
kill 函数将指定的信号 sig 发送给 pid 指定标识符的进程。成功时返回 0。要发送一个信号,发送进程必须有这样做的权限。通常,这意味着两个进程必须具有相同的用户 ID (也就是说,我们只能向自己的一个进程发送信号,尽管超级用户可以向任何进程发送信号)。
kill 将失败,返回 -1,如果给出的信号不是有效的 ( errno 设置为 EINVAL ),如果它没有权限 (EPERM),或者如果指定的进程不存在 (ESRCH),则设置 errno。
信号为你提供了一个有用的闹钟设施。进程可以使用警报函数调用在未来的某个时间调度 SIGALRM 信号。
警告调用以秒为单位调度 SIGALRM 信号的发送。事实上,由于处理延迟和调度的不确定性,警报将在那之后不久交付。值 0 将取消任何未完成的报警请求。在接收到该信号之前调用该警告将导致该 alarm 重新调度。每个进程只能有一个未处理的 alarm。alarm 返回任何未完成的警报调用将被发送之前的剩余秒数,如果调用失败则返回 -1。
要了解 alarm 是如何工作的,可以使用 fork、sleep 和 signal 来模拟它的效果。一个程序可以启动一个新的进程,唯一的目的是在稍后的某个时间发送一个信号。
(1)alarm.c
在 alarm.c 中,第一个函数 ding 模拟闹钟。
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>static int alarm_fired=0;void ding(int sig)
{alarm_fired=1;
}/*In main, you tell the child process to wait for five seconds before sending a SIGALRM signal to its parent.*/
int main()
{pid_t pid;printf("alarm application starting\n");pid=fork();switch(pid){case -1:perror("fork failed");exit(1);case 0:sleep(5);kill(getppid(),SIGALRM);exit(0);}/*The parent process arranges to catch SIGALRM with a call to signal and then waits for the inevitable.*//* if we get here we are the parent process */printf("waiting for alarm to go off\n");(void) signal(SIGALRM,ding);pause();if(alarm_fired)printf("Ding!\n");printf("done\n");exit(0);
}
这个程序引入了一个新函数 pause,它只是使程序暂停执行,直到出现信号。当它接收到信号时,运行任何已建立的处理程序,并照常继续执行。它被声明为:
并返回 -1 (如果下一个接收到的信号没有导致程序终止),当被信号中断时,errno 设置为 EINTR。更常见的是在等待信号时使用 sigsuspend。
How It Works:
闹钟模拟程序通过 fork 启动一个新的进程。这个子进程休眠 5 秒钟,然后向它的父进程发送一个 SIGALRM 。父进程安排捕获 SIGALRM,然后暂停直到接收到信号。不要在信号处理程序中直接调用 printf,相反,我们可以设置一个标志,然后在之后检查该标志。
使用信号和暂停执行是 Linux 编程的一个重要部分。这意味着程序不一定要一直运行。它可以等待事件发生,而不是在循环中不断地检查事件是否发生。这在多用户环境中尤其重要,因为在多用户环境中,进程共享一个处理器,这种繁忙等待对系统性能有很大的影响。关于信号的一个特殊问题是,我们永远不知道 “如果在系统调用中间出现一个信号会发生什么?” (答案是相当令人不满意的“视情况而定”) 通常,我们只需要担心“慢”的系统调用,例如从终端读取,如果在等待时出现信号,系统调用将返回一个错误。如果开始在程序中使用信号,则需要注意,如果信号导致在添加信号处理之前可能没有考虑到的错误条件,那么一些系统调用可能会失败。
我们必须仔细编写我们的信号,因为在使用它们的程序中可能会出现许多“竞态条件”。例如,如果我们打算调用 pause 来等待一个信号,而该信号在调用 pause 之前发生,那么程序可能会无限期地等待一个不会发生的事件。这些竞争条件,关键的时间问题,让许多新手程序员感到困惑。我们最好总是非常仔细地检查信号代码。
(2)A Robust Signals Interface
我们已经深入讨论了使用 signal 和 friends 引发和捕获信号,因为它们在较老的 UNIX 程序中非常常见。然而,X/Open 和 UNIX 规范为更健壮的信号推荐了一个更新的编程接口:sigaction。
sigaction 结构,用于定义接收到 sig 指定的信号时要采取的动作,在头文件 signal.h 中定义,且至少具有以下成员:
sigaction 函数的作用是设置与信号 sig 相关联的动作。将前一个信号动作写入它所引用的位置。如果 act 为空,这就是 sigaction 所做的一切。如果 act 不为空,则为指定信号设置动作。
与 signal 一样,sigaction 如果成功返回 0,否则返回 -1。如果指定的信号无效,或者试图捕获或忽略无法捕获或忽略的信号,则错误变量 errno 将被设置为 EINVAL。
在参数 act 所指向的 sigaction 结构中,sa_handler 是一个指针,指向接收到信号 sig 时调用的函数。这很像我们前面看到的传递给 signal 的函数 func。可以在 sa_handler 字段中使用特殊值 SIG_IGN 和 SIG_DFL,分别表示要忽略信号或将动作恢复为默认值。
sa_mask 字段指定在调用 sa_handler 函数之前要添加到进程的信号掩码中的一组信号。这些是被阻塞的信号集,不会被传递到进程。这防止了我们前面看到的在处理程序运行完成之前就接收到信号的情况。使用 sa 掩码字段可以消除这种竞态条件。
但是,sigaction 设置的处理程序捕获的信号在默认情况下不会重置,如果我们想获得前面看到的 signal 的行为,则必须将 sa_flags 字段设置为包含值 SA_RESETHAND。在更详细地研究 sigaction 之前,让我们用 sigaction 代替 signal 重写程序 ctrlc.c。
(3)ctrlc2.c
进行以下更改,以便 sigaction 拦截SIGINT。调用新程序ctrlc2.c。
#include<signal.h>
#include<stdio.h>
#include<unistd.h>void ouch(int sig)
{printf("OUCH! - I got signal %d\n",sig);
}int main()
{struct sigaction act;act.sa_handler=ouch;sigemptyset(&act.sa_mask);act.sa_flags=0;sigaction(SIGINT,&act,0);(void) signal(SIGINT,SIG_DFL);while(1){printf("Hello World!\n");sleep(1);}//exit(0);
}
当运行这个版本的程序时,当键入 Ctrl+C 时总是会收到一条消息,因为 sigaction 会重复处理 SIGINT 。要终止程序,必须键入 Ctrl+\,默认情况下会生成SIGQUIT信号。
但实际上,运行的时候按 Ctrl+C 可能也会停止。
How It Works:
该程序调用 sigaction 而不是 signal 来将 Ctrl+C (SIGINT) 的信号处理程序设置为函数 ouch。它首先必须设置一个 sigaction 结构,该结构包含处理程序、信号掩码和标志。在这种情况下,你不需要任何标志,并且用新函数创建了一个空信号掩码 sigemptyset。
运行此程序后,你可能会发现已经创建了一个核心转储 (在一个名为 core 的文件中)。你可以安全地删除它。
3、Signal Sets
头文件 signal.h 定义了 sigset_t 类型和用于操作信号集的函数。这些集合用于 sigaction 和其他函数,以在接收到信号时修改进程行为。
这些函数执行其名称所建议的操作。sigemptyset 将信号集初始化为空。sigfillset 初始化一个信号集以包含所有已定义的信号。sigaddset 和 sigdelset 从信号集中添加和删除指定的信号 (signo)。如果成功,它们都返回 0,如果 errno 设置为 error,则返回 -1。如果指定的信号无效,唯一定义的错误是 EINVAL。
sigismember 函数确定给定的信号是否是信号集的成员。如果信号是集合的成员,则返回 1;如果不是,则返回0;如果信号无效,则返回 -1,errno 设置为 EINVAL。
通过调用函数 sigprocmask 来设置或检查进程信号掩码。此信号掩码是当前被阻塞的信号集,因此当前进程不会接收到这些信号。
sigprocmask 可以根据 how 参数以多种方式更改进程信号掩码。如果信号掩码不为空,则在参数集中传递新的信号掩码值,以及前一个信号掩码将被写入信号集。
how参数可以是以下参数之一:
如果 set 参数是空指针,how 的值不会被使用,调用的唯一目的是将当前信号掩码的值取到 oset中。
如果它成功完成,sigprocmask 返回 0,如果 how 参数无效,则返回 -1,在这种情况下,errno 将被设置为 EINVAL。
如果一个信号被进程阻塞,它将不会被传递,而是保持挂起状态。程序可以通过调用 sigpending 函数来确定哪些被阻塞的信号正在等待。
它将一组被阻塞的信号写入 set 所指向的信号集中。如果成功,则返回 0,否则返回 -1,并设置 errno 以指示错误。当程序需要处理信号和控制何时调用处理函数时,此函数非常有用。
进程可以通过调用 sigsuspend 暂停执行,直到传递一组信号中的一个为止。这是前面提到的 pause 函数的一种更一般的形式。
sigsuspend 函数用 sigmask 给出的信号集替换进程信号掩码,然后暂停执行。它将在信号处理函数执行后恢复。如果接收到的信号终止程序,sigsuspend 将永远不会返回。如果接收到的信号没有终止程序,sigsuspend 返回 -1, errno 设置为 EINTR。
(1)sigaction Flags
sigaction 中使用的 sigaction 结构的 sa_flags 字段可以包含下表中所示的值,以修改信号行为:
SA_RESETHAND 标志可用于在捕获信号时自动清除信号函数,如前所述。
程序使用的许多系统调用都是可中断的;也就是说,当它们接收到一个信号时,它们将返回一个错误,errno 将被设置为 EINTR,以指示由于信号而返回的函数。这种行为需要使用信号的应用程序格外注意。如果在 sigaction 调用中的 sa_flags 字段中设置了 SA_RESTART,那么在执行信号处理函数后,可能会被信号中断的函数将被重新启动。
通常,当一个信号处理函数正在执行时,接收到的信号被添加到处理函数期间的进程信号掩码中。这将防止后续出现相同的信号,从而导致信号处理函数再次运行。如果函数不是可重入的,那么在它完成处理第一个信号之前,让另一个信号调用它可能会导致问题。但是,如果设置了 SA_NODEFER 标志,则在接收到该信号时不会更改信号掩码。
信号处理函数可能在中间被中断,然后被其他函数再次调用。当你回到第一个电话时,它仍然正确地运行是至关重要的。它不仅是递归的(调用自身),而且是可重入的(可以毫无问题地再次输入和执行)。内核中同时处理多个设备的中断服务例程需要是可重入的,因为在执行相同的代码时,一个高优先级的中断可能会“进入”。
在信号处理程序内部调用是安全的函数,即 X/Open 规范所保证的函数是可重入的,还是本身不发出信号,都在下表中列出。
所有未在下表中列出的功能都应被认为对信号不安全
(2)Common Signal Reference
接下来我们列出 Linux 和 UNIX 程序通常需要的信号及其默认行为。
下表中信号的默认操作是异常终止进程,其结果为 _exit (类似于 exit,但在返回内核之前不执行清除操作)。但是,该状态可用于等待,waitpid 表示由指定的信号异常终止。
缺省情况下,下一个表的信号也会导致异常终止。此外,可能会发生与实现相关的操作,如创建核心文件。
默认情况下,进程在接收到下表中的一个信号时被挂起。
SIGCONT 将重新启动已停止的进程,如果被未停止的进程接收,则忽略 SIGCONT。SIGCHLD 标志在默认情况下被忽略。
小结:
已经了解了进程是 Linux 操作系统的一个基本组成部分。已经了解了如何启动、终止和查看它们,以及如何使用它们解决编程问题。我们还了解了信号和事件,这些事件可用于控制正在运行的程序的操作。所有Linux进程 (包括init) 都使用对任何程序员可用的相同系统调用集。
以上,进程与信号(三)
祝好