文章目錄
- 一、理解文件
- 1.1 狹義理解
- 1.2 廣義理解
- 1.3 文件操作的歸類認識
- 1.4 系統角度:進程與文件的交互
- 1.5 實踐示例
- 二、回顧 C 文件接口
- 2.1 hello.c 打開文件
- 2.2 hello.c 寫文件
- 2.3 hello.c 讀文件
- 2.4 輸出信息到顯示器的幾種方法
- 2.5 stdin & stdout & stderr
- 三、系統文件I/O
- 3.1 一種傳遞標志位的方法
- 3.2 接口介紹
- 3.5 3-5 open函數返回值
- 3.6 文件描述符 fd
- 3.6.1 0 & 1 & 2
- 3.6.2 文件描述符的分配規則:最小未使用下標優先
- 3.6.3 應用場景:重定向的實現
- 3.6.4 dup2 系統調用
- 五、緩沖區
- 5.1 什么是緩沖區
- 5.2 為什么要引入緩沖區機制
- 5.3 緩沖類型
一、理解文件
1.1 狹義理解
- 文件在磁盤里
*磁盤作為永久性存儲介質,通過文件系統(如 EXT4、XFS)管理文件存儲。文件系統將磁盤劃分為inode(索引節點)和block(數據塊):- inode:存儲文件元數據(權限、所有者、修改時間等),每個文件唯一對應一個 inode。
- block:存儲文件實際數據,大小由文件系統決定(如 4KB)。
注意:即使 0KB 的空文件也會占用inode 空間(不同文件系統 inode 大小不同,如 EXT4 默認 256 字節),但不占用數據塊(block)。
- 磁盤是外設(輸入 / 輸出設備)
對磁盤文件的操作本質是IO(Input/Output),涉及內核與外設的數據交互(如通過 DMA 控制器讀寫磁盤)。
1.2 廣義理解
Linux下一切皆文件
系統將硬件設備、進程信息、通信管道等抽象為文件,通過統一接口管理:
- 硬件設備:
- 塊設備:以塊為單位讀寫(如硬盤
/dev/sda
,文件類型b
)。 - 字符設備:以字符流讀寫(如鍵盤
/dev/input/event0
,文件類型c
)。
- 塊設備:以塊為單位讀寫(如硬盤
- 虛擬文件系統:
/proc
:動態映射進程信息(如/proc/self/exe
是當前進程二進制文件)。/sys
:暴露內核設備驅動細節(如/sys/class/leds/
控制 LED 燈)。
- 進程通信:
- 管道文件(類型
p
):mkfifo mypipe
創建命名管道。 - 套接字文件(類型 s):
/run/docker.sock
用于Docker
進程通信。
這種抽象屏蔽了底層差異,例如讀寫/dev/tty1
(終端設備文件)與讀寫普通文件使用相同 API。
- 管道文件(類型
1.3 文件操作的歸類認識
文件 = 元數據(屬性) + 數據內容
- 元數據:
- 基礎屬性:權限(
rwx
)、所有者(uid/gid
)、硬鏈接數(ls -l
第二列)。 - 時間戳:修改時間(
mtime
)、狀態改變時間(ctime
)、訪問時間(atime
)。 - 技術屬性:inode 編號(
ls -i
)、文件大小(ls -l
第五列)、塊數(ls -s
)。
- 基礎屬性:權限(
- 數據內容:
分為文本(ASCII/UTF-8)和二進制(如可執行程序、圖片),通過cat
、hexdump
等工具查看。 - 操作分類
- 內容操作:讀寫(
read/write
系統調用)、定位(lseek
)、截斷(truncate
)。
- 內容操作:讀寫(
- 屬性操作:
- 修改權限:
chmod
(對應chmod
系統調用)。 - 更改所有者:
chown
(對應chown
系統調用)。 - 查看元數據:
stat命令
(對應stat
系統調用,返回struct stat
結構體)。
- 修改權限:
1.4 系統角度:進程與文件的交互
- 一切文件操作由進程觸發
內核通過 ** 文件描述符(File Descriptor, FD)** 標識進程打開的資源,FD
是0~1023
的整數(默認:0=stdin
,1=stdout
,2=stderr
)。
可通過ls -l /proc/$$/fd
查看當前進程打開的文件($$
為當前進程PID
)。 - 系統調用 vs 庫函數
- 系統調用:內核提供的底層接口(如
open
、read
),需從用戶態陷入內核態,開銷較高但更直接。 - 庫函數:C 標準庫封裝的高層接口(如
fopen
、fread
),內部調用系統調用并提供緩存機制(如stdio
的緩沖區)。 - 示例:
fprintf(stdout, "hello")
最終會調用write(1, "hello", 5)
系統調用。
- 系統調用:內核提供的底層接口(如
- 內核如何管理文件
- 每個打開的文件對應內核中的 file結構體,記錄文件位置、引用計數等。
- 多個進程可通過不同 FD 指向同一file結構體(如父子進程共享文件),實現數據共享。
1.5 實踐示例
- 查看文件元數據
stat test.txt # 顯示inode、權限、時間戳等詳細信息
ls -li test.txt # 查看inode編號和硬鏈接數
- 操作設備文件
echo "Hello zkp!" > /home/zkp/linux/25/6/7/file/test.txt # 向文件寫入信息
cat /home/zkp/linux/25/6/7/file/test.txt # 查看文件內容
- 理解文件描述符
exec 3<> file.txt # 在當前Shell中打開文件,FD=3可讀可寫
echo "test" >&3 # 通過FD=3寫入文件
cat <&3 # 通過FD=3讀取文件
exec 3>&- # 關閉FD=3
二、回顧 C 文件接口
2.1 hello.c 打開文件
打開的myfile
文件在哪個路徑下?
- 在程序的當前路徑下,那系統怎么知道程序的當前路徑在哪里呢?
可以使用ls /proc/[進程id]
命令查看當前正在運行進程的信息:
其中: cwd
:指向當前進程運行目錄的一個符號鏈接。exe
:指向啟動當前進程的可執行文件(完整路徑)的符號鏈接。
打開文件,本質是進程打開,所以,進程知道自己在哪里,即便文件不帶路徑,進程也知道。由此OS就能知道要創建的文件放在哪里。
2.2 hello.c 寫文件
2.3 hello.c 讀文件
2.4 輸出信息到顯示器的幾種方法
2.5 stdin & stdout & stderr
- C 默認會打開三個輸出流,分別是 stdin,stdout,stderr
- 這三個流的類型都是
FILE*
,而fopen
返回值類型也是文件指針
三、系統文件I/O
我們知道,文件的權限分為 rwx
,對應的標志位為 4,2,1。
3.1 一種傳遞標志位的方法
核心原理:位掩碼(Bit Mask)
每個標志對應 唯一的二進制位(如第 0 位、第 1 位…),通過 位運算 組合 / 解析:
- 設置標志:用
|
(按位或)組合多個標志(如FLAG_A | FLAG_B
)。 - 檢查標志:用
&
(按位與)判斷某一位是否為 1(如if (flags & FLAG_A)
)。
#include <stdio.h>// 定義權限標志位(與Linux系統保持一致)
#define PERM_READ (1 << 2) // 4: 讀權限
#define PERM_WRITE (1 << 1) // 2: 寫權限
#define PERM_EXEC (1 << 0) // 1: 執行權限// 解析權限并打印
void func(int perms) {printf("用戶權限: ");printf(perms & USER_PERMS(PERM_READ) ? "r" : "-");printf(perms & USER_PERMS(PERM_WRITE) ? "w" : "-");printf(perms & USER_PERMS(PERM_EXEC) ? "x" : "-");printf("\n");
}int main() {// 組合權限:用戶有讀寫,組有讀,其他用戶無權限int perms = (PERM_READ | PERM_WRITE)printf("權限掩碼(八進制): 0%o\n", perms); // 輸出: 0x6func(perms);return 0;
}
3.2 接口介紹
參數:
pathname
:要打開或創建的目標文件flags
:打開文件時,可以傳入多個參數選項,用下面的一個或者多個常量進行“或”運算,構成flags
。O_RDONLY
:只讀打開O_WRONLY
:只寫打開O_RDWR
:讀,寫打開
這三個常量,必須指定一個且只能指定一個O_CREAT
:若文件不存在,則創建它。需要使用mode選項,來指明新文件的訪問權限O_APPEND
:追加寫
返回值:
- 成功:新打開的文件描述符
- 失敗:-1
open
函數具體使用哪個,和具體應用場景相關,如目標文件不存在,需要open
創建,則第三個參數表示創建文件的默認權限;否則,使用兩個參數的open
。
write
、read
、close
、lseek
,類比c文件相關接口。
3.5 3-5 open函數返回值
在認識返回值之前,先來認識一下兩個概念:系統調用和庫函數
- 上面的
fopen
fclose
fread
fwrite
都是C標準庫當中的函數,我們稱之為庫函數(libc)。 - 而
open
close
read
write
lseek
都屬于系統提供的接口,稱之為系統調用接口
看下面這張圖:
系統調用接口和庫函數的關系,一目了然。
所以,可以認為,f#
系列的函數,都是對系統調用的封裝,方便二次開發。
3.6 文件描述符 fd
- 通過對
open
函數的學習,我們知道了文件描述符就是一個小整數
3.6.1 0 & 1 & 2
- Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0,標準輸出1,標準錯誤2。
- 0,1,2對應的物理設備一般是:鍵盤,顯示器,顯示器
所以輸入輸出還可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>int main()
{char buf[1024];ssize_t s = read(0, buf, sizeof(buf));if (s > 0) {buf[s] = 0;write(1, buf, strlen(buf));write(2, buf, strlen(buf));} return 0;
}
3.6.2 文件描述符的分配規則:最小未使用下標優先
- 默認初始狀態:
進程啟動時,內核會自動打開 3 個標準文件描述符:- 0:標準輸入(
stdin
,默認關聯鍵盤) - 1:標準輸出(
stdout
,默認關聯終端) - 2:標準錯誤(
stderr
,默認關聯終端)
因此,首次打開新文件時,文件描述符從 3 開始分配(依次遞增:3、4、5…)。
- 0:標準輸入(
- 關閉后復用:
如果進程主動關閉某個文件描述符(如close(0)
),后續調用open
時,內核會 掃描文件描述符表,選擇最小的未被使用的下標 分配給新文件。- 例:關閉 0 后,新打開的文件會優先占用 0;
- 若同時關閉 0 和 2,新打開的文件會依次占用 0、2,再繼續遞增(如 3、4…)。
代碼驗證:
#include <stdio.h>
#include <unistd.h> // close
#include <fcntl.h> // open, O_RDWR, O_CREATint main() {// 1. 初始打開:未關閉默認FD,從3開始int fd1 = open("test1.txt", O_RDWR | O_CREAT, 0644);printf("fd1: %d\n", fd1); // 輸出:3(0、1、2已占用)// 2. 關閉標準輸入(FD=0),后續打開優先復用0close(0); int fd2 = open("test2.txt", O_RDWR | O_CREAT, 0644);printf("fd2: %d\n", fd2); // 輸出:0(最小未使用下標)// 3. 關閉標準錯誤(FD=2),后續打開優先復用2close(2); int fd3 = open("test3.txt", O_RDWR | O_CREAT, 0644);printf("fd3: %d\n", fd3); // 輸出:2(當前最小未使用下標)// 4. 繼續打開,下一個最小未使用是3(0、2已用,1仍被stdout占用)int fd4 = open("test4.txt", O_RDWR | O_CREAT, 0644);printf("fd4: %d\n", fd4); // 輸出:3return 0;
}
運行結果:
fd1: 3
fd2: 0
fd3: 2
fd4: 3
3.6.3 應用場景:重定向的實現
- 輸出重定向示例:
# 將命令輸出寫入文件(本質是修改FD=1的指向)
ls -l > output.txt
實現邏輯:
- Shell 先關閉 FD=1(標準輸出),再打開
output.txt
,此時新文件會占用 FD=1; - 后續
ls
命令的輸出會寫入 FD=1(即output.txt
),而非終端。
- 代碼模擬重定向:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main() {// 1. 關閉標準輸出(FD=1)close(1); // 2. 打開新文件,會復用FD=1int fd = open("redirect.txt", O_WRONLY | O_CREAT, 0644);// 3. printf 會向 FD=1 寫入(此時指向 redirect.txt)printf("Hello, Redirect!\n"); close(fd);return 0;
}
運行后,redirect.txt 會包含 Hello, Redirect!,而非終端輸出
注意事項
- 關閉默認描述符(如
close(1)
)后,若后續代碼依賴標準輸出(如printf
),會導致輸出丟失或異常。 - 建議使用 dup2 實現重定向(安全關閉舊描述符,避免沖突)。
重定向的本質:
3.6.4 dup2 系統調用
示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>int main() {// 1. 打開文件(獲取新的文件描述符,如3)int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd == -1) {perror("open failed");return 1;}// 2. 將標準輸出(FD=1)重定向到 fd 指向的文件if (dup2(fd, 1) == -1) {perror("dup2 failed");close(fd);return 1;}// 3. 此時 printf 會寫入 output.txt,而非終端printf("Hello, dup2!\n");// 4. 關閉 fd(注意:標準輸出仍指向 output.txt)close(fd);// 5. 驗證:繼續向標準輸出寫入fprintf(stdout, "This will also appear in output.txt\n");return 0;
}
printf
是C庫當中的IO函數,一般往stdout
中輸出,但是stdout
底層訪問文件的時候,找的還是fd:1,但此時,fd:1下標所表示內容,已經變成了myfifile
的地址,不再是顯示器文件的地址,所以,輸出的任何消息都會往文件中寫入,進而完成輸出重定向。
五、緩沖區
5.1 什么是緩沖區
緩沖區是內存空間的一部分。也就是說,在內存空間中預留了一定的存儲空間,這些存儲空間用來緩沖輸入或輸出的數據,這部分預留的空間就叫做緩沖區。緩沖區根據其對應的是輸入設備還是輸出設備,分為輸入緩沖區和輸出緩沖區。
5.2 為什么要引入緩沖區機制
讀寫文件時,如果不會開辟對文件操作的緩沖區,直接通過系統調用對磁盤進行操作(讀、寫等),那么每次對文件進行一次讀寫操作時,都需要使用讀寫系統調用來處理此操作,即需要執行一次系統調用,執行一次系統調用將涉及到CPU狀態的切換,即從用戶空間切換到內核空間,實現進程上下文的切換,這將損耗一定的CPU時間,頻繁的磁盤訪問對程序的執行效率造成很大的影響。
為了減少使用系統調用的次數,提高效率,我們就可以采用緩沖機制。比如我們從磁盤里取信息,可以在磁盤文件進行操作時,可以一次從文件中讀出大量的數據到緩沖區中,以后對這部分的訪問就不需要再使用系統調用了,等緩沖區的數據取完后再去磁盤中讀取,這樣就可以減少磁盤的讀寫次數,再加上計算機對緩沖區的操作大大快于對磁盤的操作,故應用緩沖區可大大提高計算機的運行速度。
又比如,我們使用打印機打印文檔,由于打印機的打印速度相對較慢,我們先把文檔輸出到打印機相應的緩沖區,打印機再自行逐步打印,這時我們的CPU可以處理別的事情。可以看出,緩沖區就是一塊內存區,它用在輸入輸出設備和CPU之間,用來緩存數據。它使得低速的輸入輸出設備和高速的CPU能夠協調工作,避免低速的輸入輸出設備占用CPU,解放出CPU,使其能夠高效率工作。
5.3 緩沖類型
標準I/O提供了3種類型的緩沖區。
- 全緩沖區:這種緩沖方式要求填滿整個緩沖區后才進行I/O系統調用操作。對于磁盤文件的操作通常使用全緩沖的方式訪問。
- 行緩沖區:在行緩沖情況下,當在輸入和輸出中遇到換行符時,標準I/O庫函數將會執行系統調用操作。當所操作的流涉及一個終端時(例如標準輸入和標準輸出),使用行緩沖方式。因為標準1/O庫每行的緩沖區長度是固定的,所以只要填滿了緩沖區,即使還沒有遇到換行符,也會執行I/0系統調用操作,默認行緩沖區的大小為1024。
- 無緩沖區:無緩沖區是指標準I/O庫不對字符進行緩存,直接調用系統調用。標準出錯流stderr通常是不帶緩沖區的,這使得出錯信息能夠盡快地顯示出來。
除了上述列舉的默認刷新方式,下列特殊情況也會引發緩沖區的刷新:
- 緩沖區滿時;
- 執行
flush
語句;