Linux的进程
进程的地址空间
处于隔离性的要求,每个进程所使用的地址为虚拟地址(`VA`),其中地址为 `0x0000000000000000` ~ `0xffffffffffffffff`(合理取64位计算机为例)。进程的地址空间分为用户态地址和内核态地址,其中用户态和内核态的地址范围如下。
地址范围 | 所属 | 描述 |
---|---|---|
0x0000000000000000 ~ 0x00007fffffffffff |
用户态 | 每个进程私有,可访问 |
0xffff800000000000 ~ 0xffffffffffffffff |
内核态 | 全系统共享,只有内核代码可访问 |
- 用户态的地址空间分布
我们可以将一个用户态进程的地址空间划分如下(从低地址到高地址)
1 | +-------------------------+ |
用户态地址空间各段的解释:
区域 | 说明 |
---|---|
代码段(.text) | 存储可执行代码(只读、可执行) |
数据段(.data) | 存储已初始化的全局变量或静态变量(可读写) |
BSS段(.bss) | 存储未初始化的全局变量或静态变量(占虚拟空间,运行时初始化为0) |
堆(heap) | 由 malloc() 等函数动态分配内存,向高地址扩展,由 brk() 或 mmap() 控制 |
mmap 映射区 | 用于映射共享库(如 libc.so)、匿名内存(如 mmap(NULL,...) )等,地址位置灵活(同时也是用户态线程栈的保存位置) |
用户栈(stack) | 默认大小通常为 8MB(可调),由内核在启动线程时创建,从高地址向低地址扩展 |
VDSO / vsyscall | 提供某些用户态可调用的内核函数(如 gettimeofday() ),提升性能 |
NULL 保留页 | 地址空间的最低页通常不映射,用于捕获非法指针访问(如 NULL dereference) |
- 内核态的地址空间分布,其中内核栈和per-CPU,虽然分配在全局地址空间,但是其内容不共享,为任务私有
1 | 用户空间(每进程私有): |
内核态各段的解释
区域 | 描述 |
---|---|
直接映射物理内存区域(Direct mapping) | 将所有物理内存直接线性映射进虚拟地址空间,常用宏 __va() 与 __pa() 转换 |
vmalloc 区 | vmalloc() 申请的非连续物理页区域,适合分配大对象。实现虚拟地址到离散的物理地址的映射 |
模块映射区 | 加载的内核模块、驱动代码位置 |
内核代码段 & 数据段 | 内核自身代码与静态数据区域(内核本身的代码,不可以修改) |
fixmap 区 | 用于映射特定设备地址或中断向量表,具有固定虚拟地址 |
ioremap 映射区 | IO 设备的 MMIO 寄存器映射到的虚拟地址 |
进程的实体表示
在Linux内核中,进程的实体表示是使用 task_struct
这一数据结构来表示的,在Linux中,Linux 采用 1:1 的线程实现模型,每个用户态线程对应一个内核态线程,内核通过 task_struct
来统一表示进程和线程,线程因此也被称作“轻量级进程”(Lightweight Process, LWP) 。轻量级进程会共享主进程的 mm_struct
,fs_struct
,signal_struct
等等这些内容,他们各自的 task_struct
是各不相同的。
下面是一个 task_struct
的基本结构:
1 | task_struct |
基本标识:pid是进程的唯一标识,而tgid则是所在线程组组长的pid(主线程的pid)
调度信息:prio是优先级,se则是实际的调度实体,而policy则是调度的策略
内存信息:mm用户态内存描述符,是一个指向mm_struct的指针,active_mm则是线程当前使用的mm_struct,这是因为内核线程会借用其他进程的,VMA是虚拟内存区域,用来描述进程虚拟地址空间中一段连续虚拟内存区域的数据结构,可以管理进程的虚拟地址空间到物理地址的映射关系,采用了红黑树的数据结构来管理。
线程组信息:group_leader指向线程组的组长,thread_group则是线程组的内部链表
信号处理:signal是指向 signal_struct的,线程组共享; sighand则是指向sighand_struct(信号处理函数表)
文件系统:files指向files_struct,表明打开文件表;而fs则是维护文件系统的信息
进程的状态
从操作系统的宏观角度:
- 创建:正在创建进程,还未就绪
- 就绪:已准备好、等待被调度上 CPU
- 运行:正在 CPU 上执行
- 阻塞:等待 I/O 或资源,不能运行
- 终止:执行完毕或被终止,等待资源释放
从Linux内核的具体实现角度:
- TASK_RUNNING:可运行,可能在 CPU 上,也可能在 runqueue 中等待
- TASK_INTERRUPTIBLE:可被信号打断的睡眠,常见于等待资源、I/O
- TASK_UNINTERRUPTIBLE:不可被信号打断的睡眠,如 I/O 阻塞
- EXIT_ZOMBIE:已退出,变成僵尸进程,等待父进程回收
- __TASK_STOPPED:被
SIGSTOP
暂停 - __TASK_TRACED:被调试器(如
gdb
)暂停、单步等状态
进程的创建
在Linux中,进程的创建主要有,fork,vfork以及clone三种方式来创建进程。
系统调用 | 本质功能 | 地址空间复制 | 子进程执行时父进程状态 | 适用场景 |
---|---|---|---|---|
fork() |
创建一个子进程,复制当前进程 | 写时复制(Copy-on-write) | 父子进程并发执行 | 一般进程创建 |
vfork() |
类似 fork ,但不复制地址空间 |
共享地址空间(临时) | 父进程被阻塞直到子进程 exec() 或 _exit() |
创建后立即执行 exec |
clone() |
最灵活的创建机制,用于创建线程 | 可选共享(通过标志控制) | 父子可以并发执行 | 多线程、容器、线程池 |
fork()
和 vfork()
最终都会调用 clone()
,区别在于传入的参数不同;所有这三者最终调用内核中的 copy_process()
函数来构建新的 task_struct
进程的切换
简而言之,进程的切换就是保留当前进程的上下文,然后加载目标进程的上下文。
从更深入的角度解释则是:
1 | [当前CPU] |
进程的回收
在前面的介绍中已经知道,所有用户态进程都由 init /systemd 派生,所有内核线程都由 kthreadd 创建。因此,这里进程的回收将从子进程的角度展开介绍。
当子进程调用 exit()
正常退出,或因异常(如段错误)终止时,会进入内核的 do_exit()
函数。
在 do_exit()
中,内核会关闭打开的文件描述符,调用 exit_mm()
释放虚拟内存资源(仅限拥有 mm_struct
的用户进程),释放信号处理器资源,同时将进程状态设置为 EXIT_ZOMBIE
。
此后,内核会向父进程发送 SIGCHLD
信号,通知其可以回收当前子进程的退出状态。如果父进程已经提前退出,子进程会被重新挂载到 init
(PID 1)进程下,由其负责后续回收。
最后,do_exit()
调用 schedule()
永久让出 CPU,进程不再被调度。
父进程随后通过 wait()
或 waitpid()
等系统调用获取子进程退出信息,并触发 release_task()
对其进行彻底清理,包括释放其 task_struct
、内核栈、pid 表项等内核资源,从而完成整个进程回收流程。
进程的调度
Linux的调度是基于分时技术的,CPU的时间被分为片,每个进程的执行都是按照片为单位去执行。在进程调度的时候,调度的优先级是基于静态优先级和动态优先级结合的方法来得出的。
- 静态优先级:在进程被创建的时候分配的优先级,他是不可以改变的
- 动态优先级:调度器在运行过程中根据进程行为动态调整的优先级值,通常是通过CFS(complete Fair Schedule)完全公平调度器来通过
vcputime
来等效动态优先级
进程和线程在内核视角的区别
在 Linux 内核中,进程(process)和线程(thread)都由 task_struct
统一表示,并由 Slab 分配器分配内存。它们都是调度器中的可调度实体(schedulable entity),因此每个任务都有唯一的 PID 。
线程通常由 clone()
系统调用创建,不同的 CLONE_*
标志决定了它与父任务共享哪些资源。
- PID 与 TGID
- 每个任务(无论进程还是线程)都有唯一的 PID。
- 主线程:
PID = TGID
(线程组 ID) - 其他线程:
PID ≠ TGID
,但 TGID 等于主线程的 PID。
- 栈空间
- 每个任务有独立的 内核栈 (内核态使用,固定大小)
- 每个任务有独立的 用户栈 ;线程的用户栈分配在父进程的
mmap
区域内。
- 资源共享
- 进程:拥有独立的
mm_struct
(虚拟内存)、files_struct
(文件描述符表)、fs_struct
(文件系统信息)、sighand_struct
(信号处理表)等。 - 线程:通常共享这些资源(取决于
clone()
的标志,如CLONE_VM
、CLONE_FILES
、CLONE_FS
、CLONE_SIGHAND
等)。
- 进程:拥有独立的
- 调度
- 内核调度器并不区分进程或线程,都是以
task_struct
作为调度单位。 - 区别主要体现在地址空间是否独立,以及资源是否共享。
- 内核调度器并不区分进程或线程,都是以