首页 >> 大全

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

2023-12-31 大全 27 作者:考证青年

在业务性能分析中,很多问题都是进程调度所引起。现特地总结进程调度子系统一些关键点,参考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。
按照<ns, pid>作为key,因此一个进程如果在level 2的ns中,那么他会有3<ns, pid>,挂入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:          0          0   Non-maskable interruptsLOC:      14066      15160   Local timer interruptsSPU:          0          0   Spurious interruptsPMI:          0          0   Performance monitoring interruptsIWI:         94        154   IRQ work interruptsRTR:          0          0   APIC ICR read retriesRES:      11943      11571   Rescheduling interruptsCAL:       1779       6332   Function call interruptsTLB:        130         94   TLB shootdownsTRM:          0          0   Thermal event interruptsTHR:          0          0   Threshold APIC interruptsDFR:          0          0   Deferred Error APIC interruptsMCE:          0          0   Machine check exceptionsMCP:          2          2   Machine check pollsERR:          0MIS:          0PIN:          0          0   Posted-interrupt notification eventNPI:          0          0   Nested posted-interrupt eventPIW:          0          0   Posted-interrupt wakeup event

简写中断向量中断处理函数备注

NMI

nmi

不可屏蔽中断

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 ’s in the form by the call。没核对源码,感觉没啥用,以后再看。 调度器

每个CPU都有一个 rq结构,保存在全局变量中,含有成员 cfs和 rt_rq rt。分别记录了普通进程和实时进程的调度单元。

每个进程根据分类不同,指向不同的 结构:

主调度器函数():

根据内核源码总结进程调度配置特性(可以在/sys//debug/配置):

周期性调度器(),这个函数由传统低分辨率时钟周期调用。

实时调度器 CFS调度器 进程负载均衡 调度域 调度算法 调度时机和抢占

根据内核源码注释总结进程调度时机为:

调度器统计

查看/proc/pid/文件(设置/proc/sys//为1才能看到详细统计)

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了