1. 进程, 轻量级进程和线程
- 通常, 我们把进程定义为程序执行的一个实例
- 从内核的观点来看, 进程就是用来担当分配系统资源(cpu时间, 内存)的实体
- 大部分的多线程应用程序, 一个进程有多个用户线程组成, 每个线程代表进程的一个执行流, 通过pthread (POSIX thread)库的标准库函数集编写。 从内核角度来看, 这些多线程应用程序, 仅仅只是一个普通的进程, 程序中多个执行流的创建, 处理, 调度整个都是在用户态中进行的。他的缺点是, 线程阻塞的话, 进程也就被阻塞了。
- Linux 中, 可以采用轻量级进程, 来对多线程应用程序提供更好的支持。两个轻量级进程基本上可以共享一些资源, 如地址空间, 打开的文件等。可以通过将轻量级进程和每个线程关联起来的方式, 进行实现
2. 进程描述符
- 每个进程描述符都是task_struct类型结构, 他的字段包含了一个与进程相关的所有信息
2.1 进程状态
- 使用 state 字段可以描述进程当前所处的状态
2.2 标志一个进程
- 一般来说, 能够被独立调度的每个执行上下文都必须拥有他自己的进程描述符, 有自己的task_struct 结构
- 进程和进程描述符之间存在非常严格的一一对应关系, 内核对进程的大部分引用是通过进程描述符指针的形式进行的
- 使用进程标识符pid标识进程, 并且内核通过一个pidmap-array位图来表示当前已分配和闲置的pid号。
- Linux 把不同pid与系统中每个进程或者轻量级进程相关联, 提供最大的灵活性。
- 线程组, 一个线程组中所有的线程使用和该线程组领头线程相同的PID, 被存入进程描述符的tgid字段中。
2.2.1 进程描述符处理
- linux 通过把两个不同的数据结构(内核态的进程堆栈, 和 thread_info 线程描述符)紧凑的存放在一个单独为进程分配的存储区域内, 提升效率。
2.2.2 标志当前进程
- 将这两个数据结构,紧密的结合在一起, 可以使得内核很容易的从esp寄存器的值获取得到当前在cpu上正在运行的进程的 thread_info结构地址。
2.2.3 双向链表
2.2.4 进程链表
借助上面提到的双向链表将进程联系在一起。
2.2.5 TASK_RUNNING状态的进程链表
- Linux 2.6 内核实现的运行队列, 为了让调度程序能够在固定时间内选出“最佳”可运行程序, 而建立了多个可运行进程链表, 每种进程优先权对应一个不同的链表
- 进程优先权范围 0 ~ 139, 因而运行队列被拆成了 140 个不同运行级别的队列
2.3 进程间关系
2.3.1 pidhash 表及链表
- 在很多情况下, 内核必须能够从进程的PID中导出对应进程的描述符指针。为了加速查找的过程, 我们引入了4个散列表(为了处理不同类型PID的字段 pid, tgid, pgrp, session)
2.4 如何组织进程
2.4.1 等待队列
- 等待队列实现了在某个事件上的条件等待, 希望等待特定事件的进程把自己放进合适的等待队列, 并放弃控制权。 因而, 等待队列表示一组睡眠的进程。
- 引入了两个数据结构:
// 等待队列头
struct _ _wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct _ _wait_queue_head wait_queue_head_t;
// 等待队列中的元素
struct _ _wait_queue {
unsigned int flags;
struct task_struct * task;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct _ _wait_queue wait_queue_t;
通过等待队列头中的lock 自旋锁保证同步, 元素中的flag 标记是否为互斥进程, func回调函数。
2.4.2 等待队列的操作
互斥进程, 一次只能唤醒一个, 非互斥的进程, 可以唤醒多个
2.5 进程资源限制
3. 进程切换
3.1 硬件上下文
- 每个进程有自己的地址空间, 但是所有的进程必须共享CPU 寄存器, 因而, 在恢复一个进程执行之前, 内核必须保证每个寄存器装入了挂起进程的值
- 而必须装入寄存器的一组数据被称为硬件上下文, 进程硬件上下文的一部分放在TSS 段, 而剩余部分存放在内核态的堆栈中。
3.2 任务状态段
- 称为 TSS段
3.2.1 thread 字段
每个进程描述符中的thread_struct结构的thread 字段, 用来存放硬件上下文
3.3 执行进程切换
- 进程切换, 发生在 schedule 函数上, 分为两个步骤:
- 切换页全局目录来安装一个新的地址空间
- 切换内核态堆栈和硬件上下文
3.4 保存和加载FPU,MMU及XMM寄存器
- 8086cpu 不会再tss 中自动保存 FPU(浮点计算单元), MMX 和 XMM 寄存器信息, 利用 TS 标记, 在真正需要的时候, 实现对这些寄存器的保存和恢复操作。
4. 创建进程
- 写时复制技术, 提升进程创建的 效率。
4.1 clone, fork, vfork
底层都是通过clone实现
4.2 内核线程
- 不受不必要的用户态上下文的拖累, 效率高
- 内核线程只运行在内核态, 而普通进程可以在内核态运行, 也可以在用户态运行
5. 撤销进程
5.1 进程终止
- exit, exit_group
5.2 进程删除
- Unix 允许进程查询内核获取他的父进程的PID, 或者任何子进程的执行状态
- 因此, 不允许Unix 内核在进程一终止后就丢弃包含在进程描述符字段中的数据, 只有父进程发出了与被终止的进程相关的wait 类系统调用之后, 才允许这样做。
- 僵死状态: 技术上, 进程已经死掉了, 但是必须保存他的描述符, 直到父进程得到通知。