目录
2.8 调度器增强
2.8.1 SMP调度
2.8.2 调度域和控制组
2.8.3 内核抢占和低延迟相关工作
2.9 小结
2.8 调度器增强
2.8.1 SMP调度
进程迁移:
含义:把进程从一个CPU就绪队列迁移至另一个CPU就绪队列。
作用:CPU负荷均衡。
缺点:缓存失效,危害性能。
设置进程的亲和力affinity,控制进程可在哪些CPU上运行。
对应系统调用:
long sched_setaffinity(pid_t pid, const struct cpumask *in_mask)
最终设置task_struct->cpus_allowed成员
CPU就绪队列中SMP部分:
struct rq {
#ifdef CONFIG_SMP
struct sched_domain *sd; 调度域
int active_balance; 是否正在进行负载均衡
int push_cpu; 迁移目标CPU
int cpu; 当前就绪队列所在CPU
struct task_struct *migration_thread; 执行迁移的线程
struct list_head migration_queue; 迁移请求链表
#endif
}
调度类中SMP相关函数指针:
struct sched_class {
void (*set_cpus_allowed)(struct task_struct *p, struct cpumask *newmask);
设置进程CPU 亲和力(affinity),即确定进程可运行在哪些CPU。
void (*migrate_task_rq)(struct task_struct *p, int next_cpu); 迁移
将进程迁移到指定的CPU上运行。
int (*select_task_rq)(struct task_struct *p, int sd_flag, int flags);
根据进程亲和性,系统负载平衡策略等为进程选择一个合适的CPU运行队列。
}
周期调度中关于SMP部分:
void scheduler_tick(void)
{
#ifdef CONFIG_SMP
rq->idle_balance = idle_cpu(cpu);
trigger_load_balance(rq, cpu); 每次时钟中断都进行CPU负载均衡
#endif
}
void trigger_load_balance(struct rq *rq, int cpu)
{
raise_softirq(SCHED_SOFTIRQ);
}
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);
SCHED_SOFTIRQ软中断处理函数run_rebalance_domains:
static void run_rebalance_domains(struct softirq_action *h)
{
rebalance_domains(this_cpu, idle);
}
进行调度域内部CPU之间的负载均衡。
然后调用rebalance_domains -> load_balance,进行负载均衡操作。
load_balance:
比较struct rq的进程总负荷,寻找最忙CPU,执行对应调度类的load_balance函数。
刚exec启动新进程时,适合跨CPU迁移,此时缓存影响小。
已运行的进程不适合迁移,因为刚准备的缓存立马失效,降低性能。
根据进程的亲和力决定迁移目标CPU。
2.8.2 调度域和控制组
调度域:
含义:将所有CPU分为若干调度域,每个调度域管理一组CPU。
特点:同一调度域的所有CPU具有相同属性和调度策略。
调度域分组依据:物理距离,共享cache,拓扑结构,亲和性等。
如物理临近或共享cache的CPU组成一个调度域。
不同调度域是树状结构,顶层调度域包含所有CPU,而下层调度域是对CPU子集的分组。
五个调度域层级:
All NUMA Domain
NUMA Domain
Phy Domain
Core Domain
CPU Domain(SMT Domain)
对应树形结构:
如一个CPU被分到两个调度域,每个调度域有自己的调度策略。
对应有两个目录:
/proc/sys/kernel/sched_domain/cpuX/domain0/
/proc/sys/kernel/sched_domain/cpuX/domain1/
两个目录下有多个参数,包括:
两次负载均衡最大/最小间隔, 调度域是否繁忙,最小不平衡值等
#cat /proc/sys/kernel/sched_domain/cpu9/domain0/name
MC //MC域,即Multi-Core
#cat /proc/sys/kernel/sched_domain/cpu9/domain1/name
NUMA //NUMA域
进程迁移时,优先在调度域内部迁移,应减少跨NUMA迁移。
控制组:cgroup
管理一组进程的使用资源。如CPU,内存,磁盘IO,网络带宽等。
cgroup可把进程按用户分组, 也可任意进程组合分组。
2.8.3 内核抢占和低延迟相关工作
内核抢占:
内核任务执行时被更高优先级的任务打断,从而允许更高优先级的任务立即执行。
并不是任意时候都可以抢占当前内核任务。
禁止内核抢占时机:
当前任务处于关键时机,不能被中断。如:
1、访问 Per-CPU数据时 。(防止smp_processor_id改变)
2、访问CPU state。
3、持有spinlock时。(如果允许内核抢占可能造成死锁。)
内核中如何判断当前任务是否可被抢占?
进程struct thread_info中preempt_count:
等于0,可被抢占。(退出最后一个临界区后为0)
大于0,禁止抢占。(可嵌套递归进入多个临界区)
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable(); //进入临界区后,将preempt_count+1,实现禁止内核抢占。
...
}
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
...
preempt_enable(); //释放锁后,将preempt_count-1,允许内核抢占。
}
#define preempt_enable() \
do { \
preempt_enable_no_resched(); //将preempt_count - 1
barrier(); \
preempt_check_resched(); \ 定义如下
} while (0)
#define preempt_check_resched() \
do { \
if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \
preempt_schedule(); \ 执行抢占,让出CPU,调度其他进程。
} while (0)
thread_info中flag的TIF_NEED_RESCHED标志:
用于指示当前进程需要被抢占,让出CPU,让其他进程执行。
什么时候设置当前进程的TIF_NEED_RESCHED标志?
1、进程被唤醒时,若有更高优先级进程需要执行。
2、创建新进程时,若新进程优先级高于当前进程。
3、进程修改进程的nice值时,导致优先级高于当前进程。
执行内核抢占前,都检查TIF_NEED_RESCHED标志。若置位,调用对应适用的抢占函数。
抢占时机总结:
1. 中断处理程序返回内核态时:
preempt_schedule_irq执行抢占。
2. 调用preempt_enable允许抢占时。
调用preempt_schedule()执行抢占。
3. 显式调用schedule函数。
4. 阻塞/睡眠时。
preempt_schedule_irq和preempt_schedule函数都最终调用_schedule()
访问per-CPU变量时,也需要禁止内核抢占,防止一个CPU读时,另一个CPU写。
#define get_cpu() ({ preempt_disable(); __smp_processor_id(); })
#define put_cpu() preempt_enable()
获取当前运行进程所在CPU
#define raw_smp_processor_id() (current_thread_info()->cpu)
int preempt_count,32位。
preempt_count中的bit 27就是PREEMPT_ACTIVE值。
PREEMPT_ACTIVE:
表明内核抢占已被激活。即使preempt_count的其他部分大于0。(例如,进入一个不可抢占的区域),内核仍然可以响应更高优先级的任务或信号。