對于傳統(tǒng)的操作系統(tǒng)來說,普通的 I/O 操作一般會被內(nèi)核緩存,這種 I/O 被稱作緩存 I/O。本文所介紹的文件訪問機制不經(jīng)過操作系統(tǒng)內(nèi)核的緩存,數(shù)據(jù)直接在磁盤和應(yīng)用程序地址空間進(jìn)行傳輸,所以該文件訪問的機制稱作為直接 I/O。Linux 中就提供了這樣一種文件訪問機制,對于那種將 I/O 緩存存放在用戶地址空間的應(yīng)用程序來說,直接 I/O 是一種非常高效的手段。
“Linux 中一切皆文件”這句話已經(jīng)不知道說了多少遍了,后面也會提到很多次。那么在深入學(xué)習(xí)之前,肯定要掌握對 Linux 文件的各種操作,包括讀、寫、創(chuàng)建等基本知識。
本章配套視頻為:
“視頻 05_01 文件 IO 之 open 打開操作”
“視頻 05_02 文件 IO 之 creat 創(chuàng)建操作”
“視頻 05_03 文件 IO 之 write 寫操作”
“視頻 05_04 文件 IO 之 read 讀操作”
16.1 Linux 中 中 IO 的概念介紹
所有的 I/O 操作都是通過讀文件或者寫文件來完成的。在這里,把所有的外圍設(shè)備,包括鍵盤和顯示器,都看成是文件系統(tǒng)中的文件。
什么是緩存 I/O
緩存 I/O 又被稱作標(biāo)準(zhǔn) I/O,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統(tǒng)會將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache ) 中,也就是說,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。緩存 I/O 有
以下這些優(yōu)點:
緩存 I/O 使用了操作系統(tǒng)內(nèi)核緩沖區(qū),在一定程度上分離了應(yīng)用程序空間和實際的物理設(shè)備。
緩存 I/O 可以減少讀盤的次數(shù),從而提高性能。
當(dāng)應(yīng)用程序嘗試讀取某塊數(shù)據(jù)的時候,如果這塊數(shù)據(jù)已經(jīng)存放在了頁緩存中,那么這塊數(shù)據(jù)就可以立即返回給應(yīng)用程序,而不需要經(jīng)過實際的物理讀盤操作。當(dāng)然,如果數(shù)據(jù)在應(yīng)用程序讀取之前并未被存放在頁緩存中,那么就需要先將數(shù)據(jù)從磁盤讀到頁緩存中去。對于寫操作來說,應(yīng)用程序也會將數(shù)據(jù)先寫到
頁緩存中去,數(shù)據(jù)是否被立即寫到磁盤上去取決于應(yīng)用程序所采用的寫操作機制:如果用戶采用的是同步寫機制( synchronous writes ), 那么數(shù)據(jù)會立即被寫回到磁盤上,應(yīng)用程序會一直等到數(shù)據(jù)被寫完為止;如果用戶采用的是延遲寫機制( deferred writes ),那么應(yīng)用程序就完全不需要等到數(shù)據(jù)全部被寫回到磁盤,數(shù)據(jù)只要被寫到頁緩存中去就可以了。在延遲寫機制的情況下,操作系統(tǒng)會定期地將放在頁緩存中的數(shù)據(jù)刷到磁盤上。與異步寫機制( asynchronous writes )不同的是,延遲寫機制在數(shù)據(jù)完全寫到磁盤上的時候不會通知應(yīng)用程序,而異步寫機制在數(shù)據(jù)完全寫到磁盤上的時候是會返回給應(yīng)用程序的。所以延遲寫機制本身是存在數(shù)據(jù)丟失的風(fēng)險的,而異步寫機制則不會有這方面的擔(dān)心。
緩存 I/O 的缺點
在緩存 I/O 機制中,DMA 方式可以將數(shù)據(jù)直接從磁盤讀到頁緩存中,或者將數(shù)據(jù)從頁緩存直接寫回到磁盤上,而不能直接在應(yīng)用程序地址空間和磁盤之間進(jìn)行數(shù)據(jù)傳輸,這樣的 話,數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和頁緩存之間進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是
非常大的。
對于某些特殊的應(yīng)用程序來說,避開操作系統(tǒng)內(nèi)核緩沖區(qū)而直接在應(yīng)用程序地址空間和磁盤之間傳輸數(shù)據(jù)會比使用操作系統(tǒng)內(nèi)核緩沖區(qū)獲取更好的性能,下邊這一小節(jié)中提到的自緩存應(yīng)用程序就是其中的一種。
小貼士-關(guān)于文件的相關(guān)知識的一點說明
在 Linux 中,文件非常重要,所以在 linux 系統(tǒng)中提供了豐富的文件操作函數(shù)。
在系統(tǒng)編程中,只會介紹編程中用到的最終的 API 和必須掌握的知識,還有大量的和文件相關(guān)的知識,例如高級 IO,制作文件系統(tǒng),虛擬文件系統(tǒng),文件共享,網(wǎng)絡(luò)文件系統(tǒng)等等。
如果真是要詳細(xì)的介紹整個 linux 文件,可能好幾千頁的書都介紹不完,而且對于大家學(xué)習(xí)初期也沒有太大的幫助,得不償失。
手冊中還會以其它形式介紹文件相關(guān)的知識,有的時候會介紹一步一步介紹如何操作,例如制作文件系統(tǒng),NFS 網(wǎng)絡(luò)啟動;有時候會介紹如何使用,例如內(nèi)核教程中介紹的虛擬文件系統(tǒng)等等。大家如果感興趣,在學(xué)習(xí)教程之外可以通過學(xué)習(xí)相關(guān)知識,去了解更加具體的含義。
文件對于 Linux 實在是太重要了,不過大家學(xué)習(xí)了本章的知識和實驗,了解其他和文件系統(tǒng)相關(guān)的知識,在后面 Linux 編程中遇到的問題基本可以自行解決了。
函數(shù)頭文件
在所有的 Linux 系統(tǒng)中,如果需要對文件的進(jìn)行操作,只要包含如下 4 個頭文件即可。
#include
#include
#include
#include
上面四個頭文件中包含了打開,關(guān)閉,創(chuàng)建,讀文件,寫文件的函數(shù),還有標(biāo)志位,以及在不同 32 位以及 64 位系統(tǒng)下數(shù)據(jù)長度的宏變量定義。
16.2 打開文件函數(shù) open
使用 open 函數(shù)的時候會返回一個文件句柄,文件句柄是文件的唯一識別符 ID。對文件的操作必須從讀取句柄開始。
先來看一下函數(shù) open 的兩個原型。
int open(const char *path, int oflags);
有兩個參數(shù)的‘open’函數(shù)主要用于創(chuàng)建文件,在本章的 10.5.5 小節(jié)會和創(chuàng)建文件的函數(shù) creat 的同時介紹具體用法,并給出例子。
int open(const char *path, int oflags,mode_t mode);
open 函數(shù)可以建立一個文件或者設(shè)備的訪問路徑。在打開或創(chuàng)建文件時可以指定文件的屬性及用戶的
權(quán)限等參數(shù)。
第一個參數(shù) path 表示:路徑名或者文件名。路徑名為絕對路徑名,例如開發(fā)板中的 led 驅(qū)動的設(shè)備節(jié)點/dev/leds。
第二個參數(shù) oflags 表示:打開文件所采取的動作。下 面 三 個 選 項 是 必 須 選 擇 其 中 之 一的 。
O_RDONLY 文件只讀
O_WRONLY 文件只寫
O_RDWR 文件可讀可寫
下面是可以任意選擇的。
O_APPEND 每次寫操作都寫入文件的末尾
O_CREAT 如果指定文件不存在,則創(chuàng)建這個文件
O_EXCL 如果要創(chuàng)建的文件已存在,則返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只寫/讀寫方式打開,則清空文件全部內(nèi)容
O_NOCTTY 如果路徑名指向終端設(shè)備,不要把這個設(shè)備用作控制終端。
O_NONBLOCK 如果路徑名指向 FIFO/塊文件/字符文件,則把文件的打開和后繼 I/O 設(shè)置為非阻塞模式
(nonblocking mode),后面會介紹什么是阻塞和非阻塞。
O_NDELAY 和 O_NONBLOCK 功能類似,調(diào)用 O_NDELAY 和使用的 O_NONBLOCK 功能是一樣的。第三個參數(shù) mode 表示:設(shè)置創(chuàng)建文件的權(quán)限。
S_IRUSR,S_IWUSER,S_IXUSR,S_IRGRP,S_IWGRP,S_IXGRP,S_IROTH,S_IWOTH,S_IXOTH. 其中 R:讀,W:寫,X:執(zhí)行,USR:文件所屬的用戶,GRP:文件所屬的組,OTH: 其他用戶。第三個參數(shù)可以直接使用參數(shù)代替,參考 10.4.5 小節(jié)‘Linux 權(quán)限’。
前面用過的‘chmod 0777 helloworld’命令,其中的含義是一樣的,只不過 chmod 是在文件創(chuàng)建之后再修改權(quán)限。
open 函數(shù)代碼
編寫簡單的 open.c 文件測試 open 函數(shù)。首先添加頭文件,如下圖所示。
首先添加頭文件,如下圖所示。 ![]()
然后 main 函數(shù)如下。 ![]()
上圖中打開了三個文件分別屬于不同的情況。
/dev/leds 已經(jīng)在開發(fā)板中存在,屬于驅(qū)動的設(shè)備節(jié)點,在 linux 驅(qū)動教程中會具體介紹
/bin/test1 和/bin/test2 都不存在
使用 open 函數(shù)調(diào)用上面三個文件,如果出錯就會打印錯誤,然后打印句柄。
編譯運行測試
在 Ubuntu 系統(tǒng)下,如下圖所示,進(jìn)入前面實驗創(chuàng)建的目錄“/home/linuxsystemcode/iofile”,使用命令“mkdir iofile”新建 iofile 文件夾,將源碼 open.c 拷貝進(jìn)去,進(jìn)入新建的文件夾 iofileopen,如下圖所示。 ![]()
使用命令“arm-none-linux-gnueabi-gcc -o open open.c -static”編譯 open 文件,如下圖所示,使用命令“l(fā)s”可以看到生成了 open 可執(zhí)行文件。 ![]()
這里介紹 U 盤拷貝代碼的方法,也可以編譯進(jìn)文件系統(tǒng),具體方法參考 10.3.5 小節(jié)。將編譯成的可執(zhí)行文件 open,拷貝到 U 盤,啟動開發(fā)板,插入 U 盤,加載 U 盤,運行程序如下。 ![]()
如上圖所示,可以看到打開/dev/leds 成功,這個是板載 LED 的內(nèi)核驅(qū)動,調(diào)用的時候, 還調(diào)用了內(nèi)核驅(qū)動中的函數(shù),這個函數(shù)會打印“LEDS_CTL DEBUG evice Opened Success!”和“LEDS_CTL DEBUG eviceOpened Success!”
然后打印句柄 ID,/dev/leds fd is 3
調(diào)用“/bin/test1”會報錯“open /bin/test1 failed”,這種打開文件的方式是 linux 中標(biāo)準(zhǔn)的用法,幾乎所有對文件的 open 操作都會加上出錯報警的語句。
創(chuàng)建“/bin/test2”會打印“/bin/test2 fd is 4”,表明創(chuàng)建“/bin/test2 ”成功了。
使用命令“l(fā)s /bin/test2”,查看一下對應(yīng)目錄‘/bin’下應(yīng)該新建了“test2”,如下圖所示。 ![]()
另外的“dev/leds”本身就存在,如下圖所示,這是驅(qū)動的設(shè)備節(jié)點文件,在后面的實驗會介紹如何操作調(diào)用,在 linux 驅(qū)動實驗中會介紹這個設(shè)備節(jié)點文件是如何生成的。 ![]()
16.3 創(chuàng)建函數(shù) creat 和 和 open
creat 函數(shù)介紹
關(guān)于 creat 函數(shù),首先這個單詞并不是表示創(chuàng)建的意思,創(chuàng)建的英文單詞是“create”, 這是早期的一個小的拼寫錯誤,卻一直沿用下來。
在介紹 open 函數(shù)的時候,可以看到 open 函數(shù)有兩種形式,一個是兩個參數(shù)一個是三個參數(shù),早期的時候 open 只有三個參數(shù)的形式,三個參數(shù)的形式會導(dǎo)致 open 函數(shù)無法打開一個未創(chuàng)建的文件,也就是無法建立文件,所以就有了這個 creat 函數(shù)。
現(xiàn)在 creat 函數(shù)可以完全用 open 替代,考慮到在閱讀代碼的時候可能會碰到,所以簡單介紹一下。
creat 函數(shù)原型如下。
int creat(const char * pathname, mode_t mode);
creat 函數(shù)只有兩個參數(shù),參數(shù)的含義和 open 類似。大家看到這個函數(shù)的時候知道它是創(chuàng)建文件的就成,在寫代碼的時候完全可以用 open 代替。
creat 函數(shù)例程
編寫簡單的 creat.c 文件測試 creat 函數(shù)。首先添加頭文件,如下所示。 ![]()
然后 main 函數(shù)如下所示。 ![]()
第 22 行、27 行、31,open 可以打開已有的文件,也可以打開不存在的文件,即創(chuàng)建文件,創(chuàng)建文件的時候需要在參數(shù)中添加標(biāo)志位 O_CREAT。在第 27 行代碼中,沒有添加標(biāo)志位,運行的時候肯定會報錯,這么寫是希望大家能夠記住這個參數(shù)。
第 36 行是使用 creat 函數(shù)創(chuàng)建文件“test3”,注意一下 creat 函數(shù)和 open 函數(shù)在創(chuàng)建文件的時候,參數(shù)的區(qū)別。
編譯運行測試 在 Ubuntu 系統(tǒng)下,如下圖所示,進(jìn)入前面實驗創(chuàng)建的目錄“/home/linuxsystemcode/iofile”,將源碼creat.c 拷貝進(jìn)去,如下圖所示。 ![]()
使用命令“arm-none-linux-gnueabi-gcc -o creat creat.c -static”編譯 creat.c 文件,如下圖所示,使用命令“l(fā)s”可以看到生成了 creat 可執(zhí)行文件。 ![]()
這里介紹 U 盤拷貝代碼的方法,也可以編譯進(jìn)文件系統(tǒng),具體方法參考 10.3.5 小節(jié)
將編譯成的可執(zhí)行文件 creat,拷貝到 U 盤,啟動開發(fā)板,插入 U 盤,加載 U 盤,運行程序如下。 ![]()
如上圖所示。
打開文件"/dev/leds"成功,這個文件已經(jīng)存在
打開文件"/bin/test1"失敗,因為沒有添加參數(shù) O_CREAT,這個文件不存在,新建的時候需要參數(shù)
O_CREAT。
打開文件"/bin/test2"成功,不存在這個文件,創(chuàng)建成功。
打開文件"/bin/test3"成功,不存在這個文件,使用 creat 新建成功。
如下圖所示,使用命令“l(fā)s /bin/test* ”在“/bin”目錄下可以看到新建的文件 test2 和 test3。 ![]()
16.4 關(guān)閉函數(shù) close
任何一個文件在操作完成之后都需要關(guān)閉,這個時候需要調(diào)用 close 函數(shù)。
close 函數(shù)介紹
調(diào)用 close 函數(shù)之后,會取消 open 函數(shù)建立的映射關(guān)系,句柄將不再有效,占用的空間將被系統(tǒng)釋放。
close 函數(shù)在頭文件“#include ”中,close 函數(shù)的使用和參數(shù)都比較簡單.
int close(int fd);
參數(shù) fd,使用 open 函數(shù)打開文件之后返回的句柄。返回值,一般很少使用 close 的返回值。
close 函數(shù)例程
調(diào)用很簡單,在下一個實驗中會永到 close 函數(shù)。 16.5 寫函數(shù) write
對文件進(jìn)行寫操作,write 函數(shù)使用的比較多。
write 函數(shù)介紹
write 函數(shù)在頭文件“#include ”中。
函數(shù)原型為 ssize_t write(int fd,const void *buf,size_t count) 參數(shù) fd,使用 open 函數(shù)打開文件之后返
回的句柄。
參數(shù)*buf,需要寫入的數(shù)據(jù)。
參數(shù) count,將參數(shù)*buf 中最多 count 個字節(jié)寫入文件中。
返回值為 ssize 類型,出錯會返回-1,其它數(shù)值表示實際寫入的字節(jié)數(shù)。
write 函數(shù)例程
編寫簡單的 write.c 文件測試 write 函數(shù)。首先添加頭文件,如下所示。
//標(biāo)準(zhǔn)輸入輸出頭文件
#include
//文件操作函數(shù)頭文件
#include
#include
#include
#include
#include
然后 main 函數(shù)如下所示。 ![]()
如上圖代碼所示。
在 16 行定義了 buffer_write 字符數(shù)組。
在 18 行,進(jìn)行寫操作之前,必須得到文件的句柄,在這一行中使用 open 函數(shù)創(chuàng)建和打開文件“/bin/testwrite"。
在 23 行中會調(diào)用 write 函數(shù),將 buffer_write 字符數(shù)組中的內(nèi)容寫到新建的文件中。
在 31 行調(diào)用 close 函數(shù),將"/bin/testwrite"文件關(guān)閉。
后面測試的時候可以在超級終端中,使用 vi 編輯器打開"/bin/testwrite"文件,可以看到這個文件中有字符 Hello Write Function!.
編譯運行測試
在 Ubuntu 系統(tǒng)下,如下圖所示,進(jìn)入前面實驗創(chuàng)建的目錄“/home/linuxsystemcode/iofile”,將源碼write.c 拷貝進(jìn)去,如下圖所示。 ![]()
使用命令“arm-none-linux-gnueabi-gcc -o write write.c -static”編譯 write.c 文件,如下圖所示,使用命令“l(fā)s”可以看到生成了 write 可執(zhí)行文件。 ![]()
這里介紹 U 盤拷貝代碼的方法,也可以編譯進(jìn)文件系統(tǒng)。
將編譯成的可執(zhí)行文件 write,拷貝到 U 盤,啟動開發(fā)板,插入 U 盤,加載 U 盤,運行程序。如下圖所示,打印出了 Write Function OK!。 ![]()
在代碼中定義的文件是"/bin/testwrite",使用 vi 編輯器打開文件,如下圖所示,程序執(zhí)行運行成功。 ![]()
16.6 文件的讀 read 函數(shù)
對文件進(jìn)行寫操作,read 函數(shù)使用的比較多。
read 函數(shù)介紹
read 函數(shù)在頭文件“#include ”中。
函數(shù)原型為 ssize_t read(int fd,void *buf,size_t len)
參數(shù) fd,使用 open 函數(shù)打開文件之后返回的句柄。
參數(shù)*buf,讀出的數(shù)據(jù)保存的位置。
參數(shù) len,每次最多讀 len 個字節(jié)。
返回值為 ssize 類型,出錯會返回-1,其它數(shù)值表示實際寫入的字節(jié)數(shù),返回值大于 0 小于 len 的數(shù)
值都是正常的。
read 函數(shù)例程
編寫簡單的 read.c 文件測試 read 函數(shù)。
首先添加頭文件和定義讀函數(shù)緩沖區(qū)為 1000,如下圖所示。
//標(biāo)準(zhǔn)輸入輸出頭文件
#include
//文件操作函數(shù)頭文件 #include
#include
#include
#include
#include
然后 main 函數(shù)如下圖所示。 ![]()
如上代碼所示。 在 1-9 行,頭文件。
在 20 行,使用 open 函數(shù)打開或者新建"/bin/testwrite"文件。
在 23 行,使用 write 函數(shù)將 buffer 中的內(nèi)容寫到"/bin/testwrite"文件中。在 35 行,使用 read 函數(shù),將"/bin/testwrite"文件中的內(nèi)容讀出來。
在 38 行,使用打印函數(shù) printf 打印 read 函數(shù)讀出的數(shù)據(jù)。在 39 行,調(diào)用 close 函數(shù)關(guān)閉打開的文件,程序結(jié)束。
最終測試的時候,除了會出現(xiàn)"/bin/testwrite"文件,還會打印 read 函數(shù)讀取的數(shù)據(jù)。
編譯運行測試
在 Ubuntu 系統(tǒng)下,如下圖所示,進(jìn)入前面實驗創(chuàng)建的目錄“/home/linuxsystemcode/iofile”,將源碼read.c 拷貝進(jìn)去,如下圖所示。 ![]()
使用命令“arm-none-linux-gnueabi-gcc -o read read.c -static”編譯 read.c 文件,如下圖所示,使用命令“l(fā)s”可以看到生成了 read 可執(zhí)行文件。 ![]()
這里介紹 U 盤拷貝代碼的方法,也可以編譯進(jìn)文件系統(tǒng)。
將編譯成的可執(zhí)行文件 read,拷貝到 U 盤,啟動開發(fā)板,插入 U 盤,加載 U 盤,運行程序。如下圖所示,可以看到打印出了“Files Content is Hello Write Function”,使代碼中預(yù)期的結(jié)果。 ![]()
![]()
|