[apue] linux 文件系統那些事兒

前言

說到 linux 的文件系統,好多人第一印象是 ext2/ext3/ext4 等具體的文件系統,本文不涉及這些,因為研究具體的文件系統難免會陷入細節,甚至拉大段的源碼做分析,反而不能從宏觀的角度把握文件系統要解決的問題。一個通用的 linux 文件系統都包含哪些概念?介面如何使用?設計層面需要考慮什麼問題?這都在本文的討論範圍。當然了,內容都是從 apue 搬運過來的,經過了一點點梳理加工,原書還是基於比較老的 UFS (Unix File System) 進行說明的,有些東西可能已經過時了,不過原理層面的東西還是相通的,看過之後舉一反三就好。

文件系統總覽

開始詳細說明之前,先看下文件系統的總體結構,對一些基本的概念有個大體印象。書上有個不錯的圖直接盜過來:

從圖中可以看出,磁碟可以由多個分區組成,每個分區可以設置不同格式的文件系統。分區在 windows 上比較容易觀察,就是常說的 C/D/E/F……這些,一塊磁碟也可以只設置一個分區,不過一但系統重裝時,用戶數據就容易丟失,從這裡可以看出,分區及其上的文件系統是可以跨作業系統存在的。把系統分區從  windows 重裝成 linux,數據分區也能正常讀取 (linux 也能識別 NTFS),說明文件系統是獨立於作業系統的。

一個分區由多個柱面組成,柱面是多個碟片在同一個磁軌上形成的存儲面,這樣設計是為了減少尋道時間提高性能。每個柱面存儲了若干數據塊與對應的 inode 節點,它們都是固定長度的。inode 可以看作是文件的元數據,存放了與文件的大部分關鍵資訊,它們連續存放在一起形成 inode 表,這主要是為了提高讀取大量文件資訊的性能,另外也簡化了 inode 的定位過程,直接使用下標就可以了,一般稱之為 inode 編號。每個柱面還存放了 inode 點陣圖與塊點陣圖,方便查找空閑的 inode 節點或數據塊。

inode 存放的資訊包括:

  • 文件類型
  • 文件長度
  • 文件許可權位
  • 文件鏈接數
  • 文件時間
  • 文件數據塊編號
  • 設備號
  • ……

注意文件名是不存放在 inode 中的,文件名是變長的,最長的文件名 (255) 可能都要超過 inode 的固定長度了,不適合存儲在 inode 中。文件是包含在目錄中的,所以文件名與其對應的 inode 編號都存放在目錄的數據塊中,目錄是一種特殊的文件,其數據塊由系統維護,用戶不能直接讀寫它的內容。

 

從上圖可以看到,目錄 inode -> 目錄數據塊 -> 文件 inode -> 文件/子目錄數據塊 形成了一個閉環,通過這樣不斷迭代可以讀取到文件系統中的任意文件。

對於這個過程,可能有人會問了,inode 不是固定長度的嗎,如何保存一個文件的所有數據塊編號呢?這就涉及到數據塊定址了,當文件比較大的時候,光編號佔用的空間就直接超過 inode 本身的長度了,所以不能直接存儲在 inode 中,而要通過二級甚至三級定址來查找全部的數據塊,過程和記憶體的多級定址有異曲同工之處,受主題限制就不深入展開了,感興趣的讀者可以參考文末鏈接。

inode 與數據塊數量比例如何分配是另外一個問題,通常它們不是 1:1 的關係,這樣當 inode 消耗光的時候,即使還有數據塊,文件系統也不能創建新的文件了,這方面的案例可以參考這篇文章《[apue] Linux / Windows 系統上只能建立不超過 PATH_MAX / MAX_PATH 長度的路徑嗎? 》;但 inode 節點太多也會造成可觀的容量損失,一般沒有大量小文件的應用場景是不會將 inode 比例設置太多的。可以通過 df 來查看 inode 使用情況:

$ df -i /
Filesystem       Inodes  IUsed    IFree IUse% Mounted on
/dev/sda5      61022208 284790 60737418    1% /

這個比例可以在創建文件系統時指定,例如對於 mkfs.ext3 是通過  -i 參數指定:

-i bytes-per-inode
    Specify the bytes/inode ratio.  mke2fs creates an inode for  ev‐
    ery  bytes-per-inode bytes of space on the disk.  The larger the
    bytes-per-inode ratio, the fewer inodes will be  created.   This
    value  generally  shouldn't be smaller than the blocksize of the
    filesystem, since in that case more inodes would  be  made  than
    can  ever  be used.  Be warned that it is not possible to change
    this ratio on a filesystem after it is created,  so  be  careful
    deciding the correct value for this parameter.  Note that resiz‐
    ing a filesystem changes the number of inodes to  maintain  this
    ratio.

關於這方面更多細節請參考文末鏈接。

文件許可權

inode 存儲了文件的許可權設置,主要就是文件許可權位。關於文件許可權,這是另一個可以單獨寫一篇的話題了,請參考文章《[apue] linux 文件訪問許可權那些事兒》。這裡重點羅列一下與本文相關的結論:

  • 訪問一個文件,需要文件路徑上的每個節點都可以訪問,即所有目錄的 x 許可權位;對於文件需要有相應的 r/w/x 許可權位,具體需要哪些許可權位和操作有關
  • 新增文件,需要直屬目錄的 w 許可權位,新文件的許可權位由給定的許可權位和進程 umask 作用產生
  • 刪除文件,需要直屬目錄的 wx 許可權位,不需要具有文件的許可權位,如果直屬目錄指定了粘住位 (sticky),則還需要以下條件之一成立:
    • 擁有該文件
    • 擁有直屬目錄
    • 超級用戶
  • 遍歷文件,需要直屬目錄的 r 許可權位

訪問文件元數據 inode 的許可權與數據塊的大部分相同,一些不同點將在出現時特別指出。

文件鏈接

inode 中的文件鏈接數表示有多少目錄包含了該文件,刪除文件時,只是將鏈接數減 1,當鏈接數減為 0 時才真正的刪除文件並釋放數據塊,這種鏈接稱之為硬鏈接。文件系統支援的最大硬鏈接數可通過 pathconf(_PC_LINK_MAX, …) 查詢,可以參考這篇文章:[apue] 一個快速確定新系統上各類限制值的工具,在我的 Ubuntu 上這個值是 65000。文件鏈接到不同的目錄中時使用的文件名也可以不同,這也是第二個不將文件名放在 inode 中的原因。

由於 inode 是在每個文件系統(分區)單獨編號的,所以在進行文件鏈接時,只能指定本分區的文件,跨文件系統的硬鏈接是不被支援的 (inode 編號可能衝突)。為了消除這種限制,引入了一種新的鏈接方式——符號鏈接,也稱為軟鏈接,建立這種鏈接時不修改目標文件的鏈接數,而是新建一個獨立的文件,這個文件與普通文件有以下幾點不同:

  • 文件類型為 S_IFLINK,系統會對它做特殊處理
  • 數據塊存儲的是目標文件的路徑,可以是絕對路徑,也可以是相對路徑,使用後者時會基於進程的當前路徑進行查找
  • 鏈接文件本身的許可權位一般是被忽略的,許可權檢查時只看目標文件的許可權

之前說過目錄是一種特殊的文件,在鏈接方面也是如此,請看下面這個例子:

圖中有兩個 inode,1267 是父目錄,2549 是子目錄 testdir,每個目錄都有兩個默認項 ‘.’ 和 ‘..’,前者代表自己後者代表父目錄。一個目錄至少會被自己和 ‘.’ 項引用,這樣一來目錄的鏈接數至少是 2,如果有子目錄的話,子目錄的 ‘..’ 項又會增加自己的鏈接計數。所以從一個目錄項的鏈接數就可以知道有幾個子目錄:

$ ls -lh
total 200K
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 01.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 02.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 03.chapter
drwxrwxr-x 5 yunh yunh 4.0K Oct 30 18:02 04.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun 20  2021 05.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 06.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 07.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 08.chapter
drwxrwxr-x 3 yunh yunh 4.0K Jun  6  2021 09.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 10.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 11.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 12.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 13.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 14.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 15.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 16.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 17.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 18.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 19.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 20.chapter
drwxrwxr-x 2 yunh yunh 4.0K Jun  6  2021 21.chapter
-rw-rw-r-- 1 yunh yunh  32K Jun  6  2021 apue.c
-rw-rw-r-- 1 yunh yunh 3.5K Jun  6  2021 apue.h
-rw-rw-r-- 1 yunh yunh  35K Jun  6  2021 LICENSE
-rw-rw-r-- 1 yunh yunh  671 Jun  6  2021 log.c
-rw-rw-r-- 1 yunh yunh  143 Jun  6  2021 log.h
-rw-rw-r-- 1 yunh yunh 1.2K Jun  6  2021 Makefile
-rw-rw-r-- 1 yunh yunh 9.8K Jun  6  2021 pty_fun.c
-rw-rw-r-- 1 yunh yunh 1.6K Jun  6  2021 pty_fun.h
-rw-rw-r-- 1 yunh yunh  116 Jun  6  2021 README.md
drwxrwxr-x 3 yunh yunh 4.0K Jun  6  2021 root
-rw-rw-r-- 1 yunh yunh 3.2K Jun  6  2021 tty.c
-rw-rw-r-- 1 yunh yunh  174 Jun  6  2021 tty.h

ls 輸出的第二列就是鏈接數啦,從輸出中可以猜到 04.chapter 這個目錄鏈接數是 5,根據公式:

dirs = refs - 2

得知該目錄有 3 個子目錄,你學會了嗎?為了防止文件系統形成循環,大多數實現不允許創建目錄的硬鏈接,這也就讓上面的公式更能立得住腳了。

關於目錄硬鏈接導致文件系統形成循環的情況,動動腳趾頭也能想出來:一個葉子節點硬鏈接到自身路徑中任意一個節點都能成環,這是比較直觀的例子;還有 A 子樹葉子鏈接到 B 子樹,B 子樹葉子又鏈接到 A 子樹的八字環;如果參與的子樹超過 2 個,那就更難以探測和避免了。所以一般文件系統的實現對目錄硬鏈接會嚴防死守,書中說超級用戶可以創建目錄的硬鏈接,man ln 也是這樣講: 

-d, -F, --directory
    allow  the  superuser to attempt to hard link directories (note:
    will probably fail due to system restrictions, even for the  su‐
    peruser)

然而經過親自嘗試,這個後門已經被 Ubuntu 徹底堵死了,即使加了 -d 選項也不行:

yunh$ ln ../../ loop
ln: ../../: hard link not allowed for directory
yunh$ sudo ln ../../ loop
[sudo] password for yunh: 
ln: ../../: hard link not allowed for directory
yunh$ su
Password: 
root# ln ../../ loop
ln: ../../: hard link not allowed for directory
root# exit
exit

這一點和 man 括弧中的說明一致。

引入符號鏈接後,api 介面操作的是鏈接本身還是目標文件?這是一個問題,這個問題可以用一個詞來描述——跟隨,下表列出了常用的 api 是否跟隨符號鏈接:

api 跟隨符號鏈接 不跟隨符號鏈接
access *  
chdir *  
chmod *  
chown *  
lchown   *
creat *  
exec *  
link   *
stat *  
lstat   *
open *  
opendir *  
pathconf *  
readlink   *
remove   *
rename   *
truncate *  
unlink   *
symlink   *

可見大部分文件是跟隨符號鏈接的,這樣就為鏈接文件的透明性提供了基礎。下面分組做個說明:

  • 以 l 開頭明確表示要操作符號鏈接的 api 是不跟隨的,如 lstat/lchown
  • 符號鏈接專用的 api 也不跟隨,如 readlink/symlink 等
  • 一些 api 為了防止誤操作,也是不跟隨的,如 link/unlink/remove/rename 等
  • 一些 api 沒有列出來,是因為它們在遇到符號鏈接就直接出錯了,無所謂跟隨不跟隨的說法,這些有 mkdir/rmdir/mknod/mkinfo
  • 一些 api 是直接操作文件句柄的,也不存在跟隨問題,它們包括 fstat/fchown ……

比較有趣的是 symlink,它本身是用來創建符號鏈接的,它不跟隨目標路徑的符號鏈接,下面舉一個栗子:

$ ls -lh
total 4K
-rwxrwxr-x 1 yunh yunh  338 Jun  6  2021 rename.sh
$ ln -s rename.sh bar
$ ln -s bar foo
$ ls -lh
total 4K
lrwxrwxrwx 1 yunh yunh    9 Jan 23 21:04 bar -> rename.sh
lrwxrwxrwx 1 yunh yunh    3 Jan 23 21:05 foo -> bar
-rwxrwxr-x 1 yunh yunh  338 Jun  6  2021 rename.sh

這個例子構造了一個 foo->bar->rename.sh 的鏈接路徑,如果 link 是跟隨符號鏈接的,那麼 foo 將直接指向 rename.sh,變為 bar->rename.sh 和 foo->rename.sh,而不需要經過 bar 傳遞一手。此時 cat foo,將能正常列印目標文件 rename.sh 的內容,可見鏈接的跟隨也是遞歸的一個過程。

當符號鏈接懸空時,ls 可以看到文件,cat 卻報告文件不存在,這可能會對用戶造成一些困惑,為此可以使用 ls -l 來列印文件詳情,除了第一個字元 ‘l’ 標識了文件是符號鏈接外,文件名也通過 -> 指示了符號鏈接的目標文件,像上面展示的那樣,比較直觀。除此以外,還可以使用 ls 的 -F 參數來查看,符號鏈接將以 @ 結尾,以區別於普通文件:

$ ls -F
rename.sh  bar@      foo@

文件操作

文件操作如何影響文件系統中的各個元素,下面分類說明。

文件創建

這裡按創建的文件類型先列一下使用的介面及必需的許可權:

文件類型 介面 許可權 說明
普通文件 creat/open (pathname, oflag = O_CREAT, mode)
  • 路徑上每個節點:x
  • 直屬目錄:w
  • 只創建 pathname 的最後一個分量,路徑中其它部分應當已經存在,否則出錯返回 ENOENT
  • pathname 已存在,且 oflag 同時指定 O_EXCL,出錯返回 EEXIST
  • pathname 為符號鏈接時,跟隨符號鏈接,特別當 pathname 是懸空的符號鏈接時,會創建符號鏈接指向的文件 [注1]
  • 分配 inode 和數據塊,並在直屬目錄中添加一條目錄項指向新文件的 inode
  • 新文件的許可權由 mode & ~umask 決定
硬鏈接 link (existingpath, newpath)
  • 路徑上每個節點:x
  • existingpath:r
  • newpath 直屬目錄:w
  • 只創建 newpath 最後一個分量,路徑中其它部分應當已經存在,否則出錯返回 ENOENT
  • newpath 已存在,出錯返回 EEXIST
  • existingpath 不存在,出錯返回 ENOENT
  • existingpath 為目錄,出錯返回 EPERM
  • existingpath 與 newpath 跨分區,出錯返回 EXDEV
  • 在 newpath 的直屬目錄中添加一條目錄項指向 existingpath 的文件資訊 (inode 編號和文件名),existingpath 文件 inode 的鏈接計數增 1 [注2]
軟鏈接 symlink (actualpath, sympath)
  • 路徑上每個節點:x
  • sympath 直屬目錄:w
  • 不要求 actualpath 已存在
  • 不要求 actualpath 與 sympath 位於同一個分區
  • sympath 已存在,出錯返回 EEXIST
  • 為新文件分配 inode 和數據塊,在 sympath 直屬目錄中添加一條目錄項指向新文件的文件資訊 (inode 編號和文件名)
目錄 mkdir (pathname, mode)
  • 路徑上每個節點:x
  • 直屬目錄:w
  • 路徑名已存在,出錯返回 EEXIST [注3]
  • 自動創建新目錄的 . 和 .. 目錄項,並將它們分別指向自己和父目錄,增加父目錄的鏈接計數
  • 為新目錄分配 inode 和數據塊,在 pathname 直屬目錄中添加一條目錄項指向新目錄的文件資訊 (inode 編號和文件名)
  • 新目錄的許可權由 mode & ~umask 決定,注意不要關閉目錄的 x 許可權位,否則將不能經過該目錄訪問目錄中的文件
  • 新目錄的 uid 和 gid 的設置有一系列複雜的規則,詳情可參考文件許可權那篇文章的內容

注1:舉個栗子,符號鏈接 foo 指向不存在的文件 bar,則指定 pathname 為 foo 時,將新建文件 bar,使 foo 不再懸空

注2:創建鏈接文件與增加鏈接計數必需是原子的,當跨文件系統(分區)時,這一操作的原子性很難得到保證,這是硬鏈接不能跨文件系統的第二個原因

注3:路徑名為懸空軟鏈接時 mkdir 也會失敗,而不是像 open/creat 一樣創建鏈接文件指向的文件,關於這一點可以參考上一節中 mkdir 對符號鏈接的跟隨說明

文件創建後使用 open 打開讀取內容,對於軟鏈接和目錄而言,有專門的介面,這主要是為了隱藏實現細節:

文件類型 介面 許可權 說明
普通文件 open (pathname, oflag, …) rwx:與 oflag 相關
  • 使用 O_CREAT | O_EXCL 打開懸空的軟鏈接時,出錯返回 EEXIST [注1]
軟鏈接 readlink (pathname, buf, bufsize) r
  • 一個函數包含了 open/read/close 三個操作,但用戶不能通過這三個函數來模擬,這主要是由於 open 總是跟隨符號鏈接,且沒有 lopen 這種東東
  • 注意 buf 並不以 nul 結尾,需要手工添加 (根據 readlink 返回的長度)
目錄 DIR* opendir (pathname) r
  • 早期的系統支援直接讀取目錄文件數據,當時目錄項是固定長度的,隨著文件系統支援的文件名越來越長,目錄項因包含文件名也變為不定長了,新系統為了隱藏實現細節,已不支援直接打開目錄文件 [注2]
dirent* readdir (DIR*)
closedir(DIR*)

注1:單使用 O_CREAT 打開懸空的軟鏈接會創建軟鏈接指向的文件(使之不再懸空,參考上一小節),但同時指定 O_CREAT 和 O_EXCL 則會失敗。這主要是為了堵塞一個安全漏洞:防止具有特權的進程被誘騙對不適當的文件進行寫操作,關於這一點 man open 中也有特別說明:

       O_EXCL Ensure that this call creates the file: if this flag is specified in conjunction  with  O_CREAT,  and
              pathname already exists, then open() fails with the error EEXIST.

              When  these two flags are specified, symbolic links are not followed: if pathname is a symbolic link,
              then open() fails regardless of where the symbolic link points.

注2:有的人可能會用 struct dirent 的定義來反駁:

struct dirent
  {
#ifndef __USE_FILE_OFFSET64
    __ino_t d_ino;
    __off_t d_off;
#else
    __ino64_t d_ino;
    __off64_t d_off;
#endif
    unsigned short int d_reclen;
    unsigned char d_type;
    char d_name[256];		/* We must not include limits.h! */
  };

d_name 欄位不是 256 個字元固定長度么?還真是。文件名最大長度由文件系統決定 (pathconf),這個長度一般不超過 256,不過一些系統文件名長度是會隨文件系統而改變,所以這裡不過是一種巧合,這裡的 d_name[256] 只是 char* 的另一種表示法,實際長度可以超過或不足 256,真正佔用空間要看結尾 0 的位置。換種說法就是,這裡定義成 char d_name[1] 也是可以的 (書中原意如此,沒有做過驗證)。

文件刪除

刪除場景主要分普通文件與目錄兩個類型:

文件類型 介面 許可權 說明
普通文件 unlink (pathname)
  • 路徑上每個節點:x
  • 直屬目錄:w
  • 直屬目錄設置了粘住位時,需要額外以下三個條件之一:
    • 擁有該文件
    • 擁有直屬目錄
    • 超級用戶
  • pathname 為目錄時,unlink 出錯返回 EISDIR [注 2]
  • pathname 為符號鏈接時,只處理符號鏈接自身,不跟隨符號鏈接 [注3]
  • 直屬目錄的數據塊中移除 pathname 的目錄項
  • 將文件的鏈接數減 1,鏈接計數達到 0 時
    • 文件打開的進程數為 0,刪除文件,釋放數據塊與 inode
    • 打開的進程數大於 0,延遲刪除 [注4]
remove (pathname) [注1]
目錄 rmdir (pathname)
  • pathname 不是目錄,rmdir 出錯返回 ENOTDIR
  • 目錄不為空 [注5],出錯返回 ENOTEMPTY
  • 直屬目錄的數據塊中移除 pathname 的目錄項
  • 將目錄的鏈接數減 1,鏈接計數達到 1 時
    • 刪除目錄下的 . 和 .. 目錄項,此時鏈接計數達到 0
    • 目錄打開的進程數為 0 時,刪除目錄,釋放數據塊與 inode
    • 目錄打開的進程數大於 0 時,延遲釋放目錄空間,此時在該目錄下無法再創建新文件,嘗試創建將出錯返回 ENOENT [注6]
remove (pathname)

注1:remove 針對普通文件等價於 unlink;針對目錄等價於 rmdir

注2:書上說超級會員針對目錄也可以使用 unlink,等價於 rmdir,實測不通過

注3:沒有直接刪除符號鏈接指向文件的 api,可以結合 readlink 與 unlink 自己寫個 (注意需要處理遞歸的場景)

注4:延遲刪除指的是目錄項會從目錄的數據塊中移除,但是文件數據和 inode 仍可以被打開的進程訪問,這樣做主要是為了防止進程後續訪問無效的句柄導致未定義行為甚至崩潰。文件會在進程關閉文件句柄時徹底刪除,進程退出時系統會自動關閉所有打開的文件句柄。unlink 的這種延遲刪除能力常用於臨時文件的清理,避免進程崩潰時遺留下不必要的中間文件,具體做法就是 open 或 creat 文件成功後,立即 unlink 該文件。

注5:目錄為空是指目錄中只包含 . 與 .. 兩個目錄項

注6:空目錄刪除時如果還有進程打開該目錄,同普通文件一樣需要延遲刪除,此時禁止新文件的創建主要是為了保證在目錄關閉時可以正常釋放空間 (仍保持空目錄)

最後單獨列一下進程關閉時清理文件的過程:

  • 進程退出前系統會自動關閉進程打開的所有文件句柄
  • 關閉普通文件時,如果鏈接數為 0,且無其它進程打開該文件,刪除文件,釋放數據塊與 inode
  • 關閉目錄文件時,如果鏈接數為 0,且無其它進程打開該目錄,刪除目錄,釋放數據塊與 inode

 

文件移動

分區內的文件移動不需要移動文件數據,只修改相關文件直屬目錄的數據塊即可,這裡也主要分普通文件與目錄兩個類型說明:

文件類型 介面 許可權 說明
文件 rename (oldname, newname)
  • 路徑上每個節點:x
  • oldname 直屬目錄:w
  • oldname 直屬目錄設置了粘住位時,需要額外以下三個條件之一:
    • 擁有該文件
    • 擁有直屬目錄
    • 超級用戶
  • newname 直屬目錄:w
  • newname 已存在需要刪除時,需要 x 許可權位,如果 newname 直屬目錄設置了粘住位時,還需要額外以下三個條件之一:
    • 擁有該文件
    • 擁有直屬目錄
    • 超級用戶
  • newname 與 oldname 為同一個文件,什麼也不做,返回成功
  • oldname 與 newname 跨分區時,出錯返回 EXDEV
  • newname 已存在時
    • newname 為目錄,出錯返回 EISDIR
    • 刪除 newname 文件
  • oldname 與 newname 指向符號鏈接時,只處理符號鏈接本身,不跟隨符號鏈接 [注1]
  • 修改 newname 直屬目錄數據塊,添加 newname 的文件資訊 (inode 編號和文件名)
  • 修改 oldname 直屬目錄數據塊,刪除 oldname 的文件資訊,這個過程文件的數據塊不需要變動,inode 僅部分欄位變動,例如文件時間
目錄
  • newname 與 oldname 為同一個目錄,什麼也不做,返回成功
  • oldname 與 newname 跨分區時,出錯返回 EXDEV
  • newname 已存在時
    • newname 為非目錄文件,出錯返回 ENOTDIR
    • newname 為非空目錄,出錯返回 ENOTEMPTY
    • newname 包含 oldname 作為前綴,出錯返回 EINVAL [注2]
    • 刪除 newname 目錄
  • 修改 newname 直屬目錄數據塊,添加 newname 的文件資訊 (inode 編號和文件名)
  • 修改 oldname 直屬目錄數據塊,刪除 oldname 的文件資訊,這個過程目錄的數據塊僅 .. 目錄項的指向需要變動,inode 僅部分欄位變動,例如文件時間

注1:因為這一特性,符號鏈接和符號指向的文件對 rename 來說不是一個文件,假設符號 foo 指向文件 bar,那麼 rename foo bar 並不會被視為對同一個文件進行操作,結果將是 bar 文件被刪除,foo 文件指向了它自己,這是一個懸空符號鏈接,結果和 ln -s foo foo 差不多

注2:舉個例子,rename /usr/foo /usr/foo/bar 中的 newname (/usr/foo/bar) 包含了 oldname (/usr/foo) 作為前綴,當刪除 oldname 時會將 newname 賴以存在的一部分刪除,導致後面新建時出錯,對於這種明顯有問題的邏輯系統會提前出錯

跨文件系統(分區)的移動通常需要移動數據塊和重新分配 inode,mv 命令實現它的時候可以理解為 cp + rm 的組合。

文件修改

文件內容被修改時,直屬目錄不受影響,相對要簡單一些:

  • 更新文件數據,此時文件數據和 inode (文件長度、文件時間…) 都會更新
  • 只更新 inode,例如修改許可權位、鏈接數,此時只更新 inode

文件訪問時也分兩種情況:

  • 訪問文件數據,此時會更新 inode 中的訪問時間
  • 只訪問 inode,此時文件不受影響

關於文件時間的內容請參考下一節。

api vs command

上面羅列的都是系統提供的 api,有些和系統命令同名,如 mkdir、rmdir,有些不太一樣,如 unlink/remove vs rm、link/symlink vs ln、rename vs mv。

需要注意的是命令和 api 並不是一一對應的關係,有些命令在內部實現過程中並不是直接調用 api 的,所以會造成命令執行的結果與 api 有出入,這裡我都是自己寫程式直接調用 api 來驗證的,關於命令和 api 的異同,以後有空再補充這方面的內容。

文件時間

從上面的討論已經知道文件是由兩部分組成的,一部分存放真實的文件數據 (data),另一部分存放文件元數據 (inode),那麼對這兩部分的讀寫操作應該分別記錄時間,可以整理下面的表格:

operation data inode
read access time (atime) n/a
write modify time (mtime) change time (ctime)

從表中可以看出沒有”最近一次讀 inode 的時間” 這種記錄,所以一些操作 (如 access/stat…) 並不修改文件的任何時間。

mtime 級聯更新 ctime

所有文件時間都存放於 inode 中,那 mtime/atime 本身被修改會不會導致 ctime 更新呢?理論上是不會,例如 cat 文件後,只有 atime 會更新,ctime 並不隨 atime 更新而更新;但 write 文件後,除了 mtime 更新,ctime 也會更新。有的人會說追加文件數據後,文件長度變更了,需要更新 inode,所以 ctime 也會變更。為了驗證這一點,專門寫了一個程式用於驗證:

#include "../apue.h"

int main (int argc, char *argv[])
{
    if (argc < 5)
        err_quit ("Usage: write_api path offset length char\n", 1); 

    char *buf = NULL; 
    int fd = open (argv[1], O_WRONLY);
    if (fd < 0)
        err_sys ("open file %s failed", argv[1]); 
    
    do
    {
        int off = atoi(argv[2]); 
        int len = atoi(argv[3]); 
        char ch = argv[4][0]; 
        buf = (char *)malloc(len); 
        memset (buf, ch, len); 
        if (buf == NULL) {
            printf ("alloc buffer with len %d failed\n", len); 
            break; 
        }

        if (lseek (fd, off, SEEK_SET) != off) {
            printf ("seek to %d failed\n", off); 
            break; 
        }

        if (write (fd, buf, len) != len) {
            printf("write %d at %d failed\n", len, off); 
            break; 
        }

        printf ("write %d '%c' ok\n", len, ch); 
    } while (0);

    free (buf); 
    close (fd); 
    return 0; 
}

這個程式 (write_api) 直接調用 write 寫入文件中的一個位元組,文件長度前後不會變化,像下面這樣:

$ echo "def" > abc
$ stat abc
  File: abc
  Size: 4         	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35521031    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-05 17:23:25.223022200 +0800
Modify: 2022-03-05 17:23:25.223022200 +0800
Change: 2022-03-05 17:23:25.223022200 +0800
 Birth: -
$ ./write_api abc 1 1 o
write 1 'o' ok
$ stat abc
  File: abc
  Size: 4         	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35521031    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-05 17:23:25.223022200 +0800
Modify: 2022-03-05 17:23:48.270241647 +0800
Change: 2022-03-05 17:23:48.270241647 +0800
 Birth: -

然而 ctime 仍然更新了。從上面的輸出可以看到,inode 中的其它欄位除 mtime 外都沒有變化,所以可以認為 ctime 是隨 mtime ‘級聯’修改的。有的人可能有疑問,如果 ctime 總要隨 mtime 更新,那單獨記錄 ctime 的意義何在?其實有些場景只修改 inode 而不修改 data,此時就只更新 ctime,不更新 mtime。這種場景有很多,例如只修改文件許可權 (chmod)、只增加文件鏈接計數 (ln)、只更新文件所有者 uid 或文件所在組 gid (chown/chgrp)。

api & file times

下面的表列出了更全面的 api 對文件時間影響的清單:

api 引用的文件 文件的直屬目錄 備註
atime mtime ctime atime mtime ctime  
access              
stat/fstat/lstat              
chmod/fchmod     *        
chown/fchown     *        
lchown     *        
creat * * *   * * O_CREAT 新文件
creat   * *       O_TRUNC 現有文件
open * * *   * * O_CREAT 新文件
open   * *       O_TRUNC 現有文件
link     *   * * 新文件的直屬目錄
symlink * * *   * * 新文件的直屬目錄
unlink     *   * *  
mkdir * * *   * *  
rmdir         * * 目錄一般無硬鏈接,刪除後 inode 也將銷毀,可視作無變更
remove     *   * * 刪除文件 = unlink
remove         * * 刪除目錄 = rmdir
mkfifo * * *   * *  
pipe * * *       一般無直屬目錄
truncate/ftruncate   * *        
exec * [注1]            
rename     * [注2]   * * 對於源和目的文件的直屬目錄都是如此
read *            
readlink *            
write   * *        
utime * * *        
readdir * [注3]            

除了直接影響引用文件的三個時間,當文件在直屬目錄中增刪時,還會修改父目錄的數據塊,從而影響它的兩個時間,上面分兩列給出。

注1:exec 函數族用於啟動可執行文件,這個過程會有讀取文件數據載入記憶體的過程,因此理應影響文件的 atime,不過對於系統而言啟動進程是再正常不過的事情,如果因此頻繁更新 inode 中的 atime 則有些得不償失,為此 linux 內核 2.6.30 之後做了優化,當滿足下麵條件之一時,atime 不更新:

  • mount 文件系統時指定了 noatime/nodiratime 選項;
  • mount 文件系統時指定了 relatime 選項且滿足下面的條件之一:
    • atime < mtime
    • atime < ctime
    • atime 據上次更新達一天

仍可通過指定 strictatime 來恢復每次訪問更新 atime 的行為,具體可參考 man mount 的這段說明:

relatime
    Update  inode  access  times  relative to modify or change time.
    Access time is only updated if the previous access time was ear‐
    lier  than  the  current  modify  or  change  time.  (Similar to
    noatime, but it doesn't break mutt or  other  applications  that
    need  to know if a file has been read since the last time it was
    modified.)

    Since Linux 2.6.30, the kernel defaults to the behavior provided
    by   this   option  (unless  noatime  was  specified),  and  the
    strictatime option is required to obtain traditional  semantics.
    In  addition, since Linux 2.6.30, the file's last access time is
    always updated if it is more than 1 day old.

注2:rename 按理說只調整直屬目錄數據塊中的目錄項的 inode 指向即可,重命名文件的 data 和 inode 本身並不發生改變,但是書上說這裡 ctime 會變,特意驗證了下:

$ echo "demo" > foo
$ stat foo
  File: foo
  Size: 5         	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35520865    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-05 18:28:57.208136638 +0800
Modify: 2022-03-05 18:28:57.208136638 +0800
Change: 2022-03-05 18:28:57.208136638 +0800
 Birth: -
$ ./rename_api foo bar
rename foo to bar
$ stat bar
  File: bar
  Size: 5         	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35520865    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-05 18:28:57.208136638 +0800
Modify: 2022-03-05 18:28:57.208136638 +0800
Change: 2022-03-05 18:29:05.621027413 +0800
 Birth: -

果然 ctime 變了。這裡為了排除 mv 命令中調用其它 api 的干擾,專門寫了一個程式 rename_api,內部只調用 rename。我的理解是 rename 本身可以做到不改變引用文件的任何內容,但是這是一個比較大的變動,需要”體現”出來,而修改 ctime 是一個不錯的方式。

注3:目錄的 atime 變更和文件類似,參考注1

調整文件時間

除了被動修改,文件時間也可以主動設置,這對於一些解壓工具 (tar/cpio…) 非常有用,可以恢復文件壓縮前的時間狀態,這是通過上面表中列過的 utime 介面來實現的。目前系統只開放了兩個時間項供修改:atime & mtime,ctime 是不能主動設置的,而且每次調用 utime 都會導致 ctime 自動更新。

#include <sys/types.h>
#include <utime.h>

struct utimbuf {
    time_t actime;       /* access time */
    time_t modtime;      /* modification time */
};

int utime(const char *filename, const struct utimbuf *times);

utime 有一些特殊的許可權要求,這裡分情況討論一下:

  • utimbuf 為 NULL,atime & mtime 更新為當前時間,ctime 自動更新,需要滿足以下條件之一:
    • 進程 euid == 文件 uid
    • 進程具有文件寫許可權
  • utimbuf 不為 NULL,atime & mtime 被更新為結構體中的 actime & modtime 欄位,ctime 自動更新,需要以下條件之一:
    • 進程 euid == 文件 uid
    • 進程具有超級用戶許可權

可見更新文件時間為特定時間需要的許可權更高一些。具體可以參考 man utime 中的說明:

    Changing timestamps is permitted when: either the process has appropri‐
    ate privileges, or the effective user ID equals  the  user  ID  of  the
    file,  or  times  is  NULL and the process has write permission for the
    file.

至於 utime 總是更新文件 ctime 的設計,同 rename 更新 ctime 一樣,需要一個地方”體現”被設置了時間的文件。

命令中的文件時間

說了這麼多,命令中是如何指定文件時間的呢?下面分別來看一下。

ls

除了直接使用 stat 查看文件三個時間外,還可以使用 ls -l,它默認顯示的是文件的 mtime,-t  選項將輸出按時間排序,-r 倒序輸出:

$ ls -lhrt
total 840K
-rw-rw-r-- 1 yunh yunh  280 Feb 20 09:34 mkdir_api.c
-rw-rw-r-- 1 yunh yunh  314 Feb 20 14:02 link_api.c
-rw-rw-r-- 1 yunh yunh  324 Feb 20 14:22 symlink_api.c
-rw-rw-r-- 1 yunh yunh  367 Feb 20 14:33 readlink_api.c
-rw-rw-r-- 1 yunh yunh  272 Feb 20 14:48 unlink_api.c
-rw-rw-r-- 1 yunh yunh  445 Feb 20 14:52 open_api.c
-rw-rw-r-- 1 yunh yunh  272 Feb 20 15:21 remove_api.c
-rw-rw-r-- 1 yunh yunh  263 Feb 20 15:51 rmdir_api.c
-rw-rw-r-- 1 yunh yunh  317 Feb 20 16:37 rename_api.c
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 mkdir_api.o
-rw-rw-r-- 1 yunh yunh  67K Feb 20 19:37 apue.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 mkdir_api
-rw-rw-r-- 1 yunh yunh 7.0K Feb 20 19:37 open_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 open_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 link_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 link_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 symlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 symlink_api
-rw-rw-r-- 1 yunh yunh 6.9K Feb 20 19:37 readlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 readlink_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 unlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 unlink_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 remove_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 remove_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 rmdir_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 rmdir_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 rename_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 rename_api
-rw-rw-r-- 1 yunh yunh 1.6K Mar  5 16:30 Makefile
-rw-rw-r-- 1 yunh yunh 7.7K Mar  5 16:43 write_api.o
-rwxrwxr-x 1 yunh yunh  63K Mar  5 16:43 write_api
-rw-rw-r-- 1 yunh yunh  963 Mar  5 18:22 write_api.c
-rw-rw-r-- 1 yunh yunh    5 Mar  5 18:27 rename.sh
-rw-rw-r-- 1 yunh yunh    4 Mar  5 18:39 foo

同理還可以顯示 atime (-u) 和 ctime (-c),排序也是基於當前顯示的文件時間來的。

find

find 命令中直接通過 -atime/-ctime/-mtime 來指定要查找的文件時間,它們都只接收一個整數作為參數,表示 (-N-1, -N] 天時間區間內的 access/modify/change 的文件,例如當 N  為 0 時表示一天內的文件,當 N  為 1 時表示一天前兩天內的文件:

$ date
Sat 05 Mar 2022 07:34:58 PM CST
$ find . -type f -mtime 0 | xargs ls -lhd
-rw-rw-r-- 1 yunh yunh    4 Mar  5 18:39 ./foo
-rw-rw-r-- 1 yunh yunh 1.6K Mar  5 16:30 ./Makefile
-rw-rw-r-- 1 yunh yunh    5 Mar  5 18:27 ./rename.sh
-rwxrwxr-x 1 yunh yunh  63K Mar  5 16:43 ./write_api
-rw-rw-r-- 1 yunh yunh  963 Mar  5 18:22 ./write_api.c
-rw-rw-r-- 1 yunh yunh 7.7K Mar  5 16:43 ./write_api.o
$ find . -type f -mtime 12 | xargs ls -lh
-rw-rw-r-- 1 yunh yunh  67K Feb 20 19:37 ./apue.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./link_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./link_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./mkdir_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./mkdir_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./open_api
-rw-rw-r-- 1 yunh yunh 7.0K Feb 20 19:37 ./open_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./readlink_api
-rw-rw-r-- 1 yunh yunh 6.9K Feb 20 19:37 ./readlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./remove_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./remove_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./rename_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./rename_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./rmdir_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./rmdir_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./symlink_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./symlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./unlink_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./unlink_api.o
$ find . -type f -mtime 13 | xargs ls -lh
-rw-rw-r-- 1 yunh yunh 314 Feb 20 14:02 ./link_api.c
-rw-rw-r-- 1 yunh yunh 280 Feb 20 09:34 ./mkdir_api.c
-rw-rw-r-- 1 yunh yunh 445 Feb 20 14:52 ./open_api.c
-rw-rw-r-- 1 yunh yunh 367 Feb 20 14:33 ./readlink_api.c
-rw-rw-r-- 1 yunh yunh 272 Feb 20 15:21 ./remove_api.c
-rw-rw-r-- 1 yunh yunh 317 Feb 20 16:37 ./rename_api.c
-rw-rw-r-- 1 yunh yunh 263 Feb 20 15:51 ./rmdir_api.c
-rw-rw-r-- 1 yunh yunh 324 Feb 20 14:22 ./symlink_api.c
-rw-rw-r-- 1 yunh yunh 272 Feb 20 14:48 ./unlink_api.c

以當前時間 2022/03/05 19:35 為例,文件時間 02/20 19:37 位於 [02/20 19:35, 02/21 19:35) 之間 (13 天之內,12 天之前),而文件時間 02/20 14~16 點位於 [02/19 19:35, 02/20 19:35) 之間 (14 天之內,13 天之前),當前時間 (19:35) 的選取恰好將這批文件切分成了兩批。

N 只能指定一天的時間區段,那如何指定某個時間之前或之後的半開區間呢?這就用到 ‘+’ 和 ‘-‘ 來修飾了,-N 表示 (-N, 0] 天區間內的文件,例如當 N=3 時表示 3 天內的文件;+N 表示 (-∞, -N-1] 天區間內的文件,例如當 N=3 時表示 4 天前的文件。以上面的文件為例,假設當前時間不變,進行驗證:

$ find . -type f -mtime +12 | xargs ls -lh
-rw-rw-r-- 1 yunh yunh 314 Feb 20 14:02 ./link_api.c
-rw-rw-r-- 1 yunh yunh 280 Feb 20 09:34 ./mkdir_api.c
-rw-rw-r-- 1 yunh yunh 445 Feb 20 14:52 ./open_api.c
-rw-rw-r-- 1 yunh yunh 367 Feb 20 14:33 ./readlink_api.c
-rw-rw-r-- 1 yunh yunh 272 Feb 20 15:21 ./remove_api.c
-rw-rw-r-- 1 yunh yunh 317 Feb 20 16:37 ./rename_api.c
-rw-rw-r-- 1 yunh yunh 263 Feb 20 15:51 ./rmdir_api.c
-rw-rw-r-- 1 yunh yunh 324 Feb 20 14:22 ./symlink_api.c
-rw-rw-r-- 1 yunh yunh 272 Feb 20 14:48 ./unlink_api.c
$ find . -type f -mtime -13 | xargs ls -lh
-rw-rw-r-- 1 yunh yunh  67K Feb 20 19:37 ./apue.o
-rw-rw-r-- 1 yunh yunh    4 Mar  5 18:39 ./foo
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./link_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./link_api.o
-rw-rw-r-- 1 yunh yunh 1.6K Mar  5 16:30 ./Makefile
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./mkdir_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./mkdir_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./open_api
-rw-rw-r-- 1 yunh yunh 7.0K Feb 20 19:37 ./open_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./readlink_api
-rw-rw-r-- 1 yunh yunh 6.9K Feb 20 19:37 ./readlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./remove_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./remove_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./rename_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./rename_api.o
-rw-rw-r-- 1 yunh yunh    5 Mar  5 18:27 ./rename.sh
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./rmdir_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./rmdir_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./symlink_api
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./symlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./unlink_api
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./unlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Mar  5 16:43 ./write_api
-rw-rw-r-- 1 yunh yunh  963 Mar  5 18:22 ./write_api.c
-rw-rw-r-- 1 yunh yunh 7.7K Mar  5 16:43 ./write_api.o
$ find . -type f -mtime -12 | xargs ls -lh
-rw-rw-r-- 1 yunh yunh    4 Mar  5 18:39 ./foo
-rw-rw-r-- 1 yunh yunh 1.6K Mar  5 16:30 ./Makefile
-rw-rw-r-- 1 yunh yunh    5 Mar  5 18:27 ./rename.sh
-rwxrwxr-x 1 yunh yunh  63K Mar  5 16:43 ./write_api
-rw-rw-r-- 1 yunh yunh  963 Mar  5 18:22 ./write_api.c
-rw-rw-r-- 1 yunh yunh 7.7K Mar  5 16:43 ./write_api.o
$ find . -type f -mtime +0 | xargs ls -lh
-rw-rw-r-- 1 yunh yunh  67K Feb 20 19:37 ./apue.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./link_api
-rw-rw-r-- 1 yunh yunh  314 Feb 20 14:02 ./link_api.c
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./link_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./mkdir_api
-rw-rw-r-- 1 yunh yunh  280 Feb 20 09:34 ./mkdir_api.c
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./mkdir_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./open_api
-rw-rw-r-- 1 yunh yunh  445 Feb 20 14:52 ./open_api.c
-rw-rw-r-- 1 yunh yunh 7.0K Feb 20 19:37 ./open_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./readlink_api
-rw-rw-r-- 1 yunh yunh  367 Feb 20 14:33 ./readlink_api.c
-rw-rw-r-- 1 yunh yunh 6.9K Feb 20 19:37 ./readlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./remove_api
-rw-rw-r-- 1 yunh yunh  272 Feb 20 15:21 ./remove_api.c
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./remove_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./rename_api
-rw-rw-r-- 1 yunh yunh  317 Feb 20 16:37 ./rename_api.c
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./rename_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./rmdir_api
-rw-rw-r-- 1 yunh yunh  263 Feb 20 15:51 ./rmdir_api.c
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./rmdir_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./symlink_api
-rw-rw-r-- 1 yunh yunh  324 Feb 20 14:22 ./symlink_api.c
-rw-rw-r-- 1 yunh yunh 6.7K Feb 20 19:37 ./symlink_api.o
-rwxrwxr-x 1 yunh yunh  63K Feb 20 19:37 ./unlink_api
-rw-rw-r-- 1 yunh yunh  272 Feb 20 14:48 ./unlink_api.c
-rw-rw-r-- 1 yunh yunh 6.6K Feb 20 19:37 ./unlink_api.o

以 19:35 為分隔點,-13 和 +12 將它們分成前後兩部分,所以想從某個時刻前轉為這之後,不是簡單的 -N 變 +N,而是 -N 變 +(N-1),因為 N 那一天由 -mtime N 代表了。最後 +0 表示一天前的文件也是符合預期的,一個特例是 find -mtime 0 等價於 find -mtime -1。除了指定單位天,還可以指定單位分鐘,這由 -amin/-mmin/-cmin 指定,範圍規則同上,下面綜合一下:

參數 範圍 單位 時間項
N (-N-1, -N]:N 天/分鐘前,N+1 天/分鐘內
    • -atime
    • -mtime
    • -ctime
  • 分鐘
    • -amin
    • -mmin
    • -cmin
  • access
    • -atime
    • -amin
  • modify
    • -mtime
    • -mmin
  • change
    • -ctime
    • -cmin
-N (-N, 0]:N 天/分鐘內
+N (-∞, -N-1]:N+1 天/分鐘前

如果不想基於當前時間、而是基於每天的開始時間 (00:00),那麼可以為 find 指定 -daystart 選項。

touch

與 utime 對應的命令是 touch,之前一直用這個命令創建空白文件,沒想到它還有更新文件 atime 和 mtime 的能力,下面的表格列出了它的一些常用選項:

選項 含義
-a 只更新 atime 為當前時間 (ctime 同步更新為當前時間)
-m 只更新 mtime 為當前時間 (ctime 同步更新為當前時間)
-d DATE 同時更新 atime 和 mtime為指定時間 (ctime 同步更新為當前時間),DATE 遵循的格式非常廣泛,一般可以指定 YYYY-MM-DD HH-mm-SS [注1],如果指定了 -a/-m,則只設置其中一個
-t TIME 同時更新 atime 和 mtime 為指定時間 (ctime 同步更新為當前時間),TIME 遵循格式:[[CC]YY]MMDDhhmm[.ss] [注2],如果指定了 -a/-m,則只設置其中一個
-r FILE 同時設置 atime 和 mtime 為指定文件的 atime 和  mtime (ctime 同步更新為當前時間) [注3/4],如果指定了 -a/-m,則只設置其中一個
-c 文件不存在時不自動創建文件

注1:還可以指定 3 day ago, next Thursday 這種寬鬆的相對時間,具體可參考 man touch 說明:

注2:此處時間格式與 -d 選項不同,如果指定絕對時間,-t 選項的格式相對簡單易讀一些

注3:ctime 不在可修改時間之列,這裡的自動更新機制和 utime api 保持一致,其實 touch 底層就是調用的 utimensat

注4:超級用戶許可權進程默認只更新 mtime,-d/-t 也是如此,Ubuntu 實測結果

touch 一個比較有用的點是觸發 make 命令,make 命令是否執行更新主要就是信賴於源文件的 mtime,因此 touch 更新文件的 mtime 必然會引發 make 的關注。這主要分兩個方面,一是想單獨觸發某個目標的編譯,二是避免某個意外的更新導致的潛在編譯動作。

  1. 如果想單獨觸發某個目標的編譯,有的人可能覺得通過 Make -B foo 也可以實現,不過這樣是將 foo 所依賴的所有文件標記為臟進行重新生成,波及的面還是有點廣,如果只想對幾個源文件進行標記,就可以使用 touch  A B C… 的方式一次性標記,要比一個個打開它們再保存來的快一些
  2. 如果說上面那條還可以通過手動保存文件來實現,那麼想取消一次 mtime 更新引發的潛在編譯,則非 touch 莫屬。可以通過 touch -t/d 選項來直接設置意外更新的源文件時間早於依賴文件,也可以直接指定 -r 來將兩者設置為相同的修改時間:touch -r dest src

對於第二點,突發奇想:如果在 Makefile 規則中不小心更新了源文件的 mtime,那麼可能導致目標永遠是可被 make 的,像下面這個簡單的例子所示:

all: foo

foo: bar
	touch foo

bar: bar.c
	touch bar 
	sleep 0.01  # make bar.c mtime newer than bar to trigger make next time...
	touch bar.c
    
clean: 
	@echo "start clean..."
	-rm -f foo bar
	@echo "end clean"

.PHONY: clean

bar 的生成規則中故意更新了源文件 bar.c 的修改時間,導致每次 make 時都會執行一遍。

tar

tar 解壓時默認會恢復文件的 mtime:

$ date
Wed Mar 23 14:23:18 CST 2022
$ tar xzvf release.tar.gz 
release/
release/arm64-v8a/
release/arm64-v8a/libjni-kernel.so
release/arm64-v8a/libjni-kservice.so
release/armeabi-v7a/
release/armeabi-v7a/libjni-kernel.so
release/armeabi-v7a/libjni-kservice.so
$ ls -lhR release
release:
total 8.0K
drwxr-xr-x 2 rd rd 4.0K Dec  2 15:54 arm64-v8a
drwxr-xr-x 2 rd rd 4.0K Dec  2 15:54 armeabi-v7a

release/arm64-v8a:
total 26M
-rwxr-xr-x 1 rd rd 3.3M Mar 21 17:17 libjni-kernel.so
-rwxr-xr-x 1 rd rd  23M Mar 21 17:17 libjni-kservice.so

release/armeabi-v7a:
total 18M
-rwxr-xr-x 1 rd rd 2.7M Mar 21 17:17 libjni-kernel.so
-rwxr-xr-x 1 rd rd  15M Mar 21 17:17 libjni-kservice.so
$ ls -lhuR release
release:
total 8.0K
drwxr-xr-x 2 rd rd 4.0K Mar 23 14:23 arm64-v8a
drwxr-xr-x 2 rd rd 4.0K Mar 23 14:23 armeabi-v7a

release/arm64-v8a:
total 26M
-rwxr-xr-x 1 rd rd 3.3M Mar 23 14:23 libjni-kernel.so
-rwxr-xr-x 1 rd rd  23M Mar 23 14:23 libjni-kservice.so

release/armeabi-v7a:
total 18M
-rwxr-xr-x 1 rd rd 2.7M Mar 23 14:23 libjni-kernel.so
-rwxr-xr-x 1 rd rd  15M Mar 23 14:23 libjni-kservice.so
$ ls -lhcR release
release:
total 8.0K
drwxr-xr-x 2 rd rd 4.0K Mar 23 14:23 arm64-v8a
drwxr-xr-x 2 rd rd 4.0K Mar 23 14:23 armeabi-v7a

release/arm64-v8a:
total 26M
-rwxr-xr-x 1 rd rd 3.3M Mar 23 14:23 libjni-kernel.so
-rwxr-xr-x 1 rd rd  23M Mar 23 14:23 libjni-kservice.so

release/armeabi-v7a:
total 18M
-rwxr-xr-x 1 rd rd 2.7M Mar 23 14:23 libjni-kernel.so
-rwxr-xr-x 1 rd rd  15M Mar 23 14:23 libjni-kservice.so

上面的例子中,當前時間是 03.23,解壓後的文件 mtime 為 03.21,atime 和 ctime 與解壓時間保持一致。如果不想恢復文件壓縮前的時間,可以使用 -m 選項:

$ tar xzvmf release.tar.gz 
release/
release/arm64-v8a/
release/arm64-v8a/libjni-kernel.so
release/arm64-v8a/libjni-kservice.so
release/armeabi-v7a/
release/armeabi-v7a/libjni-kernel.so
release/armeabi-v7a/libjni-kservice.so
$ ls -lhR release
release:
total 8.0K
drwxr-xr-x 2 rd rd 4.0K Mar 23 14:27 arm64-v8a
drwxr-xr-x 2 rd rd 4.0K Mar 23 14:27 armeabi-v7a

release/arm64-v8a:
total 26M
-rwxr-xr-x 1 rd rd 3.3M Mar 23 14:27 libjni-kernel.so
-rwxr-xr-x 1 rd rd  23M Mar 23 14:27 libjni-kservice.so

release/armeabi-v7a:
total 18M
-rwxr-xr-x 1 rd rd 2.7M Mar 23 14:27 libjni-kernel.so
-rwxr-xr-x 1 rd rd  15M Mar 23 14:27 libjni-kservice.so

常用於因機器時間不一致導致解壓後的文件處於「未來」時刻。下表羅列了一些與文件時間相關的 tar 選項:

選項 作用
–atime-preserve 壓縮時保持文件的 atime 不變
–mtime=DATE-OR-FILE 壓縮時設置 mtime 為指定格式或某個文件的 mtime
–newer-mtime=DATE 壓縮時選取 mtime 比指定時間新的文件
-m/–touch 解壓時不恢復文件的 mtime

更多詳情可參考 man tar。

sed

有時使用 stat 觀察命令對文件做了哪些改動是一件很有趣的事,之前為了驗證只更新文件內容不改變文件長度時 ctime 是否變更,曾經使用 sed 做等長度字元替換,發現了這樣一幕:

$ echo "abc" > foo
$ stat foo
  File: foo
  Size: 4         	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35520843    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-06 17:15:18.780422763 +0800
Modify: 2022-03-06 17:15:18.780422763 +0800
Change: 2022-03-06 17:15:18.780422763 +0800
 Birth: -
$ sed -i 's/abc/def/' foo
$ stat foo
  File: foo
  Size: 4         	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35521013    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-06 17:15:30.668890159 +0800
Modify: 2022-03-06 17:15:30.668890159 +0800
Change: 2022-03-06 17:15:30.668890159 +0800
 Birth: -

本以為最多就是 mtime  和 ctime 變更,沒想到三個時間全更新了,再仔細一看,文件 inode 都變了,頓時搞得有點懷疑人生,但是轉念一想,sed -i 的實現可能就是新建了一個文件用來存儲轉換後的數據,再將這個文件重命名為源文件,使用 strace 查看;

......
openat(AT_FDCWD, "foo", O_RDONLY)       = 3
ioctl(3, TCGETS, 0x7ffe932f0b10)        = -1 ENOTTY (Inappropriate ioctl for device)
fstat(3, {st_mode=S_IFREG|0664, st_size=4, ...}) = 0
umask(0700)                             = 002
getpid()                                = 21268
openat(AT_FDCWD, "./sedoTrUFp", O_RDWR|O_CREAT|O_EXCL, 0600) = 4
umask(002)                              = 0700
fcntl(4, F_GETFL)                       = 0x8002 (flags O_RDWR|O_LARGEFILE)
fstat(3, {st_mode=S_IFREG|0664, st_size=4, ...}) = 0
read(3, "abc\n", 4096)                  = 4
fstat(4, {st_mode=S_IFREG|000, st_size=0, ...}) = 0
read(3, "", 4096)                       = 0
fchown(4, 1000, 1000)                   = 0
fgetxattr(3, "system.posix_acl_access", 0x7ffe932f09f0, 132) = -1 ENODATA (No data available)
fstat(3, {st_mode=S_IFREG|0664, st_size=4, ...}) = 0
fsetxattr(4, "system.posix_acl_access", "\2\0\0\0\1\0\6\0\377\377\377\377\4\0\6\0\377\377\377\377 \0\4\0\377\377\377\377", 28, 0) = 0
close(3)                                = 0
write(4, "def\n", 4)                    = 4
close(4)                                = 0
rename("./sedoTrUFp", "foo")            = 0
close(1)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

果然如此,新建的臨時文件為 sedoTrUFp,轉換結束後有對應的 rename 調用,inode 不變才怪。

文件創建時間

linux 文件時間行文至此,有些熟悉 windows 的讀者可能會問了,如何獲取一個文件的創建時間呢?畢竟在 windows 上這才是 ctime 的真正含義啊。linux ext4 之後加入了文件的創建時間,區別於 ctime 叫 crtime,通過以下幾步獲取:

  • 確定文件所在的文件系統格式為 ext4 並獲取文件系統名 (第 4 行)
$ df -T
Filesystem     Type     1K-blocks     Used Available Use% Mounted on
udev           devtmpfs   4003776        0   4003776   0% /dev
tmpfs          tmpfs       807452     1888    805564   1% /run
/dev/sda5      ext4     959862832 19523928 891510744   3% /
tmpfs          tmpfs      4037244        0   4037244   0% /dev/shm
tmpfs          tmpfs         5120        4      5116   1% /run/lock
tmpfs          tmpfs      4037244        0   4037244   0% /sys/fs/cgroup
/dev/sda1      vfat        523248       12    523236   1% /boot/efi
tmpfs          tmpfs       807448       64    807384   1% /run/user/1000
  • 獲取要查詢的文件 inode 號 (第 5 行)
$ ls -i Makefile
35520820 Makefile
$ stat Makefile
  File: Makefile
  Size: 1578      	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35520820    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-05 16:38:12.824249909 +0800
Modify: 2022-03-05 16:30:27.381022589 +0800
Change: 2022-03-05 16:30:27.709007045 +0800
 Birth: -
  • 通過 debugfs 查詢對應的 crtime (倒數第 5 行)
$ sudo debugfs -R 'stat <35520820>' /dev/sda5
[sudo] password for yunh: 
debugfs 1.45.5 (07-Jan-2020)
Inode: 35520820   Type: regular    Mode:  0664   Flags: 0x80000
Generation: 3267144101    Version: 0x00000000:00000001
User:  1000   Group:  1000   Project:     0   Size: 1578
File ACL: 0
Links: 1   Blockcount: 8
Fragment:  Address: 0    Number: 0    Size: 0
 ctime: 0x62231fa3:a90a5b14 -- Sat Mar  5 16:30:27 2022
 atime: 0x62232174:c48438d4 -- Sat Mar  5 16:38:12 2022
 mtime: 0x62231fa3:5ad7c5f4 -- Sat Mar  5 16:30:27 2022
crtime: 0x6210abfe:5531517c -- Sat Feb 19 16:36:14 2022
Size of extra inode fields: 32
Inode checksum: 0x1027e200
EXTENTS:
(0):142124549

最後一步給 debugfs 傳遞的兩個參數 filesystem 和 inode 就是從前面兩步獲取的。總體而言不太方便,僅供參考,詳情見文末鏈接。

目錄文件

目錄遍歷

前面講過各個文件系統的實現均不支援目錄的硬鏈接,主要是防止遍歷時形成死循環,而目錄的符號鏈接不存在這方面的問題,主要是對於後者一般就不繼續遞歸了,像下面演示的這樣:

$ ln -s ../../ testdir/super
$ ls -lhR
.:
total 8.0K
-rwxrwxr-x 1 yunh yunh  338 Jun  6  2021 rename.sh
drwxrwxr-x 2 yunh yunh 4.0K Jan 23 17:06 testdir

./testdir:
total 0
lrwxrwxrwx 1 yunh yunh 6 Jan 23 17:06 super -> ../../

find / grep 等命令都能正確的處理目錄的符號鏈接,反而讓人不知道怎麼構造出有問題的場景了,書上是找了一個 Solaris 上的 ftw 命令來做驗證的,在 Ubuntu 上沒有找到對應的命令,不過有一個同名的 libc 函數,拿來做了一個類似的命令:

#include <stdio.h> 
#include <ftw.h> 

int ftw_func (char const* fpath, 
              struct stat const* sb, 
              int typeflag)
{
  //printf ("%s\n", fpath); 
  switch (typeflag)
  {
    case FTW_F:
      printf ("[R] %s\n", fpath); 
      break; 
    case FTW_D:
      printf ("[D] %s\n", fpath); 
      break; 
    case FTW_DNR:
      printf ("[DNR] %s\n", fpath); 
      break; 
    case FTW_NS:
      printf ("[NS] %s\n", fpath); 
      break; 
    default:
      printf ("unknown typeflag %d\n", typeflag); 
      return -1; 
  }
  return 0; 
}

int main (int argc, char *argv[])
{
  char const* dir = 0; 
  if (argc < 2)
    dir = "."; 
  else 
    dir = argv[1]; 

  int ret = ftw (dir, ftw_func, 1000); 
  return ret; 
}

再構造一個帶循環的文件樹:

$ mkdir A B
$ cd A
$ echo "abc" > foo
$ ln -s ../B loop
$ cd ../B
$ echo "def" > bar
$ ln -s ../A loop
$ cd ..
$ ls -lhR 
tmp:
total 12K
drwxrwxr-x 2 yunh yunh 4.0K Jan 23 17:27 A
drwxrwxr-x 2 yunh yunh 4.0K Jan 23 17:28 B
-rwxrwxr-x 1 yunh yunh  338 Jun  6  2021 rename.sh

tmp/A:
total 4.0K
-rw-rw-r-- 1 yunh yunh 4 Jan 23 17:16 foo
lrwxrwxrwx 1 yunh yunh 4 Jan 23 17:17 loop -> ../B

tmp/B:
total 4.0K
-rw-rw-r-- 1 yunh yunh 4 Jan 23 17:17 bar
lrwxrwxrwx 1 yunh yunh 4 Jan 23 17:17 loop -> ../A

用上面那個自製的 ftw 跑一下:

$ ./ftw tmp
[D] tmp
[R] tmp/rename.sh
[D] tmp/A
[R] tmp/A/foo
[D] tmp/A/loop
[R] tmp/A/loop/bar

居然沒有死循環,不過看這遍歷結果有點兒不對勁,缺少目錄 B。查看 man ftw,針對 flags 有這樣一條說明:

       FTW_PHYS
              If  set, do not follow symbolic links.  (This is what you want.)
              If not set, symbolic links are followed, but no file is reported
              twice.

              If  FTW_PHYS is not set, but FTW_DEPTH is set, then the function
              fn() is never called for a directory that would be a  descendant
              of itself.

看來這個 api 自己有快取一些資訊來防止文件重複輸出,所以上面並不是沒有目錄 B,而是用 B 的等價物 A/loop 代替了 B。為了製造循環輸出的場景,還是得老老實實用 opendir/readdir/closedir/lstat 自己寫一個程式:

static int dopath (char const* path, Myfunc* func)
{
  struct stat statbuf; 
  struct dirent *dirp; 
  int ret; 
  DIR *dp; 
  char *ptr; 

  // may loop for dir soft links
  //if (stat (path, &statbuf) < 0)
  if (lstat (path, &statbuf) < 0)
    return func (path, &statbuf, FTW_NS); 

  char inbuf[PATH_MAX] = { 0 }; 
  char outbuf[PATH_MAX] = { 0 }; 
  char newpath[PATH_MAX] = { 0 }; 
  strcpy (newpath, path); 
  strcpy (inbuf, path); 
  while (S_ISLNK(statbuf.st_mode))
  {
    // handle symbolic to dir
    if (readlink (inbuf, outbuf, sizeof(outbuf)) < 0)
    {
        printf ("read symbolic path %s failed\n", inbuf); 
        return func (inbuf, &statbuf, FTW_NS); 
    }
    else 
    {
        if (lstat (outbuf, &statbuf) < 0)
            return func (outbuf, &statbuf, FTW_NS); 

        strcpy (newpath, outbuf); 
    }
  }

  if (S_ISDIR(statbuf.st_mode) == 0)
    return func (newpath, &statbuf, FTW_F); 

  if ((ret = func (newpath, &statbuf, FTW_D)) != 0)
    return ret; 

  ptr = newpath + strlen (newpath); 
  *ptr ++ = '/'; 
  *ptr = 0; 

  if ((dp = opendir (newpath)) == NULL)
  {
    ptr[-1] = 0; 
    return func (newpath, &statbuf, FTW_DNR); 
  }

  if (chdir (newpath) != 0)
    printf ("chdir %s failed\n", newpath); 

  while ((dirp = readdir (dp)) != NULL)
  {
    if (strcmp (dirp->d_name, ".") == 0 || 
        strcmp (dirp->d_name, "..") == 0)
        continue; 

    strcpy (ptr, dirp->d_name); 
    printf ("%s\n", newpath); 
    if ((ret = dopath (ptr, func)) != 0)
      break; 
  }

  ptr[-1] = 0; 
  if (chdir ("..") != 0)
    printf ("chdir back failed\n"); 

  if (closedir (dp) < 0)
    err_ret ("can't close directory %s", newpath); 

  return ret; 
}

上面就是遍歷目錄的核心邏輯了,需要注意以下幾點:

  • 使用 lstat 而不是 stat 判斷文件屬性,以便得到目錄的符號鏈接
  • 當文件是符號鏈接時,讀取並判斷指向內容是否為目錄,注意這個過程是遞歸的

最後終於得到如願以償的輸出了:

./A
A/foo
A/loop
../B/bar
../B/loop
../A/foo
../A/loop
……
../B/loop
../A/foo
../A/lo
Segmentation fault (core dumped)

太不容易了,可以看到由於陷入死循環程式最終崩潰掉了。不過這種循環比較容易破解,刪除目錄軟鏈接即可,如果循環是由硬鏈接引起的就不太好處理了,並不是一個 rmdir 可以搞定的 (仔細想一想,刪除目錄的前提是目錄為空,當形成循環時目錄不可能為空,這導致刪除的前提條件被破壞掉了),這是不引入目錄硬鏈接的第三個理由。

進程工作目錄

文件路徑分絕對路徑和相對路徑,之前提到符號鏈接中既可以存放絕對路徑,也可以存放相對路徑。當使用相對路徑時,將基於進程的工作目錄進行查找。

與許多人設想的不太一樣,內核並不存放進程完整的字元串工作路徑,取而代之的是指向目錄 vnode 的指針等目錄本身的資訊,當需要取得進程當前工作目錄的完整路徑時,我們需要一個函數來完成這件工作:getcwd,對它的邏輯作以下簡單說明:

  • 通過 .. 得到父目錄中所有目錄項,遍歷它們並與當前目錄的 inode 編號作對比,得到匹配的目錄名稱作為 dirname
  • 按照上面的方法,不斷遍歷 .. 直到根目錄,找到每一層目錄的 dirname 拼接為完整的 pathname 就是最終的結果了

一個簡單的 getcwd 底層居然做了如此多的工作,在最糟糕的情況下,它將遍歷包含工作目錄的整個文件樹,效率是不高的。那內核為什麼不存儲一個字元串的完整工作路徑呢?考察一下下面這個程式:

#include "../apue.h"
#include <limits.h> 
#include <unistd.h> 

void ch_dir(char const* dir)
{
  if (chdir (dir) < 0)
    err_sys ("chdir failed"); 

  printf ("chdir to %s succeeded\n", dir); 
  char path[PATH_MAX+1] = { 0 }; 
  char *cwd = getcwd (path, PATH_MAX); 
  printf ("getcwd = %s\n", cwd); 
}

int main (int argc, char *argv[])
{
  char dir[PATH_MAX] = { 0 }, *dir_name = NULL; 
  if (argc <= 2)
    strcpy(dir, argv[1]); 
  else if (argc <= 3)
  {
    strcpy(dir, argv[1]); 
    dir_name = argv[2]; 
  }
  else 
    err_quit ("Usage: dirch dir [dirname]", -1); 

  ch_dir(dir); 
  while (dir_name)
  {
    ch_dir(dir_name);
  }

  return 0; 
}

它接收兩個參數,參數一表示第一次切換到的目錄,參數二表示之後循環切換的目錄。再復用上一節中製造的特殊目錄結構:

$ ls -lhR 
tmp:
total 12K
drwxrwxr-x 2 yunh yunh 4.0K Jan 23 17:27 A
drwxrwxr-x 2 yunh yunh 4.0K Jan 23 17:28 B
-rwxrwxr-x 1 yunh yunh  338 Jun  6  2021 rename.sh

tmp/A:
total 4.0K
-rw-rw-r-- 1 yunh yunh 4 Jan 23 17:16 foo
lrwxrwxrwx 1 yunh yunh 4 Jan 23 17:17 loop -> ../B

tmp/B:
total 4.0K
-rw-rw-r-- 1 yunh yunh 4 Jan 23 17:17 bar
lrwxrwxrwx 1 yunh yunh 4 Jan 23 17:17 loop -> ../A

就可以這樣啟動它了:

$./dirch tmp/A loop

得到了如下的輸出:

chdir to tmp/A succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/A
chdir to loop succeeded
getcwd = /home/yunh/code/apue/04.chapter/tmp/B
chdir to loop succeeded
……

進程工作目錄將在 A 和 B  兩個目錄之前無限切換,一開始我懷疑當路徑超過 PATH_MAX 時進程會異常退出,然而觀察 getcwd 的輸出,這一幕沒有發生,當前工作路徑的長度甚至沒有變化!經過目錄軟鏈接跳轉後,進程的當前目錄節點被直接設置為目標目錄的 vnode,壓根不會感受到中間的 loop 符號鏈接節點,但是如果換作字元串路徑呢?再做一個實驗:

#! /bin/sh

main()
{
    if [ $# -lt 2 ]; then 
        echo "Usage dirch dirname [loop]"
        exit 1
    fi

    base="$1"
    dir="$2"
    cd "${base}"
    while true; do
        cd "${dir}"
        if [ $? -ne 0 ]; then 
            echo "cd ${dir} failed"
            exit 2
        else 
            echo "cd to `pwd`"
        fi
    done
}

main "$@"

抱著試試看的態度使用 shell 的 cd 和 pwd 來做實驗,希望它有不一樣的結果:

$ sh dirch.sh tmp/A loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
cd to /home/yunh/code/apue/04.chapter/tmp/A/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop/loop
dirch.sh: 14: cd: can't cd to loop
cd loop failed

這個腳本做了和之前程式一樣的事情,結果卻大相徑庭,最終因路徑超長失敗退出。從 pwd 的輸出看到 shell  貌似是存儲了當前目錄完整的字元串路徑,從而在 builtin cd 作用下越加越長,直到出錯。pwd 除了 bultin 版本,還有一個位於 /usr/bin 下面的 pwd 命令,將腳本中的 builtin pwd 替換為 /usr/bin/pwd,情況會不會改善呢?

$ sh dirch.sh tmp/A loop
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
cd to /home/yunh/code/apue/04.chapter/tmp/B
cd to /home/yunh/code/apue/04.chapter/tmp/A
dirch.sh: 14: cd: can't cd to loop
cd loop failed

答案是沒有,雖然 /usr/bin/pwd 的輸出改善了許多,但 cd 最終還是失敗了。從這裡可以得到以下結論:

  • builtin pwd 和 /usr/bin/pwd 實現不同,後者的實現更類似於 getcwd,其實就是調用了 getcwd (見下文)
  • builtin cd 的實現與 chdir 不同,更不可能調用後者
  • builtin cd/pwd 藉助了字元串路徑記錄當前工作目錄,在遇到目錄符號鏈接時會出現超長出錯的情況

為了避免上面出錯的場景,內核不記錄進程當前工作目錄的字元串路徑,這個道理你弄明白了嗎?

最後補充一下 /usr/bin/pwd 內部調用 getcwd 的 strace 證據:

$ strace /usr/bin/pwd
……
brk(NULL)                               = 0x55e3c4b4e000
brk(0x55e3c4b6f000)                     = 0x55e3c4b6f000
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=8850624, ...}) = 0
mmap(NULL, 8850624, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fe433d93000
close(3)                                = 0
getcwd("/home/yunh/code/apue/04.chapter", 4096) = 32
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
write(1, "/home/yunh/code/apue/04.chapter\n", 32/home/yunh/code/apue/04.chapter
) = 32
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

另外沒有非 builtin 的 cd 可用,這是因為更改子進程的當前目錄對父進程毫無影響。

最後回到本節開始的話題,相對路徑肯定是相對的,絕對路徑卻不一定是絕對的,它也可以是相對的,也就是說遇到 ‘/’ 開始的路徑,也不一定從系統的根目錄開始解釋,具體以哪個路徑作為根路徑,可以通過 chroot 設置,這裡就不展開說了。

設備號

每個文件都依託於設備存在,inode 中有兩個設備號:

  • st_dev:標識文件所在文件系統,該文件系統包含了這一文件的文件名與 inode
  • st_rdev:標識字元文件/塊文件所在的實際設備

每個設備號又分為主設備號與次設備號,分別通過宏 major 和 minor 獲取,其中:

  • 主設備號:標識驅動程式,有時編碼為與其通訊的外設板
  • 次設備號:標識特定的子設備

同一硬碟上的文件系統主設備號相同,次設備號不同。ls 通常不列印任何設備號,除非目標是字元/塊文件:

$ ls -l /dev/sd*
brw-rw---- 1 root disk 8,  0 Mar 19 07:59 /dev/sda
brw-rw---- 1 root disk 8,  1 Mar 19 07:59 /dev/sda1
brw-rw---- 1 root disk 8,  2 Mar 19 07:59 /dev/sda2
brw-rw---- 1 root disk 8,  5 Mar 19 07:59 /dev/sda5
brw-rw---- 1 root disk 8, 16 Mar 20 17:52 /dev/sdb
brw-rw---- 1 root disk 8, 20 Mar 20 17:52 /dev/sdb4
brw-rw---- 1 root disk 8, 32 Mar 20 18:08 /dev/sdc
brw-rw---- 1 root disk 8, 33 Mar 20 18:08 /dev/sdc1

其中第 5 列分別是主次設備號。stat 會列印普通文件的設備號,如果是字元/塊文件,還會列印它的真實設備號:

$ stat dirch.sh /dev/sda5
  File: dirch.sh
  Size: 401       	Blocks: 8          IO Block: 4096   regular file
Device: 805h/2053d	Inode: 35263364    Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-20 17:42:51.626251788 +0800
Modify: 2022-03-20 17:42:48.942182594 +0800
Change: 2022-03-20 17:42:48.978183522 +0800
 Birth: -
  File: /dev/sda5
  Size: 0         	Blocks: 0          IO Block: 4096   block special file
Device: 5h/5d	Inode: 334         Links: 1     Device type: 8,5
Access: (0660/brw-rw----)  Uid: (    0/    root)   Gid: (    6/    disk)
Access: 2022-03-20 15:45:18.474776286 +0800
Modify: 2022-03-19 07:59:48.326347594 +0800
Change: 2022-03-19 07:59:48.326347594 +0800
 Birth: -

第 3 行 Device 和 Device type 輸出的就是,第一個文件 (dirch.sh) 顯示它位於的文件系統設備號是 0x0805 (805h),第二個設備文件的真實設備號也是 0805 (8,5),這就說明 dirch.sh 這個文件是存儲在設備 /dev/sda5 這個設備上面。lsblk 命令可以列出系統中所有的設備,對快速查看設備號很有幫助:

$ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
……
sda      8:0    0 931.5G  0 disk 
├─sda1   8:1    0   512M  0 part /boot/efi
├─sda2   8:2    0     1K  0 part 
└─sda5   8:5    0   931G  0 part /
sdb      8:16   1  28.9G  0 disk 
└─sdb4   8:20   1  28.9G  0 part /media/yunh/Ubuntu 20.0
sdc      8:32   0   1.8T  0 disk 
└─sdc1   8:33   0   1.8T  0 part /media/yunh/Backup Plus
sr0     11:0    1  1024M  0 rom  

其中 sda 是系統自帶的硬碟,分為 3 個分區 sda1/2/3;sdb 是 U 盤 (vfat);sdc 是移動硬碟 (exfat),後兩者只包含一個分區。lsblk 的輸出內容有一些和 mount 相似,可以相互參考著看。查看 sda1 分區上的 efi 文件:

 stat /boot/efi/
  File: /boot/efi/
  Size: 4096      	Blocks: 8          IO Block: 4096   directory
Device: 801h/2049d	Inode: 1           Links: 3
Access: (0700/drwx------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 1970-01-01 08:00:00.000000000 +0800
Modify: 1970-01-01 08:00:00.000000000 +0800
Change: 1970-01-01 08:00:00.000000000 +0800
 Birth: -

其設備號 0x0801 與 lsblk 的輸出一致,再查看另外兩個設備 sdb4 和 sdc1 上的文件:

$ stat /media/yunh/Ubuntu\ 20.0/ /media/yunh/Backup\ Plus/
  File: /media/yunh/Ubuntu 20.0/
  Size: 8192      	Blocks: 16         IO Block: 8192   directory
Device: 814h/2068d	Inode: 1           Links: 13
Access: (0755/drwxr-xr-x)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 1970-01-01 08:00:00.000000000 +0800
Modify: 1970-01-01 08:00:00.000000000 +0800
Change: 1970-01-01 08:00:00.000000000 +0800
 Birth: -
  File: /media/yunh/Backup Plus/
  Size: 131072    	Blocks: 256        IO Block: 131072 directory
Device: 821h/2081d	Inode: 1           Links: 14
Access: (0755/drwxr-xr-x)  Uid: ( 1000/    yunh)   Gid: ( 1000/    yunh)
Access: 2022-03-20 18:08:58.000000000 +0800
Modify: 2022-03-20 18:08:58.250000000 +0800
Change: 2022-03-20 18:08:58.250000000 +0800
 Birth: -

它們的設備號分別為 0x0814 和 0x0821,展開為十進位後分別為 (8,20) 與 (8,33),和 lsblk 的輸出也能對得上。這提供了一種快速查看文件所在設備的方法,你學會了嗎?

不過這裡測試的幾個文件的主設備號都是 8,但他們明顯不在同一塊存儲設備上,因此書上的說法是有問題的。

結語

本文嘗試通過解釋 api 介面底層做了什麼來闡釋 linux 文件系統在設計層面的一些考慮,配合通俗易懂的日常命令和簡單程式來進行驗證,踐行「紙上得來終覺淺,絕知此事要躬行」的理念,目的是做一個 linux 文件系統的引入,後面有機會可以出一篇文章,專門閱讀 linux 源碼來證實本文的一些結論,想想就讓人激動~~

參考

[1]. Linux文件系統詳解

[2]. 硬碟基本知識(磁頭、磁軌、扇區、柱面)

[3]. 磁碟分區也是隱含了技術技巧的

[4]. Ext2文件系統簡單剖析(一)

[5]. Linux下對inode和塊的理解

[6]. inode 、數據塊、磁碟容量

[7]. linux文件系統—inode及相關概念 inode大小的最佳設置

[8]. APUE—UNIX文件系統

[9]. 文件atime未變問題的研究

[10]. Linux下查看和修改文件時間

[11]. Linux中8個有用的touch命令

[12]. 準確獲取linux文件的創建時間

[13]. Inode vs Vnode

[14]. Linux調試分析診斷利器——strace

[15]. Linux tar命令解壓時提示時間戳異常的處理辦法

[16]. Why atime is not preserved in tar?