大部分程序員可能會有這樣的疑問:當(dāng)在程序中調(diào)用庫函數(shù) read 時(shí),這個(gè)請求是經(jīng)過哪些處理最終到達(dá)磁盤的呢,數(shù)據(jù)又是怎么被拷貝到用戶緩存區(qū)的呢?本文介紹了從 read 系統(tǒng)調(diào)用發(fā)出到結(jié)束處理的全過程。該過程包括兩個(gè)部分:用戶空間的處理、核心空間的處理。用戶空間處理部分是系統(tǒng)調(diào)用從用戶態(tài)切到核心態(tài)的過程。核心空間處理部分則是 read 系統(tǒng)調(diào)用在 linux 內(nèi)核中處理的整個(gè)過程。 Read 系統(tǒng)調(diào)用在用戶空間中的處理過程 Linux 系統(tǒng)調(diào)用(SCI,system call interface)的實(shí)現(xiàn)機(jī)制實(shí)際上是一個(gè)多路匯聚以及分解的過程,該匯聚點(diǎn)就是 0x80 中斷這個(gè)入口點(diǎn)(X86 系統(tǒng)結(jié)構(gòu))。也就是說,所有系統(tǒng)調(diào)用都從用戶空間中匯聚到 0x80 中斷點(diǎn),同時(shí)保存具體的系統(tǒng)調(diào)用號。當(dāng) 0x80 中斷處理程序運(yùn)行時(shí),將根據(jù)系統(tǒng)調(diào)用號對不同的系統(tǒng)調(diào)用分別處理(調(diào)用不同的內(nèi)核函數(shù)處理)。系統(tǒng)調(diào)用的更多內(nèi)容,請參見參考資料。 Read 系統(tǒng)調(diào)用也不例外,當(dāng)調(diào)用發(fā)生時(shí),庫函數(shù)在保存 read 系統(tǒng)調(diào)用號以及參數(shù)后,陷入 0x80 中斷。這時(shí)庫函數(shù)工作結(jié)束。Read 系統(tǒng)調(diào)用在用戶空間中的處理也就完成了。
Read 系統(tǒng)調(diào)用在核心空間中的處理過程 0x80 中斷處理程序接管執(zhí)行后,先檢察其系統(tǒng)調(diào)用號,然后根據(jù)系統(tǒng)調(diào)用號查找系統(tǒng)調(diào)用表,并從系統(tǒng)調(diào)用表中得到處理 read 系統(tǒng)調(diào)用的內(nèi)核函數(shù) sys_read ,最后傳遞參數(shù)并運(yùn)行 sys_read 函數(shù)。至此,內(nèi)核真正開始處理 read 系統(tǒng)調(diào)用(sys_read 是 read 系統(tǒng)調(diào)用的內(nèi)核入口)。 在講解 read 系統(tǒng)調(diào)用在核心空間中的處理部分中,首先介紹了內(nèi)核處理磁盤請求的層次模型,然后再按該層次模型從上到下的順序依次介紹磁盤讀請求在各層的處理過程。 Read 系統(tǒng)調(diào)用在核心空間中處理的層次模型 圖1顯示了 read 系統(tǒng)調(diào)用在核心空間中所要經(jīng)歷的層次模型。從圖中看出:對于磁盤的一次讀請求,首先經(jīng)過虛擬文件系統(tǒng)層(vfs layer),其次是具體的文件系統(tǒng)層(例如 ext2),接下來是 cache 層(page cache 層)、通用塊層(generic block layer)、IO 調(diào)度層(I/O scheduler layer)、塊設(shè)備驅(qū)動層(block device driver layer),最后是物理塊設(shè)備層(block device layer) 圖1 read 系統(tǒng)調(diào)用在核心空間中的處理層次
相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu):
數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系: 圖2示意性地展示了上述各個(gè)數(shù)據(jù)結(jié)構(gòu)(除了 bio)之間的關(guān)系??梢钥闯觯河?dentry 對象可以找到 inode 對象,從 inode 對象中可以取出 address_space 對象,再由 address_space 對象找到 address_space_operations 對象。 File 對象可以根據(jù)當(dāng)前進(jìn)程描述符中提供的信息取得,進(jìn)而可以找到 dentry 對象、 address_space 對象和 file_operations 對象。 圖2 數(shù)據(jù)結(jié)構(gòu)關(guān)系圖: 對于具體的一次 read 調(diào)用,內(nèi)核中可能遇到的處理情況很多。這里舉例其中的一種情況:
讀數(shù)據(jù)之前,必須先打開文件。處理 open 系統(tǒng)調(diào)用的內(nèi)核函數(shù)為 sys_open 。 清單1 sys_open 函數(shù)代碼
代碼解釋:
f->f_op = fops_get(inode->i_fop); 這個(gè)賦值語句把和具體文件系統(tǒng)相關(guān)的,操作文件的函數(shù)指針集合賦給了 file 對象的 f _op 變量(這個(gè)指針集合是保存在 inode 對象中的),在接下來的 sys_read 函數(shù)中將會調(diào)用 file->f_op 中的成員 read 。
圖3顯示了 sys_open 函數(shù)返回后, file 對象和當(dāng)前進(jìn)程描述符之間的關(guān)聯(lián)關(guān)系,以及 file 對象中操作文件的函數(shù)指針集合的來源(inode 對象中的成員 i_fop)。 圖3 file 對象和當(dāng)前進(jìn)程描述符之間的關(guān)系 到此為止,所有的準(zhǔn)備工作已經(jīng)全部結(jié)束了,下面開始介紹 read 系統(tǒng)調(diào)用在圖1所示的各個(gè)層次中的處理過程。 內(nèi)核函數(shù) sys_read() 是 read 系統(tǒng)調(diào)用在該層的入口點(diǎn),清單2顯示了該函數(shù)的代碼。 清單2 sys_read 函數(shù)的代碼
代碼解析:
if (file->f_op->read)
到此,虛擬文件系統(tǒng)層所做的處理就完成了,控制權(quán)交給了 ext2 文件系統(tǒng)層。 在解析 ext2 文件系統(tǒng)層的操作之前,先讓我們看一下 file 對象中 read 指針來源。 從前面對 sys_open 內(nèi)核函數(shù)的分析來看, file->f_op 來自于 inode->i_fop 。那么 inode->i_fop 來自于哪里呢?在初始化 inode 對象時(shí)賦予的。見清單3。 清單3 ext2_read_inode() 函數(shù)部分代碼
從代碼中可以看出,如果該 inode 所關(guān)聯(lián)的文件是普通文件,則將變量 ext2_file_operations 的地址賦予 inode 對象的 i_fop 成員。所以可以知道: inode->i_fop.read 函數(shù)指針?biāo)赶虻暮瘮?shù)為 ext2_file_operations 變量的成員 read 所指向的函數(shù)。下面來看一下 ext2_file_operations 變量的初始化過程,如清單4。 清單4 ext2_file_operations 的初始化
該成員 read 指向函數(shù) generic_file_read 。所以, inode->i_fop.read 指向 generic_file_read 函數(shù),進(jìn)而 file->f_op.read 指向 generic_file_read 函數(shù)。最終得出結(jié)論: generic_file_read 函數(shù)才是 ext2 層的真實(shí)入口。 圖4 read 系統(tǒng)調(diào)用在 ext2 層中處理時(shí)函數(shù)調(diào)用關(guān)系 由圖 4 可知,該層入口函數(shù) generic_file_read 調(diào)用函數(shù) __generic_file_aio_read ,后者判斷本次讀請求的訪問方式,如果是直接 io (filp->f_flags 被設(shè)置了 O_DIRECT 標(biāo)志,即不經(jīng)過 cache)的方式,則調(diào)用 generic_file_direct_IO 函數(shù);如果是 page cache 的方式,則調(diào)用 do_generic_file_read 函數(shù)。函數(shù) do_generic_file_read 僅僅是一個(gè)包裝函數(shù),它又調(diào)用 do_generic_mapping_read 函數(shù)。 在講解 do_generic_mapping_read 函數(shù)都作了哪些工作之前,我們再來看一下文件在內(nèi)存中的緩存區(qū)域是被怎么組織起來的。 圖5顯示了一個(gè)文件的 page cache 結(jié)構(gòu)。文件被分割為一個(gè)個(gè)以 page 大小為單元的數(shù)據(jù)塊,這些數(shù)據(jù)塊(頁)被組織成一個(gè)多叉樹(稱為 radix 樹)。樹中所有葉子節(jié)點(diǎn)為一個(gè)個(gè)頁幀結(jié)構(gòu)(struct page),表示了用于緩存該文件的每一個(gè)頁。在葉子層最左端的第一個(gè)頁保存著該文件的前4096個(gè)字節(jié)(如果頁的大小為4096字節(jié)),接下來的頁保存著文件第二個(gè)4096個(gè)字節(jié),依次類推。樹中的所有中間節(jié)點(diǎn)為組織節(jié)點(diǎn),指示某一地址上的數(shù)據(jù)所在的頁。此樹的層次可以從0層到6層,所支持的文件大小從0字節(jié)到16 T 個(gè)字節(jié)。樹的根節(jié)點(diǎn)指針可以從和文件相關(guān)的 address_space 對象(該對象保存在和文件關(guān)聯(lián)的 inode 對象中)中取得(更多關(guān)于 page cache 的結(jié)構(gòu)內(nèi)容請參見參考資料)。 圖5 文件的 page cache 結(jié)構(gòu) 現(xiàn)在,我們來看看函數(shù) do_generic_mapping_read 都作了哪些工作, do_generic_mapping_read 函數(shù)代碼較長,本文簡要介紹下它的主要流程:
到此,我們知道:當(dāng)頁上的數(shù)據(jù)不是最新的時(shí)候,該函數(shù)調(diào)用 mapping->a_ops->readpage 所指向的函數(shù)(變量 mapping 為 inode 對象中的 address_space 對象),那么這個(gè)函數(shù)到底是什么呢? address_space 對象是嵌入在 inode 對象之中的,那么不難想象: address_space 對象成員 a_ops 的初始化工作將會在初始化 inode 對象時(shí)進(jìn)行。如清單3中后半部所顯示。
可以知道 address_space 對象的成員 a_ops 指向變量 ext2_aops 或者變量 ext2_nobh_aops 。這兩個(gè)變量的初始化如清單5所示。 清單5 變量 ext2_aops 和變量 ext2_nobh_aops 的初始化
從上述代碼中可以看出,不論是哪個(gè)變量,其中的 readpage 成員都指向函數(shù) ext2_readpage 。所以可以斷定:函數(shù) do_generic_mapping_read 最終調(diào)用 ext2_readpage 函數(shù)處理讀數(shù)據(jù)請求。 到此為止, ext2 文件系統(tǒng)層的工作結(jié)束。 從上文得知:ext2_readpage 函數(shù)是該層的入口點(diǎn)。該函數(shù)調(diào)用 mpage_readpage 函數(shù),清單6顯示了 mpage_readpage 函數(shù)的代碼。 清單6 mpage_readpage 函數(shù)的代碼
該函數(shù)首先調(diào)用函數(shù) do_mpage_readpage 函數(shù)創(chuàng)建了一個(gè) bio 請求,該請求指明了要讀取的數(shù)據(jù)塊所在磁盤的位置、數(shù)據(jù)塊的數(shù)量以及拷貝該數(shù)據(jù)的目標(biāo)位置——緩存區(qū)中 page 的信息。然后調(diào)用 mpage_bio_submit 函數(shù)處理請求。 mpage_bio_submit 函數(shù)則調(diào)用 submit_bio 函數(shù)處理該請求,后者最終將請求傳遞給函數(shù) generic_make_request ,并由 generic_make_request 函數(shù)將請求提交給通用塊層處理。 到此為止, page cache 層的處理結(jié)束。 generic_make_request 函數(shù)是該層的入口點(diǎn),該層只有這一個(gè)函數(shù)處理請求。清單7顯示了函數(shù)的部分代碼 清單7 generic_make_request 函數(shù)部分代碼
主要操作:
到此為止,通用塊層的操作結(jié)束。 對 make_request_fn 函數(shù)的調(diào)用可以認(rèn)為是 IO 調(diào)度層的入口,該函數(shù)用于向請求隊(duì)列中添加請求。該函數(shù)是在創(chuàng)建請求隊(duì)列時(shí)指定的,代碼如下(blk_init_queue 函數(shù)中):
函數(shù) blk_queue_make_request 將函數(shù) __make_request 的地址賦予了請求隊(duì)列 q 的 make_request_fn 成員,那么, __make_request 函數(shù)才是 IO 調(diào)度層的真實(shí)入口。 __make_request 函數(shù)的主要工作為:
將請求放入到請求隊(duì)列中后,何時(shí)被處理就由 IO 調(diào)度器的調(diào)度算法決定了(有關(guān) IO 調(diào)度器的算法內(nèi)容請參見參考資料)。一旦該請求能夠被處理,便調(diào)用請求隊(duì)列中成員 request_fn 所指向的函數(shù)處理。這個(gè)成員的初始化也是在創(chuàng)建請求隊(duì)列時(shí)設(shè)置的:
第一行是將請求處理函數(shù) rfn 指針賦給了請求隊(duì)列的 request_fn 成員。而 rfn 則是在創(chuàng)建請求隊(duì)列時(shí)通過參數(shù)傳入的。 對請求處理函數(shù) request_fn 的調(diào)用意味著 IO 調(diào)度層的處理結(jié)束了。 request_fn 函數(shù)是塊設(shè)備驅(qū)動層的入口。它是在驅(qū)動程序創(chuàng)建請求隊(duì)列時(shí)由驅(qū)動程序傳遞給 IO 調(diào)度層的。 IO 調(diào)度層通過回調(diào) request_fn 函數(shù)的方式,把請求交給了驅(qū)動程序。而驅(qū)動程序從該函數(shù)的參數(shù)中獲得上層發(fā)出的 IO 請求,并根據(jù)請求中指定的信息操作設(shè)備控制器(這一請求的發(fā)出需要依據(jù)物理設(shè)備指定的規(guī)范進(jìn)行)。 到此為止,塊設(shè)備驅(qū)動層的操作結(jié)束。 接受來自驅(qū)動層的請求,完成實(shí)際的數(shù)據(jù)拷貝工作等等。同時(shí)規(guī)定了一系列規(guī)范,驅(qū)動程序必須按照這個(gè)規(guī)范操作硬件。 當(dāng)設(shè)備完成了 IO 請求之后,通過中斷的方式通知 cpu ,而中斷處理程序又會調(diào)用 request_fn 函數(shù)進(jìn)行處理。 當(dāng)驅(qū)動再次處理該請求時(shí),會根據(jù)本次數(shù)據(jù)傳輸?shù)慕Y(jié)果通知上層函數(shù)本次 IO 操作是否成功,如果成功,上層函數(shù)解鎖 IO 操作所涉及的頁面(在 do_generic_mapping_read 函數(shù)中加的鎖)。 該頁被解鎖后, do_generic_mapping_read() 函數(shù)就可以再次成功獲得該鎖(數(shù)據(jù)的同步點(diǎn)),并繼續(xù)執(zhí)行程序了。之后,函數(shù) sys_read 可以返回了。最終 read 系統(tǒng)調(diào)用也可以返回了。 至此, read 系統(tǒng)調(diào)用從發(fā)出到結(jié)束的整個(gè)處理過程就全部結(jié)束了。
本文介紹了 linux 系統(tǒng)調(diào)用 read 的處理全過程。該過程分為兩個(gè)部分:用戶空間的處理和核心空間的處理。在用戶空間中通過 0x80 中斷的方式將控制權(quán)交給內(nèi)核處理,內(nèi)核接管后,經(jīng)過6個(gè)層次的處理最后將請求交給磁盤,由磁盤完成最終的數(shù)據(jù)拷貝操作。在這個(gè)過程中,調(diào)用了一系列的內(nèi)核函數(shù)。如圖 6 圖6 read 系統(tǒng)調(diào)用在內(nèi)核中所經(jīng)歷的函數(shù)調(diào)用層次 |
|