<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      [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?

      posted @ 2022-03-23 16:07  goodcitizen  閱讀(744)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 欧美白妞大战非洲大炮| 日韩秘 无码一区二区三区 | 亚洲 欧美 动漫 少妇 自拍| 中文文字幕文字幕亚洲色| 99精品国产一区二区三| 亚洲精品综合久中文字幕| 国产精品人妻在线观看 | 国产色悠悠综合在线观看| 一 级做人爱全视频在线看| 中文字幕免费不卡二区| 国产真人无遮挡免费视频| 中文字幕人妻色偷偷久久| 久久综合97丁香色香蕉| 蜜臀av久久国产午夜福利软件| 久久超碰色中文字幕超清| 中文人妻熟妇乱又伦精品| 亚洲日本中文字幕乱码中文| 亚洲精品国产免费av| 中文国产不卡一区二区| 欧美寡妇xxxx黑人猛交| 日本黄色一区二区三区四区| 无码国产偷倩在线播放老年人| 人妻系列无码专区69影院| 在线观看国产成人av天堂| 男人+高清无码+一区二区| 四虎在线播放亚洲成人| 四虎在线成人免费观看| 夜夜爽免费888视频| 中文字幕永久精品国产| 国产乱久久亚洲国产精品| 亚洲精品乱码久久久久久蜜桃不卡 | 极品尤物被啪到呻吟喷水| 97久久人人超碰国产精品| 中文字幕一区有码视三区| 日本高清一区免费中文视频| 高h纯肉无码视频在线观看| 中文字幕99国产精品| 精品视频不卡免费观看| 无套内射极品少妇chinese| 自拍视频亚洲精品在线| 国产一区二区三区av在线无码观看 |