本系列文章将对进程管理与调度进行知识梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中进程调度机制进行数据实时拿取与分析。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
《深入理解Linux内核》 《Linux操作系统原理与应用》 《奔跑吧Linux内核》 《深入理解Linux进程与内存》
进程调度 - 标签 - LoyenWang - 博客园 (cnblogs.com)
专栏文章目录 - 知乎 (zhihu.com)
进程调度与管理(0.预备知识)
在正式探索内核中进程调度相关知识与源码之前,有必要先对相关预备知识进行学习与梳理,这将有助于理解复杂的内核源码。
本文将对进程调度中涉及的结构体以及相关预备知识进行介绍,主要涉及到如下结构体:
task_struct sched_entity rq sched_avg
0.1 task_struct
我们都知道,在操作系统中使用PCB(进程描述符)来抽象描述一个进程,而在Linux操作系统中对应的PCB便是task_struct结构体,它是操作系统用来感知进程存在的非常重要的结构体。在Linux-6.5版的源码中,该结构体有800多行,其中包含了进程的所有信息,包括但不限于进程线程的pid、进程调度相关信息、进程所使用的内存相关信息、进程在运行期间涉及到的文件相关信息等等,这里仅展示该结构体的关键成员,具体细节可参考源码:include/linux/sched.h/struct task_struct
进程基本信息:
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;//线程相关信息
#endif
unsigned int __state;//进程状态
/*省略若干字段*/
randomized_struct_fields_start//将下面的字段进行随机排列,以增强安全性
void *stack;//指向进程内核栈的指针
refcount_t usage;//进程描述符的引用计数
/* Per task flags (PF_*), defined further below: */
unsigned int flags;//进程的标志集合
unsigned int ptrace;
/*省略若干字段*/
randomized_struct_fields_end//将上面的字段进行随机排列,以增强安全性
}
我们可以看到task_struct结构体中包含了关于进程的一些基本信息,值得一提的是几乎全部字段都在randomized_struct_fields_start
和randomized_struct_fields_end
之间,其指示编译器开始对该结构体中某些字段进行随机化排列,这是为了增强系统安全性,尤其是防御内存溢出攻击等漏洞利用技术,如“结构化攻击”或内存地址推断。通过随机化结构体字段,攻击者很难预测到结构体内各个字段的相对位置。
进程调度相关信息
这里对task_struct结构体中与调度相关的字段进行了标记,主要包括在唤醒进程时所需的字段,以及在进行进程调度切换时所涉及的字段。
struct task_struct {
randomized_struct_fields_start//将下面的字段进行随机排列,以增强安全性
/*省略若干字段*/
#ifdef CONFIG_SMP
int on_cpu;//1表示正在cpu上运行,0表示未运行
struct __call_single_node wake_entry;
unsigned int wakee_flips;//用于wake_affine特性
unsigned long wakee_flip_decay_ts;//上一次wakee_flips的时间
struct task_struct *last_wakee;//上一次唤醒的是哪个进程
int recent_used_cpu;
int wake_cpu;//上一次允许在哪个CPU上
#endif
/*进程在运行队列的状态
* 0表示进程不在运行队列中;
* 1表示进程正在运行队列中排队;
* 2表示进程队列间迁移
*/
int on_rq;
int prio;//进程调度优先级
int static_prio;//进程的静态优先级
int normal_prio;//进程动态优先级
unsigned int rt_priority;//进程实时优先级
struct sched_entity se;//进程普通调度实体
struct sched_rt_entity rt;//进程的实时调度实体
struct sched_dl_entity dl;//进程的限期调度实体
const struct sched_class *sched_class;//进程的调度类
/*省略若干字段*/
unsigned int policy;//进程的调度策略
cpumask_t cpus_mask;//进程亲和关系掩码
/*省略若干字段*/
int exit_state;//进程的退出状态
int exit_code;//进程通过exit()退出时的退出码,这个代码通常用于告诉父进程该进程的退出原因
int exit_signal;//当进程退出时,发送给父进程的信号
/* The signal sent when the parent dies: */
int pdeath_signal;//父进程终止时发送给当前进程的信号
unsigned long nvcsw;//自愿上下文切换,进程主动下CPU时
unsigned long nivcsw;//非自愿上下文切换,进程被强制下CPU时
u64 start_time;//被系统调度的开始时间,单位纳秒
u64 start_boottime;
/*省略若干字段*/
randomized_struct_fields_end//将上面的字段进行随机排列,以增强安全性
}
进程运行时间相关字段
task_struct结构体中存在着一些字段用于计算进程运行在不同状态下的时间统计:
struct task_struct {
randomized_struct_fields_start//将下面的字段进行随机排列,以增强安全性
/*省略若干字段*/
/*时间相关字段*/
u64 utime;//进程花费在用户态的绝对时间
u64 stime;//进程花费在内核态的绝对时间
#ifdef CONFIG_ARCH_HAS_SCALED_CPUTIME
u64 utimescaled;//进程花费在用户态的相对时间
u64 stimescaled;//进程花费在内核态的相对时间
#endif
u64 gtime;//进程花费在客户态的时间(虚拟机进程)
struct prev_cputime prev_cputime;//记录进程上一次使用cpu的时间
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
struct vtime vtime;
#endif
/*省略若干字段*/
u64 start_time;//被系统调度的开始时间,单位纳秒
u64 start_boottime;
/*省略若干字段*/
randomized_struct_fields_end//将上面的字段进行随机排列,以增强安全性
}
通过这几个字段,可以清楚地了解到进程在用户态,内核态运行的时间,以及开始调度的时间。
反应进程间关系的字段
在task_struct结构体中存在一些反应进程间关系的字段,通过这些字段,可以找到该进程的父进程、子进程、兄弟进程以及其他进程:
struct task_struct {
randomized_struct_fields_start//将下面的字段进行随机排列,以增强安全性
/*省略若干字段*/
struct list_head tasks;//将所有进程链接在一起的链表结构
struct task_struct __rcu *real_parent;//进程的真实父进程
struct task_struct __rcu *parent;//进程的当前父进程
struct list_head children;//子进程链表
struct list_head sibling;//兄弟进程链表
struct task_struct *group_leader;//进程所在的线程组组长
struct list_head ptraced;//当前进程正在跟着的所有任务
struct list_head ptrace_entry;
/* PID/PID hash table linkage. */
struct pid *thread_pid;//指向当前线程pid的指针
struct hlist_node pid_links[PIDTYPE_MAX];//用于查找命名空间相关的PID结构数组
struct list_head thread_group;//链表头,当前线程组中的其他线程
struct list_head thread_node;
struct completion *vfork_done;
struct nsproxy *nsproxy;//命名空间
/*省略若干字段*/
randomized_struct_fields_end//将上面的字段进行随机排列,以增强安全性
}
进程运行过程中使用到的内存信息
struct task_struct {
randomized_struct_fields_start//将下面的字段进行随机排列,以增强安全性
/*省略若干字段*/
/*内存相关字段*/
struct mm_struct *mm;//进程的内存描述符
struct mm_struct *active_mm;//进程活动内存描述符(内核级线程使用)
/*省略若干字段*/
randomized_struct_fields_end//将上面的字段进行随机排列,以增强安全性
}
进程运行过程中使用到的内存信息
struct task_struct {
randomized_struct_fields_start//将下面的字段进行随机排列,以增强安全性
/*省略若干字段*/
struct fs_struct *fs;//文件系统信息
struct files_struct *files;//打开的文件描述符信息
/*省略若干字段*/
randomized_struct_fields_end//将上面的字段进行随机排列,以增强安全性
}
这里介绍到的只是进程task_struct
结构体的冰山一角,其中很多字段都是进程在运行期间某一特定方面的入口,比如该结构体下的mm字段是进程使用内存的入口,files是进程在运行期间所打开文件的入口,后面所涉及到的内容仍需大量的探索。
0.2 sched_entity
sched_entity
调度实体,是操作系统中调度器进行调度的最小单位,它可以是一个进程也可以是一个调度组,描述进程或调度组作为一个调度实体参与调度所需的所有信息。在前面的task_struct
结构体中有相应的字段来表示当前进程作为调度实体时的信息。
具体细节可参考include/linux/sched.h/struct sched_entity
struct sched_entity {
/* For load-balancing: */
struct load_weight load;//调度实体的权重,影响其在调度决策中的优先级
struct rb_node run_node;//红黑树节点,调度实体在调度器所维护的红黑树中的节点
struct list_head group_node;//链表节点,用于管理调度实体的组
unsigned int on_rq;//调度实体是否在运行队列中
u64 exec_start;//调度实体虚拟时间的起始时间
u64 sum_exec_runtime;//调度实体的累计总运行时间,是真实时间
u64 vruntime;//调度实体的虚拟运行时间
u64 prev_sum_exec_runtime;//上一次调度时累计运行的时间
u64 nr_migrations;//该调度实体发生迁移的次数
#ifdef CONFIG_FAIR_GROUP_SCHED
int depth;//调度实体在调度层次中的深度
struct sched_entity *parent;//指向父调度实体
/* rq on which this entity is (to be) queued: */
struct cfs_rq *cfs_rq;//指向调度实体将去排队的cfs运行队列
/* rq "owned" by this entity/group: */
struct cfs_rq *my_q;//当前调度实体所拥有的cfs运行队列
/* cached value of my_q->h_nr_running */
unsigned long runnable_weight;//当前可运行任务的权重
#endif
#ifdef CONFIG_SMP
struct sched_avg avg;//调度实体的负载平均值
#endif
};
struct sched_entity
是 Linux 内核中调度器的核心组成部分,支持复杂的调度算法和负载均衡策略。各字段不仅用于追踪进程的状态和执行时间,还帮助内核进行有效的资源分配和调度决策。
0.3 rq
rq数据结构是描述CPU的通用就绪队列,每个cpu都维护一组运行队列rq。rq数据结构中记录了一个就绪队列所需要的全部信息,包括一个CFS就绪队列数据结构cfs_rq、一个实时进程调度器就绪队列数据结构rt_rq和一个实时调度器就绪队列数据结构dl_rq,以及就绪队列的负载权重等信息。linux内核中提供了this_cpu()接口来获取当前CPU的rq数据结构。
具体细节可参考源码:Linux6.5/kernel/sched/sched.h/struct rq
DECLARE_PER_CPU_SHARED_ALIGNED(struct rq, runqueues);
#define cpu_rq(cpu) (&per_cpu(runqueues, (cpu)))
#define this_rq() this_cpu_ptr(&runqueues)
#define task_rq(p) cpu_rq(task_cpu(p))
#define cpu_curr(cpu) (cpu_rq(cpu)->curr)
#define raw_rq() raw_cpu_ptr(&runqueues)
对rq结构体进行归纳总结,可将其分为以下几个方面:
rq结构体中运行队列基本信息
struct rq {
/* 运行队列基本信息: */
raw_spinlock_t __lock;//保护就绪队列的自旋锁,
unsigned int nr_running;//当前运行队列中运行的任务数量
#ifdef CONFIG_NUMA_BALANCING
unsigned int nr_numa_running;// NUMA 体系结构中运行的任务数量
unsigned int nr_preferred_running;//首选 NUMA 节点中运行的任务数量
unsigned int numa_migrate_on;//是否需要 NUMA 迁移
#endif
u64 nr_switches;//进程上下文切换次数
/*省略若干字段*/
}
rq结构体中与调度相关的字段
struct rq {
/*调度相关信息*/
struct cfs_rq cfs;//完全公平调度 (CFS) 运行队列
struct rt_rq rt;//实时调度器的运行队列
struct dl_rq dl;//限时调度器的运行队列
/*省略若干字段*/
unsigned int nr_uninterruptible;//统计不可中断(uninterruptible)状态的进程进入就绪队列的数量
struct task_struct __rcu *curr;//指向正在运行的进程
struct task_struct *idle;//指向当前cpu的idle进程
struct task_struct *stop;//指向系统的stop进程
unsigned long next_balance;//下次做负载均衡的时间
struct mm_struct *prev_mm;//进程切换时用于指向前任进程的内存描述符 mm
unsigned int clock_update_flags;//用于更新就绪队列时钟的标志位
u64 clock;//每次时钟节拍到来是会更新这个时钟
/*省略若干字段*/
}
rq结构体中与负载均衡相关的字段
struct rq {
/*省略若干字段*/
/*负载均衡相关*/
#ifdef CONFIG_SMP
struct root_domain *rd;//调度域的根
struct sched_domain __rcu *sd;//指向CPU最低等级的调度域
unsigned long cpu_capacity;//CPU的量化算力
unsigned long cpu_capacity_orig;//CPU最高算力(额定算力)
struct balance_callback *balance_callback;//负载均衡的回调函数
unsigned char nohz_idle_balance;//无时钟空闲负载平衡标志
unsigned char idle_balance;//空闲负载平衡标志
unsigned long misfit_task_load;//不合适进程(misfit_task)的量化负载
/* For active balancing */
int active_balance;//主动负载均衡
int push_cpu;//迁移的目标CPU
struct cpu_stop_work active_balance_work;
/* CPU of this runqueue: */
int cpu;//当前运行队列在哪个CPU上
int online;//当前CPU的状态,active/online
struct list_head cfs_tasks;//可运行状态的调度实体
struct sched_avg avg_rt;//实时就绪队列的负载情况
struct sched_avg avg_dl;//限时调度队列的负载情况
/*省略若干字段*/
}
0.4 sched_avg
sched_avg 数据结构主要用于描述调度实体、运行队列的负载信息,它用于负载均衡时衡量不同调度实体以及运行队列的负载情况。
具体细节可参考源码: include/linux/sched.h/struct sched_avg
struct sched_avg {
u64 last_update_time;//上一次更新时间,表示负载信息最后更新的时刻
/*load_sum 对于:
*1.调度实体:累计衰减时间
*2.运行队列:调度队列中所有进程的累计工作总负载
*/
u64 load_sum;
/*runnable_sum 对于:
*1.调度实体:累计衰减时间
*2.运行队列:调度队列中可运行状态进程的工作总负载
*/
u64 runnable_sum;
/*util_sum:用于衡量CPU的使用率
*1.正在运行状态下的累计衰减时间,
*2.统计的是时间,还没有乘调度实体或调度队列的权重;
*/
u32 util_sum;
/*period_contrib:
*1.上一次时间采样时,不能凑成一个周期的剩余时间;
*/
u32 period_contrib;
/*load_avg:;量化负载
*1.调度实体:可运行状态下的量化负载,可以衡量迁移进程的负载量
*2.调度队列:调度队列中的总量化负载;
*/
unsigned long load_avg;
/*runnable_avg
*1.调度队列:同load_avg;
*2.就绪队列中所有可运行状态下进程的总量化负载,可反映CPU的负载大小
*/
unsigned long runnable_avg;
/*util_avg:实际算力
*1.调度实体:可反映该调度实体的实际算力
*2.调度队列:可反映CPU的实际算力需求
*/
unsigned long util_avg;
struct util_est util_est;
} ____cacheline_aligned;