系統調用過程


陳民禾  原創作品轉載請注明出處 ——《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000

一.復習上周內容

       上周主要學習了內核的啟動過程可以簡單地這么來看:start_kernel從內核一啟動的時候它會一直存在,這個就是0號進程,idle就是一個while0,一直在循環着,當系統沒有進程需要執行的時候就調度到idle進程,我們在windows系統上會經常見到,叫做system idle,這是一個一直會存在的0號進程,然后呢就是0號進程創建了1號進程,這個init_process是我們的1號進程也就是第一個用戶態進程,也就是它默認的就是根目錄下的程序,也就是常會找默認路徑下的程序來作為1號進程,1號進程接下來還創建了kthreadd來管理內核的一些線程,這樣整個程序就啟動起來了。

二.內核態、用戶態、中斷等概念的介紹

用戶態和內核態的區分:

       現代計算機機中都有幾種不同的指令級別,在高執行級別下,代碼可以執行特權指令,訪問任意的物理地址,這種CPU執行級別就對應着內核態,而在相應的低級別執行狀態下,代碼的掌控范圍會受到限制,只能在對應級別允許的范圍內活動。舉例:Intrel x86 CPU有四種不同的執行級別0-3,Linux只使用了其中的0級和3級來分別表示內核態和用戶態。操作系統讓系統本身更為穩定的方式,這樣程序員自己寫的用戶態代碼很難把整個系統都給搞崩潰,內核的代碼經過仔細的分析有專業的人員寫的代碼會更加健壯一些,整個程序會更加穩定一些,注意:這里所說的地址空間是邏輯地址而不是物理地址。

     用戶態和內核態的很顯著的區分就是:CS和EIP, CS寄存器的最低兩位表明了當前代碼的特權級別;CPU每條指令的讀取都是通過CS:EIP這兩個寄存器:其中CS是代碼段選擇寄存器,EIP是偏移量寄存器,上述判斷由硬件完成。一般來說在Linux中,地址空間是一個顯著的標志:0xc0000000以上的地址空間只能在內核態下訪問,0xc00000000-0xbfffffff的地址空間在兩種狀態下都可以訪問。

中斷處理是從用戶態進入到內核態的主要的方式:

      也可能是用戶態程序執行的過程中調用了一個系統調用陷入了內核態當中,這個叫做trap,系統調用只是一種特殊的中斷。
      寄存器上下文:
            ——從用戶態切換到內核態的時候
                  必須保存用戶態的寄存器上下文
                  要保存哪些?
                  保存在哪里?
      中斷/int指令會在堆棧上保存一些寄存器的值
            ——如:用戶態棧頂地址、當時的狀態字、當時的cs:eip的值
      中斷發生的后的第一件事就是保護現場,保護現場就是進入中斷的程序保存需要用到的寄存器數據,恢復現場就是退出中斷程序,恢復、保存寄存器的數據。
       
 #define SAVE_ALL                                                      RESTORE_ALL
       "cld\n\t"\                                                      popl %ebx;
       "pushl %es\n\t"\                                                popl %ecx;
       "pushl %ds\n\t"\                                                popl %ebx;
       "pushl %eax\n\t"\                                               popl %edx;
       "pushl %ebp\n\t"\                                               popl %esi; 
       "pushl %edi\n\t"\                                               popl %edi; 
       "pushl %esi\n\t"\                                               popl %ebp;  
       "pushl %edx\n\t"\                                               popl %eax; 
       "pushl %ecx\n\t"\                                               popl %ds;              
       "pushl %ebx\n\t"\                                               popl %es;
       "movl $" STR(_KERNEL_DS)",%edx\n\t"\                            addl $4,%esp;
       "movl %edx,%ds\n\t"\                                            iret;
       "movl %edx,%es\n\t"

      iret指令與中斷信號(包括int指令),發生時的CPU的動作正好相反。

仔細分析一下中斷處理的完整過程:

      interrupt(ex:int0x80)-save//發生系統調用      
       cs:eip/ss:esp/ss:esp/efalgs(current)to kernel stack,then load cs:eip(entry of a specific ISR)and ss:esp(point to kernel stack) //保存了cs:eip的值,保存了堆棧寄存器當前的棧頂,當前的標志寄存器,當前的保存到內核堆棧里面,當前加載了中斷信號和系統調用相關聯的中斷服務程序的入口,把它加載到當前cs:eip的里面,同時也要把當前的esp和堆棧段也就是指向內核的信息也加載到cpu里面,這是由中斷向量或者說是int指令完成的。這個時候開始內核態的代碼。
SAVE_ALL
     -...//內核代碼,完成中斷服務,可能會發生進程調度  
    RESTOER_ALL                //完成之后再返回到原來的狀態
    iret-pop    
     cs:eip/ss:eip/eflag from kernel stack

三.系統調用概述

系統調用的意義:
      操作系統為用戶態進程與硬件設備進行交互提供了一組接口——系統調用:1.把用戶從底層的硬件編程中解放了出來;2.極大地提高了系統的安全性使用戶程序具有可移植性;用戶程序與具體硬件已經被抽象接口所替代。
操作系統提供的API和系統調用的關系:
     API(應用程序編程接口)和系統調用:應用編程接口和系統調用是不同的:1.API只是一個函數定義;2.系統調用通過軟中斷向內核發出了一個明確的請求。
     Libc庫定義的一些API引用了封裝例成,唯一目的就是發布系統調用:1.一般每個系統調用對應一個封裝例程;2.庫函數再用這些封裝例程定義出給用戶的API(把系統調用封裝成很多歌方便程序員使用的函數,不是每個API都對應一個特定的系統調用)
     API可能直接提供用戶態的服務 如:一些數學函數 1.一個單獨的API可能調用幾個系統調用2.不同的API可能調用了同一個系統調用返回:大部分封裝例程返回一個整數,其值的含義依賴於相應的系統調用-1在多數情況下表示內核不能滿足進程的請求,Libc中定義的errno變量包含特定的出錯碼;下面一張圖可以表示它們的工作過程:
    
       x,y,z就是函數,系統調用應用程序編程接口,這個應用程序編程接口里面封裝了一個系統調用,這會觸發一個0x80的一個中斷,這個中斷向量就對應着SYSTEM_CALL這個內核代碼的入口的起點,sys_xyz是對應的中斷服務程序,在中斷服務程序執行完之后,它可能會ret_from_sys_call, 之后就經過這個函數進行處理, 這是一個進程調度的時機,如果沒有發生系統調用的時機,如果沒有發生系統調用,它就會ireturn可能就會返回到用戶態接着執行。
我們要扒開系統調用的三層皮,我們講這三層皮分別是:xyz、system_call和sys_xyz
      第一個就是API、第二個就是中斷向量對應的這些也就是中斷服務程序,中斷向量對用的系統調用它有很多種不同的服務程序,比如sys_xyz,這就是三層皮。
      我們仔細看一下系統調用的服務歷程:中斷向量0x80與system_call綁定起來:
      當用戶態進程調用一個系統調用時,CPU切換到內核態並開始執行第一個內核函數
      1.在Linux中是通過執行ini $0x80來執行系統調用的,這條匯編指令產生向量為128的編程異常
      2. Intel Pentium ll中引進了sysenter指令(快速系統調用)
系統調用號將xyz和sys_xyz關聯起來了:
      傳參:
      1.內核實現了很多不同的系統調用
      2.進程必須指明需要哪些系統調用,這需要傳遞一個系統調用號的參數,使用eax寄存器
      系統調用也需要輸入輸出參數,例如: 
     1.實際的值 2.用戶態進程地址空間的變量的地址 3.甚至是包含指向用戶態函數的指針的數據結構的地址
 system_call是linux中所有系統調用的入口點,每個系統調用至少有一個參數,即由eax傳遞的系統調用號
     2.一個應用程序調用fork(0封裝例程,那么在執行int $0x80之前就把eax寄存器的值置為2(即_NR_fork)
     3.這個寄存器的設置是libc庫中封裝例程進行的,因此用戶一般不關心系統調用號
     4.進入sys_call之后,立即將eax的值壓入內核堆棧
寄存器傳遞參數有如下限制:
     1.每個參數的長度不能超過寄存器的長度,即32位
     2.在系統調用號eax之外,參數的個數不能超過6個(ebx,ecx,edx,esi,edi,ebp)
     超過6個怎么辦?做一個把某個寄存器作為指針,指向一塊內存,這樣進入內核態之后可以訪問所有內存空間,這就是系統調用的參數傳遞方式。
 四.庫函數API和C代碼中嵌入匯編代碼兩種方式系統調用
 首先選擇一個系統調用,我選的是write,然后是用c語言寫一段正常熟悉的系統調用代碼,如下:
#include<stdio.h>
#include<unistd.h>
int main(void)
{
    write(1,"hello world!5124\n",13);
    return 0;
 }

   下面是我的命令行內容:

      其中,write有三個參數,第一個是表示寫到終端屏幕上,1可以認為是屏幕的代號,第二個參數是寫的內容,我是把hello world!寫到屏幕上,並換行,第三個參數是寫入的字符串長度,長度要大於等於要輸出的字符串長度,否則只能輸出字符串的一部分。程序執行結果如下:

然后是把這段代碼轉寫為嵌入式匯編,嵌入式匯編的格式如下:
_asm_(
     匯編語句模塊:
     輸出部分:函數調用時候的參數
     輸入部分:函數調用時候的參數
     破壞描述部分):
     即格式為asm("statements":output_regs:input_regs:clobbered_regs);
可以看成是一個函數,有時候可以加一個_volatile_來選擇讓編譯器優化或者不讓編譯器優化。

代碼如下:

#include<stdio.h>
#include<unistd.h>
 int main()
{
   int a;
   char *ch="hello world!\n";
 
    asm volatile(
        "movl $0x4,%%eax\n\t"
        "movl $0x1,%%ebx\n\t"
        "movl $0x1,%%ecx\n\t"
        "movl $0xd,%%edx\n\t"
        "int $0x80\n\t"
        "movl %%eax,%0\n\t"
        :"=m"(a)
        :"s"(ch)
        );
      return 0;
 }

 write系統調用有三個參數,分別是:寫入的位置,內容和長度,所以轉化為匯編對應的寄存器為eax(系統調用號為4),ebx(參數),ecx(輸出位置),edx(參數長度)

執行代碼如下:
五.實驗感想
       計算機科學中有一句話,任何計算機相關問題都可以通過加一個中間層來解決。操作系統的系統調用也是這樣,system_call將api和系統函數連接起來,這樣可以保證內核的安全,不會因為用戶的失誤操作而造成問題。操作系統為了安全,把一些重要的調用放在內核部分,這樣只能通過觸發系統調用來完成相應功能,這樣可以保證內核的安全,但是不可避免的也造成了系統調用的消耗比較大。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM