目錄
一、引入
二、標志位
1、什么是標志位?
2、標志位傳遞示例?
輸出結果分析
關鍵點解釋
三、文件描述符(File Descriptor)(先大概了解)
四、接口介紹:open()函數
1、命令查看
2、頭文件
3、函數原型
4、參數說明
1. open的第一個參數pathname
2. open的第二個參數
1. 必需標志(必須指定且只能指定一個)
2. 可選標志(可組合使用)
補充說明
擴展:
3. open的第三個參數
基本權限位
特殊權限位
如何使用?mode_t?
1. 直接使用八進制數
2. 使用宏定義組合
3. 設置特殊權限
mode_t?的實際影響
5、返回值
五、接口介紹:close()函數
1、函數原型
2、參數說明
3、返回值
4、常見錯誤碼(errno)
5、基本用法
6、深入理解close操作(了解)
7、注意事項(了解)
六、接口介紹:write()函數
1. write函數原型
2. 參數說明
3. 返回值
4、對文件進行寫入操作示例
5. write函數的特點和注意事項
1.?部分寫入
2. 阻塞與非阻塞
3. 原子性
4. 文件位置指針
七、接口介紹:read()函數
1、函數原型
2、參數說明
3、返回值
4、對文件進行讀取操作示例
八、系統調用和庫函數
一、引入
????????操作系統提供多種文件訪問方式,包括C語言接口、C++接口以及其他語言接口,同時也具備底層系統調用接口,系統調用才是文件操作最底層的實現方式。相較于高級語言庫函數,系統調用更接近底層硬件。實際上,各種語言的庫函數都是對系統接口的封裝實現。
????????無論是在Linux還是Windows平臺運行C代碼,C庫函數都通過封裝各自操作系統的系統調用接口來實現跨平臺性。這種設計不僅保證了語言的通用性,也為二次開發提供了便利。
在學習系統文件I/O前,需要先掌握標志位的傳遞方法,這在系統文件I/O接口中會頻繁使用:
二、標志位
1、什么是標志位?
????????標志位(flag)是一種編程中常用的技術,它使用二進制位來表示不同的狀態或選項。每個標志位通常對應一個特定的含義,通過位運算可以單獨設置、清除或檢查這些標志位。
標志位的優點包括:
-
節省內存(多個布爾狀態可以用一個整數的不同位表示)
-
可以方便地組合多個狀態(通過位或運算)
-
可以高效地檢查特定狀態(通過位與運算)
2、標志位傳遞示例?
#include <stdio.h>// 定義三個標志位,每個標志位對應一個不同的二進制位
#define ONE 0x01 // 0000 0001 (二進制)
#define TWO 0x02 // 0000 0010
#define THREE 0x04 // 0000 0100void func(int flags) {// 檢查flags是否包含ONE標志if (flags & ONE) printf("flags has ONE!\n");// 檢查flags是否包含TWO標志if (flags & TWO) printf("flags has TWO!\n");// 檢查flags是否包含THREE標志if (flags & THREE) printf("flags has THREE!\n");printf("\n");
}int main() {func(ONE); // 只傳遞ONE標志func(THREE); // 只傳遞THREE標志func(ONE | TWO); // 傳遞ONE和TWO標志的組合func(ONE | THREE | TWO); // 傳遞所有三個標志的組合return 0;
}
輸出結果分析
-
func(ONE);
?輸出:flags has ONE!(只有ONE標志被設置) -
func(THREE);
?輸出:flags has THREE!(只有THREE標志被設置) -
func(ONE | TWO);
?輸出:flags has ONE! flags has TWO!(ONE和TWO標志被設置)
-
func(ONE | THREE | TWO);
?輸出:flags has ONE! flags has TWO! flags has THREE!(所有三個標志都被設置)
關鍵點解釋
-
flags & ONE
:這是一個位與運算,用于檢查flags變量中是否設置了ONE標志位。如果結果為非零,則表示設置了該標志。 -
ONE | TWO
:這是一個位或運算,用于組合多個標志位。結果是一個同時包含ONE和TWO標志的值。 -
標志位的值選擇:每個標志位對應一個不同的二進制位(0x01, 0x02, 0x04等),這樣它們可以獨立設置和檢查而不會相互干擾。
這種標志位技術在系統編程、硬件接口和需要高效表示多個選項的場景中非常常見。
三、文件描述符(File Descriptor)(先大概了解)
????????在Unix/Linux系統中,所有I/O操作都是通過文件描述符完成的。文件描述符是一個非負整數,用于標識打開的文件。系統為每個進程維護一個文件描述符表。
三個標準的文件描述符:
-
0: 標準輸入(stdin)
-
1: 標準輸出(stdout)
-
2: 標準錯誤(stderr)
四、接口介紹:open()
函數
? ? open()
函數是Linux/Unix系統中用于打開或創建文件的核心系統調用之一,它是文件操作的基礎。
1、命令查看
man 2 open
2、頭文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
3、函數原型
系統接口中使用open函數打開文件,open函數的函數原型如下:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
4、參數說明
1. open的第一個參數pathname
????????open函數的第一個參數是pathname,表示要打開或創建的文件路徑名,可以是相對路徑或絕對路徑。
- 若pathname以路徑的方式給出,則當需要創建該文件時,就在pathname路徑下進行創建。
- 若pathname以文件名的方式給出,則當需要創建該文件時,默認在當前路徑下進行創建。(注意當前路徑的含義)
2. open的第二個參數
????????open函數的第二個參數是flags(文件打開方式標志位),表示打開文件的標志,控制文件的打開方式和行為。flags參數由以下一個或多個值通過位或(|
)操作組合而成。
????????例如,若想以只寫的方式打開文件,但當目標文件不存在時自動創建文件,則第二個參數設置如下:
O_WRONLY | O_CREAT
1. 必需標志(必須指定且只能指定一個)
標志 | 說明 |
---|---|
O_RDONLY | 以只讀方式打開文件 |
O_WRONLY | 以只寫方式打開文件 |
O_RDWR | 以讀寫方式打開文件 |
2. 可選標志(可組合使用)
標志 | 說明 |
---|---|
O_CREAT | 如果文件不存在,則創建它(需配合?mode ?參數設置權限) |
O_EXCL | 與?O_CREAT ?一起使用,確保文件不存在時才創建(用于原子性創建文件) |
O_TRUNC | 如果文件已存在且是普通文件,則截斷為0字節(清空文件) |
O_APPEND | 追加模式,每次寫入都會自動追加到文件末尾(避免并發寫入沖突) |
O_NONBLOCK ?/?O_NDELAY | 非阻塞模式打開文件(適用于 FIFO、管道、設備文件等) |
O_SYNC | 同步 I/O,每次寫操作都會等待數據真正寫入物理存儲(性能較低,但數據更安全) |
O_NOFOLLOW | 如果路徑是符號鏈接,則不跟隨(防止符號鏈接攻擊) |
O_DIRECTORY | 如果路徑不是目錄,則打開失敗(確保只打開目錄) |
O_CLOEXEC | 設置?close-on-exec ?標志,exec 時自動關閉文件描述符(防止子進程繼承) |
補充說明
-
必需標志(
O_RDONLY
?/?O_WRONLY
?/?O_RDWR
)必須選且僅選一個。 -
可選標志可以通過?
|
(按位或)組合使用,例如:int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
-
O_EXCL
?必須與?O_CREAT
?一起使用,否則無意義。 -
O_TRUNC
?僅對普通文件有效,對目錄、設備文件等無效。 -
O_APPEND
?在多進程/多線程寫入時能避免競爭條件(Race Condition)。 -
O_SYNC
?會影響性能,但能確保數據持久化(適用于關鍵數據存儲)。
擴展:
????????系統接口open的第二個參數flags是整型,有32比特位,若將一個比特位作為一個標志位,則理論上flags可以傳遞32種不同的標志位。
實際上傳入flags的每一個選項在系統當中都是以宏的方式進行定義的:
例如,O_RDONLY、O_WRONLY、O_RDWR和O_CREAT在系統當中的宏定義如下:
#define O_RDONLY ? ? ? ? 00
#define O_WRONLY ? ? ? ? 01
#define O_RDWR ? ? ? ? ? 02
#define O_CREAT ? ? ? ?0100
????????這些宏定義選項的二進制編碼具有一個共同特征:每個選項的二進制序列中僅有一位為1(O_RDONLY選項除外,其二進制值為全0,表示默認選項)。不同選項的置1位各不相同,這使得open函數內部可以通過簡單的"與"運算來檢測特定選項是否被設置。
int open(arg1, arg2, arg3)
{if (arg2&O_RDONLY)//檢查是否設置了O_RDONLY選項{}if (arg2&O_WRONLY)//檢查是否設置了O_WRONLY選項{}if (arg2&O_RDWR)//檢查是否設置了O_RDWR選項{}if (arg2&O_CREAT)//檢查是否設置了O_CREAT選項{}//...
}
3. open的第三個參數
????????在 Unix/Linux 系統調用中,mode_t
?是一個數據類型,用于表示文件的權限模式(permission mode)。它通常是一個無符號整數類型(如?unsigned int
),用于指定文件的訪問權限。
????????當使用O_CREAT
創建新文件時,必須指定mode參數,表示新文件的權限。mode通常用八進制表示,如0644。
例如,設置mode=0666
會賦予文件-rw-rw-rw-
的權限。
????????需要注意的是,實際文件權限會受到umask
(文件創建掩碼)的影響。計算公式為:實際權限 = mode & (~umask)
。在默認umask=0002
的情況下,當mode=0666
時,實際創建的權限為0664
(即-rw-rw-r--
)。
若要完全按照mode
參數設置權限,可以在創建文件前調用umask(0)
將掩碼清零。
umask(0); //將文件默認掩碼設置為0
注意:?當不需要創建文件時,open的第三個參數可以不必設置。?
? ? open()
?函數在創建文件(使用?O_CREAT
?標志)時,需要指定文件的權限模式?mode_t
。這個參數決定了文件的讀、寫、執行權限,以及特殊權限位(如 setuid、setgid 等)。
基本權限位
mode_t
?由多個權限位組合而成,可以使用八進制數或宏定義來設置:
宏定義 | 八進制值 | 權限說明 |
---|---|---|
S_IRUSR | 0400 | 用戶(owner)可讀 |
S_IWUSR | 0200 | 用戶可寫 |
S_IXUSR | 0100 | 用戶可執行 |
S_IRGRP | 0040 | 組(group)可讀 |
S_IWGRP | 0020 | 組可寫 |
S_IXGRP | 0010 | 組可執行 |
S_IROTH | 0004 | 其他用戶(others)可讀 |
S_IWOTH | 0002 | 其他用戶可寫 |
S_IXOTH | 0001 | 其他用戶可執行 |
特殊權限位
宏定義 | 八進制值 | 權限說明 |
---|---|---|
S_ISUID | 04000 | 設置用戶ID(setuid) |
S_ISGID | 02000 | 設置組ID(setgid) |
S_ISVTX | 01000 | 粘滯位(sticky bit) |
如何使用?mode_t
?
1. 直接使用八進制數
最常見的用法是直接使用?3位八進制數?來設置權限:
int fd = open("example.txt", O_CREAT | O_WRONLY, 0644);
-
0644
?表示:-
用戶(owner):
6
(4+2
,即?rw-
) -
組(group):
4
(r--
) -
其他用戶(others):
4
(r--
)
-
2. 使用宏定義組合
也可以使用宏定義組合:
int fd = open("example.txt", O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
-
等同于?
0644
(rw-r--r--
)
3. 設置特殊權限
例如,設置?setuid
?權限(僅對可執行文件有效):
int fd = open("program", O_CREAT | O_WRONLY, S_IRWXU | S_ISUID);
-
S_IRWXU
?=?0700
(rwx------
) -
S_ISUID
?=?04000
(設置?setuid
?位) -
最終權限:
4700
(rws------
)
mode_t
?的實際影響
-
open()
?的?mode
?參數僅在?O_CREAT
?時生效(如果文件已存在,則不會修改權限)。 -
最終權限會受到?
umask
?的影響:mode_t final_mode = mode & ~umask;
例如,如果?
umask=002
,而?mode=0666
,則實際權限是?0664
(rw-rw-r--
)。
5、返回值
open函數的返回值是新打開文件的文件描述符。
- 成功:成功時返回一個非負整數文件描述符。
- 失敗:失敗時返回-1并設置errno。
我們可以嘗試一次打開多個文件,然后分別打印它們的文件描述符:?
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{umask(0);int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);printf("fd1:%d\n", fd1);printf("fd2:%d\n", fd2);printf("fd3:%d\n", fd3);printf("fd4:%d\n", fd4);printf("fd5:%d\n", fd5);return 0;
}
運行程序后可以看到,打開文件的文件描述符是從3開始連續且遞增的:
我們再嘗試打開一個根本不存在的文件,也就是open函數打開文件失敗:?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("test.txt", O_RDONLY);printf("%d\n", fd);return 0;
}
運行程序后可以看到,打開文件失敗時獲取到的文件描述符是-1:?
????????文件描述符本質上是一個指針數組的索引,該數組中的每個指針都指向一個已打開文件的文件信息。通過文件描述符即可訪問對應的文件信息。
????????當open函數成功打開文件時,系統會擴展指針數組并返回新增指針的索引值;若打開失敗則直接返回-1。因此,連續成功打開多個文件時,獲得的文件描述符是依次遞增的。
????????Linux進程默認打開三個標準文件描述符:0(標準輸入)、1(標準輸出)和2(標準錯誤)。這就是新打開文件時,文件描述符從3開始分配的原因。
open
函數的具體使用方式取決于應用場景:
- 若目標文件不存在,需要創建新文件,則使用帶三個參數的
open
(第三個參數表示創建文件的默認權限) - 若文件已存在,則使用帶兩個參數的
open
五、接口介紹:close()
函數
close()
函數是Linux/Unix系統中用于關閉已打開文件描述符的重要系統調用。
1、函數原型
#include <unistd.h>int close(int fd);
2、參數說明
fd(文件描述符)
:
????????要關閉的文件描述符(file descriptor),這是之前通過open()
、creat()
、pipe()
、dup()
等函數獲得的文件描述符。
3、返回值
-
成功時返回0
-
失敗時返回-1,并設置errno來指示錯誤原因
4、常見錯誤碼(errno)
-
EBADF
:fd不是有效的已打開文件描述符 -
EINTR
:close操作被信號中斷 -
EIO
:發生了I/O錯誤
5、基本用法
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>int main() {int fd = open("example.txt", O_RDONLY);if (fd == -1) {perror("open failed");return 1;}// 使用文件描述符進行讀寫操作...if (close(fd) == -1) {perror("close failed");return 1;}return 0;
}
6、深入理解close操作(了解)
-
資源釋放:
-
關閉文件描述符會釋放內核為該文件分配的所有資源
-
釋放文件描述符本身,使其可被后續的
open()
或pipe()
等調用重用
-
-
緩沖區刷新:
-
對于輸出文件,close操作會確保所有緩沖數據被寫入磁盤
-
對于使用
mmap()
映射的文件,close操作不會解除映射,但關閉后訪問映射內存可能導致SIGBUS信號
-
-
文件鎖釋放:
-
進程終止時所有文件描述符會自動關閉
-
關閉文件描述符會釋放該進程在該文件上設置的所有鎖(使用
fcntl()
設置的鎖)
-
7、注意事項(了解)
-
多次關閉:
-
重復關閉同一個文件描述符是錯誤行為
-
在多線程環境中尤其需要注意,可能引發競態條件
-
-
信號中斷處理:
-
如果close()被信號中斷,某些系統上需要重新調用close()
-
更安全的做法是使用以下模式:
while (close(fd) == -1) {if (errno != EINTR) {perror("close error");break;}// 如果是被信號中斷,則繼續嘗試關閉 }
-
-
文件描述符泄漏:
-
忘記關閉文件描述符是常見編程錯誤
-
長期運行的進程可能導致文件描述符耗盡
-
建議在打開文件后立即考慮關閉操作,使用
goto
或RAII模式管理資源。
-
六、接口介紹:write()
函數
? ? write()
?是Linux/Unix系統中一個非常重要的低級文件I/O函數,用于將數據寫入文件描述符對應的文件或設備。
1. write函數原型
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
2. 參數說明
-
fd
:文件描述符,通常由open()
函數返回 -
buf
:指向要寫入數據的緩沖區的指針 -
count
:要寫入的字節數
3. 返回值
-
成功時:返回實際寫入的字節數(可能小于請求的
count
) -
失敗時:返回-1,并設置errno
4、對文件進行寫入操作示例
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){perror("open");return 1;}const char* msg = "hello syscall\n";for (int i = 0; i < 5; i++){write(fd, msg, strlen(msg));}close(fd);return 0;
}
運行程序后,在當前路徑下就會生成對應文件,文件當中就是我們寫入的內容:
5. write函數的特點和注意事項
1.?部分寫入
write()
可能會執行部分寫入,即返回值小于請求的字節數。這種情況常見于:
-
磁盤空間不足
-
被信號中斷
-
非阻塞模式下資源暫時不可用
2. 阻塞與非阻塞
-
常規文件通常不會阻塞
-
管道、套接字等特殊文件可能阻塞
-
可以設置
O_NONBLOCK
標志使操作非阻塞
3. 原子性
對于常規文件,小于PIPE_BUF
大小的寫入是原子的(通常為4096字節)
4. 文件位置指針
write()
操作會更新文件的當前位置指針
七、接口介紹:read()函數
? ? read()
?函數是Linux/Unix系統中用于從文件描述符讀取數據的基本系統調用之一。它是文件I/O操作的核心函數之一,屬于POSIX標準的一部分。
1、函數原型
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
2、參數說明
-
fd (file descriptor):文件描述符,是一個整數值,指向要讀取的文件
-
通常由
open()
函數返回 -
標準輸入的文件描述符是0
-
-
buf:指向內存緩沖區的指針,用于存放讀取到的數據。必須預先分配足夠的內存空間
-
count:請求讀取的字節數。通常是緩沖區的大小
3、返回值
-
成功時:返回實際讀取的字節數
-
可能小于請求的字節數(例如接近文件末尾時)
-
返回0表示到達文件末尾(EOF)
-
-
失敗時:返回-1,并設置errno
-
常見的errno值:
-
EAGAIN/EWOULDBLOCK:非阻塞I/O且無數據可讀
-
EBADF:無效的文件描述符
-
EINTR:被信號中斷
-
EIO:I/O錯誤
-
-
4、對文件進行讀取操作示例
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("log.txt", O_RDONLY);if (fd < 0){perror("open");return 1;}char ch;while (1){ssize_t s = read(fd, &ch, 1);if (s <= 0){break;}write(1, &ch, 1); //向文件描述符為1的文件寫入數據,即向顯示器寫入數據}close(fd);return 0;
}
運行程序后,就會將我們剛才寫入文件的內容讀取出來,并打印在顯示器上:
八、系統調用和庫函數
????????總的來說,我們更加能明白開始時提到的“實際上,各種語言的庫函數都是對系統接口的封裝實現。”這句話!!!
在了解返回值之前,需要明確兩個重要概念:系統調用和庫函數
- fopen、fclose、fread、fwrite這些是C標準庫提供的函數,稱為庫函數(libc)
- 而open、close、read、write、lseek等則是操作系統直接提供的接口,稱為系統調用
- 這與我們之前講解操作系統概念時展示的系統架構圖是一致的
????????系統調用接口與庫函數的關系十分清晰。可以明確地說,f#系列函數是對系統調用的封裝,為二次開發提供了便利。