news 2026/6/22 23:05:47

Linux sched_yield主动让出CPU与sys_sched_yield实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux sched_yield主动让出CPU与sys_sched_yield实现

Linux sched_yield主动让出CPU与sys_sched_yield实现

sys_sched_yield()是唯一由用户态直接触发的自愿调度点,对应glibc的sched_yield()。其内核入口直接调用yield_to_task_fair()的封装,不经过任何时间片检查或权限验证——任何进程都可以任意调用yield释放cpu,这与SCHED_FIFO的rt_throttled机制不同,yield不受带宽限制。

```c
SYSCALL_DEFINE0(sched_yield)
{
struct rq_flags rf;
struct rq *rq;

rq = this_rq_lock_irq(&rf);

schedstat_inc(rq->yld_count);
current->sched_class->yield_task(rq);

preempt_disable();
rq_unlock_irq(rq, &rf);
sched_preempt_enable_no_resched();

schedule(); /* 主动触发reschedule */

return 0;
}
```

yield_task()在CFS调度类中实现为yield_task_fair()。该函数的核心操作是将当前se从CFS红黑树中删除后重新插入,但插入位置被调整到树的最后端——即vruntime被向后推。具体做法是通过update_curr()更新当前vruntime,然后调用clear_buddies()清除cfs_rq上的next、last、skip指针,最后调用__enqueue_entity()将se重新入队到红黑树的最右端(max vruntime位置)。

```c
static void yield_task_fair(struct rq *rq)
{
struct task_struct *curr = rq->curr;
struct cfs_rq *cfs_rq = task_cfs_rq(curr);
struct sched_entity *se = &curr->se;

if (unlikely(rq->nr_running == 1))
return; /* 只有自己一个任务,yield没有意义 */

clear_buddies(cfs_rq, se);

if (curr->policy != SCHED_BATCH) {
update_rq_clock(rq);
update_curr(cfs_rq);
rq_clock_skip_update(rq);
}

se->vruntime = cfs_rq->min_vruntime + sched_vslice(cfs_rq, se)
+ sysctl_sched_yield_slack;

list_del(&se->group_node);
/* 重新入队到红黑树最右侧 */
__enqueue_entity(cfs_rq, se);
}
```

yield时设置se->vruntime = cfs_rq->min_vruntime + sched_vslice(cfs_rq, se) + sysctl_sched_yield_slack。其中sysctl_sched_yield_slack默认值为1ms(以ns为单位),作用是防止yield后的进程因vruntime恰好等于min_vruntime而立即被再次选中。sched_vslice是当前se的时间片在CFS下的虚拟表示——这强制使当前se变为cfs_rq中vruntime最大的实体,因此在pick_next_task_fair()中,红黑树最左节点不会是该se。

一个关键竞态:yield期间调用update_curr()可能导致cfs_rq->min_vruntime被推进。如果另一个CPU上的wakeup在yield_task_fair的执行窗口中对同一个cfs_rq执行了enqueue_entity,新的min_vruntime会突然增大,使得重新入队的se的vruntime相对变小——在极端情况下yield可能完全失效。但在SMP下每个CPU有独立cfs_rq(除非CONFIG_FAIR_GROUP_SCHED开启了task group),单CPU场景没有这个窗口。

```c
static void yield_task_rt(struct rq *rq)
{
struct task_struct *curr = rq->curr;
struct rt_rq *rt_rq = &rq->rt;

if (rt_rq->rt_nr_running == 1)
return;

if (curr->policy != SCHED_RR) {
/* SCHED_FIFO调用yield意味着完全不公平——把当前任务移到队尾 */
if (!plist_head_empty(&rt_rq->pushable_tasks))
dequeue_pushable_task(rq, curr);
}

requeue_task_rt(rq, curr, 1);
set_tsk_need_resched(curr);
}
```

yield与cond_resched()有本质区别。cond_resched()只在need_resched()为true时才调用schedule(),而sys_sched_yield直接强制调用schedule()。spinlock锁持有者调用yield后如果被同一个锁的waiter抢占,会导致lock holder长时间无法释放锁——这是ABBA deadlock的一个变种。内核自旋锁持有路径通过PREEMPT_COUNT和preempt_disable()来确保在spin_unlock前不会被抢占,但yield_task_fair中的__enqueue_entity不检查preempt_count,它只对rq->lock持有情况负责。

yield的另一个陷阱是truncated yield:当一个进程连续调用sched_yield时,每次调用都会把vruntime推到当前min_vruntime + slice + slack的位置。但每次调用之间的时间间隔中如果其他进程消耗了vruntime并使min_vruntime前进,yield进程的vruntime相对值可能增长缓慢——从而导致它实际上并没有被推到队列最尾。这在实时系统中需要规避,所以POSIX标准规定SCHED_FIFO/yield的行为是移到优先级队列尾而不是修改vruntime。

yield_task_fair中对于SCHED_BATCH策略的特殊处理是跳过update_curr()。SCHED_BATCH任务期望长时间运行,update_curr()会触发load weight计算和PELT更新,这些计算在batch场景下不应该为一次yield触发。rq_clock_skip_update()标记RQC_ACT_SKIP标志来阻止随后的update_rq_clock再刷新clock。

```c
static inline void rq_clock_skip_update(struct rq *rq)
{
rq->clock_update_flags |= RQCF_ACT_SKIP;
}
```

yield_task_fair中先list_del(&se->group_node)将se从cfs_rq的leaf list中移除,然后通过__enqueue_entity重新插入。leaf list维护了cfs_rq所有se的顺序遍历链表,用于update_curr的向上传播。跳过该步骤会导致leaf list出现重复节点,进而导致update_cfs_rq_h_load()中的h_load计算进入死循环。

在调度统计层面,rq->yld_count在每次sys_sched_yield入口递增,而se.statistics中不记录single yield事件——只有通过yield触发的schedule()产生的context_switch才会计入nr_switches。这使得识别频繁yield的进程需要perf probe挂载在yield_task_fair入口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/22 23:04:27

嵌入式调试器环境变量与搜索路径配置详解

1. 项目概述:嵌入式调试器的“寻路”逻辑干了十几年嵌入式开发,从8位机到32位ARM,调试器是我每天都要打交道的“老伙计”。但不知道你有没有遇到过这种场景:明明在IDE里编译得好好的工程,一进调试器,源码窗…

作者头像 李华
网站建设 2026/6/22 23:04:15

《龙虾大模型调用Token损耗的五层治理路径》

大模型业务落地的成本失控,往往不是来自可见的功能开发,而是藏在调用链路的隐性损耗里。龙虾体系内的大模型调用场景,普遍存在超时重试的默认配置,多数团队只关注重试能否保障业务成功率,却忽略了一个核心计费规则&…

作者头像 李华
网站建设 2026/6/22 23:01:07

Java插件化漏洞扫描器Artillery:架构设计与一键Getshell实现

1. 项目概述与核心价值最近在整理自己的安全工具箱,发现很多扫描器要么太重,要么太老,要么就是纯命令行对新手不太友好。特别是想快速验证一些常见中间件、框架的漏洞时,往往需要翻好几个工具,或者写一堆脚本&#xff…

作者头像 李华
网站建设 2026/6/22 22:57:44

深入解析NXP KE1xF MCM缓存写缓冲错误与MPU协同防护机制

1. 项目概述与核心问题定位 在嵌入式系统,尤其是汽车电子和工业控制这类对可靠性要求极高的领域,系统稳定性的基石往往不是那些光鲜亮丽的功能,而是对底层硬件异常悄无声息却又精准无误的捕获与处理。我遇到过不少项目,前期功能测…

作者头像 李华