当前位置: 首页 > 科技观察

深入理解Linux内核其中一个进程休眠

时间:2023-03-18 02:09:22 科技观察

1开场白环境:处理器架构:arm64内核源码:linux-5.10.50ubuntu版本:20.04.1代码阅读工具:vim+ctags+cscope任务是否在用户态还是在内核态,经常因为等待某些事件而休眠(可能是等待IO读写完成,或者是等待其他内核路径释放锁等).本文讨论什么状态是睡眠中的任务?睡眠对任务意味着什么?内核如何管理睡眠任务?我们会结合内核源码来分析任务的睡眠,力求从各个角度进行分析。注:由于篇幅问题,文章分为两部分,这里不区分进程和任务,用任务来表示进程。主要讲解以下内容:睡眠的三种状态睡眠的内核原理用户态睡眠内核态睡眠总结2.睡眠的三种状态任务睡眠有三种状态:LightsleepModeratesleepDeepsleep2.1Lightsleep进程描述符状态使用TASK_INTERRUPTIBLE来表示这个状态。是一种可中断的休眠状态,可中断的意思是可以被信号中断(唤醒)。这是被信号中断/唤醒的代码路径:kernel/signal.cSYSCALL_DEFINE2(kill,pid_t,pid,int,sig)->kill_something_info->__kill_pgrp_info->group_send_sig_info->do_send_sig_info->send_signal->__send_signal->alcomplete_sign->signal_wake_up->signal_wake_up_state(t,resume?TASK_WAKEKILL:0)->wake_up_state(t,state|TASK_INTERRUPTIBLE)->try_to_wake_up可以看到当信号下达后,会通过signal_wake_up将任务从可中断睡眠状态唤醒.2.2中休眠进程描述符的状态使用TASK_KILLABLE来表示这个状态。可以被致命信号打断。下面是被致命信号中断/唤醒的代码路径:>__kill_pgrp_info->group_send_sig_info->do_send_sig_info->send_signal->__send_signal->complete_signal->if(sig_fatal(p,sig)&&|!(signal->flags&SIGNAL_GROUP_EXIT)&&|!sigismember(&t->real_blocked,sig)&&|(==SIGKILL||!p->ptrace)){//致命信号...signal_wake_up(t,1);->signal_wake_up_state(t,resume?TASK_WAKEKILL:0)//resume==1->wake_up_state(t,state|TASK_INTERRUPTIBLE)->try_to_wake_up...}2.3深度睡眠进程描述符的状态使用TASK_UNINTERRUPTIBLE来表示这个状态。是不可中断的休眠状态,不能被任何信号唤醒(不满足一定条件的信号唤醒可能会导致数据不一致等问题,本场景使用该休眠状态,比如等待IO读写完成).3、sleep的核心原理Sleep是自动调度的,即主动调用主调度器。睡眠的主要步骤如下:1)设置任务状态为睡眠状态2)记录睡眠任务3)发起主动调度下面详细解释一下这些步骤:3.1设置任务状态为睡眠状态是必要的。来标记它已经进入睡眠状态,其次,主调度器会根据睡眠标志从运行队列中删除任务。注意:休眠状态说明见上一节!3.2记录睡眠任务这一步也是很有必要的。内核会记录即将休眠的任务,要么加入到链表中管理,要么使用数据结构记录。比如在延迟睡眠场景下,内核会在定时器相关的数据结构中记录即将进入睡眠的任务;在可休眠信号量场景中,内核将即将休眠的任务添加到该信号量的相关链表中。记录的目的是:当满足唤醒条件时,唤醒函数可以找到要唤醒的任务。3.3发起主动调度这一步才是真正的睡眠操作,主要是调用主调度器发起主动调度让出处理器。我们看一下主调度器对任务休眠的处理:kernel/sched/core.c__schedule->prev_state=prev->state;//获取上一个任务状态if(!preempt&&prev_state){//如果是activescheduling且任务状态不为0if(signal_pending_state(prev_state,prev)){//有pendingsignalprev->state=TASK_RUNNING;//设置状态为runnable}else{deactivate_task(rq,prev,DEQUEUE_SLEEP|DEQUEUE_NOCLOCK);//删除cpu运行队列中的任务}}next=pick_next_task(rq,prev,&rf);//选择下一个任务context_switch//上下文切换看deactivate_task对睡眠任务的主要工作:deactivate_task->deactivate_task(rq,prev,DEQUEUE_SLEEP|DEQUEUE_NOCLOCK)->p->on_rq=(flags&DEQUEUE_SLEEP)?0:TASK_ON_RQ_MIGRATING;//设置任务的on_rq为0标记睡眠dequeue_task(rq,p,flags);->p->sched_class->dequeue_task(rq,p,flags)->dequeue_task_fair->dequeue_entity...if(se!=cfs_rq->curr)//不是cpu当前任务__dequeue_entity(cfs_rq,se);//cfs运行队列delete->se->on_rq=0;//判断调度实体不在运行队列中!!!->if(!(flags&DEQUEUE_SLEEP))se->vruntime-=cfs_rq->min_vruntime;//虚拟运行时间调度实体的减去cfsrunning队列的最小虚拟运行时间deactivate_task会将任务的on_rq设置为0表示休眠,然后调用调度类的dequeue_task方法,在cfs中设置se->on_rq=0表示调度实体不在cfs队列中,可见。当发起主动调度时,主调度器会做出判断:如果是主动调度,任务状态不为0(即不可运行TASK_RUNNING),如果没有pending信号,任务会从任务中“删除”cpu的运行队列,然后会选择下一个任务进行上下文切换。从cpu的运行队列中“删除”即将休眠的任务意义重大:主调度器在再次选择下一个任务时不会选择休眠的任务(因为主调度器总是选择任务运行在运行队列,除非任务被唤醒并重新加入运行队列)。注意:1、这里的删除是指设置p->on_rq=0、se->on_rq=0等相应的flags,在下次选择任务时不会加入到运行队列中。2、即将休眠的任务就是cpu上的当前任务(curr指向)。3、调用主调度器后,要休眠的任务除非被唤醒,否则不会再次加入cpu运行队列。我们来看看选择下一个与sleep相关的任务时会做些什么(不考虑组调度情况):pick_next_task->class->pick_next_task->pick_next_task_fair//kernel/sched/fair.c->if(prev)put_prev_task(rq,prev);//处理上一个任务se=pick_next_entity(cfs_rq,NULL);//选择下一个任务set_next_entity(cfs_rq,se);主要看put_prev_task:put_prev_task->prev->sched_class->put_prev_task(rq,prev)->put_prev_task_fair->put_prev_entity->if(prev->on_rq){//上一个task的调度实体on_rq不为0?update_stats_wait_start(cfs_rq,prev);/*Put'current'backintothetree.*/__enqueue_entity(cfs_rq,prev);//重新加入cfs运行队列/*in!on_rqcase,updateoccurredatdequeue*/update_load_avg(cfs_rq,prev,0);}cfs_rq->curr=NULL;//设置cfs运行队列的curr为NULL。put_prev_task的主要工作是从cfs运行队列中删除之前的任务。这里通过调用__enqueue_entity红黑树将对应的调度实体添加到cfs队列中,但是对于即将休眠的任务,在主调度器中通过deactivate_task将prev->on_rq设置为0,所以对于即将休眠的任务即将休眠,其对应的调度实体不会重新加入cfs运行队列的红黑树。我们来看看睡眠图: