Linux内核进程调度子系统总结

在业务性能分析中,很多问题都是进程调度所引起 。现特地总结进程调度子系统一些关键点 , 参考3.10内核源码 。
提纲调度器 进程负载均衡 调度器统计
在Linux内核中,是对进程和线程的统一抽象,一个结构代表了一个进程或者线程 。它也是之后调度器的基本单位,也就是说,对于调度器来说,进程和线程是同等的 。
家族关系
static struct task_struct *copy_process(unsigned long clone_flags,unsigned long stack_start,unsigned long stack_size,int __user *child_tidptr,struct pid *pid,int trace){.../* CLONE_PARENT re-uses the old parent */if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {p->real_parent = current->real_parent;p->parent_exec_id = current->parent_exec_id;} else {p->real_parent = current;p->parent_exec_id = current->self_exec_id;}...}
销毁进程时,把他的子进程的赋值一次 。
static void forget_original_parent(struct task_struct *father){ ...list_for_each_entry_safe(p, n, &father->children, sibling) {struct task_struct *t = p;do {t->real_parent = reaper;if (t->parent == father) {BUG_ON(t->ptrace);t->parent = t->real_parent;}if (t->pdeath_signal)group_send_sig_info(t->pdeath_signal,SEND_SIG_NOINFO, t);} while_each_thread(p, t);reparent_leader(father, p, &dead_children);}...}
用于接收信号和wait4() 。
当调试器trace进程时,将进程的修改为调试器进程 。停止调试时,修改为 。
/** ptrace a task: make the debugger its new parent and* move it to the ptrace list.** Must be called with the tasklist lock write-held.*/void __ptrace_link(struct task_struct *child, struct task_struct *new_parent){BUG_ON(!list_empty(&child->ptrace_entry));list_add(&child->ptrace_entry, &new_parent->ptraced);child->parent = new_parent;}void __ptrace_unlink(struct task_struct *child){BUG_ON(!child->ptrace);child->ptrace = 0;child->parent = child->real_parent;...}
当进程销毁时,被交给另外一个进程,此时如果子进程没有被调试中 , 那么还是赋值为,上文的nt()代码展示了这个逻辑 。
总之,在我的理解中 , 除非进程被调试器接管了,否则其他时候和总是相等的 。
全局id,即在init进程所在中的id号 。
struct task_struct {...pid_t pid;//进程号pid_t tgid;//线程组号...} SYSCALL_DEFINE0(getpid){return task_tgid_vnr(current);}SYSCALL_DEFINE0(gettid){return task_pid_vnr(current);}
pid形式 , 可以分别表示在所有ns层次中的id号:
static inline struct pid *task_pid(struct task_struct *task){return task->pids[PIDTYPE_PID].pid;}static inline struct pid *task_tgid(struct task_struct *task){return task->group_leader->pids[PIDTYPE_PID].pid;}static inline struct pid *task_pgrp(struct task_struct *task){return task->group_leader->pids[PIDTYPE_PGID].pid;}static inline struct pid *task_session(struct task_struct *task){return task->group_leader->pids[PIDTYPE_SID].pid;}
pid
pid 组成了树形结构 。因此,如果一个进程在level 2的pid 中创建,那么它在level 0 和 level 1中必定也有一个pid 。因此,二元组才能唯一的确定一个进程 。
通用struct pid数据结构,它代表pid,pgid,spid三种id号,以及在不同ns中的id号 。由于进程组,session组中的进程使用相同的id(组长的pid) , 那么可以直接使用leader进程的task_struct->pidsp[type]对应pid数据结构,同时把自己的task_struct挂入对应的pid数据结构的tasks[PIDTYPE_MAX]链表中 。比如,进程组使用进程组长的pid作为组id,那么就复用进程组长的pid结构 , 同理作用于session组 。struct pid{atomic_t count;unsigned int level;/* lists of tasks that use this pid */struct hlist_head tasks[PIDTYPE_MAX];struct rcu_head rcu;struct upid numbers[1];};task_struct中,有个pids[PIDTYPE_MAX]数组,将该task_struct挂入它使用的pid数据结构中 。struct pid_link{struct hlist_node node;struct pid *pid;};struct pid中可能有多个upid,对应在不同ns中的具体数值 。upid会加入一个全局hash表pid_hash 。按照作为key , 因此一个进程如果在level 2的ns中,那么他会有3个,挂入pid_hash三次 。可以参考alloc_pid()函数实现 。struct upid {/* Try to keep pid_chain in the same cacheline as nr for find_vpid */int nr;struct pid_namespace *ns;struct hlist_node pid_chain;};
常用统计值 CPU级(/proc/stat)
int (*p, void *v)函数详细解释了统计方法 。既统计了所有CPU总计值,也按单个CPU输出统计值 。
cpu消耗的时间(单位为jiffies)划分为如下几类:enum cpu_usage_stat {CPUTIME_USER,CPUTIME_NICE,CPUTIME_SYSTEM,CPUTIME_SOFTIRQ,CPUTIME_IRQ,CPUTIME_IDLE,CPUTIME_IOWAIT,CPUTIME_STEAL,CPUTIME_GUEST,CPUTIME_GUEST_NICE,NR_STATS,};
通过阅读源码可知CPU时间的分布为:
USER
NICE
IRQ
IDLE
STEAL
注意:
KVM子机vcpu运行时间会保存两次,一次保存在USER/NICE中,一次保存在GUEST/中 。所以GUEST/不算在总CPU时间分布中 。STEAL时间指在虚拟化环境下,窃取的vm中的时间,严格讲就是VCPU没有运行的时间(不包括VCPU主动idle的时间) 。细节可以参考博客 。这就意味着我们可以在自己里观察top出来的st统计值,推断出超卖情况 。执行的软中断,也会计算到软中断耗时上去 。
硬中断总次数(各个核心上通用中断数+各个核心上体系相关中断数+中断失败数),以及中断描述表中所有单种类中断总计数 。
软中断总次数,以及单种类软中断总计数 。
参考源码
单个硬中断和软中断统计可以看/proc/和/proc/ 。
这里顺便把/proc/不好理解的中断详细分析下
NMI:00Non-maskable interruptsLOC:1406615160Local timer interruptsSPU:00Spurious interruptsPMI:00Performance monitoring interruptsIWI:94154IRQ work interruptsRTR:00APIC ICR read retriesRES:1194311571Rescheduling interruptsCAL:17796332Function call interruptsTLB:13094TLB shootdownsTRM:00Thermal event interruptsTHR:00Threshold APIC interruptsDFR:00Deferred Error APIC interruptsMCE:00Machine check exceptionsMCP:22Machine check pollsERR:0MIS:0PIN:00Posted-interrupt notification eventNPI:00Nested posted-interrupt eventPIW:00Posted-interrupt wakeup event
简写中断向量中断处理函数备注
NMI
nmi
【Linux内核进程调度子系统总结】不可屏蔽中断
LOC
rupt
SPU
pt
PMI
nmi
根据不可屏蔽中断原因 , 最后调用er()
IWI
pt
RTR
(ICR)
RES
rupt
强制指定cpu进行一次进程调度
CAL
CAL和TLB走同一个中断向量,这里统计会剔除TLB数量
TLB
CAL和TLB走同一个中断向量,这里只统计TLB数量
TRM
t
THR
R
upt
DFR
R
MCE
MCP
ERR
MIS
PIN
ipi
VT-d
NPI
ECTOR
PIW
ECTOR
pid: 进程pid 。tcomm: 进程名 。state: 进程状态内核对应宏定义备注
R()
进程当前正在运行,或者正在运行队列中等待调度 。
S()
进程处于睡眠状态,正在等待某些事件发生 。进程可以被信号中断 。接收到信号或被显式的唤醒呼叫唤醒之后,进程将转变为状态 。
D(disk sleep)
此进程状态类似于,只是它不会处理信号 。中断处于这种状态的进程是不合适的,因为它可能正在完成某些重要的任务 。当它所等待的事件发生时 , 进程将被显式的唤醒呼叫唤醒 。
T()
进程已中止执行,它没有运行,并且不能运行 。接收到和等信号时,进程将进入这种状态 。接收到信号之后,进程将再次变得可运行 。
t( stop)
正被调试程序等其他进程监控时,进程将进入这种状态 。
Z()
进程已终止,它正等待其父进程收集关于它的一些统计信息 。
X(dead)
最终状态(正如其名) 。将进程从系统中删除时 , 它将进入此状态 , 因为其父进程已经通过 wait4() 或 () 调用收集了所有统计信息 。
x(dead)
同上
K()
+=。除了可以响应终止进程信号,其它跟一样
W()
主要用在()函数中,表示进程被唤醒但是还没有加入运行队列这个中间状态 。
P()
per-cpu进程 , 当cpu热拔出时,该进程进入park阻塞状态 , 即使被强制唤醒也会继续park阻塞 。除非热插入cpu 。可以参考博客
ppid:父pid , 用的是值 。pgid:进程组pid 。sid:进程会话组ID 。:当前进程的tty终端设备号 。:终端的进程组号,也就是当前运行在该任务所在终端的前台任务(包括shell 应用程序)的PID 。flags:task->flags,参考源码注释 。:该线程组的所有线程(包括正在运行的线程,以及死去的线程总和)次要缺页中断的次数总和,即无需从磁盘加载内存页 。注意,无论从该线程组中哪一个线程的/proc/pid/进入 , 看到的该统计值都是线程组的总和,如果要看单个线程的,需要从/proc/pid/task/tpid/stat查看 。接下来的,utime,stime,gtime都要遵从该规则 。:曾经的所有子进程(后被回收的子进程才加入该统计)的次要缺页中断的次数总和 , 这个统计值保存在task->,可见是通过信号传递 。:该线程组的所有线程(包括正在运行的线程,以及死去的线程总和)主要缺页中断的次数总和,即需要从磁盘加载内存页 。:曾经的所有子进程(后被回收的子进程才加入该统计)的主要缺页中断的次数总和 , 这个统计值保存在task->,可见是通过信号传递 。
utime:该线程组的所有线程(包括正在运行的线程,以及死去的线程总和)在用户态(时钟节拍数) 。stime:该线程组的所有线程(包括正在运行的线程 , 以及死去的线程总和)在内核态 。:曾经的所有子进程(后被回收的子进程才加入该统计)的在用户态总和,这个统计值保存在task->,可见是通过信号传递 。stime:曾经的所有子进程(后被回收的子进程才加入该统计)的在内核态总和,这个统计值保存在task-> , 可见是通过信号传递 。:进程的动态优先级,这里p->prio - 做了处理,仅仅用在proc展示用,内核依旧是使用p->prio来做调度 。nice:普通进程nice值 。:进程内线程数 。进程内的task/xxx/stat文件里,该值都相同 。:已废弃,永远为0 , 无意义 。:自系统启动后的进程创建那个时间偏移点,单位为 。vsize:进程分配的虚拟内存大小 。rss:等我研究完内存子系统后再补上 。
内核对rss内存统计分为三种:, ,  。:该进程允许的rss的上限,单位字节数 。mm->:进程代码段起始地址 。mm->:进程代码段结束地址 。mm->:进程栈的起始地址 。esp:ESP寄存器(栈顶指针)当前内容 。eip:EIP寄存器(指令计数器)当前内容 。: of。代码注释表明已经废弃,以后再分析 。: of。代码注释表明已经废弃,以后再分析 。: of。代码注释表明已经废弃,以后再分析 。: of。代码注释表明已经废弃,以后再分析 。wchan:如果进程处于睡眠状态,那么这里保存了该进程睡眠在哪个内核函数的地址 。0:无意义 。0:无意义 。task->:当进程退出时,发给父进程的信号集合 。:进程当前运行所在cpu编号 。
task->:实时优先级,如果进程不是实时进程,这个值就没意义 。task->:进程调度策略 。, , ,,之一 。:进程同步磁盘IO操作延时+进程swap in操作延时 。参考源码,单位为 。gtime:该线程组的所有线程(包括正在运行的线程 , 以及死去的线程总和)在guest虚拟机运行的(时钟节拍数) 。:曾经的所有子进程(后被回收的子进程才加入该统计)的在guest虚拟机运行的,这个统计值保存在task->,可见是通过信号传递 。mm->:进程data+bss段起始地址 。mm->:进程data+bss段结束地址 。mm->:进程堆起始地址 。mm->:进程参数起始地址 。mm->:进程参数结束地址 。mm->:进程环境变量起始地址 。mm->:进程环境变量结束地址 。:the ’sin the formby thecall 。没核对源码,感觉没啥用,以后再看 。调度器
每个CPU都有一个 rq结构 , 保存在全局变量中 , 含有成员cfs和 rt_rq rt 。分别记录了普通进程和实时进程的调度单元 。
每个进程根据分类不同,指向不同的 结构:
主调度器函数():
根据内核源码总结进程调度配置特性(可以在/sys//debug/配置):
周期性调度器() , 这个函数由传统低分辨率时钟周期调用 。
实时调度器 CFS调度器 进程负载均衡 调度域 调度算法 调度时机和抢占
根据内核源码注释总结进程调度时机为:
调度器统计
查看/proc/pid/文件(设置/proc/sys//为1才能看到详细统计)