🦄 個人主頁: 小米里的大麥-CSDN博客
🎏 所屬專欄: Linux_小米里的大麥的博客-CSDN博客
🎁 GitHub主頁: 小米里的大麥的 GitHub
?? 操作環境: Visual Studio 2022
文章目錄
- 基礎 `IO` —— `C` 語言文件 `I/O` 操作基礎
- 前言
- 1. C 語言文件操作函數匯總
- 1. 文件打開與關閉
- 2. 字符與字符串讀寫
- 3. 格式化讀寫
- 4. 二進制文件讀寫
- 5. 文件定位與狀態
- 6. 其他輔助函數
- 2. 什么是當前路徑?
- 3. `w` 總是先清空,再寫入
- 4. `a` 是追加寫
- 特性說明
- 特性說明
- 坑點說明
- 坑點說明
- 5. `open()` 函數
- 1. `flags` 參數詳解(支持“位或 |”組合使用)
- 2. `mode` 參數(僅在 `O_CREAT` 創建新文件時才用)
- 3. 戰代碼示例:打開 + 寫入 + 關閉
- 6. 文件描述符
- 1. C 代碼示例:文件描述符本質是數組下標
- 解釋說明:
- 2. 內核層的結構體解析([推薦 linux-2.6.11.1.tar.gz —— 09-Mar-2005 00:59 44M 這個版本查看源碼](https://www.kernel.org/pub/linux/kernel/v2.6/))
- 1. `files_struct`(表示進程級的打開文件表)
- 2. `struct file`(表示一個已打開的文件實例)
- 3. `inode`(文件元數據結構)
- 3. 三者之間的聯系總結
- 4. 文件描述符的分配規則
- 7. 文件描述符 VS `FILE *`
- 1. 文件描述符(`int fd`)
- 2. `FILE *` 指針
- 3. 二者之間的關系圖
- 4. 相互轉換方法
- 5. 區別對比總結
- 共勉
基礎 IO
—— C
語言文件 I/O
操作基礎
前言
1. 文件的基本概念
文件的定義:
- 文件 = 內容(數據) + 屬性(如權限、修改時間、所有者等)。
- 屬性是文件管理的重要依據,貫穿文件的存儲、訪問和控制全流程。
2. 文件的兩種狀態
打開的文件
- 觸發條件:由進程主動打開(如讀寫操作)。
- 存儲位置:加載到內存中。
- 核心機制:
- 進程關聯:每個打開的文件需與進程綁定,形成“進程-文件”的 多對一關系(1: n 關系:一個進程可打開多個文件)。
- 內核管理:操作系統為每個打開的文件創建 文件打開對象(如
struct XXX
),記錄文件屬性、狀態及鏈表指針(如next
),便于統一管理。未打開的文件
- 存儲位置:磁盤上。
- 核心問題:如何高效組織海量未打開文件,支持 快速增刪查改。
- 管理目標:通過目錄結構、文件系統層級等實現文件分類與定位。
3. 操作系統如何管理打開的文件
管理原則 —— 先描述,后組織:
- 描述:為每個打開的文件創建內核對象(如
struct file
),記錄文件屬性(如讀寫位置、權限)和操作接口。- 組織:通過鏈表、哈希表等數據結構管理所有打開的文件對象,實現高效訪問。
關鍵數據結構示例
struct file {mode_t permissions; // 文件權限off_t read_offset; // 當前讀位置struct inode *inode; // 指向磁盤文件的元數據(如 inode)struct file *next; // 鏈表指針,用于組織多個打開的文件 };
核心目標
- 高效管理大量打開的文件。
- 確保進程間文件操作的隔離性與安全性(如通過文件描述符隔離)。
1. C 語言文件操作函數匯總
CSDN 相關文章
1. 文件打開與關閉
函數 | 參數與模式 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fopen | (const char *filename, const char *mode) 模式: "r" (讀)、"w" (寫覆蓋)、"a" (追加)、"rb" (二進制讀)等 | FILE* (成功)NULL (失敗) | 打開文件并返回文件指針 | FILE *fp = fopen("test.txt", "r"); |
fclose | (FILE *stream) | 0 (成功)EOF (失敗) | 關閉文件流并釋放資源 | fclose(fp); |
freopen | (const char *filename, const char *mode, FILE *stream) | FILE* (新流)NULL (失敗) | 重定向已打開的流到新文件 | freopen("log.txt", "a", stdout); |
2. 字符與字符串讀寫
函數 | 參數與說明 | 返回值 | 功能描述 | 注意事項 |
---|---|---|---|---|
fputc | (int char, FILE *stream) | 寫入的字符(成功)EOF (失敗) | 向文件寫入一個字符 | 適用于文本文件 |
fgetc | (FILE *stream) | 讀取的字符(成功)EOF (失敗或結尾) | 從文件讀取一個字符 | 需用 feof 檢測結尾 |
fputs | (const char *str, FILE *stream) | 非負值(成功)EOF (失敗) | 向文件寫入字符串(不自動加 \n ) | 確保字符串以 \0 結尾 |
fgets | (char *str, int n, FILE *stream) | str (成功)NULL (失敗或結尾) | 從文件讀取一行字符串(最多 n-1 字符) | 保留換行符,末尾補 \0 |
3. 格式化讀寫
函數 | 參數與格式說明 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fprintf | (FILE *stream, const char *format, ...) | 寫入的字符數(成功) 負值(失敗) | 按格式向文件寫入數據 | fprintf(fp, "Value: %d", 42); |
fscanf | (FILE *stream, const char *format, ...) | 成功匹配的參數數量(成功)EOF (失敗) | 按格式從文件讀取數據 | fscanf(fp, "%d", &num); |
4. 二進制文件讀寫
函數 | 參數與說明 | 返回值 | 功能描述 | 注意事項 |
---|---|---|---|---|
fwrite | (const void *ptr, size_t size, size_t count, FILE *stream) | 成功寫入的項數 | 向二進制文件寫入數據塊 | 參數順序:數據指針、項大小、項數量 |
fread | (void *ptr, size_t size, size_t count, FILE *stream) | 成功讀取的項數 | 從二進制文件讀取數據塊 | 需檢查返回值以確認實際讀取量 |
5. 文件定位與狀態
函數 | 參數與說明 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fseek | (FILE *stream, long offset, int origin) origin :SEEK_SET (文件頭)、SEEK_CUR (當前位置)、SEEK_END (文件尾) | 0 (成功)非零(失敗) | 移動文件指針到指定位置 | fseek(fp, 10, SEEK_SET); |
ftell | (FILE *stream) | 當前偏移量(成功)-1L (失敗) | 獲取文件指針當前位置 | long pos = ftell(fp); |
rewind | (FILE *stream) | 無 | 重置文件指針到文件開頭 | rewind(fp); |
feof | (FILE *stream) | 非零值(到結尾)0 (未到結尾) | 檢測文件指針是否到達結尾 | if (feof(fp)) { ... } |
ferror | (FILE *stream) | 非零值(有錯誤)0 (無錯誤) | 檢測文件操作是否出錯 | if (ferror(fp)) { ... } |
6. 其他輔助函數
函數 | 參數與說明 | 返回值 | 功能描述 | 示例 |
---|---|---|---|---|
fflush | (FILE *stream) | 0 (成功)EOF (失敗) | 強制將緩沖區數據寫入文件 | fflush(fp); |
remove | (const char *filename) | 0 (成功)非零(失敗) | 刪除指定文件 | remove("temp.txt"); |
rename | (const char *oldname, const char *newname) | 0 (成功)非零(失敗) | 重命名或移動文件 | rename("old.txt", "new.txt"); |
2. 什么是當前路徑?
#include <stdio.h>
#include <unistd.h>
int main()
{FILE* fp = fopen("temp.txt", "w"); // 打開文件,如果文件不存在則創建,如果存在則覆蓋if (fp == NULL){perror(" fopen"); // 如果打開文件失敗,打印錯誤信息return 1;}fclose(fp); // 關閉文件sleep(1); // 休眠 1 秒return 0;
}
由上我們知道,當 fopen
以寫入的方式打開一個文件時,若該文件不存在,則會自動在當前路徑創建該文件,那么這里所說的當前路徑指的是什么呢?答案是 進程的當前路徑——cwd
。驗證:
#include <stdio.h>
#include <unistd.h>
int main()
{chdir("/home/hcc"); // 改變當前工作目錄為/home/hcc(注意:目錄不存在,可能會導致后續操作失敗)printf("Pid: %d\n", getpid()); // 打印進程 IDFILE* fp = fopen("temp.txt", "w"); // 打開文件,如果文件不存在則創建,如果存在則覆蓋if (fp == NULL){perror(" fopen"); // 如果打開文件失敗,打印錯誤信息return 1;}fclose(fp); // 關閉文件sleep(100); // 休眠 100 秒return 0;
}
3. w
總是先清空,再寫入
fopen
的 w
模式,當以 w 模式打開文件時:
- 如果文件不存在,則創建新文件。
- 如果文件已存在,則清空原有內容并重新開始寫入。
#include <stdio.h>
#include <unistd.h> // 包含系統調用的頭文件,如 getpid 等
#include <string.h> // 包含字符串處理函數的頭文件,如 strlen 等int main()
{printf("Pid: %d\n", getpid());// 打開文件 temp.txt,模式為 "w",表示以寫方式打開文件// 如果文件不存在,則創建該文件// 如果文件已存在,則覆蓋原有內容FILE* fp = fopen("temp.txt", "w");if (fp == NULL){perror("fopen");return 1;}const char* message = "abcd"; // 定義一個字符串常量 message,字符串常量是只讀的,不能被修改// 將字符串 message 寫入文件// fwrite 函數用于將數據寫入文件// 第一個參數是要寫入的數據的指針// 第二個參數是每個數據單元的大小,這里是 1 字節(即每個字符)// 第三個參數是數據單元的數量,這里是字符串的長度加 1(包括字符串結束符'\0')// 第四個參數是文件指針fwrite(message, strlen(message) + 1, 1, fp);fclose(fp); // 關閉文件return 0;
}
好像沒什么問題,w
會先清空文件,再將新文件內容進行寫入,但是當我們打開這個 temp.txt
文件就會發現一點問題:
注意:w
即使不寫入數據,只打開文件也會清空文件數據! 因為 w
是先清空再寫入。
echo
與 fopen
的關系
- 底層實現:
echo
是 shell 命令,但其重定向功能依賴于操作系統提供的文件 I/O 機制。 - 類比
fopen("w")
:當執行echo ... > file
時,系統調用類似fopen(file, "w")
的操作,確保輸出內容替換原有數據。
雖然 echo
本身不直接調用 fopen
,但其重定向功能在底層實現了與 fopen("w")
相同的效果:覆蓋原有文件內容。因此,可以認為 echo
的重定向機制在功能上模擬了 fopen
的 w
模式。
echo "現在有文件數據哦!" > temp.txt # 創建初始文件
echo "你好!" > temp.txt # 使用 echo 覆蓋內容
cat temp.txt # 查看結果結果顯示:temp.txt的內容被完全替換為“你好!”,證明echo的重定向行為等同于fopen的w模式。
4. a
是追加寫
特性 | 說明 |
---|---|
追加模式 | 寫入內容總在文件末尾,不覆蓋原有數據。 |
自動創建文件 | 若文件不存在,a 模式會創建新文件。 |
文本模式 vs 二進制 | 默認為文本模式("a" ),若需二進制追加,使用 "ab" 。 |
緩沖區依賴 | 寫入內容需等待緩沖區滿或顯式刷新(fflush )后才寫入磁盤。 |
1. 基本追加寫入
#include <stdio.h>
int main()
{FILE* file = fopen("log.txt", "a"); // 以追加模式打開文件if (file == NULL){perror("打開文件失敗");return 1;}fprintf(file, "這是一個測試文件\n"); // 寫入內容到文件末尾fclose(file); // 關閉文件return 0;
}
特性說明
- 追加行為:每次寫入均追加到文件末尾,不覆蓋原有內容。
- 自動創建:若文件不存在,
a
模式會自動創建新文件。
2. 文件不存在時的自動創建
#include <stdio.h>
int main()
{FILE* file = fopen("file.txt", "a"); // 文件不存在時自動創建if (file == NULL){perror("寫入文件失敗");return 1;}fprintf(file, "已創建并追加的文件。\n");fclose(file);return 0;
}
特性說明
- 權限問題:若當前目錄無寫權限,
a
模式會失敗(需處理fopen
返回的NULL
)。
3. 未關閉文件導致的數據丟失
#include <stdio.h>
int main()
{FILE* file = fopen("data.txt", "a");if (file == NULL){perror("Error");return 1;}fprintf(file, "重要數據!"); // 寫入后未關閉文件// 錯誤:未調用 fclose(file)return 0;
}
坑點說明
- 未關閉文件:若程序異常終止或忘記調用
fclose
,緩沖區中的數據可能未寫入磁盤。 - 解決方案:始終確保寫入后調用
fclose
,或顯式調用fflush(file)
強制刷新緩沖區。
4. 并發寫入的潛在問題
#include <stdio.h>
int main()
{FILE* file1 = fopen("shared.txt", "a");FILE* file2 = fopen("shared.txt", "a"); // 同一文件被多次打開fprintf(file1, "來自 file1 的消息\n");fprintf(file2, "來自 file2 的消息\n");fclose(file1);fclose(file2);return 0;
}
坑點說明
- 并發寫入:若多個進程/線程同時追加文件,內容可能交錯(如
"來自 file2 的消息\n"
)。 - 解決方案:在多線程/多進程場景中,需通過鎖機制或原子操作保證順序。
5. open()
函數
題外話:比特位方式的標志位傳遞。其核心原理是通過 二進制位的每一位 來表示不同的標志狀態,利用位運算符(如 |
和 &
)高效地組合和檢測多個標志(了解,以后詳解)。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>#define ONE (1<<0) // 0001 -> 十進制 1
#define TWO (1<<1) // 0010 -> 十進制 2
#define THREE (1<<2) // 0100 -> 十進制 4
#define FOUR (1<<3) // 1000 -> 十進制 8void show(int flags)
{if(flags&ONE) printf("hello function1\n"); // 如果第 0 位是 1if(flags&TWO) printf("hello function2\n"); // 如果第 1 位是 1if(flags&THREE) printf("hello function3\n"); // 如果第 2 位是 1if(flags&FOUR) printf("hello function4\n"); // 如果第 3 位是 1
}int main()
{printf("-----------------------------\n");show(ONE);printf("-----------------------------\n");show(TWO);printf("-----------------------------\n");show(ONE|TWO);printf("-----------------------------\n");show(ONE|TWO|THREE);printf("-----------------------------\n");show(ONE|THREE);printf("-----------------------------\n");show(THREE|FOUR);printf("-----------------------------\n");
}
open()
是 Linux 系統調用中最核心、最常用的函數之一,是一切文件/設備操作的起點。
1. 頭文件
#include <fcntl.h> // 提供 open 函數、O_RDONLY 等常量
#include <sys/types.h>
#include <sys/stat.h>
2. 函數原型 & 參數解釋
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode); // 創建文件時用到
3. 參數說明
參數 | 說明 |
---|---|
pathname | 文件路徑(絕對或相對路徑) |
flags | 文件打開方式和行為控制(重點!) |
mode | 權限位(只有當創建新文件時才用!) |
4. 返回值
返回值 | 含義 |
---|---|
>= 0 | 打開成功,返回的是 文件描述符(整數) |
< 0 | 打開失敗,返回 -1 ,具體錯誤原因通過 errno 查看 |
if (fd < 0)
{perror("open failed");
}
1. flags
參數詳解(支持“位或 |”組合使用)
1. 訪問方式(必須選一個)
常量 | 說明 |
---|---|
O_RDONLY | 只讀打開(Read Only) |
O_WRONLY | 只寫打開(Write Only) |
O_RDWR | 讀寫都打開(Read + Write) |
2. 控制行為(可選,多個之間用 |
連接)
常量 | 含義 |
---|---|
O_CREAT | 文件不存在就創建(需要配合 mode 參數) |
O_EXCL | 和 O_CREAT 同用,文件存在則失敗(避免重復創建) |
O_TRUNC | 打開文件時清空原內容(通常配合寫) |
O_APPEND | 寫入內容追加到文件末尾 |
O_NONBLOCK | 非阻塞模式打開文件(常用于設備/管道) |
O_CLOEXEC | 在 exec 調用時關閉該文件描述符 |
O_SYNC | 寫入時直接同步到硬盤(安全但慢) |
2. mode
參數(僅在 O_CREAT
創建新文件時才用)
open("file.txt", O_WRONLY | O_CREAT, 0664);
mode_t
用來設置 新文件的權限,與chmod
類似- 常用組合:
權限數字 | 含義說明 |
---|---|
0664 | 用戶讀寫,組讀寫,其他只讀 |
0644 | 用戶讀寫,其他只讀 |
代碼示例解讀:
// 代碼 1:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("temp.txt", O_WRONLY);if (fd < 0){printf("open file error\n");return 1;}return 0;
}// 代碼 2:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd = open("temp.txt", O_WRONLY | O_CREAT);if (fd < 0){printf("open file error\n");return 1;}return 0;
}// 代碼 3:頭文件一樣,后面不再重復
int main()
{int fd = open("temp.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){printf("open file error\n");return 1;}return 0;
}// 代碼 4:
int main()
{umask(0);int fd = open("temp.txt", O_WRONLY | O_CREAT, 0666);if (fd < 0){printf("open file error\n");return 1;}return 0;
}
代碼編號 | 是否自動創建文件 | 權限設定 | 是否受 umask 影響 | 常見用途 |
---|---|---|---|---|
1?? | ? 否 | 無 | - | 僅打開已存在文件 |
2?? | ?? 可能報錯 | ? 缺少權限參數(可能是亂碼) | - | 不推薦用法 |
3?? | ? 創建 | 0666 - umask(與 0666 不符,原因 umask ) | ? 是 | 正常用法 |
4?? | ? 創建 | 0666 | ? 不受影響 | 特殊場合 |
3. 戰代碼示例:打開 + 寫入 + 關閉
#include <fcntl.h> // 包含文件控制相關的定義和函數聲明
#include <unistd.h> // 包含 POSIX 操作函數的聲明,如 open、close、write 等
#include <stdio.h> // 包含標準輸入輸出函數的聲明,如 perror 等
int main()
{// 打開或創建文件,并設置為只寫模式、創建文件、截斷文件int fd = open("demo.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0){perror("open failed"); // 打印錯誤信息,open failed 為自定義的錯誤前綴return 1; // 打開文件失敗,返回非零值表示程序異常退出}const char* msg = "實習 / 工作要掌握 open 的實戰用法!\n"; // 定義要寫入文件的字符串,將字符串寫入文件write(fd, msg, strlen(msg)); // 第一個參數是文件描述符,第二個是數據的指針,第三個是數據的長度close(fd); // 關閉文件,釋放資源return 0; // 程序正常退出
}
6. 文件描述符
先出結論:open()
的返回值 就是文件描述符(fd),它是一個 數組下標,指向當前進程的 打開文件表(fd table) 中的一個 struct file *
指針。
int fd = open("a.txt", O_WRONLY); // 返回 3
write(fd, "Hello", 5); // 實際就是 write(fd_table [3], ...)
我們通過一段完整的 C 代碼 演示:“訪問文件的本質,其實是 數組下標訪問”。接著再來講解內核中是如何通過 struct file
、struct files_struct
等結構體來描述和管理已打開的文件。
1. C 代碼示例:文件描述符本質是數組下標
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>int main()
{// 打開三個不同的文件int fd1 = open("file1.txt", O_CREAT | O_WRONLY, 0666);int fd2 = open("file2.txt", O_CREAT | O_WRONLY, 0666);int fd3 = open("file3.txt", O_CREAT | O_WRONLY, 0666);if (fd1 < 0 || fd2 < 0 || fd3 < 0){perror("open");return 1;}printf("fd1: %d\n", fd1); // 通常是 3printf("fd2: %d\n", fd2); // 通常是 4printf("fd3: %d\n", fd3); // 通常是 5write(fd2, "Hello file2\n", 12); // 通過 fd2 向 file2.txt 寫入內容close(fd1);close(fd2);close(fd3);return 0;
}
輸出示例(實際運行):
fd1: 3
fd2: 4
fd3: 5
解釋說明:
為什么編號是從 3
開始的?0
,1
,2
去哪了?
答案:Linux
進程默認情況下會有 3
個缺省打開的文件描述符,分別是標準輸入 0
,標準輸出 1
,標準錯誤 2
。0,1,2 對應的物理設備一般是:鍵盤,顯示器,顯示器。
- 標準輸入(stdin):文件描述符是 0
- 標準輸出(stdout):是 1
- 標準錯誤(stderr):是 2
后續打開的文件就是從 下標 3 開始往上分配。所以:fd
本質上是一個 打開文件表的下標(int 類型的索引)
2. 內核層的結構體解析(推薦 linux-2.6.11.1.tar.gz —— 09-Mar-2005 00:59 44M 這個版本查看源碼)
用戶層調用 open()
后,內核做了什么?
Linux 內核有三層數據結構來描述一個打開的文件:
1. files_struct
(表示進程級的打開文件表)
struct files_struct
{struct file *fd_array[NR_OPEN]; // 進程文件描述符數組(最多可打開的文件數)
};
- 每個進程都有一個
files_struct
實例。 fd_array[i]
中存的是指向struct file
的指針。- 這個數組的下標就是我們用戶層看到的
fd
!
2. struct file
(表示一個已打開的文件實例)
struct file
{struct inode *f_inode; // 指向文件的 inode 結構loff_t f_pos; // 當前讀寫位置(文件偏移量)struct file_operations *f_op; // 操作函數表...
};
- 表示一次文件打開操作。
- 不同進程打開同一個文件,會有 不同的
struct file
。 struct file
直接或間接包含的屬性:在磁盤的什么位置、基本屬性(權限、大小、讀寫位置、誰打開的……)、文件的內核緩沖區信息、struct file *next
指針、引用計數count
等。- 類似于“文件打開上下文”,記錄偏移、標志等狀態。
3. inode
(文件元數據結構)
struct inode
{// 文件類型、權限、擁有者、指向數據塊的指針等等
};
- 一個
inode
代表文件系統中一個“實際的文件”。 - 所有打開該文件的
file
都指向同一個inode
。
3. 三者之間的聯系總結
小結:用戶空間的文件描述符(fd)就是內核中的 struct file *
數組的下標!通過這個下標,進程就能訪問并操作該文件在內核中對應的資源。
4. 文件描述符的分配規則
先出結論:Linux 內核始終分配當前未被使用的最小下標,作為新的文件描述符。當你調用 open()
或 dup()
等函數時,內核會在 fd_array[]
中從頭開始查找:尋找當前未被使用的最小下標,作為新的文件描述符(fd)。
關閉 fd 后: 它會被回收,下一次打開文件就可能重復使用這個編號。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main()
{close(0); // 關閉標準輸入(fd = 0)int fd1 = open("test1.txt", O_CREAT | O_RDWR, 0666);printf("fd1=%d\n", fd1); // 輸出 fd1 = 0close(1); // 關閉標準輸出(fd = 1)int fd2 = open("test2.txt", O_CREAT | O_RDWR, 0666);printf("fd2=%d\n", fd2); // 無法輸出到終端(因為 stdout 已關閉),重定向到文件觀察close(2); // 關閉標準錯誤(fd = 2)int fd3 = open("test3.txt", O_CREAT | O_RDWR, 0666);printf("fd3=%d\n", fd3); // 同樣無法輸出到終端return 0;
}
運行與驗證:
-
編譯并運行程序,將輸出重定向到文件:
gcc test.c -o test && ./test > output.txt 2>&1
-
查看
output.txt
:fd1=0 fd2=1 fd3=2
說明:關閉 0/1/2
后,新打開的文件的描述符依次復用這些最小下標。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
// dprintf(fd, ...) 就是“向 fd 指定的地方 像用 printf 一樣 輸出內容”,非常適合標準輸出關閉或重定向時使用。
int main()
{printf("原始狀態:stdin=0, stdout=1, stderr=2\n"); // 先確認 fd = 0,1,2 都是開啟狀態close(1); // 關閉標準輸出(fd = 1),但保留 stdin(0) 和 stderr(2)int fd1 = open("test1.txt", O_CREAT | O_RDWR, 0666); // 打開文件 1dprintf(2, "fd1 = %d\n", fd1); // 輸出到 stderr(fd = 2),應為 1close(0); // 關閉標準輸入(fd = 0)int fd2 = open("test2.txt", O_CREAT | O_RDWR, 0666); // 打開文件 2dprintf(2, "fd2 = %d\n", fd2); // 應為 0int fd3 = open("test3.txt", O_CREAT | O_RDWR, 0666); // 打開文件 3(fd = 2 還在用)dprintf(2, "fd3 = %d\n", fd3); // 應為 3,因為 0 和 1 被占用,新開只能用 3return 0;
}
輸出內容會寫入 stderr
(fd = 2),因為 stdout
被關閉。示例輸出:
fd1 = 1 ← 因為 fd=1 被關閉,最小空位就是 1
fd2 = 0 ← fd=0 被關閉,最小空位變為 0
fd3 = 3 ← 此時 0 和 1 被占用,2 還在用,所以下一空位是 3
注意: 當你只關閉了 fd=1
,而 0
和 2
保持開啟,新文件會被分配到 1。不會跳到 2,因為 2 是正在使用中的文件描述符(stderr)。所以:
- 內核分配新 fd 的順序是:從低到高,找第一個沒用的。
- 如果你關閉了某個 fd,那么它會被下一次
open()
回收復用。 - 只有當 0、1、2 都被關閉,才會讓新文件從 0 開始重新分配。
7. 文件描述符 VS FILE *
一句話總結:文件描述符(fd)
是 Linux 系統內核的低層 I/O 機制,而 FILE \*
是 C 標準庫(stdio.h)封裝的高級 I/O 結構,它內部依賴文件描述符實現功能。
1. 文件描述符(int fd
)
- 是 Linux 內核分配的一個 整數索引
- 用于標識當前進程打開的某個文件(實際上是指向內核
struct file
的下標) - 使用
open()
,read()
,write()
,close()
等系統調用操作 - 屬于 低級 I/O
示例:
int fd = open("a.txt", O_RDWR); // 打開文件 "a.txt",以讀寫模式(O_RDWR)打開,返回文件描述符 fd
write(fd, "hello", 5); // 向文件描述符 fd 指向的文件中寫入字符串 "hello",寫入長度為 5 字節
close(fd); // 關閉文件描述符 fd,釋放相關資源
2. FILE *
指針
- 是
stdio.h
定義的 高級抽象結構體 - 內部其實就是封裝了一個
int fd
+ 緩沖區 + 文件狀態等信息 - 使用
fopen()
,fread()
,fwrite()
,fprintf()
,fclose()
等函數操作 - 屬于 高級 I/O
示例:
FILE* fp = fopen("a.txt", "w"); // 以寫模式("w")打開文件 "a.txt",返回文件指針 fp,注意:以 "w" 模式打開會清空文件原有內容
fprintf(fp, "hello\n"); // 使用 fprintf 向文件指針 fp 指向的文件中寫入字符串 "hello\n"
fclose(fp); // 關閉文件指針 fp,確保數據被正確寫入并釋放相關資源
3. 二者之間的關系圖
FILE *fp ───? struct __FILE (庫層) ───? int fd ───? 內核打開文件表(files_struct)
4. 相互轉換方法
從 FILE *
獲取 fd
:
int fd = fileno(fp);
從 fd
獲取 FILE *
:
FILE *fp = fdopen(fd, "w");
5. 區別對比總結
特性 | 文件描述符(int fd) | FILE * 指針 |
---|---|---|
所屬層次 | 內核層(系統調用) | C 標準庫(用戶空間) |
使用頭文件 | <fcntl.h> , <unistd.h> | <stdio.h> |
是否有緩沖機制 | ? 無緩沖 | ? 有緩沖 |
速度 | 快(系統級) | 慢(用戶態帶緩沖) |
函數接口 | open/read/write | fopen/fread/fwrite |
可否轉換 | ? 可以互相轉換 | ? 可以互相轉換 |
控制精細程度 | 更細粒度(如非阻塞、異步) | 比較抽象、功能豐富 |
實戰建議:
場景 | 建議使用 |
---|---|
寫系統調用、驅動、IO 重定向等底層功能 | fd (文件描述符) |
做格式化文本輸出、文件讀寫、緩存優化等 | FILE * (標準庫) |
共勉