1. 程式人生 > >LINUX0.11 核心閱讀筆記

LINUX0.11 核心閱讀筆記

一.原始碼目錄

圖1

二.系統總體流程:

系統從boot開始動作,把核心從啟動盤裝到正確的位置,進行一些基本的初始化,如檢測記憶體,保護模式相關,建立頁目錄和記憶體頁表,GDT表,IDT表。然後進入main進行初始化設定,main完成系統各個模組要用到的所有資料結構和外部裝置的初始化。使得系統可以正常的工作。然後才進入使用者模式。執行第一個fork生成程序1執行init,執行shell,接受並執行使用者命令.

這裡整個系統建立起來了,OS就處於被動狀態,靠中斷和系統呼叫來完成每一項服務。

三.各個目錄的閱讀總結:

(一) boot

1.bootsect.s :

bootsect.s編譯結果生成一個512BYTE(一個扇區)映象。這個扇區的最後一個字是0xAA55,倒數第二個字是root_dev變數,值為ROOT_DEV(306),即根檔案系統所在的裝置號。這段程式碼必須寫入到啟動盤的啟動扇區,也就第一個物理扇區上。這樣機器啟動後,BIOS自動把它載入到7C00H處並跳到那裡開始執行。bootsect將自己移動到90000H(576K)處,並跳至那裡相應位置執行。然後利用BIOS中斷將setup直接載入到自己的後面(90200h)(576.5K處),並將system載入到地址10000h處。 跳到setup中執行。

2setup.s

利用BIOS中斷把系統引數如顯示卡,硬碟引數儲存到記憶體90000開始的位置,即覆蓋原bootsect所在的記憶體位置。再把整個system 模組移動到00000 位置。載入GDTR和IDTR,這裡的GDT表是臨時的,儲存了兩個描述符,即核心程式碼、核心資料段描述符,其段基地址為0。而載入IDT除了進入保護模式需要載入IDTR之外,沒有任何意義。開啟A20 地址線開啟擴充套件記憶體。重新設定8259中斷碼0x20~0x2f。進入保護模式(PE置1)跳轉到system模組中的head.s中(0處)執行。Bootsect.s和setup.s執行時記憶體變化情況。

<!--[if !vml]--><!--[endif]-->

圖2

3head.s

前4KB的程式碼將被頁目錄覆蓋掉。這些程式碼執行的操作包括:設定系統堆疊為_stack_start。重新設定GDTR和IDTR。gdt,idt表都定義在head.s的末端,長度均為256項(2KBYTE)。第2個頁面到第5個頁面是系統的4張頁表。最後一個頁表後面的程式碼執行分頁操作,即填充的4個頁目錄和4張頁表的內容,實現對等對映,即實體地址=線性地址。每個頁表項屬性為存在並且使用者可讀寫。設定好後置CR3頁目錄地址即0。啟動分頁標誌,CR0的PG標誌置1。跳到main函式中執行。

跳到main之前,記憶體佈局如下:從0到16M

                    頁目錄4K(0x0開始)

                    頁表1 4K

                    頁表2 4K

                    頁表3 4K

                    頁表4 4K

                     軟盤緩衝區1K

                     head.s後半部分程式碼

                     IDT表2K

                     GDT表2K

                     main.o程式碼部分

                  核心其餘部分(大約到512K,end值為結束地址)

                  setup儲存的系統引數(90000H~900200)這個區間還儲存著root_dev.

                  BIOS(640K-1M)

                     主記憶體區(1M-16M)

現在初始化好了核心工作依賴的主要的資料結構是GDT和IDT表,還有頁表。

(二)核心初始化init

main.c將進行進一步初始化工作。主要方面:分配主記憶體功能,IDT表各中斷描述符重新設定,對核心其它模組如mm,fs進行初始化,然後移到使用者模式下生成程序1執行init,常駐程序0死迴圈執行pause。程序init載入根檔案系統,設定終端標準IO,建立程序2以/etc/rc為標準輸入檔案執行shell.完成rc檔案中的命令。

init等程序2退出,進入死迴圈:建立子程序,建立新會話,設定標準IO終端,以登入方式執行shell.

至此係統動作起來了。

所以整個系統的建立起來後除了兩個死迴圈的程序idle和init,其它的動作都是由使用者在shell下執行命令,產生系統呼叫來工作的。

通過執行move_to_usermdoe(),idle和init程序都屬於使用者態下的程序。而核心則完全是中斷驅動的。也就是說只有通過中斷才能進入系統,如時鐘和系統呼叫等。

所以問題的重點就在於核心各部分資料結構的建立、初始化、操作是怎樣進行的。這些初始化流程涉及到核心各個模組全部重要的資料結構。

現在從main執行的一系列初始化程式碼來淺窺一下:

1.根據記憶體的大小,設定高速緩衝的末端。16M記憶體把高速緩衝末端設為4M。緩衝末端到主存末端為主記憶體區。

2.mem_init(main_memory_start,memory_end);主記憶體區初始化

 設定高階記憶體HIGH_MEMORY=memory_end,

 設定記憶體對映位元組圖mem_map [ PAGING_PAGES ],將不可用的全部置為USED,可用的置為0。mem_map陣列是系統mm模組核心資料結構,記載了每個記憶體頁使用計數。

3.trap_init().硬體中斷向量表設定。

  向IDT中填充各個中斷描述符,使其指向對應的中斷處理程式。對於錯誤,基本是結束當前程序。其他如外設中斷都是各個模組初始化的時候向IDT表中相應項進行設定。

4.blk_dev_init();     // 塊裝置初始化。

       初始化請求陣列request[],將所有請求項置為空閒項(dev = -1)。

5.chr_dev_init();    // 字元裝置初始化。尚為空操作。

6.tty_init();            // tty 初始化。                    

       /// tty 終端初始化函式。                                                 

       // 初始化串列埠終端和控制檯終端。                                          

       void tty_init (void)                                                     

       { 

              rs_init ();                   // 初始化序列中斷程式和序列介面1 和2。(serial.c, 37)   

              con_init ();                   // 初始化控制檯終端。(console.c, 617)                

       }

       rs_init 初始化兩個串列埠,安裝串列埠中斷處理IDT項。

       con_init 初始化顯示器和鍵盤。安裝鍵盤中斷處理IDT項。                                                                 

7.time_init().取CMOS 時鐘,並設定開機時間 startup_time(為從1970-1-1-0 時起到開機時的秒數)

8.sched_init(); // 排程程式初始化(載入了任務0 的tr, ldtr)

 這裡初始化與程序排程有關的資料結構。

 手工設定了任務0的TSS和LDT到GDT表中。

 清GDT表和task[NR_TASKS]陣列其餘部分。

 ltr (0);                 // 將任務0 的TSS 載入到任務暫存器tr。

lldt (0);                // 將區域性描述符表載入到區域性描述符表暫存器。

設定核心的工作心跳--8253定時器,安裝定時器中斷。

設定系統呼叫中斷門:set_system_gate (0x80, &system_call);

9.buffer_init(buffer_memory_end);// 緩衝管理初始化,建記憶體連結串列等。

  在核心的結束地址end(由連線程式生成)到buffer_memory_end之間(除掉640kb-1M的BIOS範圍)區域中,建立緩衝區連結串列(表頭start_buffer)並分配緩衝塊(1KB)。

  初始化空閒表free_list,HASH表hash_table。

10.hd_init();// 硬碟初始化。

 blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; //設定硬碟的裝置請求函式為do_hd_request.

 設定硬碟中斷處理IDT項。

11.floppy_init();//軟盤初始化。

       blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;

       設定軟盤中斷處理IDT項。

12.sti()開中斷。

13.move_to_user_mode();移動到使用者態執行。

這是個巨集。由嵌入彙編程式碼組成。設定核心堆疊中的CS為任務0程式碼段(使用者態),通過中斷返回iret, 自動載入了LDT0的程式碼段到CS,資料段到SS,DS等,完成了從特權級從0跳到3。其實執行的程式碼在記憶體中的位置完全相同。只是完成執行權跳到使用者態而已。這樣,核心執行變成了任務0的執行。

14.fork();生成程序1,執行init();

這裡的fork()是行內函數。為了不使用使用者棧。

程序0從此死迴圈執行pause();

15 init();

程序1執行init()函式。

呼叫setup取硬碟分割槽資訊hd.載入虛擬盤,程序init載入根檔案系統,設定終端標準IO,建立程序2以/etc/rc為標準輸入檔案執行shell.完成rc檔案中的命令。載入完根檔案系統之後,整個OS就已經完整地執行起來了。

init等程序2退出,進入死迴圈:建立子程序,建立新會話,設定標準IO終端,以登入方式執行shell.,剩下的動作由使用者來決定了。

(三)kernel:

<!--[if !vml]-->
<!--[endif]-->

圖3

個人認為最主要的是中斷程式碼,然後是中斷程式碼會呼叫的通用程式碼。為什麼這麼說呢,無論是排程schedule,還是fork,都只有在使用者程序執行int 0x80 中斷進行系統呼叫或者是硬體中斷才能進入核心程式碼,執行核心函式。當核心初始化結束後所有程序都是使用者態程序,只有通過IDT表中定義的那些中斷函式去執行核心程式碼。所以中斷是OS的主線,只是在功能上分成了多個模組。

在traps.c中,設定了絕大多數中斷向量,通過set_trap_gate()或者set_intr_gate設定對應IDT描述符。set_trap_gate()不會遮蔽中斷,而set_intr_gate遮蔽外部中斷,中斷處理程式都是用匯編定義的,大部分在asm.s中定義,其餘在system_call.s,keyboard.s,rs_io.s中定義。 彙編程式中再呼叫C語言程式做具體的處理。

比較重要的中斷時鐘中斷int 0x20,系統呼叫中斷int 0x80,頁故障中斷int14,還有一些外部裝置如鍵盤,硬碟等也很重要,不過屬於fs模組的內容。大多數異常只是簡單呼叫sys_exit()結束當前程序,並重新排程其他程序schedule()。

從幾個重要中斷去中斷執行流程去弄清OS怎麼工作的:

1)int 0x20 時鐘中斷

時鐘是整個OS工作的心跳。8253每10ms產生一箇中斷。中斷服務執行do_timer(),然後do_signal();

do_timer主要判斷當前程序時間片是否用完,如果用完且處於使用者態則執行schedule()重新排程。如果中斷時當前程序正在核心態執行,則不能進行切換。也是說linux在核心態不支援任務搶佔。這樣使得核心的設計大大的簡化了,因為除了程序自己放棄執行(如sleep,wait類)不用擔心臨界區資源競爭的問題。

如果當前程序是使用者程序,判斷當前訊號點陣圖中是否有未處理的訊號,取最小訊號然後呼叫do_signal()。這個函式想要執行使用者定義的訊號處理函式。

do_signal()把訊號對應的處理函式插入到核心堆疊eip處,並修改使用者堆疊使中斷返回後用戶程序執行訊號處理函式,

訊號處理函式返回後執行一個sa_restorer,恢復使用者堆疊為正常中斷退出之後的狀態。(這是一個技巧,它實現了核心空間呼叫使用者空間的函式!!!)

另外核心空間與使用者空間資料交換預設通過fs來完成。

使用者程序總是通過中斷進入核心態,訊號判斷總是發生在時鐘中斷和系統呼叫中斷處理之後。所以實時性也很強,因此稱為軟中斷。

關於訊號,在schedule()中,對當前系統中的所有程序alarm訊號定時判斷,可睡眠打斷程序如果有未遮蔽訊號置位喚醒(狀態改為就緒)。

因此時鐘,系統呼叫,以及最頻繁呼叫的schedule()裡面都會處理訊號。因此訊號總是可以及時地得到"觸發"。

2)int 0x80 系統呼叫

系統呼叫的架構就是一個統一的中斷入口和出口_system_call,保護現場,準備引數(最多三個),取呼叫號,呼叫系統呼叫函式列表中對應處理函式,多是名為sys_XXX()的C函式。C處理函式返回後的後期流程:

如果程序系統呼叫後狀態不為就緒態或者時間片用完,執行排程schedule();判斷中斷前是使用者程序且有未處理的訊號?執行do_signal(),中斷返回。

最重要的系統呼叫莫過於fork()和execve;

首先程序的重要組成部分:

任務陣列task[],每個任務佔一項(假定序號為nr),每個虛擬地址空間都是64M, 範圍從nr*64M到(nr+1)*64M-1 ,在頁目錄表中最多佔16項,每項對應一個頁表即4M空間。程序任務資料結構task和核心棧共用一頁空間,核心棧頂在頁空間末端,task資料在頁起始端。

程序的頁表佔用的頁需要通過記憶體管理提供的介面get_free_page()來申請。 每個程序在GDT表中佔用兩個描述符項,LDT(nr)和TSS(nr)。

fork()流程:

呼叫find_empty_process()找一個空閒的程序任務task[]下標即任務號nr,並取得一個不重複的pid.
呼叫copy_process(...),裡面一堆引數都是系統棧中儲存的全部內容。(彙編和C混合程式設計技巧!)。copy_process 向mm申請一頁記憶體儲存task資料和設定核心堆疊。把父程序也就是當前程序的task資料全部拷貝,然後修改。
設定tss內容,(需要修改的主要是ss0=核心資料段,esp0=申請的頁底部,eax=0)。

copy_mem拷貝程序空間。注意任務號nr意義在於程序的虛擬地址空間在nr*64M~(nr+1)*64M範圍內。copy_mem先計運算元程序虛擬地址空間基址和父程序空間大小,設定子程序LDT中的程式碼段和資料段描述符基址和段限。呼叫copy_page_tables複製程序空間。copy_page_tables就是把父程序佔用的頁目錄項和全部頁表中指定的有效的物理頁面全部拷貝到子程序的頁目錄項和頁表中去。同時把父子程序的頁表設為共享的也就是隻讀的,一旦任意一個程序執行寫記憶體操作,將發生頁錯誤中斷。這個中斷將導致系統為程序重新分配可寫的記憶體頁。copy_page_tables先計算父子程序虛擬地址空間佔用的目錄項(16個最多)開始地址,對每個有效的目錄項先為子程序分配一個頁面作為頁表,然後對該目錄項下所有有效的頁表項進行復制。同時把r/w位都置0。把對應物理頁的mem_map[]加1。這樣做非常高效而且非常巧妙。最大限度地共享了本身就只讀或者不需要再寫的頁面。每當程序和核心之間要交換資料時尤其是核心向程序空間寫資料時總是要先驗證程序給的線性地址是否有效。如verify_area,write_verify..這兩個函式最終會呼叫un_wp_page,取消頁面的防寫。對mem_map[]=1的直接置r/w為1,mem_map[]>1表明頁面共享了,記憶體頁對映表mem_map[]-1然後申請空閒物理頁,設定到頁表項中,並複製頁面copy_page。可見,父子程序先寫程序者將申請空閒頁並拷貝頁面內容,另一個則可以直接使用原來的頁面,因為這時mem_map[]=1了。

程序空間拷貝完畢之後,再設定一些task結構資料。給GDT表填加兩項LDT(nr),TSS(nr).程序狀態設為就緒,等待被排程就OK了。。

這就是所謂的寫時複製,太神奇了。。

execve提供了需求載入的機制。

它載入一個程式檔案到程序中執行,因此把原來程序擁有的頁表項和頁表全部釋放掉。同時分配一頁記憶體存放參數。

根據可執行檔案頭把程序任務資料結構task[nr]所有資料都設定好,但是並不載入一頁程式碼資料。所以整個程序就是一副空架子。

從程序空間的第一條語句開始執行就會產生中斷,然後根據PC的值從外設中載入所在頁到記憶體中。這個中斷將執行do_no_page.

這個函式在fs模組中定義,在fs模組中再仔細分析。

3)缺頁中斷int14

這個中斷是十分有用的,它實現寫時複製。fork和execve沒做完的事情都會由這個中斷提供的功能來了結。

中斷錯誤號為出錯頁表項的最後3位。根據P位0或1判斷是缺頁中斷或防寫中斷。

缺頁中斷呼叫do_no_page,防寫呼叫do_wp_page.

do_wp_page提供寫時複製機制,

取消頁面保護(對於主記憶體區而言),頁面不是共享狀態,即mem_map[]=1,則置r/w=1返回。

如果頁面是共享狀態(mem_map[]>1),mem_map[]--,申請一頁記憶體,並拷貝,對映到程序空間。

do_no_page提供的需求載入機制。

CR2提供發生錯誤時的線性地址。如果當前程序沒有可執行檔案且該地址比資料段末地址大,這可能是因為堆疊伸長引起的,直接申請一頁記憶體對映到該線性地址所在頁。

否則嘗試頁面共享,即如果有執行檔案而且其inode使用計數>1,表明系統中可能有程序也在執行這個程式,這樣可以查詢到這個程序並把其對應地址處的頁面共享到自己空間,也就是修改這兩個程序對應的頁表項。而且是共享方式,所以只能讀不能寫,如果有一個程序要執行寫,則會引起防寫中斷,系統再給寫的程序另外再分配記憶體頁並拷貝一頁內容。如果嘗試頁面共享失敗,沒辦法只得從外設中載入,找到可執行檔案的inode.計算要讀的邏輯塊號(注意第一塊是檔案頭),讀一頁(4塊)到記憶體。分配頁面,複製緩衝中的4塊資料,把頁面對映到程序空間引起中斷的線性地址處。

(四)mm記憶體管理

linux的mm雖然只有兩個檔案memory.c和page.s,但是內容卻很不簡單。必須對分頁機制有很好的理解才能讀明白。
這個版本的核心每個程序虛擬空間64M,共支援4G/64M=64的任務數。所有程序共用一個頁目錄,但是卻有自己的頁表。
對虛擬地址的劃分使得在頁目錄中也存在劃分。每個程序虛擬空間最大佔用16個目錄項,每個目錄項指向一個頁表(1024個記憶體頁),對應4M空間。
線性地址分三段,每段都是一個索引index或者叫偏移(offset),第一段索引是在頁目錄(基址在CR3)中找到頁目錄項,頁目錄項裡儲存的是一張頁表的基地址。以線性地址的第二段為索引加上這個基地址,得到的頁表項儲存的是實際記憶體頁的起始地址。再加上線性地址第三段為偏移,得到線性地址對映的實際實體地址。

記憶體管理提供的功能主要有管理頁面,操作程序空間,缺頁中斷處理(寫時複製,需求載入),共享記憶體頁。其中大多數函式都會訪問頁目錄和頁表,都使用上述的計算的原理。

記憶體管理mm和核心kernel兩部分程式碼聯絡十分密切

記憶體管理提供的主要的功能函式可以分為

1管理頁面    :取一個空閒頁get_free_page,釋放一頁free_page. 
2操作程序空間:free_page_tables釋放程序頁目錄和頁表
                            copy_page_tables在程序空間之間複製頁目錄和頁表。主要提供給fork()使用,實現寫時分配。
                            put_page 把一頁記憶體對映到程序空間中去。
                            write_verify程序空間有效性驗證,當核心向用戶空間寫資料之前必須進行驗證。為可能為無效的地址區域分配頁面
3頁面共享&頁故障中斷:
                            try_to_share 嘗試在開啟檔案表中找當前執行程式inode,已經存在的話就查詢所有程序中executalbe與當前程序相同的任務。有則嘗試共享對應地址的對映的物理頁面。即新增到自己相應位置的頁表項中去。(share_page) 
                            do_no_page 缺頁中斷。判斷地址是否超出end_data,是則可能是堆疊伸長,分配頁面到相應位置(get_free_page,put_page),否則表示地址在可執行檔案內部,先嚐試共享,不成功則從線性地址計算需載入部分在檔案上內部的塊號,通過bmap把檔案內部塊號對映的裝置邏輯塊號計算出來。申請空閒頁並通過bread_page讀取一頁,最近由put_page把這頁對映到發生中斷的程序頁面上。

un_wp_page在防寫中斷中呼叫,取消頁表保護,實現寫時複製。

(五)檔案系統模組fs:

1.總體結構:

Linux把所有裝置都做為檔案來看待。提供統一的開啟,關閉,讀寫系統呼叫介面。          

下面是檔案系統層次關係:

<!--[if !vml]--><!--[endif]-->

圖4

總體來說,檔案系統提供兩類外部介面(系統呼叫),檔案讀寫和檔案管理控制。

上圖中Read_write代表的是檔案讀寫系統呼叫介面read,wirte。它根據操作檔案的型別分別呼叫了四種讀寫函式:

字元型檔案tty_read,tty_write,在kernel/chr_drv驅動模組中定義;
FIFO檔案  pipe_read,pipe_write 都是記憶體操作。Fs/pipe.c中定義
block_dev塊裝置檔案 :block_read,block_wirte,間接呼叫bread。

File_dev 常規檔案。File_read,file_write,   涉及的內容是fs主要的內容。

圖中Open stat fcntl 則是檔案系統的系統管理控制介面如建立開啟關閉,狀態訪問修改功能。這主要針對常規檔案,如根檔案系統下的全部檔案。這些都需要底層檔案系統函式支援,主要包括檔案系統超級塊,i結點點陣圖和邏輯塊點陣圖,i結點,檔名解析等操作。而這些底層檔案系統函式則建立於buffer提供的緩衝管理機制之上。這些是對上圖的大體歸納吧!

在上面總結kernel的時候,沒有提及blk_drv和chr_drv,因為我覺得把它們放在檔案系統裡面來更合適。

Blk_drv目錄是塊裝置驅動程式碼。實現了HD(硬碟),FD(軟盤),RD(Ramdisk)三種塊裝置的底層驅動,並提供一個外部呼叫的介面ll_rw_block(dev,nr)。就是上圖中右下虛框示意的層次上。

圖5

同樣的,char_drv實現了字元裝置(序列終端)的驅動,包括控制檯(鍵盤螢幕),兩個串列埠。實現供上層呼叫的讀寫介面read_tty , write_tty。下面是原始碼關係圖:

<!--[if !vml]--><!--[endif]-->

圖6

2下面分別從從底層向高層總結一下各個層次中原始碼實現的主要細節:

2.1 塊裝置驅動部分 kernel/blk_drv

塊裝置工作流程(粗略):

1)檔案裝置介面呼叫底層塊裝置讀寫函式ll_rw_block(int rw,buffer_head *bh).這裡bh要讀的裝置號,塊號,已經寫入bh, rw是讀或者寫指令

2)ll_rw_block(int rw,buffer_head *bh)取主裝置號major,呼叫make_request(major,rw,bh);

3)make_request(major,rw,bh)申請一個請求項,根據rw和bh相應設定填充req各欄位值,     並呼叫add_request (major + blk_dev, req)插入到裝置major的請求佇列。

4)add_request (major + blk_dev, req)檢查裝置等待佇列是否為空,為空則把req新增到佇列中並馬上呼叫裝置的請求執行函式。
       對於硬碟,這個函式就是do_hd_request,它將根據請求項的各個欄位設定向硬碟發出相應的命令. 如果請求佇列不為空,則按照電梯演算法把req加到佇列中。
       ll_rw_block函式返回。

整個ll_rw_block()返回到上層呼叫(緩衝管理部分buffer.c)。然後呼叫程序將執行等待wait_on_buffer(bh);程序切換。

硬碟接受命令後,完成req要求的讀/寫一個扇區後將發出中斷。hd_interrupt(定義於kernel/system_call.s)被執行。呼叫_do_hd。do_hd是裝置當前要呼叫的中斷處理函式的指標。根據當前請求,do_hd_request在呼叫hd_out向硬碟控制器發命令(如讀寫或復位等)時根據命令型別指定do_hd為read_intr, write_intr或其它。如果為讀,do_hd=read_intr。寫則do_hd=write_intr.

read_intr 將判斷當前請求項請求讀的扇區數是否已經全部讀完,如果沒有,再次設定do_hd=read_intr,然後返回。如果全部完成,則呼叫end_request(1)去喚醒等待的程序。然後呼叫do_hd_request去處理其餘請求項。

write_intr 將判斷當前請求項請求寫的扇區數是否已經寫完,如果沒有,把一扇區資料複製到硬碟緩衝區內,然後再次設定do_hd=write_intr並返回。如果寫完,則呼叫end_request(1),更新並有效快取塊,然後呼叫do_hd_request去處理其餘請求項。

整個硬碟讀寫流程如下 :


圖7

對於軟盤,大體的流程差不多。只是軟盤有啟動馬達等延時寫時操作,比較瑣碎一些。

對於ramdisk,速度很快所以不需要中斷機制,當然請求佇列也最多隻有當前一個。像上面的過程一樣,make_request會呼叫add_request,而由於前面的請求佇列一定為空,所以會馬上執行do_rd_request. 在do_rd_request中直接讀、寫資料。然後就end_request(1).

2.2 字元裝置驅動 kernel/chr_drv

序列/字元裝置在linux下叫TTY,每個TTY對就一個tty_struct結構。0.11版本一共三個,一個控制檯兩個串列埠。每個tty_struct有三個緩衝區,read_q,  write_q,  secondary 。
read_q儲存原始的輸入字元佇列,write_q儲存的是輸出的字元佇列,secondary裡面是輸入字元序列通過行規則處理後的字元序列。
tty_struct中的termios儲存的是終端IO屬性等。這個結構通過tty_ioctl.c中tty_ioctl()來對tty進行相應的控制或設定。
避開非常瑣碎的行規則,從char_dev.c中函式rw_tty呼叫來看整個過程的粗略脈絡。 rw_tty(int rw,unsigned minor,char * buf,int count, off_t * pos)。檢測程序有無終端,有則根據rw呼叫tty_read 或者tty_write. 
先看tty_read(minor,buf,count),對於要求讀取的位元組數nr,在定時時間內,迴圈讀取secondary中的字元,直到讀到nr個為止。如果secondary空了,  程序等待於secondary的等待佇列上。如果超時,則返回。

再看tty_write(minor,buf,count),如是tty寫緩衝write_q已經滿了,睡眠sleep_if_full (&tty->write_q); 對於要求寫位元組數nr,迴圈拷貝到write_q中去。如果拷貝過程中write_q滿了或者已經拷貝完呼叫寫函式。沒拷貝完則切換程序。剩下的工作交給中斷處理程式去完成。

對於讀操作,當tty收到一個字元,比如串列埠收到一個字元或者是使用者按下鍵盤,系統將進入相應中斷程式。中斷程式對收到的字元進行處理,然後把字元放入對應tty的read_q中,呼叫do_tty_interrupt,do_tty_interrupt直接呼叫copy_to_cooked(tty).  copy_to_cooked(tty)把read_q的全部字元通過行規則處理。然後放到secondary佇列中去。如果tty回顯標誌置位,則把轉換後的字元也放到寫佇列write_q中,並呼叫tty->write (tty); 如果中ISIG 標誌置位,收到INTR、QUIT、SUSP 或DSUSP 字元時,需要為程序產生相應的訊號。最後喚醒等待該輔助緩衝佇列的程序(如果有的話)。wake_up (&tty->secondary.proc_list); 中斷返回。

對於寫操作,如果tty是控制檯,其tty寫操作為con_write (tty),這個函式直接把write_q中所有字元按照格式寫到視訊記憶體中去或者調整游標。如果是串列埠,tty寫操作為rs_write(tty);這個函式更簡單,開啟串列埠傳送緩衝空閒允許中斷位就返回。這樣,CPU會馬上收到一箇中斷,在中斷程式中,寫操作才會真正進行。串列埠寫緩衝空中斷執行時先判斷寫佇列是否為空,如果為空,喚醒可能的等待佇列,並且禁止傳送緩衝空中斷允許並中斷返回。如果不空,但是寫佇列字元數小於256,也喚醒可能的寫等待佇列,然後從寫佇列中取一個字元寫入串列埠傳送暫存器。中斷返回。

2.3檔案系統之緩衝管理fs/buffer.c

緩衝管理部分兩個作用,利用cache機制提供更高效的使用外部塊裝置,給使用塊裝置的其它程式提供簡單的介面如bread。使得上層程式對裝置操作全部變成對緩衝塊的操作。給塊裝置如軟硬碟提供一種cache機制。每個緩衝塊buffer_head都對應一個裝置dev和邏輯塊號block,引用計數count,修改標誌dirt,有效標誌uptodate。 類比CPU,修改標誌與有效標誌是cache機制必需的,而dev和block號則相當於地址。緩衝管理負責裝置資料塊與緩衝對映塊資料一致性。緩衝管理具體去呼叫裝置驅動程式ll_rw_block().

Buffer.c主要提供的函式有申請、釋放快取,同步(buffer與裝置內容一致),讀取。而寫操作則總是在緩衝不足的情況下利用同步進行的。讀取的時候總是根據給定的dev和block先查詢當前快取中是否存在有效的對應塊,如果存在就不再訪問裝置。否則取一個空閒緩衝,呼叫裝置驅動ll_rw_block。

緩衝塊連結串列實現了hash連結串列和LRU演算法,所有的緩衝塊都連線於連結串列中,連結串列頭總是空閒度最高的緩衝塊,連結串列尾則是最近剛申請的塊。查詢空閒緩衝從頭開始比較空閒度,找最大的。這就實現了LRU演算法。 所有具有相同hash值的緩衝塊連線於同一個hash_table[nr]項上。nr值由裝置號和塊號經過一個hash演算法得到。這樣查詢速度會快好多倍。

所有對外設邏輯塊的讀寫都在這裡被轉化為對緩衝塊的讀寫。每次讀寫前總是先根據裝置號和邏輯塊號到hash_table[]表中查詢hash連結串列,若已經存在且有效,則直接對緩衝讀寫。寫後要置修改標誌dirt。這樣當執行同步操作,或者getblk()找不到乾淨的空閒塊的時候會把所有dirt為1的未被佔用(count=0)的緩衝塊寫入磁碟。

2.4 檔案系統之檔案系統底層操作。

檔案底層操作。(bitmap.c,inode.c,namei.c,super.c,truncate.c,)這部分按照檔案系統對硬碟等塊裝置的使用規則,實現了相應規則的操作函式。檔案系統把塊裝置按邏輯塊管理,功能劃分:

引導塊

超級塊

i結點點陣圖塊區

邏輯塊點陣圖塊區

i結點塊區

資料塊區

    超級塊指明瞭整個檔案系統各區段的資訊。兩個點陣圖區分別指示i結點區和資料塊區的佔用和空閒狀況,i結點區中每個i結點都代表一個檔案或者目錄。整個檔案系統的根目錄在第1個i結點處。通過它可以找出任何路徑下的檔案的i結點。

i結點指示一個檔案或者目錄,一個i結點的內容主要是檔案佔用的資料塊號。直接塊號,一、二次間接塊號提供了靈活的機制來線性地查詢檔案中一個數據塊對應在哪一個具體的物理塊上。目錄檔案的資料塊內容是目錄項,它包含的所在目錄的全部檔案和子目錄的資訊。每個目錄項儲存一個檔案或者子目錄的inode號和檔名。

相應地,bitmap.c提供了i結點點陣圖,資料塊點陣圖的佔用和釋放操作。super.c實現對超級塊的讀定安裝解除安裝操作。inode.c實現是獲取指定nr的inode(iget(dev,nr)),寫回inode ( iput(inode) )等操作。

namei.c則實現了按照檔名來獲取inode的操作。從而提供了通過檔名來管理檔案(inode)的方法。

這些操作之間的層次並不十分清晰,相互呼叫很多。注意塊裝置是按塊為最小單位訪問的,這些操作不過是按照檔案系統對裝置塊使用的定義對各個塊以及塊中的資料做解析和操作罷了。檔案底層操作都貌似在訪問塊裝置,但是卻僅僅呼叫了緩衝管理提供的介面。它們操作了記憶體。緩衝管理去實現裝置的讀寫。比如在系統安裝根檔案系統的時候,超級塊已經讀如緩衝,根據超級塊的資訊,將i結點點陣圖塊,邏輯點陣圖塊讀入到記憶體緩衝中了。

下面對各個原始檔的實現進行小結:

bitmap.c: 點陣圖操作,主要提供檔案系統中i結點點陣圖,邏輯塊點陣圖相關操作,包括申請和釋放inode,申請和釋放block.首先檔案系統的點陣圖塊在mount_root中已經快取到buffer中,緩衝塊指標由超級塊s_zmap[],s_imap[]指向。所以申請釋放操作主要的一部分------對點陣圖相應位置位或者復位就變成對緩衝塊置位復位了,然後修改標誌dirt=1就行了。
new_block除了要對找到的空閒位置位外,還要申請一塊空閒緩衝(清0)並填申請塊的dev和block。置有效和修改標誌。這麼做其實就等於一個寫操作,即把申請的裝置塊清0。(當然,可能申請後馬上就要寫這一塊,所以這麼做最高效了。)

truncate.c: 對檔案(inode)長度清0。主要呼叫free_block對點陣圖進行操作。直接塊直接釋放,對一次間接塊上所有有效塊號釋放,然後再釋放一次間接塊。二次間接塊同理。

inode.c:   主要提供三個函式,iget,iput,bmap.  iget是獲取指定裝置和i結點號的記憶體i結點。使用計數加1。主要呼叫read_inode(呼叫buffer管理部分)  ;iput是把一個記憶體i節點寫回到裝置中去。使用計數減1。主要呼叫write_inode(呼叫buffer管理部分)
bmap是把檔案塊號對應到裝置塊號(邏輯塊號)中去。檔案塊號是按直接塊,一直間接,二次間接順序計算的索引。邏輯塊號則是儲存在它們裡面的塊號。有點像頁表,頁的線性地址對應檔案塊號,頁的實體地址對應邏輯塊號。頁表項中的儲存的地址就是頁實體地址。bmap有建立和不建立兩種方式。建立時會根據檔案塊號給檔案(inode)申請邏輯塊存放可能需要的一、二次間接塊和資料塊。

super.c:對檔案系統超級塊的相關操作。如get_super,put_super,read_super,sys_mount,sys_umount;超級塊對應一個檔案系統。
get_super(dev)在系統超級塊陣列中查詢並返回匹配的超級塊。
put_super(dev)釋放超級塊陣列中超級塊資訊,並釋放超級塊i結點,邏輯塊點陣圖佔用的緩衝區。
read_super(dev)先在超級塊陣列中查詢,有直接返回,沒有則先在超級塊陣列找一空閒項。讀dev 1號塊,取得超級塊資訊,如點陣圖佔多少塊,再讀點陣圖塊(i點陣圖,邏輯塊點陣圖)到緩衝中。設定完畢返回。
sys_mount(devname,dirname,rw_flag) 在目錄dirname上安裝devname裝置的檔案系統。取dirname和devname的i結點判斷二者都有效?然後讀dev超級塊read_super(dev),置超級塊安裝結點為direname的i結點 sb->s_imount=dir_i. 置目錄i結點安裝標誌1。所以i結點的安裝標誌表明該目錄是否安裝了一個檔案系統。而要知道安裝的檔案系統的具體資訊則要查詢超級塊陣列,看看哪一個超級塊的s_imount等於該i結點。。。

namei.c:提供檔案路徑名到i結點的操作。大部函式引數都直接給出檔案路徑名,所以它實現了通過檔名來管理檔案系統的介面。如開啟建立刪除檔案、目錄,建立刪除檔案硬連線等。
大部分函式的原理都差不多:呼叫get_dir取得檔案最後一層目錄的i結點dir。如果是查詢就呼叫find_entry從dir的資料塊中查詢匹配檔名字串的目錄項。這樣通過目錄項就取得了檔案的i結點。如果是建立(sys_mknod)就申請一個空的inode,在dir的資料塊中找一個空閒的目錄項,把檔名和inode號填入目錄項。建立目錄的時候要特殊一些,要為目錄申請一個數據塊,至少填兩個目錄項,.和..  (sys_mkdir)。
刪除檔案和目錄的時候把要釋放i結點並刪除所在目錄的資料塊中佔用的目錄項。
開啟函式open_namei()基本上實現了open()的絕大部分功能。它先按照上述過程通過檔案路徑名查詢 最後一層目錄i結點,在目錄資料塊中查詢目錄頂。如果沒找到且有建立標誌,則建立新檔案,申請一個空閒的inode和目錄項進行設定。 對於得到的檔案inode,根據開啟選項進行相應處理。成功則通過呼叫引數返回inode指標。
這個檔案用得最多的功能函式莫過於namei();根據檔名返回i節點。
這裡任何對inode的操作都是通過iget,iput這類更底層的函式去實現,iget和iput所在的層次基於buffer管理和記憶體inode表的操作之上。

2.5 檔案系統之檔案資料訪問操作這提供讀寫系統呼叫介面read,write

主要包括檔案:
block_dev.c:定義了塊裝置檔案的讀寫函式,block_write,block_read. 
file_dev.c :定義正規檔案讀寫函式。file_read,  file_write
pipe.c   :定義FIFO檔案讀寫及管道系統呼叫。read_pipe, write_pipe, sys_pipe

char_dev.c :定義字元型裝置訪問讀寫,rw_ttyx, rw_tty, rw_char. 最終都呼叫tty_read,tty_write
read_write.c:實現檔案系統讀寫介面 read,write,lseek。
read,write的引數是檔案控制代碼fd,這需要通過系統呼叫sys_open來獲取。函式根據程序task的filp[fd]指向的系統開啟檔案表項獲取inode、讀寫許可權、當前讀寫位置等。由inode的型別(上面四種之一)呼叫相應讀寫函式(參看圖4)。對於正規檔案,過程如下:由inode指向的記憶體inode項獲取檔案在裝置上的位置大小等資訊。通過inode和bmap計算要讀取的檔案資料在裝置的邏輯塊號。通過bread讀資料塊,然後對緩衝塊進行讀寫。到此就不用管了。緩衝管理的作用真的是太神奇了。

2.6 檔案系統高層操作&管理部分

包括檔案open.c,exec.c,stat.c,fcntl.c,ioctl.c 實現檔案系統中對檔案的管理和操作,如建立,開啟,設定/讀取檔案資訊,執行一個檔案程式。這個層次位於檔案底層操作之上,呼叫底層函式去實現。

open.c: 定義了系統呼叫sys_ustat,sys_access,sys_chmod,sys_chdir,sys_chroot,sys_open,sys_close.引數基本上是檔名或者檔案控制代碼。 
可以分為三類:修改inode屬性類(前3個),修改程序根/當前目錄,開啟關閉檔案。     
第一類通過namei找到i結點,修改相關屬性域,iput寫回裝置。
第二類通過namei找到i結點,把程序task資料相應域設為對應inode. 
第三類開啟時主要呼叫open_namei返回一個檔名對應的i結點,並設定系統開啟檔案表和程序開啟檔案表。返回檔案控制代碼。關閉則清程序開啟檔案表項,處理對應的系統開啟檔案表項(引用減1),寫回i結點。

execv.c:主要一個函式do_execve.往往在系統執行完fork之後會呼叫execve簇函式去執行一個全新的程式。必須重新對程序空間進行初始化。
主要流程:找到執行程式inode,進行相應判斷(如如許可權等),讀檔案頭(第1個數據塊)資訊。
如果是指令碼檔案,則取shell檔名和引數,以shell為執行程式去執行該指令碼檔案,這時重新以shell為檔名,以本指令碼檔案為引數,執行上述過程。
根據檔案頭資訊得到檔案各段長度,entry位置,修改程序任務結構中相應的資料。然後拷貝引數到程序空間末端,設定堆疊指標。
清空原程序空間佔用的頁目錄和頁表。 修改系統呼叫返回地址為程序空間的起始地址。
該系統呼叫返回後,新程序執行第一條語句,會引起一個缺頁中斷。根據要求的線性地址和executable,在中斷中執行do_no_page進行共享或者需求載入。

stat.c : 系統呼叫sys_stat,sys_fstat.取檔案狀態資訊。

fcntl.c: 實現sys_dup,sys_dup2,sys_fcntl. 
dup複製到從0開始找最小的空閒控制代碼。
dup2指定開始搜尋的最小控制代碼。
fcntl主要根據flag引數不同。可以實現四方面的操作:複製檔案控制代碼同dup,設/取close_on_exec標誌。設/取檔案狀態和訪問模式。給檔案上/解鎖。

ioctl.c:主要實現系統呼叫sys_ioctl,間接呼叫tty_ioctl.主要對終端裝置進行命令控制、引數設定、狀態讀取等。

相關推薦

LINUX0.11 核心閱讀筆記

一.原始碼目錄圖1二.系統總體流程:系統從boot開始動作,把核心從啟動盤裝到正確的位置,進行一些基本的初始化,如檢測記憶體,保護模式相關,建立頁目錄和記憶體頁表,GDT表,IDT表。然後進入main進行初始化設定,main完成系統各個模組要用到的所有資料結構和外部裝置的初始

LINUX0.11核心閱讀筆記 (2)

 (五)檔案系統模組fs: 1.總體結構: Linux把所有裝置都做為檔案來看待。提供統一的開啟,關閉,讀寫系統呼叫介面。下面是檔案系統層次關係: <!--[if !vml]--><!--[endif]--> 圖4 總體來說,檔案系統提供兩類外部

LINUX0.11核心閱讀筆記 (1)

 我是通過閱讀趙炯老師編的厚厚的linux核心完全剖析看完LINUX0.11的程式碼,不得不發自內心的說Linus真的是個天才。雖然我覺得很多OS設計的思想他是從UNIX學來的,但是他自己很周全很漂亮很巧妙地實現瞭如此龐大一個系統的絕大多數程式碼。這裡面有太多環節需要注

Linux0.11核心讀書筆記/boot/bootsect.s

果凍QQ:457283! 本程式完成的主要功能! 1.bootsect.s從0x7c00處開始執行! 2.將自己複製到0x90000處! 3.將setup.s程式從磁碟第2扇區讀取到0x90200處! 4.將system讀取到0x10000處! 5.獲取根檔案系統裝置號! 6

《代碼大全》第11閱讀筆記

筆記 好習慣 下劃線 個人 ans 閱讀 代碼大全 我們 介紹    記得這次與core組對接,為了一個命名為suanshi的文件笑了好久,其實我們自己在命名過程中也比較隨意,雖然早過了大一那會用abc命名的年紀,但命名往往還是有點隨心所欲,大小寫,下劃線,有的時候第二次用

Linux0.11核心引導啟動過程概述

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Linux0 11核心引導啟動過程概述

Linux0.11僅支援x86架構。它的核心引導啟動程式在資料夾boot內,共有三個彙編程式碼檔案。按照啟動流程依次是:     (1)bootsect.s。boot是啟動引導的意思,sect即sector,是扇區的意思,二者合在一起啟動引導扇區。這是 磁碟載

ubutu14 下編譯linux0.11核心

下載 linux-0.11-gdb-rh9-050619.tar.gz 程式碼,以它為藍本編譯。 1. boot/head.s:45: Error: unsupported instruction `mov' 原因: 這是因為本機系統為64位, 因此需要給所有Makefi

總結linux0.11核心中的主,次裝置號

老會忘,記下來方便後面查閱 主裝置 型別 說明 請求操作函式 0 無 無 NULL 1 塊/字元 ram,記憶體裝置(虛擬盤等) do_rd_request 2 塊 fd,軟碟機裝置 do_fd_request 3 塊 hd,硬碟裝置 do_hd_request 4 字元

Linux0.12核心讀書筆記

實踐 一.準備工作 1.程式碼下載 http://oldlinux.org/Linux.old/kernel/0.1x/linux-0.12.tar.gz 讀書筆記 第六章 引導啟動程式 Boot 1.PC在電源開啟後,80x86 CPU將進入真實模式,並從地址0xFFFF

linux環境下編譯linux0.11核心

原部落格很老了,我並沒有編譯通過,網上大多編譯成功的是用gcc-4.3以下的版本,也有在gcc-4.6編譯成功的,折騰了幾天,這是我在網上找到的最新的資料了, 但是ubuntu源裡面最老的版本也是gcc4.7版本的,嘗試編譯低版本的gcc原始碼,但編譯不通過. 上面的

linux0.11核心空間與使用者空間資料交換

學習linux到現在對於這個問題一直都沒有在意,細看程式碼時發現這確實是一個大問題,並且感覺很巧妙,具體在segment.h檔案中函式實現。 當用戶程序執行系統呼叫進入核心空間時,所有段都指向核心段,但是fs卻除外,它需要扮演負責核心空間與使用者空間資料的交換的重要角色。其

用bochs安裝linux0.11核心

參考:http://www.oldlinux.org/oldlinux/forumdisplay.php?fid=4 1.先用gcc編譯linux0.11核心(不含檔案系統) 這是別人修改後可以gcc編譯的,原始碼已經放到: \\Cnpc0165-cd\Books\Comp

利用VS2013構搭linux0.11核心除錯環境

VS2013的下載連結:連結:http://pan.baidu.com/s/1mh7iLfy 密碼:ir2o linux 0.11的工程 連結:http://pan.baidu.com/s/1eRU

Linux核心完全註釋 閱讀筆記:3.5、Linux 0.11目標檔案格式

為了生成核心程式碼檔案,Linux 0.11使用了兩種編譯器。第一種是彙編編譯器as86和相應的連結程式(或稱為連結器)ld86。它們專門用於編譯和連結,執行在實地址模式下的16位核心引導扇區程式bootsect.s和設定程式setup.s。第二種是GNU的彙編器as

2017.11.11-構建之法:現代軟件工程-閱讀筆記

能夠 測試 日誌 目標 問題 歸類 用戶隱私 心理 了解 用戶調研 1.焦點小組:一群人在一起討論,意見難以統一。 2.深入面談:了解用戶背景、需求、心理。也可以稱為“軟件可用性研究” 3.卡片分類:通過卡片進行整理雜亂無章的意見,討論->明確定義->歸類-&g

Java核心技術Ⅰ 閱讀筆記

n) bst bstr 程序設計 等號 java虛擬機 isn 代碼 引用 目錄 Java的基本程序設計結構 Java的基本程序設計結構 當我們編譯Java源代碼後,會產生包含類字節碼的文件,使用java明類執行時,Java虛擬機會從指定類中的main方法開始執行。

公信寶gxs核心程式碼閱讀筆記1-剛剛開始(霜之小刀)

公信寶gxs核心程式碼閱讀筆記1-剛剛開始(霜之小刀) 歡迎轉載和引用,若有問題請聯絡 若有疑問,請聯絡 Email : [email protected] QQ:2279557541 1、測試環境簡介 這裡我使用的是mbp,蘋果的開發環境

Linux核心完全註釋 閱讀筆記:3.3、C語言程式

By: Ailson Jack Date: 2018.09.14 本小節給出核心中經常用到的一些gcc擴充語句的說明。 1、C程式編譯和連結          使用gcc編譯器編譯C語言程式時,通常會經過4個處理階段,即預處理階段、編譯階段、彙編階段和連結階段

Linux核心完全註釋 閱讀筆記:3.4、C與彙編程式的相互呼叫

1、C函式呼叫機制          函式呼叫操作包括從一塊程式碼到另一塊程式碼之間的雙向資料傳遞和執行控制轉移。資料傳遞通過函式引數和返回值來進行。另外,我們還需要在進入函式時為函式的區域性變數分配儲存空間,並且在退出函式時收回這部分空間。Intel 80x86 CP