Linux系统篇(十一)——进程(一):从零入门操作系统:冯诺依曼体系到进程的完整理解
从冯诺依曼体系讲起,理解操作系统内核与系统调用,再到进程与 PCB,用 fork 与写时拷贝实战入门进程编程
项目背景
计算机是现代生活与编程学习的基础,而操作系统是连接硬件与软件的核心桥梁。想真正理解程序如何运行、系统如何工作,必须从底层原理开始:程序为什么必须先加载到内存?CPU 为什么只能和内存打交道?进程和程序到底有什么区别?
本文将循序渐进地梳理操作系统相关知识:从冯·诺依曼体系结构出发,理解操作系统的定位,再经由系统调用进入进程的世界,最后用 fork 实战创建进程,帮助读者建立完整的底层认知框架。
技术方案
1. 冯·诺依曼体系结构:计算机的骨架
冯·诺依曼体系是现代计算机的底层逻辑,定义了计算机的五大核心部件与数据流动规则。
核心组成:
- 输入设备:键盘、鼠标、网卡、摄像头等,负责把数据传入计算机;
- 输出设备:显示器、声卡、打印机等,负责把数据从计算机传出;
- 存储器(内存):数据的临时中转站,CPU 只能直接与内存交互;
- 运算器:负责算术与逻辑运算,是 CPU 的计算核心;
- 控制器:指挥协调各部件,控制指令的执行流程。
关键规则:
- 程序必须先加载到内存才能运行;
- CPU 只和内存打交道,外设与内存之间的数据交互由操作系统协调完成;
- 数据流动的本质是"拷贝",即在不同存储介质间的复制过程。
性能视角:
设备的"带宽"决定了系统瓶颈,速度排序为:
寄存器 > 高速缓存 > 内存 > 磁盘/外设
速度从上到下递减,成本也随之降低。操作系统的核心工作之一,就是通过调度和缓存让数据尽量停留在高速设备中,减少低速 IO 等待。
2. 操作系统:软硬件之间的"大管家"
操作系统是一组管理软硬件资源的程序集合,既是硬件的管理者,也是上层软件的服务提供者。
两种口径:
- 狭义的操作系统:即内核,负责进程管理、内存管理、文件管理、驱动管理四大核心功能;
- 广义的操作系统:内核 + 外壳程序(如 shell)+ 系统调用 + 系统库 + 第三方工具。例如安卓系统就是用 Linux 内核加外壳包装得到的。
设计目的:
- 对下管理硬件:统一管理 CPU、内存、磁盘、外设等资源,解决多程序竞争资源的冲突;
- 对上提供服务:为用户和应用程序提供稳定、安全、易用的接口,屏蔽硬件底层细节;
- 提高资源利用率:通过合理调度让 CPU 和 IO 设备尽可能并行工作,减少资源闲置。
校园类比:
可以把内核、系统服务与进程的关系想象成校园管理:校长(内核)在办公楼制定资源规则与调度秩序,握着分配教室、图书馆、网络的最终决定权;辅导员(系统服务/中间层)守在教学楼接收学生申请;学生(应用进程)需要教室(CPU 时间)、座位(内存)、黑板(显示设备)、网络接口(外设),但谁也不能直接闯进校长办公室,只能先向辅导员提交申请。
内核只需握住第一个学生节点的地址,就能通过单向链表遍历管理全校学生:新生入学就创建节点并链入链表,毕业就摘除节点,状态变化就更新节点信息——内核无需直接接触每个学生,就能高效完成全校资源与秩序的管控。
计算机硬件管理哲学总结:先描述(用 struct),再组织(用链表等高效数据结构)。
3. 系统调用与库函数
操作系统为了安全,将运行空间划分为用户态和内核态。用户程序无法直接操作硬件,必须通过系统调用向内核发起请求。
银行柜台模型:
- 用户程序就像银行客户,内核就像银行柜台;
- 客户不能直接进金库操作,必须通过柜台(系统调用)办理业务;
- 系统调用是用户态进入内核态的唯一入口,也是操作系统提供给用户的服务接口。
系统调用与库函数的关系:
库函数(如 C 标准库)是对系统调用的封装,提供更易用的接口。例如 printf() 最终会调用 write() 系统调用把数据输出到标准输出设备。库函数运行在用户态,而系统调用会触发用户态到内核态的切换。
系统架构
进程:程序运行的动态实体
程序是存储在磁盘上的静态文件,进程是程序运行起来后在内存中的动态实体。
- 进程 = 程序代码 + 数据 + 进程控制块(PCB);
- 每个进程都有独立的地址空间,包含代码段、数据段、堆栈段;
- 在 Linux 下,PCB 就是
struct task_struct,内核通过它描述和管理进程。
PCB 的主要内容:
| 类别 | 说明 |
|---|---|
| 标识符 | 描述本进程的唯一标识,用于区分其他进程 |
| 状态 | 任务状态、退出代码、退出信号等 |
| 优先级 | 相对其他进程的优先级 |
| 程序计数器 | 下一条即将执行的指令地址 |
| 内存指针 | 程序代码、进程数据及共享内存块的指针 |
| 上下文数据 | 进程执行时处理器寄存器中的数据 |
| I/O 状态信息 | I/O 请求、分配的设备、使用的文件列表 |
| 记账信息 | 处理器时间总和、时钟数、时间限制、记账号等 |
进程的组织: Linux 内核中,所有进程以 task_struct 双向链表的形式存在内核中。
进程关键属性:
- PID:进程的唯一标识符,类似"身份证号";
- PPID:创建当前进程的父进程 ID;
- 进程状态:运行态、就绪态、阻塞态等;
- 资源信息:打开的文件描述符、内存地址、CPU 寄存器状态等。
task_struct 核心字段示意:
struct task_struct {
// 1. 进程标识
pid_t pid; // 进程ID
pid_t tgid; // 线程组ID(主线程PID = 进程ID)
struct task_struct *parent; // 父进程指针
struct list_head children; // 子进程链表头
uid_t uid; // 真实用户ID
gid_t gid; // 真实组ID
// 2. 状态与调度
volatile long state; // 进程状态(核心字段)
int prio, static_prio, normal_prio; // 优先级
unsigned int rt_priority; // 实时进程优先级
struct sched_entity se; // 调度实体
// 3. 内存管理
struct mm_struct *mm; // 用户态地址空间
struct mm_struct *active_mm; // 内核态地址空间
// 4. 文件与IO
struct files_struct *files; // 文件描述符表
struct fs_struct *fs; // 文件系统信息
// 5. 信号处理
struct signal_struct *signal; // 信号相关全局信息
struct sighand_struct *sighand; // 信号处理函数集合
sigset_t blocked; // 阻塞的信号集
// 6. 上下文与栈
void *stack; // 进程内核栈指针
struct thread_struct thread; // CPU寄存器上下文
// 7. 时间统计
cputime_t utime, stime; // 用户态/内核态耗时
};
实施过程
1. 查看进程
Linux 中可用以下方式查看进程:
ls /proc # 查看内核暴露的进程信息目录
top # 动态监控进程资源占用
ps # 静态查看进程快照
2. 通过系统调用获取进程标识符
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("我的进程:%d\n", getpid()); // 获取当前进程 PID
printf("我的父进程:%d\n", getppid()); // 获取父进程 PID
return 0;
}
3. 通过 fork 创建进程
fork() 会创建一个新进程,返回值有三种情况:
- 小于 0:创建失败;
- 等于 0:当前代码运行在子进程中;
- 大于 0:当前代码运行在父进程中,返回值是子进程的 PID。
#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;
}
为什么会有两行输出?因为 fork() 创建了一个与父进程几乎一模一样的子进程,此后两个进程各自独立运行,都从 fork() 之后的代码继续执行。
4. 区分父子进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if (ret < 0)
{
perror("fork fail!\n");
return 1;
}
else if (ret == 0)
{
printf("我是一个子进程\n");
}
else
{
printf("我是一个父进程\n");
}
return 0;
}
注意:一个变量,两个返回值——因为 fork 之后存在两个进程,各自持有自己的变量副本。
关键特性:
fork()之后父子进程谁先执行,由操作系统调度器决定,顺序不确定;- 父子进程共享代码段,但数据段独立,修改数据互不影响。
5. 进程独立性:写时拷贝机制
父子进程的数据看似共享,实际通过**写时拷贝(Copy-On-Write, COW)**实现真正独立:
fork()刚创建子进程时,父子进程页表指向同一块物理内存,数据段只读共享;- 当任一进程尝试修改数据时,操作系统为修改方复制一份数据副本;
- 复制完成后,父子进程页表各自指向自己的物理内存,互不干扰。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int val = 100;
pid_t pid = fork();
if (pid == 0)
{
// 子进程修改 val
val = 200;
printf("子进程:val = %d,地址:%p\n", val, &val);
}
else
{
sleep(1); // 等待子进程先执行
printf("父进程:val = %d,地址:%p\n", val, &val);
}
return 0;
}
运行结果中父子进程的 val 值互不影响,印证了写时拷贝带来的数据独立性。
应用价值
- 建立底层认知:理解了冯·诺依曼体系、内核职责与进程模型,就能看懂程序从磁盘加载到 CPU 执行的完整链路;
- 指导日常排障:
ps、top、/proc等查看手段,是定位高 CPU、内存泄漏等问题的基本功; - 夯实系统编程基础:
fork、getpid等系统调用是编写守护进程、多进程服务的起点,写时拷贝机制更是理解进程隔离与性能优化的钥匙; - 为后续深入学习铺路:进程状态、调度、信号等进阶主题,都建立在本篇的概念框架之上。
SEO关键词
Linux, 操作系统, 冯诺依曼, 进程, PCB, 系统调用, fork, 写时拷贝
