线程定义、进程线程核心区分、进程内存结构、程序与进程对比简答+选择题解析

线程定义、进程线程核心区分、进程内存结构、程序与进程对比简答+选择题解析
线程定义、进程线程核心区分、进程内存结构、程序与进程对比简答选择题解析这篇东西最初是我给考研学生整理的复习讲义后来发现每次讲完他们还是会在缺CPU到底是就绪还是阻塞这种题上栽跟头索性把讲义扩写成一篇完整的文章。内容覆盖线程定义、进程线程的本质区别、进程内存布局、程序与进程的对比关系最后附一道经典选择题的逐选项拆解。期末、考研、面试都能用。目录目录一、线程是什么二、进程是什么三、进程与线程的核心区分四、进程内存结构五、程序与进程的对比六、一道经典选择题七、容易踩的坑八、补充问题九、自测与参考一、线程是什么二、进程是什么三、进程与线程的核心区分四、进程内存结构五、程序与进程的对比六、一道经典选择题七、容易踩的坑八、补充问题九、自测与参考一、线程是什么1.1 从一个场景说起你开着 Chrome后台在下一个 2G 的安装包前台在看 B 站视频旁边还挂着一个在线文档。如果浏览器内部只有一条执行流那下载的时候视频就得卡住所有任务排队来。但实际上这三件事是同时在跑的。原因很朴素浏览器内部有多条线程各管各的事。单核上它们交替执行并发多核上真正同时跑并行。线程存在的意义就一句话让一个程序能同时干多件事不用傻等。1.2 定义线程Thread是操作系统能够进行运算调度的最小单位。它被包含在进程之中是进程中的实际运作单位。这句话信息量不小我习惯拆成四个点来讲第一“运算调度”。CPU 时间片是发给线程的不是发给进程的。调度器挑的是下一个该跑哪个线程它不关心进程这个概念。第二“最小单位”。操作系统不会再往下拆了。你可能会想到协程但协程是用户态的东西内核根本不知道它的存在不算在操作系统的调度范畴内。第三“被包含在进程中”。线程不能脱离进程单独活着。进程被 kill底下所有线程一起死没有例外。第四“实际运作单位”。进程本身不执行任何一条指令。你在任务管理器里看到的进程真正在 CPU 上跑的是它里面的线程。教材里还有一句补充描述一条线程指的是进程中一个单一顺序的控制流一个进程中可以并发多个线程每条线程并行执行不同的任务。这里要区分并发和并行面试老爱问并发单核──A──B──A──B──A──B──→ 交替执行宏观上同时 并行多核 核心1──A──A──A──A──→ 核心2──B──B──B──B──→ 物理上真正同时一句话并发是逻辑上的同时并行是物理上的同时。1.3 内核线程和用户线程在 Unix System V 和 SunOS 的老文献里有个词叫轻量进程LWP第一次看到很容易懵。其实对应关系很简单轻量进程LWP 内核线程由内核调度器直接管理线程Thread 用户线程由用户态线程库比如 pthread管理内核不感知现代 Linux 里pthread_create()底层调的是clone()系统调用创建出来的线程本质上就是一个共享了地址空间的task_struct。Linux 内核眼里没有线程这个独立概念只有任务区别在于clone()时传了什么 flagsCLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD。这个知识点在 Linux 内核设计与实现 第三章讲得很清楚想深入的同学可以翻翻。1.4 生命周期新建 ──start()──→ 就绪 ──被调度──→ 运行 ↑ │ │ │ 等待I/O │ ▼ └──I/O完成── 阻塞 │ 终止 ←───────────┘有一条铁律必须记住阻塞不能直接跳到运行必须先回就绪态等调度。这条考试特别爱考后面选择题还会碰到。1.5 写段代码感受一下光说概念太虚跑一段代码直观得多publicclassThreadDemo{publicstaticvoidmain(String[]args){// main方法本身就跑在主线程上ThreaddownloadnewThread(()-{for(inti0;i100;i20){System.out.println( [下载] i%);try{Thread.sleep(400);}catch(InterruptedExceptione){}}});ThreadrendernewThread(()-{for(intf1;f5;f){System.out.println( [渲染] 第f帧);try{Thread.sleep(250);}catch(InterruptedExceptione){}}});download.start();render.start();System.out.println([主线程] 子线程已启动);}}跑一下你会发现输出顺序每次都不太一样。这就是线程调度的不确定性——三个线程共享同一个 JVM 进程的堆但各自有独立的栈和 PC谁先抢到 CPU 谁先输出。二、进程是什么2.1 定义进程Process是操作系统进行资源分配的最小单元是程序关于某数据集合上的一次运行活动是系统进行资源分配和调度的基本单位。注意核心词是资源分配。操作系统分内存、分文件描述符、分 I/O 设备对象都是进程不是线程。线程只是借用进程的资源来干活。2.2 PCBPCBProcess Control Block是进程存在的唯一标志。操作系统不认你的源代码只认 PCB。没有 PCB操作系统就不知道这个进程的存在。Linux 里对应的结构体是task_struct东西很多挑关键的列一下structtask_struct{pid_tpid;// 进程IDpid_ttgid;// 线程组IDstructtask_struct*parent;// 父进程volatilelongstate;// 状态// TASK_RUNNING(0) → 运行/就绪// TASK_INTERRUPTIBLE(1) → 可中断阻塞// TASK_UNINTERRUPTIBLE(2) → 不可中断阻塞intprio;// 动态优先级intstatic_prio;// 静态优先级unsignedintpolicy;// 调度策略structmm_struct*mm;// 虚拟地址空间structfiles_struct*files;// 文件描述符表structthread_structthread;// CPU上下文structlist_headchildren;// 子进程链表};创建进程的本质就是创建并初始化一个 PCB撤销进程就是回收 PCB。这个在汤小丹《计算机操作系统》第二章有详细展开也可以参考 王道考研复习指导 的对应章节。2.3 状态转换三态模型就绪 ←──────────────┐ │ │ │ 被调度 │ I/O完成 ▼ │ 运行 ──等待I/O──→ 阻塞 │ │ 时间片到/被抢占 ▼ 就绪回去排队 │ │ 执行完毕 ▼ 终止两条不能走的路线考试反复考阻塞 → 运行不行必须经过就绪。就绪 → 阻塞不行只有运行中的进程才能主动阻塞。三、进程与线程的核心区分3.1 一句话进程是资源分配的最小单元线程是运算调度的最小单位。整篇文章如果只能记一句话就记这句。选择题、简答题、面试这一句话能覆盖绝大多数考法。3.2 对比维度进程线程定位资源分配的最小单元运算调度的最小单位地址空间独立的共享进程的资源拥有内存、文件、设备借用进程的自己只有栈、PC、寄存器创建开销大要分配地址空间、页表小共享地址空间分配个栈就行切换开销大切页表、刷TLB小同进程内不用切页表通信IPC管道、共享内存、Socket…直接读写共享变量但要加锁独立性强一个崩了不影响别人弱一个线程段错误整个进程完蛋终止进程死 → 所有线程死一个线程死不影响其他线程3.3 打个比方我上课喜欢用这个类比进程是公司线程是员工。公司有办公室、电脑、打印机资源员工用公司的设备干活。公司间沟通走邮件、合同IPC慢员工间直接喊一嗓子就行共享变量快。一家公司倒闭不影响其他公司进程隔离一个员工搞出严重事故可能把整个团队拖下水线程崩溃拖垮进程。注册公司贵创建进程开销大招个员工便宜创建线程开销小。3.4 实测创建开销说进程创建比线程贵到底贵多少跑个数据看看importtime,threading,multiprocessingdefnoop():passdefbench_process(n100):t0time.perf_counter()ps[multiprocessing.Process(targetnoop)for_inrange(n)]forpinps:p.start()forpinps:p.join()returntime.perf_counter()-t0defbench_thread(n100):t0time.perf_counter()ts[threading.Thread(targetnoop)for_inrange(n)]fortints:t.start()fortints:t.join()returntime.perf_counter()-t0if__name____main__:ptbench_process()ttbench_thread()print(f100个进程:{pt:.4f}s)print(f100个线程:{tt:.4f}s)print(f倍数:{pt/tt:.1f}x)我机器上Ubuntu 22.04, i7-12700跑出来大概是 18 倍左右。不同机器会有差异但量级上差一个数量级是稳定的。这就是为什么 Nginx 用多进程 多路复用而不是为每个连接 fork 一个进程——fork 太贵了。3.5 切换开销差在哪进程切换比线程切换慢主要慢在两步切换页目录基址寄存器x86 上是 CR3——告诉 CPU “现在用新进程的页表了”刷新 TLB——TLB 是页表的缓存换了页表旧缓存就全废了后续访存全部 miss要重新从内存加载页表项同进程内的线程切换不需要这两步因为页表是同一个。所以快很多。但有个前提同进程内。如果是不同进程的线程之间切换该切页表还是得切跟进程切换没区别。线程切换快这个结论只在同进程内成立考试出判断题的时候注意这个限定条件。四、进程内存结构4.1 全景这张图我画了很多遍每次给学生讲都会强调建议截图存下来高地址 ┌─────────────────────────────────────────────┐ │ 内核空间用户态不可访问 │ ├─────────────────────────────────────────────┤ │ 线程A栈 │ 线程B栈 │ 线程C栈 │ ... │ ← 私有 │ (局部变量、参数、返回地址) │ ├─────────────────────────────────────────────┤ │ ↓ 空闲区域 ↓ │ ├─────────────────────────────────────────────┤ │ 堆 (Heap) │ ← 共享 │ malloc / new 动态分配 │ ├─────────────────────────────────────────────┤ │ BSS未初始化全局/静态变量 │ ← 共享 ├─────────────────────────────────────────────┤ │ Data已初始化全局/静态变量 │ ← 共享 ├─────────────────────────────────────────────┤ │ Text代码段只读 │ ← 共享 └─────────────────────────────────────────────┘ 低地址4.2 共享与私有共享的所有线程都能访问代码段——大家执行的是同一份机器指令复制多份纯属浪费数据段 / BSS——全局变量、静态变量天然需要被所有线程读写堆——动态分配的对象线程间要协作访问私有的每个线程独立一份栈——每个线程有自己的函数调用链不能混程序计数器PC——记录我执行到哪了各线程进度不同寄存器集合——线程切换时要保存/恢复的上下文设计思路很直觉共享的是数据私有的是执行状态。数据是协作的基础执行状态是独立性的保证。你想想如果栈也共享了会怎样线程 A 调func_a()压栈线程 B 同时调func_b()也压栈两者的局部变量和返回地址互相覆盖程序立刻崩溃。所以栈必须私有这不是设计偏好是逻辑必然。4.3 用代码验证#includestdio.h#includepthread.hintglobal_counter0;// 全局变量Data段共享void*worker(void*arg){intlocal0;// 局部变量栈上私有for(inti0;i1000000;i){global_counter;// 有竞争local;// 安全}printf(线程%ld: local%d\n,(long)arg,local);returnNULL;}intmain(){pthread_tt1,t2;pthread_create(t1,NULL,worker,(void*)1);pthread_create(t2,NULL,worker,(void*)2);pthread_join(t1,NULL);pthread_join(t2,NULL);printf(global_counter%d (应该是2000000)\n,global_counter);return0;}编译运行gcc-O2-odemo demo.c-lpthread./demo我跑出来的结果线程1: local1000000 线程2: local1000000 global_counter1387452 (应该是2000000)local永远是精确的一百万——栈上的私有变量别的线程碰不到。global_counter少了六十多万——两个线程同时做global_counter这个操作在 x86 上至少是三条指令load、add、store不是原子的大量更新被覆盖了。解决办法就是加锁pthread_mutex_tmtxPTHREAD_MUTEX_INITIALIZER;pthread_mutex_lock(mtx);global_counter;pthread_mutex_unlock(mtx);或者用原子操作__sync_fetch_and_add或 C11 的_Atomic性能比互斥锁好不少。这块内容在 APUE 第 11 章 讲得很细。五、程序与进程的对比5.1 定性程序是静态的进程是动态的。二者不是一一对应的关系。5.2 三个维度动静特性不同。程序是静态的就是磁盘上躺着的一个文件.exe、.class、.py不运行也存在可以永久保存。进程是动态的是程序跑起来之后的那个活动有创建、运行、终止的生命周期是暂时的。我常用的比方程序是乐谱进程是演奏。乐谱放一百年还是那个乐谱演奏完了就没了。组成结构不同。程序就是代码指令的集合没别的了。进程 程序段 数据段 PCB。PCB 是进程存在的唯一标志——没有 PCB 就不能叫进程。对应关系不同。不是一一对应的有两种一对多1 个程序 → 多个进程多开你双击三次计算器就出来三个计算器进程。一个 chrome.exe 可以对应好几个浏览器进程。1 个进程 → 多个程序调用一个 LOL 进程里面跑着 UI 模块、渲染模块、网络模块、音频模块。一个进程调用了多个程序模块。5.3 答题模板考试碰到这道简答题按这个结构写基本不会丢分题目比较程序与进程的区别。答1动静特性不同。进程是动态的程序是静态的。程序是有序代码的集合可永久保存进程是程序的执行过程有生命周期是暂时的。2组成结构不同。进程由程序段、数据段和进程控制块PCB组成PCB 是进程存在的唯一标志程序仅由代码指令构成。3对应关系不同。一个程序可多次执行对应多个进程如浏览器多开一个进程可包含多个程序模块如游戏进程包含 UI、渲染、网络等子程序。二者不是一一对应的关系。六、一道经典选择题题目下面关于进程的描述中正确的是 A. 进程获得 CPU 运行是通过调度得到的B. 优先级是进程调度的重要依据一旦确定就不能改变C. 在单 CPU 的系统中任意时刻都有一个进程处于运行状态D. 进程申请 CPU 得不到满足时其状态变为阻塞答案A逐项分析A 是对的。调度器的活儿就是决定下一个谁上 CPU。进程从就绪变运行唯一的路就是被调度器选中。没什么好纠结的。B 错在不能改变。优先级分两种静态优先级确实创建后就不变了但动态优先级是会根据运行情况调整的。比如等太久了给你提提优先级防止饿死跑太久了给你降降防止垄断 CPU。Linux 的 CFS 调度器里进程的 vruntime 就是在动态调整的。选项说一旦确定就不能改变一竿子打死了。C 错在任意时刻。想想这个场景所有进程都在等磁盘 I/O全部阻塞了CPU 在那空转执行 HLT 指令进入低功耗状态。这时候没有任何进程处于运行态。任意时刻太绝对了。D 是本题最大的坑也是最高频的易错点。申请 CPU 得不到满足状态还是就绪不是阻塞。为什么回到定义想“就绪的意思是万事俱备只欠 CPU”——你缺的就是 CPU那你当然还在就绪队列里排着。“阻塞的意思是给你 CPU 你也干不了活”——比如在等磁盘数据数据没来之前给你 CPU 你也不知道执行什么。所以缺 CPU → 就绪排队等着缺 I/O / 缺锁 / 缺资源 → 阻塞等外部事件这条搞清楚D 选项就不会选错了。我每年阅卷D 选项的错误率都在 40% 以上就是因为很多同学把得不到满足一律理解成阻塞没有区分缺的是什么。七、容易踩的坑“线程就是轻量级进程所以线程约等于进程”不是。“轻量进程只是早期 Unix 对内核线程的叫法。线程没有独立地址空间不能脱离进程存在不能被操作系统独立分配资源。进程是资源的拥有者”线程是资源的使用者这是本质区别。“多进程一定比多线程好”不一定。多进程隔离性好但创建切换贵、通信麻烦、数据共享困难。该用多线程的场景数据库连接池、Web 服务器处理请求你硬上多进程性能反而更差。Chrome 用多进程是因为标签页之间需要强隔离Nginx 用多路复用是因为连接之间共享大量状态。看场景选方案没有银弹。“阻塞态可以直接变运行态”不行。I/O 完成后进程只是具备了继续跑的条件但 CPU 给不给、什么时候给是调度器的事。必须先回就绪态排队。“线程切换一定比进程切换快”同进程内的线程切换确实快不用切页表。但如果是不同进程的线程之间切换该切页表还是得切跟进程切换没区别。这个限定条件考试出判断题时经常挖坑。“程序和进程是一回事”程序是死的文件进程是活的执行实例。双击一个程序OS 把代码加载到内存、创建 PCB、分配资源、创建主线程、开始执行——这一系列操作完成后才有了进程。而且一个程序可以跑出多个进程多开一个进程里可以包含多个程序模块。八、补充问题协程和线程什么关系协程是用户态的东西由程序员或运行时库调度不经过内核。切换开销极小不用陷入内核态通常几十纳秒但单线程内的协程没法利用多核。Go 的 goroutine、Python 的 asyncio、Kotlin 的 coroutine 都是协程实现。调度粒度进程 线程 协程切换开销也是这个顺序。Linux 里线程和进程到底啥区别内核层面都是task_struct没有独立的数据结构。线程就是clone()时共享了地址空间、文件描述符这些东西的 task。pthread是对clone()的封装。想看源码的话kernel/fork.c里的copy_process()函数是入口。Chrome 为什么用多进程隔离性。每个标签页可能跑不可信的 JS如果用多线程一个标签页段错误整个浏览器就没了。多进程下一个渲染进程崩了只影响一个标签页。代价是内存占用高——你开 20 个标签页任务管理器里能看到 20 多个 chrome 进程每个都占几十到几百 MB。Java 线程是内核线程还是用户线程HotSpot JVMJDK 1.2 以后是 1:1 映射每个new Thread()对应一个内核线程。能利用多核但创建开销也比纯用户线程大。顺带提一句Python 的 CPython 实现有 GIL全局解释器锁同一时刻只有一个线程能执行 Python 字节码所以 CPython 的多线程没法利用多核做 CPU 密集型并行这是个很常见的坑。进程切换 TLB 一定要刷吗传统实现是的。但现代 x86 有 PCIDProcess Context IdentifierARM 有 ASIDAddress Space ID可以在 TLB 表项里标记所属进程切换时不用全刷只更新当前标识就行开销小很多。这个在 Intel SDM Volume 3 里有详细说明。九、自测与参考自测线程是______的最小单位进程是______的最小单元。同一进程内的线程共享______私有______。阻塞态能直接变运行态吗一个程序能对应多个进程吗举个例子。缺 CPU 是什么态缺 I/O 是什么态优先级有几种哪种能变单 CPU 系统任意时刻都有进程在运行吗答案运算调度资源分配堆、数据段代码段栈、PC、寄存器不能必须经过就绪态能双击三次计算器就是三个进程就绪阻塞静态不变和动态可变两种不一定所有进程都阻塞时 CPU 空闲参考汤小丹、梁红兵.《计算机操作系统》第四版. 西安电子科技大学出版社. 第 2、3 章.Andrew S. Tanenbaum.《现代操作系统》第四版. 机械工业出版社. 第 2 章.王道考研.《操作系统考研复习指导》. 第 2 章.Robert Love.《Linux 内核设计与实现》第三版. 第 3、4 章.W. Richard Stevens.《UNIX 环境高级编程》第三版. 第 11、12 章.B 站南京大学 操作系统课程蒋炎岩讲得体系化适合从头捋一遍。最后这些概念本身真不复杂但很多人就是记不住、用不对。我带了几年课觉得问题出在学习方式上——大部分人在背结论而不是在理解为什么。你理解了栈为什么必须私有因为每个线程的调用链不同共享了就乱套就不用死记线程私有栈。你理解了就绪的定义就是只缺 CPU就不会把 D 选项选错。理解到位了考场上不需要回忆答案自己就出来了。2026 年 7 月更新