从内核源码角度看——进程和线程在内核里是同一个东西:struct task_struct。区别只在于创建时共享了哪些资源。本页用图拆开结构体、fork / execve / clone 的真实调用路径。
这是内核里最核心的结构体,几百个字段。按功能分组点击查看(节选与进程/线程最相关的字段):
task_struct 把「一个能独立跑的东西」需要的全部上下文都装下了:它是谁(pid/tgid)、住哪(mm)、开了哪些文件(files)、收到信号怎么办(sighand)、现在啥状态(__state)、该被怎么调度(se)、它爹是谁(parent)。
所以内核调度器只认 task_struct,根本不区分「这是进程还是线程」——它只关心这个 task_struct 该不该上 CPU。进程/线程的差异完全由「创建时共享了哪些字段」决定,下一节会看到。
task_struct 的 mm 指向它。这是「进程」和「线程」在内核层面的唯一本质分水岭:进程有自己独立的 mm,线程与同组共享同一个 mm。
用户态 fork() → glibc 包成 clone() 系统调用 → 内核 kernel_clone() → copy_process() → wake_up_new_task()。
fork 并不立刻复制物理内存。copy_mm 只是复制页表并把所有页标记为写时复制(COW)。父子共享同一份物理页,直到某一方写入才真正分裂。
这就是 fork 极快的原因——配合「子进程先被调度」,常见 fork+exec 场景下子进程往往还没写内存就 execve 换镜像了,COW 几乎从不会发生。
execve() 不新建 task_struct,而是把当前进程「换脑子」:丢弃旧地址空间,按新 ELF 建新地址空间,跳到新入口。pid 不变。
用户态 pthread_create()(glibc/NPTL)→ 调用 clone() 系统调用,带上一组共享标志。内核走的还是 kernel_clone → copy_process,区别全在这些标志改变了 copy_* 的行为。
| 资源 | fork 出的进程 | pthread 线程(CLONE_*) |
|---|---|---|
| mm_struct(地址空间) | 独立副本(COW) | 共享同一份 |
| 页表 / 物理内存 | 复制,写时分裂 | 完全共用 |
| 文件描述符表 | 默认复制 | 共享 |
| 信号处理表 | 复制 | 共享 |
| pid | 各自独立 | 各自独立(gettid 不同) |
| tgid(getpid 看到) | 各自不同 | 相同(同组) |
| 内核对象类型 | 都是 task_struct,调度器一视同仁 | |
CFS 调度器操作的是 task_struct 里的 sched_entity se。无论进程还是线程,只要是一个 task_struct,就是一个可被调度上 CPU 的实体。所以 N 个线程是 N 个并行调度单位(受限于 CPU 核数),这正是多线程能真正并发的原因。
内核线程(如 ksoftirqd、kworker)也是 task_struct,但mm = NULL——它们从不跑用户代码、没有用户地址空间。调度切换时借上一个任务的 mm 作为 active_mm,避免无谓刷 CR3。它们由 kthread_create() → kernel_thread() → clone 创建,标志里通常带 CLONE_VM 但 mm 仍为空。
| 问题 | 内核里的真相(结构体 / 函数) |
|---|---|
| 进程和线程在内核是两种对象吗? | 不是。 都是 struct task_struct(include/linux/sched.h)。 |
| 那区别到底是什么? | 线程创建带 CLONE_VM 等标志 → 共享 mm_struct 且同 tgid;进程则不共享 mm。 |
| getpid 为何多线程一样? | 它返回 tgid,而同组线程 tgid 相同。 |
| fork 干了啥? | kernel_clone→copy_process:dup_task_struct + 各 copy_*(copy_mm 复制 mm、页表 COW)。 |
| execve 干了啥? | 不新建 task_struct,load_elf_binary 拆旧 mm、建新 mm/VMA、铺栈、start_thread 跳入口。 |
| 线程创建干了啥? | 还是 copy_process,但 CLONE_VM 让它共享 mm、CLONE_THREAD 让 tgid 相同。 |
| 地址空间在哪描述? | struct mm_struct(mm_types.h):mmap(VMA链表)+pgd(页表)+各区域边界。 |
| 内核线程特殊在哪? | mm=NULL,调度时用 active_mm 借用页表。 |