首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

那些永不消逝的进程-2

那些永不消逝的进程-2

图 2. 进程组和会话现在我们再来回顾一下清单 3 中的那些"巧合":
  • 在 login shell 进程(PID=21682)被创建出来的同时,一个新的会话(SID=21682)和新的进程组(PGID=21682)                    也同时被创建了出来,会话的控制进程(或会话先导)即 login shell 进程本身,控制终端是 pts/20;
  • 当 sleep                    进程(PID=23661)被创建出来的同时,新的进程组(PGID=23661)也同时被创建了起来,进程组被置于后台运行(命令行末尾有&)。由于隶属于会话                    21682,所以该进程组的控制终端也是 pts/20;
大致搞清了进程组和会话之后,现在我们再回到最初的那个问题:信号 SIGHUP 在这一设计体系下到底扮演了什么角色?
SIGHUP,如其字面所述,这是一个用来描述 "挂断" 状态的信号,也就是说,当终端连接被关闭或无法维系之时,就需要这个信号出场了。具体来讲,每当:
  • 终端连接中断时,SIGHUP 会被发送到控制进程,后者会在将这个信号转发给会话中所有的进程组之后自行了断;
  • 控制进程被关闭时,SIGHUP 会被直接发送给会话中所有的进程组;
顺便说一句,一般进程对于 SIGHUP 信号的默认处理也同样是终结自己。
这样一来,笔者当年的困惑就被解答了:用于编译的 make 程序没有对 SIGHUP                信号做任何特殊处理,所以当终端连接中断时(远程终端应用程序被关闭),慢悠悠的编译进程也就这么被终止了。
永不消逝的进程 v1:下面让我们通过一个实际例子来看一看一个被 nohup 处理过的进程在其所属的会话的控制进程收到 SIGHUP 信号时会发生些什么:
清单 4 实例:一个 nohup                    处理过的进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#首先远程 SSH 登陆一台 Linux 服务器
$ ssh zhang@9.115.241.18
#然后打开一个后台进程 (27871) 直接进入休眠
$ nohup sleep 1000 &
[1] 10590
#利用 ps j 命令来查看一下 27871 进程的作业(job)相关信息
$ ps j 10590
PPID    PID PGID    SID TTY TPGID   STAT    UID TIME    COMMAND
10417   10590   10590   10417   pts/20      10655   S       1000    0:00    sleep 1000
#一切正常,现在断开连接,关闭会话
$ exit
#最后再在远程运行 ps -j 命令来检查 10590 进程当前的状态
$ ssh zhang@9.115.241.18 ‘ps -j’
PPID    PID PGID    SID TTY TPGID   STAT    UID TIME    COMMAND
1   10590   10590   10417   ?       -1  S   1000    0:00    sleep 1000




比较一下 exit 前后的 ps 输出,可以发现:
  • 由于原有父进程 10417 已死,10590 变成了孤儿进程(orphan);
  • 由于 pts/20 随着会话 10417 被关闭了,TTY 和 TPGID 被置为了?和-1;
  • 而原有的 PGID 和 SID 保持不变,只不过 process group leader 和 session leader 分别变成了 10590;
因此,我们得出了结论:一个忽略了 SIGHUP                信号的进程,在它所属的会话的控制进程被终结之后依旧可以继续运行;但此时由于原有控制终端已经不再存在了,它便不再有终端输入或输出的能力;此外,原有的会话依旧存在,只不过会话先导(session                leader,由于此时的会话中已没有任何终端,因此不能称之为控制进程了)变为该进程。
总而言之,看起来 SIGHUP 基本可以满足笔者的需求了,下文的清单 5 是笔者对清单 1 中例程略作修改之后的结果,这一次,即使 shell 进程被关闭,child                process 仍然可以继续在后台欢快的运行,称得上拥有"不死之身"了。有兴趣的读者可以在自己的环境里尝试一下。
清单 5 永不消逝的进程 v1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import signal
import time
from multiprocessing import Process
from os import getpid


def child_process():
    # child process
    # 忽略 child process 的 SIGHUP 信号
    signal.signal(signal.SIGHUP, signal.SIG_IGN)
    print("child process's pid: %d" % getpid())
    while (1):
        print("child's still alive.")
        time.sleep(1)


def main():
    p = Process(target=child_process)
    """
    这里就不能再设置 daemon 属性为 True 了,
    因为如果 daemon 属性为 True,则 Process 进程结束时会自动 terminate 所有的子进程
    这样就没 SIGHUP 什么事了
    """
    p.start()
    # parent process
    print("Parent process ends here.")
    print("Will child process live forever?")


if __name__ == '__main__':
    main()




看着 ps 输出里的 client process 在那里闷声发大财,是不是有点小激动?好,现在到了泼冷水的时候了,下面咱们来探讨一下用屏蔽 SIGHUP                实现出的"不死"进程都有些什么痛脚。
nohup 的局限性咱们先来说结论,用屏蔽 SIGHUP 的方式来实现守护进程,平时个人用用偷着乐还行;真要做成通用的解决方案,那还是有一定差距的。
最容易想到的问题来自于易用性:显而易见,控制进程被杀之后带来的最直接的软肋就是控制终端就此失效,这给想要获取程序的运行状态的用户带来了一个难题。以前文的'nohup                make &'为例,nohup 会很体贴的默认将应用程序的 stdout 或 stderr 转到文件 nohup.out                之中,但如若这个文件被删,那就欲哭无泪了。因此为守护进程提供一个稳定的执行结果输出方案是非常必要的;
获取了输出,那下一步要考虑的就是如何对守护进程输入了:一般来说,配置文件是这类问题的入门机配置。但是,如何告诉一个正在运行的进程去重新载入用户刚刚修改过的配置呢?如果你不打算额外实现点什么,那么最简便的方法就是利用操作系统已经实现好了的机制:信号。
那么问题又来了,使用什么信号量告诉程序重载配置文件比较好呢?很遗憾,解决这个问题的常规方式还是 SIGHUP,理由也比较充分:POSIX                中信号量的确是定义了不少,但却各司其职;且守护进程本就没有控制终端了,那不用 SIGHUP 用谁?
于是我们又开始要面对一个小小的悖论了:屏蔽 SIGHUP 真的好吗?
当然,上面说的两点之外,我们还需要面对一箩筐的问题:比如守护进程的工作目录无法被                umount;再比如绝对不可以允许守护进程再偷偷的拥有一个控制终端;再比如如何清理那些遗留在系统边边角角的守护进程……
成功?我们才刚上路呢。
返回列表