请添加图片描述
🔥铅笔小新z:个人主页
🎬博客专栏:Linux学习
💫滴水不绝,可穿石;步履不休,能至渊。
请添加图片描述


引言

想象一下你平时用电脑的最熟悉的场景:你正在一边带耳机听着歌,一边在微信上和朋友疯狂敲字聊天,同时后台还在默默下载着一个几个G的大型游戏。你的电脑仿佛长了三头六臂,把这一切安排得井井有条、互不干扰。

但你有没有停下来想过,在那块冰冷的硅晶板和复杂的电路之下,操作系统究竟是如何做到这种“多线操作”和“无缝切换”的?它到底是怎么管理这些成百上千的任务的?

答案就藏在今天我们要聊的这位主角身上——进程(Process)

对于很多初学计算机、或者刚接触后端开发的朋友来说,“进程”听起来像是一个枯燥、高深又及其抽象的底层概念。但别慌,咱们今天不背教科书上的定义,用大白话来拆解它:

如果说“程序”(Program)是静静躺在书架上的一本菜谱,那么“进程”(Process)就是你系上围裙、拿起锅铲,按照菜谱真刀真枪的在厨房里炒菜的全过程。

菜谱是死的,是一堆写在纸上的指令;但炒菜这个动作是活的,它需要占用你的厨房空间(内存),消耗你的食材(数据),并且需要你亲自上阵翻炒(CPU执行)。

在今天的这篇博客里,我将带大家撕开操作系统的神秘面纱,彻底搞懂这个核心概念。

这篇博客的目标就是:

  1. 认识冯·诺依曼体系
  2. 操作系统概念与定位,理解“管理”
  3. 深入理解进程概念,了解PCB

一、冯·诺依曼体系

1.1 什么是冯·诺依曼体系

在冯·诺依曼体系诞生之前,早期的计算机(比如著名的ENIAC)是个彻头彻尾的“重体力活”。那时候你想让计算机算个新数学题,几十个工程师得拿着线缆拔来插去,物理层面重新接线(也就是所谓的硬连线)。

直到1945年,冯大神在一篇报告中提出了一个极具颠覆性的核心思想:
“存储程序,顺序执行” (Stored-program concept)
他认为:我们不应该把程序用硬件的电线焊死,而是应该把“程序(指令)”和“数据”一样,都转换成二进制(0和1),统统塞进一个叫“存储器”的地方。计算机工作时,只要一步步从里面把指令拿出来执行就可以了。

这就好比从“每次炒不同的菜都要重新装修一次厨房”,变成了“厨房格局固定,厨师只需要照着不同的菜谱做菜”。

为了实现这个伟大的构想,他为计算机设计了五大核心组件:
在这里插入图片描述

  1. 运算器 (Arithmetic Logic Unit, ALU): 这是厨房里真正拿刀切菜、拿锅铲翻炒的厨师的手。它负责执行所有的算术运算(加减乘除)和逻辑运算(大于、小于、等于)。
  2. 控制器 (Control Unit, CU): 这是厨师的大脑。它负责从菜谱(内存)里读出下一步该干什么,然后指挥运算器、输入输出设备去干活。(在现代计算机中,运算器和控制器被合并封装在了一起,也就是我们天天挂在嘴边的 CPU)。
  3. 存储器 (Memory): 这是厨房里的备菜台和冰箱。它同时存放着你要炒的菜(数据)和你的那本菜谱(程序/指令)。
  4. 输入设备 (Input): 这是送菜员/点单员。把外部的信息(键盘敲击、鼠标点击、麦克风声音)转换成计算机能懂的二进制送进系统。
  5. 输出设备 (Output): 这是传菜窗口。把计算机算好的结果(显示器画面、音响声音)呈现给我们。

1.2 冯·诺依曼的缺陷

刚才我们提到,“指令”和“数据”是放在同一个存储器里的,而且它们共用同一条系统总线(Bus)来传输。 这意味着什么?意味着 CPU 每次要么只能去拿指令,要么只能去拿数据,不能同时进行。

随着摩尔定律的发展,CPU 的运算速度像坐了火箭一样飙升,但是内存的读写速度却像是在骑自行车。 CPU 这个顶级大厨切菜炒菜只需 1 秒钟,但他每次去冰箱拿食材却要在路上花 100 秒。结果就是:CPU 大部分时间都在闲着发呆,等内存把数据送过来。

现代工程师是怎么“缝补”这个瓶颈的呢?
这就不得不提大家买 CPU 时常看到的参数——缓存(Cache,即 L1/L2/L3 缓存)。既然大冰箱太远,那我就在厨师手边放几个小盒子(缓存),把最常用、最快要用的食材提前放进去。虽然我们现在依然没有跳出冯·诺依曼体系的五指山,但通过多级缓存架构,我们极大地缓解了这个问题。

1.3 进一步认识体系的流程

1.3.1 场景一:在QQ上给朋友发一句“吃了吗?”(文本信息流转)

1.3.1.1 第一阶段:你的电脑(信息的诞生与发出)
  1. 敲击键盘(输入设备 -> 内存):
    当你用手指敲下“吃了吗?”时,键盘输入设备)会产生电信号。这个信号会触发一个“中断”,强行打断CPU控制器)正在做的事情。CPU跑过来一看:“哦,键盘有输入了!” 于是,它把这些拼音字母转化为二进制数据,悄悄放进了QQ这个进程所在的内存存储器)中。
  2. 屏幕回显(内存 -> CPU -> 输出设备)
    为了让你能看到自己打了什么字,CPU(运算器) 会从内存里读取这些字符,进行渲染计算,然后把图像数据发给显卡和显示器(输出设备)。这时候,你就在聊天框里看到了这几个字。
  3. 点击发送(内存 -> CPU -> 内存 -> 输出设备)
    当你按下回车键(发送)时,重头戏来了!
    • CPU 会把内存里的那句“吃了吗?”拿出来。
    • CPU 会用复杂的算法对这句话进行加密,并给它套上一层层“快递包装”(TCP/IP 网络协议头),然后再放回内存中。
    • 最后,CPU 命令网卡(在这个场景下充当输出设备),把内存里打包好的数据变成电信号或光信号,顺着网线“biu”地一下发射出去。

核心逻辑: 所有的处理(加密、封包)必须由 CPU 完成,而 CPU 只能从内存拿数据。网卡不能直接读取你的键盘,必须经过 “输入 -> 内存 -> CPU -> 内存 -> 输出” 这个经典的冯·诺依曼数据流。

1.3.1.2 第二阶段:网络世界(腾讯服务器的接力)

你的消息在物理网线中穿梭,来到了腾讯的服务器。

请注意,服务器也是一台标准的冯·诺依曼机器!

服务器的网卡(输入)接收到信号放入内存 -> CPU 拆开看了一眼发现是发给你朋友的 -> CPU 重新查路由打包放入内存 -> 服务器网卡(输出)再发向你朋友所在的网络节点。

1.3.1.3 朋友的电脑(信息的接收与呈现)
  1. 接收数据(输入设备 -> 内存)
    朋友电脑的**网卡(输入设备)**接收到了网线传来的光电信号,将其还原成二进制,并存放到朋友电脑的内存中。
  2. 处理数据(内存 -> CPU -> 内存)
    网卡给朋友的 CPU 发个通知:“来活了!”朋友的 CPU 赶紧从内存里读取这些数据包,一层层剥开网络协议的“快递包装”,解密后,把真正的消息内容(吃了吗?)更新到 QQ 进程的内存空间里。
  3. 呈现消息(内存 -> CPU -> 输出设备)
    最后,朋友的 CPU 从内存里读取这句解密后的明文,画好界面,把指令发给显示器(输出设备)音响(输出设备)
    于是,朋友的屏幕上弹出了你的消息,同时伴随着熟悉的“滴滴滴”提示音。

1.3.2 场景二:在 QQ 上发送一个 1GB 的视频文件

发文件和发消息,在冯·诺依曼的宏观框架下是一样的,但数据源头和底层细节发生了巨大的变化。

发消息时,那几个字节的数据就在内存里。但 1GB 的视频文件是静静躺在你的 硬盘(外部存储器,通常也被视为 I/O 设备) 里的。

如果完全按照传统的冯·诺依曼流程,发文件会怎样?

  1. CPU 告诉硬盘(输入):把视频读出来放到内存。
  2. CPU 从内存把视频读进自己的寄存器(运算器)。
  3. CPU 把视频加工打包,放回内存。
  4. CPU 告诉网卡(输出):把内存里的视频发出去。

问题来了: 我们上一篇文章讲过“冯·诺依曼瓶颈”!CPU 速度极快,硬盘和网卡速度很慢。如果传 1GB 的文件都要 CPU 亲自去硬盘里一点点搬运数据到网卡,CPU 就什么都不用干了,你的电脑会卡死,歌也听不了,鼠标也动不了。

现代计算机的“作弊技巧”:DMA(直接内存访问)

为了解决大文件传输对 CPU 的压榨,现代硬件引入了 **DMA(Direct Memory Access)**技术。有了它,发文件的数据流变成了这样:

  • 数据搬运不再烦劳 CPU: 你点击发送文件,CPU 只需对 DMA 控制器(一个小小的协处理器)下达一句命令:“你负责把硬盘 C 盘里的那个视频,搬到内存里,然后再从内存交给网卡发出去。干完了叫我,我先去处理听歌和打游戏的进程了。”
  • 数据流向: 硬盘(外存/输入) -> 内存 -> 网卡(输出)。
    在这个过程中,庞大的数据流绕过了 CPU,直接在各个设备的内存之间流转。

博主敲黑板: 发文件和发消息最大的区别在于数据量级和存储位置。发消息是 CPU 深度参与的“内存-CPU-网卡”密集计算流;而发大文件则是通过 DMA 技术优化的“硬盘-内存-网卡”大吞吐量 I/O 流。


二、操作系统(Operator System)

2.1 操作系统的概念

我们从三个不同的维度把它拆解开来:

在这里插入图片描述

2.1.1 从系统所处的位置看:它是“承上启下”的中间层

在这里插入图片描述
在计算机的层次结构中,操作系统处于一个及其关键的卡位:

  • 它的下面是“裸机”(硬件): 包含 CPU、内存、硬盘、网卡等物理设备。
  • 它的上面是“应用软件”和“用户”: 比如你用的浏览器、微信、游戏,以及正在敲键盘的你。

概念解析: 操作系统就是夹在硬件和应用程序之间的一层软件。它“承下”屏蔽了硬件的复杂性,“启上”为应用程序提供了统一的运行环境和接口。没有这层缓冲,软件工程师将不得不对着电路板写代码。

2.1.2 从软件的属性看:它是“最底层、最核心”的系统软件

软件通常分为“应用软件”(帮你完成特定任务,如听歌、打字)和“系统软件”(维持计算机自身运行)。

概念解析: 操作系统是系统软件中的基石。当你按下电脑开机键,经过 BIOS 初始化后,第一个被加载到内存中运行的庞大软件就是操作系统。它是直接运行在“裸机”之上的第一层软件。任何其他的应用程序,都必须在操作系统的支持和允许下才能运行(就像是在操作系统的地盘上租房子的房客)。

2.1.3 从表现形式看:它是一个庞大的“程序集合”

虽然我们总说“一个”操作系统,但它并不是一个单一的可执行文件。

概念解析: 操作系统是由成千上万个程序组成的庞大集合体。这个集合体中包含了:

  • 内核(Kernel): 这是操作系统最核心的部分,常驻内存,拥有最高权限,专门负责上面提到的 CPU 调度、内存管理等最底层的脏活累活。
  • 驱动程序(Drivers): 专门用来教操作系统如何与特定硬件(比如某款特定型号的显卡)沟通的代码。
  • 系统工具和接口(Shell/GUI 等): 比如 Windows 的桌面系统、命令提示符,Linux 的 Bash 终端等,它们为用户提供操作电脑的入口。

终极概念比喻:政府与国家:
如果要用一个通俗的模型来概括操作系统的概念,最恰当的比喻就是政府

  • 硬件是这个国家的国土、矿产、工厂(物理资源)。
  • 应用软件是这个国家里的企业和公民(需要消耗资源来创造价值)。
  • 操作系统(OS) 就是这个国家的政府。

政府本身并不直接创造具体的财富(操作系统自己不负责编辑文档、不负责放音乐),但政府存在的核心概念和意义在于:它通过制定法律(权限控制)、分配土地和电力(内存与 CPU 调度)、修筑高速公路(文件系统与 I/O),来建立一个安全、稳定、高效的环境,让所有的企业和公民(应用程序)能够在这个环境中顺利地繁荣运转。

2.2 设计操作系统的目的

在没有操作系统的远古时代,程序员需要直接用机器语言操作硬件(比如指定哪个磁道、哪个扇区去读数据),这不仅反人类,而且极其容易出错。因此,设计OS的目的主要有三个:

  1. 方便性(为用户提供美好的幻觉): 硬件是丑陋且复杂的(各种寄存器、物理地址)。OS通过一层层封装,把丑陋的硬件变成了简单易用的接口。你不需要知道硬盘是怎么转的,你只需要知道“双击打开文件”。
  2. 有效性(压榨硬件的每一滴性能): 计算机里有CPU、内存、硬盘等昂贵的资源。如果一个程序在等键盘输入,CPU就闲着,这太浪费了。OS的目的就是合理调度,让所有硬件资源都能高效运转,不留空闲。
  3. 可扩充性(拥抱未来的变化): 硬件技术日新月异,OS必须设计成模块化的结构,以便随时支持新的硬件设备(比如插上一个新的USB鼠标就能直接用)。

2.3 核心功能

操作系统的核心功能可以概括为四大管理。你可以把它想象成一个大型企业的四个核心部门:

  1. 进程与线程管理(HR部门 - 掌管计算资源): CPU是企业里最厉害的打工人,但只有一个(或几个)。OS负责决定现在该让CPU去执行哪个程序(进程),执行多久后切换给下一个。这叫做CPU调度。
  2. 内存管理(后勤部门 - 掌管空间资源): 内存是程序运行时的舞台。OS负责给每个程序分配舞台空间,并且保证它们互相之间不打架(比如微信不能去偷看支付宝的内存空间)。如果物理内存不够了,OS还会聪明地用硬盘虚拟出一部分内存(虚拟内存)。
  3. 文件系统管理(档案部门 - 掌管数据存储): 硬盘里只是一堆0和1。OS将其组织成“目录”和“文件”的树状结构,负责文件的读写、权限控制和存储空间分配。
  4. 设备管理(公关部门 - 掌管输入输出 I/O): 负责管理键盘、鼠标、显示器、网卡等所有外设。OS屏蔽了不同设备的物理差异,提供统一的读写接口。

2.4 如何理解“管理”

要通透理解OS的“管理”,你需要从两个视角来看待它。操作系统其实是一个“双面人”:

  1. 视角一:自上而下的“魔术师”—— 制造抽象
    对于运行在上面的软件来说,OS是一个魔术师。它给每个程序制造了一个美丽的幻觉:让每个程序都以为自己独占了整个CPU(通过快速切换),独占了整个超大内存(通过虚拟内存)。这种管理的本质是 “抽象”,隐藏复杂,呈现简单。
  2. 视角二:自下而上的“裁判员”—— 资源分配与复用
    对于底层的物理硬件来说,OS是一个铁面无私的裁判员。当多个程序同时抢占CPU、抢占内存、抢占网卡时,OS负责制定规则、分配资源、解决冲突。这种管理的本质是 “复用”
    • 时间复用: CPU按时间片分给不同程序用。
    • 空间复用: 内存按区域划分给不同的程序用。

2.5 系统调用 vs 库函数(用户与内核的沟通桥梁)

这是很多初学者容易混淆的地方。理解这两个概念,必须先引入一个极其重要的前置知识:用户态(User Mode)与内核态(Kernel Mode)

为了保护系统不被恶意程序搞崩溃,CPU在硬件级别划分了权限:

  • 用户态: 权限极低,只能执行普通的计算指令。你写的普通程序都在这里运行。
  • 内核态: 权限极高,可以控制所有硬件(读写磁盘、分配内存、控制网卡)。操作系统内核在这里运行。

这就带来一个问题:处于用户态的程序,如果想把数据写到硬盘里(这需要极高权限),怎么办?

  1. 系统调用:
    • 概念: 它是操作系统内核主动暴露给用户程序的“唯一合法接口”。就像政府的办事窗口。
    • 过程: 用户程序调用它时,会触发一个特殊的硬件指令(Trap/陷入),让CPU从用户态切换到内核态,交由OS去执行危险操作,执行完再切回用户态。
    • 特点: 极其安全、执行成本较高(因为涉及状态切换)。例如:read()write()fork()
  2. 库函数:
    • 概念: 它是编程语言(如C、Java)或者第三方提供的一套代码库。它的目的是为了让程序员开发更方便。它就像是你花钱请的 “代办中介”
    • 与系统调用的关系:
      • 不涉及底层硬件的库函数: 完全在用户态执行,不需要系统调用。比如:strlen()abs()
      • 涉及底层硬件的库函数: 它们本质上是对系统调用的封装。比如著名的打印函数 printf()。它不仅帮你格式化了字符串(处理 %d,%s等),最后它在内部还是得乖乖调用系统调用 write(),又OS帮你把字符显示到屏幕上。

总结比喻: 你想去买一块地盖厂房(写磁盘数据)。
库函数是你公司的法务团队,他们帮你整理好厚厚的申请材料(缓冲、格式化)。
系统调用是国土局的盖章窗口,法务最终必须把材料递交到窗口,由国土局(操作系统)审查通过并盖章(执行内核态指令),这事儿才算办成。


三、进程

3.1 进程的概念

很多初学者分不清“程序”和“进程”。记住一句极其经典的话:程序是死的,进程是活的

  • 程序(Program): 它是存放在硬盘上的一堆可执行代码和数据的集合(比如一个 .exe 文件)。它就像是一本菜谱,安安静静地躺在书架上,没有任何动作。
  • 进程(Process): 当操作系统把这个程序加载到内存中,并分配了 CPU 去执行它,它就变成了一个进程。它就像是厨师(CPU)正在根据菜谱(程序)炒菜的过程。在这个过程中,需要用到锅碗瓢盆(内存)、调料肉菜(数据),并且是随着时间动态推进的。

总的来说:进程 = 内核数据结构(task_struct) + 自己的程序代码和数据

专业定义: 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

3.2 PCB(进程控制块):进程的“电子档案”

在现代操作系统中,同时可能有几百个进程在跑。操作系统怎么记住它们谁是谁?谁占了多少内存?谁正在等键盘输入?

这就必须引入 PCB(Process Control Block,进程控制块)

通俗理解: PCB 就是操作系统给每个进程建立的 “HR 员工档案”或“病历本”。只要进程一出生,OS 就会为它创建一个 PCB;进程死亡,PCB 就被销毁。操作系统管理进程,本质上就是在管理这些 PCB。

一个标准的 PCB 里通常包含这几大块硬核信息:

  1. 进程标识符(PID): 就像你的身份证号,是全系统唯一的数字。
  2. 处理机状态(CPU 上下文): 极其重要!当 CPU 决定去执行别的进程时,必须把当前进程算到一半的数据(各种寄存器的值、程序计数器 PC)拍照保存在 PCB 里。等下次轮到它执行时,再从 PCB 里恢复这些数据,继续往下算。(这就是上下文切换的本质)。
  3. 进程调度信息: 进程的状态(是正在跑,还是在排队,还是在等 I/O?)、优先级(谁比较 VIP?)。
  4. 资源分配清单: 这个进程被分配了哪些内存空间?打开了哪些文件?使用了哪些网络端口?

3.3 task_struct

PCB 只是所有操作系统的通用理论概念,而在真正的 Linux 操作系统源码中,PCB 的具体实现名字就叫做 task_struct

在 Linux 内核(C 语言编写)中,task_struct 是一个极其庞大、极其复杂的结构体(在早期的内核版本中就有上百个字段,现在更庞大)。所有的 task_struct 会被连接成一个双向循环链表(Task List),Linux 内核就是通过遍历这个链表来管理所有进程的。

为了让你有直观的“硬核感”,这里列举 Linux task_struct 中的部分源码:

/* * 摘自 Linux 内核源码:include/linux/sched.h
 * 这是操作系统中最重要的数据结构:进程控制块 (PCB)
 */
struct task_struct {

    /* ========================================================
     * 1. 进程状态与身份标识 (我是谁?我现在在干嘛?)
     * ======================================================== */
     
    // 进程的状态。
    // 比如:TASK_RUNNING (运行/就绪), TASK_INTERRUPTIBLE (可中断睡眠)
    // 注意:在较新的内核(5.14+)中,这个字段被改名并保护为 unsigned int __state;
    volatile long state;    

    // 进程的唯一标识符 (Process ID)
    pid_t pid;

    // 线程组 ID。如果是主线程,tgid 等于 pid。
    // 这就是为什么在 Linux 里,线程在底层也被当成轻量级进程(LWP)来管理。
    pid_t tgid;


    /* ========================================================
     * 2. 调度信息 (CPU HR 部门最关心的数据:什么时候安排你干活?)
     * ======================================================== */
     
    // 进程的优先级。数字越小,优先级越高。
    int prio;
    int static_prio;
    int normal_prio;
    unsigned int rt_priority; // 实时进程的优先级

    // 调度类:决定了这个进程归哪个具体的调度算法管(比如 CFS 完全公平调度器)
    const struct sched_class *sched_class;

    // 进程到底能在哪个 CPU 核心上运行(CPU 亲和性)
    int nr_cpus_allowed;
    cpumask_t cpus_mask;


    /* ========================================================
     * 3. 内存管理 (后勤部门:你的地盘在哪里?)
     * ======================================================== */
     
    // 极其重要!指向进程的虚拟地址空间。
    // 如果是普通进程,这个指针指向它专属的内存页表。
    // 如果是内核线程,它没有自己的用户空间,这个字段就是 NULL。
    struct mm_struct *mm;
    
    // 当前正在使用的内存空间(处理内核线程借用用户空间的情况)
    struct mm_struct *active_mm;


    /* ========================================================
     * 4. 家族族谱 (进程的社会关系:你的爸爸是谁?兄弟是谁?)
     * ======================================================== */
     
    // 真实的父进程(生你的那个)
    struct task_struct __rcu *real_parent;

    // 当前的父进程(通常和 real_parent 一样。
    // 但如果你被别的进程使用 ptrace 跟踪调试了,parent 会暂时变成那个调试器进程)
    struct task_struct __rcu *parent;

    // 你的孩子们(这是一个双向链表的头,链着你 fork 出来的所有子进程)
    struct list_head children;

    // 你的兄弟姐妹们(跟你同一个爸爸的进程,大家手拉手连成链表)
    struct list_head sibling;
    
    // 把所有进程串起来的全局双向链表(贯穿整个系统的 Task List)
    struct list_head tasks;


    /* ========================================================
     * 5. 文件系统与打开的文件 (档案部门:你拿了哪些资料?)
     * ======================================================== */
     
    // 进程所在的文件系统信息(比如当前的工作目录是哪个)
    struct fs_struct *fs;

    // 极其重要!进程打开的所有文件的清单(文件描述符表就在这里面)
    // 当你调用 read(fd) 时,内核就是顺着这个指针去查到底对应哪个物理文件的。
    struct files_struct *files;


    /* ========================================================
     * 6. 权限与安全 (保卫处:你有什么级别的通行证?)
     * ======================================================== */
     
    // 进程的身份凭证(你是 root 跑的,还是普通用户跑的?决定了你能不能删系统文件)
    const struct cred __rcu *real_cred;
    const struct cred __rcu *cred;

    // ... 省略数百行代码 (包含信号处理、命名空间 namespaces、cgroups 控制组等) ...
};

3.4 查看进程

3.4.1 用 /proc查看

进程的信息可以通过 /proc系统文件夹查看。

如果要获取 PID 为1的进程信息,你需要查看 /proc/1这个文件夹。

在这里插入图片描述

3.4.2 用 topps查看

大多数进程信息同样可以用 topps这些用户级工具来获取。

比如下面的代码就是 code 文件中的。

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

int main()
{
	while(1)
	{
		sleep(1);
	}
	return 0;
}

进行下面指令就可以获取进程信息:

ps aux | grep code | grep -v grep

3.4.3 通过系统调用获取进程标识符

  • 进程idPID
  • 父进程idPPID
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>

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

3.4.4 通过系统调用创建进程 - fork()

  • 运行 man fork 认识 fork()
  • fork() 有两个返回值
  • 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
	int ret = fork();
	if (ret < 0)
	{
		perror("fork");
		return 1;
	}
	else if (ret == 0) // child
	{
		printf("I am child : %d!,ret : %d\n",getpid(),ret);
	}
	else // father
	{
		printf("I am father : %d!,ret : %d\n",getpid(),ret);
	}
	sleep(1);
	return 0;
}

在 Linux 的世界里,除了系统刚启动时由内核创造的第一个“总管家”进程(PID=1 的 initsystemd),其他所有的进程,都是通过“克隆”生出来的。

这个执行克隆魔法的系统调用函数,就叫做 fork()

3.4.4.1 “分身”:fork()

假设有一个父进程正在运行,这时候系统调用了fork(),就相当于按下了克隆机的按钮。
“唰”的一下,系统里多出了一个一模一样的子进程。

  • 长得一样: 子进程拥有和和父进程一模一样的 task_struct档案(除了PID等少数几个身份标识不同)。
  • 记忆一样: 子进程的程序计数器(PC)值和父进程相同。

fork()函数调用一次会返回两次!

  • 在父进程那里,fork()会返回子进程的PID,意思是:“你的分身做好了,这是他的编号。”
  • 在子进程那里,fork()会返回0,意思是:“你是一个新诞生的分身。”
    程序员就是通过判断这个返回值,来让父子进程接下来去干不同的活儿。
3.4.4.2 写时拷贝

你想想,如果父进程占用了一个超级大的内存,按下fork()的瞬间,如果操作系统老老实实地去复制这个空间给子进程,不仅及其消耗物理内存,而且非常慢!

更要命的是,很多时候子进程被创建出来后,马上就回去执行一个新程序(调用exec()函数运行新程序),它根本不需要父进程那个空间内存。如果刚才真复制了那么大的内存,就白干了!

解决绝招:Copy-On-Write(写时拷贝)

Linux 操作系统极其聪明,它采用了“渣男”策略——只给承诺,不给实物

  1. 共享读取(假装有自己的内存空间):fork()刚发生时,操作系统并不会真的去复制物理内存。它只是给子进程发了一副“VR眼镜”(复制了虚拟内存的页表),让子进程以为自己拥有了一个一模一样的内存空间。实际上,父进程和子进程此时用的是共享物理内存。
  2. 写时触发: 只要两个进程都只是在进行只读操作,大家都相安无事,系统什么也不i用做。
  3. 真实拷贝: 但是!一旦子进程发生写操作,想要修改内存数据时,操作系统会瞬间察觉。此时,操作系统会立刻出手,把子进程要修改的那一小块区域(通常是 4KB 的内存页)真正地复制一份出来给它用。

总结

fork()创建子进程时,内核并不会立刻复制父进程的物理内存空间,而是让父子进程共享物理内存。只有当父进程或子进程试图修改某块内存数据时,内核才会引发缺页中断,将被修改的内存页复制一份出来。这种按需分配的延迟操作机制叫做写时拷贝,它极大地提高了进程创建的效率,节省了内存资源。

缺页中断:
当 CPU 发现进程要访问的虚拟内存地址,并没有真正映射到物理内存上,或者访问权限不匹配时,CPU 就会触发一个硬件中断,暂停当前进程,把控制权交给操作系统内核来处理。

3.4.4.3 为什么返回值有两个

fork() 之所以看起来返回了两次,是因为在 fork() 这个系统调用执行的过程中,操作系统成功地把当前的进程(父进程)克隆出了一份(子进程)。
也就是说,当 fork() 函数快要执行结束、准备把结果返回给用户代码时,系统里已经存在两个一模一样的进程了。是这两个进程各自从 fork() 函数中返回了一次,所以你在代码里才看到了“两个返回值”。

总结

fork() 并不是返回了两次,而是它创建了一个新的子进程,父子两个进程各自从系统调用中返回了一次。

好了,到这里我们的Linux中进程的一部分讲完了,感谢大家的观看!!!


请添加图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐