read系統調用剖析_第1頁
read系統調用剖析_第2頁
read系統調用剖析_第3頁
read系統調用剖析_第4頁
已閱讀5頁,還剩12頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

1、read系統調用剖析 read 系統調用剖析 級別:初級 趙健博(),碩士,中國科學院計算技術研究所 2021 年 3 月 13 日 大部分程序員可能會有這樣的疑問:當在程序中調用庫函數 read 時,這個請求是經過哪些處理最終到達磁盤的呢,數據又是怎么被拷貝到用戶緩存區的呢?本文介紹了從 read 系統調用發出到結束處理的全過程。該過程包括兩個部分:用戶空間的處理、核心空間的處理。用戶空間處理部分是系統調用從用戶態切到核心態的過程。核心空間處理部分則是 read 系統調用在 linux 內核中處理的整個過程。read 系統調用在用戶空間中的處理過程 linux 系統調用(sci,system

2、 call interface)的實現機制實際上是一個多路匯聚以及分解的過程,該匯聚點就是 0x80 中斷這個入口點(x86 系統結構)。也就是說,所有系統調用都從用戶空間中匯聚到 0x80 中斷點,同時保存具體的系統調用號。當 0x80 中斷處理程序運行時,將根據系統調用號對不同的系統調用分別處理(調用不同的內核函數處理)。系統調用的更多內容,請參見參考資料。 read 系統調用也不例外,當調用發生時,庫函數在保存 read 系統調用號以及參數后,陷入 0x80 中斷。這時庫函數工作結束。read 系統調用在用戶空間中的處理也就完成了。 read 系統調用在核心空間中的處理過程 0x80 中

3、斷處理程序接管執行后,先檢察其系統調用號,然后根據系統調用號查找系統調用表,并從系統調用表中得到處理 read 系統調用的內核函數sys_read,最后傳遞參數并運行 sys_read 函數。至此,內核真正開始處理read 系統調用(sys_read 是 read 系統調用的內核入口)。 在講解 read 系統調用在核心空間中的處理部分中,首先介紹了內核處理磁盤請求的層次模型,然后再按該層次模型從上到下的順序依次介紹磁盤讀請求在各層的處理過程。 read 系統調用在核心空間中處理的層次模型 圖 1 顯示了 read 系統調用在核心空間中所要經歷的層次模型。從圖中看出:對于磁盤的一次讀請求,首先

4、經過虛擬文件系統層(vfs layer),其次是具體的文件系統層(例如 ext2),接下來是 cache 層(page cache 層)、通用塊層(generic block layer)、io 調度層(i/o scheduler layer)、塊設備驅動層(block device driver layer),最后是物理塊設備層(block device layer) 圖 1 read 系統調用在核心空間中的處理層次 虛擬文件系統層的作用:屏蔽下層具體文件系統操作的差異,為上層的操作提供一個統一的接口。正是因為有了這個層次,所以可以把設備抽象成文件,使得操作設備就像操作文件一樣簡單。在具體的

5、文件系統層中,不同的文件系統(例如 ext2 和 ntfs)具體的操作過程也是不同的。每種文件系統定義了自己的操作集合。關于文件系統的更多內容,請參見參考資料。引入 cache 層的目的是為了提高 linux 操作系統對磁盤訪問的性能。cache 層在內存中緩存了磁盤上的部分數據。當數據的請求到達時,如果在 cache 中存在該數據且是最新的,則直接將數據傳遞給用戶程序,免除了對底層磁盤的操作,提高了性能。通用塊層的主要工作是:接收上層發出的磁盤請求,并最終發出 io 請求。該層隱藏了底層硬件塊設備的特性,為塊設備提供了一個通用的抽象視圖。io調度層的功能:接收通用塊層發出的 io 請求,緩存

6、請求并試圖合并相鄰的請求(如果這兩個請求的數據在磁盤上是相鄰的)。并根據設置好的調度算法,回調驅動層提供的請求處理函數,以處理具體的 io 請求。驅動層中的驅動程序對應具體的物理塊設備。它從上層中取出 io 請求,并根據該 io 請求中指定的信息,通過向具體塊設備的設備控制器發送命令的方式,來操縱設備傳輸數據。設備層中都是具體的物理設備。定義了操作具體設備的規范。相關的內核數據結構: dentry:聯系了文件名和文件的 i 節點 inode:文件 i 節點,保存文件標識、權限和內容等信息 file:保存文件的相關信息和各種操作文件的函數指針集合:操作文件的函數接口集合 address_spac

7、e:描述文件的 page cache 結構以及相關信息,并包含有操作 page cache 的函數指針集合address_space_operations:操作 page cache 的函數接口集合 bio:io 請求的描述數據結構之間的關系: 圖 2 示意性地展示了上述各個數據結構(除了 bio)之間的關系。可以看出:由 dentry 對象可以找到 inode 對象,從 inode 對象中可以取出 address_space 對象,再由 address_space 對象找到address_space_operations 對象。 file 對象可以根據當前進程描述符中提供的信息取得,進而可以

8、找到dentry 對象、address_space 對象和對象。 圖 2 數據結構關系圖: 前提條件: 對于具體的一次 read 調用,內核中可能遇到的處理情況很多。這里舉例其中的一種情況: 要讀取的文件已經存在文件經過 page cache 要讀的是普通文件磁盤上文件系統為 ext2 文件系統,有關 ext2 文件系統的,參見參考資料準備: 注:所有清單中代碼均來自 linux 2.6.11 內核原代碼 讀數據之前,必須先打開文件。處理 open 系統調用的內核函數為sys_open。所以我們先來看一下該函數都作了哪些事。清單 1 顯示了 sys_open的代碼(省略了部分內容,以后的程序清

9、單同樣方式處理) 清單 1 sys_open 函數代碼 asmlinkage long sys_open(const char _user* flags,int mode)fd=get_unused_fd();if(fd=0)struct (tmp,flags,mode);fd_install(fd,f);return fd; 代碼解釋: get_unuesed_fd():取回一個未被使用的文件描述符(每次都會選取最小的未被使用的文件描述符)。filp_open():調用 open_namei()函數取出和該文件相關的 dentry 和 inode(因為前提指明了文件已經存在,所以 dentr

10、y 和 inode能夠查找到,不用創建),然后調用 dentry_open()函數創建新的 file 對象,并用 dentry 和 inode 中的信息初始化 file 對象(文件當前的讀寫位置在 file對象中保存)。注意到 dentry_open()中有一條語句:f-f_op=fops_get(inode-i_fop); 這個賦值語句把和具體文件系統相關的,操作文件的函數指針集合賦給了file 對象的 f _op 變量(這個指針集合是保存在 inode 對象中的),在接下來的sys_read 函數中將會調用中的成員 read。 fd_install():以文件描述符為索引,關聯當前進程描述

11、符和上述的 file對象,為之后的 read 和 write 等操作作準備。函數最后返回該文件描述符。圖3 顯示了 sys_open 函數返回后,file 對象和當前進程描述符之間的關聯關系,以及 file 對象中操作文件的函數指針集合的來源(inode 對象中的成員i_fop)。 圖 3 file 對象和當前進程描述符之間的關系 到此為止,所有的準備工作已經全部結束了,下面開始介紹 read 系統調用在圖 1 所示的各個層次中的處理過程。 虛擬文件系統層的處理: 內核函數 sys_read()是 read 系統調用在該層的入口點,清單 2 顯示了該函數的代碼。 清單 2 sys_read 函

12、數的代碼 asmlinkage ssize_t sys_read(unsigned int fd,char _user*buf,size_t count)struct ;ssize_t ret=-ebadf;int fput_needed;(fd,fput_needed);if(file)loff_t pos=(file);ret=vfs_read();();fput_light();return ret; 代碼解析: fget_light():根據 fd 指定的索引,從當前進程描述符中取出相應的file 對象(見圖 3)。如果沒找到指定的 file 對象,則返回錯誤如果找到了指定的 file

13、 對象:調用()函數取出此次讀寫文件的當前位置。調用 vfs_read()執行文件讀取操作,而這個函數最終調用()指向的函數,代碼如下:if()ret=(); 調用()更新文件的當前讀寫位置。調用 fput_light()更新文件的引用計數。最后返回讀取數據的字節數。到此,虛擬文件系統層所做的處理就完成了,控制權交給了 ext2 文件系統層。 在解析 ext2 文件系統層的操作之前,先讓我們看一下 file 對象中 read 指針來源。 file 對象中 read 函數指針的來源: 從前面對 sys_open 內核函數的分析來看,來自于 inode-i_fop。那么inode-i_fop 來自

14、于哪里呢?在初始化 inode 對象時賦予的。見清單 3。 清單 3 ext2_read_inode()函數部分代碼 void ext2_read_inode(struct inode*inode)if(s_isreg(inode-i_mode)inode-i_op=ext2_;inode-i_fop=ext2_;if(test_opt(inode-i_sb,nobh)inode-i_mapping-a_ops=ext2_nobh_aops;else inode-i_mapping-a_ops=ext2_aops; 從代碼中可以看出,如果該 inode 所關聯的文件是普通文件,則將變量ext2

15、_的地址賦予 inode 對象的 i_fop 成員。所以可以知道:inode-i_fop.read函數指針所指向的函數為 ext2_變量的成員 read 所指向的函數。下面來看一下ext2_變量的初始化過程,如清單 4。 清單 4 ext2_的初始化 struct ext2_.llseek=generic_,; 該成員 read 指向函數 generic_。所以,inode-i_fop.read 指向 generic_函數,進而指向 generic_函數。最終得出結論:generic_函數才是 ext2 層的真實入口。 ext2 文件系統層的處理 圖 4 read 系統調用在 ext2 層中處

16、理時函數調用關系 由圖 4 可知,該層入口函數 generic_調用函數_generic_,后者判斷本次讀請求的訪問方式,如果是直接 io(filp-f_flags 被設置了 o_direct 標志,即不經過 cache)的方式,則調用 generic_函數;如果是 page cache 的方式,則調用 do_generic_函數。函數 do_generic_僅僅是一個包裝函數,它又調用do_generic_mapping_read 函數。 在講解 do_generic_mapping_read 函數都作了哪些工作之前,我們再來看一下文件在內存中的緩存區域是被怎么組織起來的。 文件的 page

17、 cache 結構 圖 5 顯示了一個文件的 page cache 結構。文件被分割為一個個以 page 大小為單元的數據塊,這些數據塊(頁)被組織成一個多叉樹(稱為 radix 樹)。樹中所有葉子節點為一個個頁幀結構(struct page),表示了用于緩存該文件的每一 個頁。在葉子層最左端的第一個頁保存著該文件的前 4096 個字節(如果頁的大小為 4096 字節),接下來的頁保存著文件第二個 4096 個字節,依次類推。樹中的所有中間節點為組織節點,指示某一地址上的數據所在的頁。此樹的層次可以從 0 層到 6 層,所支持的文件大小從 0 字節到 16 t 個字節。樹的根節點指針可以從和文

18、件相關的 address_space 對象(該對象保存在和文件關聯的 inode 對象中)中取得(更多關于 page cache 的結構內容請參見參考資料)。 圖 5 文件的 page cache 結構 現在,我們來看看函數 do_generic_mapping_read 都作了哪些工作,do_generic_mapping_read 函數代碼較長,本文簡要介紹下它的主要流程: 根據文件當前的讀寫位置,在 page cache 中找到緩存請求數據的 page 如果該頁已經最新,將請求的數據拷貝到用戶空間否則,lock 該頁調用 readpage函數向磁盤發出添頁請求(當下層完成該 io 操作時

19、會解鎖該頁),代碼:error=mapping-a_ops-readpage(filp,page); 再一次 lock 該頁,操作成功時,說明數據已經在 page cache 中了,因為只有 io 操作完成后才可能解鎖該頁。此處是一個同步點,用于同步數據從磁盤到內存的過程。解鎖該頁到此為止數據已經在 page cache 中了,再將其拷貝到用戶空間中(之后 read 調用可以在用戶空間返回了)到此,我們知道:當頁上的數據不是最新的時候,該函數調用 mapping-a_ops-readpage 所指向的函數(變量 mapping 為 inode 對象中的 address_space 對象),那么

20、這個函數到底是什么呢? readpage 函數的由來 address_space 對象是嵌入在 inode 對象之中的,那么不難想象:address_space 對象成員 a_ops 的初始化工作將會在初始化 inode 對象時進行。如清單 3 中后半部所顯示。 if(test_opt(inode-i_sb,nobh)inode-i_mapping-a_ops=ext2_nobh_aops;else inode-i_mapping-a_ops=ext2_aops; 可以知道 address_space 對象的成員 a_ops 指向變量 ext2_aops 或者變量ext2_nobh_aops。

21、這兩個變量的初始化如清單 5 所示。 清單 5 變量 ext2_aops 和變量 ext2_nobh_aops 的初始化 struct address_space_operations ext2_aops=.readpage=ext2_readpage,.readpages=ext2_readpages,.writepage=ext2_writepage,.sync_page=block_sync_page,.prepare_write=ext2_prepare_write,.commit_write=generic_commit_write,.bmap=ext2_bmap,.direct_i

22、o=ext2_direct_io,.writepages=ext2_writepages,;struct address_space_operations ext2_nobh_aops=.readpage=ext2_readpage,.readpages=ext2_readpages,.writepage=ext2_writepage,.sync_page=block_sync_page,.prepare_write=ext2_nobh_prepare_write,.commit_write=nobh_commit_write,.bmap=ext2_bmap,.direct_io=ext2_d

23、irect_io,.writepages=ext2_writepages,; 從上述代碼中可以看出,不論是哪個變量,其中的 readpage 成員都指向函數 ext2_readpage。所以可以斷定:函數 do_generic_mapping_read 最終調用ext2_readpage 函數處理讀數據請求。 到此為止,ext2 文件系統層的工作結束。 page cache 層的處理 從上文得知:ext2_readpage 函數是該層的入口點。該函數調用mpage_readpage 函數,清單 6 顯示了 mpage_readpage 函數的代碼。 清單 6 mpage_readpage 函數

24、的代碼 int mpage_readpage(struct page*page,get_block_t get_block)struct bio*bio=null;sector_t last_block_in_bio=0;bio=do_mpage_readpage(bio,page,1,last_block_in_bio,get_block);if(bio)mpage_bio_submit(read,bio);return 0; 該函數首先調用函數 do_mpage_readpage 函數創建了一個 bio 請求,該請求指明了要讀取的數據塊所在磁盤的位置、數據塊的數量以及拷貝該數據的目標位置-

25、緩存區中 page 的信息。然后調用 mpage_bio_submit 函數處理請求。mpage_bio_submit 函數則調用 submit_bio 函數處理該請求,后者最終將請求傳遞給函數 generic_make_request,并由 generic_make_request 函數將請求提交給通用塊層處理。 到此為止,page cache 層的處理結束。 通用塊層的處理 generic_make_request 函數是該層的入口點,該層只有這一個函數處理請求。清單 7 顯示了函數的部分代碼 清單 7 generic_make_request 函數部分代碼 void generic_ma

26、ke_request(struct bio*bio)dochar bbdevname_size;q=bdev_get_queue(bio-bi_bdev);block_wait_queue_running(q);/*if this device has partitions,remap block n*of partition pto block n+start(p)of the disk.*/blk_partition_remap(bio);ret=q-make_request_fn(q,bio);while(ret); 主要操作: 根據 bio 中保存的塊設備號取得請求隊列 q 檢測當前

27、io 調度器是否可用,如果可用,則繼續;否則等待調度器可用調用 q-make_request_fn 所指向的函數將該請求(bio)加入到請求隊列中到此為止,通用塊層的操作結束。 io 調度層的處理 對 make_request_fn 函數的調用可以認為是 io 調度層的入口,該函數用于向請求隊列中添加請求。該函數是在創建請求隊列時指定的,代碼如下(blk_init_queue 函數中): q-request_fn=rfn;blk_queue_make_request(q,_make_request); 函數 blk_queue_make_request 將函數_make_request 的地址

28、賦予了請求隊列 q 的 make_request_fn 成員,那么,_make_request 函數才是 io 調度層的真實入口。 _make_request 函數的主要工作為: 檢測請求隊列是否為空,若是,延緩驅動程序處理當前請求(其目的是想積累更多的請求,這樣就有機會對相鄰的請求進行合并,從而提高處理的性能),并跳到 3,否則跳到 2 試圖將當前請求同請求隊列中現有的請求合并,如果合并成功,則函數返回,否則跳到 3 該請求是一個新請求,創建新的請求描述符,并初始化相應的域,并將該請求描述符加入到請求隊列中,函數返回將請求放入到請求隊列中后,何時被處理就由 io 調度器的調度算法決定了(有關

29、 io調度器的算法內容請參見參考資料)。一旦該請求能夠被處理,便調用請求隊列 中成員 request_fn 所指向的函數處理。這個成員的初始化也是在創建請求隊列時設置的: q-request_fn=rfn;blk_queue_make_request(q,_make_request); 第一行是將請求處理函數 rfn 指針賦給了請求隊列的 request_fn 成員。而rfn 則是在創建請求隊列時通過參數傳入的。 對請求處理函數 request_fn 的調用意味著 io 調度層的處理結束了。 塊設備驅動層的處理 request_fn 函數是塊設備驅動層的入口。它是在驅動程序創建請求隊列時由驅動程序傳遞給 io 調度層的。 io 調度層通過回調 request_fn 函數的方式,把請求交給了驅動程序。而驅動程序從該函數的參數中獲得上層發出的 io 請求,并根據請求中指定的信息操作設備控制

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論