ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

进程与线程:一篇搞懂核心区别与 Linux 实现

进程与线程:一篇搞懂核心区别与 Linux 实现 一、进程磁盘上的可执行文件本身只是一个静态程序。例如./test当我们执行它以后操作系统会为程序创建运行环境包括建立虚拟地址空间加载代码和数据创建栈、堆等内存区域建立文件描述符等内核资源创建用于调度和管理的内核数据结构这时静态程序才真正变成一个运行中的进程。可以简单理解程序 运行所需资源 进程一个进程通常拥有自己的虚拟地址空间文件描述符内存映射信号处理信息工作目录用户权限PID 等资源不同进程的用户地址空间默认相互隔离因此一个进程不能直接通过普通指针访问另一个进程的数据。如果进程之间需要交换数据一般需要使用 IPC管道 FIFO 共享内存 消息队列 Socket 信号二、线程线程可以理解为进程内部的一条执行流也是 CPU 调度的重要单位。一个进程至少存在一个线程即主线程。例如int main() { while (1) { } }运行以后可以理解为进程 P └── 主线程如果创建多个线程std::thread t1(func); std::thread t2(func);那么进程 P │ ├── 主线程 ├── 线程 1 └── 线程 2虽然存在多条执行流但它们仍然属于同一个进程。因此可以先记住一句话进程偏向资源管理 线程偏向程序执行三、共享资源同一个进程中的线程不会各自复制一整套进程资源。它们通常共享代码段 数据段 全局变量 静态变量 堆 mmap 映射区域 虚拟地址空间 文件描述符表但每个线程必须拥有自己的线程 ID 用户栈 内核栈 CPU 寄存器现场 程序计数器 PC 调度状态可以画成一个进程 │ ┌────────────┼────────────┐ │ │ │ 线程1 线程2 线程3 │ │ │ 栈1 栈2 栈3 │ │ │ PC1 PC2 PC3 共同共享 │ ┌───────────┼───────────┐ │ │ │ 代码 堆 mmap │ 全局数据最重要的一句话线程共享进程资源但保留独立的执行现场。Linux 中task_struct里一般保存什么在 Linux 中无论是进程还是线程本质上都对应一个可以被调度的task每个 task 都拥有自己的task_struct。需要注意的是task_struct中没有单独叫做LWP的字段线程的 LWP 本质上就是线程 ID也就是该 task 的pid而同一个进程中的多个线程拥有相同的tgid因此可以简单理解为pid更接近线程 TID/LWPtgid更接近我们平时看到的进程 PID主线程满足pid tgid。task_struct本身可以看成 Linux 内核中一个执行流的“总档案”其中通常包含线程身份信息、调度信息、内存信息、文件信息、信号信息以及执行现场等例如struct task_struct { pid_t pid; // 当前 task 的 TID / LWP pid_t tgid; // 所属进程的 PID struct task_struct *parent; struct task_struct *group_leader; int prio; // 调度优先级 unsigned int policy; // 调度策略 struct sched_entity se; // 调度实体 struct mm_struct *mm; // 虚拟地址空间 struct files_struct *files; // 文件描述符表 struct fs_struct *fs; // 工作目录等文件系统信息 struct signal_struct *signal; struct sighand_struct *sighand; struct cred *cred; // 用户权限信息 void *stack; // 内核栈 // 还有 cgroup、namespace、CPU affinity、 // 时间统计、ptrace、seccomp 等大量信息 };不同线程拥有不同的task_struct、pid、栈和调度现场但同一进程中的线程可以让mm、files等指针指向同一个对象从而共享虚拟地址空间、堆、文件描述符等资源。因此 Linux 中所谓“线程比进程轻量”并不是线程没有自己的 PCB而是它虽然有独立的task_struct却共享了大量进程级资源。四、线程栈线程的栈保存局部变量函数参数返回地址函数调用现场假设线程1 → func1() 线程2 → func2()两个线程的函数调用过程完全可能不同。如果它们共用同一个普通线程栈那么局部变量 返回地址 函数调用现场都会互相覆盖程序会立即混乱。因此线程1 → 用户栈1 线程2 → 用户栈2 线程3 → 用户栈3教材中有时会出现“进程栈”这个说法。对于单线程进程可以粗略认为进程栈 ≈ 主线程栈但从严格意义上说栈属于线程而不是整个进程只有一份栈。五、“线程堆”通常没有所谓的“线程堆”。同一个进程中的线程共享整个虚拟地址空间因此也共享Heap例如int* p new int(10);如果线程 A 得到了p线程 B 也拿到了这个地址那么两个线程都可以访问这块内存。因此进程虚拟地址空间 代码段 ← 共享 数据段 ← 共享 堆 Heap ← 共享 mmap ← 共享 线程1栈 ← 独立 线程2栈 ← 独立 线程3栈 ← 独立正因为线程共享堆、全局变量等资源多线程程序才容易发生数据竞争 Use After Free 竞态条件所以通常需要mutex semaphore spinlock atomic进行同步。六、Linux 中进程和线程是怎么实现的传统操作系统中经常使用PCB Process Control Block 进程控制块来描述进程。Linux 中一个非常重要的数据结构是struct task_struct可以把它粗略理解为 Linux 中描述一个可调度任务的数据结构。这里有一个非常关键的知识点Linux 中每一个线程都有自己的 task_struct。例如一个进程有三个线程线程 T1 → task_struct A 线程 T2 → task_struct B 线程 T3 → task_struct C因此 Linux 更接近于一个可独立调度的执行流 ↓ task ↓ task_struct而不是一个进程 ↓ 一个 task_struct ↓ 里面再装很多线程这也是理解 Linux 线程模型的关键。七、同一个进程的线程共享资源虽然每个线程都有自己的task_struct但它们可以让其中的一些资源指针指向同一个对象。例如虚拟地址空间task_struct A ──┐ │ task_struct B ──┼──→ 同一个 mm_struct │ task_struct C ──┘mm_struct用来描述进程的虚拟地址空间。因此多个线程共享同一个mm_struct自然就共享代码 数据 堆 mmap文件描述符也是类似task_struct A ──┐ │ task_struct B ──┼──→ 同一个 files_struct │ task_struct C ──┘所以线程 Aint fd open(a.txt, O_RDWR);得到fd 3线程 B 如果知道3一般也能够write(3, ...);因为两个线程使用的是同一个文件描述符表。因此 Linux 线程可以理解为拥有不同 task_struct但共享大量进程级资源的 task。八、线程为什么还有自己的内核栈每个线程一般都有两种非常重要的栈用户栈 内核栈当线程执行普通代码func();运行在用户态使用线程自己的用户栈当线程执行read(fd, buf, 100);进入系统调用用户态 ↓ 内核态 ↓ 执行 Linux 内核代码这时内核需要保存自己的函数调用现场因此会使用该线程对应的内核栈所以线程 A ├── 用户栈 A └── 内核栈 A 线程 B ├── 用户栈 B └── 内核栈 B即使它们属于同一个进程也不能共用普通的线程栈。九、为什么线程切换通常比进程切换轻假设 CPU 从进程 A切换到进程 B两个进程通常拥有不同的mm_struct 页表 虚拟地址空间因此除了切换寄存器、PC、调度状态之外还可能涉及地址空间切换 页表切换 TLB 影响 Cache 影响所以进程切换通常比较重。而同一进程中的两个线程线程 A ──┐ ├──→ 同一个 mm_struct 线程 B ──┘通常不需要切换整个地址空间。主要切换寄存器 PC 栈 调度状态因此一般来说线程切换成本 进程切换成本注意这里只能说通常更低并不是线程切换完全没有成本。十、进程和线程各有什么优缺点1. 进程隔离性更强不同进程拥有独立虚拟地址空间。例如进程 A 崩溃int* p nullptr; *p 10;通常不会直接破坏进程 B 的用户空间。因此进程的优势是隔离性强 安全性高 一个进程出错不容易直接污染另一个进程2. 线程通信更加方便线程共享同一个地址空间因此可以直接共享变量int global 10;线程 Aglobal 20;线程 Bcout global;不需要像进程那样专门使用 IPC。因此线程通信方便 创建和切换成本通常较低但代价就是同步更加复杂 一个线程的内存错误可能影响整个进程十一、fork 和创建线程的区别Linux 中fork();主要创建一个新的进程。父进程 │ fork │ ┌─┴─┐ │ │ 父进程 子进程父子进程在逻辑上拥有独立地址空间。刚fork()时Linux 通常通过COW Copy-On-Write 写时复制暂时共享物理页。但一旦发生写操作就会逐渐分离。因此逻辑上仍然是两个独立进程而pthread_create();创建的是同一进程中的新线程。多个线程会共享地址空间 堆 文件描述符 mmap 全局变量fork ↓ 创建独立进程 ↓ 资源相对独立 pthread_create ↓ 创建线程 ↓ 大量资源共享十二、为什么 Linux 线程被称为轻量级进程Linux 没有把“进程”和“线程”完全设计成两套毫无关系的对象。内核统一把它们看成task每个 task 都有自己的task_struct 调度信息 执行现场区别主要体现在它们共享多少资源。普通进程可能是task A → mm_struct A task B → mm_struct B而同一个进程中的线程task A ──┐ ├──→ mm_struct X task B ──┘文件、信号等资源也可以类似共享。因此线程可以粗略理解成共享了大量资源的轻量级 task。这也是 Linux 中“轻量级进程”这一说法的来源。十三、进程与线程核心区别对比项进程线程核心作用资源管理、隔离CPU 执行流地址空间不同进程独立同进程线程共享代码/数据/堆不同进程通常独立同进程线程共享栈独立每个线程独立CPU 寄存器独立每个线程独立PC独立每个线程独立文件描述符不同进程通常分别维护同进程线程通常共享通信通常需要 IPC可直接共享变量创建成本较高较低切换成本通常较高通常较低隔离性强弱Linux task_struct每个 task 一个每个线程一个十四、模型图Linux 内核 task_struct A 线程 A │ │ ├──────────────┐ │ │ task_struct B │ 线程 B │ │ │ └──────┬───────┘ │ ┌─────────────┴─────────────┐ │ │ ▼ ▼ mm_struct files_struct │ │ 共享地址空间 共享文件表 │ ┌───────┼────────┐ │ │ │ 代码 数据 堆 │ mmap 线程 A 自己拥有 ├── 用户栈 A ├── 内核栈 A ├── PC └── CPU 寄存器现场 线程 B 自己拥有 ├── 用户栈 B ├── 内核栈 B ├── PC └── CPU 寄存器现场总结1. 进程是资源管理和隔离的基本单位线程是 CPU 执行和调度的基本单位。2. 同一进程中的线程共享地址空间、代码、数据、堆、mmap 和文件描述符等资源。3. 每个线程都有自己独立的用户栈、内核栈、PC、寄存器现场和调度状态。4. Linux 中每个线程都有自己的task_struct同一进程的线程通过共享mm_struct、files_struct等资源形成线程组。5. 线程通信方便、切换通常更轻但隔离性更弱也更容易产生数据竞争因此需要同步机制。
返回列表