Linux sched_yield主动让出CPU与sys_sched_yield实现

发布时间:2026/6/22 23:06:05

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不受带宽限制。cSYSCALL_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位置。cstatic 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场景没有这个窗口。cstatic 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。cstatic 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入口。

相关新闻