13.基礎IO(1)
文章目錄
- 13.基礎IO(1)
- 文件的基本概念:內容與屬性
- 文件的打開機制:fopen 和 open
- 被打開的文件與磁盤文件的區別
- 文件的內核數據結構
- 文件與進程的交互方式
- 標準輸入/輸出/錯誤與文件流
- 系統調用與文件描述符
- 文件打開模式(r/w/a/a+)與權限控制
- C 語言與系統調用在文件管理中的應用
- 權限控制與 `umask` 的影響
- 位圖與標志位參數傳遞的機制
文件的基本概念:內容與屬性
在 Linux 中,文件是操作系統存儲數據的基本單位,它包含兩部分:內容(即數據本身)和屬性(metadata)。屬性包括文件名、類型、大小、所有者、權限、創建/修改時間等元信息。據講稿與相關資料指出,任何文件都包含內容和屬性,即使一個文件沒有任何內容(空文件),它仍具有名稱、權限等屬性,并且這些屬性也需要占用磁盤空間。例如,用 ls -l
可以看到空文件大小為 0,但目錄中仍保留了它的 inode 信息和屬性;這是因為文件的屬性也記錄在磁盤上。
文件的內容則存儲在磁盤的數據塊中,當文件被打開后才會被加載到內存供進程訪問。根據馮諾依曼體系結構,CPU 只能直接訪問內存而無法直接訪問磁盤,因此要訪問一個文件,必須先將文件加載到內存,這個過程即為“打開文件”。綜上,文件 = 內容 + 屬性,而文件的屬性往往保存在 inode 等結構中。
【課外補充】在 Linux 文件系統中,每個文件在磁盤上用 inode(索引節點)來記錄其元數據和數據塊位置,包括權限、類型、大小、時間戳、硬鏈接數等信息。例如,ls -il
顯示的第一列就是 inode 編號,通過 inode 可以獲取文件的各種屬性。
文件的打開機制:fopen 和 open
在編程中訪問文件時,必須顯式地打開文件。C 語言標準庫提供 fopen
函數(位于 <stdio.h>
)來打開文件,返回一個 FILE*
類型的文件指針供后續讀寫使用;而在系統調用層面,Linux 提供了 open
系統調用(位于 <fcntl.h>
)來打開文件,返回一個非負整數的文件描述符(file descriptor)。兩者的主要區別是:fopen
是庫函數,會對調用參數進行封裝并返回 FILE*
;open
是直接與內核交互的系統調用,需要傳入文件路徑、標志位 (flags) 和權限模式 (mode),返回一個 int
型文件描述符。
只有當程序執行到打開文件的語句時,文件才真正被加載到內存中。例如,在下面代碼段中,只有當 fopen
或 open
運行時,文件才被打開:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main() {// 使用 fopen 打開文件,返回 FILE*FILE *fp = fopen("example.txt", "w");if (fp == NULL) {perror("fopen");return 1;}fputs("Hello, world!\n", fp);fclose(fp);// 使用 open 打開文件,返回文件描述符int fd = open("example2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perror("open");return 1;}write(fd, "Hello, world!\n", 14);close(fd);return 0;
}
在上例中,直到程序運行到 fopen
或 open
并執行成功時,文件才真正被打開并加載到內存中。執行 fopen
返回 FILE*
后,可以通過 fread/fwrite/fputs
等接口對文件內容進行讀寫;執行 open
返回的文件描述符可以與 read
、write
、lseek
等系統調用配合使用。如果打開失敗,fopen
返回 NULL
,open
返回 -1
,并設置相應的錯誤碼。
被打開的文件與磁盤文件的區別
磁盤上的文件和被打開的文件是兩個不同的概念。磁盤上的文件是靜態存在的,包含文件內容和元數據(屬性),但尚未加載到內存。只有當進程調用 fopen
或 open
并成功時,文件才被打開,它的內容和屬性才被加載到內存,并由內核為其分配數據結構以供訪問。這種加載過程類似將文件從磁盤映射到內核空間,使得 CPU 可以直接操作它。
簡言之:未打開的文件只能在磁盤上存在;被打開的文件則在內存中有對應結構體供進程訪問。根據講稿和資料的描述,當我們打開一個文件時,操作系統會把文件的內容和屬性加載到內存中。在內存中的文件通常用文件對象(如 Linux 內核中的 struct file
)來表示,該結構體包含文件的各種屬性以及指向文件數據的指針。相比之下,磁盤上的文件只是存儲在文件系統上的數據塊和 inode 信息。
因此,在一個系統中存在大量磁盤文件,但只有當進程需要時才會打開其中的少數文件。例如,一個 Linux 系統可能有上萬文件,但同時被打開的可能只有幾百個。操作系統需要跟蹤管理哪些文件已打開、對應哪個進程以及何時關閉,保障文件訪問的正確性和資源回收。
文件的內核數據結構
在 Linux 內核中,每個被打開的文件都會對應內核級數據結構來表示其狀態和屬性。常見的結構有 struct file
(表示已打開的文件實例)和 struct inode
(表示磁盤上的文件元數據)。講稿中提到,文件在內核中本質上也等于 內容 + 屬性,即有對應的結構體來描述,包括文件大小、權限、讀寫位置等信息。我們可以把 struct file
理解為文件的“內核鏡像”,它會保存文件當前位置(offset)、文件系統操作函數指針、引用計數等,當進程通過文件描述符讀寫文件時,實際上是通過這些結構體進行操作的。
操作系統如何管理被打開的文件呢?每個進程都有一個 task_struct
,其中包含一個指向 struct files_struct
的指針。files_struct
中維護了一個文件描述符數組 fd_array
,該數組的每個元素指向一個內核中的 struct file
結構。當進程調用 open
打開新文件時,內核會在這個數組中找到一個空閑的下標(例如 3、4、5 等)分配給該文件,并返回該下標作為文件描述符。可以說,系統層面訪問文件的唯一途徑就是文件描述符。例如,進程啟動時默認占用 0、1、2 三個文件描述符(分別對應 stdin
、stdout
、stderr
),后續打開的第一個文件會獲得描述符 3。
此外,struct file
結構中還包含一個引用計數,用以記錄有多少個文件描述符或進程引用該文件。這意味著同一個文件可以被多個描述符(甚至不同進程)共享讀取或寫入。文件操作(讀/寫/關閉)最終都會通過這些內核結構執行,內核維護的這種數據結構保證了對文件的并發訪問和正確釋放。
文件與進程的交互方式
在 Linux 中,進程是訪問和操作文件的主體。只有進程才能調用 fopen
、open
等接口來操作文件。講稿強調,當程序中出現了 fopen
或 open
語句,并且進程實際執行到這一句時,文件才被打開。也就是說,即便源代碼中有 fopen
調用,如果程序尚未運行或者未執行到該行,文件也不會被打開。執行完成后,進程會得到一個文件指針或文件描述符,通過它可以進行后續的讀寫操作,最后再調用 fclose
或 close
關閉文件釋放資源。
一個進程可以同時打開多個文件。實際上,每個進程啟動時就自動打開了三個標準流(stdin
,stdout
,stderr
),而用戶程序可以根據需要繼續打開其他文件。操作系統為每個進程維護獨立的文件描述符表,確保各進程對文件的操作互不干擾。當進程結束或主動關閉文件時,內核會關閉對應的 struct file
,更新引用計數,并回收內存和描述符。
總之,文件操作的發生總是伴隨著一個進程:訪問文件的始終是進程,而非靜態的代碼文本。進程運行時必須先打開文件,此時操作系統將文件加載到內存,并返回用于標識該文件的文件描述符或 FILE*
。后續對文件內容的讀寫,都是通過該進程內的指針或描述符發起的。進程關閉文件后,文件可以從內存中卸載,相關資源被釋放。
標準輸入/輸出/錯誤與文件流
每個進程默認啟動時都會打開三個標準流,用于與外界(鍵盤、顯示器等)進行交互:標準輸入(stdin
,通常對應鍵盤,文件描述符 0)、標準輸出(stdout
,通常對應顯示器,文件描述符 1)和標準錯誤(stderr
,也對應顯示器或終端,文件描述符 2)。在 C 語言中,這三個標準流都是 FILE*
類型指針,分別指向 stdin
,stdout
,stderr
。例如,printf
默認向 stdout
寫入,而 scanf
則從 stdin
讀取。雖然鍵盤和顯示器是硬件設備,但 C 標準庫將它們抽象為文件流(通過底層的系統調用與操作系統交互),因此對它們的讀寫操作與普通文件類似。
標準流的具體對應關系如下:
stdin
:標準輸入(鍵盤),文件描述符 0。stdout
:標準輸出(顯示器或終端),文件描述符 1。stderr
:標準錯誤(顯示器或終端),文件描述符 2。
例如,下面的代碼會從標準輸入讀取一行,然后再通過標準輸出打印出來:
#include <stdio.h>int main() {char buf[100];// 從標準輸入(stdin)讀一行if (fgets(buf, sizeof(buf), stdin)) {// 將讀取到的內容打印到標準輸出(stdout)printf("你輸入了: %s", buf);}return 0;
}
可以看到,我們使用 stdin
和 stdout
完成了輸入輸出。由于它們都是 FILE*
,底層實際上對應文件描述符 0 和 1。總之,標準輸入/輸出/錯誤在實現上也是文件,只是內核默認為每個新進程打開了這三個文件流。
系統調用與文件描述符
在 Linux 中進行文件操作的系統調用有 open
、close
、read
、write
等。文件描述符是內核為進程打開文件后分配的整數句柄,用于索引進程的文件描述符表。調用 open
時,如果成功,內核返回一個非負整數(通常從 3 開始,因為 0、1、2 已被標準流占用)。這個整數就是文件描述符。例如:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main() {int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) {perror("open失敗");return 1;}printf("打開文件得到文件描述符: %d\n", fd);// 可以使用 write(fd, ..., ...) 和 close(fd)close(fd);return 0;
}
上例中,如果 log.txt
打開成功,將打印出類似 3
這樣的文件描述符值。打開失敗時 fd
為 -1
,并通過 perror
輸出錯誤信息。我們注意到文件描述符是一個索引,它指向內核中某個 struct file
結構。只有通過文件描述符,系統調用才能找到對應的文件資源。
此外,所有針對文件的系統調用(如 read
、write
、lseek
、close
等)都以文件描述符作為參數。內核通過進程的 files_struct
中的 fd_array
找到對應的內核文件對象,然后執行操作。因此,系統層面上訪問文件的唯一途徑就是文件描述符。正因如此,FILE*
之類的用戶態結構內部也保存了一個文件描述符(可通過 fp->_fileno
獲取),以便通過系統調用完成實際的讀寫。
?現在知道,?件描述符就是從0開始的?整數。當我們打開?件時,操作系統在內存中要創建相應的數據結構來描述?標?件。于是就有了file結構體。表??個已經打開的?件對象。?進程執?open系統調?,所以必須讓進程和?件關聯起來。每個進程都有?個指針*files, 指向?張表files_struct,該表最重要的部分就是包含?個指針數組,每個元素都是?個指向打開?件的指針!所以,本質上,?件描述符就是該數組的下標。所以,只要拿著?件描述符,就可以找到對應的?件。
文件打開模式(r/w/a/a+)與權限控制
在 C 標準庫的 fopen
中,文件打開模式通過模式字符串指定,常用模式包括:
"r"
:以只讀方式打開文件,文件必須存在;讀操作從文件開頭開始。"r+"
:以讀寫方式打開文件,文件必須存在;操作位置在開頭。"w"
:以寫方式打開文件,如果文件不存在則創建;如果文件存在則清空原內容,相當于截斷文件再寫入。"w+"
:以讀寫方式打開,效果類似于w
。"a"
:以追加方式打開文件,如果文件不存在則創建;寫入時總是追加到文件末尾。"a+"
:以讀寫方式打開,寫操作追加到末尾。
例如,上述代碼示例中 fopen("log.txt", "w")
會創建新文件或清空已有文件,并將數據寫入。如果改為 "a"
模式,則不會清空原文件,而是將內容追加到末尾。這些模式與 shell 中的重定向符號類似:>
對應清空寫入,>>
對應追加寫入。
對于系統調用 open
,訪問模式和標志通過參數 flags
指定。常見的標志包括:
- 訪問模式:
O_RDONLY
(只讀)、O_WRONLY
(只寫)、O_RDWR
(讀寫) - 創建模式:
O_CREAT
(如果文件不存在則創建)、O_EXCL
(配合O_CREAT
使用,如果文件已存在則打開失敗) - 截斷追加:
O_TRUNC
(如果文件存在則清空其內容)、O_APPEND
(寫操作追加到末尾)
這些標志是 位標志,可以通過按位或組合多個選項(位圖方式)。每個宏對應一個二進制位,例如 O_CREAT
= 0x40。我們可以寫 O_WRONLY | O_CREAT
來同時設置只寫和創建標志。例如:
int fd = open("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
上述調用試圖以讀寫模式打開 data.txt
,如果不存在則創建,并在打開時清空原有內容。權限參數 0666
指定新文件的初始權限(隨后會受 umask
掩碼的影響,詳見下一節)。
需要注意的是,open
系統調用本身并不設置文件訪問權限,它只是使用提供的參數和當前進程的 umask
決定文件的最終權限。如果我們只讀打開已有文件(不需要創建),可以省略權限參數,只傳兩個參數即可。
C 語言與系統調用在文件管理中的應用
在 C 語言中,文件操作常用標準庫函數,如 fopen
/fclose
、fread
/fwrite
、fprintf
/fscanf
等。這些函數在用戶態提供了方便的接口,但它們底層最終都會調用相應的系統調用。簡而言之:
- C 標準庫函數(例如
fopen
,fclose
,fread
,fwrite
)是一種對系統調用的封裝,使用起來更方便,自動管理緩沖區。 - 系統調用(例如
open
,close
,read
,write
)是內核提供的底層接口,功能更原始,需要程序員自己處理緩沖和錯誤。
講稿總結道:
fopen, fclose, fwrite, fread
—— C 庫函數;
open, close, write, read
—— 系統調用;
C 庫函數就是系統調用的封裝。
我們在前面的示例代碼中就使用了 fopen
/fputs
和 open
/write
的組合。二者的使用基本相同,只是一個返回 FILE*
,另一個返回 int fd
。C 庫函數在底層自動調用了相同功能的系統調用,并通常帶有文件緩沖機制。
比如,可以用下面代碼分別展示兩種方法寫文件:
// 使用 C 庫函數 fwrite
#include <stdio.h>
int main() {FILE *fp = fopen("test.txt", "w");if (!fp) return 1;fprintf(fp, "Hello, libc!\n");fclose(fp);return 0;
}
// 使用系統調用 write
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {int fd = open("test_sys.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (fd < 0) return 1;const char *msg = "Hello, syscall!\n";write(fd, msg, strlen(msg));close(fd);return 0;
}
兩個示例效果類似,但實現層次不同。第二個示例是直接與內核交互,第一種方式在內部會調用第二種方式并做緩沖。了解兩者的差異有助于在需要精細控制時直接使用系統調用,而大部分應用開發可以繼續使用更易用的 C 庫函數。
權限控制與 umask
的影響
Linux 中每個文件都有訪問權限設置,一般用三組 rwx
位表示屬主、屬組和其它用戶的讀寫執行權限。對于新創建的文件,open
系統調用第三個參數指定了文件的初始權限(如 0666
,即默認可讀可寫),但最終權限還會受進程的文件模式掩碼(umask
)影響。操作系統會將給定的權限值與 umask
做按位與再取反,從而得出文件的實際權限。例如:如果進程的 umask
為 022
,新文件的默認權限 0666
會變成 0644
(去掉了對“其它用戶”的寫權限)。
講稿中演示了這一過程:在 open("log.txt", O_CREAT, 0666)
后發現文件權限并非 0666
而是根據掩碼修正后的值。可以在程序中調用 umask(0)
來臨時將掩碼清零,這樣之后創建文件就會嚴格使用指定的權限。要注意,umask
是進程級的屬性,修改當前進程的 umask
不會影響其他進程。
例如:
# 進程默認 umask 為 022,創建文件時權限=0666&~022=0644
$ touch file1.txt
$ ls -l file1.txt
-rw-r--r-- # 改變 umask 后再創建
$ umask 000
$ touch file2.txt
$ ls -l file2.txt
-rw-rw-rw- # 此時文件權限就是0666
在上述示例中,可以看到 umask
影響了新文件的權限。此外,open
的權限參數只有在指定 O_CREAT
時才有效,如果只讀打開現有文件就不需提供權限參數。
位圖與標志位參數傳遞的機制
在許多系統調用(如 open
、mmap
、socket
等)中,為了同時傳遞多個選項,Linux 通常采用**位掩碼(bitmap)**的方式。即將整型參數的每一位作為獨立的開關。這樣我們可以用按位或(|
)來組合多個選項,僅需一個參數即可表示多個布爾配置。
以 open
為例,其 flags
參數就是一個 32 位的位圖,每個標志宏(如 O_RDONLY=0x0000
、O_WRONLY=0x0001
、O_CREAT=0x0040
等)在這個整數中只有一個位為 1。多個標志可以組合,比如 O_WRONLY | O_CREAT | O_TRUNC
。下面這個示例片段演示了位掩碼的原理(簡化示意):
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)void Test(int flags) {if (flags & ONE) printf("ONE\n");if (flags & TWO) printf("TWO\n");if (flags & THREE) printf("THREE\n");
}int main() {Test(ONE | THREE); // 輸出 ONE 和 THREEreturn 0;
}
在 open("file", O_WRONLY | O_CREAT)
的調用中,flags
參數的值就是上述位或的結果。內核讀取這個整數后,通過與操作檢查各個位是否被設置,從而知道用戶希望啟用哪些功能。這種位圖傳參機制非常靈活,能夠支持在單個參數中傳遞多個選項,也避免了傳遞過多單獨參數。對開發者來說,只需記住各個宏代表的含義,并使用按位或即可組合使用。
【課外補充】此處介紹的位圖傳參方法在很多系統調用和庫接口中都很常見,不限于文件操作。例如 open
、fcntl
、mmap
、網絡編程的 socket
等調用都使用類似方式傳遞標志位。