系统调用是常见一种类型的异常,也是应用代码从用户空间主动进入内核空间的唯一方式。
运行在用户空间的进程会被动陷入到内核空间,进行中断处理程序的处理。
中断处理程序处理完后返回到用户空间,在返回至用户空间之前,判断是否要进行进程调度。
如果需要则再进行一次进程调度。
中断处理程序
// 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));
}
...
}