linux内核中断返回调度标志是如何设置的

系统调用是常见一种类型的异常,也是应用代码从用户空间主动进入内核空间的唯一方式。

运行在用户空间的进程会被动陷入到内核空间,进行中断处理程序的处理。

中断处理程序处理完后返回到用户空间,在返回至用户空间之前,判断是否要进行进程调度。

如果需要则再进行一次进程调度。

中断处理程序

// 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_opfork 一样最终执行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));
    }
    ...
}
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值