系统调用是常见一种类型的异常,也是应用代码从用户空间主动进入内核空间的唯一方式。
运行在用户空间的进程会被动陷入到内核空间,进行中断处理程序的处理。
中断处理程序处理完后返回到用户空间,在返回至用户空间之前,判断是否要进行进程调度。
如果需要则再进行一次进程调度。
中断处理程序
// arch/arm64/kernel/entry.S:838
// arm64 的中断入口
el0_irq:
...
处理中断
...
// 回到用户空间
b ret_to_user
// arch/arm64/kernel/entry.S:895
ret_to_user:
...
ldr x1, [tsk, #TSK_TI_FLAGS]
and x2, x1, #_TIF_WORK_MASK
cbnz x2, work_pending
el0_irq
被中断暂停的当前进程将?tsk 数据结构中,偏移量为?TSK_TI_FLAGS ?传递给 x1 寄存器。
TSK_TI_FLAGS ?常量在?asm-offsets.c ?文件中被定义。
//?arch/arm64/kernel/asm-offsets.c:48
int?main(void)?{
????...
????DEFINE(TSK_TI_FLAGS,?offsetof(struct?task_struct,?thread_info.flags))
????...????????
}
?task_struct ?结构中的?thread_info ?结构中的?flags ?字段的偏移量:
//?include/linux/sched.h:592
struct?task_struct?{
????...
????struct?thread_info?thread_info;
????...
}
//?arch/arm64/include/asm/thread_info.h:39
struct?thread_info?{
????...
????unsigned?long?flags;
????...
}
ret_to_user
取出?task_struct->thread_info->flags ?字段,与?_TIF_WORK_MASK ?进行 and 操作:
//?arch/arm64/include/asm/thread_info.h:118
#define?_TIF_WORK_MASK??(_TIF_NEED_RESCHED?|?_TIF_SIGPENDING?|?\
?????_TIF_NOTIFY_RESUME?|?_TIF_FOREIGN_FPSTATE?|?\
?????_TIF_UPROBE?|?_TIF_FSCHECK)
flags ?与?_TIF_WORK_MASK ?进行 and 操作之后。
如二进制位的值不为 0,cbnz跳转 到?work_pending ?方法。
// arch/arm64/kernel/entry.S:884
work_pending:
...
bl do_notify_resume
...
// arch/arm64/kernel/signal.c:915
// 参数thread_flags就是保存在x1寄存器中的值 即task_struct>thread_info->flags
void do_notify_resume(... long thread_flags) {
...
if (thread_flags & _TIF_NEED_RESCHED) {
schedule();
}
...
}
当中断处理程序返回用户空间的时候,如果被中断的进程被设置了需要进程调度标志,那么就进行一次进程调度。
如何被设置调度标志?
只有进入到内核空间才能够设置当前进程的需要调度标志。
而系统调用是主动从用户空间进入内核空间的唯一方式。
有哪些系统调用会设置当前进程调度的标志???
fork创建新进程设置调度标志
通过fork 系统调用创建新的进程。
//?kernel/fork.c:2291
SYSCALL_DEFINE0(fork)?{
????...
????return?_do_fork(...);
}
//?kernel/fork.c:2196
long?_do_fork(...)?{
????struct?task_struct?*p;
????...
//?copy继承父进程资源
????p?=?copy_process(...);
????...
//?创建子进程后 wakeup子进程
????wake_up_new_task(p);
????...
}
创建完新进程后调用wake_up_new_task 唤醒新进程:
//?kernel/sched/core.c:2413
void?wake_up_new_task(struct?task_struct?*p)?{
????...
//?当前进程设置为RUNNING状态
? p->state?=?TASK_RUNNING;
????...
//?是否要抢占当前进程
? check_preempt_curr(rq,?p,?WF_FORK);
????...
}
check_preempt_curr ?会根据当前进程的调度类型,执行对应的方法:
//?kernel/sched/core.c:854
void?check_preempt_curr(struct?rq?*rq,?struct?task_struct?*p,?int?flags)?{
????...
//?rq当前cpu上的进程队列、curr当前正在cpu运行的进程、sched_class当前进程的调度
????rq->curr->sched_class->check_preempt_curr(rq,?p,?flags);
????...
}
sched_class 表示进程的调度类型。
//?include/linux/sched.h:592
struct?task_struct?{
????...
//??sched_class?在进程的数据结构中
//??表示调度类型,我们后面的系列文章再详细分析?
????const?struct?sched_class?*sched_class;
????...
}
//?kernel/sched/sched.h:1715
//?Linux?中所有的调度类型
extern?const?struct?sched_class?stop_sched_class;
extern?const?struct?sched_class?dl_sched_class;
extern?const?struct?sched_class?rt_sched_class;
extern?const?struct?sched_class?fair_sched_class;
extern?const?struct?sched_class?idle_sched_class;
fair_sched_class为 一般进程的调度类型(公平调度)。
fair_sched_class ?的?check_preempt_check ?方法:
//?kernel/sched/fair.c:10506
const?struct?sched_class?fair_sched_class?=?{
.check_preempt_curr?=?check_preempt_wakeup
}
//?kernel/sched/fair.c:6814
static?void?check_preempt_wakeup(rq?*rq,?task_struct?*p...)?{
????struct?task_struct?*curr?=?rq->curr;
? struct?sched_entity?*se?=?&curr->se,?*pse?=?&p->se;
?
//?如pse的虚拟时间小于当前进程的虚拟时间 则抢占
????if?(wakeup_preempt_entity(se,?pse)?==?1)?{
?? goto?preempt;
? }
preempt:
//?设置一个标志 在异常处理返回的时统一调度
? resched_curr(rq);
}
se 表示当前进程的调度实体,pse 表示 fork 出来的进程的调度实体。
调度实体这个对象也定义在进程的数据结构中。
//?include/linux/sched.h:592
struct?task_struct?{
????...
????struct?sched_entity?se;
????...
}
check_preempt_wakeup 方法中的wakeup_preempt_entity:
//?kernel/sched/fair.c:6767
static?int?wakeup_preempt_entity(struct?sched_entity?*curr,?struct?sched_entity?*se)?{
? s64?gran,?vdiff?=?curr->vruntime?-?se->vruntime;
? if?(vdiff?<=?0)
?? return?-1;
????
//?gran进程运行的最小时间片
? gran?=?wakeup_gran(se);?
? if?(vdiff?>?gran)
?? return?1;
? return?0;
}
公平调度通过进程的优先级和历史运行情况,计算一个进程运行的虚拟时间。
虚拟时间小的进程可以抢占虚拟时间大的进程。
如果当前进程的时间片已到,当前进程的虚拟时间小于?fork ?出来的进程的虚拟时间片,返回1进入到preempt执行 resched_curr 。
//?kernel/sched/core.c:465
void?resched_curr(struct?rq?*rq)?{
????...
????set_tsk_need_resched(curr);
????...
}????
//?include/linux/sched.h:1676
static?inline?void?set_tsk_need_resched(struct?task_struct?*tsk)?{
? set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}
resched_curr ?给当前进程设置一个标志,需要进行一次调度。
在下一次中断返回到用户空间的时候,就会进行一次调度。
futex唤醒进程设置调度标志
futex系统调用的时,也会设置需要调度的标志。
//?kernel/futex.c:3633
SYSCALL_DEFINE6(futex,?...?op,?...)?{
????...
????return?do_futex(...?op,?...);
}
用户传递的op参数是?FUTEX_WAKE_OP, 需要进行唤醒操作:
//?kernel/futex.c:3573
long?do_futex(...int?op,...)?{
?int?cmd?=?op?&?FUTEX_CMD_MASK;
?switch?(cmd)?{
????????case?FUTEX_WAKE_OP:
????????????return?futex_wake_op(...);
????...
????}
????...
}
//?kernel/futex.c:1683
static?int?futex_wake_op(...)?{
????...
????wake_up_q(...);?//?:1766
????...
}
//?kernel/sched/core.c:436
void?wake_up_q(...)?{
????wake_up_process(task);
}
//?kernel/sched/core.c:1667
// 最终执行该函数
static?void?ttwu_do_wakeup(...)?{
????check_preempt_curr(...);
}
futex_ wake_op 与fork ?一样最终执行check_preempt_curr函数。
该函数做的就是给当前线程设置一个需要调度的标志,在下一次中断返回时进行一次调度。
周期调度设置标志
除了系统调用外。
内核还有一个定时调度机制。
周期地调用scheduler_tick 执行调度。
//?kernel/sched/core.c:3049
/*
?*?This?function?gets?called?by?the?timer?code,?with?HZ?frequency.
?*/
void?scheduler_tick(void)?{
????...
//?当前使用的cpu
????int?cpu?=?smp_processor_id();
//?得到cpu的进程队列
????struct?rq?*rq?=?cpu_rq(cpu);
//?得到cpu前运行的进程
????struct?task_struct?*curr?=?rq->curr;
????...
????curr->sched_class->task_tick(rq,?curr,?0);
????...
}
curr->sched_class->task_tick为当前进程的调度类的方法,即上文提到的五种方法。 ?
extern?const?struct?sched_class?stop_sched_class;
extern?const?struct?sched_class?dl_sched_class;
extern?const?struct?sched_class?rt_sched_class;
extern?const?struct?sched_class?fair_sched_class;
extern?const?struct?sched_class?idle_sched_class;
task_tick 公平调度类方法为例:
//?kernel/sched/fair.c:10506?
const?struct?sched_class?fair_sched_class?=?{
????...????
????.task_tick?=?task_tick_fair,
????...
}
//?kernel/sched/fair.c:10030
static?void?task_tick_fair(struct?rq?*rq,?struct?task_struct?*curr,?int?queued)?{
?struct?cfs_rq?*cfs_rq;
?struct?sched_entity?*se?=?&curr->se;
????...
//?为当前cpu上公平调度类的进程队列
????cfs_rq?=?cfs_rq_of(se);
????entity_tick(cfs_rq,?se,?queued);
????...
}
//?kernel/sched/fair.c:4179
static?void?entity_tick(struct?cfs_rq?*cfs_rq,?struct?sched_entity?*curr,?int?queued)?{
//?更新当前进程的运行时间
? update_curr(cfs_q);
????...
//?更新当前进程的?load
?update_load_avg(cfs_rq,?curr,?UPDATE_TG);
????...
//?如cpu有就绪进程
? if?(cfs_rq->nr_running?>?1)
?? check_preempt_tick(cfs_rq,?curr);
}
cfs_rq->nr_running 为当前cpu,公平调度类型的就绪进程、运行进程之和。
大于1表示有待调度的就绪进程。
然后调用?check_preempt_tick :
该函数计算进程理想运行时间?=?调度周期*当前调度实体权重/所有实体权重。
如当前进程运行的时间超过理想运行时间,就尝试一次调度,即调用?resched_curr函数。
//?kernel/sched/fair.c:4023
static?void?check_preempt_tick(struct?cfs_rq?*cfs_rq,?struct?sched_entity?*curr)?{
? unsigned?long?ideal_runtime,?delta_exec;
? struct?sched_entity?*se;
????...
? ideal_runtime?=?sched_slice(cfs_rq,?curr);
? delta_exec?=?curr->sum_exec_runtime?-?curr->prev_sum_exec_runtime;
? if?(delta_exec?>?ideal_runtime)?{
?? resched_curr(rq_of(cfs_rq));
? }
????...
}
|