小白从一块主板到一行代码:彻底搞懂进程、线程、栈和堆

你有没有想过,当你在电脑上运行一个 C 程序时,屏幕后面到底发生了什么?

前阵子我从「电脑里到底有什么」这个最朴素的问题出发,一路往下追问,最终把进程、线程、栈和堆这些概念串成了一条完整的链路。这篇文章,就是把这条链路完整记录下来,帮你把散落的知识点拼成一张图。


一、先看硬件:一台电脑里到底有什么

电脑可以拆成我们看得见的三部分:显示器主机键盘鼠标。真正的秘密都在主机(机箱)里。

主机内部大致是这样的:
在这里插入图片描述

电脑
├── 显示器
├── 主机(机箱)
│     ├── 主板(就是那一大块 PCB 板)
│     │     ├── CPU        (直接插在 CPU 插座上)
│     │     ├── 内存条      (插在内存插槽上)
│     │     ├── 显卡        (插在 PCIe 插槽上)
│     │     └── CMOS 电池   (装/焊在主板上)
│     ├── 硬盘(通过 SATA 线连接到主板)
│     ├── 电源
│     └── 散热风扇
└── 键盘 / 鼠标

这里有两个容易误会的地方,先讲清楚:

  • 主板就是那块 PCB 板。 PCB(印刷电路板)是材质名字,主板是功能名字,它们是同一个东西,不是两个器件。
  • 硬盘一般不「在」主板上。 CPU、内存条是直接插在主板上的,但硬盘是独立的部件,通过数据线连到主板。

再记住三个角色分工:

部件角色为什么
CPU大脑负责计算,所有指令都靠它执行
内存条临时仓库通电才能存数据,断电就清空
硬盘永久仓库断电数据还在

一句话:CPU 干活,内存存「正在用的」,硬盘存「长期留的」。


二、操作系统:一个有特权的常驻程序

硬件之上,是操作系统(Linux、Windows 等)。它和我们的程序一样,本质是软件,是一堆 C 代码编译成的机器指令。

但它有个特别之处:

  • 平时:安静地躺在硬盘里,什么都不干。
  • 开机时:BIOS/UEFI 把它从硬盘加载进内存,然后启动它。
  • 运行中:常驻内存不走,一直管理所有硬件。

它凭什么能管硬件?靠硬件提供的特权机制——CPU 被分成两种状态:

状态谁在里面能干什么
内核态操作系统直接操作 CPU、内存、硬盘、网卡
用户态你的程序只能算自己的数据,碰不了硬件

所以你的程序想读文件时,自己没权限碰硬盘,只能喊一句 open()——这就是系统调用:CPU 切到内核态,内核帮你读硬盘,把结果还给你,再切回用户态。

还有个容易被搞混的点:CPU 执行的,不只是操作系统的代码。 操作系统内核代码在 CPU 上跑,你的程序代码也在 CPU 上跑,两者交替。操作系统是「调度者」,它安排自己跑一会儿(管硬件),再安排你的程序跑一会儿。


三、进程:操作系统发的「虚拟地址空间」

我们申请创建一个进程,可以理解成申请一个「任务」。操作系统同意后,会给这个进程分配一个虚拟地址空间

关键在这里:这块内存并不是进程真正独享的物理内存,而是操作系统在物理内存里划出来借给进程用的一片区域,让进程「以为」自己拥有连续的内存。

这个虚拟地址空间内部,被分成四片区域:

在这里插入图片描述

四片区域各司其职:

区域装什么谁来管
栈 Stack局部变量、函数参数、返回地址编译器自动管理
堆 Heapmalloc / new 分配的内存我们手动管 + OS 配合
数据段全局变量、静态变量程序结束才释放
代码段编译后的机器指令只读,不可改

四、栈和堆:一个自动,一个手动

栈和堆是初学者最常迷糊的一对,用一张表说清楚:

对比项栈 Stack堆 Heap
放什么局部变量、函数参数malloc / new 动态分配
谁管理编译器自动程序员手动 free
速度快(指针一挪就好)慢(要找空闲块)
大小小(通常几 MB)大(受物理内存限制)
增长方向向下(高地址 → 低地址)向上(低地址 → 高地址)
生命周期函数结束自动消失手动释放才消失,忘了就泄漏

关于「谁管理」再精确一句:栈是编译器自动生成代码管理的(CPU 硬件还有专用的栈指针配合);堆是靠 malloc / free 这些库函数管理,而这些库函数底层会调系统调用,找操作系统要地皮。所以堆是「我们 + OS 一起工作」。

可以用一小段代码直观验证四个区域的地址:

#include <stdio.h>
#include <stdlib.h>

int global_var = 100;                 // 数据段

int main(void) {
    int stack_var = 10;               // 栈
    int *heap_ptr = malloc(sizeof(int)); // 堆
    *heap_ptr = 20;

    printf("全局变量(数据段): %p\n", (void*)&global_var);
    printf("局部变量(栈)    : %p\n", (void*)&stack_var);
    printf("malloc(堆)      : %p\n", (void*)heap_ptr);
    printf("函数地址(代码段): %p\n", (void*)main);

    free(heap_ptr);                   // 堆必须手动释放
    return 0;
}

编译运行后,你会看到四类地址的大小关系,正好对应上面的内存布局图:栈地址通常最大(顶部),代码段地址最小(底部)。


五、线程:进程里的多个「小任务」

一个进程里可以有多条线程,相当于把大任务拆成几个可以并发执行的小任务。

线程最核心的一条规则:

同一个进程的多个线程,共享堆、数据段、代码段,只有栈是各自独立的

为什么栈要独立?因为每个线程有自己的函数调用链(main 调 A,A 再调 B……),需要各自的空间来保存这些调用现场。而它们要操作同一份数据(共享堆和全局变量),所以其他三区共享。

举个创建线程的例子:

#include <stdio.h>
#include <pthread.h>

void *worker(void *arg) {
    int id = *(int *)arg;
    printf("线程 %d 在跑\n", id);
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    int a = 1, b = 2;

    pthread_create(&t1, NULL, worker, &a);  // 创建线程 1
    pthread_create(&t2, NULL, worker, &b);  // 创建线程 2

    pthread_join(t1, NULL);                  // 等待线程结束
    pthread_join(t2, NULL);
    return 0;
}

编译时要链接 pthread 库:

gcc demo.c -o demo -pthread
./demo

一个常见的疑问:多线程是不是必须多核 CPU 才能跑?

答案是:单核也能跑多线程。 单核上靠「时间片轮转」快速切换,看起来像同时进行;多核上才是真正的并行。简单说,多线程任务不依赖多核,但多核能让它真正同时跑、更快。


六、串起来:从代码到 CPU 的完整链路

把上面所有概念拼起来,就是这一条链路:

硬件(CPU / 内存 / 硬盘)
   ↓ 被管理
操作系统(内核代码,有特权,常驻内存)
   ↓ 分配虚拟地址空间 + 调度
进程(一个虚拟地址空间 = 栈 / 堆 / 数据段 / 代码段)
   ↓ 内部真正干活的
线程(执行流,共享空间,各有各的栈)
   ↓ 被 OS 调度到
CPU 核心(从内存取指令,真正执行)

注意最后一步的精确说法:CPU 执行的不是内存,而是代码(指令)。内存是装指令和数据的地方。操作系统把线程调度到 CPU 核心上,CPU 从内存里取出指令来执行。

拿「厨师」打个比方:内存是菜谱本,CPU 是厨师,操作系统是安排谁去做菜的人。是厨师照着菜谱本做菜,不是菜谱本自己去做菜。


七、顺带一问:编译器从哪来

既然操作系统是 C 写的,C 又需要编译器来翻译,那编译器又是哪来的?这是个「先有鸡还是先有蛋」的经典问题。

答案是自举(bootstrapping):

最底层:机器码(0101)
   ↑ 手写
汇编器
   ↑ 用汇编写
第一个简单的 C 编译器
   ↑ 用旧编译器编译新编译器
更强的编译器
   ↑ 不断自举
现在的 gcc / clang(能编译它们自己)

最早的程序员只能直接手写 0101 机器码,或者手写汇编器;有了汇编器,才写出第一个简单的 C 编译器;之后每一次升级,都是「旧编译器编译新编译器」,一步步自我增强到现在。


写在最后

把整条链用一句话收个尾:

硬件(CPU、内存、硬盘)上跑着操作系统;操作系统是有特权的常驻程序,负责调度 CPU、管理内存;我们申请进程时会分到一个虚拟地址空间(栈、堆、数据段、代码段四区);线程共享这个空间但各有各的栈;最后操作系统把线程调度到 CPU 核心上,CPU 从内存取指令执行。

搞懂这条链路,进程、线程、栈和堆就不再是四个孤立的名词,而是同一张图上的不同楼层。剩下的,就是多写几段代码,去实际感受它们各自的位置和脾气。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐