发送信号

【摘自《Linux/Unix系统编程手册》】

kill()

一个进程能够使用 kill() 系统调用向另一个进程发送信号。(之所以选择 kill 作为术语,是因为早期UNIX实现中大多数信号的默认行为是终止进程)

#include <signal.h>
int kill(pid_t pid, int sig);
        Returns 0 on success, or -1 on error

 pid 参数标识一个或多个目标进程,而 sig 则指定了要发送的信号。如何解释 pid,要视以下4种情况而定:

  • 如果 pid 大于 0,那么会发送信号给 pid 指定的进程
  • 如果 pid 等于 0,那么会发送信号给与调用进程同组的每个进程,包括调用进程自身
  • 如果 pid 小于 -1,那么会向组 ID 等于该 pid 绝对值的进程组内所有下属进程发送信号。向一个进程组的所有进程发送信号在 shell 作业控制中有特殊有途
  • 如果 pid 等于 -1,那么信号的发送范围是:调用进程有权将信号发往的每个目标进程,除去 init(进程 ID 为 1)和调用进程自身。如果特权级进程发起这一调用,那么会发送信号给系统中的所有进程,上述两个进程除外。显而易见,有时也将这种信号发送方式称之为广播信号

如果并无进程与指定的 pid 相匹配,那么 kill() 调用失败,同时将 errno 置为 ESRCH(“查无此进程”)

 

进程要发送信号给另一进程,需要适当的权限,权限规则如下:

  • 特权级(CAP_KILL)进程可以向任何进程发送信号
  • 以 root 用户和组运行的 init 进程(进程号为 1),是一种特例,仅能接受已安装了处理器函数的信号。这可以防止系统管理员意外杀死 init 进程——这一系统运作的基石
  • 如图,如果发送者的实际或有效用户 ID 匹配于接受者的实际用户 ID 或者保存设置用户 ID (saved set-user-id),那么非特权进程也可以向另一个进程发送信号。利用这一规则,用户可以向由他们启动的 set-user-ID 程序发送信号,而无需考虑目标进程有效用户 ID 的当前设置。将目标进程有效用户 ID 排除在检查范围之外,这一举措的辅助作用在于防止用户某甲向用户某乙的进程发送信号,而该进程正在执行的 set-user-ID 程序又属于用户某甲。
  • SIGCONT 信号需要特殊处理。无论对用户 ID 的检查结果如何,非特权进程可以向同一会话中的任何其他进程发送这一信号。利用这一规则,运作作业控制的 shell 可以重启已停止的作业(进程组),及时作业进程已经修改了它们的用户 ID。

如果进程无权发送信号给所请求的 pid, 那么 kill() 调用将失败,且将 errno 置为 EPERM。若 pid 所指为一系列进程(即 pid 为负值)时,只要可以向其中之一发送信号,则 kill() 调用成功。

 

检查进程的存在 

kill() 系统调用还有另一重功用。若将参数 sig 指定为 0(即所谓空信号),则无信号发送。相反,kill() 仅会去执行错误检查,查看是否可以向目标进程发送信号。从另一个角度来看,这意味着,可以使用空信号来检测具有特定进程 ID 的进程是否存在。若发送空信号失败,且 errno 为 ESRCH,则表明目标进程不存在。如果调用失败,且 errno 为 EPERM(表示进程存在,但无权向目标进程发送信号)或者调用成功(有权向进程发送信号),那么就表示进程存在。

验证一个特定进程 ID 的存在并不能保证特定程序仍在运行。因为内核会随着进程的生灭而循环使用进程 ID。而一段时间之后,同一进程 ID 所指恐怕是另一进程了。此外,特定进程 ID 可能存在,但是一个僵尸(进程已死,但其父进程尚未执行 wait() 来获取其终止状态)。

还可使用各种其他技术来检查某一特定进程是否正在运行,其中包括如下技术:

  • wait() 系统调用:仅用于监控调用者的子进程
  • 信号量和排他文件锁:如果进程持续持有某一信号量或文件锁,并且一直处于被监控状态,那么如能获取到信号量或锁时,即表明该进程已经终止。
  • 诸如管道和 FIFO 之类的 IPC 通道:可对监控目标进程进行设置,令其在自身生命周期内持有对通道进行写操作的打开文件描述符。同时,令监控进程持有针对通道进行读操作的打开文件描述符,且当通道写入端关闭时(遭遇文件结束符),即可获知监控目标进程已经终止。监控进程对此情况的判定,既可借助于自身文件描述符的读取,可也采用第63章所述的描述符监控技术之一
  • /proc/PID 接口:例如,如果进程 ID 为12345 的进程存在,那么目录 /proc/12345 将存在,可以发起诸如 stat() 之类的调用来进行检查

出去最后一项之外,循环使用进程 ID 不会影响上诉所有技术。

 1 #include <signal.h>
 2 #include "tlpi_hdr.h"
 3 
 4 int main(int argc, char* argv[])
 5 {
 6     int s, sig;
 7     if (argc != 3 || strcmp(argv[1], "--help") == 0)
 8         usageErr("%s sig-num pid\n", argv[0]);
 9 
10     sig = getInt(argv[2], 0, "sig-num");
11     s = kill(getLong(argv[1], 0, "pid"), sig);
12 
13     if (sig != 0) {
14         if (s == -1)
15             errExit("kill");
16     } else { /* Null signal: process existence check*/
17         if (s == 0) {
18             printf("Process exists and we can send it a signal\n");
19         } else {
20             if (errno == EPERM)
21                 printf("Process exists, but we don't have permission to send it a signal\n");
22             else if (errno == ESRCH)
23                 printf("Process does not exist\n");
24             else
25                 errExit("kill");
26         }
27     }
28     exit(EXIT_SUCCESS);
29 }


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

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

#include <signal.h>
int raise(int sig);
        Returns 0 on success, or nonzero on error

在单线程程序中,调用 raise() 相当于对 kill() 的如下调用:

kill(getpid(), sig);

支持线程的系统会将 raise() 实现为:

pthread_kill(pthread_self(), sig);

该实现意味着将信号传递给调用 raise() 的特定线程。相比之下,kill(getpid(), sig) 调用会发送一个信号给调用进程,并可将该信号传递给该进程的任一线程。

当进程使用 raise() (或者kill())向自身发送信号时,信号将立即传递(即在 raise() 返回调用者之前)

注意,raise() 出错将返回非 0 值(不一定为 -1)。调用 raise() 唯一可能发生的错误为 EINVAL,即 sig 无效。

 

killpg() 函数向某一进程组的所有成员发送一个信号

#include <signal.h>
int killpg(pid_t pgrp, int sig);
        Returns 0 on success, or -1 on error

killpg() 的调用相当于对 kill() 的如下调用:

kill(-pgrp, sig);

如果指定 pgrp 的值为 0,那么会向调用者所属进程组的所有进程发送此信号。

 

posted @ 2016-02-04 10:53  Kjing  阅读(832)  评论(0编辑  收藏  举报