前面我们重点分析了如何通过 fork, vfork, pthread_create 去创建一个进程或者线程,以及后面说了它们共同调用 do_fork 的实现。现在已经知道一个进程是如何创建的,但是进程何时被执行,需要调度器来选择。所以这一节我们介绍下进程调度和进程切换的详情。
在 CPU 的角度看进程行为的话,可以分为两类:
CPU 消耗型进程需要高的吞吐率,IO 消耗型进程需要强的响应性,这两点都是调度器需要考虑的。
为了更快响应 IO 消耗型进程,内核提供了一个抢占(preempt)机制,使优先级更高的进程,去抢占优先级低的进程运行。内核用以下宏来选择内核是否打开抢占机制:
先来看几个相关的数据结构:
我们先把 task_struct 中和调度相关的结构拎出来:
struct task_struct {
......
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
......
struct sched_dl_entity dl;
......
unsigned int policy;
......
}
分配给 CPU 的 task,作为调度实体加入到运行队列中。
runqueue 运行队列是本 CPU 上所有可运行进程的队列集合。每个 CPU 都有一个运行队列,每个运行队列中有三个调度队列,task 作为调度实体加入到各自的调度队列中。
struct rq {
......
struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;
......
}
三个调度队列:
struct cfs_rq {
...
struct rb_root_cached tasks_timeline
...
};
struct sched_entity {
...
struct rb_node run_node;
...
u64 vruntime;
...
};
这些数据结构的关系如下图所示:
调度的本质就是选择下一个进程,然后切换。在执行调度之前需要设置调度标记 TIF_NEED_RESCHED,然后在调度的时候会判断当前进程有没有被设置 TIF_NEED_RESCHED,如果设置则调用函数 schedule 来进行调度。
为 CPU 上正在运行的进程 thread_info 结构体里的 flags 成员设置 TIF_NEED_RESCHED。
那么,什么时候设置TIF_NEED_RESCHED呢 ?
2 . wake_up_process 唤醒进程的时候
3 . do_fork 创建新进程的时候
4 . set_user_nice 修改进程nice值的时候
5 . smp_send_reschedule 负载均衡的时候
Kernel 判断当前进程标记是否为 TIF_NEED_RESCHED,是的话调用 schedule 函数,执行调度,切换上下文,这也是上面抢占(preempt)机制的本质。那么在哪些情况下会执行 schedule 呢?
ret_to_user 是异常触发,系统调用,中断处理完成后都会调用的函数。
2 . 内核态抢占
可以看出无论是用户态抢占,还是内核态抢占,最终都会调用 schedule 函数来执行真正的调度:
还记得调度的本质吗?调度的本质就是选择下一个进程,然后切换。如上图所示,用函数 pick_next_task 选择下一个进程,其本质就是调度算法的实现;用函数 context_switch 完成进程的切换,即进程上下文的切换。下面我们分别看下这两个核心功能。
字段 | 版本 |
---|---|
O(n) 调度器 | linux0.11 - 2.4 |
O(1) 调度器 | linux2.6 |
CFS调度器 | linux2.6至今 |
O(n) 调度器是在内核2.4以及更早期版本采用的算法,O(n) 代表的是寻找一个合适的任务的时间复杂度。调度器定义了一个 runqueue 的运行队列,将进程的状态变为 Running 的都会添加到此运行队列中,但是不管是实时进程,还是普通进程都会添加到这个运行队列中。当需要从运行队列中选择一个合适的任务时,就需要从队列的头遍历到尾部,这个时间复杂度是O(n),运行队列中的任务数目越大,调度器的效率就越低。
所以 O(n) 调度器有如下缺陷:
内核2.6采用了O(1) 调度器,让每个 CPU 维护一个自己的 runqueue,从而减少了锁的竞争。每一个runqueue 运行队列维护两个链表,一个是 active 链表,表示运行的进程都挂载 active 链表中;一个是 expired 链表,表示所有时间片用完的进程都挂载 expired 链表中。当 acitve 中无进程可运行时,说明系统中所有进程的时间片都已经耗光,这时候则只需要调整 active 和 expired 的指针即可。每个优先级数组包含140个优先级队列,也就是每个优先级对应一个队列,其中前100个对应实时进程,后40个对应普通进程。如下图所示:
总的来说 O(1) 调度器的出现是为了解决 O(n) 调度器不能解决的问题,但 O(1) 调度器有个问题,一个高优先级多线程的应用会比低优先级单线程的应用获得更多的资源,这就会导致一个调度周期内,低优先级的应用可能一直无法响应,直到高优先级应用结束。CFS调度器就是站在一视同仁的角度解决了这个问题,保证在一个调度周期内每个任务都有执行的机会,执行时间的长短,取决于任务的权重。下面详细看下CFS调度器是如何动态调整任务的运行时间,达到公平调度的。
CFS是 Completely Fair Scheduler 简称,即完全公平调度器。CFS 调度器和以往的调度器不同之处在于没有固定时间片的概念,而是公平分配 CPU 使用的时间。比如:2个优先级相同的任务在一个 CPU 上运行,那么每个任务都将会分配一半的 CPU 运行时间,这就是要实现的公平。
但现实中,必然是有的任务优先级高,有的任务优先级低。CFS 调度器引入权重 weight 的概念,用 weight 代表任务的优先级,各个任务按照 weight 的比例分配 CPU 的时间。比如:2个任务A和B,A的权重是1024,B的权重是2048,则A占 1024/(1024+2048) = 33.3% 的 CPU 时间,B占 2048/(1024+2048)=66.7% 的 CPU 时间。
在引入权重之后,分配给进程的时间计算公式如下:
实际运行时间 = 调度周期 * 进程权重 / 所有进程权重之和
CFS 调度器用nice值表示优先级,取值范围是[-20, 19],nice和权重是一一对应的关系。数值越小代表优先级越大,同时也意味着权重值越大,nice值和权重之间的转换关系:
const int sched_prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
数组值计算公式是:weight = 1024 / 1.25nice。
如果一个 CPU 上有 N 个优先级相同的进程,那么每个进程会得到 1/N 的执行机会,每个进程执行一段时间后,就被调出,换下一个进程执行。如果这个 N 的数量太大,导致每个进程执行的时间很短,就要调度出去,那么系统的资源就消耗在进程上下文切换上去了。
所以对于此问题在 CFS 中则引入了调度周期,使进程至少保证执行0.75ms。调度周期的计算通过如下代码:
static u64 __sched_period(unsigned long nr_running)
{
if (unlikely(nr_running > sched_nr_latency))
return nr_running * sysctl_sched_min_granularity;
else
return sysctl_sched_latency;
}
static unsigned int sched_nr_latency = 8;
unsigned int sysctl_sched_latency = 6000000ULL;
unsigned int sysctl_sched_min_granularity = 750000ULL;
当进程数目小于8时,则调度周期等于6ms。当进程数目大于8时,则调度周期等于进程的数目乘以0.75ms。
根据上面进程实际运行时间的公式,可以看出,权重不同的2个进程的实际执行时间是不相等的,但是 CFS 想保证每个进程运行时间相等,因此 CFS 引入了虚拟时间的概念。虚拟时间(vriture_runtime)和实际时间(wall_time)转换公式如下:
vriture_runtime = (wall_time * NICE0_TO_weight) / weight
其中,NICE0_TO_weight 代表的是 nice 值等于0对应的权重,即1024,weight 是该任务对应的权重。
权重越大的进程获得的虚拟运行时间越小,那么它将被调度器所调度的机会就越大,所以,CFS 每次调度原则是:总是选择 vriture_runtime 最小的任务来调度。
为了能够快速找到虚拟运行时间最小的进程,Linux 内核使用红黑树来保存可运行的进程。CFS跟踪调度实体sched_entity的虚拟运行时间vruntime,将sched_entity通过enqueue_entity()和dequeue_entity()来进行红黑树的出队入队,vruntime少的调度实体sched_entity排列到红黑树的左边。
如上图所示,红黑树的左节点比父节点小,而右节点比父节点大。所以查找最小节点时,只需要获取红黑树的最左节点即可。
相关步骤如下:
4 . 在下一次任务调度的时候,选择虚拟运行时间少的调度实体来运行。pick_next_task 函数就是从从就绪队列中选择最适合运行的调度实体,即虚拟时间最小的调度实体,下面我们看下 CFS 调度器如何通过 pick_next_task 的回调函数 pick_next_task_fair 来选择下一个进程的。
pick_next_task_fair 会判断上一个 task 的调度器是否是 CFS,这里我们默认都是 CFS 调度:
update_curr 函数用来更新当前进程的运行时间信息:
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
if (unlikely(!curr))
return;
delta_exec = now - curr->exec_start; ------(1)
if (unlikely((s64)delta_exec <= 0))
return;
curr->exec_start = now; ------(2)
schedstat_set(curr->statistics.exec_max,
max(delta_exec, curr->statistics.exec_max));
curr->sum_exec_runtime += delta_exec; ------(3)
schedstat_add(cfs_rq->exec_clock, delta_exec);
curr->vruntime += calc_delta_fair(delta_exec, curr); ------(4)
update_min_vruntime(cfs_rq); ------(5)
account_cfs_rq_runtime(cfs_rq, delta_exec);
}
pick_next_entity 函数会从就绪队列中选择最适合运行的调度实体(虚拟时间最小的调度实体),即从 CFS 红黑树最左边节点获取一个调度实体。
static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
struct sched_entity *left = __pick_first_entity(cfs_rq); ------(1)
struct sched_entity *se;
/*
* If curr is set we have to see if its left of the leftmost entity
* still in the tree, provided there was anything in the tree at all.
*/
if (!left || (curr && entity_before(curr, left)))
left = curr;
se = left; /* ideally we run the leftmost entity */
/*
* Avoid running the skip buddy, if running something else can
* be done without getting too unfair.
*/
if (cfs_rq->skip == se) {
struct sched_entity *second;
if (se == curr) {
second = __pick_first_entity(cfs_rq); ------(2)
} else {
second = __pick_next_entity(se); ------(3)
if (!second || (curr && entity_before(curr, second)))
second = curr;
}
if (second && wakeup_preempt_entity(second, left) < 1)
se = second;
}
/*
* Prefer last buddy, try to return the CPU to a preempted task.
*/
if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
se = cfs_rq->last;
/*
* Someone really wants this to run. If it's not unfair, run it.
*/
if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
se = cfs_rq->next;
clear_buddies(cfs_rq, se);
return se;
}
put_prev_entity 会调用 __enqueue_entity 将prev进程(即current进程)加入到 CFS 队列 rq 上的红黑树,然后将 cfs_rq->curr 设置为空。
static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node; //红黑树根节点
struct rb_node *parent = NULL;
struct sched_entity *entry;
bool leftmost = true;
/*
* Find the right place in the rbtree:
*/
while (*link) { ------(1)
parent = *link;
entry = rb_entry(parent, struct sched_entity, run_node);
/*
* We dont care about collisions. Nodes with
* the same key stay together.
*/
if (entity_before(se, entry)) { ------(2)
link = &parent->rb_left;
} else {
link = &parent->rb_right;
leftmost = false;
}
}
rb_link_node(&se->run_node, parent, link); ------(3)
rb_insert_color_cached(&se->run_node, ------(4)
&cfs_rq->tasks_timeline, leftmost);
}
set_next_entity 会调用 __dequeue_entity 将下一个选择的进程从 CFS 队列的红黑树中删除,然后将 CFS 队列的 curr 指向进程的调度实体。
理解了下一个进程的选择后,就需要做当前进程和所选进程的上下文切换。
Linux 内核用函数 context_switch 进行进程的上下文切换,进程上下文切换主要涉及到两部分:进程地址空间切换和处理器状态切换:
将下一个进程的 pgd 虚拟地址转化为物理地址存放在 ttbr0_el1 中(这是用户空间的页表基址寄存器),当访问用户空间地址的时候 mmu 会通过这个寄存器来做遍历页表获得物理地址。完成了这一步,也就完成了进程的地址空间切换,确切的说是进程的虚拟地址空间切换。
其中 x19-x28 是 arm64 架构规定需要调用保存的寄存器,可以看到处理器状态切换的时候将前一个进程(prev)的 x19-x28,fp,sp,pc 保存到了进程描述符的 cpu_contex 中,然后将即将执行的进程 (next) 描述符的 cpu_contex 的 x19-x28,fp,sp,pc 恢复到相应寄存器中,而且将 next 进程的进程描述符 task_struct 地址存放在 sp_el0 中,用于通过 current 找到当前进程,这样就完成了处理器的状态切换。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/OR8Zbm9acYN6xG9RvUmGtA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。