创建进程
使用fork函数创建进程
int pid = fork();
在执行此函数后,即从当前进程开了一个新的子进程。
pid=0表示当前是新进程,不为0代表当前为主进程,pid即子进程的编号。
创建线程
- 线程复制执行二进制指令
- 多进程缺点: 创建进程占用资源多; 进程间通信需拷贝内存, 不能共享
- 线程相关操作
- pthread_exit(A), A 是线程退出的返回值
- pthread_attr_t 线程属性, 用辅助函数初始化并设置值; 用完需要销毁
- pthread_create 创建线程, 四个参数(线程对象, 属性, 运行函数, 运行参数)
- pthread_join 获取线程退出返回值, 多线程依赖 libpthread.so
- 一个线程退出, 会发送信号给 其他所有同进程的线程
- 线程中有三类数据
- 线程栈本地数据, 栈大小默认 8MB; 线程栈之间有保护间隔, 若误入会引发段错误
- 进程共享的全局数据
- 线程级别的全局变量(线程私有数据, pthread_key_create(key, destructer)); key 所有线程都可以访问, 可填入各自的值(同名不同值的全局变量)
- 数据保护
- Mutex(互斥), 初始化; lock(没抢到则阻塞)/trylock(没抢到则返回错误码); unlock; destroy
- 条件变量(通知), 收到通知, 还是要抢锁(由 wait 函数执行); 因此条件变量与互斥锁配合使用
- 互斥锁所谓条件变量的参数, wait 函数会自动解锁/加锁
-
broadcast(通知); destroy
进程数据结构
- 内核中进程, 线程统一为任务, 由 taks_struct 表示
- 通过链表串起 task_struct
- task_struct 中包含: 任务ID; 任务状态; 信号处理相关字段; 调度相关字段; 亲缘关系; 权限相关; 运行统计; 内存管理; 文件与文件系统; 内核栈;
- 任务 ID; 包含 pid, tgid 和 *group_leader
- pid(process id, 线程的id); tgid(thread group id, 所属进程[主线程]的id); group_leader 指向 tgid 的结构体
- 通过对比 pid 和 tgid 可判断是进程还是线程
- 信号处理, 包含阻塞暂不处理; 等待处理; 正在处理的信号
- 信号处理函数默认使用用户态的函数栈, 也可以开辟新的栈专门用于信号处理, 由 sas_ss_xxx 指定
- 通过 pending/shared_pending 区分进程和线程的信号
- 任务状态; 包含 state; exit_state; flags
- 准备运行状态 TASK_RUNNING
- 睡眠状态:可中断; 不可中断; 可杀
- 可中断 TASK_INTERRUPTIBLE, 收到信号要被唤醒
- 不可中断 TASK_UNINTERRUPTIBLE, 收到信号不会被唤醒, 不能被kill, 只能重启
- 可杀 TASK_KILLABLE, 可以响应致命信号, 由不可中断与 TASK_WAKEKILL 组合
- 停止状态 TASK_STOPPED, 由信号 SIGSTOP, SIGTTIN, SIGTSTP 与 SIGTTOU 触发进入
- 调试跟踪 TASK_TRACED, 被 debugger 等进程监视时进入
- 结束状态(包含 exit_state)
- EXIT_ZOMBIE, 父进程还没有 wait()
- EXIT_DEAD, 最终状态
- flags, 例如 PF_VCPU 表示运行在虚拟 CPU 上; PF_FORKNOEXEC _do_fork 函数里设置, exec 函数中清除
- 进程调度; 包含 是否在运行队列; 优先级; 调度策略; 可以使用那些 CPU 等信息.
- 运行统计信息, 包含用户/内核态运行时间; 上/下文切换次数; 启动时间等;
- 进程亲缘关系
- 拥有同一父进程的所有进程具有兄弟关系
- 包含: 指向 parent; 指向 real_parent; 子进程双向链表头结点; 兄弟进程双向链表头结点
- parent 指向的父进程接收进程结束信号
- real_parent 和 parent 通常一样; 但在 bash 中用 GDB 调试程序时, GDB 是 real_parent, bash 是 parent
- 进程权限, 包含 real_cred 指针(谁能操作我); cred 指针(我能操作谁)
- cred 结构体中标明多组用户和用户组 id
- uid/gid(哪个用户的进程启动我)
- euid/egid(按照哪个用户审核权限, 操作消息队列, 共享内存等)
- fsuid/fsgid(文件操作时审核)
- 这三组 id 一般一样
- 通过 chmod u+s program, 给程序设置 set-user-id 标识位, 运行时程序将进程 euid/fsuid 改为程序文件所有者 id
- suid/sgid 可以用来保存 id, 进程可以通过 setuid 更改 uid
- capability 机制, 以细粒度赋予普通用户部分高权限 (capability.h 列出了权限)
- cap_permitted 表示进程的权限
- cap_effective 实际起作用的权限, cap_permitted 范围可大于 cap_effective
- cap_inheritable 若权限可被继承, 在 exec 执行时继承的权限集合, 并加入 cap_permitted 中(但非 root 用户不会保留 cap_inheritable 集合)
- cap_bset 所有进程保留的权限(限制只用一次的功能)
- cap_ambient exec 时, 并入 cap_permitted 和 cap_effective 中
- 内存管理: mm_struct
- 文件与文件系统: 打开的文件, 文件系统相关数据结构
- 用户态/内核态切换执行如何串起来
- 用户态函数栈; 通过 JMP + 参数 + 返回地址 调用函数
- 栈内存空间从高到低增长
- 32位栈结构: 栈帧包含 前一个帧的 EBP + 局部变量 + N个参数 + 返回地址
- ESP: 栈顶指针; EBP: 栈基址(栈帧最底部, 局部变量起始)
- 返回值保存在 EAX 中
- 64位栈结构: 结构类似
- rax 保存返回结果; rsp 栈顶指针; rbp 栈基指针
- 参数传递时, 前 6个放寄存器中(再由被调用函数 push 进自己的栈, 用以寻址), 参数超过 6个压入栈中
- 内核栈结构:
- Linux 为每个 task 分配了内核栈, 32位(8K), 64位(16K)
- 栈结构: [预留8字节 +] pt_regs + 内核栈 + 头部 thread_info
- thread_info 是 task_struct 的补充, 存储于体系结构有关的内容
- pt_regs 用以保存用户运行上下文, 通过 push 寄存器到栈中保存
- 通过 task_struct 找到内核栈
- 直接由 task_struct 内的 stack 直接得到指向 thread_info 的指针
- 通过内核栈找到 task_struct
- 32位 直接由 thread_info 中的指针得到
- 64位 每个 CPU 当前运行进程的 task_struct 的指针存放到 Per CPU 变量 current_task 中; 可调用 this_cpu_read_stable 进行读取
进程调度
调度策略与调度类
- 进程包括两类: 实时进程(优先级高),普通进程;实时进程(0-99); 普通进程(100-139)
- 两种进程调度策略不同: task_struct->policy 指明采用哪种调度策略(有6种策略)
- 实时调度策略, 高优先级可抢占低优先级进程
- FIFO: 相同优先级进程先来先得
- RR: 轮流调度策略, 采用时间片轮流调度相同优先级进程
- Deadline: 在调度时, 选择 deadline 最近的进程
- 普通调度策略
- normal: 普通进程
- batch: 后台进程, 可以降低优先级
- idle: 空闲时才运行
- 调度类: task_struct 中 * sched_class 指向封装了调度策略执行逻辑的类(有5种)
- stop: 优先级最高. 将中断其他所有进程, 且不能被打断
- dl: 实现 deadline 调度策略
- rt: RR 或 FIFO, 具体策略由 task_struct->policy 指定
- fair: 普通进程调度
- idle: 空闲进程调度
- 普通进程的 fair 完全公平调度算法 CFS(Linux 实现)
- 记录进程运行时间( vruntime 虚拟运行时间)
- 优先调度 vruntime 小的进程
- 按照比例累计 vruntime, 使之考虑进优先级关系
- 调度队列和调度实体
- CFS 中需要对 vruntime 排序找最小, 不断查询更新, 因此利用红黑树实现调度队列
- task_struct 中有 实时, deadline 和 cfs 三个调度实体, cfs 调度实体即红黑树节点
- 每个 CPU 都有 rq 结构体, 里面有 dl_rq, rt_rq 和 cfs_rq 三个调度队列以及其他信息; 队列描述该 CPU 所运行的所有进程
- 先在 rt_rq 中找进程运行, 若没有再到 cfs_rq 中找; cfs_rq 中 rb_root 指向红黑树根节点, rb_leftmost指向最左节点
- 调度类如何工作
- 调度类中有一个成员指向下一个调度类(按优先级顺序串起来)
- 找下一个运行任务时, 按 stop-dl-rt-fair-idle 依次调用调度类, 不同调度类操作不同调度队列
进程抢占
- 抢占式调度
- 两种情况: 执行太久, 需切换到另一进程; 另一个高优先级进程被唤醒
- 执行太久: 由时钟中断触发检测, 中断处理调用 scheduler_tick
- 取当前进程 task_struct->task_tick_fair()->取 sched_entity cfs_rq 调用 entity_tick()
- entity_tick() 调用 update_curr 更新当前进程 vruntime, 调用 check_preempt_tick 检测是否需要被抢占
- check_preempt_tick 中计算 ideal_runtime(一个调度周期中应该运行的实际时间), 若进程本次调度运行时间 > ideal_runtime, 则应该被抢占
- 要被抢占, 则调用 resched_curr, 设置 TIF_NEED_RESCHED, 将其标记为应被抢占进程(因为要等待当前进程运行
__schedule
)
- 另一个高优先级进程被唤醒: 当 I/O 完成, 进程被唤醒, 若优先级高于当前进程则触发抢占
- try_to_wake_up()->ttwu_queue() 将唤醒任务加入队列 调用 ttwu_do_activate 激活任务
- 调用 tt_do_wakeup()->check_preempt_curr() 检查是否应该抢占, 若需抢占则标记
- 执行太久: 由时钟中断触发检测, 中断处理调用 scheduler_tick
- 抢占时机: 让进程调用
__schedule
, 分为用户态和内核态- 用户态进程
- 时机-1: 从系统调用中返回, 返回过程中会调用 exit_to_usermode_loop, 检查
_TIF_NEED_RESCHED
, 若打了标记, 则调用 schedule() - 时机-2: 从中断中返回, 中断返回分为返回用户态和内核态(汇编代码: arch/x86/entry/entry_64.S), 返回用户态过程中会调用 exit_to_usermode_loop()->shcedule()
- 时机-1: 从系统调用中返回, 返回过程中会调用 exit_to_usermode_loop, 检查
- 内核态进程
- 时机-1: 发生在 preempt_enable() 中, 内核态进程有的操作不能被中断, 会调用 preempt_disable(), 在开启时(调用 preempt_enable) 时是一个抢占时机, 会调用 preempt_count_dec_and_test(), 检测 preempt_count 和标记, 若可抢占则最终调用
__schedule
- 时机-2: 发生在中断返回, 也会调用
__schedule
- 时机-1: 发生在 preempt_enable() 中, 内核态进程有的操作不能被中断, 会调用 preempt_disable(), 在开启时(调用 preempt_enable) 时是一个抢占时机, 会调用 preempt_count_dec_and_test(), 检测 preempt_count 和标记, 若可抢占则最终调用
- 用户态进程
进程创建
- fork -> sys_call_table 转换为 sys_fork()->
_do_fork
- 创建进程做两件事: 复制初始化 task_struct; 唤醒新进程
- 复制并初始化 task_struct, copy_process()
- dup_task_struct: 分配 task_struct 结构体; 创建内核栈, 赋给
* stack
; 复制 task_struct, 设置 thread_info; - copy_creds: 分配 cred 结构体并复制, p->cred = p->real_cred = get_cred(new)
- 初始化运行时统计量
- sched_fork 调度相关结构体: 分配并初始化 sched_entity; state = TASK_NEW; 设置优先级和调度类; task_fork_fair()->update_curr 更新当前进程运行统计量, 将当前进程 vruntime 赋给子进程, 通过 sysctl_sched_child_runs_first 设置是否让子进程抢占, 若是则将其 sched_entity 放前头, 并调用 resched_curr 做被抢占标记.
- 初始化文件和文件系统变量
- copy_files: 复制进程打开的文件信息, 用 files_struct 维护;
- copy_fs: 复制进程目录信息, 包括根目录/根文件系统; pwd 等, 用 fs_struct 维护
- 初始化信号相关内容: 复制信号和处理函数
- 复制内存空间: 分配并复制 mm_struct; 复制内存映射信息
- 分配 pid
- dup_task_struct: 分配 task_struct 结构体; 创建内核栈, 赋给
- 唤醒新进程 wake_up_new_task()
- state = TASK_RUNNING; activate 用调度类将当前子进程入队列
- 其中 enqueue_entiry 中会调用 update_curr 更新运行统计量, 再加入队列
- 调用 check_preempt_curr 看是否能抢占, 若 task_fork_fair 中已设置 sysctl_sched_child_runs_first, 直接返回, 否则进一步比较并调用 resched_curr 做抢占标记
- 若父进程被标记会被抢占, 则系统调用 fork 返回过程会调度子进程
线程创建
- 线程的创建
- 线程是由内核态和用户态合作完成的, pthread_create 是 Glibc 库的一个函数
- pthread_create 中
- 设置线程属性参数, 如线程栈大小
- 创建用户态维护线程的结构, pthread
- 创建线程栈 allocate_stack
- 取栈的大小, 在栈末尾加 guardsize
- 在进程堆中创建线程栈(先尝试调用 get_cached_stack 从缓存回收的线程栈中取用)
- 若无缓存线程栈, 调用
__mmap
创建 - 将 pthread 指向栈空间中
- 计算 guard 内存位置, 并设置保护
- 填充 pthread 内容, 其中 specific 存放属于线程的全局变量
- 线程栈放入 stack_used 链表中(另外 stack_cache 链表记录回收缓存的线程栈)
- 设置运行函数, 参数到 pthread 中
- 调用 create_thread 创建线程
- 设置 clone_flags 标志位, 调用
__clone
- clone 系统调用返回时, 应该要返回到新线程上下文中, 因此
__clone
将参数和指令位置压入栈中, 返回时从该函数开始执行
- 设置 clone_flags 标志位, 调用
- 内核调用
__do_fork
- 在 copy_process 复制 task_struct 过程中, 五大数据结构不复制, 直接引用进程的
- 亲缘关系设置: group_leader 和 tgid 是当前进程; real_parent 与当前进程一样
- 信号处理: 数据结构共享, 处理一样
- 返回用户态, 先运行 start_thread 同样函数
- 在 start_thread 中调用用户的函数, 运行完释放相关数据
- 如果是最后一个线程直接退出
- 或调用
__free_tcb
释放 pthread 以及线程栈, 从 stack_used 移到 stack_cache 中