目录
1. 前言
本专题我们开始学习进程管理部分,本文为概述部分。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
kernel版本:5.10
平台:arm64
2.进程相关
进程和线程的区别
- 进程
1.进程是处于执行期的程序,进程=程序+执行
2.进程是资源封装的单位,拥有独立的资源空间,包含很多资源:打开的文件、挂起的信号量、内存管理、处理器状态、一个或多个执行线程或数据段等;
3.进程通常通过fork系统调用来创建;
4.新创建的进程可以通过exec创建地址空间(用户栈初始化等),并载入新的可执行程序
5.进程退出可以自愿退出或非自愿退出 - 线程
1.线程是轻量级进程,是操作系统调度的最小单位
2.一个进程可以有多个线程线程共享进程的资源空间
3.线程通过clone方法来创建,会确定哪些资源与父进程共享,哪些资源线程独享
获取当前进程
在内核态,ARM64运行级别为EL1,SP_EL0寄存器在EL1上下文没有使用,利用SP_EL0寄存器存放当前进程描述符task_struct的地址。
创建进程的标志
低字节指定子进程结束时发送给父进程的信号代码,一般为SIGCHLD信号,剩余3字节给一clone标志组用于编码
/*
* cloning flags:
*/
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
/*共享内存描述符和所有的页表*/
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
/*共享根目录和当前工作目录*/
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
/*共享打开文件表*/
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
/*共享信号处理程序表、阻塞信号表和挂起信号表*/
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
/*
* 在创建子进程时启用Linux内核的完成量机制,wait_for_completion()会使父进程进入睡眠状态,
* 直到子进程调用execve()/exit()释放内存资源
*/
#define CLONE_PIDFD 0x00001000 /* set if a pidfd should be placed in parent */
/*如果父进程被跟踪,子进程也被跟踪*/
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
/*vfork系统调用时设置*/
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
/*设置子进程的父进程为调用进程的父进程*/
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
/*把子进程插入到父进程同一线程组中,并迫使子进程共享父进程的信号描述符,此标志为true则必须设置CLONE_SIGHAND*/
#define CLONE_THREAD 0x00010000 /* Same thread group? */
/*当clone需要自己的命名空间(即子进程自己的已挂载文件系统视图)时设置这个标志*/
#define CLONE_NEWNS 0x00020000 /* New mount namespace group */
/*共享System V IPC取消信号量的操作*/
#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */
/*为子进程创建新的线程局部存储段(TLS)*/
#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */
/*设置父进程的TID*/
#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */
/*
* 如果该标志被设置,则内核建立一种触发机制,用在子进程要退出或要开始执行新程序时
* 这些情况下,内核将消除由参数ctid所指向的用户态变量,并唤醒等待这个事件的任何进程
*/
#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */
/*遗留标志,内核忽略*/
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
/*设置这个标志使得CLONE_PTRACE标志失去作用*/
#define CLONE_UNTRACED 0x00800000/*set if the tracing process can't force CLONE_PTRACE on this clone*/
/*设置子进程TID*/
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
#define CLONE_NEWCGROUP 0x02000000 /* New cgroup namespace */
/*为子进程创建新的utsname命名空间*/
#define CLONE_NEWUTS 0x04000000 /* New utsname namespace */
/*为子进程创建新的ipc命名空间*/
#define CLONE_NEWIPC 0x08000000 /* New ipc namespace */
/*为子进程创建新的user命名空间,包括User id,Group id*/
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
/*为子进程创建新的pid命名空间*/
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
/*为子进程创建新的network命名空间*/
#define CLONE_NEWNET 0x40000000 /* New network namespace */
/*复制IO上下文*/
#define CLONE_IO 0x80000000 /* Clone io context */
进程的3个数据结构
进程是资源封装的单位,linux有3个数据结构来组织进程描述符:
(1) 链表:所有task_struct形成一张链表
(2) 树:所有task_struct指向它的父、兄弟、子,所有task_struct形成树,pstree可以看出进程树情况,父进程类似于子进程的监视器,子进程死父进程通过查看子进程的尸体(task_struct)可以知道子进程死亡原因,并将其清理
(3) 哈希表,通过PID可以快速检索出task_struct
进程的生命周期
- TASK_RUNNING对应运行和就绪,进程要么在CPU上执行,要么准备执行
- TASK_INTERRUPTIBLE:浅度睡眠,除了等IO ready唤醒,信号也可以唤醒,进程状态修改为TASK_RUNNING
- TASK_UNINTERRUPTIBLE:深度睡眠,一定要等到IO ready唤醒,不能被异步信号打断的挂起状态, 对于不可中断的执行流程会用到这个状态
- TASK_STOPPED:进程的执行被暂停。当进程收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU信号会暂停
- TASK_ZOMBLE: 进程变为僵尸态后,所有的资源都被free 了,只剩下task_struct了。因为linux认为应用态有很多不可以预知的行为导致进程挂掉,因此它会在进程挂掉时自动释放进程资源,只保留task_struct来给父进程看原因,一旦父进程通过waitpid返回则task_struct也被释放。
3. 进程调度相关
通过调度策略来选用调度器,每个调度器都定义了一个调度类,每个调度类中实现了本调度器的关键算法。
所有的调度类链接在一起并通过优先级排列,大内核调度器(scheduler)调度的时候将按照调度类的优先级进行调度。
调度器
调度器是一个操作系统的核心部分。可以比作是CPU时间的管理员。调度器主要负责选择某些就绪的进程来执行。不同的调度器根据不同的方法挑选出最适合运行的进程。目前Linux支持的调度器主要包括如下:
Stop scheduler、Deadline scheduler、RT scheduler、CFS scheduler、Idle scheduler
将调度器公共的部分抽象,使用struct sched_class结构体描述一个具体的调度类,他们都使用struct sched_class来定义调度类,系统核心调度代码会通过struct sched_class结构体的成员调用具体调度类的核心算法。
struct sched_class {
u8 state;
u8 idx;
struct ch_sched_params info;
enum sched_bind_type bind_type;
struct list_head entry_list;
atomic_t refcnt;
};
针对以上每一个调度器定义了一个调度类,有5个调度类。对于每一个调度类,系统中有明确的优先级概念。
#include/asm-generic/vmlinux.lds.h
/*
* The order of the sched class addresses are important, as they are
* used to determine the order of the priority of each sched class in
* relation to each other.
*/
#define SCHED_DATA \
STRUCT_ALIGN(); \
__begin_sched_classes = .; \
*(__idle_sched_class) \
*(__fair_sched_class) \
*(__rt_sched_class) \
*(__dl_sched_class) \
*(__stop_sched_class) \
__end_sched_classes = .;
优先级从高到低排列为:
stop_sched_class -> dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class
这个顺序是在如上链接脚本中定义的。
/* Defined in include/asm-generic/vmlinux.lds.h */
extern struct sched_class __begin_sched_classes[];
extern struct sched_class __end_sched_classes[];
#define sched_class_highest (__end_sched_classes - 1)
#define sched_class_lowest (__begin_sched_classes - 1)
#define for_class_range(class, _from, _to) \
for (class = (_from); class != (_to); class--)
#define for_each_class(class) \
for_class_range(class, sched_class_highest, sched_class_lowest)
通过for循环先处理最高优先级的调度类,调用它的调度算法找到下个调度实体,再处理次高优先级的调度类,调用它的调度算法找到下个调度体,直到处理最低优先级的调度类。
注:这部分的代码分析可参考 pick_next_task部分
关于调度类参考http://www.wowotech.net/process_management/447.html
补充:
1.O(N)调度器
1992年发布,每次调度前遍历所有的进程,选出优先级最高的进程作为下一个调度进程,算法复杂度O(N)
2.O(1)调度器
a.应用于内核2.6.23,是对O(1)调度器的优化,算法复杂度O(1)
b.通过为每个CPU维护一组进程优先级队列,每个队列对应一个优先级,并对应一张位图
c.通过查找每个优先级对应的位图看是否有就绪的进程
d.对交互式进程反应缓慢,且对NUMA支持不完善
3.CFS调度器
Con Kolivas提出了RSDL(楼梯调度算法)来实现公平性,RedHat公司的Ingo Molnar借鉴RSDL的思想提出一个CFS调度算法。
调度策略
/*
* Scheduling policies
*/
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
用户空间程序可以通过sched_setscheduler来设定用户进程的调度策略。其中:
- SCHED_NORMAL和SCHED_BATCH为CFS调度器的调度策略
- SCHED_FIFO和SCHED_RR为realtime调度器的调度策略
- SCHED_IDLE为dle调度器的调度策略
- SCHED_DEADLINE为deadline调度器的调度策略
进程优先级
进程分类(按调度策略) | 调度器 | 调度策略 | 优先级(normal_prio) | ||||||
---|---|---|---|---|---|---|---|---|---|
|
|
|
| ||||||
|
|
| |||||||
|
|
|
|
通过调度器一节和调度策略一节可知,每一个进程都属于某一个调度器的调度队列,每个调取器(或进程)都有自己的调度策略,依据采用的调度策略可将进程划分为两类:实时进程和普通进程。
- 实时进程
- deadline进程
进程采用deadline调度器,是优先级最高的实时进程,采用SCHED_DEADLINE实时调度策略,进程的调度优先级为-1; - realtime进程
进程采用Realtime调度器,是普通实时进程,采用SCHED_FIFO或SCHED_RR的实时调度策略,进程的调度优先级为:0~99;
- 普通进程
普通进程是采用非SCHED_FIFO和SCHED_RR的非实时调度策略的进程,一般采用CFS调度器,进程的调度优先级为:100~139;
(1).普通进程用户空间有一个传统的nice值(范围为-20~19,默认为0),映射到普通进程即100-139的进程,用于体现不同优先级的轮转公平性,普通进程优先级只体现在配额和唤醒后的抢占上
(2).普通进程的nice值的范围为-20~19,默认nice值为0,nice值的含义类似于优先等级,nice值越高,优先级越低
(3) nice值为0的权重为1024,其它nice值的权重通过查表获取,内核预先计算了一个表prio_to_weight[40],每个成员分别对应nice值分别为-20~19的权重
注:
1.进程的优先级分为静态优先级(static_prio)、动态优先级(prio)、标准优先级(normal_prio),实时优先级(rt_priority);
2.其中静态优先级和动态优先级只有普通进程才有;实时优先级为实时进程所特有;标准优先级为普通进程和实时进程都有
3.静态优先级(static_prio)是在进程启动时被初始化,数值越小,优先级越高,可以通过设置nice值来调整;
4.动态优先级(prio)是在进程执行中根据进程的行为(如 CPU 使用率)动态调整的;
5.实时优先级(rt_priority)也是在实时进程启动时设定,数值越小,优先级越高,可通过chrt 命令行工具来修改;
6.标准优先级(normal_prio)是为进程提供一个“标准化”的优先级值,供调度器使用
7.对普通进程,初始时静态优先级=动态优先级=标准优先级,通过set_user_nice函数可修改静态优先级,进而通过effective_prio函数修改动态优先级;
注:初始时普通进程的动态优先级初始化为父进程的标准优先级
8.对实时进程,通过sched_setparam可修改实时进程优先级(rt_priority),进而通过normal_prio函数修改标准优先级= MAX_RT_PRIO-1 - p->rt_priority;
关于进程优先级可参考:https://blog.youkuaiyun.com/gatieme/article/details/51719208
进程的权重
-
prio_to_weight[40]
记录不同的nice值对应的权重,nice越低权重越高,nice越高权重越低。下标值记录了nice从-20到19对应的权重
注:只针对优先级为100~139的普通进程 -
prio_to_wmult[40]
记录不同nice值的权重的倒数乘以2^32即inv_weight,计算公式如下,其中inv weight是inverse weight的缩写,代表权重被翻转了,用于计算方便
引入此的目的,如:CFS计算虚拟运行时间:
其中: vruntime表示进程虚拟的运行时间,delta_execi表示实际的运行时间,nice_0_weight表示nice为0的权重值,weight表示该进程的权重值。
为了计算方便,避免浮点运算,引入了inv_weight,因此vruntime可以表示为:
内核提供了set_load_weight(struct task_struct *p)来查询如上两张表,然后将结果存放到p->se.load数据结构,即struct load_weight
就绪队列
- 系统中每个CPU都会有一个全局的就绪队列(cpu runqueue),使用struct rq结构体描述,它是per-cpu类型,即每个cpu上都会有一个struct rq结构体
- 每一个调度类也有属于自己管理的就绪队列。例如,struct cfs_rq是CFS调度类的就绪队列,管理就绪态的struct sched_entity调度实体
进程切换
进程切换分为自愿(VOLUNTARY)与强制切换(INVOLUNTARY)
- 自愿切换(Voluntary)
进程运行不下去了:比如因为要等待IO完成,或者等待某个资源、某个事件 - 强制切换(Involuntary)
(1)进程还在运行,但内核不让它继续使用CPU了(发生了抢占):
比如进程的时间片用完了,或者优先级更高的进程来了,所以该进程必须把CPU的使用权交出来
(2)进程还可以运行,但它自己的算法决定主动交出CPU给别的进程:
用户程序可以通过系统调用sched_yield()来交出CPU,内核则可以通过函数cond_resched()或者yield()来做到
在支持抢占调度的系统里,进程调度与进程抢占同义?
参考:http://linuxperf.com/?p=209
进程抢占
|- - -抢占的过程
- 第一步触发抢占
给正在CPU上运行的当前进程设置一个请求重新调度的标志(TIF_NEED_RESCHED),仅此而已,此时进程并没有切换- 第二步执行抢占
在随后的某个时刻,内核会检查TIF_NEED_RESCHED标志并调用schedule()执行抢占
|- - -抢占的时机
- 触发抢占的时机
每个进程都包含一个TIF_NEED_RESCHED标志,内核根据这个标志判断该进程是否应该被抢占,设置TIF_NEED_RESCHED标志就意味着触发抢占。直接设置TIF_NEED_RESCHED标志的函数是 set_tsk_need_resched(),触发抢占的函数是resched_curr()。
(1). 周期性的时钟中断
时钟中断处理函数会调用scheduler_tick(),这是调度器核心层(scheduler core)的函数,它通过调度类(scheduling class)的task_tick方法 检查进程的时间片是否耗尽,如果耗尽则触发抢占,这里对deadline调度类、rt调度类、CFS调度类分别进行判断触发抢占
(2). 唤醒进程的时候
当进程被唤醒的时候,如果优先级高于CPU上的当前进程,就会触发抢占。
相应的内核代码中,try_to_wake_up()最终通过check_preempt_curr()检查是否触发抢占。
(3). 新进程创建的时候
如果新进程的优先级高于CPU上的当前进程,会触发抢占,跟唤醒进程类似,它会通过check_preempt_curr()检查是否触发抢占
(4). 进程修改nice值的时候?
如果进程修改nice值导致优先级高于CPU上的当前进程,也会触发抢占。内核代码参见 set_user_nice()
(5). 进行负载均衡的时候
在多CPU的系统上,进程调度器尽量使各个CPU之间的负载保持均衡,而负载均衡操作可能会需要触发抢占。
比如CFS类在load_balance()中触发抢占;RT类的负载均衡基于overload,如果当前运行队列中的RT进程超过一个,就调用push_rt_task()把进程推给别的CPU,在这里会触发抢占- 执行抢占的时机
(1).执行User Preemption(用户态抢占)的时机
a. 从系统调用(syscall)返回用户态时;
b. 从中断返回用户态时
(2).执行Kernel Preemption(内核态抢占)的时机
a. CONFIG_PREEMPT_NONE=y
不允许内核抢占。这是SLES的默认选项;
b. CONFIG_PREEMPT_VOLUNTARY=y
在一些耗时较长的内核代码中主动调用cond_resched()让出CPU。这是RHEL的默认选项;
c. CONFIG_PREEMPT=y
允许完全内核抢占。
-中断处理程序返回内核空间之前会检查TIF_NEED_RESCHED标志,如果置位则调用preempt_schedule_irq()执行抢占
-当内核从non-preemptible(禁止抢占)状态变成preemptible(允许抢占)的时候,并且当前进程设置了TIF_NEED_RESCHED标志则执行抢占
注:对于某些情况下,不需要触发抢占,即不需要设置TIF_NEED_RESCHED标志,直接通过调用schedule执行抢占,如:mutex, semaphore, waitequeue等执行阻塞操作时会直接调用schedule执行抢占
|- - -抢占的条件
preempt_count == 0 && !irq_disable()
jiffies和HZ
- jiffies
记录了系统启动以来,系统定时器已经触发的次数。内核每秒钟将jiffies变量增加HZ次。
对于HZ值为100的系统,1个jiffy等于10ms,而对于HZ为1000的系统,1个jiffy仅为1ms - HZ
系统定时器能以可编程的频率中断处理器。此频率即为每秒的定时器节拍数,对应着内核变量HZ。
选择合适的HZ值需要权衡。HZ值大,定时器间隔时间就小,因此进程调度的准确性会更高;
HZ值越大也会导致开销和电源消耗更多,因为更多的处理器周期将被耗费在定时器中断上下文中
参考文档
- 奔跑吧,Linux内核
- ULK
- ULA