淺析Linux操做系統工做的基礎

環境:lubuntu 13.04   kernel 3.9.7 php

做者:SA12226265 kataohtml

簡介: 本文根據 Linux™ 系統工做基礎的分析,對存儲程序計算機、堆棧(函數調用堆棧)機制和中斷機制進行概述。文中將爲您提供操做系統(內核)如何工做的細節進一步從宏觀概述結合關鍵點進行微觀(CS:EIP、EBP/ESP等的變化)分析。linux


 

1、存儲程序計算機                                                                                                                                                     web

  首先讓咱們瞭解一下,什麼是存儲程序計算機,並對存儲程序計算機的整個運行過程及所需的硬件組件進行簡單介紹ubuntu

  上圖是程序存儲計算機的物理框架,主要包含CPU(包含各種寄存器,如程序寄存器,指令寄存器等),主存,I/O設備,一個最簡單的的程序存儲計算機只須要如下部件來完成計算機工做:數組

    – 主存,也就是咱們普通PC上內存,用於存儲指令和數據
  – 處理器, 用於執行算術和邏輯操做
  – 控制單元, 解析須要操做的指令集框架

  程序存儲在計算機主存當中,並以數據的形式被CPU訪問和讀寫,程序中各條指令都被獲取並放到一個EIP寄存器,EIP寄存器中數據控制整個處理單元的運行,取「下一條」指令,繼續運行異步

  PC = 程序計數器
  IR = 指令寄存器
  MAR = 存儲器地址寄存器
  MBR = 存儲器緩衝寄存器ide

  在Linux系統中,通常同時會有幾個程序一塊兒運行,運行過程當中這些程序的都存儲在主存中,而CPU只會在同一時間內運行其中優先級較高的某一個,並根據優先級順序不斷的切換多個進程運行,使得計算機操做者會有多個程序同時運行的錯覺。函數

  在存儲程序計算機中,最重要的部分就是多個進程的切換,是什麼控制着進程間的切換,如何保證進程切換過程當中可以使得多個進程運行時不發生混亂,這一切都是由Linux內核控制的,下面咱們深刻解析Linux內核的在進程切換時的工做機制。

  先看調度的方式。

  因爲調度時機發生時進程在進入了內核態這樣,內核必須等待該進程即將結束內核態時才進行切換操做,而進程若是正在用戶態時則切換工做會當即執行,因此,通常進程調度發生在當前進程從內核態(包括從系統調用而進入內核態)返回用戶態的前夕。至於調度的政策,均按照前面所提到的以優先級爲基礎的調度。

  針對不一樣的進程有不一樣的調度政策,主要有SCHED_FIFO,SCHED_RR, SCHED_OTHER(源碼集中在kernel\sched 目錄下),其中FIFO適用於時間性要求比較高的進程,而RR針對時間片耗盡的進程,因爲沒有研究過源碼這裏不作詳細描述。

  當切換進程已經選好後,就開始用戶虛擬空間的處理,而後就是進程的切換switch_to()。所謂進程的切換主要就是堆棧的切換,這是由宏操做switch_to()完成的,定義於\linux-3.9.7\arch\x86\include\asm\switch_to.h中:

 1 #define switch_to(prev, next, last)                    \
 2 do {                                    \
 3     /*                                \
 4      * Context-switching clobbers all registers, so we clobber    \
 5      * them explicitly, via unused output variables.        \
 6      * (EAX and EBP is not listed because EBP is saved/restored    \
 7      * explicitly for wchan access and EAX is the return value of    \
 8      * __switch_to())                        \
 9      */                                \
10     unsigned long ebx, ecx, edx, esi, edi;                \
11                                     \
12     asm volatile("pushfl\n\t"        /* save    flags */    \
13              "pushl %%ebp\n\t"        /* save    EBP   */    \
14              "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */ \
15              "movl %[next_sp],%%esp\n\t"    /* restore ESP   */ \
16              "movl $1f,%[prev_ip]\n\t"    /* save    EIP   */    \
17              "pushl %[next_ip]\n\t"    /* restore EIP   */    \
18              __switch_canary                    \
19              "jmp __switch_to\n"    /* regparm call  */    \
20              "1:\t"                        \
21              "popl %%ebp\n\t"        /* restore EBP   */    \
22              "popfl\n"            /* restore flags */    \
23                                     \
24              /* output parameters */                \
25              : [prev_sp] "=m" (prev->thread.sp),        \
26                [prev_ip] "=m" (prev->thread.ip),        \
27                "=a" (last),                    \
28                                     \
29                /* clobbered output registers: */        \
30                "=b" (ebx), "=c" (ecx), "=d" (edx),        \
31                "=S" (esi), "=D" (edi)                \
32                                            \
33                __switch_canary_oparam                \
34                                     \
35                /* input parameters: */                \
36              : [next_sp]  "m" (next->thread.sp),        \
37                [next_ip]  "m" (next->thread.ip),        \
38                                            \
39                /* regparm parameters for __switch_to(): */    \
40                [prev]     "a" (prev),                \
41                [next]     "d" (next)                \
42                                     \
43                __switch_canary_iparam                \
44                                     \
45              : /* reloaded segment registers */            \
46             "memory");                    \
47 } while (0)

  這裏的輸出部分有三個參數,表示這段程序執行後有三項數據會有改變。其中[prev_sp]、[prev_ip] 都在內存中分別爲prev->thread.sp、prev->thread.ip,而最後一個參數則與寄存器EAX結合,對應於參數中的last。而輸入部則有4個參數,其中[next_sp]、[next_ip]在內存中,分別爲next->thread.sp 與next->thread.ip,剩餘的兩個參數則與寄存器EAX,EDX結合,分別對應prev,next。

  先看開頭有兩條push指令和結尾處有兩條pop指令,再看14行將當前的esp,也就是當前進程的prev的內核態的堆棧指針存入prev->thread.sp,第15行又將新收到調度要進入運行的進程next的內核態的堆棧之爭next->thread.sp置入esp。這樣一來,CPU在第15行與第16行這兩條指令之間就已經切換了堆棧。假定咱們有A,B兩個進程,在本次切換中prev指向A,而next指向B。也就是說,在本次切換中A爲要「調離」的進程,而B爲要「切入」的進程。那麼,在這裏的第12到15行是在使用A的堆棧,而從第16行開始就是在用B的堆棧了。換言之,從第16行開始,「當前進程」,已是B而不是A了。在內核代碼中當須要訪問當前進程的task_struct結構時使用的指針current時其實是宏定義,它根據當前的堆棧指針的ESP計算出所需的地址。若是第16行處引用current的話,那就是已經指向B的task_struct結構了。因此進程切換其實在第15行指令執行完就已經完成了。可是,構成一個進程的另外一個要素是程序的執行,因此還要進行其餘步驟。因爲12,13行事push進A的堆棧,而在 21行至22行從B的堆棧中POP出來,本質就是恢復新切入的進程在上一次被調離時的push進堆棧的內容。理論上了,進程的切換過程當中,多個進程都已經在執行了只是暫時的撤離cpu,因此切換過程就是在堆棧之間進程切換。

  那麼如何完成程序執行的切換,看一下以後的16行至20行。第16行的[prev_ip]所在位置,實際上就是將第21行的pop指令所在的地址保持在prev->thread.ip中,做爲進程A下一次被調度運行而切入時的「返回」地址。而後,又將next->thread.ip壓入堆棧。因此,這裏的next->thread.ip正是進程B上一次被調離時在第16行中保存的。它也指向這裏的[prev_ip],即21行的pop指令。接着,在19行經過jmp命令,而不是call命令,轉入了一個函數__switch_to()。暫時不討論__switch_to(),當CPU執行到哪裏的iret指令時,因爲是經過jmp指令轉過去的,最後進入對戰的next->thread.ip就變成了返回地址,而這就是[prev_ip]所在的地址,也就是21行的pop指令所在的地址。因爲每一個進程在被調離時都要執行這裏的第16行,這就決定了每一個進程在收到調度恢復運行時都是從這裏的第21行開始。

  上面都是已有進程的切換。但新建立的進程會是怎麼樣切換的。新建立的進程並無在「上一次調離時」執行過這裏的第12至16行,因此要將其task_struct結構中的thread.ip事先設置好,而且設置「返回地址」時不必定是[prev_ip]所在的地址,這裏取決於內核態堆棧的設置。

  那麼,咱們能夠看看前面上一篇文章淺析Linux計算機進程地址空間與內核裝載ELF中fork()的介紹,這個地址在copy_thread()中肯定,因爲未能完整的閱讀整個process_32.c中的源碼,如下內容只是推測若有錯誤請博友指正,在新進程被建立時,在父進程執行完fork以後只會返回會從調用系統調用時狀態,而子進程的「返回地址」也被設置成這個地址,因此__switch_to()一執行ret指令就直接回到了那裏。

  最後,在__switch_to()中到底幹了些什麼呢?看一下Linux3.9.7中/arch/x86/kernel/process_32.c  248行

 1 __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
 2 {
 3     struct thread_struct *prev = &prev_p->thread,
 4                  *next = &next_p->thread;
 5     int cpu = smp_processor_id();
 6     struct tss_struct *tss = &per_cpu(init_tss, cpu);
 7     fpu_switch_t fpu;
 8 
 9     /* never put a printk in __switch_to... printk() calls wake_up*() indirectly */
10 
11     fpu = switch_fpu_prepare(prev_p, next_p, cpu);
12 
13     /*
14      * Reload esp0.
15      */
16     load_sp0(tss, next);
17 
18     /*
19      * Save away %gs. No need to save %fs, as it was saved on the
20      * stack on entry.  No need to save %es and %ds, as those are
21      * always kernel segments while inside the kernel.  Doing this
22      * before setting the new TLS descriptors avoids the situation
23      * where we temporarily have non-reloadable segments in %fs
24      * and %gs.  This could be an issue if the NMI handler ever
25      * used %fs or %gs (it does not today), or if the kernel is
26      * running inside of a hypervisor layer.
27      */
28     lazy_save_gs(prev->gs);
29 
30     /*
31      * Load the per-thread Thread-Local Storage descriptor.
32      */
33     load_TLS(next, cpu);
34 
35     /*
36      * Restore IOPL if needed.  In normal use, the flags restore
37      * in the switch assembly will handle this.  But if the kernel
38      * is running virtualized at a non-zero CPL, the popf will
39      * not restore flags, so it must be done in a separate step.
40      */
41     if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
42         set_iopl_mask(next->iopl);
43 
44     /*
45      * Now maybe handle debug registers and/or IO bitmaps
46      */
47     if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
48              task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
49         __switch_to_xtra(prev_p, next_p, tss);
50 
51     /*
52      * Leave lazy mode, flushing any hypercalls made here.
53      * This must be done before restoring TLS segments so
54      * the GDT and LDT are properly updated, and must be
55      * done before math_state_restore, so the TS bit is up
56      * to date.
57      */
58     arch_end_context_switch(next_p);
59 
60     /*
61      * Restore %gs if needed (which is common)
62      */
63     if (prev->gs | next->gs)
64         lazy_load_gs(next->gs);
65 
66     switch_fpu_finish(next_p, fpu);
67 
68     this_cpu_write(current_task, next_p);
69 
70     return prev_p;
71 }

  這裏主要處理的是TSS,核心在16行,把next_p->thread.esp0裝入對應於本地cpu的tss的esp0字段;任何由sysenter彙編指令產生的從用戶態到內核態的特權級轉換將把這個地址拷貝到esp寄存器中。其次段寄存器gs中的內容也作了相應的切換。而後把next進程使用的縣城局部存儲(TLS)段裝入本地CPU的全局描述符表;三個段選擇符保存在進程描述符內的tls_array數組中。

  因此,除了剛建立新進程外,全部進程在受到調度時的切入點都在宏定義switch_to()中的標號[prev_ip],一直運行到在下一次進入switch_to()之後在__switch_to()中執行ret爲止。或者也能夠認爲,切入點在switch_to()中的21行,一直運行到在下一次進入switch_to()後的19行。總之,這新、舊當前進程的交接點就在switch_to這段代碼中。


2、堆棧(函數調用堆棧)機制                                                                                                                                

  接下來,咱們分析一下在Linux系統中的函數調用堆棧的機制,以前在淺析Linux計算機工做機制全面的分析了函數在堆棧調用過程當中堆棧中變量及寄存器的數值的變化。

  理解函數調用棧最重要的兩點是:棧的結構,ebp,esp,eip寄存器的做用。從這個基礎上發現堆棧在操做系統的工做中的做用:經過堆棧來保存任務切換過程當中的上下文,進而在順序執行的基礎上支持了多任務操做。
  
  函數調用堆棧過程可分解爲:參數入棧時的push操做,通常會有多個。另外一方面在調用過程當中確定會使用call指令,call指令內部其實還暗含了一個將返回地址(即call指令下一條指令的地址)壓棧的動做。Linux中gcc都會在每一個函數體以前插入相似以下指令:
  
1  pushl    %ebp
2  .cfi_def_cfa_offset 8
3  .cfi_offset 5, -8
4  movl    %esp, %ebp

  即,在程序執行到一個函數的真正函數體時,已經有如下數據順序入棧:參數,返回地址,EBP。由此獲得相似以下的棧結構(參數入棧順序跟調用方式有關)

  「pushl %ebp」「movl %esp,%ebp」先將EBP入棧,而後將棧頂指針ESP賦值給EBP。此時EBP寄存器中存儲着棧中的一個地址(原EBP入棧後的棧頂),從該地址爲基準,向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值,而該地址處又存儲着上一層函數調用時的EBP值。
  通常而言,ss:[ebp+4]處爲返回地址,ss:[ebp+8]處爲第一個參數值(最後一個入棧的參數值,此處假設其佔用4字節內存),ss:[ebp-4]處爲第一個局部變量,ss:[ebp]處爲上一層EBP值。
  因爲ebp中的地址處老是「上一層函數調用時的ebp值」,而在每一層函數調用中,都能經過當時的EBP值「向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值」。
  如此造成遞歸,直至到達棧底。這就是函數調用棧。
 

3、中斷機制                                                                                                                                                      

  中斷是CPU提供的一種功能,不屬於linux內核,而對應的中斷處理程序則屬於內核控制,在執行新指令前,控制單元會檢查在執行前一條指令的過程當中是否有中斷髮生,若是有控制大院就會拋下指令,進入下面流程:

1.肯定與中斷關聯的向量i(0<=i<=255) 2.尋找向量對於的處理程序 3.保存當前的「工做現場」,執行中斷處理程序 4.處理程序執行完畢後,把控制權交還給控制單元 5.控制單元恢復現場,返回繼續執行原程序

  因此整個中斷的處理過程當中,對於CPU,處理過程是同樣的,中斷現行程序,轉到中斷服務程序處執行,回到被中斷的程序繼續執行。CPU總共能夠處理256種中斷。

  那什麼是中斷處理程序,在介紹中斷處理程序以前,讓咱們先了解一下什麼是軟中斷、tasklet和工做隊列:

  軟中斷:軟中斷是利用硬件中斷的概念,用軟件方式進行模擬,實現宏觀上的異步執行效果。不少狀況下,軟中斷和"信號"有些相似,同時,軟中斷又是和硬中斷相對應的,"硬中斷是外部設備對CPU的中斷","軟中斷一般是硬中斷服務程序對內核的中斷","信號則是由內核(或其餘進程)對某個進程的中斷"(《Linux內核源代碼情景分析》第三章)。

  tasklet:tasklet是由軟中斷引出的, 內核定義了兩個軟中斷掩碼HI_SOFTIRQ和TASKLET_SOFTIRQ(二者優先級不一樣), 這兩個掩碼對應的軟中斷處理函數做爲入口, 進入tasklet處理過程.

  工做隊列:定義一個work結構(包含了處理函數), 而後在中斷處理過程當中調用schedule_work函數, work便被添加到workqueue中, 等待處理.工做隊列有着本身的處理線程, 這些work被推遲到這些線程中去處理.內核默認啓動了一個工做隊列, 對應一組工做線程events/n(n表明處理器編號, 這樣的線程有n個). 驅動程序能夠直接向這個工做隊列添加任務. 某些驅動程序還可能會建立並使用屬於本身的工做隊列.

  那麼,讓咱們看看目前最主要引發中斷的緣由:IO中斷,時鐘中斷,系統調用。所謂的中斷處理程序就是在響應一個特定中斷的時候,內核會執行一個函數,該函數就是中斷處理程序。下圖爲中斷處理程序的處理流程:

  

  從上圖能夠看出,每一箇中斷處理都要經歷保存、處理與恢復過程,咱們能夠總結出如下步驟:1.保存現場。2.執行具體的中斷處理程序。3.從中斷處理返回。4.恢復現場。

  那麼爲何會有軟中斷,tasklet,和工做隊列呢??因爲中斷處理程序通常都是在中斷請求關閉的條件下執行的,以免嵌套而使中斷控制複雜化。可是,中斷是一個隨機事件,它隨時會到來,若是關中斷的時間太長,CPU就不能及時響應其餘的中斷請求,從而形成中斷的丟失。所以,Linux內核的目標就是儘量快的處理完中斷請求,盡其所能把更多的處理向後推遲。所以,內核把中斷處理分爲兩部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中斷處理程序)內核當即執行,而下半部(就是一些內核函數)留着稍後處理。對應於上下半部的處理,纔有了以上這些概念。

  那麼,什麼狀況下使用工做隊列,什麼狀況下使用tasklet。若是推後執行的任務須要睡眠,那麼就選擇工做隊列。若是推後執行的任務不須要睡眠,那麼就選擇tasklet。另外,若是須要用一個能夠從新調度的實體來執行你的下半部處理,也應該使用工做隊列。它是惟一能在進程上下文運行的下半部實現的機制,也只有它才能夠睡眠。這意味着在須要得到大量的內存時、在須要獲取信號量時,在須要執行阻塞式的I/O操做時,它都會很是有用。若是不須要用一個內核線程來推後執行工做,那麼就考慮使用tasklet。

 


參數資料                                                                                                                                                             

【1】http://www.longene.org/forum/viewtopic.php?f=21&t=4646

【2】http://wenku.baidu.com/view/79fe36c10c22590103029d04

【3】 深刻理解Linux內核


 本文綜述部分系我的理解,若有錯誤請指正,轉載請聲明

相關文章
相關標籤/搜索