檔案輸入與輸出
檔案在Linux內是什麼樣子
檔案(file) 檔案是一堆數據的有序集合 對作業系統而言,可以由「目錄系統」找到一個檔案在硬碟上的 位置 對程式而言,必須先告訴作業系統,準備「使用」哪些檔案,作 業系統會「開啟」該檔案,並給該檔案一個代碼(file descriptor),隨後該程式使用該「代碼」操作該檔案
檔案(file) 對每一個行程,每一個開啟的檔案都會有一個檔案指標 檔案指標代表目前正在對「該位置」做操作 read、write、lseek會改變檔案指標的位置 檔案指標 檔案
檔案(file) 對每一個行程,每一個開啟的檔案都會有一個檔案指標 檔案指標代表目前正在對「該位置」做操作 read、write、lseek會改變檔案指標的位置 檔案內部可能有空洞 這些空洞在邏輯意義上都是0 某些檔案系統不支援有空洞的檔案 檔案指標 檔案 空洞 空洞
為什麼檔案系統需要支援「空洞」 例如一間公司,員工編號共五碼,第1XXXX代表製造部、 2XXXX代表研發部、3XXXX代表行銷部 如果檔案系統支援「洞」,那麼可以直接使用員工編號當index, 而不需要擔心浪費磁碟空間的問題,如: 資料 洞 資料 洞 資料 洞 研發部 行銷部 製造部
用一個例子開始:mycp
mycp.c
一堆的#include <xxx.h> 問題 舉例 記得函數的名稱就好 如果忘記或者不知道include 某個.h檔案,編譯器會告訴你 某函數未定義 針對該函數使用man查詢他需 要include哪些 $man perror NAME perror - print a system error message SYNOPSIS #include <stdio.h> void perror(const char *s);
mycp.c
open int open(const char *pathname, int flags); open的傳回值是file descriptor(檔案描述子),在系統中從0開始編 號 如果前面的號碼有缺號,open會優先使用最小的號碼當file descriptor 如:系統已經使用了0, 2, 3, 4,當使用open再開啟一個檔案時,file descriptor會是「1」 一個行程能夠開啟的檔案是有限的 可以使用getrlimit()的RLIMIT_FSIZE查看 當回傳值為-1代表發生了錯誤,例如:超出RLIMIT_FSIZE
open int open(const char *pathname, int flags, mode_t mode) 當flags設定為O_CREAT時mode的意義如下 owner, group, others的權限 set-user-ID、set-group-ID及sticky bit 介紹檔案系統會再介紹權限的相關意義
open() open (argv [1], O_RDONLY); 第一個參數是“路徑名” 為了讀 為了寫 open (argv [1], O_RDONLY); 第一個參數是“路徑名” 第二個參數告訴OS開啟這個 檔案的目的是「只讀取」 open(argv[2], O_WRONLY | O_CREAT, S_IRUSR| S_IWUSR); 第一個參數是“路徑名” 第二個參數告訴OS這個檔案只用 來寫入(O_WRONLY),如果 檔案不存在,就建立檔案 (O_CREAT) 第三個參數代表新建立的檔案的 讀寫屬性(owner可讀寫)
自學open man 2 open 2代表系統裡面的第二本書, system call
open重要參數 int open(const char *pathname, int flags); O_APPEND O_TRUNC 每次都會將資料加到檔案的最尾巴,就算是多個行程同時寫入,也能保 證原子性(完整性)的加到最尾巴 O_TRUNC 將檔案大小歸為零,我們在設計存檔功能時,通常需要加上這個參數才 可以保證不會有舊資料 O_CLOEXEC 使用execve時自動關閉檔案(execve後面會介紹) 避免另外一個程序存取原程序所開啟的檔案
自我學習 先打開檔案,讀取後再儲存 int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length);
mycp.c
read() ssize_t read(int fd, void *buf, size_t count); 會從fd所代表的檔案讀取「最多」count個byte到指標buf所指向的記憶體 當回傳值大於1,代表讀取了多少個byte 回傳值等於0代表讀到了EOF(檔案結尾) 回傳值-1,代表讀取發生了錯誤
write() ssize_t write(int fd, const void *buf, size_t count); 將buf指向的資料共count個byte,寫入fd所代表的檔案 傳回值代表總共寫入了多少個byte 當傳回值為-1,代表發生了錯誤
mycp.c
perror void perror(const char *s); 依照1. 依照errno印出訊息 2. 字串S 假設errno是1,perror(“the error is”)會印出「the error is: Operation not permitted」
什麼是errno errno是系統內的錯誤訊息代碼 如果呼叫一個C函數時發生了錯誤,則errno會被設定為該錯誤所 代表的號碼 所有errno對應的錯誤訊息在sys_errlist
mycp.c
close() int close(int fd); 使用完一個檔案,使用close告訴作業系統使用完畢 作業系統會依照當時的狀況(最後一個存取該檔案的行程),決 定是否釋放相關資源 成功回傳0,失敗回傳-1
如果忘記close() 程式執行結束時,作業系統會自 動幫忙關閉檔案 但如果程式會執行很久呢? daemon? 使用lsof查看到底哪些檔案還沒 關閉 重要參數-p PROCESS_ID
自我學習 原子性的讀取和寫入 ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
小節 初步了解open、read、write,並用這幾個函數設計了簡單的cp open可以接很多參數,同學們應該主動學習
lseek & file holes
hole.c
lseek() off_t lseek(int fd, off_t offset, int whence); 將檔案fd的檔案指標移動到從whence起算,偏移offset的位置 傳統上UNIX支援的whence有三種選擇 SEEK_SET:絕對位置 SEEK_CUR:從現在位置起算 SEEK_END:從結束位置起算 傳回值為從檔案開始的偏移值 錯誤時: 在執行lseek前先將errno設定為0 檢查傳回值是否等於-1「並且」errno不為0
hole.c 因此hole.c會產生一個名為 myHole的檔案,在開始位置寫 入1,往後移動10000 byte在 寫入2,往後移動10000 byte 在寫入3 $ls myHole -lhs 12K -rw------- 1 shiwulo shiwulo 196K Jan 13 04:24 myHole /*檔案大小為196K,佔據磁碟空間12K*/
使用mycp複製myhole $ ./mycp myHole myHole2 $ ls myH* -lhs 12K -rw------- 1 shiwulo shiwulo 196K Jan 13 04:24 myHole 196K -rw------- 1 shiwulo shiwulo 196K Jan 13 04:31 myHole2 /*檔案大小都是196K,但是myHole2佔據磁碟空間196K而非12K*/ $cmp myHole myHole2 /*使用cmp比較二者無差異*/
myHole內部構造 10000個0 10000個0 1 0000…0000 2 3
進階版的mycp.c,mycp2.c(第一部分) 一開始要宣告_GNU_SOURCE才可以使用進階版的lseek()
man lseek
SEEK_HOLE & SEEK_DATA 在新版的UNIX提供這二個新的選項,但必須手動打開,即 #define _GNU_SOURCE 洞的最前面 資料的最前面 1 0000…0000 2 3
進階版的mycp.c,mycp2.c(第二部分) 取得每個資料區段的位置及大小 移動到該區段的開頭位置 進行該區段的複製
結果 $./mycp myHole myHole2 $ ./mycp2 myHole myHole3 $ ls myH* -lhs 12K -rw------- 1 shiwulo shiwulo 196K Jan 13 04:24 myHole 196K -rw------- 1 shiwulo shiwulo 196K Jan 13 05:08 myHole2 12K -rw------- 1 shiwulo shiwulo 196K Jan 13 05:09 myHole3
小節 對於Linux及大多數的UNIX而言,「洞」並不會佔據空間 讀取「洞」,裡面的值都是0,因此第一個版本的cp會讓「洞」 佔據空間 使用Linux的系統擴充,SEEK_DATA及SEEK_DATA可以找出 洞,複製時可以跳過這些洞
協調式鎖定檔案flock
lock.c
執行結果 $ ./lock myHole e fd = 3 is opened end 先執行 後執行 $ ./lock myHole e fd = 3 is opened end $ ./lock myHole e fd = 3 is opened /*被鎖住了,除非另外一個行程unlock或者結束*/
flock() int flock(int fd, int operation); LOCK_SH:分享鎖,除了互斥鎖,可以多個人同時編譯 LOCK_EX:互斥鎖,只可以這個行程進行編譯 LOCK_UN:解開這個鎖 請注意,如果另外一個行程並未使用flock,那麼另一個行程就不需要遵照這些「鎖」
強制鎖
強制鎖(不建議使用) 在Solaris, HP-UX, and Linux上可以使用set-grup-id,讓一個檔 案只可以由一個行程開啟 具體的做法是在chmod前加上2,例如: chmod 2644 test
確保寫入 sync & fsync & fdatasync
三個類似的函數 void sync(void); int fsync(int fd); int fdatasync(int fd); 將所有的資料(包含meta-data)寫回磁碟 int fsync(int fd); 將fd代表的檔案的所有的資料(包含meta-data)寫回磁碟 int fdatasync(int fd); 將fd代表的檔案的所有的資料(「不」包含meta-data)寫回 磁碟
sync.c int main() { int fd; int num; fd = open("./hello1",O_WRONLY | O_CREAT, 0644); for(num=0; num <=100000; num++) { write(fd, "1234", sizeof("1234")); fsync(fd); if (num%10000==1) { write(1, "*", sizeof("*")); fsync(1); } return 0;
datasync.c int main() { int fd; int num; fd = open("./hello3",O_WRONLY | O_CREAT, 0644); for(num=0; num <=100000; num++) { write(fd, "1234", sizeof("1234")); fdatasync(fd); if (num%10000==1) { write(1, "*", sizeof("*")); fsync(1); } return 0;
nsync.c int main() { int fd; int num; fd = open("./hello2",O_WRONLY | O_CREAT, 0644); for(num=0; num <=100000; num++) { write(fd, "1234", sizeof("1234")); if (num%10000==1) { write(1, "*", sizeof("*")); fsync(1); } return 0;
sync.c的執行結果 $ time ./sync ********** real 0m21.215s user 0m0.136s sys 0m5.272s
比較 sync fdatasync no sync real 0m21.215s 0m17.545s 0m0.076s user sys 0m5.272s 0m3.980s 0m0.068s
小結 當多個程式讀取檔案時,可以用flock上鎖,但先決條件是所有的 程式在讀取之前都先使用flock 檔案的寫入會變更檔案的屬性,此外檔案內容的變化是否先暫存 在記憶體(buffer)呢。「同步更新」的東西越多,速度越慢, 但也越安全(例如系統突然斷電)
作業 設計一個程式,允許多名助教同時輸入成績 執行檔名稱addGrade 學號 成績,所有的成績都刊登在grade檔 案內,必須按照學號排序