Skip to content

Classic Shell Scripting 讀書筆記 (六)

  • Ops

文件與文件系統

簡單來說,文件是計算機系統裡的一堆數據,可以用單一實體的方式被引用

文件命名

原始的 UNIX 文件系統設計者,決定將ASCII 256 個元素集合都可用於文件名,但有兩個例外:

  • 控制字符 NUL(此字符所有位址皆為0),這是許多程式語言用來表示字串結尾的字符
  • 斜槓(/)字符

最好考慮加上以下限制:

  • 是可視字符
  • 避免使用 Shell 的 meta 字符,也就是大部分的標點符號
  • 避免用連字號開頭,看起來像是 UNIX 的命令選項

UNIX 文件名是 Case Sensitive,慣用小寫,除了重要文件會用大寫或大小寫混用,如 READMEMakefile,原因是在 ASCII 裡,大寫排在小寫之前,會列在前方(現行系統排序則是參考 locale)

命名長度普遍允許使用到 255 個字符,POSIX 中定義 NAME_MAX 來限制其長度

ASCII

1963 年,美國標準學會以 American Standard Code for Information Interchange 名稱提出 7 位元的字符集,允許 128 個不同字符。

7 位元對世界語言是不夠的,由於現在系統都使用 8 位元作為最小定址儲存單位,允許 256 個不同字符,前半段被拿來客製化,後半段留給 ASCII,在未遵循國際標準的情況下,也因此出現了幾百種不同的字符指定方式,或稱內碼頁(code page)。

8 位元對歐洲語系仍是不夠的,因此 ISO 為此開發了一系列的代碼頁。

90 年代,單一萬國字符集 Unicode 開始運作,所有字符最終需要大約 21 個位元。由於許多操作系統只使用到 16 個位元,UNIX 系統使用一個可變動的位寬度編碼: UTF-8,允許已存在的 ASCII 文件成為有效的 Unicode 文件。

文件裡有什麼

以另一個觀點來看,UNIX 文件不過是 0 個或多個不知名數據字節所集結而成的字節流

複製一個文件:

許多工具設計上使用「大的但大小固定」的緩衝區來保存文本行,如果輸入過長的行,可能導致錯誤,建議長度限制在易讀的範圍,例如 50-70 個字符

所有文件被視為是二進制文件:每一個包含在其中的字節,都有 256 種可能的值。

文本文件(有分行的文本)可視為二進制文件的子集,以 ASCII linefeed (LF) 表示行的界線,也就是換行字符,在程式語言通常以 \n 來表示(比起 Windows 使用一組 carriage-return/linefeed 簡單多了)

文件中會保留字節數的計數,當嘗試讀取超越此計數時,返回 end-of-line 的暗示,因此不可能看到任何磁盤區塊之前的內容

文件系統架構

UNIX 文件系統是可嵌套的樹狀結構,目錄使用 directory 而非 folder(偏向紙本的),結構的根源為根目錄,使用特殊名稱 /

當目錄底下有過多的文件,應該以子目錄重新組織,提昇查找效率

文件名完整路徑長度沒有特殊限制,POSIX 中定義 PATH_MAX 來限制其長度,通常為 256 個字符

UNIX 目錄本身就是文件,但擁有特殊屬性且有特定訪問方式

所有 UNIX 目錄,就算是空的,也總是包含兩個特殊目錄: ..(父目錄) 及 . (當前目錄本身),根目錄的父目錄就是自己(//..是一樣的)

路徑結尾若以斜槓 / 結束,則該文件是一個目錄,沒以斜槓結尾,不一定不是目錄

WWW 的 URL 結構就是 UNIX 風格的

層級式文件系統

UNIX 允許將某個文件系統,邏輯性地放置於令一個文件系統的任意目錄之上,稱為掛載(mounting)

掛載的相關細節,除存在一個特殊文件中,通常為 /etc/fstab/etc/vfstab

有些掛載/卸載需要特殊權限,有些則允許非特定用戶也可以操作,如 CD-ROM、隨身碟

Index Node (inode)

文件系統建立時,一個管理原指定的固定大小表格也隨之建立,稱為 inode

inode 包含了系統辨識文件時所需的 metadata,但文件名不包含在內,文件名保存在目錄裡

  • 列出目錄下的文件時,不須多次查詢 inode
  • 一個 inode 編號可以對應到多個文件名,也就是 UNIX 中的連結(link)功能

inode 所保存的訊息包含文件的: 1) inode 序列號 2) 類型 3) 連結 4) 大小 5) 權限 6)時間戳

當一個物理文件,其有多個名稱時(代表至少存在一個連結),哪一個才能刪除物理文件?——
inote 表包含了連接到文件的計數,當計數為 0,文件區塊最終才會重新指派給可用空間的列表

軟連接與硬連接

同一個文件系統下的連接,指向的是 inode 編號(hard-link),但連結跨越文件系統時,inode 會紀錄該文件類型是符號連接/軟連接(symbolic/ soft link),指向的是「一個 UNIX 路徑」而非 inode 編號

為了避免早成死循環,目錄通常不能有硬連接,除了 ...

設備作為文件

所有 UNIX 系統都有 /dev 目錄,存放「設備文件」,由各個特殊軟體控制,也就是設備的驅動程式(device driver)

透過文件的「開啟——處理——關閉」的概念,來操作設備

文件大小

UNIX 文件受限於:1) 在 inode 中分配到的位數 2) 文件系統本身的大小

大部分現行的 UNIX 文件系統使用 32 位整數,以保存文件大小,由於文件定位系統調用,可以在文件中前後移動,該整數必須帶有正負號,因此最大可能的文件大小為 2^31 – 1 個字節,約為 2GB;在 64 位元的系統上,則可以支持 8 億GB

UNIX 文件系統建立時,基於效能理由會保留約 10% 的空間,給 root 執行的進程使用,另外會保留空間給 inode,因此,磁碟有效空間通常是估計的 80%

文件區塊異常小的文件多半有洞(hole),這是因為使用直接訪問的方式寫入字節在指定的位置。數據庫程序就是這樣儲存鬆散式的表格

沒有名稱的文件

UNIX 打開供輸入或輸出的文件名稱,不會被保留在內核的數據結構中。因此,在命令行上針對標準輸出、標準輸入、標準錯誤輸出而被重定向的文件名,都不被引用的進程所知。

較新的 UNIX 系統提供了 /dev/stdindev/stdoutdev/stderr 名稱來彌補這個缺陷

文件所有權

UNIX 文件有兩種所有權:使用者(user)、群組(group),兩者各有自己的權限,對於不具備所有權的,稱為其他人(other),也有特定的權限

這兩種所有權透過 chownchgrp 指令變更

在 inode 中,使用者與群組都以序列號識別而非名稱,序列號與名稱對應的表格,各為密碼文件 /etc/passwd 與組文件 /etc/group,現今偏好使用函式 [set|get|end][pw|gr]ent()setpwent() 來訪問這兩個檔案

文件權限

分為三種類型:讀(read)、寫(write)、執行(execute),透過 chmod 指令變更

對於新建立的文件,會受到默認權限影響,默認權限為 3 個八進制組成的遮罩,透過 umask 指令設置,表示「要被拿走的權限」,例如 umask=077 代表使用者具備完整權限,而群組與其他人不具任何權限

umask 通常為 002,即刪除其他人的寫入權限

對於複製的文件,會保留原文件的權限,但此權限會同樣套用預設權限的遮罩

所有 UNIX 文件系統都提供額外的權限位: set-user-ID、set-group-ID、sticky(黏滯位),絕不應該在 Shell 腳本裡面設置 set-user-ID、set-group-ID 避免安全漏洞

需要非默認權限時,Shell 腳本應該於開始處就明白且直接下達 umask 命令

目錄權限

目錄權限的解讀與文件的權限不同:

  • 讀取:可列出目錄的內容
  • 寫入:可在目錄下建立或刪除文件
  • 執行:可以訪問目錄下的文件或子目錄(受子目錄的權限限制)

區分「可列出」(讀取)和「可訪問」(執行)的意義在於,為了在看不到父目錄的情況下,仍能看到子目錄的文件

目錄設置 sticky 位時(即 chmod +t <path-of-dir>),裡頭的文件就只有「文件所有者」或「目錄所有者」才能刪除。此功能最常應用在公用目錄,如 /tmp,避免使用者刪除不屬於他們的文件

文件時間戳

inode 紀錄文件的三種時間戳:訪問時間、inode 變更時間(metadata 的更新)、修改時間(內容的更新)

UNIX 時間戳(epoch)是從 0 開始,由 1970/1/1 00:00:00 UTC 算起。大部分現行系統都有一個帶正負號 32 位元的時間計數器,每一秒加 1,且允許日期的表示往前推到 1901 年晚期,往後則推到 2038 年,當計數器在 2038 年溢出時,會回到 1901;在 64 位元系統上,即使以百萬之一秒計算,還是能擴展到五十萬年以上。

文件連結

軟連結是讓兩個已隔離的文件樹接在一起,移動了含有連結的子樹會造成連結斷裂,產生不一致的情況。

替換文件會產生一個新的 inode 編號、連結計數加 1,並切斷原有的硬連接;更新文件位置(in-place)會保留擁有者及群組,但複製或變更名稱會將原擁有者重設為執行操作的人

軟連結通常使用相對路徑比較好,而且連結的目錄最好是位於同一層級或更低層級

只有在引用連結的當下才會知道連結已經斷裂(如朋友搬了家沒通知你)

文件類型

  • -: 文件
  • d: 目錄
  • l: 連結
  • b: 塊設備(位於/dev)
  • c: 字符設備(位於/dev)
  • p: 命名的管道 (named pipe)
  • s: Socket,一種特定的網路連接

文件處理

ls 命令

  • 文件參數必須存在,否則會回傳錯誤
  • 如果輸出並非終端,以一行一個顯示;如果輸出是終端,以多欄顯示(除非提供-1參數)
  • 如果輸出到終端,無法打印的字符會轉換成問號;輸出至非終端則不做改變

範例:顯示有無終端的輸出格式

範例:計算系統文件區塊(block)大小

  • 使用 -s 列出文件的區塊大小

2270300 / 2220 = 1022.6,由此可知一個區塊大小為 2^10 = 1024 個字節

範例:列出隱藏文件

  • 當命令行參數為目錄時,ls 會列出該目錄的內容
  • 使用 -d 選項來避免顯示目錄內容
  • 使用 -a 選項列出所有包含隱藏的文件

假設目錄結構為,且當前目錄位於 di1

列出當其目錄隱藏文件,但同時匹配且展開當前目錄與父目錄

列出隱藏文件,不展開目錄

列出文件,不展開目錄

文件 Metadata

透過 -l 參數可取得文件細節報告,包含 9 個特定欄位:

  1. 文件類型與權限
  2. 連結數量
  3. 擁有者
  4. 所屬群組
  5. 以 Byte 為單位的文件大小(不包含完整目錄大小)
  6. 最後修改時間戳:月
  7. 最後修改時間戳:日
  8. 最後修改時間戳:年,若為六個月內修改的文件,則顯示修改時間(%H:%M

第一欄的文件描述結構

[-dl] [r-][w-][x-] [r-][w-][x-] [r-][w-][x-]
文件類型,-為一般文件;d為目錄;l為連結 使用者的權限 群組的權限 除此之外的其他人的權限

touch 指令

  • 雖然有許多方式可以建立空文件,但使用 touch 是比較安全的,避免使用定位符 > 導致以存在的文件內容遺失
  • 常用於更新文件時間戳(預設或使用 -m 更新修改時間;-a 更新存取時間)
  • 常用於鎖定文件,避免啟動第二個程序實例

範例:touch 以外建立文件的方式

範例:更新為指定時間

臨時文件

  • UNIX 提供兩個特殊目錄: /tmp/var/tmp(舊系統為 /usr/tmp),避免臨時檔案弄亂一般目錄
  • /tmp 於開機時清空,/var/tmp 則會保留
  • 因為存取頻繁,可以將其放在常駐內存型(memory-resident)的文件系統(文件系統在替換空間(swap)裡,表示它存在於內存)
  • 為了避免 DOS(denial of service),Shell 腳本都應該使用 umask 命令,或是先以 touch 建立必須的臨時文件,再執行 chmod 設置適當權限
  • 在文件開啟的狀態下,執行 unlink() 系統調用,就會馬上刪除文件,由於仍在開啟狀態,所以仍可繼續訪問,直到文件關閉或是工作結束任一先發生為止
  • 文件名最好是不可預知的,避免安全性問題,可透過 mktemp 指令建立難以預測的文件名,或是通過 /dev/random 與 /dev/urandom 特殊文件產出

範例:建立臨時文件名

從 /dev/urandom 讀取二進制字節流,轉成 16 進制並去除空格

尋找文件

locate 命令

將文件系統的所有文件名壓縮成資料庫,並透過 cron 與 updatedb 指令建立

type 命令

為內建的 Shell 命令,認得別名與函數,適合用來查詢一個命令所在的文件路徑

find 命令

使用遞迴向下深入目錄樹,在不保證順序的狀態下尋找文件與輸出結果

範例:列出我的目錄下不屬於我的文件

範例:停用目錄向下尋找

  • 要注意第一個參數是「目錄或文件」,未提供的話則視為「當前目錄」

不要在當前目錄尋找

尋找當前目錄下的文件

**範例:找出目錄中大於 1 MB 的文件

範例:找出空文件、或過去一年都未讀取過的文件

範例:計算當前目錄大小

註:要計算整個系統的文件大小、可用空間請改用 dfdu 命令

範例:根據修改紀錄,建立文件與目錄的多個列表,提醒近期做過什麼事

xargs 命令

來自命令替換的產生的輸出有時會很長,甚至超出環境變量,導致看到 Argument list too long. 的錯誤

xargs 可以在標準輸入上取得參數列表、一行一個,再將它們以適當大小組合(由主機的 ARG_MAX 直決定),傳給下一個命令(此命令作為 xargs 參數)

範例:在系統標投文件,查找關鍵字

註:在 grep 沒有給定文件參數的情況下,會讀取標準輸入,所以可以提供 /dev/null 這樣的參數,確保不會因為 find 未產生輸出,而一直卡在等待終端輸入

文件校驗和匹配

要是你懷疑可能有許多文件有相同的內文,而使用 cmpdiff 進行的比對會隨著文件數的增加呈現指數的增長,這時候可以使用文件校驗和(file checksum)來取得近似線性的性能

有很多工具可以使用,包括 sumchsumchecksum,消息摘要工具 md5md5sum,安全性散列(secure-hash)算法工具 shasha1sumsha256sha384

長的十六進制簽名字串只不過是一個具有許多位數的整數,他是由文件的所有字節計算得來,在這種計算方式下,幾乎不可能有任何其他字節流能產生相同的值。使用好的算法、較長的簽名一般來說較可能具有唯一性。

生日悖論(birthday paradox)

如果你從 N 個項目選一個,則有 1/N 的機會被選中。如果選 M 個項目,則有 M(M-1)/2 可能的配對,找到一個相同配對的機率是 (M(M-1)/2)/N,對於 M 而言,該值到達可能性 1/2 約是 N 的平方根

舉例來說,一個 32 個十六進制的簽名,等同於 128 位元,要具有相同簽名的可能性,約為 2^64 = 1.84 x 10^19 分之一

範例:列出近似的文件

將簽名作為索引,僅報告計數結果大於 1 的情況

數字簽名驗證

以經典的 Alice/Bob 來看:

  1. 要「確保訊息由 Alice 發出」,訊息必須以 Alice 的公鑰進行解密
  2. 要「確保訊息只有 Bob 能讀」,訊息必須以 Bob 的公鑰進行加密

對整個訊息加密是沒有必要的,我們可以只加密文件的校驗和,即數字簽名(digital signature),不過需要有方法驗證簽名的真實性

GNU Privacy Guard (GPG) 與 Pretty Good Privacy(PGP) 提供很多公鑰加密演算法,用來驗證數字簽名。GPG 架構較簡單,也適用於較多平台

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *