文件與文件系統
簡單來說,文件是計算機系統裡的一堆數據,可以用單一實體的方式被引用
文件命名
原始的 UNIX 文件系統設計者,決定將ASCII 256 個元素集合都可用於文件名,但有兩個例外:
- 控制字符
NUL
(此字符所有位址皆為0),這是許多程式語言用來表示字串結尾的字符 - 斜槓(
/
)字符
最好考慮加上以下限制:
- 是可視字符
- 避免使用 Shell 的 meta 字符,也就是大部分的標點符號
- 避免用連字號開頭,看起來像是 UNIX 的命令選項
UNIX 文件名是 Case Sensitive,慣用小寫,除了重要文件會用大寫或大小寫混用,如 README
、Makefile
,原因是在 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 個或多個不知名數據字節所集結而成的字節流
複製一個文件:
1 2 3 4 5 6 7 |
try-to-get-a-gyte while (have-a-byte) { put-a-byte try-to-get-a-byte } |
許多工具設計上使用「大的但大小固定」的緩衝區來保存文本行,如果輸入過長的行,可能導致錯誤,建議長度限制在易讀的範圍,例如 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/stdin
、dev/stdout
、dev/stderr
名稱來彌補這個缺陷
文件所有權
UNIX 文件有兩種所有權:使用者(user)、群組(group),兩者各有自己的權限,對於不具備所有權的,稱為其他人(other),也有特定的權限
這兩種所有權透過 chown
及 chgrp
指令變更
在 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
參數) - 如果輸出到終端,無法打印的字符會轉換成問號;輸出至非終端則不做改變
範例:顯示有無終端的輸出格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
~$ ls /bin/*sh /bin/ash /bin/bash /bin/bsh /bin/sh ~$ ls /bin/*sh | cat /bin/ash /bin/bash /bin/bsh /bin/sh ~$ ls /bin/*sh -1 /bin/ash /bin/bash /bin/bsh /bin/sh |
範例:計算系統文件區塊(block)大小
- 使用
-s
列出文件的區塊大小
1 2 3 |
ls -lgs 2220 -r-xr-xr-t 1 sys 2270300 Nov 4 1999 /lib/libc.so.1 |
2270300 / 2220 = 1022.6,由此可知一個區塊大小為 2^10 = 1024 個字節
範例:列出隱藏文件
- 當命令行參數為目錄時,
ls
會列出該目錄的內容 - 使用
-d
選項來避免顯示目錄內容 - 使用
-a
選項列出所有包含隱藏的文件
假設目錄結構為,且當前目錄位於 di1
:
1 2 3 4 5 6 7 8 9 10 |
d1 ├── d1a │ ├── d1a.f1a │ └── .d1a.f1b ├── .d1b │ ├── d1b.f1a │ └── .d1b.f1b ├── d1.f1a └── .d1.f1b |
列出當其目錄隱藏文件,但同時匹配且展開當前目錄與父目錄
1 2 3 4 5 6 7 8 9 |
$ ls .* .d1a.f1b .: d1a.f1a ..: d1a d1.f1a |
列出隱藏文件,不展開目錄
1 2 3 |
$ ls -d .* . .. .d1a.f1b |
列出文件,不展開目錄
1 2 3 4 5 6 7 |
$ ls ./* # 指定當前目錄 ./d1a.f1a $ ls -d ../* # 指定父目錄 ../d1a ../d1.f1a |
文件 Metadata
透過 -l
參數可取得文件細節報告,包含 9 個特定欄位:
- 文件類型與權限
- 連結數量
- 擁有者
- 所屬群組
- 以 Byte 為單位的文件大小(不包含完整目錄大小)
- 最後修改時間戳:月
- 最後修改時間戳:日
- 最後修改時間戳:年,若為六個月內修改的文件,則顯示修改時間(
%H:%M
)
第一欄的文件描述結構
[-dl] |
[r-][w-][x-] |
[r-][w-][x-] |
[r-][w-][x-] |
---|---|---|---|
文件類型,- 為一般文件;d 為目錄;l 為連結 |
使用者的權限 | 群組的權限 | 除此之外的其他人的權限 |
touch 指令
- 雖然有許多方式可以建立空文件,但使用 touch 是比較安全的,避免使用定位符
>
導致以存在的文件內容遺失 - 常用於更新文件時間戳(預設或使用
-m
更新修改時間;-a
更新存取時間) - 常用於鎖定文件,避免啟動第二個程序實例
範例:touch 以外建立文件的方式
1 2 3 4 5 |
cat /dev/null > myfile printf "" > myfile cat /dev/null >> myfile printf "" >> myfile |
範例:更新為指定時間
1 2 |
touch -t 197607040000.00 myfile |
臨時文件
- 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 進制並去除空格
1 2 3 4 |
$ TMPFILE=/tmp/secret.$(cat /dev/urandom | od -x | tr -d ' ' | head -n 1) $ echo $TMPFILE /tmp/secret.00000005b6d65b602fa06d2667a9258da20f011 |
尋找文件
locate 命令
將文件系統的所有文件名壓縮成資料庫,並透過 cron 與 updatedb 指令建立
type 命令
為內建的 Shell 命令,認得別名與函數,適合用來查詢一個命令所在的文件路徑
find 命令
使用遞迴向下深入目錄樹,在不保證順序的狀態下尋找文件與輸出結果
範例:列出我的目錄下不屬於我的文件
1 2 |
find $HOME/. ! -user $USER |
範例:停用目錄向下尋找
- 要注意第一個參數是「目錄或文件」,未提供的話則視為「當前目錄」
不要在當前目錄尋找
1 2 3 |
find -prune find . -prune |
尋找當前目錄下的文件
1 2 |
find * -prune |
**範例:找出目錄中大於 1 MB 的文件
1 2 |
find -type f -size +1024k |
範例:找出空文件、或過去一年都未讀取過的文件
1 2 |
find -size 0 -o -atime +365 |
範例:計算當前目錄大小
1 2 |
find -ls | awk '{Sum += $7} END {printf("Total: %.0f bytes\n", Sum)}' |
註:要計算整個系統的文件大小、可用空間請改用 df
與 du
命令
範例:根據修改紀錄,建立文件與目錄的多個列表,提醒近期做過什麼事
xargs 命令
來自命令替換的產生的輸出有時會很長,甚至超出環境變量,導致看到 Argument list too long. 的錯誤
xargs 可以在標準輸入上取得參數列表、一行一個,再將它們以適當大小組合(由主機的 ARG_MAX
直決定),傳給下一個命令(此命令作為 xargs 參數)
範例:在系統標投文件,查找關鍵字
1 2 |
find /usr/indluce -type f | xargs grep POSIX_OPEN_MAX /dev/null |
註:在 grep 沒有給定文件參數的情況下,會讀取標準輸入,所以可以提供 /dev/null 這樣的參數,確保不會因為 find 未產生輸出,而一直卡在等待終端輸入
文件校驗和匹配
要是你懷疑可能有許多文件有相同的內文,而使用 cmp
或 diff
進行的比對會隨著文件數的增加呈現指數的增長,這時候可以使用文件校驗和(file checksum)來取得近似線性的性能
有很多工具可以使用,包括 sum
、chsum
、checksum
,消息摘要工具 md5
、md5sum
,安全性散列(secure-hash)算法工具 sha
、sha1sum
、sha256
、sha384
長的十六進制簽名字串只不過是一個具有許多位數的整數,他是由文件的所有字節計算得來,在這種計算方式下,幾乎不可能有任何其他字節流能產生相同的值。使用好的算法、較長的簽名一般來說較可能具有唯一性。
生日悖論(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 來看:
- 要「確保訊息由 Alice 發出」,訊息必須以 Alice 的公鑰進行解密
- 要「確保訊息只有 Bob 能讀」,訊息必須以 Bob 的公鑰進行加密
對整個訊息加密是沒有必要的,我們可以只加密文件的校驗和,即數字簽名(digital signature),不過需要有方法驗證簽名的真實性
GNU Privacy Guard (GPG) 與 Pretty Good Privacy(PGP) 提供很多公鑰加密演算法,用來驗證數字簽名。GPG 架構較簡單,也適用於較多平台