以下只是個人看了《linux內核完全注釋》的一點理解,如果有錯誤,歡迎指正!
1 eip中保存的地址是邏輯地址、線性地址還是物理地址?
這個應該要分情況。eip保存的是下一條要執行的指令地址,也就是說cpu是根據eip到內存中去尋找指定的內容。如果cpu工作在實模式,那么eip保存的就是物理地址;如果cpu工作在保護模式下,那么cpu在去內存尋找指定的內容之前要先將eip加上當前程序代碼段的基址(通過當前cs所指向的代碼段描述符獲得),即獲得當前程序的線性地址,如果cpu沒有開啟分頁機制,那么這個線性地址就是實際的物理地址了;如果cpu開啟了分頁機制,那么就要通過線性地址查找頁目錄表和頁表來獲得實際的物理地址。所以如果cpu在保護模式下,不管有沒有開啟分頁機制,eip保存的都是程序的邏輯地址。
2 每個進程有幾個堆棧?
每個進程都有兩個堆棧,一個是工作在用戶態的用戶堆棧,一個是工作在內核態的內核堆棧。內核堆棧只有一頁,即4k,該頁的低地址保存了任務結構。
3 進程的狀態是怎么變化的?
一般發生硬件中斷,程序異常,程序執行系統調用,cpu會將當前執行的程序轉換為內核狀態去執行中斷處理程序,並且使用內核堆棧。系統調用是進程讓自己主動轉換為內核狀態的唯一方法,而硬件中斷和程序異常是cpu強制將當前進程轉換為內核態去執行中斷處理程序。linux0.12是通過將系統調用0x80設置為DPL為3的陷阱門,而硬件中斷和程序異常都被設置為DPL為0的中斷門來實現的。
4 任務是怎樣被切換的?
是通過ljmp指令實現的,如果ljmp的操作數是GDT表(全局描述符表)中某個任務的任務段描述符或者IDT表(中斷描述符表)中的任務門描述符,那么cpu會切換去執行這個新任務,在切換前會將cpu中各種寄存器的狀態存放到被切換出去的任務的任務段中。並把該新任務任務段中的信息恢復到cpu的各個寄存器中。
5 進程為什么會被切換?
讓進程切換有幾個原因,一個是進程的時間片用完了,時間中斷處理程序會在每次被調用的時候檢查當前進程的時間片是否用戶,如果用完了就切換當前進程;一個是進程通過調用系統調用(如pause,wait等)讓出cpu,即程序為了等待資源而主動讓出cpu。
6 信號處理程序是什么時候被調用的?
每次中斷處理程序(包括時鍾中斷,系統調用等)結束之前都會去檢查當前程序的信號位圖,如果某個信號位圖相應的位為1,並且該信號未被阻塞,則通過調用do_signal中斷程序來設置內核態堆棧(通過修改保存在堆棧中的用戶態eip來指向信號處理程序)和用戶態堆棧(保存調用系統調用的下一條指令地址,信號處理程序參數等),使得在系統調用返回后能馬上去執行信號處理程序而不是執行調用系統調用的程序的下一條指令。
7 等待信號發生的程序是怎樣被喚醒的?
每次調度程序schedule被調用的時候,都會去檢查系統中所有任務,如果該任務的狀態為可中斷的等待,並且該任務有未阻塞的信號到達,則將該任務的狀態設置為可運行狀態等待被調度。
8 每個進程的線性地址是怎么計算的?
每個進程都有64M的地址空間,每個進程都有自己的頁表,但都共用存放在物理地址為0的頁目錄表,每個進程在頁目錄表中有16項。頁目錄表占一個內存頁,即4k,每個目錄項為4B,所以目錄表中對多有1024項,即系統總共可以運行64個進程。每個進程的段基址可以通過該進程的任務號nr乘以64M得到,段基址加上邏輯地址即得到進程的線性地址。
9 內核態的進程是否可以訪問所有的內存空間?
linux0.12能訪問的內存最大為16M,head.s程序將4個4k的頁表放在頁目錄表的后面,並且該四個頁表占用頁目錄的前四項,也即內核程序最多為16M。並且將GDT表中的代碼段和數據段的段基址設置為0,也就是說此時內核程序的邏輯地址和物理地址是一樣的,所以內核程序都能訪問16M的物理地址,即所有的內存空間。
10 處於內核態的進程是否會發生任務切換?
不會,即使進程的時間片用完了,當然如果進程自己主動讓出cpu,那也是會切換任務的。
11 進程是怎樣主動讓出cpu的?
不管進程處在用戶態還是內核態,只要進程調用interruptible_sleep_on或者sleep_on函數,進程就會主動讓出cpu。interruptible_sleep_on和sleep_on函數都是調用__sleep_on函數,__sleep_on函數輸入參數是某等待隊列的頭指針(這個等待隊列是task_struct結構的,用於存儲等待同一類資源的所有任務指針,至於要傳入等待隊列頭指針的原因是一般程序主動讓出cpu是由於所需的資源當前不可用)。值得注意的是如果等待同一資源的進程調用__sleep_on函數,表示等待該資源的等待隊列的頭指針會指向這個新的進程,而在__sleep_on函數中有一個task_struct結構的指針temp指向上一個等待該資源的進程,這樣就形成了一個由task_struct結構組成的鏈表等待隊列。當等待隊列中某個進程獲得cpu時,如果它不是等待隊列的頭,該進程會喚醒等待隊列的頭運行,而自己則繼續等待,重復這一過程,直到等待隊列的頭是當前進程。

圖1 buffer_wait等待隊列
12 linux緩沖區管理的原理是什么?
linux在內存中內核代碼的結束后面放了一定大小的緩沖區(大小根據內存實際大小安排)。初始化緩沖區時,從緩沖區頭和尾同時進行,緩沖區頭存放緩沖塊頭結構,緩沖區尾存放實際的緩存塊,緩沖塊大小和實際磁盤塊大小一樣,為1K,一個緩沖區頭管理一個緩沖區塊。所有緩存塊的頭被鏈接成一個雙向鏈表結構,如圖2。free_list是指向空閑緩沖塊鏈表的頭,free_list的b_prev_free指向指向空閑緩沖塊頭鏈表的尾,而緩沖塊頭鏈表的尾則指向緩沖塊鏈表的頭,這樣空閑緩沖塊鏈表就形成了一個循環雙鏈表,如圖3。

圖2 緩沖區初始化

圖2 空閑緩沖塊雙向循環鏈表
緩沖塊頭結構如下:
struct buffer_head { char * b_data; /* pointer to data block (1024 bytes) */ unsigned long b_blocknr; /* block number */ unsigned short b_dev; /* device (0 = free) */ unsigned char b_uptodate; unsigned char b_dirt; /* 0-clean,1-dirty */ unsigned char b_count; /* users using this block */ unsigned char b_lock; /* 0 - ok, 1 -locked */ struct task_struct * b_wait; struct buffer_head * b_prev; struct buffer_head * b_next; struct buffer_head * b_prev_free; struct buffer_head * b_next_free; };
b_data是緩沖塊地址;b_blocknr和b_dev表示緩沖塊中存放的是哪個設備的幾號塊;b_uptodate表示當前緩沖塊是否有效,為1是有效,為0是無效;b_dir表示當前緩存塊是否和磁盤塊的數據一致,為1表示不一致,為0表示一致;b_count表示引用當前緩沖塊的進程數;b_lock表示當前緩沖塊是否被鎖定(一般緩沖塊與實際物理磁盤傳輸數據時,緩沖塊是被鎖定的);b_wait指向等待當前緩沖塊的任務隊列的頭;b_prev和b_next主要用於查找緩沖塊,具體見下面;b_prev_free和b_next_free用於形成空閑緩沖塊雙向循環鏈表。
為了快速而有效地在緩沖區尋找並判斷出請求的數據塊已經被讀入緩沖區中,將每個已經讀入緩沖區的數據塊加入到一個hash鏈表中,每個hash鏈表的頭指針存放在hash數組中,通過b_blocknr和b_dev共同決定將數據塊放到哪個hash鏈中,如圖3所示。

圖3 hash緩沖鏈
從圖中可以看出每個hash鏈都是一個雙向鏈表,通過b_prev和b_nex來實現。如果要判斷某個數據塊是否在緩沖區中, 通過b_blocknr和b_dev直接找到對應的hash鏈表,在鏈表中查看是否有b_blocknr和b_dev一致的數據塊就可以判斷數據塊是否在緩沖區中了。為了實現緩沖區滿了要替換出最近最久未使用(LRU)的緩沖塊,每次都將新分配的緩沖塊放到free_list的最后面,而每次找空閑緩存塊則從free_list最前面開始找,這樣就實現LRU了。
