Linux應用開發:文件IO進階
文章目錄
linux應用開發:文件IO進階
一、管理文件方式
1.1 存儲文件方式
1.2 操作文件方式
1.3 文件打開時狀態
二、錯誤處理
2.1 errno變量
2.2 strerror 函數
2.3 perror 函數
三、程序退出
四、空洞文件
五、open函數的標志補充
5.1 O_TRUNC 標志
5.2 O_APPEND 標志
六、文件重復打開
七、文件描述符的復制
7.1 dup 函數
7.2 dup2 函數
八、共享文件
九、原子操作與競爭冒險
9.1 競爭冒險
9.2 原子操作
十、文件管理函數
10.1 fcntl 函數
10.2 ioctl 函數
十一、截斷文件
Linux應用開發:文件IO進階
一、管理文件方式
1.1 存儲文件方式
文件存放在磁盤文件系統中,以一種固定的形式進行存放,稱為靜態文件
硬盤的最小存儲單位叫做“扇區”(Sector),每個扇區儲存 512 字節(相當于 0.5KB), 操作系統讀取硬盤的時候是一次性連續讀取多個扇區組成的“塊”,‘’塊‘’是文件存取的最小單位。“塊”的大小,常見的是 4KB,即連續八個 sector 組成一個 block
1.2 操作文件方式
靜態文件的數據存放在磁盤設備不同的“塊”中,那調用文件操作函數時是如何找到對應的‘’塊‘’呢?
方法就是對磁盤進行分區,最開始格式化的時候會將其分為兩個區域: 數據區和 inode 區
inode table 中存放的是一個一個的 inode(也成為 inode 節點),不同的 inode 就可以表示不同的文件,每一個文件都必須對應一個 inode,inode 實質上是一個結構體,這個結構體中有很多的元素來記錄文件的不同信息,如文件字節大小、文件所有者、文 件對應的讀/寫/執行權限、文件時間戳(創建時間、更新時間等)、文件類型、文件數據存儲的 block(塊) 位置等等信息
inode table 和 inode 關系圖
每一個文件都有唯一的一個 inode,每一個 inode 都有一個與之相對應的數字編號,通過這個數字編號就可以找到 inode table 中所對應的 inode
可以通過 ls -i 或者 stat查看文件 inode 編號:
綜上,系統打開文件的方式一般分為三步:
操作系統找到這個文件名所對應的 inode 編號;
通過 inode 編號從 inode table 中找到對應的 inode 結構體;
根據 inode 結構體中記錄的信息,確定文件數據所在的 block,并讀出數據。
1.3 文件打開時狀態
當調用 open 函數去打開文件的時候,內核會申請一段內存(一段緩沖區),并且將靜態文件內容從磁盤讀取到內存中進行管理、緩存
這段內存中的文件數據叫做動態文件或者內核緩沖區
打開文件后,對這個文件的讀寫操作,都是針對內存中這一份動態文件進行相關的操作, 而并不是針對磁盤中存放的靜態文件,主要因為靜態文件存放在磁盤上,操作單元是以塊為單位,且讀寫速率低,如果在內存中,操作單元就是以字節為單位,讀寫靈活且速度快!
動態文件和靜態文件不是自動同步的,當讀寫完成后,系統內核會自動同步動態文件到靜態文件
我們知道在 RTOS 里面有任務控制塊 TCB ,在 Linux 系統中也有進程控制塊 PCB ,就是一個專門的數據結構用于管理該進程,譬如用于記錄進程的狀態信息、運行特征等,在 Linux 的 PCB 中有有一個指針指向了文件描述符表(File descriptors),文件描述符表中的每一個元素索引到對應的文件表(File table),文件表也是一個數據結構體,其中記錄了很多文件相關的信息,譬如文件狀態標志、引用計數、當前文件的讀寫偏移量以及 i-node 指針(指向該文件對應的 inode)等,進程打開的所有文件對應的文件描述符都記錄在文件描述符表中,每一個文件描述符都會指向一個對應的文件表, 其示意圖如下所示:
二、錯誤處理
2.1 errno變量
代碼執行時肯定會遇到各種錯誤,所以 Linux 系統對常見的錯誤做了一個編號,每一個 編號都代表著每一種不同的錯誤類型,當函數執行發生錯誤的時候,操作系統會將這個錯誤所對應的編號賦值給 errno 變量,每一個進程(程序)都維護了自己的 errno 變量,它是程序中的全局變量,該變量用于存儲就近發生的函數執行錯誤編號,也就意味著下一次的錯誤碼會覆蓋上一次的錯誤碼
errno 是一個 int 類型的變量,調用系統調用或者庫函數出錯時,Linux 會設置 errno ,具體函數的錯誤可以通過 man 手冊進行查詢,如 man 2 open
想要在進程中使用 errno 方法就是包含他的頭文件,直接調用就行
#include
1
之后就可以在程序中直接使用 errno !
2.2 strerror 函數
2.1節中的 errno變量是一個錯誤編號,而strerror() 該函數可以將對應的 errno 轉換成適合我們查看的字符串信息,函數原型:
#include
1
2
3
傳入 errno ,函數返回錯誤字符串
代碼示例:
打開一個不存在的文件,看看 strerror 會返回什么保存:
1 #include
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
編譯執行,報錯找不到路徑,很直觀的顯示錯誤信息
2.3 perror 函數
perror 函數直接用于查看錯誤信息, 調用此函數不需要傳入 errno,函數內部會自己去獲取 errno 變量的值,調用此函數會直接將錯誤提示字符串打印出來,而不是返回字符串,除此之外還可以在輸出的錯誤提示字符串之前加入自己的打印信息,函數原型如下:
#include
1
2
參照2.2里面的代碼,將 strerror 換成如下代碼
perror("ERR Test:");
1
編譯執行結果:
三、程序退出
Linux 系統下,進程(程序)退出可以分為正常退出和異常退出,異常退出一般是程序錯誤,或者系統異常報錯,這里不研究,這里研究一下正常退去,及程序手動退出,進程正常退出除了可以使用 return 之外,還可以使用 exit()、_exit() 以及 _Exit(),三者函數原型如下:
exit() :
#include
1
2
_exit() :
#include
1
2
_Exit() :
#include
1
2
具體功能:
四、空洞文件
空洞文件就是文件里面存在空洞,沒有存儲任何數據,然依然占據了一部分文件空間,以下面的圖為例子
文件總大小為 3k ,首尾各 1k 大小的實際數據,中間 1k 字節為空,稱為空洞,該文件就是空洞文件
空洞文件的主要作用就是 多線程操作文件,如果一個文件不同部分需要幾個進程一起寫入,空洞文件就可以預留對應要寫入數據的空間位置
五、open函數的標志補充
補充上一章 文件IO 入門中 open 函數的 flags 標志
5.1 O_TRUNC 標志
使用 O_TRUNC 標志,調用 open 函數打開文件的時候會原文件的內容全部丟棄,文件大小變為 0 ,相對于清空后打開,編寫例程:
1 #include
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
程序執行,將 test.c 清空了
5.2 O_APPEND 標志
O_APPEND 標志,調用 open 函數打開文件, 當每次使用 write()函數對文件進行寫操作時,都會自動把文件當前位置偏移量移動到文件末尾,從文件末尾開始寫入數據,續寫文件,使用方式和上面差不多,不做示例了
六、文件重復打開
一個文件在一個進程或者說多個進程中是可以被多次打開的,有如下的一些注意要點
一個進程內多次 open 打開同一個文件,那么會得到多個不同的文件描述符 fd,同理在關閉文件的 時候也需要調用 close 依次關閉各個文件描述符
一個進程內多次 open 打開同一個文件,在內存中并不會存在多份動態文件,即使多次打開同一個文件,內 存中也只有一份動態文件
一個進程內多次 open 打開同一個文件,不同文件描述符所對應的讀寫位置偏移量是相互獨立的
加入了 O_APPEND 標志后,分別寫已經變成了接續寫,相對于打開的多個文件描述符的偏移量被關聯起來了
七、文件描述符的復制
open 返回得到的文件描述符 fd 可以進行復制,復制成功之后可以得到一個新的文件描述符,復制得到的文件描述符與舊的文件描述符指向的是同一個文件表,使用新的文件描述符和舊的文件描述符都可以對文件進行 IO 操作,復制得到的文件描述符和舊的文件描述符擁有相同的權限,譬如使用舊的文件描述符對文件有讀寫權限,那么新的文件描述符同樣也具 有讀寫權限;在 Linux 系統下,可以使用 dup 或 dup2 這兩個系統調用對文件描述符進行復制
7.1 dup 函數
函數原型
#include
1
2
oldfd: 需要被復制的文件描述符。
返回值: 成功時將返回一個新的文件描述符,由操作系統分配,分配置原則遵循文件描述符分配原則; 如果復制失敗將返回-1,并且會設置 errno 值。
7.2 dup2 函數
函數原型
#include
1
2
oldfd: 需要被復制的文件描述符。
newfd: 指定一個文件描述符(需要指定一個當前進程沒有使用到的文件描述符)。
返回值: 成功時將返回一個新的文件描述符,也就是手動指定的文件描述符 newfd;如果復制失敗將返 回-1,并且會設置 errno 值。
八、共享文件
文件共享指的是同一個文件(譬如磁盤上的同一個文件,對應同一個 inode)被 多個獨立的讀寫體同時進行 IO 操作。多個獨立的讀寫體大家可以將其簡單地理解為對應于同一個文件的多 個不同的文件描述符,譬如多次打開同一個文件所得到的多個不同的 fd,或使用 dup()(或 dup2)函數復制 得到的多個不同的 fd 等
文件共享的核心是:如何制造出多個不同的文件描述符來指向同一個文件。其實方法在上面的內容中都 已經給大家介紹過了,譬如多次調用 open 函數重復打開同一個文件得到多個不同的文件描述符、使用 dup() 或 dup2()函數對文件描述符進行復制以得到多個不同的文件描述符
常見的三種文件共享的實現方式
同一個進程中多次調用 open 函數打開同一個文件,各數據結構之間的關系如下圖所示:
多次調用 open 函數打開同一個文件會得到多個不同的文件描述符,并且多個文件描述符對應多個不同的文件表,所有的文件表都索引到了同一個 inode 節點,也就是磁盤上的同一個文件
不同進程中分別使用 open 函數打開同一個文件,其數據結構關系圖如下所示:
上兩個獨立的進程(理解為兩個獨立的程序),在他們各自的 程序中分別調用 open 函數打開同一個文件,獲得對應的文件描述符,文件描述符各自的文件表都索引到了同一個 inode 節 點,從而實現共享文件
同一個進程中通過 dup(dup2)函數對文件描述符進行復制,其數據結構關系如下圖所示:
兩個文件描述符共同使用一個文件表!
九、原子操作與競爭冒險
9.1 競爭冒險
操作系統的一個特色就是支持多進程、多線程,有的時候會遇到多個進程對有限的資源進行競爭,打斷對方的操作,導致另外一個進程的程序執行錯誤,這種現象稱為競爭冒險
比如有兩個進程A和B,A和B共享文件1,初始時偏移量都是0,進程A寫入數據到文件1,寫到100位置時,A的偏移量在100,時間片耗盡,任務切換到B,此時B的偏移量為0,B開始寫入100個字節,覆蓋掉了A,造成了A的不正常執行,產生了競爭與冒險
簡單理解一下就是:每個進程(或線程)去操作文件的順序是不可預期的,即這些進程獲得 CPU 使用權的先后順序是不可預期的,完全由操作系統調配,進程與線程處于競爭狀態
為了避免競爭狀態的進程與線程互相打斷,Linux 引入了原子操作
9.2 原子操作
原子操作就是將多步操作組成的一個操作,原子操作要么一步也不執行,要么一旦執行,必須要執行完所有步驟,不能只執行所有步驟中的一個子集
以 9.1 的示例為例子,因為A和B覆蓋寫入,我們引入原子操作,在讀寫之前加入一個 lseek 定位,定位到文件尾部進行寫入,比如系統調用 pread() 和 pwrite() ,函數原型如下
#include
1
2
3
調用 pread 函數或 pwrite 函數可傳入一個位置偏移量 offset 參數, 用于指定文件當前讀或寫的位置偏移量,所以調用 pread 相當于調用 lseek 后再調用 read;同理,調用 pwrite 相當于調用 lseek 后再調用 write,lseek 和 讀寫綁定執行,形成原子操作,Linux 的原子操作很多,后面遇到再分析
十、文件管理函數
本小節介紹兩個新的系統調用:fcntl()和 ioctl()
10.1 fcntl 函數
fcntl()函數可以對一個已經打開的文件描述符執行一系列控制操作,譬如復制一個文件描述符、獲取/設置文件描述符標志、獲取/設置文件狀態標志等,類似于一個多功能文件描述符管理工具箱,函數原型如下:
#include
1
2
3
4
cmd 操作命令大致可以分為以下 5 種功能:
復制文件描述符(cmd=F_DUPFD 或 cmd=F_DUPFD_CLOEXEC);
獲取/設置文件描述符標志(cmd=F_GETFD 或 cmd=F_SETFD);
獲取/設置文件狀態標志(cmd=F_GETFL 或 cmd=F_SETFL);
獲取/設置異步 IO 所有權(cmd=F_GETOWN 或 cmd=F_SETOWN);
獲取/設置記錄鎖(cmd=F_GETLK 或 cmd=F_SETLK);
10.2 ioctl 函數
ioctl()可以認為是一個文件 IO 操作的雜物箱,可以處理的事情非常雜、不統一,一般用于操作特殊文件或硬件外設,譬如可以通過 ioctl 獲取 LCD 相關信息等,后面學習到在詳細分析,函數原型
#include
1
2
3
十一、截斷文件
用系統調用 truncate()或 ftruncate()可將普通文件截斷為指定字節長度,其函數原型如下所示:
#include
1
2
3
4
5
Linux Unix
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。