C语言多进程创建和回收的实现实例

在操作系统中,多进程编程是实现并发执行的重要方式。通过fork()系统调用,父进程可以创建子进程,子进程几乎完全复制父进程的地址空间,从而实现任务的并行处理。本文将通过一个完整的C语言实例,详细介绍多进程的创建与回收过程,包括关键函数的使用、进程间的区别、以及如何正确管理和回收子进程,确保程序的稳定性和资源的有效利用。

图片[1]_C语言多进程创建和回收的实现实例_知途无界

一、多进程基础概念

1. 进程与程序

  • 程序​:是存储在磁盘上的可执行文件,是一系列指令的集合。
  • 进程​:是程序在操作系统中的一次执行实例,拥有独立的内存空间、资源及系统状态。

2. fork()系统调用

fork()是创建新进程的主要方式。它被调用一次,但返回两次:一次在父进程中返回子进程的PID(进程ID),一次在子进程中返回0。通过返回值的不同,可以区分父进程和子进程。

3. 进程标识符

  • PID(Process ID)​​:每个进程的唯一标识符。
  • PPID(Parent Process ID)​​:父进程的PID。

4. 进程的生命周期

  • 创建​:通过fork()创建子进程。
  • 执行​:父进程和子进程并发执行,操作系统调度它们的运行。
  • 终止​:进程完成任务后通过exit()_exit()终止。
  • 回收​:父进程通过wait()waitpid()回收子进程的资源,避免僵尸进程的产生。

二、多进程创建与回收的实现步骤

1. 包含必要的头文件

在C语言中,进行多进程编程需要包含以下头文件:

  • <stdio.h>:标准输入输出函数。
  • <stdlib.h>:标准库函数,如exit()
  • <unistd.h>:提供fork()getpid()getppid()等系统调用。
  • <sys/types.h>:提供进程相关的类型定义,如pid_t
  • <sys/wait.h>:提供进程等待相关的函数,如wait()waitpid()

2. 使用fork()创建子进程

fork()函数的原型为:

#include <unistd.h>
pid_t fork(void);
  • 返回值​:
    • 在父进程中,返回子进程的PID(大于0)。
    • 在子进程中,返回0。
    • 如果出错,返回-1。

3. 区分父进程和子进程

通过判断fork()的返回值,可以确定当前代码是在父进程还是子进程中执行。

4. 子进程与父进程的执行

父进程和子进程从fork()返回处开始并发执行,操作系统根据调度算法决定它们的执行顺序。

5. 进程的终止

  • 子进程​:通常在完成任务后调用exit()_exit()终止。
  • 父进程​:需要调用wait()waitpid()等待子进程结束并回收其资源。

6. 回收子进程

  • ​**wait()**​:阻塞父进程,直到任意一个子进程结束。
  • ​**waitpid()**​:可以指定等待某个特定的子进程,支持非阻塞模式。

三、完整实例代码

以下是一个完整的C语言示例程序,演示了如何创建多个子进程并正确回收它们。该程序创建指定数量的子进程,每个子进程打印自己的PID和PPID,然后退出。父进程等待所有子进程结束后再退出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    int num_children = 3; // 指定要创建的子进程数量
    pid_t pid;
    int i;
    int status;

    printf("父进程开始,PID = %d\n", getpid());

    for(i = 0; i < num_children; i++) {
        pid = fork(); // 创建子进程

        if(pid < 0) {
            // fork失败
            perror("fork失败");
            exit(EXIT_FAILURE);
        } else if(pid == 0) {
            // 子进程
            printf("子进程 %d 开始,PID = %d,PPID = %d\n", i+1, getpid(), getppid());
            // 子进程完成任务后退出
            sleep(1); // 模拟任务执行
            printf("子进程 %d 结束,PID = %d\n", i+1, getpid());
            exit(EXIT_SUCCESS); // 子进程退出,返回0
        } else {
            // 父进程,继续循环创建下一个子进程
            // 这里不等待,让子进程并发执行
        }
    }

    // 父进程等待所有子进程结束
    for(i = 0; i < num_children; i++) {
        pid = wait(&status); // 等待任意一个子进程结束
        if(pid < 0) {
            perror("wait失败");
            exit(EXIT_FAILURE);
        }
        if(WIFEXITED(status)) {
            printf("父进程:子进程 PID = %d 正常退出,退出状态 = %d\n", pid, WEXITSTATUS(status));
        } else if(WIFSIGNALED(status)) {
            printf("父进程:子进程 PID = %d 被信号终止,信号编号 = %d\n", pid, WTERMSIG(status));
        }
    }

    printf("父进程结束,PID = %d\n", getpid());
    return EXIT_SUCCESS;
}

四、代码解析

1. 包含头文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
  • ​**stdio.h**​:用于标准输入输出,如printf
  • ​**stdlib.h**​:包含exit函数。
  • ​**unistd.h**​:包含forkgetpidgetppid等系统调用。
  • ​**sys/types.h**​:定义了pid_t等类型。
  • ​**sys/wait.h**​:包含waitwaitpid函数及相关宏。

2. 主函数

int main() {
    int num_children = 3; // 指定要创建的子进程数量
    pid_t pid;
    int i;
    int status;

    printf("父进程开始,PID = %d\n", getpid());
  • ​**num_children**​:定义要创建的子进程数量,这里设置为3。
  • ​**pid**​:用于存储fork()的返回值。
  • ​**status**​:用于存储子进程的退出状态。

3. 创建子进程

    for(i = 0; i < num_children; i++) {
        pid = fork(); // 创建子进程

        if(pid < 0) {
            // fork失败
            perror("fork失败");
            exit(EXIT_FAILURE);
        } else if(pid == 0) {
            // 子进程
            printf("子进程 %d 开始,PID = %d,PPID = %d\n", i+1, getpid(), getppid());
            // 子进程完成任务后退出
            sleep(1); // 模拟任务执行
            printf("子进程 %d 结束,PID = %d\n", i+1, getpid());
            exit(EXIT_SUCCESS); // 子进程退出,返回0
        } else {
            // 父进程,继续循环创建下一个子进程
            // 这里不等待,让子进程并发执行
        }
    }
  • ​**fork()**​:创建子进程。循环中每次调用fork()都会创建一个新的子进程。
  • 返回值判断​:
    • ​**pid < 0**​:fork()失败,输出错误信息并退出程序。
    • ​**pid == 0**​:当前代码在子进程中执行。子进程打印自己的PID和PPID,模拟执行任务(通过sleep(1)),然后退出。
    • ​**pid > 0**​:当前代码在父进程中执行,pid为刚创建的子进程的PID。父进程继续循环创建下一个子进程,不等待子进程结束,使得子进程能够并发执行。

4. 回收子进程

    // 父进程等待所有子进程结束
    for(i = 0; i < num_children; i++) {
        pid = wait(&status); // 等待任意一个子进程结束
        if(pid < 0) {
            perror("wait失败");
            exit(EXIT_FAILURE);
        }
        if(WIFEXITED(status)) {
            printf("父进程:子进程 PID = %d 正常退出,退出状态 = %d\n", pid, WEXITSTATUS(status));
        } else if(WIFSIGNALED(status)) {
            printf("父进程:子进程 PID = %d 被信号终止,信号编号 = %d\n", pid, WTERMSIG(status));
        }
    }

    printf("父进程结束,PID = %d\n", getpid());
    return EXIT_SUCCESS;
}
  • ​**wait(&status)**​:父进程调用wait()函数阻塞,直到任意一个子进程结束。status变量用于存储子进程的退出状态。
  • 循环回收​:父进程通过循环调用wait(),确保所有子进程都被回收。每次wait()调用会回收一个结束的子进程。
  • 退出状态解析​:
    • ​**WIFEXITED(status)**​:判断子进程是否正常退出(通过exit()_exit())。
      • ​**WEXITSTATUS(status)**​:获取子进程的退出状态码。
    • ​**WIFSIGNALED(status)**​:判断子进程是否被信号终止。
      • ​**WTERMSIG(status)**​:获取终止子进程的信号编号。
  • 父进程结束​:所有子进程回收后,父进程打印结束信息并退出。

五、编译与运行

1. 编译代码

使用gcc编译上述代码,例如将代码保存为multi_process.c,则编译命令为:

gcc multi_process.c -o multi_process

2. 运行程序

在终端中运行生成的可执行文件:

./multi_process

3. 示例输出

运行结果可能类似于以下内容(具体的PID和顺序可能因系统调度不同而有所变化):

父进程开始,PID = 12345
子进程 1 开始,PID = 12346,PPID = 12345
子进程 2 开始,PID = 12347,PPID = 12345
子进程 3 开始,PID = 12348,PPID = 12345
子进程 1 结束,PID = 12346
父进程:子进程 PID = 12346 正常退出,退出状态 = 0
子进程 2 结束,PID = 12347
父进程:子进程 PID = 12347 正常退出,退出状态 = 0
子进程 3 结束,PID = 12348
父进程:子进程 PID = 12348 正常退出,退出状态 = 0
父进程结束,PID = 12345

说明​:

  • 父进程首先打印自己的PID,然后创建三个子进程。
  • 子进程打印自己的PID和PPID,模拟执行任务(通过sleep(1)暂停1秒),然后退出。
  • 父进程通过wait()依次回收每个子进程,并打印子进程的退出状态。
  • 最终,父进程结束。

注意​:由于并发执行的特性,子进程的输出顺序可能与创建顺序不同,具体顺序取决于操作系统的调度。

六、关键点与注意事项

1. fork()的返回值

  • 父进程通过判断fork()的返回值是否大于0来识别自己是父进程,并获取子进程的PID。
  • 子进程通过判断fork()的返回值是否等于0来识别自己是子进程。
  • fork()返回-1,则表示创建子进程失败,需处理错误情况。

2. 子进程与父进程的并发执行

  • 父进程和子进程从fork()返回处开始并发执行,操作系统根据调度算法决定它们的执行顺序。
  • 在上述示例中,父进程不等待子进程结束,使得子进程能够并发执行。如果父进程需要等待特定子进程,可以使用waitpid()

3. 进程的终止

  • 子进程通过调用exit(EXIT_SUCCESS)exit(EXIT_FAILURE)正常终止,返回相应的状态码。
  • 也可以使用_exit()函数立即终止进程,但不执行一些清理操作(如刷新标准I/O缓冲区)。

4. 回收子进程

  • 父进程必须回收子进程,否则子进程将成为僵尸进程(Zombie Process)​,占用系统资源。
  • 僵尸进程​:子进程已经终止,但其退出状态尚未被父进程回收,进程表中仍保留其条目。
  • 使用wait()waitpid()可以回收子进程,获取其退出状态,并释放相关资源。

5. 避免僵尸进程

  • 确保每个子进程的退出都被父进程通过wait()waitpid()回收。
  • 可以使用信号处理机制(如SIGCHLD)来异步回收子进程,但需谨慎处理,以避免遗漏。

6. 错误处理

  • 在调用fork()后,需检查返回值,处理可能的错误情况,如系统资源不足导致无法创建新进程。
  • 在调用wait()后,也需检查返回值,处理可能的错误,如没有子进程可等待。

七、扩展:使用waitpid()进行更精确的控制

waitpid()函数提供了比wait()更灵活的进程等待方式,允许父进程等待特定的子进程,或者以非阻塞模式等待。

waitpid()函数原型

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

参数说明

  • ​**pid**​:
    • > 0:等待进程ID等于pid的子进程。
    • -1:等待任意一个子进程(类似于wait())。
    • 0:等待与父进程同组的任意一个子进程。
    • < -1:等待进程组ID等于|pid|的任意一个子进程。
  • ​**status**​:用于存储子进程的退出状态,与wait()中的status类似。
  • ​**options**​:
    • 0:阻塞等待,直到指定的子进程结束。
    • WNOHANG:非阻塞模式,如果没有子进程结束,立即返回。
    • 其他选项可参考waitpid()的文档。

示例:使用waitpid()等待特定子进程

以下示例展示了如何使用waitpid()等待特定的子进程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid1, pid2;
    int status;

    printf("父进程开始,PID = %d\n", getpid());

    pid1 = fork();
    if(pid1 < 0) {
        perror("fork1失败");
        exit(EXIT_FAILURE);
    } else if(pid1 == 0) {
        // 子进程1
        printf("子进程1 开始,PID = %d,PPID = %d\n", getpid(), getppid());
        sleep(2);
        printf("子进程1 结束,PID = %d\n", getpid());
        exit(EXIT_SUCCESS);
    } else {
        pid2 = fork();
        if(pid2 < 0) {
            perror("fork2失败");
            exit(EXIT_FAILURE);
        } else if(pid2 == 0) {
            // 子进程2
            printf("子进程2 开始,PID = %d,PPID = %d\n", getpid(), getppid());
            sleep(1);
            printf("子进程2 结束,PID = %d\n", getpid());
            exit(EXIT_SUCCESS);
        } else {
            // 父进程,等待子进程2先结束
            pid_t terminated_pid = waitpid(pid2, &status, 0);
            if(terminated_pid < 0) {
                perror("waitpid失败");
                exit(EXIT_FAILURE);
            }
            if(WIFEXITED(status)) {
                printf("父进程:子进程 PID = %d 正常退出,退出状态 = %d\n", terminated_pid, WEXITSTATUS(status));
            }

            // 等待子进程1结束
            terminated_pid = waitpid(pid1, &status, 0);
            if(terminated_pid < 0) {
                perror("waitpid失败");
                exit(EXIT_FAILURE);
            }
            if(WIFEXITED(status)) {
                printf("父进程:子进程 PID = %d 正常退出,退出状态 = %d\n", terminated_pid, WEXITSTATUS(status));
            }

            printf("父进程结束,PID = %d\n", getpid());
            return EXIT_SUCCESS;
        }
    }
}

说明​:

  • 父进程创建了两个子进程pid1pid2
  • 父进程使用waitpid()首先等待pid2结束,然后等待pid1结束。
  • 通过指定不同的pid参数,父进程可以精确控制等待哪个子进程。

八、总结

通过本文的介绍和示例代码,您应该对C语言中多进程的创建与回收有了全面的了解。关键点包括:

  1. 使用fork()创建子进程​:理解fork()的返回值及其在父进程和子进程中的不同含义。
  2. 区分父进程和子进程​:通过返回值判断当前代码在哪个进程中执行,从而执行不同的逻辑。
  3. 进程的终止与回收​:子进程通过exit()正常终止,父进程通过wait()waitpid()回收子进程,避免僵尸进程的产生。
  4. 并发执行​:父进程和子进程并发执行,操作系统根据调度算法决定它们的执行顺序。
  5. 错误处理​:在创建和回收进程时,需处理可能的错误情况,确保程序的健壮性。

多进程编程在需要并行处理、提高程序效率的场景中非常有用,但也带来了进程管理、资源共享与同步等复杂性。在实际开发中,需根据具体需求选择合适的进程管理策略,并结合进程间通信(IPC)机制,实现高效、稳定的多进程应用。

注意​:多进程编程在不同的操作系统上可能有所差异,本文以类Unix系统(如Linux、macOS)为例,使用了POSIX标准的系统调用。在Windows系统上,进程管理的方式有所不同,需使用Windows API进行相应操作。

© 版权声明
THE END
喜欢就点个赞,支持一下吧!
点赞10 分享
评论 抢沙发
头像
欢迎您留下评论!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容