Linux read系統調用,linuxread系統調用

來源:互聯網
上載者:User

Linux read系統調用,linuxread系統調用

最近一個項目做了一個類比u盤的裝置,但是在read虛擬u盤的內容時必須每次都從磁碟內讀取,而不是從系統的cache中讀取,由於這個問題,就查資料看了下read的系統調用,以及檔案系統的一些內容。由於檔案系統涉及面較廣,例如虛擬檔案系統(VFS),頁緩衝,塊緩衝,資料同步等內容,不可能全部分析到位,這裡只記錄和read有關的兩種使用方式。cached IO和direct IO。

1. 什麼是系統調用

首先系統調用能做那些事呢?概括來說,大概有下面這些事需要系統調用來實現。

  1. 控制硬體:系統調用往往作為硬體資源和使用者空間的抽象介面,比如讀寫檔案時用到的write/read調用。
  2. 設定系統狀態或讀取核心資料:因為系統調用是使用者空間和核心的唯一通訊手段,所以使用者佈建系統狀態,比如開/關某項核心服務(設定某個核心變數),或讀取核心資料都必須通過系統調用。比如getpgid、getpriority、setpriority、sethostname
  3. 進程管理:用來保證系統中進程能以多任務在虛擬記憶體環境下得以運行。比如 fork、clone、execve、exit等

那為什麼一定要用系統調用來訪問作業系統的內容呢,其實這可以看做對核心的保護,linux分為使用者空間和核心空間,而使用者空間是不允許訪問核心空間的資料的。那麼在使用者空間的程式需要訪問核心空間的資源時就必須通過系統調用這個中間人來實現。這樣可以對使用者空間的行為進行限制,只有特定的得到許可的(事先規定的)使用者空間行為才能進入核心空間。一句話,系統調用是核心給使用者空間提供的一個可以訪問核心資源的一個介面。

另外多說一句,從使用者進程切換到核心進程只有兩種方式,一種就是系統調用,另一種是中斷。

要實現系統調用,首先要能從使用者空間切換到核心空間,這個切換在IA-32系統上是用彙編指令int $0x80來引發軟體中斷實現的。這部分內容一般是在C標準庫中實現的。進入核心空間後,系統調用中樞處理代碼(所有的系統調用都由一處中樞代碼處理)根據傳遞的參數(參數是有寄存器傳遞的包括唯一的系統調用號)和一個靜態表分別執行不同的函數。例如read系統調用,0x80 中斷處理常式接管執行後,先檢查其系統調用號,然後根據系統調用號尋找系統調用表,並從系統調用表中得到處理 read 系統調用的核心功能 sys_read ,最後傳遞參數並運行 sys_read 函數。至此,核心真正開始處理 read 系統調用(sys_read 是 read 系統調用的核心入口)。

2. read系統調用在核心空間的處理層次模型

為read 系統調用在核心空間中所要經曆的層次模型。看出:對於磁碟的一次讀請求,首先經過虛擬檔案系統層(vfs layer),其次是具體的檔案系統層(例如 ext2),接下來是 cache 層(page cache 層)、通用塊層(generic block layer)、IO 調度層(I/O scheduler layer)、塊裝置驅動層(block device driver layer),最後是物理塊裝置層(block device layer)。

  • 虛擬檔案系統層的作用:屏蔽下層具體檔案系統操作的差異,為上層的操作提供一個統一的介面。正是因為有了這個層次,所以可以把裝置抽象成檔案,使得操作裝置就像操作檔案一樣簡單。
  • 在具體的檔案系統層中,不同的檔案系統(例如 ext2 和 NTFS)具體的操作過程也是不同的。每種檔案系統定義了自己的操作集合。關於檔案系統的更多內容,請參見參考資料。
  • 引入 cache 層的目的是為了提高 linux 作業系統對磁碟訪問的效能。 Cache 層在記憶體中緩衝了磁碟上的部分資料。當資料的請求到達時,如果在 cache 中存在該資料且是最新的,則直接將資料傳遞給使用者程式,免除了對底層磁碟的操作,提高了效能。
  • 通用塊層的主要工作是:接收上層發出的磁碟請求,並最終發出 IO 請求。該層隱藏了底層硬體塊裝置的特性,為塊裝置提供了一個通用的抽象視圖。
  • IO 調度層的功能:接收通用塊層發出的 IO 請求,緩衝請求並試圖合并相鄰的請求(如果這兩個請求的資料在磁碟上是相鄰的)。並根據設定好的調度演算法,回調驅動層提供的請求處理函數,以處理具體的 IO 請求。
  • 驅動層中的驅動程式對應具體的物理塊裝置。它從上層中取出 IO 請求,並根據該 IO 請求中指定的資訊,通過向具體塊裝置的裝置控制器發送命令的方式,來操縱裝置傳輸資料。
  • 裝置層中都是具體的物理裝置。定義了操作具體裝置的規範。
 3. 相關的核心資料結構
  • dentry(目錄項) : 聯絡了檔案名稱和檔案的 i 節點
  • inode(索引節點) : 檔案 i 節點,儲存檔案標識、許可權和內容等資訊
  • file : 儲存檔案的相關資訊和各種操作檔案的函數指標集合
  • file_operations :操作檔案的函數介面集合
  • address_space :描述檔案的 page cache 結構以及相關資訊,並包含有操作 page cache 的函數指標集合
  • address_space_operations :操作 page cache 的函數介面集合
  • bio : IO 請求的描述

以上結構的定義可參考VFS檔案系統以及核心源碼。

資料結構之間的關係如所示:

展示了上述各個資料結構(除了 bio)之間的關係。可以看出:由 dentry 對象可以找到 inode 對象,從 inode 對象中可以取出 address_space 對象,再由 address_space 對象找到 address_space_operations 對象。File 對象可以根據當前進程描述符中提供的資訊取得,進而可以找到 dentry 對象、 address_space 對象和 file_operations 對象。

4. read系統調用的過程4.1. 前提條件

對於具體的一次 read 調用,核心中可能遇到的處理情況很多。這裡舉例其中的一種情況:

  • 要讀取的檔案已經存在
  • 檔案經過 page cache
  • 要讀的是普通檔案
  • 磁碟上檔案系統為 ext2 檔案系統,有關 ext2 檔案系統的相關內容,參見參考資料
4.2. read前的open

open系統調用對應的核心功能是sys_open。sys_open調用do_sys_open:

long do_sys_open(int dfd, const char __user *filename, int flags, int mode){struct open_flags op;int lookup = build_open_flags(flags, mode, &op);char *tmp = getname(filename);int fd = PTR_ERR(tmp);if (!IS_ERR(tmp)) {fd = get_unused_fd_flags(flags);if (fd >= 0) {struct file *f = do_filp_open(dfd, tmp, &op, lookup);if (IS_ERR(f)) {put_unused_fd(fd);fd = PTR_ERR(f);} else {fsnotify_open(f);fd_install(fd, f);}}putname(tmp);}return fd;}

其中主要代碼的解釋:

  • get_unused_fd_flags:取回一個未被使用的檔案描述符(每次都會選取最小的未被使用的檔案描述符)。
  • do_filp_open:調用 open_namei() 函數取出和該檔案相關的 dentry 和 inode (因為前提指明了檔案已經存在,所以 dentry 和 inode 能夠尋找到,不用建立),然後調用 dentry_open() 函數建立新的 file 對象,並用 dentry 和 inode 中的資訊初始化 file 對象(檔案當前的讀寫位置在 file 對象中儲存)。注意到 dentry_open() 中有一條語句:f->f_op = fops_get(inode->i_fop);
    這個賦值語句把和具體檔案系統相關的,操作檔案的函數指標集合賦給了 file 對象的 f _op 變數(這個指標集合是儲存在 inode 對象中的),在接下來的 sys_read 函數中將會調用 file->f_op 中的成員 read 。
  • fd_install:以檔案描述符為索引,關聯當前進程描述符和上述的 file 對象,為之後的 read 和 write 等操作作準備。

函數最後返回該檔案的檔案描述符。

4.3. 虛擬檔案系統層的處理

read系統調用對應的核心功能是sys_read。實現如下(read_write.c):

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count){ struct file *file; ssize_t ret = -EBADF; int fput_needed;file = fget_light(fd, &fput_needed); if (file) { loff_t pos = file_pos_read(file); ret = vfs_read(file, buf, count, &pos); file_pos_write(file, pos); fput_light(file, fput_needed); }return ret;}

代碼解析:

  • fget_light() :根據 fd 指定的索引,從當前進程描述符中取出相應的 file 對象。
  • 調用 file_pos_read() 函數取出此次讀寫檔案的當前位置。
  • 調用 vfs_read() 執行檔案讀取操作,而這個函數最終調用 file->f_op.read() 指向的函數,代碼如下:

if (file->f_op->read)

ret = file->f_op->read(file, buf, count, pos);

  • 調用 file_pos_write() 更新檔案的當前讀寫位置。
  • 調用 fput_light() 更新檔案的引用計數。
  • 最後返回讀取資料的位元組數。

到此,虛擬檔案系統層所做的處理就完成了,控制權交給了 ext2 檔案系統層。

4.4. ext2層及後續的處理

查看ext2_file_operations的初始化,我們可以看到,ext2的read指向do_sync_read,而在do_sync_read中又調用了ext2的aio_read函數,而aio_read指向generic_file_aio_read,所以generic_file_aio_read就是ext2層的入口。

generic_file_aio_read的大致走向(filemap.c):

4.4.1. 檔案的page cache結構

在 Linux 作業系統中,當應用程式需要讀取檔案中的資料時,作業系統先分配一些記憶體,將資料從存放裝置讀入到這些記憶體中,然後再將資料分發給應用程式;當需要往檔案 中寫資料時,作業系統先分配記憶體接收使用者資料,然後再將資料從記憶體寫到磁碟上。檔案 Cache 管理指的就是對這些由作業系統分配,並用來隱藏檔資料的記憶體的管理。 Cache 管理的優劣通過兩個指標衡量:一是 Cache 命中率,Cache 命中時資料可以直接從記憶體中擷取,不再需要訪問低速外設,因而可以顯著提高效能;二是有效 Cache 的比率,有效 Cache 是指真正會被訪問到的 Cache 項,如果有效 Cache 的比率偏低,則相當部分磁碟頻寬會被浪費到讀取無用 Cache 上,而且無用 Cache 會間接導致系統記憶體緊張,最後可能會嚴重影響效能。

檔案 Cache 是檔案資料在記憶體中的副本,因此檔案 Cache 管理與記憶體管理系統和檔案系統都相關:一方面檔案 Cache 作為實體記憶體的一部分,需要參與實體記憶體的分配回收過程,另一方面檔案 Cache 中的資料來源於存放裝置上的檔案,需要通過檔案系統與存放裝置進行讀寫互動。從作業系統的角度考慮,檔案 Cache 可以看做是記憶體管理系統與檔案系統之間的聯絡紐帶。因此,檔案 Cache 管理是作業系統的一個重要組成部分,它的效能直接影響著檔案系統和記憶體管理系統的效能。

Linux核心中檔案預讀演算法的具體過程是這樣的:對於每個檔案的第一個讀請求,系統讀入所請求的頁面並讀入緊隨其後的少數幾個頁面(不少於一 個頁面,通常是三個頁面),這時的預讀稱為同步預讀。對於第二次讀請求,如果所讀頁面不在Cache中,即不在前次預讀的group中,則表明檔案訪問不 是順序訪問,系統繼續採用同步預讀;如果所讀頁面在Cache中,則表明前次預讀命中,作業系統把預讀group擴大一倍,並讓底層檔案系統讀入 group中剩下尚不在Cache中的檔案資料區塊,這時的預讀稱為非同步預讀。無論第二次讀請求是否命中,系統都要更新當前預讀group的大小。此外,系 統中定義了一個window,它包括前一次預讀的group和本次預讀的group。任何接下來的讀請求都會處於兩種情況之一:第一種情況是所請求的頁面 處於預讀window中,這時繼續進行非同步預讀並更新相應的window和group;第二種情況是所請求的頁面處於預讀window之外,這時系統就要 進行同步預讀並重設相應的window和group。

檔案被分割為一個個以 page 大小為單元的資料區塊,這些資料區塊(頁)被組織成一個多叉樹(稱為 radix 樹)。樹中所有葉子節點為一個個頁幀結構(struct page),表示了用於緩衝該檔案的每一個頁。在葉子層最左端的第一個頁儲存著該檔案的前4096個位元組(如果頁的大小為4096位元組),接下來的頁儲存著檔案第二個4096個位元組,依次類推。樹中的所有中間節點為組織節點,指示某一地址上的資料所在的頁。此樹的層次可以從0層到6層,所支援的檔案大小從0位元組到16 T 個位元組。樹的根節點指標可以從和檔案相關的 address_space 對象(該對象儲存在和檔案關聯的 inode 對象中)中取得。

一個物理頁可能由多個不連續的物理磁碟塊組成。也正是由於頁面中映射的磁碟塊不一定連續,所以在頁快取中檢測特定資料是否已被緩衝就變得不那麼容易了。另外linux頁快取對被快取頁面的範圍定義的非常寬。緩衝的目標是任何基於頁的對象,這包含各種類型的檔案和各種類型的記憶體映射。為了滿足普遍性要求,linux使用定義在linux/fs.h中的結構體address_space結構體描述頁快取中的頁面。

4.4.2. ext2層的處理

do_generic_file_read做的工作:

  • 根據檔案當前的讀寫位置,在 page cache 中找到緩衝請求資料的 page
  • 如果該頁已經最新,將請求的資料拷貝到使用者空間
  • 否則, Lock 該頁
  • 調用 readpage 函數向磁碟發出添頁請求(當下層完成該 IO 操作時會解鎖該頁),代碼:error = mapping->a_ops->readpage(filp, page);
  • 再一次 lock 該頁,操作成功時,說明資料已經在 page cache 中了,因為只有 IO 操作完成後才可能解鎖該頁。此處是一個同步點,用於同步資料從磁碟到記憶體的過程。
  • 解鎖該頁
  • 到此為止資料已經在 page cache 中了,再將其拷貝到使用者空間中(之後 read 調用可以在使用者空間返回了)

到此,我們知道:當頁上的資料不是最新的時候,該函數調用 mapping->a_ops->readpage 所指向的函數(變數 mapping 為 inode 對象中的 address_space 對象),那麼這個函數到底是什麼呢?在Ext2檔案系統中,readpage指向ext2_readpage。

4.4.3. page cache層的處理

從上文得知:ext2_readpage 函數是該層的進入點。該函數調用 mpage_readpage 函數,如下mpage_readpage 函數的代碼。

int mpage_readpage(struct page *page, get_block_t get_block){ struct bio *bio = NULL; sector_t last_block_in_bio = 0; struct buffer_head map_bh; unsigned long first_logical_block = 0; map_bh.b_state = 0; map_bh.b_size = 0; bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio, &map_bh, &first_logical_block, get_block); if (bio) mpage_bio_submit(READ, bio); return 0;}

該函數首先調用函數 do_mpage_readpage 函數建立了一個 bio 請求,該請求指明了要讀取的資料區塊所在磁碟的位置、資料區塊的數量以及拷貝該資料的目標位置——緩衝區中 page 的資訊。然後調用 mpage_bio_submit 函數處理請求。 mpage_bio_submit 函數則調用 submit_bio 函數處理該請求,後者最終將請求傳遞給函數 generic_make_request ,並由 generic_make_request 函數將請求提交給通用塊層處理。

到此為止, page cache 層的處理結束。

4.4.4. 通用塊層的處理

generic_make_request 函數是該層的進入點,該層只有這一個函數處理請求。函數的代碼參見blk-core.c。

主要操作:
根據 bio 中儲存的塊裝置號取得請求隊列 q
檢測當前 IO 調度器是否可用,如果可用,則繼續;否則等待調度器可用
調用 q->make_request_fn 所指向的函數將該請求(bio)加入到請求隊列中
到此為止,通用塊層的操作結束。

4.4.5. 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 的地址賦予了請求隊列 q 的 make_request_fn 成員,那麼, __make_request 函數才是 IO 調度層的真實入口。
__make_request 函數的主要工作為:

  1. 檢測請求隊列是否為空白,若是,延緩驅動程式處理當前請求(其目的是想積累更多的請求,這樣就有機會對相鄰的請求進行合并,從而提高處理的效能),並跳到3,否則跳到2。
  2. 試圖將當前請求同請求隊列中現有的請求合并,如果合并成功,則函數返回,否則跳到3。
  3. 該請求是一個新請求,建立新的請求描述符,並初始化相應的域,並將該請求描述符加入到請求隊列中,函數返回。

將請求放入到請求隊列中後,何時被處理就由 IO 調度器的調度演算法決定了(有關 IO 調度器的演算法內容請參見參考資料)。一旦該請求能夠被處理,便調用請求隊列中成員 request_fn 所指向的函數處理。這個成員的初始化也是在建立請求隊列時設定的:
q->request_fn = rfn;
blk_queue_make_request(q, __make_request);
第一行是將請求處理函數 rfn 指標賦給了請求隊列的 request_fn 成員。而 rfn 則是在建立請求隊列時通過參數傳入的。
對請求處理函數 request_fn 的調用意味著 IO 調度層的處理結束了。

4.4.6. 塊裝置驅動層的處理

request_fn 函數是塊裝置驅動層的入口。它是在驅動程式建立請求隊列時由驅動程式傳遞給 IO 調度層的。

IO 調度層通過回調 request_fn 函數的方式,把請求交給了驅動程式。而驅動程式從該函數的參數中獲得上層發出的 IO 請求,並根據請求中指定的資訊操作裝置控制器(這一請求的發出需要依據物理裝置指定的規範進行)。

到此為止,塊裝置驅動層的操作結束。

4.4.7. 塊裝置層的處理

接受來自驅動層的請求,完成實際的資料拷貝工作等等。同時規定了一系列規範,驅動程式必須按照這個規範操作硬體。

4.4.8. 後續工作

當裝置完成了 IO 請求之後,通過中斷的方式通知 cpu ,而中斷處理常式又會調用 request_fn 函數進行處理。

當驅動再次處理該請求時,會根據本次資料轉送的結果通知上層函數本次 IO 操作是否成功,如果成功,上層函數解鎖 IO 操作所涉及的頁面。

該頁被解鎖後, 就可以再次成功獲得該鎖(資料的同步點),並繼續執行程式了。之後,函數 sys_read 可以返回了。最終 read 系統調用也可以返回了。

至此, read 系統調用從發出到結束的整個處理過程就全部結束了。

來自:http://www.coderonline.net/?p=711

更多文章:核心驅動,android等文章請查看http://www.coderonline.net

關注公眾平台:程式員互動聯盟(coder_online),你可以第一時間擷取原創技術文章,和(java/C/C++/Android/Windows/Linux)技術大牛做朋友,線上交流編程經驗,擷取編程基礎知識,解決編程問題。程式員互動聯盟,開發人員自己的家。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.