1. 程式人生 > >linux 用戶態和內核態以及進程上下文、中斷上下文 內核空間用戶空間理解

linux 用戶態和內核態以及進程上下文、中斷上下文 內核空間用戶空間理解

內存映射 自旋鎖 復制。 系統性能 準備 emc 進入 就會 運行環境

技術分享圖片 1、特權級 Intel x86架構的cpu一共有0~4四個特權級,0級最高,3級最低,ARM架構也有不同的特權級,硬件上在執行每條指令時都會對指令所具有的特權級做相應的檢查。硬件已經提供了一套特權級使用的相關機制,軟件自然要好好利用,這屬於操作系統要做的事情,對於UNIX/LINUX來說,只使用了0級特權級別和3級特權級,即最高最低特權級。也就是說在UNIX/LINUX系統中,一條工作在0級特權級的指令具有了CPU能提供的最高權力,而一條工作在3級特權的指令具有CPU提供的最低或者說最基本權力 以上是從cpu執行指令角度理解特權,其實虛擬地址到物理地址映射由mmu硬件實現,即分頁機制是硬件對分頁的支持,進程中有頁表數據結構指向用戶空間和內核空間,使用戶態和內核態訪問內存空間不同。 2、用戶態和內核態 內核棧:Linux中每個進程有兩個棧,分別用於用戶態和內核態的進程執行,其中的內核棧就是用於內核態的堆棧,它和進程的task_struct結構,更具體的是thread_info結構一起放在兩個連續的頁框大小的空間內。 現在我們從特權級的調度來理解用戶態和內核態就比較好理解了,當程序運行在3級特權級上時,就可以稱之為運行在用戶態,因為這是最低特權級,是普通的用戶進程運行的特權級,大部分用戶直接面對的程序都是運行在用戶態;反之,當程序運行在0級特權級上時,就可以稱之為運行在內核態。 雖然用戶態下和內核態下工作的程序有很多差別,但最重要的差別就在於特權級的不同,即權力的不同。運行在用戶態的程序不能訪問操作系統內核數據結構合程序。 當我們在系統中執行一個程序時,大部分時間是運行在用戶態下的。在其需要操作系統幫助完成某些它沒有權力和能力完成的工作時就會切換到內核態。 Linux進程的4GB地址空間,3G-4G部分大家是共享的,是內核態的地址空間,這裏存放在整個內核的代碼和所有的內核模塊,以及內核所維護的數據。用戶運行一個程序,該程序所創建的進程開始是運行在用戶態的,如果要執行文件操作,網絡數據發送等操作,必須通過write,send等系統調用,這些系統調用會調用內核中的代碼來完成操作,這時,必須切換到Ring0,然後進入3GB-4GB中的內核地址空間去執行這些代碼完成操作,完成後,切換回Ring3,回到用戶態。這樣,用戶態的程序就不能隨意操作內核地址空間,具有一定的安全保護作用。
保護模式,通過內存頁表操作等機制,保證進程間的地址空間不會互相沖突,一個進程的操作不會修改另一個進程的地址空間中的數據。在內核態下,CPU可執行任何指令,在用戶態下CPU只能執行非特權指令。當CPU處於內核態,可以隨意進入用戶態;而當CPU處於用戶態,只能通過中斷的方式進入內核態。一般程序一開始都是運行於用戶態,當程序需要使用系統資源時,就必須通過調用軟中斷進入內核態.

處理器總處於以下狀態中的一種:

1、內核態,運行於進程上下文,內核代表進程運行於內核空間;

2、內核態,運行於中斷上下文,內核代表硬件運行於內核空間;

3、用戶態,運行於用戶空間。

3、用戶態和內核態的轉換 1)用戶態切換到內核態的3種方式 a. 系統調用 這是用戶態進程主動要求切換到內核態的一種方式,用戶態進程通過系統調用申請使用操作系統提供的服務程序完成工作。而系統調用的機制,其核心還是使用了操作系統為用戶特別開放的一個中斷來實現,例如lx86的int 80h, powerpc的sc b. 異常 當CPU在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關的程序中,也就是轉到了內核態,比如缺頁異常。 c. 外圍設備的中斷 當外圍設備完成用戶請求的操作後,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那麽這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作的完成,系統會切換到硬盤讀寫的中斷處理程序中執行後續操作等。 這3種方式是系統在運行時由用戶態轉到內核態的最主要方式,其中系統調用可以認為是用戶進程主動發起的,異常和外圍中斷是被動的。 4、具體的切換操作 從觸發方式上看,可以認為純在前述3種不同的類型,但是從最終實際完成由用戶態到內核態的切換操作上來說,涉及的關鍵步驟是完全一致的,沒有任何區別,都相當於執行了一個中斷響應的過程
,因為系統調用實際上最終是中斷機制實現的,而異常和中斷的處理機制基本上也是一致的。關於中斷處理機制的細節合步驟這裏不做過多分析,涉及到有用戶態切換到內核態的步驟主要包括: 【1】從當前進程的描述符中提取其內核棧的ss0及esp0信息 【2】使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個過程也完成了由用戶棧到內核棧的切換過程,同時保存了被暫停執行的程序的下一條指令。 【3】將先前又中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始執行中斷處理程序,這時就轉到內核態的程序執行了。 5、下面介紹進程上下文以及中斷上下文 這是在內核態和用戶態的子概念

內核空間和用戶空間是操作系統重要的理論知識,用戶程序運行在用戶空間,內核功能模塊運行在內核空間,二者是空間是不能互相訪問的,內核空間和用戶空間指其代碼和數據存放內存空間。用戶態的程序要想訪問內核空間,須使用系統調用。當用戶空間的應用程序通過系統調用進入內核空間時,就會涉及到上下文的切換。用戶空間和內核空間具有不同的地址映射、通用寄存器和專用寄存器組以及堆棧區,而且用戶空間的進程要傳遞很多變量、參數給內核,內核也要保存用戶進程的一些寄存器、變量等,以便系統調用結束後回到用戶空間繼續執行。

所謂的進程上下文,就是一個進程傳遞給內核的那些參數和CPU的所有寄存器的值、進程的狀態以及堆棧中的內容,也就進程在進入內核態之前的運行環境。所以在切換到內核態時需要保存當前進程的所有狀態,即保存當前進程的上下文,以便再次執行該進程時,能夠恢復切換時的狀態,繼續執行。同理,硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間。這個過程中,硬件的一些變量和參數也要傳遞給內核,內核通過這些參數進行中斷處理,中斷上下文就可以理解為硬件傳遞過來的這些參數和內核需要保存的一些環境(主要是被中斷的進程的環境)。

當一個進程在執行時,CPU的所有寄存器中的值、進程的狀態以及堆棧中的內容被稱為該進程的上下文。當內核需要切換到另一個進程時,它需要保存當前進程的所有狀態,即保存當前進程的上下文,以便在再次執行該進程時,能夠必得到切換時的狀態執行下去。在LINUX中,當前進程上下文均保存在進程的任務數據結構中。在發生中斷時,內核就在被中斷進程的上下文中,在內核態下執行中斷服務例程。但同時會保留所有需要用到的資源,以便中繼服務結束時能恢復被中斷進程的執行。

上下文簡單說來就是一個環境,相對於進程而言,就是進程執行時的環境。相對於中斷而言就是中斷執行時的環境。

一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。

(1)用戶級上下文: 正文、數據、用戶堆棧以及共享存儲區;
(2)寄存器上下文: 通用寄存器、程序寄存器(IP)、處理器狀態寄存器(EFLAGS)、棧指針(ESP);
(3)系統級上下文: 進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。

進程上下文切換分為進程調度時和系統調用時兩種切換,消耗資源不同,當發生進程調度時,進行進程切換就是上下文切換(context switch).操作系統必須對上面提到的全部信息進行切換,新調度的進程才能運行。而系統調用進行的模式切換(mode switch)與進程切換比較起來,容易很多,而且節省時間,因為模式切換最主要的任務只是切換進程寄存器上下文的切換。在進程上下文中,可以用current宏關聯當前進程,也可以睡眠,也可以調用調度程序。



中斷上下文不支持搶占,運行在進程上下文的內核代碼是可以被搶占的(Linux2.6支持搶占),就是支持進程調度。但是一個中斷上下文,通常都會始終占有CPU(當然中斷可以嵌套,但我們一般不這樣做),不可以被打斷。正因為如此,運行在中斷上下文的代碼就要受一些限制,不能做下面的事情:

(1)睡眠或者放棄CPU。

這樣做的後果是災難性的,因為內核在進入中斷之前會關閉進程調度,一旦睡眠或者放棄CPU,這時內核無法調度別的進程來執行,系統就會死掉

(2)嘗試獲得信號量、執行自旋鎖

如果獲得不到信號量,代碼就會睡眠,會產生和上面相同的情況

(3)執行耗時的任務

中斷處理應該盡可能快,因為內核要響應大量服務和請求,中斷上下文占用CPU時間太長會嚴重影響系統功能。

(4)訪問用戶空間的虛擬地址

因為中斷上下文是和特定進程無關的,它是內核代表硬件運行在內核空間,所以在中斷上下文無法訪問用戶空間的虛擬地址

6、下面介紹用戶空間以及內核空間概念

用戶空間與內核空間  

  我們知道現在操作系統都是采用虛擬存儲器,那麽對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操心系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證用戶進程不能直接操作內核,保證內核的安全,操心系統將虛擬空間劃分為兩部分,一部分為內核空間,一部分為用戶空間。針對linux操作系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱為內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。每個進程可以通過系統調用進入內核,因此,Linux內核由系統內的所有進程共享。於是,從具體進程的角度來看,每個進程可以擁有4G字節的虛擬空間。空間分配如下圖所示:

技術分享圖片

  有了用戶空間和內核空間,整個linux內部結構可以分為三部分,從最底層到最上層依次是:硬件-->內核空間-->用戶空間。如下圖所示:

技術分享圖片

  需要註意的細節問題:

從上圖可以看出內核的組成

(1) 內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處於虛擬空間中。

(2) Linux使用兩級保護機制:0級供內核使用,3級供用戶程序使用。

為什麽不把所有的地址空間都分配給內核?

若把所有地址空間都給內存,那麽用戶進程怎麽使用內存?怎麽保證內核使用內存和用戶進程不起沖突?


(1)讓我們忽略Linux對段式內存映射的支持。 在保護模式下,我們知道無論CPU運行於用戶態還是核心態,CPU執行程序所訪問的地址都是虛擬地址,MMU 必須通過讀取控制寄存器CR3中的值作為當前頁面目錄的指針,進而根據分頁內存映射機制(參看相關文檔)將該虛擬地址轉換為真正的物理地址才能讓CPU真 正的訪問到物理地址。

(2)對於32位的Linux,其每一個進程都有4G的尋址空間,但當一個進程訪問其虛擬內存空間中的某個地址時又是怎樣實現不與其它進程的虛擬空間混淆 的呢?每個進程都有其自身的頁面目錄PGD,Linux將該目錄的指針存放在與進程對應的內存結構task_struct.(struct mm_struct)mm->pgd中。每當一個進程被調度(schedule())即將進入運行態時,Linux內核都要用該進程的PGD指針設 置CR3(switch_mm())。

(3)當創建一個新的進程時,都要為新進程創建一個新的頁面目錄PGD,並從內核的頁面目錄swapper_pg_dir中復制內核區間頁面目錄項至新建進程頁面目錄PGD的相應位置,具體過程如下:
do_fork() --> copy_mm() --> mm_init() --> pgd_alloc() --> set_pgd_fast() --> get_pgd_slow() --> memcpy(&PGD + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t))
這樣一來,每個進程的頁面目錄就分成了兩部分,第一部分為“用戶空間”,用來映射其整個進程空間(0x0000 0000-0xBFFF FFFF)即3G字節的虛擬地址;第二部分為“系統空間”,用來映射(0xC000 0000-0xFFFF FFFF)1G字節的虛擬地址。可以看出Linux系統中每個進程的頁面目錄的第二部分是相同的,所以從進程的角度來看,每個進程有4G字節的虛擬空間, 較低的3G字節是自己的用戶空間,最高的1G字節則為與所有進程以及內核共享的系統空間。

(4)現在假設我們有如下一個情景:
在進程A中通過系統調用sethostname(const char *name,seze_t len)設置計算機在網絡中的“主機名”.
在該情景中我們勢必涉及到從用戶空間向內核空間傳遞數據的問題,name是用戶空間中的地址,它要通過系統調用設置到內核中的某個地址中。讓我們看看這個 過程中的一些細節問題:系統調用的具體實現是將系統調用的參數依次存入寄存器ebx,ecx,edx,esi,edi(最多5個參數,該情景有兩個 name和len),接著將系統調用號存入寄存器eax,然後通過中斷指令“int 80”使進程A進入系統空間。由於進程的CPU運行級別小於等於為系統調用設置的陷阱門的準入級別3,所以可以暢通無阻的進入系統空間去執行為int 80設置的函數指針system_call()。由於system_call()屬於內核空間,其運行級別DPL為0,CPU要將堆棧切換到內核堆棧,即 進程A的系統空間堆棧。我們知道內核為新建進程創建task_struct結構時,共分配了兩個連續的頁面,即8K的大小,並將底部約1k的大小用於 task_struct(如#define alloc_task_struct() ((struct task_struct *) __get_free_pages(GFP_KERNEL,1))),而其余部分內存用於系統空間的堆棧空間,即當從用戶空間轉入系統空間時,堆棧指針 esp變成了(alloc_task_struct()+8192),這也是為什麽系統空間通常用宏定義current(參看其實現)獲取當前進程的 task_struct地址的原因。每次在進程從用戶空間進入系統空間之初,系統堆棧就已經被依次壓入用戶堆棧SS、用戶堆棧指針ESP、EFLAGS、 用戶空間CS、EIP,接著system_call()將eax壓入,再接著調用SAVE_ALL依次壓入ES、DS、EAX、EBP、EDI、ESI、 EDX、ECX、EBX,然後調用sys_call_table+4*%EAX,本情景為sys_sethostname()。

(5)在sys_sethostname()中,經過一些保護考慮後,調用copy_from_user(to,from,n),其中to指向內核空間 system_utsname.nodename,譬如0xE625A000,from指向用戶空間譬如0x8010FE00。現在進程A進入了內核,在 系統空間中運行,MMU根據其PGD將虛擬地址完成到物理地址的映射,最終完成從用戶空間到系統空間數據的復制。準備復制之前內核先要確定用戶空間地址和 長度的合法性,至於從該用戶空間地址開始的某個長度的整個區間是否已經映射並不去檢查,如果區間內某個地址未映射或讀寫權限等問題出現時,則視為壞地址, 就產生一個頁面異常,讓頁面異常服務程序處理。過程如 下:copy_from_user()->generic_copy_from_user()->access_ok()+__copy_user_zeroing().

(6)小結:
*進程尋址空間0~4G
*進程在用戶態只能訪問0~3G,只有進入內核態才能訪問3G~4G
*進程通過系統調用進入內核態
*每個進程虛擬空間的3G~4G部分是相同的
*進程從用戶態進入內核態不會引起CR3的改變但會引起堆棧的改變 7、內存管理單元(MMU)介紹:其是實現虛擬地址和物理地址空間以及內核空間、用戶空間的基礎

MMU是存儲器管理單元的縮寫,是用來管理虛擬內存系統的器件。MMU通常是CPU的一部分,本身有少量存儲空間存放從虛擬地址到物理地址的匹配表,一種轉換方法(算法)。此表稱作TLB(轉換旁置緩沖區)。所有數據請求都送往MMU,由MMU決定數據是在RAM內還是在大容量存儲器設備內。如果數據不在存儲空間內,MMU將產生頁面錯誤中斷,外部存儲器地址空間由頁、行、列組成。

MMU的兩個主要功能是:

1. 將虛地址轉換成物理地址。

2. 控制存儲器存取允許。MMU關掉時,虛地址直接輸出到物理地址總線:比如uboot前部分。

在實踐中,使用MMU解決了如下幾個問題:

①使用DRAM作為大容量存儲器時,如果DRAM的物理地址不連續,這將給程序的編寫調試造成極大不便,而適當配置MMU可將其轉換成虛擬地址連續的空間,將不連續的物理空間變為連續的虛擬地址空間。

②ARM內核的中斷向量表要求放在0地址,對於ROM在0地址的情況,無法調試中斷服務程序,所以在調試階段有必要將可讀寫的存儲器空間映射到0地址。

③系統的某些地址段是不允許被訪問的,否則會產生不可預料的後果,為了避免這類錯誤,可以通過MMU匹配表的設置將這些地址段設為用戶不可存取類型,即內核空間和用戶空間區別。

啟動程序中生成的匹配表中包含地址映射,存儲頁大小(1M,64K,或4K)以及是否允許存取等信息,這是實現上述功能基礎。

例如:目標板上的16兆DRAM的物理地址區間為0xc000,0000~0xc07f,ffff,;0xc100,0000~0xc17f,ffff;這些地址都是邏輯地址即一個地址一個8位數據,16兆ROM的虛擬地址區間為:0x0000,0000~0x00ff,ffff。匹配表配置(匹配表輸入有虛擬地址 頁大小 是否允許存取 頁表)如下:

可以看到左邊是連續的虛擬地址空間,右邊是不連續的物理地址空間,而且將DRAM映射到了0地址區間。 MMU通過虛擬地址和頁面表位置信息,按照轉換邏輯獲得對應物理地址,輸出到地址總線上。

應註意到的是使能MMU後,程序繼續運行,但是對於程序員來說程序計數器的指針已經改變,指向了ROM所對應的虛擬地址。

MMU的作用有兩個

MMU的作用有兩個:地址翻譯和地址保護 軟件的職責是配置頁表,硬件的職責是根據頁表完成地址翻譯和保護工作。 那三個函數是用來訪問頁表的。如果cpu沒有硬件MMU那麽這張表將毫無意義。 你必須從cpu的角度去理解內存映射這個概念。內存映射不是調用一個函數,然後讀取返回值。而是cpu通過MMU把一條指令中要訪問的地址轉換為物理地址,然後發送到總線上的過程。 有本書叫做understand linux kernel,耐心看,那本書寫的非常好。

MMU是處理器復雜到一定程度出現的產物。這個東西和操作系統的內存管理如果結合起來學習和理解,效果最好。

嵌入式系統中,存儲系統差別很大,可包含多種類型的存儲器件,如FLASH,SRAM,SDRAM,ROM等,這些不同類型的存儲器件速度和寬度等各不相同;在訪問存儲單元時,可能采取平板式的地址映射機制對其操作,或需要使用虛擬地址對其進行讀寫;系統中,需引入存儲保護機制,增強系統的安全性。為適應如此復雜的存儲體系要求,ARM處理器中引入了存儲管理單元來管理存儲系統,這是mmu意義。

一 內存管理單元(MMU)概述

在ARM存儲系統中,使用MMU實現虛擬地址到實際物理地址的映射。為何要實現這種映射?首先就要從一個嵌入式系統的基本構成和運行方式著手。系統上電時,處理器的程序指針從0x0(或者是由0Xffff_0000處高端啟動)處啟動,順序執行程序,在程序指針(PC)啟動地址,屬於非易失性存儲器空間範圍,如ROM、FLASH等。然而與上百兆的嵌入式處理器相比,FLASH、ROM等存儲器響應速度慢,已成為提高系統性能的一個瓶頸。而SDRAM具有很高的響應速度,為何不使用SDRAM來執行程序呢?為了提高系統整體速度,可以這樣設想,利用FLASH、ROM對系統進行配置,把真正的應用程序下載到SDRAM中運行,這樣就可以提高系統的性能。然而這種想法又遇到了另外一個問題,當ARM處理器響應異常事件時,程序指針將要跳轉到一個確定的位置,假設發生了IRQ中斷,PC將指向0x18(如果為高端啟動,則相應指向0vxffff_0018處),而此時0x18處仍為非易失性存儲器所占據的位置,則程序的執行還是有一部分要在FLASH或者ROM中來執行的。那麽我們可不可以使程序完全都SDRAM中運行那?答案是肯定的,這就引入了MMU,利用MMU,可把SDRAM的地址完全映射到0x0起始的一片連續地址空間,而把原來占據這片空間的FLASH或者ROM映射到其它不相沖突的存儲空間位置。例如,FLASH的地址從0x0000_0000-0x00ff_ffff,而SDRAM的地址範圍是0x3000_0000-0x31ff_ffff,則可把SDRAM地址映射為0x0000_0000-0x1fff_ffff而FLASH的地址可以映射到0x9000_0000-0x90ff_ffff(此處地址空間為空閑,未被占用)。映射完成後,如果處理器發生異常,假設依然為IRQ中斷,PC指針指向0x18處的地址,而這個時候PC實際上是從位於物理地址的0x3000_0018處讀取指令。通過MMU的映射,則可實現程序完全運行在SDRAM之中,這些物理器件的地址是實際的掛載到總線上的地址。

在實際的應用中,可能會把兩片不連續的物理地址空間分配給SDRAM。而在操作系統中,習慣於把SDRAM的空間連續起來,方便內存管理,且應用程序申請大塊的內存時,操作系統內核也可方便地分配。通過MMU可實現不連續的物理地址空間映射為連續的虛擬地址空間。

操作系統內核或者一些比較關鍵的代碼,一般是不希望被用戶應用程序所訪問的。通過MMU可以控制地址空間的訪問權限,從而保護這些代碼不被破壞,內核空間與用戶空間區別。

linux 用戶態和內核態以及進程上下文、中斷上下文 內核空間用戶空間理解