排程時機分析之被動排程(之中斷處理返回)
之前一篇部落格中是關於被動排程的系統呼叫返回部分,這篇部落格將接著寫被動排程的中斷返回部分。
分析基於核心版本2.6.12.6
Linux程序的排程主要分為主動排程和被動排程兩大類。整個linux執行過程中,被動排程分為使用者態搶佔排程和核心態搶佔排程。
使用者態搶佔排程發生在當系統呼叫、中斷處理、異常處理等返回使用者態時,或者程序的時間片用完時。
這篇部落格就是寫使用者態搶佔排程的中斷處理返回部分的,是看程式碼的一個總結,寫下來希望能夠加深理解。
中斷處理返回
當從中斷返回時是一個排程點,下面分析從中斷返回相關的程式碼。
ENTRY(common_interrupt) /*中斷公共入口 interrupt do_IRQ /* 0(%rsp): oldrsp-ARGOFFSET */ ret_from_intr: popq %rdi cli //關中斷 subl $1,%gs:pda_irqcount #ifdef CONFIG_DEBUG_INFO movq RBP(%rdi),%rbp #endif leaq ARGOFFSET(%rdi),%rsp exit_intr: GET_THREAD_INFO(%rcx) //獲取當前核心棧的thread_info的地址,儲存到rcx暫存器中 testl $3,CS-ARGOFFSET( je retint_kernel //中斷髮生在核心態,返回核心態 |
common_interrupt函式是中斷的公共入口,它首先使用用巨集interrupt來呼叫do_IRQ函式處理中斷。ret_from_intr標籤開始就是執行完中斷之後,中斷返回相關的程式碼。
先來看下interrupt巨集的實現程式碼:
/* 0(%rsp): interrupt number */ .macro interrupt func CFI_STARTPROC simple /*用在每個函式的開始,用於初始化一些內部資料結構*/ CFI_DEF_CFA rsp CFI_REL_OFFSET rsp,(RSP-ORIG_RAX) CFI_REL_OFFSET rip,(RIP-ORIG_RAX) cld /*清方向標誌,在串指令操作期間,方向標誌為DI和SI暫存器選擇遞增方式或遞減方式。如果D=1.則暫存器內容自動地遞減;如果D=0,則暫存器內容自動地遞增*/ SAVE_ARGS /*儲存現場*/ leaq -ARGOFFSET(%rsp),%rdi # arg1 for handler testl $3,CS(%rdi) /*判斷中斷髮生時CPU是否運行於使用者態*/ je 1f /*是核心態*/ swapgs /*切換gs暫存器的使用者態值和核心值*/ 1: addl $1,%gs:pda_irqcount # RED-PEN should check preempt count movq %gs:pda_irqstackptr,%rax cmoveq %rax,%rsp pushq %rdi # save old stack call \func /*呼叫func函式*/ .endm |
SAVE_ARGS巨集是將暫存器壓棧,也就是所謂的“儲存現場”。執行SAVE_ARGS巨集之後系統堆疊的示意圖如下:
程式碼段暫存器cs的最低兩位代表著中斷髮生時cpu的執行級別CPL,0表示核心態,3表示使用者態。如果最低兩位為非0,則說明中斷髮生於使用者空間。此巨集中判斷中斷髮生時cpu是否運行於使用者態,如果是核心態則直接跳轉到標籤1處執行,否則先呼叫swapgs命令切換gs暫存器的使用者態值和核心值。Testl指令將$3和CS(%rdi)相與,je就是當相與結果為0,也就是為核心態。
接下來就是將當前cpu的pda的pda_irqcount欄位加1,並將pda中的pda_irqstackptr欄位賦給rsp,即中斷棧幀。最後使用call命令呼叫中斷處理函式func,也就是do_IRQ函式。do_IRQ函式的引數為pt_regs結構,引數是通過rdi暫存器傳入的。
從ret_from_intr開始處理中斷返回,如果中斷髮生在核心態,則返回核心態,跳轉到retint_kernel執行;如果中斷髮生在使用者態,則返回使用者態,跳轉到retint_with_reschedule執行。
retint_kernel將在核心態搶佔排程部分分析,這裡是分析中斷處理返回時使用者態搶佔的情況,因此接下來看看retint_with_reschedule程式碼。
/* Interrupt came from user space */ /* * Has a correct top of stack, but a partial stack frame * %rcx: thread info. Interrupts off. */ retint_with_reschedule: movl $_TIF_WORK_MASK,%edi retint_check: movl threadinfo_flags(%rcx),%edx andl %edi,%edx jnz retint_careful //判斷是否還有其他工作需要做,有的話跳轉到retint_careful執行 retint_swapgs: swapgs //當處理器離開核心時,使用swapgs命令在gs暫存器的核心與使用者態值之間切換 retint_restore_args: cli RESTORE_ARGS 0,8,0 iret_label: iretq |
首先判斷是否還有其他工作需要處理,有其他工作處理的情況下跳轉到retint_careful執行。沒有其他工作處理則首先呼叫swapgs命令在gs暫存器的核心與使用者態值之間切換,為離開核心做準備。然後呼叫RESTORE_ARGS巨集恢復之前SAVE_ARGS巨集儲存現場所壓棧的那些暫存器。最後就是呼叫iretq離開中斷。
下面看下當還有其他工作需要處理時retint_careful的執行情況。
/* edi: workmask, edx: work */ retint_careful: bt $TIF_NEED_RESCHED,%edx //檢查是否需要重新排程 jnc retint_signal //不需要重新排程,跳轉到retint_signal sti //開中斷 pushq %rdi call schedule popq %rdi GET_THREAD_INFO(%rcx) cli //關中斷 jmp retint_check |
首先檢查是否需要重新排程,不需要重新排程就跳轉到retint_signal執行。需要重新排程的話先開中斷,然後呼叫schedule函式執行任務的排程。當排程的任務執行完之後,重新獲得cpu時,跳轉到retint_check處再次檢查是否有工作需要處理。