第8節 理解程序排程時機跟蹤分析程序排程與程序切換的過程【Linux核心分析】
一、實驗要求
分析並理解Linux中程序排程與程序切換過程,仔細分析程序的排程時機、switch_to及對應的堆疊狀態。需要總結並闡明自己對“Linux系統一般執行過程”的理解
二、實驗內容
理解Linux系統中程序排程的時機,可以在核心程式碼中搜索schedule()函式,看都是哪裡呼叫了schedule(),判斷我們課程內容中的總結是否準確;
使用gdb跟蹤分析一個schedule()函式 ,驗證您對Linux系統程序排程與程序切換過程的理解。
- 特別關注並仔細分析switch_to中的彙編程式碼,理解程序上下文的切換機制,以及與中斷上下文切換的關係;
三、實驗環境
本地linux環境(ubuntu14.04 64bit)
主要優點:使用方便,方便儲存,不受網路影響。
四、實驗過程
1.開啟shell終端,執行以下命令:
cd LinuxKernel
rm -rf menu
git clone https://github.com/mengning/menu.git
cd menu
mv test_exec.c test.c
make rootfs
2.通過增加-s -S啟動引數開啟除錯模式
qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S
3.開啟gdb進行遠端除錯
gdb
file ../linux-3.18.6/vmlinux
target remote:1234
4.設定斷點
根據雲課堂上的知識,這次我們主要通過除錯跟蹤linux核心中的schedule函式,進而分析linux系統進行程序排程和程序切換的過程。
執行MenuOS,設定3個斷點:schedule、context_switch、switch_to。
b schedule
b context_switch
b switch_to
按c(/continue)後,模擬器繼續執行,在第一個斷點處停止,即schedule函式處,使用l(/list)命令檢視其程式碼,s(/step)命令逐條分析。
繼續執行,到第二個斷點處停止了,即context_switch處停下。與上相同,繼續單步執行。在這個過程中,要留心一下switch_to。
switch_to試了好幾次都進不去,每次都是在中斷點2和1之間進行跳轉。
後來通過list檢視程式碼發現, context_switch中呼叫了switch_to函式。
在該行添加了一個斷點,停下的地方卻是__switch_to函式。switch_to是個巨集定義,在預處理階段就把巨集定義命令轉換了,導致沒辦法除錯到該斷點。
五、程式碼分析
0.在switch_to函式中,通過嵌入如下的彙編程式碼實現程序上下文的切換以及與中斷上下文的切換:
asm volatile("pushfl\n\t" /* 儲存標誌位 */
"pushl %%ebp\n\t" /* 儲存 EBP */
"movl %%esp,%[prev_sp]\n\t" /* 儲存 ESP */
"movl %[next_sp],%%esp\n\t" /* 恢復 ESP */
"movl $1f,%[prev_ip]\n\t" /* 儲存 EIP */
"pushl %[next_ip]\n\t" /* 恢復 EIP */
__switch_canary
"jmp __switch_to\n" /* 跳轉l */
"1:\t"
"popl %%ebp\n\t" /* 恢復 EBP */
"popfl\n" /* 恢復標誌位*/
/* output parameters */
: [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip),
"=a" (last),
/* clobbered output registers: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)
__switch_canary_oparam
/* input parameters: */
: [next_sp] "m" (next->thread.sp),
[next_ip] "m" (next->thread.ip),
/* regparm parameters for __switch_to(): */
[prev] "a" (prev),
[next] "d" (next)
\
__switch_canary_iparam
\
: /* reloaded segment registers */
"memory");
1. schedule
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;//來獲取當前程序
sched_submit_work(tsk);//避免死鎖
__schedule();//處理切換過程
}
2. __schedule
static void __sched __schedule(void)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
need_resched:
preempt_disable(); //關閉核心搶佔
cpu = smp_processor_id();
rq = cpu_rq(cpu); //找到當前cpu上的就緒佇列rq,跟當前程序相關的runqueue的資訊被儲存在rq中
rcu_note_context_switch(cpu);
prev = rq->curr; //將正在執行的程序curr儲存到prev中
schedule_debug(prev);//如果禁止核心搶佔,而又呼叫了cond_resched就會出錯,這裡就是用來捕獲該錯誤的
if (sched_feat(HRTICK))
hrtick_clear(rq);
smp_mb__before_spinlock();
raw_spin_lock_irq(&rq->lock);
switch_count = &prev->nivcsw;//切換次數記錄, 預設認為非主動排程計數
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE))//如果核心態沒有被搶佔,並且核心搶佔有效
{
//如果當前程序有非阻塞等待訊號,並且它的狀態是TASK_INTERRUPTIBLE
if (unlikely(signal_pending_state(prev->state, prev)))
{
prev->state = TASK_RUNNING; //將當前程序的狀態設為:TASK_RUNNING
}
else
{
deactivate_task(rq, prev, DEQUEUE_SLEEP);//將當前程序從runqueue(執行佇列)中刪除
prev->on_rq = 0; //標識當前程序不在runqueue中
if (prev->flags & PF_WQ_WORKER)
{
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
switch_count = &prev->nvcsw;
}
pre_schedule(rq, prev);
if (unlikely(!rq->nr_running))//如果runqueue中沒有正在執行的程序
idle_balance(cpu, rq); //就會從其它CPU拉入程序
put_prev_task(rq, prev); //通知排程器,當前程序要被另一個程序取代,做好準備
next = pick_next_task(rq); //從runqueue中選擇最適合的程序
clear_tsk_need_resched(prev); //清除當前程序的重排程標識
rq->skip_clock_update = 0;
//當前程序與所選程序是否是同一程序,不屬於同一程序才需要切換
if (likely(prev != next))
{
rq->nr_switches++;
rq->curr = next; //所選程序代替當前程序
++*switch_count;
context_switch(rq, prev, next); //負責底層上下文切換
cpu = smp_processor_id();
rq = cpu_rq(cpu);
}
else
raw_spin_unlock_irq(&rq->lock); //如果不需要切換程序,則只需要解鎖
post_schedule(rq);
sched_preempt_enable_no_resched();
if (need_resched())
goto need_resched;
}
put_prev_task和pick_next_task是程序的排程的核心;理解context_switch是理解程序的切換的必要過程。在系統進行上下文切換中,首先需要判斷prev和next是否是同一個程序,如果是,則保持原狀,不切換;否則接著將next設定為rq->curr,然後呼叫context_switch來進行實際的上下文切換。
3. context_switch
/*
* context_switch - switch to the new MM and the new thread's register state.
*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;
/* 完成程序切換的準備工作 */
prepare_task_switch(rq, prev, next);
mm = next->mm;
oldmm = prev->active_mm;
/*
* For paravirt, this is coupled with an exit in switch_to to
* combine the page table reload and the switch backend into
* one hypercall.
*/
arch_start_context_switch(prev);
/* 如果next是核心執行緒,則執行緒使用prev所使用的地址空間
* schedule( )函式把該執行緒設定為懶惰TLB模式
* 核心執行緒並不擁有自己的頁表集(task_struct->mm = NULL)
* 它使用一個普通程序的頁表集
* 不過,沒有必要使一個使用者態線性地址對應的TLB表項無效
* 因為核心執行緒不訪問使用者態地址空間。
*/
if (!mm) /* 核心執行緒無虛擬地址空間, mm = NULL*/
{
/* 核心執行緒的active_mm為上一個程序的mm
* 注意此時如果prev也是核心執行緒,
* 則oldmm為NULL, 即next->active_mm也為NULL */
next->active_mm = oldmm;
/* 增加mm的引用計數 */
atomic_inc(&oldmm->mm_count);
/* 通知底層體系結構不需要切換虛擬地址空間的使用者部分
* 這種加速上下文切換的技術稱為惰性TBL */
enter_lazy_tlb(oldmm, next);
}
else /* 不是核心執行緒, 則需要切切換虛擬地址空間 */
switch_mm(oldmm, mm, next);
/* 如果prev是核心執行緒或正在退出的程序
* 就重新設定prev->active_mm
* 然後把指向prev記憶體描述符的指標儲存到執行佇列的prev_mm欄位中
*/
if (!prev->mm)
{
/* 將prev的active_mm賦值和為空 */
prev->active_mm = NULL;
/* 更新執行佇列的prev_mm成員 */
rq->prev_mm = oldmm;
}
/*
* Since the runqueue lock will be released by the next
* task (which is an invalid locking op but in the case
* of the scheduler it's an obvious special-case), so we
* do an early lockdep release here:
*/
lockdep_unpin_lock(&rq->lock);
spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
/* Here we just switch the register state and the stack.
* 切換程序的執行環境, 包括堆疊和暫存器
* 同時返回上一個執行的程式
* 相當於prev = switch_to(prev, next) */
switch_to(prev, next, prev);
/* switch_to之後的程式碼只有在
* 當前程序再次被選擇執行(恢復執行)時才會執行
* 而此時當前程序恢復執行時的上一個程序可能跟引數傳入時的prev不同
* 甚至可能是系統中任意一個隨機的程序
* 因此switch_to通過第三個引數將此程序返回
*/
/* 路障同步, 一般用編譯器指令實現
* 確保了switch_to和finish_task_switch的執行順序
* 不會因為任何可能的優化而改變 */
barrier();
/* 程序切換之後的處理工作 */
finish_task_switch(this_rq(), prev);
}
4. switch_to
/*
* Saving eflags is important. It switches not only IOPL between tasks,
* it also protects other tasks from NT leaking through sysenter etc.
*/
#define switch_to(prev, next, last) \
do { \
/* \
* Context-switching clobbers all registers, so we clobber \
* them explicitly, via unused output variables. \
* (EAX and EBP is not listed because EBP is saved/restored \
* explicitly for wchan access and EAX is the return value of \
* __switch_to()) \
*/ \
unsigned long ebx, ecx, edx, esi, edi; \
\
asm volatile("pushfl\n\t" /* save flags 儲存就的ebp、和flags暫存器到舊程序的核心棧中*/ \
"pushl %%ebp\n\t" /* save EBP */ \
"movl %%esp,%[prev_sp]\n\t" /* save ESP 將舊程序esp儲存到thread_info結構中 */ \
"movl %[next_sp],%%esp\n\t" /* restore ESP 用新程序esp填寫esp暫存器,此時核心棧已切換 */ \
"movl $1f,%[prev_ip]\n\t" /* save EIP將標號1:的地址儲存到prev->thread.ip中,下一次該程序被呼叫的時候從1的位置開始執行*/ \
"pushl %[next_ip]\n\t" /* restore EIP 將新程序的ip值壓入到新程序的核心棧中 */ \
__switch_canary \
"jmp __switch_to\n" /* regparm call */ \
"1:\t" \
"popl %%ebp\n\t" /* restore EBP 該程序執行,恢復ebp暫存器*/ \
"popfl\n" /* restore flags 恢復flags暫存器*/ \
\
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp),/*m表示把變數放入記憶體,即把[prev_sp]儲存的變數放入記憶體,最後再寫入prev->thread.sp*/\
[prev_ip] "=m" (prev->thread.ip), \
"=a" (last), /*=表示輸出,a表示把變數last放入ax,eax = last*/ \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx),/*b 變數放入ebx,c表示放入ecx,d放入edx,S放入si,D放入edi*/\
"=S" (esi), "=D" (edi) \
\
__switch_canary_oparam \
\
/* input parameters: */ \
: [next_sp] "m" (next->thread.sp), /*next->thread.sp 放入記憶體中的[next_sp]*/\
[next_ip] "m" (next->thread.ip), \
\
/* regparm parameters for __switch_to(): */ \
[prev] "a" (prev), \
[next] "d" (next) \
\
__switch_canary_iparam \
\
: /* reloaded segment registers */ \
"memory"); \
} while (0)
5.__switch_to
__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
int cpu = smp_processor_id();/* 得到當前程式碼執行的CPU編號 */
struct tss_struct *tss = &per_cpu(init_tss, cpu);/* 得到當前CPU的TSS init_tss為一個per cpu變數*/
fpu_switch_t fpu;
/* never put a printk in __switch_to... printk() calls wake_up*() indirectly */
fpu = switch_fpu_prepare(prev_p, next_p, cpu);/* 載入FPU、MMX、XMM的暫存器組 */
/*
* Reload esp0.
*/
load_sp0(tss, next);//重新載入esp0:把next_p->thread.esp0裝入對應於本地cpu的tss的esp0欄位;
//任何由sysenter彙編指令產生的從使用者態到核心態的特權級轉換將把這個地址拷貝到esp暫存器中
/*
* Save away %gs. No need to save %fs, as it was saved on the
* stack on entry. No need to save %es and %ds, as those are
* always kernel segments while inside the kernel. Doing this
* before setting the new TLS descriptors avoids the situation
* where we temporarily have non-reloadable segments in %fs
* and %gs. This could be an issue if the NMI handler ever
* used %fs or %gs (it does not today), or if the kernel is
* running inside of a hypervisor layer.
*/
lazy_save_gs(prev->gs);
/*
* Load the per-thread Thread-Local Storage descriptor.
*/
load_TLS(next, cpu);//裝載每個執行緒的執行緒區域性儲存描述符:
//把next程序使用的執行緒區域性儲存(TLS)段 裝入本地CPU的全域性描述符表;
//三個段選擇符儲存在程序描述符內的tls_array陣列中
/*
* Restore IOPL if needed. In normal use, the flags restore
* in the switch assembly will handle this. But if the kernel
* is running virtualized at a non-zero CPL, the popf will
* not restore flags, so it must be done in a separate step.
*/
if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
set_iopl_mask(next->iopl);
/*
* If it were not for PREEMPT_ACTIVE we could guarantee that the
* preempt_count of all tasks was equal here and this would not be
* needed.
*/
task_thread_info(prev_p)->saved_preempt_count = this_cpu_read(__preempt_count);
this_cpu_write(__preempt_count, task_thread_info(next_p)->saved_preempt_count);
/*
* Now maybe handle debug registers and/or IO bitmaps
*/
if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
__switch_to_xtra(prev_p, next_p, tss);
/*
* Leave lazy mode, flushing any hypercalls made here.
* This must be done before restoring TLS segments so
* the GDT and LDT are properly updated, and must be
* done before math_state_restore, so the TS bit is up
* to date.
*/
arch_end_context_switch(next_p);
this_cpu_write(kernel_stack,
(unsigned long)task_stack_page(next_p) +
THREAD_SIZE - KERNEL_STACK_OFFSET);
/*
* Restore %gs if needed (which is common)
*/
if (prev->gs | next->gs)
lazy_load_gs(next->gs);
switch_fpu_finish(next_p, fpu);
this_cpu_write(current_task, next_p);
return prev_p;
}
六、總結
本次課程學習了作業系統如何進行程序的切換以及系統的一般執行過程。其中還是脫離不出中斷的使用,程序切換最主要的時機就在於中斷的過程,對關鍵函式switch_to的跟蹤分析以及函式解讀清晰地展示了堆疊如何變化的。
schedule()函式用來選擇一個新的程序來執行,並呼叫context_switch()進行上下文的切換,這個巨集呼叫switch_to()來進行關鍵上下文切換,其中pick_next_task()函式封裝了程序排程演算法。
1.對“Linux系統一般執行過程”的理解:
Linux系統中,一個程序的一般執行過程:
即從正在執行的使用者態程序X切換到執行使用者態程序Y的過程。
2.這裡有幾個特殊情況:
通過中斷處理過程中的排程時機,使用者態程序與核心執行緒之間互相切換和核心執行緒之間互相切換,與最一般的情況非常類似,只是核心執行緒執行過程中發生中斷沒有程序使用者態和核心態的轉換;
核心執行緒主動呼叫schedule(),只有程序上下文的切換,沒有發生中斷上下文的切換,與最一般的情況略簡略;
建立子程序的系統呼叫在子程序中的執行起點及返回使用者態,如fork;
載入一個新的可執行程式後返回到使用者態的情況,如execve。
3.典型的Linux作業系統的結構
4.switch_to從A程序切換到B程序的步驟如下:
step1:複製兩個變數到暫存器:
[prev]"a" (prev)
[next]"d" (next)
即
eax <== prev_A或eax<==%p(%ebp_A)
edx <== next_A 或edx<==%n(%ebp_A)
step2:儲存程序A的ebp和eflags
pushfl /*將狀態暫存器eflags壓棧*/
pushl %ebp
因為現在esp還在A的堆疊中,所以它們是被儲存到A程序的核心堆疊中。
step3:儲存當前esp到A程序核心描述符中:
movl%%esp, %[prev_sp]\n\t /*save ESP */
即
prev_A->thread.sp<== esp_A
在呼叫switch_to時,prev是指向A程序自己的程序描述符的。
step4:從next(程序B)的描述符中取出之前從B切換出去時儲存的esp_B。
movl %[next_sp], %%esp\n\t/* restore ESP */
即
esp_B <==next_A->thread.sp
step5:把標號為1的指令地址儲存到A程序描述符的ip域:
movl $1f, %[prev_ip]\n\t/* save EIP */
即
prev_A->thread.ip<== %1f
當A程序下次從switch_to回來時,會從這條指令開始執行。具體方法要看後面被切換回來的B的下一條指令。
step6:將返回地址儲存到堆疊,然後呼叫switch_to()函式,switch_to()函式完成硬體上下文切換。
pushl %[next_ip]\n\t/* restoreEIP */
jmp switch_to\n /* regparmcall */
如果之前B也被switch_to出去過,那麼[next_ip]裡存的就是下面這個1f的標號,但如果程序B剛剛被建立,之前沒有被switch_to出去過,那麼[next_ip]裡存的將是ret_ftom_fork(參看copy_thread()函式)。
當這裡switch_to()返回時,將返回值prev_A又寫入了%eax,這就使得在switch_to巨集裡面eax暫存器始終儲存的是prev_A的內容,或者,更準確的說,是指向A程序描述符的“指標”。
step7:從switch_to()返回後繼續從1:標號後面開始執行,修改ebp到B的核心堆疊,恢復B的eflags:
popl %%ebp\n\t/* restore EBP */
popfl\n/*restore flags */
如果從switch_to()返回後從這裡繼續執行,那麼說明在此之前B肯定被switch_to調出過,因此此前肯定備份了ebp_B和flags_B,這裡執行恢復操作。
step8:將eax寫入last,以在B的堆疊中儲存正確的prev資訊。
"=a"(last)
即
last_B <== %eax
而從context_switch()中看到的呼叫switch_to的方法是:switch_to(prev,next, prev);