操作系统与进程核心全解:从冯诺依曼到fork系统调用

操作系统与进程核心全解:从冯诺依曼到fork系统调用


🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《系统深入Linux操作系统》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


📝 前言

在学习Linux系统编程的过程中,你是否有过这样的困惑:程序到底是如何在计算机上运行的?操作系统是如何管理成百上千个进程的?为什么一个**fork()**调用能"神奇地"返回两个不同的值?

提到进程,很多人的第一反应是"程序的执行实例",但这个定义背后隐藏着极其精妙的设计。从冯诺依曼体系结构进程控制块PCB,从系统调用父子进程关系,每一个环节都体现着操作系统设计的智慧。

通过本文,你将掌握:

技能 应用场景
冯诺依曼体系结构 理解计算机硬件与软件的数据流转
操作系统的管理本质 掌握"先描述,再组织"的核心思想
进程与PCB 深入理解进程的本质是内核数据结构
task_struct内容 了解进程标识符、状态、优先级等核心属性
进程查看与获取 使用ps、/proc、getpid/getppid等工具
fork系统调用 理解父子进程创建机制与写时拷贝
fork返回值原理 解释为什么fork能返回两个不同的值

📌 前置知识: 具备基本的C语言编程基础,了解Linux基本命令操作

文章目录

  • 操作系统与进程核心全解:从冯诺依曼到fork系统调用
    • [📝 前言](#📝 前言)
    • [一、🖥️ 冯诺依曼体系结构:计算机的基石](#一、🖥️ 冯诺依曼体系结构:计算机的基石)
      • [1.1 什么是冯诺依曼体系](#1.1 什么是冯诺依曼体系)
      • [1.2 冯诺依曼的核心规则](#1.2 冯诺依曼的核心规则)
      • [1.3 数据流动示例:QQ聊天过程](#1.3 数据流动示例:QQ聊天过程)
    • [二、⚙️ 操作系统:管理一切软硬件的"大管家"](#二、⚙️ 操作系统:管理一切软硬件的"大管家")
      • [2.1 操作系统的概念](#2.1 操作系统的概念)
      • [2.2 设计操作系统的目的](#2.2 设计操作系统的目的)
      • [2.3 核心功能:管理](#2.3 核心功能:管理)
      • [2.4 操作系统管理硬件的方法](#2.4 操作系统管理硬件的方法)
    • [三、🔄 进程:程序的执行实例](#三、🔄 进程:程序的执行实例)
      • [3.1 进程的基本概念](#3.1 进程的基本概念)
      • [3.2 描述进程:PCB(进程控制块)](#3.2 描述进程:PCB(进程控制块))
      • [3.3 task_struct的内容分类](#3.3 task_struct的内容分类)
      • [3.4 进程的组织方式](#3.4 进程的组织方式)
    • [四、🔍 查看与获取进程信息](#四、🔍 查看与获取进程信息)
      • [4.1 通过/proc文件系统查看](#4.1 通过/proc文件系统查看)
      • [4.2 使用ps命令查看进程](#4.2 使用ps命令查看进程)
      • [4.3 通过系统调用获取进程标识符](#4.3 通过系统调用获取进程标识符)
    • [五、🌲 通过fork创建进程](#五、🌲 通过fork创建进程)
      • [5.1 fork初识](#5.1 fork初识)
      • [5.2 fork的基本用法](#5.2 fork的基本用法)
      • [5.3 fork后的分流处理](#5.3 fork后的分流处理)
        • [5.3.1 代码共享的底层实现:物理内存完全共享](#5.3.1 代码共享的底层实现:物理内存完全共享)
        • [5.3.2 代码相同的具体表现](#5.3.2 代码相同的具体表现)
          • [5.3.2.1 执行完全相同的指令流](#5.3.2.1 执行完全相同的指令流)
          • [5.3.2.2 函数地址、全局函数指针完全相同](#5.3.2.2 函数地址、全局函数指针完全相同)
          • [5.3.2.3 动态链接库代码也共享](#5.3.2.3 动态链接库代码也共享)
        • [5.3.3 与数据段"相同"的本质区别](#5.3.3 与数据段"相同"的本质区别)
        • [5.3.4 特殊情况:代码段的写保护](#5.3.4 特殊情况:代码段的写保护)
    • 这一保护机制确保了代码段的完整性,也保证了fork后代码共享的安全性。
    • [六、🤔 fork核心问题深度解析](#六、🤔 fork核心问题深度解析)
      • [6.1 问题一:为什么fork给父子返回各自的不同的返回值?](#6.1 问题一:为什么fork给父子返回各自的不同的返回值?)
      • [6.2 问题二:为什么一个函数会返回两次?](#6.2 问题二:为什么一个函数会返回两次?)
      • [6.3 问题三:为什么一个变量,既==0,又>0?](#6.3 问题三:为什么一个变量,既==0,又>0?)
    • [七、📊 进程状态概览](#七、📊 进程状态概览)
    • 本节完

一、🖥️ 冯诺依曼体系结构:计算机的基石

1.1 什么是冯诺依曼体系

冯诺依曼体系结构是现代计算机的基础架构,无论是我们日常使用的笔记本电脑,还是数据中心的服务器,大多都遵循这一体系。

它由以下几个核心硬件组件构成:

组件 功能 典型设备
输入单元 将外部数据送入计算机 键盘、鼠标、扫描仪、麦克风
存储器 存储程序和数据 内存(RAM)
中央处理器(CPU) 执行运算和控制 包含运算器和控制器
输出单元 将结果呈现给用户 显示器、打印机、音箱

1.2 冯诺依曼的核心规则

关于冯诺依曼体系,有几个必须强调的关键点:

💡 核心规则一: 这里的存储器指的是内存,而不是磁盘。
💡 核心规则二: 不考虑缓存情况,CPU能且只能对内存进行读写,不能直接访问外设(输入或输出设备)。
💡 核心规则三: 外设(输入或输出设备)要输入或输出数据,也只能写入内存或者从内存中读取。
⚠️ 一句话总结:所有设备都只能直接和内存打交道!

1.3 数据流动示例:QQ聊天过程

为了加深理解,让我们分析一个实际场景------当你登录QQ并与朋友聊天时,数据是如何流动的?

发送消息的过程:

复制代码
键盘输入 → 内存(QQ程序缓冲区)→ CPU处理 → 内存(网络缓冲区) → 网卡发送

接收消息的过程:

复制代码
网卡接收 → 内存(网络缓冲区) → CPU解析 → 内存(显示缓冲区) → 显示器显示

💡 思考题: 如果在QQ上发送文件呢?数据又是如何流动的?(提示:文件先要从磁盘读入内存)


二、⚙️ 操作系统:管理一切软硬件的"大管家"

2.1 操作系统的概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS, Operating System)。笼统地说,操作系统包括:

  • 内核:进程管理、内存管理、文件管理、驱动管理
  • 其他程序:函数库、shell程序等

2.2 设计操作系统的目的

目标 说明
对下 与硬件交互,管理所有的软硬件资源
对上 为用户程序(应用程序)提供一个良好的执行环境

2.3 核心功能:管理

在整个计算机软硬件架构中,操作系统的定位是:一款纯正的**"搞管理"**的软件。

那么,如何理解"管理"呢?让我们用一个生活中的例子来说明:

管理的例子:学生、辅导员、校长

假设一所学校有 thousands of 学生,校长是如何"管理"这些学生的?

  • 校长不可能认识每一个学生
  • 但校长可以通过数据来了解学生情况
  • 每个学生的信息被描述成一张表格(姓名、学号、成绩等)
  • 所有学生的表格被组织起来(按班级、年级分类)

类比到操作系统:

现实场景 操作系统
学生 进程/硬件资源
学生信息表 struct结构体(描述)
按班级组织 链表/红黑树等数据结构(组织)

2.4 操作系统管理硬件的方法

复制代码
┌─────────────────────────────────────────┐
│           应用程序(用户层)               │
├─────────────────────────────────────────┤
│  系统调用接口(System Call Interface)    │
├─────────────────────────────────────────┤
│  操作系统内核(进程/内存/文件/驱动管理)    │
├─────────────────────────────────────────┤
│           硬件(物理层)                  │
└─────────────────────────────────────────┘

💡 承上启下: 那么在还没有学习进程之前,操作系统是怎么进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来


三、🔄 进程:程序的执行实例

3.1 进程的基本概念

进程的定义可以从不同角度来看:

视角 定义
课本概念 程序的一个执行实例,正在执行的程序
内核观点 担当分配系统资源(CPU时间、内存)的实体
当前理解 进程 = 内核数据结构(task_struct) + 自己的程序代码和数据

3.2 描述进程:PCB(进程控制块)

**PCB(Process Control Block,进程控制块)**是操作系统中用于描述进程属性的核心数据结构。

操作系统 PCB名称
通用概念 PCB
Linux task_struct

task_struct是Linux内核的一种数据结构类型,它会被装载到RAM(内存)中,并且包含着进程的所有信息。

3.3 task_struct的内容分类

一个完整的task_struct包含以下关键信息:

字段 说明
标识符 描述本进程的唯一标识符(PID),用来区别其他进程
状态 任务状态(运行、睡眠、停止等)、退出代码、退出信号等
优先级 相对于其他进程的优先级
程序计数器 程序中即将被执行的下一条指令的地址
内存指针 包括程序代码和进程相关数据的指针
上下文数据 进程执行时处理器的寄存器中的数据
I/O状态信息 包括显示的I/O请求、分配给进程的I/O设备等
记账信息 处理器时间总和、使用的时钟数总和等

3.4 进程的组织方式

所有运行在系统里的进程都以task_struct双链表的形式存在内核中。

复制代码
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ task_1   │←──→│ task_2   │←──→│ task_3   │←──→│ task_4   │
│ (init)   │    │ (bash)   │    │ (vim)    │    │ (chrome) │
└──────────┘    └──────────┘    └──────────┘    └──────────┘

四、🔍 查看与获取进程信息

4.1 通过/proc文件系统查看

Linux提供了一个特殊的文件系统**/proc**,它以文件系统的方式提供了访问内核数据的接口。

bash 复制代码
# 查看所有进程信息
ls /proc

# 获取PID为1的进程信息(init/systemd进程)
ls /proc/1

/proc目录下的每个数字文件夹代表一个进程,文件夹名就是进程的PID。

4.2 使用ps命令查看进程

ps是Linux下最常用的进程查看工具:

bash 复制代码
# 显示所有进程(包括其他用户的)
ps aux

# 显示进程归属的进程组ID、会话ID、父进程ID
ps axj

# 常用组合:查看特定进程
ps ajx | grep myprocess

参数说明:

参数 含义
a 显示一个终端所有的进程,包括其他用户的进程
x 显示没有控制终端的进程(后台守护进程)
j 显示进程组ID、会话ID、父进程ID等作业控制信息
u 以用户为中心的格式显示,提供详细信息

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

在C程序中,可以使用以下系统调用获取进程ID:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    // 获取当前进程ID
    printf("pid: %d\n", getpid());
    
    // 获取父进程ID
    printf("ppid: %d\n", getppid());
    
    return 0;
}
函数 返回值 说明
getpid() pid_t 返回当前进程的PID
getppid() pid_t 返回父进程的PID

⚠️ 注意: ctrl+c是杀掉前台进程的快捷键!也可以使用kill -9 PID强制终止指定进程。


五、🌲 通过fork创建进程

5.1 fork初识

**fork()**是Linux中创建新进程的核心系统调用。调用fork后,会创建一个与当前进程几乎完全相同的子进程。

fork的特点:

  • 父子进程代码共享
  • 数据各自开辟空间,私有一份(采用写时拷贝技术)
  • fork有两个返回值

5.2 fork的基本用法

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    // 创建子进程
    int ret = fork();
    
    // 这行代码会被执行两次!
    printf("hello proc : %d!, ret: %d\n", getpid(), ret);
    sleep(1);
    return 0;
}

输出示例:

复制代码
hello proc : 1234!, ret: 0      ← 子进程打印(返回0)
hello proc : 1233!, ret: 1234   ← 父进程打印(返回子进程PID)

5.3 fork后的分流处理

由于fork会返回不同的值,通常用if-else进行分流:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int ret = fork();
    
    if(ret < 0) {
        // fork失败
        perror("fork");
        return 1;
    }
    else if(ret == 0) {
        // 子进程(返回0)
        printf("I am child : %d!, ret: %d\n", getpid(), ret);
    }
    else {
        // 父进程(返回子进程PID)
        printf("I am father : %d!, ret: %d\n", getpid(), ret);
    }
    
    sleep(1);
    return 0;
}

fork创建子进程时,代码段的共享是其最核心的特性之一,这一设计极大节省了内存资源,也是fork高效的根本原因。下面从底层实现、具体表现和本质原理三个层面详细说明:

5.3.1 代码共享的底层实现:物理内存完全共享

程序被加载到内存后,会被划分为多个段,其中代码段(Text Segment) 存储的是程序的二进制指令,具有只读属性(由MMU硬件和内核共同保护)。

fork创建子进程时,内核不会为子进程复制代码段的物理内存,而是做以下操作:

  1. 为子进程创建独立的task_struct(PCB)和独立的页表
  2. 子进程的页表中,代码段对应的页表项直接指向父进程代码段的物理内存页面
  3. 所有代码段的物理页面被标记为"只读",确保父子进程都无法修改

也就是说:父子进程的代码段,虚拟地址相同,且映射到完全相同的物理内存地址。整个系统中,这份代码只存在一份物理副本,被父子进程共享执行。

5.3.2 代码相同的具体表现
5.3.2.1 执行完全相同的指令流

父子进程拥有完全一致的代码逻辑,会执行程序中所有相同的函数、循环和分支判断。

关键注意点 :子进程不会从头开始执行整个程序 ,而是从fork()函数的返回点开始执行。

  • 父进程:执行完fork()的创建逻辑后,返回子进程PID,继续向下执行
  • 子进程:内核将其程序计数器(PC)设置为fork()的返回地址,直接从这里开始执行

这就是为什么fork之前的代码只会被父进程执行一次,而fork之后的代码会被父子进程各执行一次。

5.3.2.2 函数地址、全局函数指针完全相同

由于代码段物理共享,父子进程中:

  • 任何函数的地址(如main()printf()、自定义函数)完全相同
  • 全局函数指针的值完全相同
  • 跳转指令、函数调用指令的目标地址完全相同
5.3.2.3 动态链接库代码也共享

不仅程序自身的代码段共享,进程加载的所有动态链接库(如libc.so)的代码段也遵循同样的规则:

  • 系统中所有进程共享同一份动态库代码的物理内存
  • fork后的父子进程自然也共享这些动态库代码
5.3.3 与数据段"相同"的本质区别

很多初学者会混淆"代码相同"和"数据初始相同",这两者有本质区别:

对比项 代码段 数据段/堆/栈
物理内存 完全共享,只有一份副本 fork后初始指向相同物理页,但采用写时拷贝(COW)
修改行为 不可修改,修改会触发段错误 未修改时共享,任意一方修改时,内核复制一份新的物理页给修改方
最终状态 永远共享 修改后彻底分离,各自拥有独立副本

核心原因:代码是只读的,永远不会被修改,因此可以安全共享;而数据是可写的,必须保证进程的独立性,因此通过写时拷贝实现"读时共享,写时分离"。

5.3.4 特殊情况:代码段的写保护

如果尝试在程序中修改代码段的内容(例如通过指针修改函数地址),会触发段错误(Segmentation Fault)。这是因为:

  1. 内核将代码段的页表项标记为只读
  2. MMU在执行写操作时会检查权限,发现写只读页面时触发缺页异常
  3. 内核处理该异常时,发现是非法写操作,直接向进程发送SIGSEGV信号终止进程

这一保护机制确保了代码段的完整性,也保证了fork后代码共享的安全性。

六、🤔 fork核心问题深度解析

6.1 问题一:为什么fork给父子返回各自的不同的返回值?

这是一个设计上的精妙之处:

返回值 进程类型 用途
0 子进程 子进程可以通过getppid()获取父进程ID,所以只需要返回0表示自己"是子进程"
子进程PID (>0) 父进程 父进程需要知道创建了哪个子进程,以便后续管理(如wait、kill等)

设计意图: 让父进程能够区分并管理不同的子进程。

6.2 问题二:为什么一个函数会返回两次?

这看似奇怪的现象,实际上是因为:

💡 fork创建了一个新的进程!

执行流程如下:

复制代码
父进程: fork()调用 → 创建子进程 → 返回子进程PID → 继续执行后续代码
                          ↓
子进程:                  从fork返回 → 返回0 → 继续执行后续代码
  • 父进程中的fork返回子进程的PID
  • 子进程中的fork返回0

所以"返回两次"实际上是两个不同的进程分别返回

6.3 问题三:为什么一个变量,既==0,又>0?

这是最令人困惑的问题:

cpp 复制代码
int ret = fork();
if(ret == 0) {      // 子进程走这里
    // ...
}
else if(ret > 0) {  // 父进程走这里
    // ...
}

答案: 这不是"同一个变量",而是两个进程中的两个独立变量

详细解释:

  1. 父进程执行int ret = fork(),创建子进程
  2. 子进程会继承父进程的代码和数据(包括ret变量)
  3. 但子进程中的ret是独立的副本,与父进程的ret不是同一个内存地址
  4. 当fork返回时:
    • 父进程的ret被赋值为子进程PID(>0)
    • 子进程的ret被赋值为0

💡 本质: 父子进程有各自独立的地址空间,ret只是恰好同名而已,实际上是两个不同的变量!


七、📊 进程状态概览

Linux内核中,进程有以下状态(定义在kernel源代码中):

c 复制代码
static const char *const task_state_array[] = {
    "R (running)",      /* 0  - 运行状态 */
    "S (sleeping)",     /* 1  - 睡眠状态(可中断) */
    "D (disk sleep)",   /* 2  - 磁盘休眠(不可中断) */
    "T (stopped)",      /* 4  - 停止状态 */
    "t (tracing stop)", /* 8  - 追踪停止 */
    "X (dead)",         /* 16 - 死亡状态 */
    "Z (zombie)",       /* 32 - 僵尸状态 */
};
状态 说明
R (Running) 运行状态,进程要么在运行中,要么在运行队列中
S (Sleeping) 睡眠状态,进程在等待事件完成(可中断睡眠)
D (Disk Sleep) 磁盘休眠,不可中断睡眠,通常等待IO结束
T (Stopped) 停止状态,可通过SIGSTOP信号停止,SIGCONT恢复
Z (Zombie) 僵尸状态,子进程退出但父进程未读取退出状态

本节完

✅ 本节完...

📝 作者:say-fall | 编辑:say-fall | 🌟 原创不易,如果对你有帮助,记得 👍 点赞 + ⭐ 收藏 哦!

相关推荐
能喵烧香11 小时前
深度解析:Linux 与 Windows 超级权限账户的本质差异
linux·windows
开维游戏引擎11 小时前
AI自动生成游戏时,deepseek和mimo对比
android·游戏·语言模型·游戏引擎·ai编程
pixcarp11 小时前
知识库系统的内容资产闭环怎么设计
服务器·数据库·后端·golang
江畔柳前堤11 小时前
github实战指南01-账号配置与 SSH 密钥
运维·人工智能·深度学习·ssh·github·pyqt·信号处理
Moshow郑锴13 小时前
Ubuntu 26.04 中文输入法 : fcitx5+Rime中州韵引擎
linux·运维·ubuntu
莫名的好感°13 小时前
手机RAR解压怎么选?2026年二季度四款产品问答
服务器·网络·智能手机
qq_1631357514 小时前
Linux 【04-more命令超详细教程】
linux
sevencheng79815 小时前
【ADB】adb命令行常用按键模拟代码
linux·adb·模拟按键,返回键,音量键
暗影天帝15 小时前
BPI-R3 Mini 刷 Yuzhii DHCPD U-Boot 教程
linux
小赖同学啊15 小时前
智能连接器集群化高可用生产方案
linux·运维·人工智能