news 2026/5/11 3:05:48

三、进程概念(操作系统与进程(1))

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
三、进程概念(操作系统与进程(1))

1. 冯诺依曼体系结构

常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

我们所认识的计算机,都是由一个个的硬件组件组成

• 输入单元:包括键盘, 鼠标,扫描仪, 写板等

• 中央处理器(CPU):含有运算器和控制器等

• 输出单元:显示器,打印机等

关于冯诺依曼,必须强调几点:

• 这里的存储器指的是内存

• 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)

• 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。

2. 操作系统(Operator System)

2-1 概念

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

笼统的理解,操作系统包括:

• 内核(进程管理,内存管理,文件管理,驱动管理)

• 其他程序(例如函数库,shell程序等等)

2-2 设计OS的目的

• 对下,与硬件交互,管理所有的软硬件资源

• 对上,为用户程序(应用程序)提供一个良好的执行环境

2-3 操作系统的定位

看一张分层图就懂了:

┌──────────────────────────────┐ │ 用户程序(浏览器、VSCode) │ ← 想用硬件 ├──────────────────────────────┤ │ 操作系统(Linux/Windows) │ ← 搞管理的 ├──────────────────────────────┤ │ CPU、内存、磁盘、网卡、显卡 │ ← 被管理的硬件 └──────────────────────────────┘

操作系统自己不干"计算"的活(不算账、不画图、不传文件),它只做一件事:谁想用硬件?我来安排。

类比:

角色现实中计算机里
老板/管理者安排谁用会议室、用多久操作系统
干活的人设计师、程序员用户程序
资源会议室、打印机CPU、内存、磁盘

操作系统不生产计算结果,它只是硬件资源的搬运工。


2-4 什么叫"管理"

管任何东西,就两步:

第一步:描述 —— 先把这东西是什么说清楚

管理一个学生,得先有个学籍表:

struct student { char name[20]; // 姓名 int id; // 学号 int age; // 年龄 char class[10]; // 班级 };

管理硬件完全一样。Linux 内核对每种硬件资源都定义了对应的结构体:

// 管理CPU里的一个进程——用 task_struct 描述 struct task_struct { pid_t pid; // 进程编号 long state; // 当前状态(运行/睡眠/暂停) unsigned int time_slice; // 还能用CPU多久 // ... 几百个字段 }; // 管理内存中的一块区域——用 mm_struct 描述 struct mm_struct { unsigned long start_code; // 代码段起始地址 unsigned long end_code; // 代码段结束地址 unsigned long start_data; // 数据段起始地址 // ... }; // 管理磁盘上的一个文件——用 file 结构体描述 struct file { loff_t f_pos; // 当前读写位置 fmode_t f_mode; // 读写权限 // ... };

规律:内核里每种硬件资源,都有一个对应的struct把它描述清楚。

第二步:组织 —— 把这些描述串起来方便查找

表有了,但不能让 1000 个学生信息散在地上。得用数据结构串起来:

// Linux 内核用链表组织进程 struct list_head tasks; // 所有进程串成一条双向链表

效果:

task_struct A → task_struct B → task_struct C → task_struct D ↓ ↓ ↓ ↓ pid=1 pid=42 pid=99 pid=301 state=睡眠 state=运行 state=暂停 state=运行

需要调度 CPU 时,遍历这条链表找"运行"状态的进程即可。


总结:计算机管理硬件的核心思路

就拿之前 GDB 调试的那个mycmd程序来说,它在 Linux 眼里长这样:

1. 描述 — struct task_struct 里面有: pid = 79913 # 进程编号 state = RUNNING # 当前正在跑 mm → 占了多少内存 files → 打开了哪些文件 ... 2. 组织 — 这个 task_struct 被挂到: 运行队列(CPU 调度用) 进程树(ps 命令查看用) 等待队列(阻塞时暂存用) ...

一句话:struct描述,再数据结构组织。所有"管理"本质上就这两个动作。

操作系统对各种资源的管理都逃不出这个套路:

被管对象描述(struct)组织(数据结构)
进程task_struct链表 + 树 + 哈希表
内存mm_structvm_area_struct链表 + 红黑树
文件fileinode链表 + 哈希表
设备devicepci_dev链表 + 树
网络包sk_buff链表(队列)

这就是操作系统"搞管理"的全部秘密——先描述,再组织,剩下所有花活(进程调度、内存分配、文件读写)都是在这两个基础上查表、改字段、挪位置。

2-5 系统调用和库函数

• 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用, 这部分由操作系统提供的接口,叫做系统调用。

• 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部 分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开 发。

操作系统是怎么进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来

┌────────────────────────────────────────────┐ │ 你的代码(printf, scanf, malloc) │ ← 你写的东西 ├────────────────────────────────────────────┤ │ 库函数(C标准库 / glibc) │ ← 别人封装好的,方便你用 │ 对系统调用做了封装,加了一层胶水 │ ├────────────────────────────────────────────┤ │ 系统调用(open, read, write, fork) │ ← 操作系统暴露的接口 ├────────────────────────────────────────────┤ │ 操作系统内核(进程管理、文件系统、内存管理)│ ← 干管理活的地方 ├────────────────────────────────────────────┤ │ 硬件 │ └────────────────────────────────────────────┘

系统调用是什么

操作系统把自己"锁了个壳子",不让用户程序直接碰硬件。但它留了几个窗口,你要干什么事,从窗口递条子进来:

用户程序(我是普通人) 操作系统(我是管理员) │ │ │ "我想开个文件" │ │ ──────────→ open() ────── │ 系统调用 │ │ 内核帮你访问磁盘 │ "我想读数据" │ │ ──────────→ read() ────── │ 系统调用 │ │ 内核帮你从磁盘读 │ "我想多生一个进程" │ │ ──────────→ fork() ────── │ 系统调用 │ │ 内核帮你创建新进程

系统调用 = 操作系统暴露的接口,是用户程序合法访问硬件资源的唯一通道。

库函数是什么

系统调用太"底层"了,不好用。比如write()

write(1, "hello\n", 6); // 要手动指定文件描述符1,手动数字符串长度6

有人把它包了一层:

printf("hello\n"); // 一样的效果,但好写得多

printf内部最终调了write这个系统调用,但它帮你处理了格式化、缓冲区这些细节。

// printf 内部干了啥(简化版): int printf(const char *fmt, ...) { char buf[1024]; // 1. 格式化字符串(把 %d %s 替换成实际值) vsprintf(buf, fmt, ...); // 2. 调用系统调用 write 输出 write(1, buf, strlen(buf)); // 3. 返回 }

两层的关系

系统调用库函数
谁写的内核开发者库开发者(glibc 团队等)
功能基础、原始封装过的,更友好
举例open,read,write,forkfopen,printf,scanf
函数名特征短小直接往往带f前缀或更长的名字
跨平台不同 OS 不一样库函数屏蔽差异,一套代码多平台

一句话:系统调用是毛坯房,库函数是精装修。你不一定住毛坯,但你住的精装房下面一定是毛坯。


操作系统怎么管理进程

把之前学的套路套进来就清楚了——先描述,再组织。

第一步:描述进程 —— struct task_struct

内核为每个进程建一张"档案表",就是task_struct

struct task_struct { pid_t pid; // 进程编号,比如你GDB里看到的79913 long state; // 状态:正在跑 / 在睡觉 / 暂停了 unsigned int time_slice; // 还能用CPU多久 struct mm_struct *mm; // 占了多少内存 struct files_struct *files; // 打开了哪些文件 // 还有几百个字段... };

你 GDB 里调试的那个mycmd,在 Linux 看来就是一张填好的表:

pid = 79913 state = RUNNING(或者 SLEEPING) mm → 指向它占的那几M内存 files → 指向打开的文件列表(stdin, stdout, stderr)

ps命令看到的所有信息,本质就是在查这张表里的字段。

第二步:组织进程 —— 链表 / 树 / 队列

内核里同时有几百上千个进程,不能散着放。用不同数据结构按不同目的串起来:

task_struct task_struct task_struct ┌──────────┐ ┌──────────┐ ┌──────────┐ │ pid=1 │─→ │ pid=42 │─→ │ pid=79913│ ← 链表:所有进程大串联 │ 全部排队用│ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ ┌──────────────┐ │ 运行队列 │ ← task_struct A → task_struct C → task_struct D │ (CPU调度用) │ 只有 state=RUNNING 的才挂在这个队列上 └──────────────┘ ┌──────────────┐ │ 等待队列 │ ← task_struct B → task_struct E │ (等IO完成) │ 点了 continue 的进程,在等键盘输入,先挂这睡觉 └──────────────┘ ┌──────────────┐ │ 进程树 │ bash ──→ gdb ──→ mycmd │ (父子关系) │ ps 命令看到的就是这棵树 └──────────────┘

套回 GDB 的场景

你在 GDB 里敲continue之后发生了什么,从进程管理角度走一遍:

1. mycmd(task_struct,pid=79913)正在跑 2. 你在 GDB 里敲 continue 3. 内核把 mycmd 从"暂停队列"挪到"运行队列" 4. CPU 调度到这个进程,执行 result += i 5. 时间片用完或又遇到断点 → 内核把它挪回"暂停队列"

你每次敲runcontinue,甚至程序里的printf,背后都是内核在查这些链表、改state字段、挪task_struct的位置


总结

进程管理 =task_struct描述 + 多种数据结构组织 + 根据情景查表挪位置。

跟之前学到的套路完全一样——管文件、管内存、管设备,全是这个思路。操作系统就是一个大管家,手里全是各种 struct 和链表。

3.进程

3-1 进程基本概念


程序和进程不是一回事

程序 进程 ─── ─── ~/test/mycmd ← 躺在磁盘上的可执行文件 ./mycmd 跑起来以后 ↓ 硬盘(死的,静态的) ┌──────────┐ ┌──────────┐ │ pid: 1234│ │ mycmd │ ─── 运行 ──→ │ 内存 │ │ 二进制文件│ │ CPU时间片 │ └──────────┘ └──────────┘ 内存(活的,动态的)

程序 = 菜谱。进程 = 照着菜谱正在炒菜。

同一个程序可以启动多个进程:

./mycmd & # 进程 pid=1001 ./mycmd & # 进程 pid=1002 ./mycmd & # 进程 pid=1003 # 三个进程互不影响,各自有各自的内存和CPU时间

内核怎么看进程

内核不关心你程序是干啥的,它只关心一件事:这个进程要占多少资源?给多少?给多久?

内核眼中: 进程 = 一个要CPU时间 + 要内存 + 要读写文件的"实体" 内核的活: "你要CPU?给你2ms,到点交出来" ← 调度 "你要内存?给你4KB" ← 分配 "你要读文件?等着" ← I/O

3-1-2 PCB — 进程的户口本

概念

管理进程,先得给每个进程建一份档案。这份档案就叫PCB(Process Control Block,进程控制块)

┌─────────────────────────────────┐ │ PCB (task_struct) │ │ ┌─────────────────────────────┐│ │ │ 标识符 (pid=79913) ││ ← 你是谁 │ │ 状态 (正在跑 / 在睡觉) ││ ← 你现在啥情况 │ │ 优先级 (排前面还是排后面) ││ ← 你重要不重要 │ │ 程序计数器 ││ ← 你下一条要执行的指令在哪 │ │ 内存指针 ││ ← 你占了哪块内存 │ │ 寄存器现场 ││ ← 把你暂停时CPU里暂存的值存这 │ │ I/O信息 ││ ← 你打开了哪些文件 │ │ 记账信息 ││ ← 你总共用了多少CPU时间 │ └─────────────────────────────┘│ └─────────────────────────────────┘

Linux 里的 PCB 就叫task_struct,是一个超大的 C 结构体,定义在内核源码里。

3-1-3 task_struct

1. 标识符(pid)— 唯一编号
pid_t pid; // 进程ID,比如你GDB里看到的 79913

跟你学号一样,全系统唯一,用ps能查。

2. 状态 — 现在在干嘛
long state; // RUNNING(在跑) / INTERRUPTIBLE(浅度睡眠) / UNINTERRUPTIBLE(深度睡眠) / STOPPED(暂停) / ZOMBIE(僵尸)

你在 GDB 里打断点停住,mycmd就从RUNNING变成STOPPED。你敲continue,又变回RUNNING

3. 优先级 — 谁先用 CPU
优先级高 ──→ 先跑,占CPU时间多 优先级低 ──→ 后跑,占CPU时间少
4. 程序计数器(PC)— 下一条指令在哪
int result = 0; ← 刚执行完 for(int i = s; ...) ← 刚执行完 result += i; ← 下一条要执行!!! PC 就指向这里

CPU 里有个寄存器叫 PC(Program Counter),存的就是下一条指令的内存地址。进程暂停时,PC 的值存进task_struct;恢复时从task_struct写回 CPU 寄存器。这就是为什么断点能精确定位到第 8 行。

5. 内存指针 — 占了哪块地
struct mm_struct *mm; // 指向描述该进程内存布局的结构体
task_struct mm_struct ┌──────────┐ ┌────────────────┐ │ *mm ─────┼──────────→ │ start_code: 0x1000 │ ← 你的 mycmd 代码在哪 └──────────┘ │ end_code: 0x1800 │ │ start_data: 0x2000 │ ← 你的全局变量在哪 │ stack: 0x7000 │ ← 你的局部变量在哪(i,result) └────────────────┘
6. 上下文数据 — 暂停时的快照

这个最关键,解释为什么 GDB 能随时暂停/恢复:

CPU内部有一堆寄存器:eax, ebx, ecx, edx, eip, esp, ebp ... ↑ 这些东西你在GDB info locals 看到的 i=1, result=0 就寄存在这些寄存器里 进程被暂停时: CPU 里所有寄存器的值 → 存入 task_struct 的 context 字段 进程被恢复时: task_struct 里的 context → 写回 CPU 寄存器 效果:进程接着断点处继续跑,好像从来没停过。

可以类比成单反相机的"开关机"——关机能回到上次拍摄位置(参数全存着),开机瞬间恢复。

7. I/O 信息 — 打开了哪些文件
struct files_struct *files; // 文件描述符表
task_struct files_struct ┌───────────┐ ┌────────────────┐ │ *files ───┼──────────→│ fd[0] → 键盘 │ ← stdin └───────────┘ │ fd[1] → 屏幕 │ ← stdout │ fd[2] → 屏幕 │ ← stderr └────────────────┘

printf("hello")时,内核通过这个表找到屏幕设备,把数据写过去。

8. 记账信息 — 用了多少资源
u64 utime; // 用户态跑了多久 u64 stime; // 内核态跑了多久

time ./mycmd能看到:

real 0m0.003s ← 实际过了多久 user 0m0.001s ← utime,你的代码核心跑了多久 sys 0m0.001s ← stime,内核帮你干活跑了多久

组织进程 — 链表串联

所有task_struct都挂在内核的一条双向链表上:

内核 task_struct 链表头 │ ├── task_struct(pid=1, init进程) │ ↓ ├── task_struct(pid=42, bash) │ ↓ ├── task_struct(pid=79913, mycmd) ← 你的程序 │ ↓ ├── task_struct(pid=80001, gdb) │ ↓ └── ...

ps auxtop命令看到的进程列表,就是内核顺着这条链表一条一条读出 task_struct 的字段显示给你看。


总结

进程 = 程序跑起来的实例 PCB = 这个实例的"档案本" Linux PCB = task_struct task_struct 里面有: 我是谁 (pid) 我在干嘛 (state) 我排第几 (priority) 我接下来跑哪行 (pc/程序计数器) 我占了哪块内存 (mm指针) 我被暂停时现场是啥 (寄存器上下文) 我开了哪些文件 (files指针) 我花了多少CPU时间 (utime/stime) 组织方式: 链表 —— 所有进程串起来,方便遍历

管进程和管学生没本质区别:先建档案(task_struct),再把档案按不同需求分类排队(链表/队列/树)。

3-1-4 查看进程

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

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

2. 大多数进程信息同样可以使用top和ps这些用户级工具来获取

3-1-5 通过系统调用获取进程标示符

• 进程id(PID)

• 父进程id(PPID)

#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-1-6 通过系统调用创建进程-fork初识

• 运行 man fork 认识fork

• fork有两个返回值

• 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。后面细讲

3-2 进程状态

3-2-1 简介

• R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行 队列里。

• S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。

• D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个 状态的进程通常会等待IO的结束。

• T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的 进程可以通过发送 SIGCONT 信号让进程继续运行。

• X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

3-2-2 进程状态查看

ps aux / ps axj 命令

• a:显示一个终端所有的进程,包括其他用户的进程。

• x:显示没有控制终端的进程,例如后台运行的守护进程。

• j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息

• u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等

3-2-3 Z(zombie)-僵尸进程

• 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后 面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

• 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

• 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

3-2-4 僵尸进程危害

• 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我 办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态

• 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中, 换句话说,Z状态一直不退出,PCB一直都要维护

• 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置 进行开辟空间。这里其实就是内存泄漏了 。 如何避免?后面讲

3-2-5 孤儿进程

• 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?

• 父进程先退出,子进程就称之为“孤儿进程”

• 孤儿进程被1号init进程领养,由init进程回收

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/11 3:05:47

AI代码助手性能基准测试:从原理到实践的科学评估方法

1. 项目概述&#xff1a;AI代码助手性能基准测试的“军备竞赛”最近在GitHub上闲逛&#xff0c;发现了一个挺有意思的项目&#xff1a;ameerkhan9394/ide-ai-benchmark。光看名字&#xff0c;核心信息就呼之欲出了——这是一个针对集成开发环境&#xff08;IDE&#xff09;中AI…

作者头像 李华
网站建设 2026/5/11 3:04:00

对接AI大模型之nginx代理配置SSE接口

一、背景 在对接AI大模型的时候&#xff0c;采用流式输出&#xff0c;可以较好地缓解用户等待的焦虑&#xff0c;但是&#xff0c;接口极其容易超时。 前端这时候会一直报错&#xff1a; Expected content-type to be text/event-stream, Actual: text/html而后端的错误显示…

作者头像 李华
网站建设 2026/5/11 2:57:30

SYsU-lang:模块化编译器教学框架,从LLVM IR到操作系统编译实践

1. 项目概述&#xff1a;SYsU-lang&#xff0c;一个为教学而生的编译器框架如果你正在学习编译原理&#xff0c;或者对“如何从零开始构建一个编译器”感到好奇&#xff0c;但又苦于无从下手&#xff0c;那么SYsU-lang这个项目很可能就是你一直在寻找的“脚手架”。它不是一个完…

作者头像 李华
网站建设 2026/5/11 2:56:44

2026 年 .NET 客户端常用 MVVM 框架推荐(附带使用情况投票)

前言随着 .NET 客户端开发持续演进&#xff0c;MVVM 依然是 WPF、WinUI、Avalonia、MAUI 等技术栈中最常见的架构模式之一。进入 2026 年&#xff0c;社区里可选的 MVVM 框架已经相当丰富&#xff0c;不同框架在易用性、功能完整度、社区活跃度和适配场景上各有侧重。本文将结合…

作者头像 李华
网站建设 2026/5/11 2:54:57

AI工具搭建自动化视频生成Vault

这个话题挺有意思。做视频的人应该都有过这种体验&#xff0c;剪片子剪到凌晨三点&#xff0c;调字幕调得眼睛发酸&#xff0c;换BGM换了七八首还是觉得不对味儿。后来我发现了一个路子&#xff0c;就是用AI来搭一个自动化的视频生成管道&#xff0c;我管它叫“Vault”&#xf…

作者头像 李华
网站建设 2026/5/11 2:54:34

wmux:原生Windows终端复用器,集成AI与浏览器自动化

1. 项目概述&#xff1a;为什么我们需要一个原生的 Windows 终端复用器&#xff1f;如果你是一名长期在 Windows 上工作的开发者&#xff0c;尤其是深度依赖命令行工具和 AI 编程助手&#xff08;如 Claude Code、Cursor、GitHub Copilot CLI&#xff09;的开发者&#xff0c;那…

作者头像 李华