Unix 环境高级编程-进程

程序的执行实例称为进程,Unix确保每个进程都有一个唯一的标识,即进程ID。

创建新进程 fork

一个现有的进程可以调用 fork 函数创建一个新进程。

fork函数被调用一次,但返回两次。区别是子进程返回值是0,父进程返回是子进程的进程ID,这样做是因为:

  • 一个进程的子进程可以有多个,没有一个函数可以获得所有子进程的进程ID
  • 一个子进程只会有一个父进程,子进程可以调用 getppid 获得父进程的进程ID

fork 之后,父进程与子进程一起向下执行,子进程获得父进程的数据空间、堆、栈。子进程是父进程的副本。子进程并不共享存储空间部分。但是共享正文段(CPU执行机器指令部分)

父进程先执行还是子进程先执行是不确定的,这取决于内核使用的调度算法。如果要求父子进程之间互相同步,则要求某种形式的进程间通信。

父进程与子进程区别如下:

  • fork返回值不同
  • 进程ID不同
  • 子进程的进程时间设置为0
  • 子进程的未处理闹钟被清除
  • 子进程的未处理信号集设置为空

fork失败的主要原因:

  • 系统有太多的进程
  • 该用户ID的进程总数超出系统限制

进程终止 wait 和 waitpid

父进程检查子进程终止状态的不同方法,如果子进程正常终止,父进程可以获得子进程的退出状态

当终止进程的父进程调用 wait 或 waitpid 时,可以得到子进程一定量的信息,包括进程ID、种植状态、CPU时间量。

对于父进程已经终止的所有进程,他们的父进程都改变为 init 进程。

一个已经终止、但父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它占用的资源)的进程将成为僵死进程。

当一个进程正常或异常终止时,内核向其父进程发送 SIGCHLD 信号。

调用 wait 可能会发生:

  • 如果所有子进程还在运行,则阻塞
  • 如果一个子进程终止,正等待父进程获取终止状态,则取得该子进程终止状态立即返回
  • 如果没有任何子进程,则出错返回。

waitpid 和 wait 的区别:

  • 在一个子进程终止前,wait 会阻塞。只要有一个子进程终止,wait 就返回。如果有多个子进程就需要循环调用wait。而 waitpid 提供了这种功能。
  • waitpid 还可以等待特定进程

waitpid参数如下:
pid == -1 等待任一子进程,与wait等效
pid > 0 等待pid这个子进程
pid == 0 等待调用进程组ID的任一子进程
pid < -1 等待调用进程组ID等于pid绝对值的任意子进程

进程组

每个进程除了有一个进程ID以外,还属于一个进程组。

进程组是一个或多个进程的集合。同一进程组中的各进程接收来自同一终端的各种信号。每个进程组有一个组长进程,组长进程的进程组ID等于其进程ID

进程组组长可以创建一个进程组,创建改组中的进程,然后终止。只要某个进程组中有一个进程存在,则该进程组就存在,与组长进程是否终止无关。从进程组创建开始到最后一个进程离开为止称为进程组的生命期。

进程调用 setpgid 可以加入一个现有的进程组,或使用 setsid 创建一个新的进程组

一个进程只能为它自己或它的子进程设置进程组ID。在大多数作业控制中,使用 fork 之后,使父进程设置子进程的进程组ID,并且也使子进程设置自己的进程组ID,这样可以保证父子属于同一个进程组。

会话

会话是一个或多个进程组的集合。

进程调用 setsid 函数建立一个新会话

如果调用此函数的进程不是一个进程组的组长,则此函数会创建一个新会话,然后将会发生:

  1. 该进程变成新会话的会话首进程。
  2. 该进程成为一个新的进程组的组长进程
  3. 该进程没有控制终端,如果调用 setsid 之前该进程有一个控制终端这种联系也被切断。

如果该调用进程已经是一个进程组的组长,则返回出错。

通常先fork,然后父进程终止,子进程继续,因为子进程继承了父进程的进程组ID,而其进程ID是新分配的,不可能相等,所以保证子进程不是一个进程组的组长。

控制终端

一个会话可以有一个控制终端,会话和进程组有一些特性:

  • 建立与控制终端链接的会话首进程,称为控制进程
  • 一个会话的几个进程组可以分成一个前台进程组以及多个后台进程组
  • 如果一个会话有一个控制终端,那么它有一个前台进程组,其他进程组为后台进程组
  • 中断键(CTRL+C)将发送中断信号至前台进程组的所有进程
  • 退出键(关闭会话)将发送退出信号至前台进程组的所有进程
  • 如果网络断开链接,则挂断信号发送至控制进程(会话首进程),

信号

信号是软件中断,信号提供了一种处理异步事件的方法。

产生信号的事件对进程而言是随机出现的,进程不能简单的测试一个变量来判断是否发生了一个信号,而是必须告诉内核在此信号发生时,请执行下列操作

在某个信号出现时,可以告诉内核按下列3种方式之一进行处理:

  • 忽略此信号,但有两种信号不能被忽略,SIGKILL和SIGSTOP。因为它们提供了进程终止和停止的可靠方法
  • 捕捉信号,通知内核在某种信号发生时,执行一个回调。
  • 执行系统默认动作。对于大多数信号的系统默认动作是终止该进程。

守护进程

守护进程是生存期长的一种进程。因为没有控制终端,所以他们是在后台运行的。

编写守护进程需遵循一些基本原则:
1. 调用umask设置文件模式,如果守护进程要创建文件,它可能要设置特定的权限
2. 调用fork,然后使父进程exit,这样实现了几点,如果改守护进程是作为一条简单的shell启动的,父进程终止会让shell认为这条命令已经完毕。虽然子进程继承了父进程的进程组ID,但获得了新进程ID,保证子进程不是一个进程组的组长进程。这是执行setsid的先决条件。
3. 调用setsid创建一个新会话,将执行3个步骤:1. 成为新会话的首进程。2.成为一个新进程组的组长进程。3.没有控制终端
(有人建议此时再次调用fork,终止父进程,这样使第二次fork的子进程脱离会话首进程,防止它主动取得控制终端。)
(如果不使用上面的方法,那么需要在主动打开终端设备时,制定O_NOCTTY)
4. 将当前工作目录更改为根目录。为了防止父进程挂载某个可能被卸载的文件系统中。
5. 关闭不需要的文件描述符
6. 某些守护进程打开/dev/null,使其具有文件描述符0/1/2。这样可以让输入、输出、错误不会产生任何效果。

单实例守护进程

使用文件和记录锁,提供方便的互斥机制,在该守护进程终止时,自动删除锁。

惯例

锁文件一般存储在/var/run目录,锁文件名一般是name.pid
配置文件一般放在/etc目录,配置文件一般是name.conf
守护进程可以使用命令行启动,通常是系统初始化脚本之一

如何使用 PHP 实现一个守护进程

<?php
umask(0);

// 脱离进程组
$pid = pcntl_fork();
if ($pid === -1) {
    die('could not fork');
} elseif ($pid > 0) {
    exit(0);
}

// 创建新会话
if (posix_setsid() === -1) {
    exit(0);
}

// 再次fork,脱离会话首进程,防止主动获取控制终端
$pid = pcntl_fork();
if ($pid === -1) {
    die('could not fork');
} else if ($pid) {
    exit(0);
}

// 以下为测试代码,死循环输出
$line = 1;
while (1) {
    file_put_contents('a.txt', $line++ . "daemon\n", FILE_APPEND);
    sleep(1);
}