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

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



()檔案系統模組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_drvchr_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)申請一個請求項,根據rwbh相應設定填充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_hddo_hd是裝置當前要呼叫的中斷處理函式的指標。根據當前請求,do_hd_request在呼叫hd_out向硬碟控制器發命令(如讀寫或復位等)時根據命令型別指定do_hdread_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.ctty_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收到一個字元,比如串列埠收到一個字元或者是使用者按下鍵盤,系統將進入相應中斷程式。中斷程式對收到的字元進行處理,然後把字元放入對應ttyread_q中,呼叫do_tty_interruptdo_tty_interrupt直接呼叫copy_to_cooked(tty). copy_to_cooked(tty)read_q的全部字元通過行規則處理。然後放到secondary佇列中去。如果tty回顯標誌置位,則把轉換後的字元也放到寫佇列write_q,並呼叫tty->write (tty); 如果中ISIG 標誌置位,收到INTRQUITSUSP 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機制必需的,而devblock號則相當於地址。緩衝管理負責裝置資料塊與緩衝對映塊資料一致性。緩衝管理具體去呼叫裝置驅動程式ll_rw_block().

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

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

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

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

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

引導塊

超級塊

i結點點陣圖塊區

邏輯塊點陣圖塊區

i結點塊區

資料塊區

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

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

相應地,bitmap.c提供了i結點點陣圖,資料塊點陣圖的佔用和釋放操作。super.c實現對超級塊的讀定安裝解除安裝操作。inode.c實現是獲取指定nrinode(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)並填申請塊的devblock。置有效和修改標誌。這麼做其實就等於一個寫操作,即把申請的裝置塊清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裝置的檔案系統。取dirnamedevnamei結點判斷二者都有效?然後讀dev超級塊read_super(dev),置超級塊安裝結點為direnamei結點 sb->s_imount=dir_i. 置目錄i結點安裝標誌1。所以i結點的安裝標誌表明該目錄是否安裝了一個檔案系統。而要知道安裝的檔案系統的具體資訊則要查詢超級塊陣列,看看哪一個超級塊的s_imount等於該i結點。。。

namei.c:提供檔案路徑名到i結點的操作。大部函式引數都直接給出檔案路徑名,所以它實現了通過檔名來管理檔案系統的介面。如開啟建立刪除檔案、目錄,建立刪除檔案硬連線等。大部分函式的原理都差不多:呼叫get_dir取得檔案最後一層目錄的i結點dir。如果是查詢就呼叫find_entrydir的資料塊中查詢匹配檔名字串的目錄項。這樣通過目錄項就取得了檔案的i結點。如果是建立(sys_mknod)就申請一個空的inode,dir的資料塊中找一個空閒的目錄項,把檔名和inode號填入目錄項。建立目錄的時候要特殊一些,要為目錄申請一個數據塊,至少填兩個目錄項,...(sys_mkdir)刪除檔案和目錄的時候把要釋放i結點並刪除所在目錄的資料塊中佔用的目錄項。開啟函式open_namei()基本上實現了open()的絕大部分功能。它先按照上述過程通過檔案路徑名查詢 最後一層目錄i結點,在目錄資料塊中查詢目錄頂。如果沒找到且有建立標誌,則建立新檔案,申請一個空閒的inode和目錄項進行設定。 對於得到的檔案inode,根據開啟選項進行相應處理。成功則通過呼叫引數返回inode指標。這個檔案用得最多的功能函式莫過於namei();根據檔名返回i節點。這裡任何對inode的操作都是通過iget,iput這類更底層的函式去實現,igetiput所在的層次基於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來獲取。函式根據程序taskfilp[fd]指向的系統開啟檔案表項獲取inode、讀寫許可權、當前讀寫位置等。由inode的型別(上面四種之一)呼叫相應讀寫函式(參看圖4)。對於正規檔案,過程如下:由inode指向的記憶體inode項獲取檔案在裝置上的位置大小等資訊。通過inodebmap計算要讀取的檔案資料在裝置的邏輯塊號。通過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_opensys_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_statsys_fstat.取檔案狀態資訊。

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

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

結束.

看這本書的剖析的linux程式碼之前覺得LINUX很神祕,現在覺得親切多了。心裡面對核心的動作已經有了比較清晰的概念。但是還遠遠不足以運用到嵌入式中去,最新的核心與0.11相比,我覺得好像自己還是啥也不知道一樣,差得太多,顯得很陌生。下一步必須看看2.6版本的核心分析一類的書籍,瞭解最新的核心。