fork子进程

本文详细介绍了Linux下fork函数的使用方法,解释了孤儿进程与僵尸进程的概念,并提供了具体的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、相关函数

fork函数:创建一个新的进程,当前进程是新进程的父进程
通过man 2 fork命令可以查看linux手册中第二节关于fork函数的介绍

 #include <unistd.h>
  pid_t fork(void);

fork函数会返回两次,一次在父进程、一次在子进程。它有3中可能的返回结果:

  • -1:创建失败,在父进程中返回-1
  • 0:创建成功,当前进程是子进程
  • 其他:创建成功,在父进程中返回子进程的pid(进程 ID)

需要注意:fork的操作比较重,fork出来的子进程会获得父进程的完整副本,当然了Linux不是在子进程一fork出来就将父进程的资源复制出一个副本给子进程的,Linux采用了写时复制,一开始两个进程是共享数据的,但是当子进程尝试修改数据,就会触发写时复制,为子进程创建一个该数据的副本。

linux系统还提供了一些函数,用来获取进程 ID,查看手册man 2 getpid结果如下:

 #include <unistd.h>
 //获取当前进程的pid
 pid_t getpid(void);
 //获取父进程的pid
 pid_t getppid(void);

二、详解

1、fork的使用

编写 forkdemo.c,代码如下:

#include <unistd.h>
#include <stdio.h>
int main(){
    printf("before fork\n");
    pid_t pid=fork();
    if(pid == -1){
      printf("创建子进程失败");
    }else if(pid == 0){
      printf("我是子进程,我的pid是:%d,父进程的pid是:%d\n",getpid(),getppid());
    }else{
      sleep(1);
      printf("我是父进程,我的pid是:%d,子进程的pid是:%d\n",getpid(),pid);
    }
    printf("after fork\n");
}

编译 gcc forkdemo.c -o forkdemo 后执行./forkdemo,结果如下:

before fork
我是子进程,我的pid是:28473,父进程的pid是:28472
after fork
我是父进程,我的pid是:28472,子进程的pid是:28473
after fork

从结果可以看出,fork函数会返回两次,在子进程中返回0,父进程中返回的是子进程的pid,这里让父进程sleep 1s是为了保证子进程先执行完。

  • 问题:子进程中为什么也是从fork出开始执行,为什么不从main函数开始执行?
    通过执行 fork系统调用,子进程获得父进程中数据空间,堆和栈等的副本,但它们并不共享存储空间,只共享代码段。由于把当前运行的位置都复制到子进程 ,那么子进程当然也是接着fork继续执行。

2、孤儿进程

现在如果我让子进程 sleep(1), 让父进程先执行完会如何呢?修改后代码如下:

#include <unistd.h>
#include <stdio.h>
int main(){
    printf("before fork\n");
    pid_t pid=fork();
    if(pid == -1){
      printf("创建子进程失败");
    }else if(pid == 0){
      //子进程休眠1s
      sleep(1);
      printf("我是子进程,我的pid是:%d,父进程的pid是:%d\n",getpid(),getppid());
    }else{
      printf("我是父进程,我的pid是:%d,子进程的pid是:%d\n",getpid(),pid);
    }
    printf("after fork\n");
}

编译执行后结果如下:

before fork
我是父进程,我的pid是:28513,子进程的pid是:28514
after fork
我是子进程,我的pid是:28514,父进程的pid是:1
after fork
  • 问题:父进程先结束了,然后子进程的父进程不再是创建它的那个进程了,这是为何?

要知道为什么有这个结果,首先要知道什么是孤儿进程?
孤儿进程:父进程先于子进程结束,则子进程会成为孤儿进程。
借助现实的例子:作为一个单亲父亲的孩子,如果他的父亲死亡了,那么这个孩子就会成为孤儿,会有专门的机构"孤儿院"来收养这个孩子。
Linux中托孤的行为和这个例子很相似,当父进程运行结束,但子进程还在运行,那么这个子进程就会变为孤儿进程,为避免孤儿进程退出时无法释放相关的资源(进程描述符)而变为僵尸进程,所以孤儿进程会被 init 进程(pid 为 1 )所 “收养”,由init进程来进行“善后”。

3、僵尸进程

当一个进程完成它的工作终止后,它的所有资源都会被回收,但是该进程的进程描述符依然被保留了,这是为了让父进程可以知道子进程的退出状态,因此进程结束后所占用资源的回收和进程描述符的释放是分开的。它的父进程可以调用wait()或waitpid()系统调用来取得子进程的终止状态,然后内核负责回收子进程残留的进程描述符等资源。需要注意这其实就是僵尸进程产生的原因,虽然子进程结束了,但是它的进程描述符依然被保留,没被释放。

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(){
    printf("before fork\n");
    pid_t pid=fork();
    if(pid == -1){
      printf("创建子进程失败");
    }else if(pid == 0){
      sleep(3);
      printf("我是子进程,我的pid是:%d,父进程的pid是:%d\n",getpid(),getppid());
    }else{
      printf("我是父进程,我的pid是:%d,子进程的pid是:%d\n",getpid(),pid);
      int status;
      //作用:阻塞等待子进程结束、获取子进程的退出状态status、回收子进程残留资源
      wait(&status);
     printf("我是父进程,子进程已经被回收,我也要结束了\n");
    }
    printf("after fork\n");
}
### Hello程序在Bash Shell中的Fork进程创建过程及作用 #### 1. Fork系统调用简介 在Unix/Linux系统中,`fork()` 是一种重要的系统调用,用于创建新进程。通过 `fork()`,当前进程(称为父进程)会复制自己,从而生成一个几乎完全相同的副本(称为子进程)。这两个进程的区别在于返回值:对于父进程而言,`fork()` 返回的是子进程的PID;而对于子进程中,`fork()` 的返回值为0[^2]。 #### 2. Hello程序的执行流程 假设有一个简单的Shell脚本或命令名为 `hello`,它的主要功能是打印字符串 "Hello, World!" 到标准输出设备。以下是其可能的实现方式及其背后的进程创建机制: ```bash #!/bin/bash echo "Hello, World!" ``` 当用户在终端输入此命令时,实际上经历了一系列复杂的内部操作,其中包括但不限于以下几个重要环节: ##### (a) 解析命令行输入 首先,Shell负责解析用户的输入,并决定如何处理这条命令。如果发现这是一个外部可执行文件而非内置命令,则进入下一步骤准备加载并运行该目标程序[^3]。 ##### (b) 创建子进程 为了不影响正在运行的主Session(即Interactive Session),通常情况下不会直接让Parent Process去执行新的应用程序,而是先调用 `fork()` 来派生出一个新的Child Process。此时两者共享大部分资源但各自拥有独立的任务上下文环境[^1]。 - **父进程行为**:继续等待子进程完成任务之后再恢复自己的活动; - **子进程行为**:接管后续的实际业务逻辑运算部分——在这里指代启动真正的 `/path/to/hello` 应用本身。 ##### (c) 替换原有代码段 一旦成功建立了Subprocess实例后,紧接着就需要改变原先仅有的空壳状态以便真正开始运作指定的服务内容。这一步借助于Exec系列函数家族成员共同协作达成目的。具体来说,就是把原本属于Copy-On-Write模式下的临时镜像替换成为实际所需的Binary Image,进而激活关联的方法集以供调用[^4]。 例如,在C语言层面模拟这一场景可以写出如下示范片段: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(){ pid_t child_pid; /* Create Child */ child_pid = fork(); if(child_pid == 0){ // In the context of CHILD PROCESS now... execl("/absolute/path/to/your/executable", "/absolute/path/to/your/executable", NULL); perror("execl failed"); // If reach here means error occurred during exec() _exit(EXIT_FAILURE); // Terminate immediately with failure status code }else{ // Parent process waits until child finishes execution before proceeding further. wait(NULL); } return EXIT_SUCCESS; } ``` 此处需要注意几点事项: - 绝大多数现代操作系统都会优化掉不必要的内存占用现象,因此即使表面上看起来像是完整的克隆动作,但实际上只会在必要时刻才会触发真实的物理拷贝动作。 - Exec族函数群的存在意义就在于无缝衔接起始点与终点之间的桥梁角色定位,确保整体架构清晰明了的同时还能兼顾性能表现方面的要求。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值