課程內容#
文件操作#
- 【引入】之前學過的 cp、mv、cat 命令,都涉及到文件的讀寫
- cp:讀→寫
- mv:讀→寫→刪
- cat:讀→寫
- 上面這些步驟是如何實現的呢?
【底層操作,基於文件描述符】
open#
打開或創建一個文件 [別名:openat、create]
- man 2 open【關注函數原型及描述】
- 原型
- 返回值 int:文件描述符 或 -1
- 常用文件描述符:0-stdin、1-stdout、2-stderr
- -1:發生錯誤,並會設置 errno [可供 perror 使用,見代碼演示]
- flags:文件的打開方式
- [PS] 不需要特意記頭文件
- 描述
- 系統調用 [system call]:幫助你做你沒有權限做的事情
- 如果 open 的文件不存在,可能會新建該文件 [當 flags 裡定義了 O_CREAT]
- O_CREAT
- open 函數的 flag
- C 語言體系中,全大寫表示宏定義
- 底層是一個 int 型數據,叫做位掩碼
- 32 位,可表示 32 種狀態,每一位表示一種狀態
- 狀態之間可使用與、或、異或的方式轉換
- O_CREAT
- 文件描述符 [file descriptor]
- 小、非負、後續系統可調用 [read、write...]
- 返回值永遠是當前進程中可以取的最小的數字
- 可用來判斷文件數量 [如果返回 1000,當前文件數量一定超過 1000 了]
- 打開文件後,文件指針默認在文件頭部
- 文件描述 [file description]
- 每次調用 open 會創建一個新的 open 文件描述,它是系統全局文件表中的一條信息
- 記錄文件偏移量和文件的狀態
- [PS] 文件描述符是一個 open 文件描述的引用,不因 pathname 的改變而受影響
- 每次調用 open 會創建一個新的 open 文件描述,它是系統全局文件表中的一條信息
- ⭐flags
- 必須包含 O_RDONLY、O_WRONLY、O_RDWR 中的一個
- flag 之間用位或組合
- O_CREAT 創建
- O_TRUNC 截斷
- O_DIRECT 直接 IO
- 直接 IO—— 同步寫,文件會直接寫進去,而不經過緩衝
- 緩衝 IO
- 緩衝結束條件:① 攒到一堆數據;② 等固定時間
- 往磁碟寫一個字符 a,不會馬上寫入磁碟,可以減小成本
- 但斷電時易丟失數據
- [PS]
- 磁碟的最小單元是塊,每個塊是 4K
- 所以磁碟又叫塊設備
- 類似 printf 輸出到 stdout 的條件 [行緩衝]
- 遇到回車 / 程序結束,系統自動沖洗緩衝區
- 緩衝區滿,自動沖洗
- fflush 函數,手動沖洗
- 磁碟的最小單元是塊,每個塊是 4K
- 緩衝結束條件:① 攒到一堆數據;② 等固定時間
- O_NONBLOCK 非阻塞 IO
- 阻塞
- 如:scanf 的時候,要等標準輸入流中有輸入才能進行後面的操作
- 缺點:浪費資源
- 非阻塞
- 不會等
- 缺點
- 需頻繁回來查看,也浪費資源
- 需有某種機制監測,花費技術成本
- 阻塞
- O_TMPFILE 創建臨時文件
- 進程運行結束後文件就會被刪除,交易關閉也會
- 類似系統的臨時文件夾 /tmp
- ❗ 底層讀寫文件前,需要調用 open 函數獲得文件描述符
read#
通過文件描述符讀取數據
- man read
- 原型
- 返回值 ssize_t:讀取的字節數 或 -1
- 以_t 結尾,一般是用戶自定義類型
- 猜測:也是基本類型之一,可能是 long long,可能是 int
- 通過 ctags 一步步找具體類型:ctrl + ] 、ctrl + o
- 答案:int [32 位系統下];long int [64 位系統下]
- [PS] 按理說,32 位系統下,long int 大小等同於 int
- buf、count:每次最多讀取 count 字節數據到 buf 中
- 描述 + 返回值
- 嘗試讀取最多 count 字節到 buffer 中
- 讀取字節數達不到 count 的情況:被人中斷 [signal];數據本身不足 count 大小
- 每成功讀取 num [≤ count] 字節數據,文件偏移量 [像指針] 會自動往後走 num 大小
- 如果文件偏移量在 EOF [沒有數據可讀了],函數返回 0
- count
- 如果設為 0,錯誤可能被檢測出來,如果沒有檢測到錯誤,返回 0
- 如果大於 SSIZE_MAX [int /long int 的最大值],返回的結果將是定義好的 [POSIX.1 標準]
- 返回值
- ≤ count
- 出錯時返回 - 1,並會設置 errno
- [PS] ERRORS
- EAGAIN
- 讀文件 [包括 socket] 的時候,儘管文件已被設置為 O_NONBLOCK,read 將會阻塞
- EAGAIN
write#
通過文件描述符寫數據
- man 2 write
- 原型
- 與 read 很相似
- 描述 + 返回值
- 與 read 很相似
- 寫的字節數達不到 count 的情況:物理空間不夠;系統資源限制;被 signal 中斷
- open 文件時設置了 O_APPEND [追加]
- 文件偏移量 [offset] 在文件末尾,寫操作會追加
- 否則放在開頭,寫操作會覆寫
close#
關閉一個文件描述符
- man close
- 原型
- 主要就是關閉文件描述符
- [PS]
- 記錄鎖將被移除
- 特殊情況
- close 的是文件描述的最後一個文件描述符,文件描述對應的資源會被釋放
- close 的是文件的最後一個引用的文件描述符,文件將會被刪除
- 暫時不用在意內核具體做了什麼
【標準文件操作,基於文件指針】
<stdio.h>
fopen#
通過流打開文件
- man fopen
- 原型
- 返回值 FILE *:文件指針
- 早期是宏定義,這裡大寫是為了兼容性
- mode
- 類型是 char *,而不是 int
- 描述
- 關聯一個流 [stream]
- [PS] 網絡上發布數據,字節流;文件流 < 類型:FILE *>
- mode
- r /r+:讀 / 讀寫
- 流在文件開始
- w /w+:讀 / 讀寫
- 流在文件開始
- 文件存在,截斷文件 [打開就會將原數據清除]
- 文件不存在,創建文件
- a /a+:追加 / 讀與追加
- 追加時,流在 EOF;讀時,流在文件開始
- 文件不存在就會創建
- +:讀寫通吃
- [PS]
- b:可以在 mode 字符串的最後或者兩個字符之間,可用於處理二進制文件,但在 Linux 上一般沒有效果
- ❓ 任何被創建的文件會被進程的 umask value 修正
- r /r+:讀 / 讀寫
- 返回值
- 成功時,返回文件指針
- 出錯時,返回 NULL,並設置 errno
fread、fwrite#
二進制流的 IO
- man fread / fwrite
- fread:從 stream 中讀 nmeb 次數據 [size 字節 / 次] 到 ptr
- fwrite:把 ptr 的數據寫 nmeb 次數據 [size 字節 / 次] 到 stream
- 返回值 size_t:讀 / 寫的 items 數量 [成功]
- [無符號的 ssize_t]
- 發生錯誤或提前遇到 EOF 時 👉 0 ≤ 返回值 < nmeb
- ❗ 所以不能通過返回值區分 EOF 和錯誤,需使用 feof、ferror 確認
- [PS] 當 size 為 1 時,返回值等於傳輸的字節數
fclose#
刷新流並關閉文件描述符
- man fclose
- 刷新流實際調用的是 fflush
- 返回值
- 0 [成功]
- -1 (EOF),並設置 errno [失敗]
- 未定義行為 [傳入的是一個非法指針或已經被 fclose 過]
- ⭐標準 IO 裡的所有操作都是緩衝 IO
- 本身沒有權限寫,需等待內核控制
- ❓ 標準 IO 更適合文本 [用戶],底層的 IO 更適合二進制文件
目錄操作#
本質上也是文件 [早期可以直接 open]
opendir#
- man opendir
- 返回值 DIR *:目錄流指針 或 NULL
- 目錄流默認被放置在目錄的第一個條目
- 發生錯誤時,返回 NULL,並設置 errno
readdir#
- man readdir
- 返回值 struct dirent *:目錄項 或 NULL
- 目錄流中下一個目錄項 [構造體] 的指針
- 構造體的主要字段:d_ino、d_name
- [PS]
- 一次一次地返回下一個文件
- d_off:與 telldir 返回的值一樣,又類似 ftell ()
- 此 offset [每個文件的大小不同] 與一般意義 [字節為單位] 上的不一樣
- ftell () 獲取當前文件所在位置指示器的值
- NULL [遇到目錄流結尾或發生錯誤時]
- 目錄流中下一個目錄項 [構造體] 的指針
closedir#
- 關閉目錄
實現 ls -al 的基本思路#
- ls -al 效果
- 需要的信息有:文件權限、連接數、用戶名、組名、文件大小、修改時間、文件名
- 思路
- readdir()
- man readdir
- 讀取目錄裡的每一個文件
- 可獲取文件名
- stat()、lstat()
- man 2 stat
- 根據文件路徑獲取文件信息:stat 結構體
- 可獲取文件權限、硬連接數、uid、gid、文件大小、修改時間
- 可參考裡面的 EXAMPLE:lstat
- lstat () 和 stat () 的區別
- lstat () 可以查看軟連接的信息,而不會跳到軟連接指向的文件
- getpwuid()
- man getpwuid
- 根據 uid 獲取 passwd 結構體
- 可獲取對應的用戶名
- getgrgid
- man getgrgid
- 根據 gid 獲取 group 結構體
- 可獲取對應的組名
- 如果自己去實現 → 讀文件、切分
- 用戶信息:/etc/passwd
- 組的信息:/etc/group
- readdir()
- 其他細節
- 顏色
- 排序
- 純 ls 命令輸出的顯示列數隨寬度變化
- 獲取終端大小
- 參考 ioctl,man ioctl
- 列寬如何確定,可用暴力方式、二分查找、慢慢逼近
代碼演示#
底層文件操作#
- ⭐ 詳見註釋,關注使用
- ❗ 避免亂碼情況
- 字符串 buff 末尾留一位放 '\0'
- sizeof(buff) - 1
- 最後一次不足 512 字節的讀取,需要排除多餘字節的干擾
- 方法①:手動 menset (buff, 0, sizeof (buff))
- 方法②:始終保持數據末尾是 '\0',buff [nread] = '\0'
- [PS] 學習系統上層命令時,不必太關注這些
- 字符串 buff 末尾留一位放 '\0'
- perror 打印一個系統錯誤信息
- man 3 perror
- 原型
- fopen 等發生錯誤時就會設置 errno
- 描述
- 在 stderr 上輸出上一次調用的錯誤信息
- s 通常包含函數的名稱
- 建一個常用的 common 頭文件夾,放常用的頭文件
- head.h
標準文件操作#
- buffer 放在循環裡,每次都會初始化
- nread 為非負數,並且不能區分 EOF 和錯誤
標準 IO 是緩衝 IO#
- 第一個 "Hello world" 直接輸出,stderr 沒有緩衝
- 第二個 "Hello world" 本來會等 sleep 結束,無法輸出到 stdout,但是可以立馬輸出,通過👇
- 手動刷新緩衝區:fflush
- 輸出換行
- sleep 函數在 unistd.h 中
附加知識點#
- ulimit -a,可查看可打開的文件數量上限
- 每個進程中文件打開數量上限為 1024
- 超過會使系統崩潰
- [PS]
- 系統崩潰還需考慮內存
- 要做一個負責任的程序:手動 close /free、輸出錯誤日誌
- 只有標準輸出是行緩衝的
思考點#
- ❓ 文件保存就會立即寫入磁碟嗎?
- 參考 Are file edits in Linux directly saved into disk?——StackExchange
Tips#
- 在 vim 中,Shift + K 可跳到 man 手冊
- 推薦複製即翻譯軟件:CopyTranslator
- man 手冊在線文檔:man page——die.net