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

那些永不消逝的进程-1

那些永不消逝的进程-1

本帖最后由 look_w 于 2017-12-19 20:05 编辑

本文缘起于最近几天笔者实现的一段代码,目的是利用 python 在 Linux 中实现一个常驻内存的后台守护进程负责向其他进程提供服务,起初笔者自信的认为                multiprocessing.Process 类的 daemon 属性应该符合要求,于是乎不假思索的挥毫写下测试代码如下:
清单 1 永不消逝的进程 v0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from multiprocessing import Process
from time import sleep

def child_process():
    # 子进程函数
    # 建立一个进程,每隔一秒钟输出"child's still alive."
    while (1):
        print("child's still alive.")
        sleep(1)

def main():
    # 主进程函数
    p = Process(target=child_process)
    p.daemon = True                             # 设置 daemon 属性为 True
    p.start()
    sleep(10)                                   # 休眠 10 秒后结束
    print("Main process ends.")
    print("Will child process live forever?")   # 我们期待子进程继续活着,但事实上……

if __name__ == "__main__":
    main()




这段程序的目的很简单:主进程(main())利用 Process 对象 fork 出一个子进程(child_process()),设上 daemon                属性,然后结束自己,期待着子进程可以就这么活下去,安静地每隔 1 秒输出一行"child's still alive"。
当然,如果运行结果如前所述的话,那笔者估计也不会作此拙作了。
实际的运行结果是:伴随着"will child process live                forever?",子进程的输出也戛然而止,这意味着子进程最终也还是随着父进程的消亡而消逝了。
这个问题着实困扰了笔者一阵,直到在中的 daemon                属性下看到了这句描述:
When a process exits, it attempts to terminate all of its daemonic child                processes.
真相大白,原来这里的 daemon 并非真正意义上的守护进程,而是"守护父进程的进程",当父进程结束的时候,"守护着"它的进程也会被自动销毁。
于是,在感叹写程序切不可望文生义的同时,笔者也不得不开始琢磨如何自己动手丰衣足食,另辟蹊径来实现守护进程了。
所幸,在 Linux 下,想要实现出一个不死的进程,办法还是很多的。
从                nohup 说开去开始这一部分正文之前,先说一小段题外话:若干年前,笔者刚刚才加工作之时,曾经参加过一款基于嵌入式 Linux                的模块的研发。如今回想起来,当时最为印象深刻的,就是这个模块的软件系统极其庞杂,限于开发服务器的性能,一次完整的编译,有时候竟需要半个小时甚至更久的时间。倘若有幸在临近下班时分下载一份全新的代码进行编译,那欲哭无泪的画面实在是美的令人不忍直视。
那时笔者尚属菜鸟,于是便数次毫无悬念地在 terminal 前面等待满长的编译结束直到华灯初上。直到有一天一位过路神仙给笔者支了个招:
nohup make &
关机!下班!
然后,等到笔者次日懵懵懂懂的回到办公室打开电脑,编译完毕的二进制文件早已安安静静的躺在服务器的硬盘里了。
——知识就是力量!
好,题外话告一段落,现在咱们来看一看这 nohup 的力量到底来自哪里:
解密                nohup一般而言,man 命令是了解绝大部分 Linux 命令的绝佳入口,但是打开 nohup 的 man page,却只能发现寥寥数语:
nohup - run a command immune to hangups, with output to a non-tty
这样简略的信息只怕是不够我们理解其原理的,幸而 nohup 是 GNU Coreutil 的一部分,本着死代码不说谎的原则,笔者又寻到了源码,却发现其实现出人意料的简单(其实现摘要如清单 2 所示,中文注释为笔者所加):
清单 2 nohup.c
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
int
main (int argc, char **argv)
{
  /* …… */
  if (ignoring_input)
    {
      /* 重定向标准输入到/dev/null */
    }

  if (redirecting_stdout || (redirecting_stderr && stdout_is_closed))
    {
      /* 重定向标准输出到文件 */
    }

  if (redirecting_stderr)
    {
      /* 重定向标准错误到文件 */
    }

  /* 忽略 SIGHUP 信号 */
  signal (SIGHUP, SIG_IGN);

  /* 执行 cmd */
  char **cmd = argv + optind;
  execvp (*cmd, cmd);
  /* …… */  
  return exit_status;
}




抛去一堆重定向带来的视觉杂讯,我们不难发现,在创建一个新的进程执行以参数形式传入的 cmd 之前(execvp),nohup 忽略了 SIGHUP 信号,这意味着,作为                nohup 子进程被执行的命令,如果其自身不做任何特殊处理(例如重新为 SIGHUP 信号绑定一个 handler),同样会继承其父进程对所有信号的处理方式,即对                SIGHUP 信号不闻不问。
结合从 man page 中得到的信息,我们很容易将"immune to hangups"和"signal (SIGHUP,                SIG_IGN)"等同起来,但是,为什么忽略了 SIGHUP 信号的子进程就不会随着父进程的结束而消逝?在什么样的场景下,一个进程会收到 SIGHUP 信号呢?
要回答这个问题,我们首先要了解 Linux 系统中描述进程关系(Process Relationships)的两个非常重要的术语:进程组(Process                Group)和会话(Session)。
进程组和会话在开始枯燥的术语介绍之前,先让我们来看一看在一个真实的 Linux 环境下的进程组和会话到底长什么样:
清单 3 利用 ps -j                    显示进程组和会话信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#首先远程 SSH 登陆一台 Linux 服务器
$ ssh zhang@9.115.241.18
#然后打开一个后台进程直接进入休眠
$ sleep 1000 &
[1] 23661               #这里的 23661 号进程就是我们的研究对象
#接下来我们利用 ps j 命令来查看一下当前 login shell 进程 ($$) 和 23661 进程的作业(job)相关信息
$ ps j 23661 $$
PPID    PID PGID    SID TTY TPGID   STAT    UID TIME    COMMAND
4721    21682   21682   21682   pts/20      23856   Ss      1000    0:00    -bash
21682   23661   23661   21682   pts/20      23856   S       1000    0:00    sleep 1000
#上表的返回值中,PID 指进程 id;PPID 指父进程 PID;PGID 指进程组 id
#SID 指会话 id;TTY 指会话的控制终端设备;COMMAND 指进程所执行的命令
#TPGID 指前台进程组的 PGID。
#由于当前掌握着控制终端的是 ps 进程,故上述两个进程的 TPGID 都为 23856。




由清单 3 最后的 ps 结果可以发现若干貌似巧合的结果,例如进程 23661 的 PGID 恰好等于 PID;又比如进程 23661(sleep 1000)和                21682(login shell 进程)共享同一个 SID(亦即 login shell 的                PID)。在接下来的内容里笔者将通过对进程组和会话的解读,向读者展示这些巧合的背后到底隐藏了怎么样的设计。
进程组和会话都是 Unix 早期被引入的概念,其中进程组的设计在早期 AT&T Unix 发行版中就已初见端倪;而会话则要略晚一些,其设计雏形直到 SVR4                才被引入。
本着先来后到的原则,笔者先来介绍进程组:
  • 顾名思义,进程组就是一系列相互关联的进程集合,系统中的每一个进程也必须从属于某一个进程组;
  • 每个进程组中都会有一个唯一的 ID(process group id),简称 PGID;PGID 一般等同于进程组的创建进程的 Process                    ID,而这个进进程一般也会被称为进程组先导(process group leader),同一进程组中除了进程组先导外的其他进程都是其子孙;
  • 进程组的存在,方便了系统对多个相关进程执行某些统一的操作,例如,我们可以一次性发送一个信号量给同一进程组中的所有进程。
在早期 Unix 的设计中,进程组主要是用于终端访问控制(control terminal access)。以 SVR3                为例,一个比较典型的应用场景是:每当有一个终端通过某一 TTY 来访问服务器,一个包含了 login shell                进程的进程组就会被建立起来,因此进程组先导一般是为该终端而建的 shell 进程。当时还没有作业控制的概念,于是所有在该 shell                中被建立的新进程都会自动的隶属于同一进程组之下。同时该 tty 也会被设置为该进程组下所有进程共有的控制终端 (Controlling Terminal)                ,所有的进程可以同时对控制终端进行读写。下图大致反映了当有终端用户接入时早期 Unix 环境下的进程布局:
图 1. 早期                    Unix(SVR3)下的进程组设计诚然,以事后诸葛亮的眼光来看,这样的设计是存在不少弊端的,比如进程组对控制终端缺乏有效的管理手段;再比如所有进程无差别共享控制终端的设计会带来灾难性的混乱。
于是在 SVR4 之后,作业控制(job control)的概念被提了出来,会话的设计也随即被引入了进来:
  • 会话是一个若干进程组的集合,同样的,系统中每一个进程组也都必须从属于某一个会话;
  • 一个会话只拥有最多一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共用。当然和早期设计中所有进程都可以无差别读写控制终端不同,这一次,进程被以进程组为单位划分为两类:前台进程组(foreground                    process group)和后台进程组(background process                    group)。一个会话中前台进程组只会有一个,只有其中的进程才可以和控制终端进行交互;除了前台进程组外的进程组,都是后台进程组;
  • 和进程组先导类似,会话中也有会话先导(session leader)的概念,用来表示建立起到控制终端连接的进程。在拥有控制终端的会话中,session                    leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login                    shell);
  • 为了支持作业控制,很多 shell 工具也做了相应的修改:在执行一个新的命令时,新生成的进程都会被置于一个和 Shell                    进程不一样的全新的进程组之下;
一言以蔽之,新的设计将控制终端(tty 或                pty)的访问和控制完全置于了会话的管理之下,最大限度的避免了旧设计所带来的弊端。下图反映了在引入了会话的设计之后,有终端用户访问系统时进程的大致布局。
返回列表