【Linux】进程的生命之旅——诞生、消逝与守候(fork/exit/wait)

article/2025/7/28 8:39:05

🎬 个人主页:谁在夜里看海.

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 一念既出,万山无阻


目录

📖一、进程创建

1.fork函数

📚高层封装特性

📚fork返回值

2.写时拷贝

3.调用失败

📚资源耗尽

📚进程数限制

📚内核限制

📖二、进程终止

1.退出场景

2.status退出码

3.退出方法

📚exit函数

📚_exit函数

📚main函数返回

📖三、进程等待

1.wait方法

📚语法

📚总结

2.waitpid方法

📚语法

📚总结


📖一、进程创建

1.fork函数

操作系统中进程的创建通常是通过系统调用实现的,在Linux中是通过fork(),它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。        

#include<unistd.h> // 使用需包含头文件unistd.h
pid_t pid = fork(); // 在子进程中返回0,父进程中返回子进程pid,出错返回-1

fork是操作系统提供的一种高层封装,它抽象了进程创建的复杂过程,fork将底层的一系列操作封装在一个简单的系统调用中,屏蔽了许多复杂的细节:

📚高层封装特性

① 简化进程创建的步骤:

fork的调用接口非常简洁,只需要调用一次,系统会自动创建一个子进程并返回。父进程和子进程共享相同的代码,子进程可以继续从父进程的当前执行点运行

② 屏蔽底层细节:

底层需要分配新的内存空间、复制父进程的状态、初始化子进程的资源,fork函数将这些细节全部封装起来。

③ 依赖操作系统:

fork的具体实现以来于操作系统内核,它负责管理进程表等关键数据结构,系统调用fork,当控制转移到内核中的fork代码后,内核做:

1. 分配新的内存块和内核数据结构给子进程

2. 将父进程部分数据结构内容拷贝给子进程

3. 添加子进程到系统进程列表当中

4. fork返回,开始调度器调dan'ddandan

当一个进程调用fork之后就会有两个二进制代码相同的进程,并且都能运行到相同的地方,各自开始往下走:

int main() {printf("Before: pid is %d, ppid is %d\n",getpid(),getppid());fork();printf("After: pid is %d, ppid is %d\n",getpid(),getppid());return 0;
}

这里为什么只有三行输出,子进程共享父进程的代码,并各自独立执行,应该是打印两次Before才对。分析打印结果,10226应该是父进程,确实打印了Before,而子进程10227没有打印Before,说明子进程并没有执行Before的代码,正如上面所说的,子进程继续从父进程的当前执行点运行,也就是从fork代码处往下执行,而fork之前的不会被执行:

所以,在一个进程调用fork之前,该进程单独执行,调用fork之后,父子两个进程执行流各自执行

📚fork返回值

在子进程中,fork返回0;

在父进程中,frok返回子进程pid。

❓这里提出一个问题,在父进程中fork返回值没有异议,因为fork函数是父进程调用的,自然会有返回值,但是在子进程中fork也有返回值,那么是不是子进程也调用了fork函数呢? 这不肯定,因为一个进程调用fork函数之后会创建出它的子进程,而子进程再调用fork函数再创建。。。这显然不对,所以子进程并没有调用fork函数,但是为什么会有fork函数的返回值呢

✅上面说到,子进程是从父进程的执行点开始往下执行的,所以对上述问题合理的解释是:父进程创建子进程时的执行点在fork函数调用之后,返回之前,所以子进程往下执行也会有返回值产生

2.写时拷贝

父子进程的代码是共享的,所以它们往后执行相同的操作,那么它们的数据也是共享的吗?的确,在没有进行写入时,父子进程的数据也是共享的,只有当一方尝试对共享数据进行写入时,系统才会拷贝一份数据用于写入,这样既确保了资源的高效利用,又保证了父子进程间的独立性 。

3.调用失败

fork()调用失败通常于系统资源、权限、或操作系统限制有关,下面是常见的原因:

📚资源耗尽

当系统的资源不足时(如内存或进程表项不足),fork()会失败:

内存不足:操作系统需要为每个新进程分配内存,如果系统内存耗尽,fork() 就会失败。

进程表已满:每个进程都有一个进程控制块(PCB),操作系统维护一个进程表。如果系统中运行的进程数量已经达到限制,无法再为新进程分配进程控制块时,fork() 会失败。

堆栈空间不足:如果子进程的堆栈空间无法分配(尤其在某些嵌入式或资源受限的环境中),fork() 也会失败。

📚进程数限制

大多数操作系统对一个用户或系统总共能创建的进程数有限制。若当前用户或系统已经达到了此限制,调用 fork() 时就会失败。

可以通过 ulimit -u 查看单个用户的最大进程数:

📚内核限制

内核的资源,如文件描述符和信号等,也可能导致 fork() 失败。例如,如果父进程持有太多打开的文件句柄,可能会达到系统文件描述符的限制。


📖二、进程终止

1.退出场景

进城退出场景无非下面三种:

①:代码运行完毕,结果正确

②:代码运行完毕,结果不正确

③:代码异常终止(没有运行完)

第一种情况自然是最好的,但是如果是另外两种情况,我们就需要进行额外处理,但是我们怎么才能知道进程退出是哪种情况呢(什么时候需要处理,什么时候不需要呢)?

这个时候就需要进程退出时,做一些标记(返回退出码),告知操作系统或程序员具体的退出情况

2.status退出码

status状态码用于表示进程的退出状态,提供了进程执行结果的信息,状态码遵循以下约定:

  • 0:表示命令成功执行,没有错误发生。
  • 非0:表示命令执行失败。具体的非0值表示不同类型的错误,具体含义通常与执行的程序或命令相关。例如:
    • 1:一般性错误。
    • 2:命令语法错误。
    • 126:命令不能执行(权限问题)。
    • 127:命令未找到。
    • 128:命令因信号导致终止(例如,程序被 kill 命令中断)。
    • 130:程序因接收到 Ctrl+C(SIGINT)信号而退出。

status通常被定义成整形,但是并不能当作一般的整形看待,而是要看作成位图:

我们有一个 32 位的 status,其中高8位用于表示退出状态,低8位用于表示因信号退出的原因。

  1. 高8位(退出状态)可以有 256 种可能的退出码:

    0:正常退出。

    1127:表示不同的错误。

    128255:表示因信号终止,计算方式为 128 + 信号编号

  2. 低8位(信号终止标志):

    如果进程是由于信号终止的,那么低8位会记录相应的信号编号(例如,SIGKILL 对应 9,SIGSEGV 对应 11)。

    如果进程不是由信号终止的,低8位通常为 0。

3.退出方法

进程退出的常见方法有:exit(),_exit()以及main()函数返回,下面依次进行介绍:

📚exit函数

exit(int status):这是进程正常终止的一种方式。调用exit()后,进程会清理其资源(文件描述符、内存等),并将状态码status返回给操作系统。当返回0时表示成功退出,返回非0表示出现错误。

在多线程程序中,exit() 会终止当前进程以及所有线程。

#include <stdio.h>
#include <stdlib.h>int main() {printf("This process will exit normally.\n");exit(0);  // 正常退出,状态码为0
}
📚_exit函数

_exit(int status):这个函数与 exit() 很相似,但它不会执行标准库的清理操作(如缓冲区刷新等),直接终止进程。来看下面这段代码:

int main()
{printf("this is a process, pid is %d, ppid is %d",getpid(),getppid());exit(0);
}

调用exit时,正常打印;

int main()
{printf("this is a process, pid is %d, ppid is %d",getpid(),getppid());_exit(0);
}

❓调用_exit时,没有正常打印,这是为什么呢?

✅printf输出时如果没有加上\n,此时输出的内容会存在标准输出缓冲区中,并不会立刻显示在终端,而调用_exit函数时,由于它不会执行标准库的清理操作,所以缓冲区的内容就不会显示在终端

exit函数最后其实会调用_exit函数,只不过在调用之前,多做了如清理缓冲区的操作:

📚main函数返回

return:在 main 函数中使用时,程序会结束并返回指定的退出状态码(通常为 0 表示成功,非 0 表示错误)。return 结束当前函数的执行,但如果在 main 函数中调用,它会导致程序退出。

return返回和exit调用的效果是一样的,其实他们本质上是等价的:return 0 等价于 exit(0)

只不过在main函数中用return返回作为程序终止的标志更符合函数的语义,可读性更强。


📖三、进程等待

之前的博客讲过,子进程退出,如果父进程不做任何处理,就会引发内存泄露(进程表等信息不会被清理),产生僵尸进程。 博客链接在此:详解僵尸进程于孤儿进程

那么避免僵尸进程的办法就是进程等待,父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

1.wait方法

wait()是一个比较简化的系统调用,用于让父进程等待任意一个子进程的终止。wait()函数会阻塞父进程,直到有子进程终止,并且返回一个子进程的PID。

📚语法
#include <sys/wait.h>
pid_t wait(int *status);

status:用于返回子进程的退出状态。

返回值:如果调用成功,返回子进程的PID;如果没有则返回-1。

📚总结

1. 父进程调用wait()时会阻塞,直到有子进程结束并回收它的状态;

2. 如果有多个子进程退出,wait()返回任意一个子进程的PID;

3. 如果没有子进程,wait()会返回-1。

2.waitpid方法

waitpid()wait() 的更为灵活和可控制的版本,允许父进程等待特定的子进程结束,或者通过指定参数进行更精细的控制。 

📚语法
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
  • pid:指定需要等待的子进程的 PID。可以取以下几种值:
    • pid > 0:等待指定 PID 的子进程。
    • pid == -1:等待同组进程中的任意子进程。
  • status:与 wait() 相同,保存子进程的退出状态。
  • options:控制行为的标志,常用的选项有:
    • WNOHANG:非阻塞模式,如果没有子进程退出,立即返回,而不是阻塞。
    • WUNTRACED:如果子进程已经停止(但没有退出),也返回。
  • 返回值:
    • 返回子进程的 PID,如果没有子进程或者发生错误,返回 -1
    • 如果 status 中的退出状态有特殊状态(如退出信号),需要使用宏来解析。
int main()
{pid_t pid = fork();if(pid == 0){// 子进程printf("this is child process,pid is %d,ppid is %d\n",getpid(),getppid());exit(20);}// 父进程printf("this is father process,pid is %d\n",getpid());int status;pid_t child_pid = waitpid(-1,&status,WNOHANG);printf("child process has exited,code is %d,pid is %d\n",WEXITSTATUS(status),child_pid);exit(0);
}

父进程调用 wait()waitpid() 时,它会传递一个指向 status 变量的指针,用于写入子进程的退出状态,所以我们需要在外部定义一个status变量,并通过取地址的方式传入函数内部。

❓定义成其他变量名可以吗:完全可以!

✅变量名只是内存的一个标识符,是用户自定义的,wait()waitpid() 只关心的是传递给它的地址,而不是变量的名字,只不过定义成status这样代码更加易读。

❓定义成其他类型可以吗:不可以!

status 参数必须是一个指向 int 类型的指针。如果传递其他类型(例如 float*char*),程序可能会产生编译错误,这是因为 wait()waitpid() 会在 status 指向的内存中写入整数值,用来存储子进程的退出状态。如果指针指向的类型不匹配,内存解释将出错。

上述代码中由于waitpid内部设置为WNOHANG模式,没有子进程返回时直接退出,不阻塞:

 需要sleep(1)等待子进程退出后,waitpid才能接收到退出信息:

  // 父进程printf("this is father process,pid is %d\n",getpid());int status;sleep(1);pid_t child_pid = waitpid(-1,&status,WNOHANG);printf("child process has exited,code is %d,pid is %d\n",WEXITSTATUS(status),child_pid);exit(0);

其中WEXITSTATUS是一个宏函数,用于解码退出状态,因为上面讲过,32位status的高8位存储退出状态,所以不能直接引用status查看,而要用一个宏函数进行解码。

📚总结
特性wait()waitpid()
等待目标等待任意子进程的结束可以指定特定的子进程(通过 pid 参数)
阻塞与非阻塞总是阻塞,直到至少有一个子进程结束可以通过 WNOHANG 使其非阻塞
灵活性较少灵活性,只能等待任何一个子进程更灵活,可以等待指定的子进程或进程组
选项没有额外选项支持更多控制选项,如 WNOHANG
返回值返回一个子进程的 PID返回指定子进程的 PID,或者 -1 错误
错误处理如果没有子进程,返回 -1如果没有子进程,返回 -1


以上就是【进程的生命之旅——诞生、消逝与守候】的全部内容,欢迎指正~ 

 码文不易,还请多多关注支持,这是我持续创作的最大动力!


http://www.hkcw.cn/article/fLiDNBoLxB.shtml

相关文章

《Linux权威指南:从小白到系统管理员(上册)》深度解析与实践指南

&#x1f482; 个人网站:【 摸鱼游戏】【神级代码资源网站】【星海网址导航】摸鱼、技术交流群&#x1f449; 点此查看详情 引言 Linux 作为现代计算的核心操作系统之一&#xff0c;广泛应用于服务器、云计算、嵌入式开发等领域。《Linux权威指南&#xff1a;从小白到系统管理…

【Linux】信号

目录 一、信号的概念二、信号的产生2.1 通过键盘进行信号的产生2.2 通过系统调用进行信号的产生2.2.1 kill函数2.2.2 raise函数2.2.3 abort函数 2.3 通过异常的方式进行信号的产生2.4 通过软件条件的方式进行信号的产生2.4.1 关闭管道读端2.4.2 alarm函数 2.5 Core Dump&#x…

「模型部署系列」ubuntu 使用vllm部署Qwen3-8B模型

1、下载vllm v0.8.5&#xff08;此处已经下好了&#xff0c;去仓库拉资源&#xff09; 2、 下载Qwen3-8B 方式1: 在下载前&#xff0c;请先通过如下命令安装ModelScope pip install modelscope 命令行下载 下载完整模型库 modelscope download --model Qwen/Qwen3-8B 下…

亮数据与 AI 深度集成:构建电商策略自动化系统新范式

目录 1.引言&#xff1a;电商增长遇瓶颈&#xff0c;AI 能否破局&#xff1f;2.挖掘痛点&#xff1a;精准营销为何难以落地&#xff1f;3.解决之道&#xff1a;为什么选择亮数据而不是传统爬虫&#xff1f;3.1轻松绕过反爬机制&#xff0c;保障数据采集稳定性3.2 零代码门槛&am…

YOLOv12环境配置,手把手教你使用YOLOv12训练自己的数据集和推理(附YOLOv12网络结构图),全文最详细教程

文章目录 前言一、YOLOv12代码下载地址1.YOLOv12模型结构图 二、YOLO环境配置教程1.创建虚拟环境2.激活虚拟环境3.查询自己电脑可支持最高cuda版本是多少&#xff08;无显卡的同学可以跳过这个步骤&#xff09;4.pytorch安装5.验证 PyTorch GPU 是否可用&#xff08;没有显卡的…

Nginx下载与安装(Liunx环境)

1、Nginx下载 官网地址&#xff1a;https://nginx.org/en/download.html 2、安装依赖包 //安装gcc yum install gcc-c //安装PCRE pcre-devel yum install -y pcre pcre-devel //安装zlib yum install -y zlib zlib-devel //安装Open SSL yum install -y openssl openssl-deve…

雷达中实信号与复信号

一、什么是实信号和复信号 实信号是指信号的时域取值在数学表示和物理实现中始终为实数的信号&#xff0c;其基本的表达式为&#xff1a;&#xff1b;复信号是指时域取值在数学表示中始终为复数的信号&#xff0c;其基本的表达式为&#xff1a;。从实信号与复信号的定义可知&am…

【存储基础】NUMA架构

文章目录 1. 前置知识:物理CPU和CPU核心物理CPUCPU核心关系 2. NUMA架构2.1 NUMA架构是什么&#xff1f;2.2 NUMA架构详解2.3 查看NUMA信息2.4 NUMA架构在分布式存储中的应用数据本地化 Data Locality计算与存储协同调度NUMA感知的网络通信内存池优化与跨节点均衡 3 补充&#…

HTTP协议解析

HTTP&#xff08;超文本传输协议&#xff09;是万维网的基础协议&#xff0c;自1991年诞生以来&#xff0c;已成为最广泛使用的应用层协议。本文将深入解析HTTP协议的核心概念、工作原理及实际应用。 HTTP协议基础 什么是HTTP&#xff1f; HTTP (全称为 "超文本传输协…

小麦“颗粒归仓”有了“最强大脑”

全国小麦主产区自南向北陆续进入紧张抢收阶段,夏种也全面展开。河南夏种已完成四成,以玉米、花生为主。安徽夏种已完成近三成,以水稻和玉米为主。各地如何针对天气情况抢抓收获“窗口期”,确保粮食“颗粒归仓”?目前,安徽4300多万亩的小麦收获已接近尾声。当记者来到安徽…

数据结构:递归(Recursion)

目录 示例1&#xff1a;先打印&#xff0c;再递归 示例2&#xff1a;先递归&#xff0c;再打印 递归的两个阶段 递归是如何使用栈内存 复杂度分析 递归中的静态变量 内存结构图解 递归&#xff1a;函数调用自己 必须有判断条件来使递归继续或停止 我们现在通过这两个示…

Python入门手册:类和对象

在Python中&#xff0c;面向对象编程&#xff08;OOP&#xff09;是一种核心的编程范式。通过类和对象&#xff0c;我们可以模拟现实世界中的事物和行为&#xff0c;使代码更加模块化、可复用和易于维护。今天&#xff0c;就让我们深入探讨Python中的类和对象&#xff0c;包括它…

从冷上电到main()函数,Bootloader都做了什么?

目录 1、硬件初始化 2、引导模式与应用模式的抉择 3、启动代码 在嵌入式系统中&#xff0c;从设备上电到执行应用程序的main()函数&#xff0c;Bootloader扮演着至关重要的角色。作为系统启动的首个程序&#xff0c;Bootloader负责初始化硬件、设置运行环境&#xff0c;并最…

电路图识图基础知识-保护环节、自锁环节及互锁环节(十)

1 电路中的自锁环节 自锁环节是指继电器得电后能通过自身的常开触点闭合&#xff0c;给其线圈供电的环节。如图所示的电路图中&#xff0c;辅助电路中并联于启动按钮开关SB2 旁边的KM 常开触点就是自锁环节(此触 电称为自锁触电)。 图中所示的自锁过程是&#xff1a;当QK 闭合后…

Linux Windows之wsl安装使用简介

参考资料 如何使用 WSL 在 Windows 上安装 Linuxwindows11 安装WSL2全流程旧版 WSL 的手动安装步骤 目录 一. 前期准备1.1 确认windows的版本1.2 开启Linux子系统的支持1.2.1 图形化方式1.2.2 命令行方式 1.3 安装wsl软件1.4 安装Linux分发版 二. 基本配置2.1 Windows Termina…

网红家装企业上海总部人去楼空 欠款风波引关注

端午节放假前,每天有上百人来找住范儿,因为公司欠了不少钱。6月1日下午,记者来到住范儿上海公司所在地,发现公司大门被木板封得严严实实。守在门口的保安指着木板上的通知对记者说:“也省得你报警了,直接打派出所电话吧。”据官网介绍,住范儿是家居建材新零售服务商,成…

正则表达式笔记

正则表达式笔记 前言一、基本字符匹配二、字符类三、量词四、定位符五、贪婪匹配和非贪婪匹配六、旗标七、分组和引用八、前瞻九、后顾 前言 参考GeekHour视频和资料&#xff0c;讲的挺好的&#xff0c;B站有[GeekHour正则表达式] 正则表达式在线工具网站&#xff1a;https://…

齐达内拒利雅得新月一亿欧年薪合同 静候法国国家队帅位

齐达内拒绝了利雅得新月开出的1亿欧元年薪合同。沙特球队利雅得新月正在寻找新主帅,并希望邀请赋闲在家的齐达内。利雅得新月愿意为齐达内支付一亿欧元年薪,签约一年,让他率队参加今夏世俱杯。然而,齐达内已经拒绝了这份高薪邀请。随后,利雅得新月开始联系国米主帅小因扎吉…

【论文解读】DETR | End-to-End Object Detection with Transformers

论文地址&#xff1a;https://arxiv.org/pdf/2005.12872 代码地址&#xff1a;https://github.com/facebookresearch/detr 摘要 本研究提出了一种新的方法&#xff0c;该方法将目标检测视为一个直接的集合预测问题。本研究的方法简化了检测流程&#xff0c;有效地消除了对许多…

(C++)STL:string类(三)非成员重载函数和类型转化函数解析使用

string类&#xff08;三&#xff09; 非成员重载函数relational operaters 关系运算符operatoroperator<< operator>>getline <string>头文件内的函数string转化为数字类型其他数值类型转化为string练习&#xff1a;字符串最后一个单词的长度 非成员重载函数…