`
tomotoboy
  • 浏览: 166851 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

UNIX 进程揭秘——探索运行在 UNIX 操作系统下的进程的生命周期

阅读更多
原文地址:http://www.ibm.com/developerworks/cn/aix/library/au-unixprocess.html
2007 年 7 月 16 日

研究进程的生命周期,以便您能将所看到的系统上发生的事情与内核中发生的事情联系起来。系统管理员必须知道 UNIX® 环境中的进程是如何创建和销毁的,以便了解系统是如何组合起来的,以及如何管理行为异常的进程。类似地,开发人员必须了解 UNIX 进程模型,以便编写稳定的应用程序,这些应用程序在无人参与的情况下运行,并且不会给系统管理员带来问题。
分配给系统管理员的许多工作之一是确保用户的程序正确运行。因为系统上存在其他并发运行的程序,所以此任务变得更加复杂。由于种种原因,这些程序可能会失败、挂起或行为异常。在构建更可靠的系统时,了解 UNIX® 环境如何创建、管理和销毁这些作业是至关重要的步骤。

开发人员还必须积极了解内核如何管理进程,因为与系统的其他部分和睦相处的应用程序会占用更少的资源,并且不会频繁地给系统管理员带来麻烦。由于导致僵死进程(将在稍后对其进行描述)而频繁重新启动的应用程序明显是不可取的。对控制进程的 UNIX 系统调用的了解可以使开发人员编写能够在后台自动运行的软件,而不是需要一个始终保持在用户屏幕上的终端会话。

管理这些程序的基本构件就是进程。进程是赋予某个由操作系统执行的程序的名称。如果您熟悉 ps 命令,则您应该熟悉进程清单,如清单 1 所示。

sunbox#ps -ef 
     UID   PID  PPID   C    STIME TTY         TIME CMD
    root     0     0   0 20:15:23 ?           0:14 sched
    root     1     0   0 20:15:24 ?           0:00 /sbin/init
    root     2     0   0 20:15:24 ?           0:00 pageout
    root     3     0   0 20:15:24 ?           0:00 fsflush
  daemon   240     1   0 20:16:37 ?           0:00 /usr/lib/nfs/statd
...


前三列对这里的讨论非常重要。第一列列出用于运行该进程的用户身份,第二列列出进程的 ID,第三列列出该进程的父进程 ID。最后一列是进程的描述,通常是所运行的二进制文件的名称。每个进程都被分配一个标识符,称为进程标识符(Process Identifier,PID)。进程还有父进程,在大多数情况下就是启动它的进程的 PID。
父 PID (PPID) 的存在意味着这是一个由别的进程创建的进程。最初创建进程的原始进程名为 init,它始终被赋予 PID 1。init 是将在内核启动时启动的第一个实际进程。启动系统的其余部分是 init 的工作。init 和其他具有 PPID 0 的进程属于内核。
使用 fork 系统调用
fork(2) 系统调用创建一个新进程。清单 2 显示了一个简单 C 代码片段中使用的 fork。
清单 2. 简单的 fork(2) 用法
sunbox$ cat fork1.c
#include <unistd.h>
#include <stdio.h>

int main (void) {

        pid_t p; /* fork returns type pid_t */
        p = fork();
        printf("fork returned %d\n", p);
}

sunbox$ gcc fork1.c -o fork1
sunbox$ ./fork1
fork returned 0
fork returned 698

fork1.c 中的代码不过就是发出 fork 调用,并通过一个 printf 调用来打印整数结果。虽然只发出了一个调用,但是打印了两次输出。这是因为在 fork 调用中创建了一个新进程。现在有两个单独的进程在从该调用返回结果。这通常被描述为“调用一次,返回两次”。
fork 返回的值非常有趣。其中一个返回 0;另一个返回一个非零值。获得 0 的进程称为子进程,非零结果属于原始进程,即父进程。您将使用返回值来确定哪个是父进程,哪个是子进程。由于两个进程都在同一空间中继续运行,唯一有实际意义的区别是从 fork 返回的值。

0 和非零返回值的基本原理在于,子进程始终可以通过 getppid(2) 调用来找出其父进程是谁,但是父进程要找出它的所有子进程却很困难。因此,要告诉父进程关于其新的子进程的信息,而子进程可在需要时查找其父进程。

考虑到 fork 的返回值,现在该代码可以检查确定它是父进程还是子进程,并进行相应的操作。清单 3 显示了一个基于 fork 的结果来打印不同输出的程序。
清单 3. 更完整的 fork 用法示例               
sunbox$ cat fork2.c
#include <unistd.h>
#include <stdio.h>

int main (void) {

        pid_t p;

        printf("Original program, pid=%d\n", getpid());
        p = fork();
        if (p == 0) {
                printf("In child process, pid=%d, ppid=%d\n",
                        getpid(), getppid());
        } else {
                printf("In parent, pid=%d, fork returned=%d\n",
                        getpid(), p);
        }
}
sunbox$ gcc fork2.c -o fork2
sunbox$ ./fork2
Original program, pid=767
In child process, pid=768, ppid=767
In parent, pid=767, fork returned=768

清单 3 在每个步骤打印出 PID,并且该代码检查从 fork 返回的值来确定哪个进程是父进程,哪个进程是子进程。对所打印的 PID 进行比较,可以看到原始进程是父进程 (PID 767),并且子进程 (PID 768) 知道其父进程是谁。请注意子进程如何通过 getppid 来知道其父进程以及父进程如何使用 fork 来定位其子进程。
现在您已经了解了复制某个进程的方法,下面让我们研究如何运行一个不同的进程。fork 只是进程机制中的一半。exec 系列系统调用运行实际的程序。

使用 exec 系列系统调用
exec 的工作是将当前进程替换为一个新进程。请注意“替换”这个措词的含义。在您调用 exec 以后,当前进程就消失了,新进程就启动了。如果希望创建一个单独的进程,您必须首先运行 fork,然后在子进程中执行 (exec) 新的二进制文件。清单 4 显示了这样一种情况。
清单 4. 通过将 fork 与 exec 配合使用来运行不同的程序               
sunbox$ cat exec1.c
#include <unistd.h>
#include <stdio.h>

int main (void) {

        /* Define a null terminated array of the command to run
           followed by any parameters, in this case none */
        char *arg[] = { "/usr/bin/ls", 0 };

        /* fork, and exec within child process */
        if (fork() == 0) {
                printf("In child process:\n");
                execv(arg[0], arg);
                printf("I will never be called\n");
        }
        printf("Execution continues in parent process\n");
}
sunbox$ gcc exec1.c -o exec1
sunbox$ ./exec1
In child process:
fork1.c      exec1        fork2       exec1.c      fork1
fork2.c      
Execution continues in parent process

清单 4 中的代码首先定义一个数组,其中第一个元素是要执行的二进制文件的路径,其余元素充当命令行参数。根据手册页的描述,该数组以 Null 结尾。在从 fork 系统调用返回以后,将指示子进程执行 (execv) 新的二进制文件。
execv 调用首先取得一个指向要运行的二进制文件名称的指针,然后取得一个指向您前面声明的参数数组的指针。该数组的第一个元素实际上是二进制文件的名称,因此参数实际上是从第二个元素开始的。请注意,该子进程一直没有从 execv 调用返回。这表明正在运行的进程已被新进程所替换。
还存在其他执行 (exec) 某个进程的系统调用,它们的区别在于接受参数的方式和是否需要传递环境变量。execv(2) 是替换当前映像的较简单方法之一,因为它不需要关于环境的信息,并且它使用以 Null 结尾的数组。其他选项包括 execl(2)(它单独接受各个参数)或 execvp(2)(它也接受一个以 Null 结尾的环境变量数组)。使问题复杂化的是,并非所有操作系统都支持所有变体。关于使用哪一种变体的决定取决于平台、编码风格和是否需要定义任何环境变量。

调用 fork 时,打开的文件会发生什么情况呢?
当某个进程复制它自身时,内核生成所有打开的文件描述符的副本。文件描述符是指向打开的文件或设备的整数,并用于执行读取和写入。如果在调用 fork 前,某个程序已经打开了一个文件,如果两个进程都尝试执行读取或写入,会发生什么情况呢?一个进程会改写另一个进程中的数据吗?是否会读取该文件的两个副本?清单 5 对此进行了研究,它打开两个文件——一个文件用于读取,另一个文件用于写入——并让父进程和子进程同时执行读取和写入。

清单 5. 同时对同一文件执行读取和写入的两个进程
                
#include <stdio.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void) {

        int fd_in, fd_out;
        char buf[1024];

        memset(buf, 0, 1024); /* clear buffer*/
        fd_in = open("/tmp/infile", O_RDONLY);
        fd_out = open("/tmp/outfile", O_WRONLY|O_CREAT);

        fork(); /* It doesn't matter about child vs parent */

        while (read(fd_in, buf, 2) > 0) { /* Loop through the infile */
                printf("%d: %s", getpid(), buf);
                /* Write a line */
                sprintf(buf, "%d Hello, world!\n\r", getpid());
                write(fd_out, buf, strlen(buf));
                sleep(1);
                memset(buf, 0, 1024); /* clear buffer*/
        }
        sleep(10);
}

sunbox$ gcc fdtest1.c -o fdtest1
sunbox$ ./fdtest1
2875: 1
2874: 2
2875: 3
2874: 4
2875: 5
2874: 6
2874: 7
sunbox$ cat /tmp/outfile
2875 Hello, world!
2874 Hello, world!
2875 Hello, world!
2874 Hello, world!
2875 Hello, world!
2874 Hello, world!
2874 Hello, world!

清单 5 是用于打开文件的简单程序,并派生 (fork) 为父进程和子进程。每个进程从同一文件描述符(它只是一个包含数字 1 至 7 的文本文件)执行读取操作,并连同 PID 一起打印所读取的内容。在读取一行之后,将 PID 写到输出文件。当输入文件中没有其他字符可供读取时,循环结束。
清单 5 的输出表明,当一个进程从该文件读取时,两个进程的文件指针都在移动。同样地,当向某个文件写入时,下一个字符被写到文件结尾。这是非常有意义的,因为内核跟踪打开文件的信息。文件描述符只不过是进程的标识符。
您可能还知道,标准输出(屏幕)也是一个文件描述符。此文件描述符在 fork 调用期间被复制,这就是两个进程都能对屏幕执行写入操作的原因。
父进程或子进程的终止
进程必须在某个时候终止。问题只是哪个进程首先终止:父进程还是子进程。
父进程在子进程之前终止
如果父进程在子进程之前终止,孤立的子进程需要知道它们的父进程是谁。记住,每个进程都有父进程,并且您可以跟踪从每个子进程一直到 PID 1(或称为 init)的整个进程家族树。当某个父进程终止时,init 将接纳所有子进程,如清单 6 所示。

清单 6. 在子进程之前终止的父进程
               
#include <unistd.h>
#include <stdio.h>

int main(void) {

        int i;
        if (fork()) {
                /* Parent */
                sleep(2);
                _exit(0);
        }
        for (i=0; i < 5; i++) {
                printf("My parent is %d\n", getppid());
                sleep(1);
        }
}
sunbox$ gcc die1.c -o die1
sunbox$ ./die1
My parent is 2920
My parent is 2920
sunbox$ My parent is 1
My parent is 1
My parent is 1

在此例中,父进程调用 fork,等待两秒钟,然后退出。子进程在五秒钟内继续打印其父 PID。可以看到,PPID 在父进程终止后更改为 1。Shell 提示符的返回也是非常有趣的。由于子进程在后台运行,父进程一终止,控制即返回到 Shell。
子进程在父进程之前终止
清单 7 与清单 6 相反——即在父进程之前终止的子进程。为更好地说明所发生的事情,进程本身中没有打印任何内容。而有趣的信息来自于进程清单。

清单 7. 子进程在父进程之前终止
               
sunbox$ cat die2.c
#include <unistd.h>
#include <stdio.h>

int main(void) {

        int i;
        if (!fork()) {
                /* Child exits immediately*/
                _exit(0);
        }
	/* Parent waits around for a minute */
        sleep(60);
}

sunbox$ gcc die2.c -o die2
sunbox$ ./die2 &
[1] 2934
sunbox$ ps -ef | grep 2934
    sean  2934  2885   0 21:43:05 pts/1       0:00 ./die2
    sean  2935  2934   0        - ?           0:00 <defunct>
sunbox$ ps -ef | grep 2934
[1]+  Exit 199                ./die2


die2 使用 & 操作符在后台运行,然后显示一个进程清单,并且仅显示正在运行的进程及其子进程。PID 2934 是父进程,PID 2935 是派生 (fork) 并立即终止的进程。尽管子进程提前退出,但它仍然在进程表中作为失效 (defunct) 进程存在,或称为僵死 (zombie) 进程。当父进程在 60 秒以后终止时,两个进程都消失了。
当子进程终止时,会使用一个名为 SIGCHLD 的信号来通知其父进程。该通知的确切机制现在对您并不重要。重要的是父进程必须以某种方式确认子进程的终止。子进程从终止时起就一直处于僵死状态,直到父进程确认该信号为止。僵死进程不运行或消耗 CPU 周期;它只是占用进程表空间。当父进程终止时,内核最终能够回收未确认的子进程以及父进程。这意味着可消除僵死进程的唯一方法是终止父进程。处理僵死进程的最好方法是首先确保它们不会发生。
清单 8 中的代码实现了一个处理传入的 SIGCHLD 信号的信号处理程序。
清单 8. 实际操作中的信号处理程序
                
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

void sighandler(int sig) {
        printf("In signal handler for signal %d\n", sig);
        /* wait() is the key to acknowledging the SIGCHLD */
        wait(0);
}

int main(void) {

        int i;
        /* Assign a signal handler to SIGCHLD */
        sigset(SIGCHLD, &sighandler);
        if (!fork()) {
                /* Child */
                _exit(0);
        }
        sleep(60);
}
sunbox$ gcc die3.c -o die3
sunbox$ ./die3 &
[1] 3116
sunbox$ In signal handler for signal 18
ps -ef | grep 3116
    sean  3116  2885   0 22:37:26 pts/1       0:00 ./die3

由于使用了 sigset 函数(它向信号处理程序分配一个函数指针),清单 8 比前一个示例稍微复杂一点,。每当进程接收到某个已处理的信号时,就会调用通过 sigset 分配的函数。对于 SIGCHLD 信号,应用程序必须调用 wait(3c) 函数,以等待子进程退出。由于该进程已经退出,这相当于向内核确认了子进程的终止。实际上,父进程所做的工作可能不只是确认该信息。它还可能需要清理子进程的数据。
在执行 die3 以后,代码检查了进程清单,并干净地执行子进程。然后使用值 18 (SIGCHLD) 来调用信号处理程序,确认子进程的退出,并且父进程返回到 sleep(60)。

总结
UNIX 进程是在某个进程调用 fork 时创建的,fork 将正在运行的可执行进程一分为二。然后该进程可以执行 exec 系列中的某个系统调用,从而将当前运行的映像替换为新的映像。
当父进程终止时,其所有子进程将由 PID 为 1 的 init 接纳。如果子进程在父进程之前终止,则会向父进程发送一个信号,然后子进程转变为僵死状态,直到该信号得到确认,或父进程被终止。
现在您已了解了进程是如何创建和销毁的,您已经为处理运行您系统的进程作了更好的准备,尤其是大量使用多进程的系统,例如 Apache。如果您需要执行某些故障排除,能够跟踪某个特定进程的进程树还允许您将任何应用程序追溯到创建它的进程。
分享到:
评论

相关推荐

    UNIX 进程揭秘(探索运行在 UNIX 操作系统下的进程的生命周期)

    在UNIX操作系统中,进程是操作系统资源分配的基本单位,它们代表正在执行的程序实例。了解进程的生命周期对于系统管理员和开发者来说至关重要,因为它涉及到系统性能、稳定性以及资源管理。本文将深入探讨UNIX进程的...

    UNIX操作系统教程——一切从零开始

    ### UNIX操作系统教程——一切从零开始 #### 一、UNIX操作系统概述 ##### 1.1 UNIX特点 **多任务(Multi-tasking):** UNIX操作系统是一个典型的多任务操作系统,这意味着它可以同时处理多个任务。与单任务操作系统...

    理解Unix进程_超清中文带目录版pdf

    1. **进程的概念与生命周期**:书中首先会介绍进程的基本定义,包括进程ID(PID)、进程状态(如就绪、运行、阻塞、结束等)以及进程的创建、执行、等待、结束等生命周期阶段。 2. **进程控制块(PCB)**:每个进程...

    Unix系统大全——系统管理员卷1

    《Unix系统大全——系统管理员卷1》是一本深入探讨Unix操作系统的权威著作,旨在帮助读者掌握Unix系统的管理和维护技能。本书全面涵盖了Unix操作系统的核心概念、常用命令及其在系统管理中的应用,是系统管理员和对...

    实验一 进程通信——管道和信号实验报告.doc

    【实验报告概述】 本次实验是关于操作系统中的进程...实验的实践操作有助于理解操作系统中的核心概念,特别是进程的生命周期、同步与通信机制。通过这些练习,学生能够更好地掌握实际操作系统中进程管理的原理和应用。

    UNIX操作系统(Solaris,AIX,UNIX).pdf

    Solaris 操作系统能在 Sparc 和 X86 环境下运行,也配有最新的文件系统 ZFS,近年来在这方面已取得了极大的进步,包括目前将其作为 ROOT 文件系统的能力。ZFS 具有成为最佳全方位 Unix 文件系统的潜质。 HP-UX 操作...

    操作系统实验——进程创建与进程间通信

    操作系统是计算机系统的核心组成部分,它负责管理系统的硬件资源和软件服务,为应用程序提供运行环境。在操作系统中,进程是程序的执行实例,每个进程都有自己的内存空间和上下文。本实验将深入探讨如何在操作系统中...

    理解Unix进程[图灵系列].pdf

    《理解Unix进程》是深入探讨Unix系统中进程管理机制的一本经典著作,它详细解析了Unix操作系统中的核心概念,帮助读者构建对Unix进程操作的全面理解。这本书属于图灵系列,通常这类书籍以其深入浅出的讲解方式和丰富...

    操作系统编程实验——创建进程.pdf

    操作系统编程实验——创建进程 操作系统编程实验——创建进程是操作系统编程的基础之一,本实验的目的是熟悉操作系统程序接口的使用,并学习如何使用fork()函数创建子进程。 知识点1:操作系统编程实验的重要性 ...

    UNIX进程调度详解

    在多任务操作系统中,尤其是像UNIX这样的分时系统中,进程调度是一项至关重要的功能。由于CPU是所有进程共享的关键资源之一,因此合理有效地分配CPU时间成为了操作系统的核心职责。进程调度不仅决定了哪个进程可以...

    理解Unix进程 扫描中文版.pdf

    《理解Unix进程》提供的许多简单而强大的技术,能够帮助Web开发人员深入了解Unix系统的并发性、守护进程、生成进程(spawning process)与信号等。同时,读者也可以使用这些技术和方法编写并调试自己的服务器。此外,...

    Unix操作系统设计

    2.1 UNIX操作系统的体系结构 2.2 系统概念介绍 2.2.1 文件子系统概貌 2.2.2 进程 2.3 内核数据结构 2.4 系统管理 2.5 本章小结 2.6 习题 第3章 数据缓冲区高速缓冲 3.1 缓冲头部 3.2 缓冲池的结构 3.3 缓冲区的检索...

    Unix进程管理说明

    2. 进程的存在是暂时的,有生命周期的,有创建、执行、消亡的一个过程,而程序的存在是永久的。 3. 进程是程序加数据两部分组成,程序和数据是不可分离的实体,数据是程序的操作对象。 4. 一个程序可以对应多个进程...

    返璞归真——UNIX技术内幕

    《返璞归真——UNIX技术内幕》是一本深入探讨UNIX操作系统的著作,旨在揭示其核心技术与设计理念。UNIX作为历史悠久且广泛应用于科学计算、网络服务、软件开发等领域的操作系统,其内在的精妙之处吸引着无数IT专业...

    理解Unix进程3

    书中细致地介绍了进程的创建、管理、终止等生命周期内的操作,使读者能够对进程有一个全面的认识。 系统调用是Unix进程章节中的另一个重要知识点。系统调用是进程与操作系统内核进行交互的主要方式,它允许用户空间...

    实验一-进程通信——管道和信号实验报告.doc

    - **进程的生命周期**:进程从创建到结束的整个过程。 - **进程控制**:包括进程的创建、挂起、恢复、终止等操作。 - **进程间通信(IPC)**:进程之间传递数据的方式,包括管道、信号量、共享内存等。 - **Unix系统...

    王道考研——操作系统PPT.zip

    "王道考研——操作系统PPT.zip" 提供的资料很可能是为了帮助备考者深入理解这一主题。在这个压缩包中,包含的"操作系统PPT"很可能是一系列关于操作系统的教学幻灯片,这些幻灯片通常涵盖了操作系统的主要章节和关键...

    E09:UNIX V6++进程的调度状态(参考答案)1

    在操作系统领域,特别是针对UNIX V6++这样的早期UNIX版本,进程调度是系统管理资源的关键机制。本篇将详细探讨在UNIX系统中进程的调度状态及其转换,特别关注核心态和用户态之间的区别。 1. 进程状态转换: 在UNIX...

Global site tag (gtag.js) - Google Analytics