Bo2SS

Bo2SS

1 文件、目錄操作與實現ls的思路

課程內容#

文件操作#

  • 【引入】之前學過的 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 種狀態,每一位表示一種狀態
          • 狀態之間可使用與、或、異或的方式轉換
    • 文件描述符 [file descriptor]
      • 小、非負、後續系統可調用 [read、write...]
      • 返回值永遠是當前進程中可以取的最小的數字
        • 可用來判斷文件數量 [如果返回 1000,當前文件數量一定超過 1000 了]
    • 打開文件後,文件指針默認在文件頭部
    • 文件描述 [file description]
      • 每次調用 open 會創建一個新的 open 文件描述,它是系統全局文件表中的一條信息
        • 記錄文件偏移量和文件的狀態
      • [PS] 文件描述符是一個 open 文件描述的引用,不因 pathname 的改變而受影響
    • ⭐flags
      • 必須包含 O_RDONLY、O_WRONLY、O_RDWR 中的一個
      • flag 之間用位或組合
      • O_CREAT 創建
      • O_TRUNC 截斷
      • O_DIRECT 直接 IO
        • 圖片
        • 直接 IO—— 同步寫,文件會直接寫進去,而不經過緩衝
        • 緩衝 IO
          • 緩衝結束條件:① 攒到一堆數據;② 等固定時間
            • 往磁碟寫一個字符 a,不會馬上寫入磁碟,可以減小成本
            • 但斷電時易丟失數據
            • [PS]
              • 磁碟的最小單元是塊,每個塊是 4K
                • 所以磁碟又叫塊設備
              • 類似 printf 輸出到 stdout 的條件 [行緩衝]
                • 遇到回車 / 程序結束,系統自動沖洗緩衝區
                • 緩衝區滿,自動沖洗
                • fflush 函數,手動沖洗
      • 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 將會阻塞

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 修正
  • 返回值
    • 圖片
    • 成功時,返回文件指針
    • 出錯時,返回 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
  • 其他細節
    • 顏色
    • 排序
    • 純 ls 命令輸出的顯示列數隨寬度變化
      • 獲取終端大小
      • 圖片
      • 參考 ioctl,man ioctl
    • 列寬如何確定,可用暴力方式、二分查找、慢慢逼近

代碼演示#

底層文件操作#

  • 圖片
  • ⭐ 詳見註釋,關注使用
  • ❗ 避免亂碼情況
    • 字符串 buff 末尾留一位放 '\0'
      • sizeof(buff) - 1
    • 最後一次不足 512 字節的讀取,需要排除多餘字節的干擾
      • 方法①:手動 menset (buff, 0, sizeof (buff))
      • 方法②:始終保持數據末尾是 '\0',buff [nread] = '\0'
    • [PS] 學習系統上層命令時,不必太關注這些
  • 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、輸出錯誤日誌
  • 只有標準輸出是行緩衝的

思考點#

Tips#

  • 在 vim 中,Shift + K 可跳到 man 手冊
  • 推薦複製即翻譯軟件:CopyTranslator
  • man 手冊在線文檔:man page——die.net

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。