UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲

lienhua34
2014-10-05

1 进程控制三部曲概述

UNIX 系统提供了 fork、exec、exit 和 wait 等基本的进程控制原语。通过这些进程控制原语,我们即可完成对进程创建、执行和终止等基本操作。进程的控制可以划分为三部曲,

• 第一部:fork 创建新进程。

• 第二部:exec 执行新程序。

• 第三部:exit 和 wait 处理终止和等待终止。

2 第一部:fork 创建新进程

在一个现有的进程中,我们可以通过调用 fork 函数来创建一个新进程,

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程中返回子进程ID,出错则返回-1

由 fork 创建的新进程被称为子进程。fork 函数调用一次,但返回两次。两次返回的唯一区别是:子进程返回值为 0,而父进程的返回值是新子进程的进程 ID。因为 UNIX 系统没有提供一个函数以获取某个进程的所有子进程 ID,所以父进程可以通过 fork 函数的返回值获取其子进程的进程 ID,并进行后续的处理。而在子进程中,可以通过调用 getppid 函数获取其父进程的进程 ID。

fork 函数返回之后,子进程和父进程都各自继续执行 fork 调用之后的指令。子进程是父进程的副本。例如,子进程获得了父进程数据空间、堆和栈的副本。但是,父子进程共享正文段。

例子:

下面程序调用 fork 创建一个新进程,在父子进程中都分别打印当前进程 ID 和父进程 ID。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int
main(void)
{
    pid_t pid;
    printf("before fork\n");
    if ((pid = fork()) < 0) {
        printf("fork error: %s\n", strerror(errno));
        exit(-1);
     } else if (pid == 0) {
        printf("in child process, process ID: %d, parent process ID: %d\n", getpid(),  getppid());
    } else {
        printf("in parent process, process ID: %d, child process ID: %d\n", getpid(), pid);
        sleep(1);
    }
    exit(0);
}

编译该程序,生成 forkdemo 文件,然后执行该文件,

lienhua34:demo$ gcc -o forkdemo forkdemo.c
lienhua34:demo$ ./forkdemo
before fork
in parent process, process ID: 3499, child process ID: 3500
in child process, process ID: 3500, parent process ID: 3499

第二部:exec 执行新程序

用 fork 函数创建子进程后,子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程执行的程序完全替换为新程序,而新程序则从其 main 函数开始执行。调用 exec 并没有创建新进程,所以进程 ID 没有改变,exec 只是用一个新的程序替换了当前进程的正文、数据、堆和栈段。

UNIX 提供了 6 种不同的 exec 函数可供使用。我们在这里只说明其中的一种,

#include <unistd.h>
int execv(const char *pathname, char *const argv[]);
返回值:若出错则返回-1,若成功则没有返回值

其中 pathname 是进程要执行的新程序文件的路径,而 argv 参数则是要传递给新程序的参数列表。

例子:

我们有一个 sayhello.c 的程序文件,其代码如下,

#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
    printf("Hello World! process ID: %d\n", getpid());
    exit(0);
}

编译 sayhello.c 程序,生成执行文件 sayhello,

lienhua34:demo$ gcc -o sayhello sayhello.c
lienhua34:demo$ pwd
/home/lienhua34/program/c/apue/ch08/demo
lienhua34:demo$ ./sayhello
Hello World! process ID: 3545

在 execdemo.c 程序文件中,我们通过 fork 创建新进程,然后在子进程中调用 execv 函数执行 sayhello 文件,其代码如下,

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
char sayhelloPath[] = "/home/lienhua34/program/c/apue/ch08/demo/sayhello";
int
main(void)
{
    pid_t pid;
    if ((pid = fork()) < 0) {
        printf("fork error: %s\n", strerror(errno));
        exit(-1);
    } else if (pid == 0) {
        printf("in child process, process ID: %d, parent process ID: %d\n", getpid(), getppid());
        if (execv(sayhelloPath, NULL) < 0) {
            printf("execv error: %s\n", strerror(errno));
            exit(-1);
        }
    } else {
        printf("in parent process, process ID: %d, child process ID: %d\n", getpid(), pid);
        sleep(2);
    }
    exit(0);
}

编译 execdemo.c 文件,生成并执行 execdemo 文件,

lienhua34:demo$ gcc -o execdemo execdemo.c
lienhua34:demo$ ./execdemo
in parent process, process ID: 3561, child process ID: 3562
in child process, process ID: 3562, parent process ID: 3561
Hello World! process ID: 3562

从上面的运行结果可以看出,子进程(ID:3562)正常执行 sayhello 文件。

4 第三部:exit 和 wait 处理终止和等待终止

exit 函数提供了进程终止的功能,在“进程终止”一文中,我们已经学习了 exit 函数的基本知识。我们这里补充一下关于进程的终止状态。在学习 exit 函数时,我们已经知道 exit 函数的参数作为程序的退出状态(exit status)。在进程终止时,内核会将程序的退出状态作为进程的终止状态(termination status)。在进程异常终止的时候,内核会产生一个指示其异常终止原因的终止状态。无论进程是正常终止还是异常终止,该终止进程的父进程都可以通过 wait 或 waitpid 函数获取其终止状态。

在前面两节中的 forkdemo.c 和 execdemo.c 程序中,fork 调用之后的父进程都调用了 sleep 函数休眠一下。这是因为,在调用 fork 函数之后是父进程先执行还是子进程先执行是不确定的。于是,我们在父进程中调用sleep 休眠一段时间,让子进程先执行结束(注:这样子也不能确保)。

我们可以在父进程中调用 wait 来等待子进程结束,

#include <sys/wait.h>
pid_t wait(int *statloc);
返回值:若成功则返回进程ID,若出错则返回-1

statloc 参数是一个整型指针。如果 statloc 不是一个空指针,则终止进程的终止状态就存储在该指针指向的内存单元中。如果不关心终止状态,则可以将参数设置为 NULL。

进程调用 wait 会导致该调用者阻塞,直到某个子进程终止。如果调用wait 函数时,调用进程没有任何子进程则立即出错返回。

例子:

下面程序在上一节的 execdemo.c 基础上,将 fork 之后父进程的 sleep语句替换成 wait 来等待子进程的结束。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
char sayhelloPath[] = "/home/lienhua34/program/c/apue/ch08/demo/sayhello";
int
main(void)
{
    pid_t pid;
    int waitres;
    if ((pid = fork()) < 0) {
        printf("fork error: %s\n", strerror(errno));
        exit(-1);
    } else if (pid == 0) {
        printf("in child process, process ID: %d, parent process ID: %d\n", getpid(), getppid());
        if (execv(sayhelloPath, NULL) < 0) {
            printf("execv error: %s\n", strerror(errno));
            exit(-1);
       }
    } else {
        printf("in parent process, process ID: %d, child process ID: %d\n", getpid(), pid);
        if ((waitres = wait(NULL)) < 0) {
            printf("wait error: %s\n", strerror(errno));
            exit(-1);
        } else {
            printf("termination child process: %d\n", waitres);
        }
    }
    exit(0);
}

编译该程序,生成并执行 execdemo 文件,

lienhua34:demo$ gcc -o execdemo execdemo.c
lienhua34:demo$ ./execdemo
in parent process, process ID: 3795, child process ID: 3796
in child process, process ID: 3796, parent process ID: 3795
Hello World! process ID: 3796
termination child process: 3796

(done)

时间: 2024-12-25 03:09:13

UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲的相关文章

UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数

lienhua342014-10-12 当一个进程正常或者异常终止时,内核就向其父进程发送 SIGCHLD信号.父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用的函数(信号处理程序).对于这种信号的系统默认动作是忽略它. 在文档“进程控制三部曲”中,我们讲的第三部曲是使用 wait 函数来获取终止子进程的终止状态.那么,有几个问题我们这里需要详细的学习一下. 1. 父进程一定能够获取到子进程的终止状态吗?如果子进程在父进程调用 wait 函数前就终止了,怎么办? 2. 如果父进程没有获

UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习

lienhua342014-10-07 在“进程控制三部曲”中,我们学习到了 fork 是三部曲的第一部,用于创建一个新进程.但是关于 fork 的更深入的一些的东西我们还没有涉及到,例如,fork 创建的新进程与调用进程之间的关系.父子进程的数据共享问题等.fork 是否可以无限制的调用?如果不行的话,最大限制是多少?另外,我们还将学习一个 fork 的变体 vfork. 1 fork 创建的新进程与调用进程之间的关系 UNIX 操作系统中的所有进程之间的关系呈现一个树形结构.除了进程 ID

UNIX环境编程学习笔记(17)——进程管理之进程的几个基本概念

lienhua342014-10-05 1 main 函数是如何被调用的? 在编译 C 程序时,C 编译器调用链接器在生成的目标可执行程序文件中,设置一个特殊的启动例程为程序的起始地址.当内核执行 C 程序时,在调用 main 前先调用这个特殊的启动例程,该启动例程从内核取得命令行参数和环境变量值. 2 共享库 共享库使得可执行文件中不再需要包含共用的库例程,而只需在所有进程都可引用的存储区中维护这种库例程的一个副本.程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链

UNIX环境编程学习笔记(20)——进程管理之exec 函数族

lienhua342014-10-07 在文档“进程控制三部曲”中,我们提到 fork 函数创建子进程之后,通常都会调用 exec 函数来执行一个新程序.调用 exec 函数之后,该进程就将执行的程序替换为新的程序,而新的程序则从 main 函数开始执行. UNIX 提供了 6 种不同的 exec 函数供我们使用.它们的原型如下所示, #include <unistd.h>int execl(const char *pathname, const char *arg0, ... /* (cha

UNIX环境编程学习笔记(24)——信号处理进阶学习之信号集和进程信号屏蔽字

lienhua342014-11-03 1 信号传递过程 信号源为目标进程产生了一个信号,然后由内核来决定是否要将该信号传递给目标进程.从信号产生到传递给目标进程的流程图如图 1 所示, 图 1: 信号产生.传递到处理的流程图 进程可以阻塞信号的传递.当信号源为目标进程产生了一个信号之后,内核会执行依次执行下面操作, 1. 如果目标进程设置了忽略该信号,则内核直接将该信号丢弃. 2. 如果目标进程没有阻塞该信号,则内核将该信号传递给目标进程,由目标进程执行相对应操作. 3. 如果目标进程设置阻塞

UNIX环境编程学习笔记(26)——多线程编程(一):创建和终止线程

lienhua342014-11-08 在进程控制三部曲中我们学习了进程的创建.终止以及获取终止状态等的进程控制原语.线程的控制与进程的控制有相似之处,在表 1中我们列出了进程和线程相对应的控制原语. 表 1: 进程原语和线程原语的比较 进程原语 线程原语 描述 fork pthread_create 创建新的控制流 exit pthread_exit 从现有的控制流中退出 waitpid pthread_join 从控制流中得到退出状态 atexit pthread_cleanup_push

UNIX环境编程学习笔记(23)——信号处理初步学习

lienhua342014-10-29 1 信号的概念 维基百科中关于信号的描述是这样的: 在计算机科学中,信号(英语:Signals)是 Unix.类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式.它是一种异步的通知机制,用来提醒进程一个事件已经发生.当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断.如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数. 关于这段描述,我们可以从中学习到下面几点关于信号

UNIX环境编程学习笔记(9)——文件I/O之文件访问权限的屏蔽和更改

lienhua342014-09-10 1 文件访问权限 在文件访问权限和进程访问控制中,我们已经讲述过文件访问权限位,为了方便,我们重新列在下面, 表 1: 文件的 9 个访问权限位  st_mode 屏蔽  意义  S_IRUSR  用户 -读  S_IWUSR  用户 -写  S_IXUSR  用户 -执行  S_IRGRP   组 -读  S_IWGRP  组 -写  S_IXGRP  组 -执行  S_IROTH  其他 -读  S_IWOTH  其他 -写  S_IXOTH  其他

UNIX环境编程学习笔记(3)——文件I/O之内核 I/O 数据结构

lienhua342014-08-27 内核使用三种数据结构表示打开的文件,分别是文件描述符表.文件表和 V 节点表. (1) 每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,每个描述符占用一项.与每个文件描述符相关联的是: (a) 文件描述符标志. (b) 指向一个文件表项的指针. (2) 内核为所有打开文件维持一张文件表.每个文件表项包含: (a) 文件状态标志(读.写.添写.同步和非阻塞等). (b) 当前文件偏移量. (c) 指向该文件 V 节点表项的指针. (3)