參考韋東山老師教程:https://www.bilibili.com/video/BV1kk4y117Tu?p=12
目錄
- 1. 文件IO函數分類
- 2. 函數原型
- 2.1 系統調用接口
- 2.2 標準IO接口
- 3. fileio內部機制
- 3.1 系統調用接口內部流程
- 3.1 dup函數使用
- 3.2 dup2函數使用
- 4. open file
- 4.1 open實例
- 4.2 open函數分析
- 5. create file
- 5.1 create實例
- 5.2 create分析
- 6. write file
- 6.1 write實例
- 6.2 write分析
- 7. read file
- 7.1 read實例
- 7.2 read分析
- 8.簡單實例——處理.csv表格
1. 文件IO函數分類
在 Linux 上操作文件時,有兩套函數:標準 IO、系統調用 IO。
標準 IO 的相關函數是:fopen/fread/fwrite/fseek/fflush/fclose 等 。
系統調用 IO 的相關函數是:open/read/write/lseek/fsync/close。
這 2 種 IO 函數的差別如下圖所示:
2. 函數原型
2.1 系統調用接口
open/read/write/lseek/fsync/close 這幾個函數的用法:
函數名 | 函數原型 | 描述 |
---|---|---|
open | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); | 作用:打開或者創建一個文件 pathnam:文件路徑名,或者文件名 flags:表示打開文件所采用的操作 O_RDONLY:只讀模式 O_WRONLY:只寫模式 O_RDWR:可讀可寫 O_APPEND 表示追加,如果原來文件里面有內容,則這次寫入會寫在文件的最末尾 O_CREAT 表示如果指定文件不存在,則創建這個文件 O_EXCL 表示如果要創建的文件已存在,則出錯,同時返回 -1,并且修改 errno 的值 O_TRUNC 表示截斷,如果文件存在,并且以只寫、讀寫方式打開,則將其長度截斷為 0 O_NOCTTY 如果路徑名指向終端設備,不要把這個設備用作控制終端 O_NONBLOCK 如果路徑名指向 FIFO/塊文件/字符文件,則把文件的打開和后繼 I/O 設置為非阻塞模式(nonblocking mode) O_DSYNC 等待物理 I/O 結束后再 write。在不影響讀取新寫入的數據的前提下,不等待文件屬性更新 O_RSYNC read 等待所有寫入同一區域的寫操作完成后再進行 O_SYNC 等待物理 I/O 結束后再 write,包括更新文件屬性的I/O mode: 文件訪問權限的初始值 |
read | #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); | 作用:從給定的文件描述符指定的文件中,讀取 count 個字節的數據,存放至 buf 中。 fd:指定要讀寫的文件描述符 buf:緩沖區,一般是一個數組,用于存放讀取的內容 count:一次要讀取的最大字節數 |
write | #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); | 作用:將 buf 中的 count 字節數據寫入指定文件描述符的文件中 fd:指定要寫入的文件描述符 buf:緩沖區,一般是一個數組,讀取存放于該數組的內容存放于文件中 count:要寫入的實際字節數 |
dup dup2 dup3 | #include <unistd.h> int dup(int oldfd); int dup2(int oldfd, int newfd); #define _GNU_SOURCE #include <fcntl.h> #include <unistd.h> int dup3(int oldfd, int newfd, int flags); | 作用:復制文件描述符,就是兩個文件句柄指向同一個文件描述符,這兩個文件句柄共享文件偏移地址、狀態 oldfd:被復制的文件句柄 newfd:復制得到的文件句柄 注意:dup 函數返回的文件句柄是“未使用的最小文件句柄”,dup2 可以指定復制得到的文件句柄為 newfd,dup3 跟 dup2 類似,flags 參數必要么是 0,要么是 O_CLOEXEC |
lseek | #include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence); | 作用:重新定位讀/寫文件偏移 fd:指定要偏移的文件描述符 offset:文件偏移量 whence:開始添加偏移 offset 的位置 SEEK_SET,offset 相對于文件開頭進行偏移 SEEK_CUR,offset 相對文件當前位置進行偏移 SEEK_END,offset 相對于文件末尾進行偏移 |
fsync | #include <unistd.h> int fsync(int fd); | 作用:同步內存中所有已修改的文件數據到儲存設備 fd:指定要同步的文件描述符 |
close | #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int close(int fd); | 作用:關閉已打開的文件 |
2.2 標準IO接口
fopen/fread/fwrite/fseek/fflush/fclose 這幾個函數的用法:
函數名 | 函數原型 | 描述 |
---|---|---|
fopen | #include<stdio.h> FILE *fopen(const char *filename, const char *mode); | 作用:打開或者創建一個文件 filename:文件路徑名,或者文件名 mode:文件的訪問模式 r 打開一個用于讀取的文件。該文件必須存在。 w 創建一個用于寫入的空文件。如果文件名稱與已存在的文件相同,則會刪除已有文件的內容,文件被視為一個新的空文件。 a 追加到一個文件。寫操作向文件末尾追加數據。如果文件不存在,則創建文件。 r+ 打開一個用于更新的文件,可讀取也可寫入。該文件必須存在。 w+ 創建一個用于讀寫的空文件。 a+ 打開一個用于讀取和追加的文件。 |
fread | #include<stdio.h> size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | 作用:從給定流 stream 讀取數據到 ptr 所指向的數組中 ptr:指向帶有最小尺寸 size*nmemb 字節的內存塊的指針 size:這是要讀取的每個元素的大小,以字節為單位 nmemb:元素的個數,每個元素的大小為 size 字節 stream:這是指向 FILE 對象的指針,該 FILE 對象指定了一個輸入流 |
fwrite | #include <stdio.h> size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); | 作用:把 ptr 所指向的數組中的數據寫入到給定流 stream 中 ptr:這是指向要被寫入的元素數組的指針 size:這是要被寫入的每個元素的大小,以字節為單位 nmemb:這是元素的個數,每個元素的大小為 size 字節 stream:這是指向 FILE 對象的指針,該 FILE 對象指定了一個輸出流 |
fseek | #include <stdio.h> int fseek(FILE *stream, long int offset, int whence); | 作用:設置流 stream 的文件位置為給定的偏移 offset stream:這是指向 FILE 對象的指針,該 FILE 對象標識了流 offset:這是相對 whence 的偏移量,以字節為單位 whence:表示開始添加偏移 offset 的位置 SEEK_SET,offset 相對于文件開頭進行偏移 SEEK_CUR,offset 相對文件當前位置進行偏移 SEEK_END,offset 相對于文件末尾進行偏移 |
fflush | #include <stdio.h> int fflush(FILE *stream); | 作用:刷新流 stream 的輸出緩沖區 stream:這是指向 FILE 對象的指針,該 FILE 對象指定了一個緩沖流 |
fclose | #include <stdio.h> int fclose(FILE *stream); | 作用:關閉流 stream,且刷新其緩沖區 stream:這是指向 FILE 對象的指針,該 FILE 對象指定了要被關閉的流 |
3. fileio內部機制
3.1 系統調用接口內部流程
在Linux操作系統中,用戶態(User Mode)和內核態(Kernel Mode)是兩種不同的處理器運行模式,它們代表了程序執行時的不同權限級別和對系統資源訪問的能力。
用戶態(User Mode):
- 應用程序運行的正常狀態。在用戶態下,進程可以執行的指令受到限制,主要目的是保護系統資源和保持系統穩定性。應用程序不能直接訪問硬件資源(如內存、CPU寄存器、I/O設備等)或者執行特權指令。
- 用戶態下的進程運行在較低的CPU特權級別(通常是Ring 3),這意味著它們沒有直接操作硬件的權限。
- 當應用程序需要執行某些特權操作(如讀寫文件、網絡通信、分配內存等)時,它必須通過系統調用(system call)向操作系統內核發出請求,這時會從用戶態轉換到內核態。
內核態(Kernel Mode):
- 操作系統內核運行的狀態,擁有最高權限。內核可以直接訪問所有硬件資源,執行任意指令,并且可以改變處理器狀態。
- 在內核態下,進程運行在最高的CPU特權級別(Ring 0),享有對所有系統資源的完全控制權。
- 內核態負責管理硬件資源、調度進程、處理中斷和系統調用等核心功能。當系統調用發生時,處理器從用戶態切換到內核態,執行內核代碼完成請求的服務,然后返回用戶態繼續執行應用程序。
- 在內核態下,還有一種特殊的上下文稱為“中斷上下文”,這是當硬件中斷發生時,CPU暫停當前任務,轉而執行中斷處理程序的狀態。
簡而言之,用戶態保證了應用程序的安全隔離,而內核態則提供了對系統資源的直接訪問和管理能力,兩者之間的轉換是操作系統管理和控制資源的關鍵機制。
fileio也是一樣,系統調用接口open/read/write/lseek/fsync/close就是用戶態應用程序能夠調用的函數,這些接口是由GLIBC(GNU C Library)提供的,用來訪問Linux的內核服務。
GLIBC提供的open/read/write/lseek/fsync/close函數訪問Linux的步驟:
- 當使用open/read/write/lseek/fsync/close函數時,會設置異常原因,如open、read等,再調用匯編指令swi/svc來觸發一個異常,并攜帶異常原因;
- CPU發現異常后會分辨異常原因,(假設是open函數)在sys_call_table[NR_open]中分辨異常原因后,會在fs/open.c文件中調用SYSCALL_DEFINE3即sys_open函數執行open文件操作。
其中
ABI(Application Binary Interface):應用二進制接口
OABI(Old ABI):舊的應用二進制接口
EABI(Embedded ABI):“E”代表“Embedded”,表示ARM嵌入式系統設計的一種新的ABI規范
sys_call_table的函數指針數組:
fs/open.c中SYSCALL_DEFINE3函數
3.1 dup函數使用
dup函數使用代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>int main(int argc, char **argv)
{char buf[10];char buf2[10];if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}int fd = open(argv[1], O_RDONLY);int fd2 = open(argv[1], O_RDONLY);int fd3 = dup(fd);printf("fd = %d\n", fd);printf("fd2 = %d\n", fd2);printf("fd3 = %d\n", fd3);if (fd < 0 || fd2 < 0 || fd3 < 0){printf("can not open %s\n", argv[1]);return -1;}read(fd, buf, 1);read(fd2, buf2, 1);printf("data get from fd : %c\n", buf[0]);printf("data get from fd2: %c\n", buf2[0]);read(fd3, buf, 1);printf("data get from fd3: %c\n", buf[0]);return 0;
}
上傳到Ubuntu后編譯運行:
3.2 dup2函數使用
dup2函數使用代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./dup2 1.txt* argc = 2* argv[0] = "./dup2"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0777);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}/* fd=1的文件是系統標準輸出文件,如printf*/dup2(fd, 1);printf("hello, world\n"); /* 打印到fd=1的文件 */return 0;
}
上傳到Ubuntu后編譯運行:
4. open file
4.1 open實例
open代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./open 1.txt* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}while (1){sleep(10);}close(fd);return 0;
}
上傳到Ubuntu后編譯運行:
補充:error變量、strerror函數、perror函數
errno:errno h頭文件定義了整數變量,它在系統調用和一些庫函數在發生錯誤時被設置,以指示出錯的地方。
在man errno中能夠找到對應的錯誤碼:
strerror:函數 char *strerror(int errnum); 指向errnum對應的字符串,使用 printf(“err: %s\n”, strerror(errno)); 就能打印出對應的錯誤碼含義:
perror:函數 void perror(const char *s); 打印參數字符串s,后跟冒號和空白,然后顯示一條與當前值oferrno相對應的錯誤消息和一行新行:
4.2 open函數分析
使用編寫的open應用程序在后臺分別打開2個文件,發現獲得的文件句柄fd都是3,查看2個后臺正在運行的程序進程,發現2個進程都有4個文件句柄,其中fd 0表示系統標準輸入(stdin),例如scanf;fd 1表示系統標準輸出(stdout),例如printf;fd 2表示標準錯誤(stderr),存放錯誤信息;fd 3是應用程序剛剛打開的文件句柄:
由此可知,先后2次使用open函數在后臺打開文件,每次執行open時分配一個進程號,2次open屬于不同的進程,所以2次open文件返回的文件句柄都是3并不沖突。
在open的執行過程中,文件句柄fd是如何與進程號關聯起來的?需要根據代碼進行分析:
由3.1章節可知,調用open函數時最終調用到fs/open.c中do_sys_open內核函數來打開文件:
繼續分析fd_install函數:
其中current結構體為什么是task_struct結構體還需要后續再分析。
查看task_struct結構體,在task_struct結構體中有files_struct結構體:
files_struct結構體中會有一個fdtab:
fdtable結構體中會有file結構體,保存文件句柄fd:
因此對于每次執行的open文件操作都會創建一個task_struct結構體,在對應的fdtable中會保存此進程打開的文件句柄fd:
在fdtable的file結構體中會有一個f_pos保存當前偏移位置,在調用lseek、read、write 都會更新f_pos的位置:
5. create file
5.1 create實例
create代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./create 1.txt* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0777);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}while (1){sleep(10);}close(fd);return 0;
}
上傳到Ubuntu后編譯運行:
5.2 create分析
“open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0777);”參考2.1章節系統調用接口中open描述,第2個參數是以可讀可寫、若文件不存在則創建文件、截取文件的方式open文件,第3個參數是open的文件權限是777,但是創建的文件2.txt權限并不是777,而是775,原因是系統的umask是2,所以其他用戶的寫權限是默認被關閉的:
create是使用open函數實現的,在do_sys_open中調用build_open_flags打開文件是會使用到flags和mode參數:
操作與open函數基本相同。
6. write file
6.1 write實例
write代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./write 1.txt str1 str2* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;int i;int len;if (argc < 3){printf("Usage: %s <file> <string1> <string2> ...\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0644);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}for (i = 2; i < argc; i++){len = write(fd, argv[i], strlen(argv[i]));if (len != strlen(argv[i])){perror("write");break;}write(fd, "\r\n", 2);}close(fd);return 0;
}
上傳到Ubuntu后編譯運行:
write_in_pos代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./write 1.txt str1 str2* argc = 2* argv[0] = "./open"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;int i;int len;if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDWR | O_CREAT, 0644);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}printf("lseek to offset 3 from file head\n");lseek(fd, 3, SEEK_SET);write(fd, "123", 3);close(fd);return 0;
}
上傳到Ubuntu后編譯運行:
6.2 write分析
搜索SYSCALL_DEFINE3調用write的函數:
分析SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)函數過程:
7. read file
7.1 read實例
read代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/** ./read 1.txt* argc = 2* argv[0] = "./read"* argv[1] = "1.txt"*/int main(int argc, char **argv)
{int fd;int i;int len;unsigned char buf[100];if (argc != 2){printf("Usage: %s <file>\n", argv[0]);return -1;}fd = open(argv[1], O_RDONLY);if (fd < 0){printf("can not open file %s\n", argv[1]);printf("errno = %d\n", errno);printf("err: %s\n", strerror(errno));perror("open");}else{printf("fd = %d\n", fd);}/* 讀文件/打印 */while (1){len = read(fd, buf, sizeof(buf)-1);if (len < 0){perror("read");close(fd);return -1;}else if (len == 0){break;}else{/* buf[0], buf[1], ..., buf[len-1] 含有讀出的數據* buf[len] = '\0'*/buf[len] = '\0';printf("%s", buf);}}close(fd);return 0;
}
上傳到Ubuntu后編譯運行:
7.2 read分析
搜索SYSCALL_DEFINE3調用read的函數:
分析SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)函數過程:
8.簡單實例——處理.csv表格
背景描述:創建一個score.csv表格,文件中包含張三、李四、王五同學的語文、數學、英語分數。
處理步驟:
- 打開score.csv表格
- 通過score.csv表格中的分數計算出每位同學的總分并對分數進行評價,最終結果輸出到指定的表格文件(result.csv)中(評價標準:3科總分>=270為A+等級、3科總分>=240為A等級、其余成績為B等級)
補充:使用notepad++打開score.csv表格,每一列之間用’,'隔開,每一行之間用回車換行隔開
處理score.csv表格數據時需要先讀取score.csv表格中的數據到一個buf中,此時可以用回車換(0x0d和0x0a)作為讀取每一行數據的結束標志,參考函數read_line()。
代碼:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>/* 返回值: n表示讀到了一行數據的數據個數(n >= 0)* -1(讀到文件尾部或者出錯)*/
static int read_line(int fd, unsigned char *buf)
{/* 循環讀入一個字符 *//* 如何判斷已經讀完一行? 讀到0x0d, 0x0a */unsigned char c;int len;int i = 0;int err = 0;while (1){len = read(fd, &c, 1);if (len <= 0){err = -1;break;}else{if (c != '\n' && c != '\r'){buf[i] = c;i++;}else{/* 碰到回車換行 */err = 0;break;}}}buf[i] = '\0';if (err && (i == 0)){/* 讀到文件尾部了并且一個數據都沒有讀進來 */return -1;}else{return i;}
}void process_data(unsigned char *data_buf, unsigned char *result_buf)
{/* 示例1: data_buf = ",語文,數學,英語,總分,評價" * result_buf = ",語文,數學,英語,總分,評價" * 示例2: data_buf = "張三,90,91,92,," * result_buf = "張三,90,91,92,273,A+" **/char name[100];int scores[3];int sum;char *levels[] = {"A+", "A", "B"};int level;if (data_buf[0] == 0xef) /* 對于UTF-8編碼的文件,它的前3個字符是0xef 0xbb 0xbf */{strcpy(result_buf, data_buf);}else{sscanf(data_buf, "%[^,],%d,%d,%d,", name, &scores[0], &scores[1], &scores[2]);//printf("result: %s,%d,%d,%d\n\r", name, scores[0], scores[1], scores[2]);//printf("result: %s --->get name---> %s\n\r", data_buf, name);sum = scores[0] + scores[1] + scores[2];if (sum >= 270)level = 0;else if (sum >= 240)level = 1;elselevel = 2;sprintf(result_buf, "%s,%d,%d,%d,%d,%s", name, scores[0], scores[1], scores[2], sum, levels[level]);//printf("result: %s", result_buf);}
}/** ./process_excel data.csv result.csv* argc = 3* argv[0] = "./process_excel"* argv[1] = "data.csv"* argv[2] = "result.csv"*/int main(int argc, char **argv)
{int fd_data, fd_result;int i;int len;unsigned char data_buf[1000];unsigned char result_buf[1000];if (argc != 3){printf("Usage: %s <data csv file> <result csv file>\n", argv[0]);return -1;}fd_data = open(argv[1], O_RDONLY);if (fd_data < 0){printf("can not open file %s\n", argv[1]);perror("open");return -1;}else{printf("data file fd = %d\n", fd_data);}fd_result = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644);if (fd_result < 0){printf("can not create file %s\n", argv[2]);perror("create");return -1;}else{printf("resultfile fd = %d\n", fd_result);}while (1){/* 從數據文件里讀取1行 */len = read_line(fd_data, data_buf);if (len == -1){break;}//if (len != 0)// printf("line: %s\n\r", data_buf);if (len != 0){/* 處理數據 */process_data(data_buf, result_buf);/* 寫入結果文件 *///write_data(fd_result, result_buf);write(fd_result, result_buf, strlen(result_buf));write(fd_result, "\r\n", 2);}}close(fd_data);close(fd_result);return 0;
}
代碼及score.csv文件上傳到ubuntu后運行效果:
將result.csv文件傳回到windows打開: