Linux 下幾個檔案操作命令的程式碼實現
用 C 語言實現命令 cp、df、mkdir、rm、tac
本文章中的示例程式碼是在 CentOS 5.4 64 位環境下執行通過的,在其它 unix 系統上沒有測試過。
Linux 作業系統中的命令實際上是編譯好的可執行程式,比如說 ls 這個命令,這個檔案位於 /bin 目錄下面,當我們用 file /bin/ls 命令檢視的時候會有以下輸出:
1 2 3 4 |
|
這個命令通過呼叫 stat 系統呼叫和 /usr/share/file/magic.mgc 檔案來決定檔案的型別。如上的 /bin/ls 是一個 ELF 格式的動態連結的 64 位的可執行檔案。
系統呼叫是使用者程式和作業系統核心之間的介面,我們可以使用作業系統提供的系統呼叫來請求分配資源和服務。我們可以通過 man 2 章節來查詢 Linux 提供的系統呼叫的具體使用方法。有關檔案操作的常見系統呼叫命令有:open、creat、close、read、write、lseek、opendir、readdir、mkdir、stat 等等。
cp 命令的實現
cp 命令的模擬實現
大家也都知道 cp 這個命令主要的作用就是把一個檔案從一個位置複製到另一個位置。比如現在 /root 目錄下有一個 test.txt 檔案,如果我們用 cp test.txt test2.txt 命令的話,在同一個目錄下面就會生成一個同樣內容的 test2.txt 檔案了。
那麼 cp 命令是怎麼實現的呢,我們看如下程式碼:
清單 1. cp 命令實現程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
該程式的主要實現思想是:開啟一個輸入檔案,建立一個輸出檔案,建立一個 BUFFERSIZE 大小的緩衝區;然後在判斷輸入檔案未完的迴圈中,每次讀入多少就向輸出檔案中寫入多少,直到輸入檔案結束。
cp 命令實現的說明
讓我來詳細的講述一下這個程式:
- 開頭四行包含了 4 個頭檔案,<stdio.h> 檔案包含了 fprintf、perror 的函式原型定義;<unistd.h> 檔案包含了 read、write 的函式原型定義;<fcntl.h> 檔案包含了 open、creat 的函式原型定義、<stdlib.h> 檔案包含了 exit 的函式原型定義。這些函式原型有些是系統呼叫、有些是庫函式,通常都可以在 /usr/include 目錄中找到這些標頭檔案。
- 接下來的 2 行以巨集定義的方式定義了 2 個常量。BUFFERSIZE 用來表示緩衝區的大小、COPYMODE 用來定義建立檔案的許可權。
- 接下來的一行定義了一個函式原型 oops,該函式的具體定義在最後出現,用來輸出出錯資訊到 stderr,也就是標準錯誤輸出的檔案流。
- 接下來主程式開始。首先定義了 2 個檔案描述符、一個存放讀出位元組數的變數 n_chars、和一個 BUFFERSIZE 大小的字元陣列用來作為拷貝檔案的緩衝區。
- 接下來判斷輸入引數的個數是否為 3,也就是程式名 argv[0]、拷貝原始檔 argv[1]、目標檔案 argv[2]。不為 3 的話就輸出錯誤資訊到 stderr,然後退出程式。
- 接下來的 2 行,用 open 系統呼叫以 O_RDONLY 只讀模式開啟拷貝原始檔,如果開啟失敗就輸出錯誤資訊並退出。如果想了解檔案開啟模式的詳細內容請使用命令 man 2 open,來檢視幫助文件。
- 接下來的 2 行,用 creat 系統呼叫以 COPYMODE 的許可權建立一個檔案,如果建立失敗函式的返回值為 -1 的話,就輸出錯誤資訊並退出。
- 接下來的迴圈是拷貝的主要過程。它從輸入檔案描述符 in_fd 中,讀入 BUFFERSIZE 位元組的資料,存放到 buf 字元陣列中。在正常讀入的情況下,read 函式返回實際讀入的位元組數,也就是說只要沒有異常情況和檔案沒有讀到結尾,那麼 n_chars 中存放的就是實際讀出的位元組的數字。然後 write 函式將從 buf 緩衝區中,讀出 n_chars 個字元,寫入 in_out 輸出檔案描述符。由於 write 系統呼叫返回的是實際寫入成功的位元組數。所以當讀出 N 個字元,又成功寫入 N 個字元到輸出檔案描述符中去的時候,就表示寫成功了,否則就報告寫入錯誤。
- 最後就是用 close 系統呼叫關閉開啟的輸入和輸出檔案描述符。
rm 命令的實現
rm 命令的模擬實現
rm 命令主要是用來刪除一個檔案。
該命令的實現程式碼如下:
清單 2. rm 命令程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
其中程式的關鍵是 unlink 系統呼叫,unlink 函式原型包含在 <unistd.h> 標頭檔案裡面。
用 strace 來跟蹤命令
我們從這個程式的建立過程來分析這個程式。
這個命令的模擬程式是怎麼寫出來的呢?
首先,我們可以在機器上 touch test 建立一個 test 檔案,然後呼叫 strace rm test 命令來檢視 rm 命令具體使用了那些系統呼叫。
通過檢視,我們看到主要使用的系統呼叫如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
我們可以看到起主要作用的就是 unlink(“test”) 這個系統呼叫。
讓我們來分析一下這些輸出的含義:
- 首先第一行 execve 系統呼叫。該系統呼叫執行引數“/bin/rm”中的程式(以 #! 開頭的可執行指令碼也可以),後面第一個方括號中表示執行的引數,第二個方括號中表示執行的環境變數。
- 接下來的 brk 和 mmap 命令,主要是用來給可執行命令分配記憶體空間。
- 後面的 lstat 系統呼叫用來確定檔案的 mode 資訊,包括檔案的型別和許可權,檔案大小等等。
- 然後 access 系統呼叫檢查當前使用者程序對於 test 檔案的寫入訪問許可權。這裡返回值為 0 也就是說程序對於 test 檔案有寫入的許可權。
- 最後呼叫 unlink 系統呼叫刪除檔案。
這裡如果我們建立一個目錄 test1,然後用 rm test1 去刪除這個目錄會有什麼結果呢?
我們看到有如下輸出:
1 |
|
這時我們用 strace 命令來追蹤一下,發現輸出主要是如下不同。
1 |
|
這裡說明了刪除不掉的原因是 unlink 系統呼叫報錯,unlink 它認為 test 是一個目錄,不予處理。
那麼怎麼刪除一個目錄呢?應該是用 rmdir 系統呼叫,這樣就不會出現上述的問題了。
mkdir 命令的實現
mkdir 命令的模擬實現
再讓我們來看看 mkdir 的實現。
完整的程式碼如下:
清單 3. mkdir 實現程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
這段程式碼也比較簡單,我這裡就不逐行解釋了,主要說以下幾點:
首先 mkdir 函式是定義於 <sys/stat.h> 和 <sys/types.h> 標頭檔案之中的。
而 fprintf 函式是位於 <stdio.h> 檔案之中的。
mkdir 的函式原型如下:
1 |
|
mode 宣告為 mode_t 型別。
那麼 mode_t 資料型別是什麼資料型別,應該從哪個檔案去檢視它的定義呢?
mode_t 資料型別究竟是什麼型別
讓我們逐步查詢一下。
首先從檔案 /usr/include/sys/stat.h 中找到 mode_t 型別
/usr/include/sys/stat.h -> typedef __mode_t mode_t;
說明 mode_t 只是對 __mode_t 的一種定義。
然後從 /usr/include/bits/types.h 中找到 __mode_t 型別
/usr/include/bits/types.h -> __STD_TYPE __MODE_T_TYPE __mode_t;
說明 __mode_t 也只是對 __MODE_T_TYPE 的一種定義。
/usr/include/bits/typesizes.h -> #define __MODE_T_TYPE __U32_TYPE
說明 __MODE_T_TYPE 是對 __U32_TYPE 的一種定義。
/usr/include/bits/types.h -> #define __U32_TYPE unsigned int
最後 __U32_TYPE 是一種無符號的整數的定義。
從上述推導可以看出,mode_t 實際上也就是一種無符號整數。
另外如下結構 struct stat 定義中的 st_mode 成員變數也是使用的 mode_t 型別的變數。
從 man 2 stat 中可以找到結構 struct stat 的定義,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
該結構也是我們在後面的 tac 命令實現中需要用到的結構體。我們需要用到結構體中的 st_size 成員,該成員反映了被讀取的檔案描述符對應的檔案的大小。
tac 命令的實現
tac 命令的模擬實現
tac 命令主要用來以倒序的方式顯示一個文字檔案的內容,也就是先顯示最後一行的內容,最後顯示第一行的內容。程式碼如下:
清單 4. tac 命令實現程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
讓我們來執行一下該程式:
程式的執行情況如下,假設編譯後的可執行檔名為 emulatetac,有一個文字檔案 test.txt。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
可以看出檔案內容以倒序方式顯示輸出了。
tac 命令實現的說明
下面逐行講解:
- #include 的標頭檔案,都應該通過 man 2 系統呼叫命令來查詢,這裡就不多說了。
- 下面定義了一個巨集常量 SIZE,該常量主要用來表示能夠讀入最大多少個位元組的檔案,當檔案過大的時候程式就不執行,直接退出。然後定義了巨集常量 NLINE 表示換行符'\n'。
- 接下來主程式體開始了:首先定義一個字元陣列 buf,用來把讀入檔案的每個位元組都存在該數組裡面。
- 然後定義了 4 個字串指標,一個指向結構體 struct stat 的指標 fp,一個檔案描述符。
- 然後為指向結構體的指標 fp 分配儲存空間。
- 接下來判斷輸入引數是否為 2 個,也就是命令本身和檔名。不是 2 個就直接退出。
- 然後以只讀方式開啟輸入檔名的檔案,也就是 test.txt。開啟成功的話,把開啟的檔案賦值到檔案描述符 fd 中,錯誤的話退出。
- 然後用 fstat 系統呼叫把檔案描述符 fd 中對應檔案的元資訊,存放到結構體指標 fp 指向的結構中。
- 下面判斷當檔案的大小超過緩衝區陣列 buf 的大小 SIZE-1 時,就退出。
- 下面將把檔案 test.txt 中的每個字元存放到陣列 buf 中。
- 下面是程式的核心部分:首先我們找到字串 buf 中的第一個換行字元存放到 p1 指標裡面,然後把最後一個換行字元置為字串結束符。
- 接下來我們從後往前查詢字串 buf 中的換行符,直到遇到第一個換行符 p1。同時列印每個找到的換行符'\n'中的下一個字元開始的字串,也就剛好是一行文字。
- 最後當從後向前找到第一個換行字元時,列印第一行,程式結束。
df 命令的實現
df 命令的模擬實現
通過 strace 命令檢視 df 主要使用瞭如下的系統呼叫:open、fstat、read、statfs
我這裡實際上是模擬實現的 df --block-size=4096 這個命令,也就是說以 4096 位元組為塊大小來顯示磁碟使用情況。
這裡最為關鍵的是 statfs 這個結構體,該結構體的某些欄位被用作 df 命令的輸出欄位:
1 2 3 4 5 6 7 8 9 10 11 |
|
比如:df --block-size=4096 的輸出如下(縱向列出):
1 2 3 4 5 6 7 8 9 10 11 12 |
|
模擬實現的程式碼如下:
清單 5. 模擬實現程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
df 命令實現的說明
下面解釋一下這個程式:
- 首先,該程式定義了一個函式 displayapartition, 這裡先定義它的函式原型。
- 然後我們從主程式說起:首先定義了一個 char tmpline[SIZE1] 陣列,該陣列用來存放從巨集定義常量 FN 代表的檔案中,開啟後存入檔案的每行記錄。
- 接著定義了一個檔案流指標和 3 個字串指標。
- 接下來開啟檔案 FN 並把結果賦值給檔案流變數 fp, 如果開啟失敗就退出。
- 下面從開啟的檔案流中讀出 SIZE1 個字元到臨時陣列 tmpline。比如讀出一行資料為:/dev/sda1 / ext3 rw 0 0 將把 /dev/sda1 放入陣列 tmpline,把載入點 / 放入指標 pt2,同時判斷字串 tmpline 是否包含 /dev 字串,這樣來判斷是否是一個磁碟檔案,如果是的話就呼叫子函式 displayapartition,不是則返回。
- 子函式 displayapartition 是做什麼的呢?該函式接受 2 個引數,一個是行 /dev/sda1 / ext3 rw 0 0 中的第一列比如:/dev/sda1 也就是實際磁碟作為 pt 指標,一個是行中的第二列比如:/ 也就是掛載點作為 pt1 指標。然後子函式通過 pt1 指標,讀取掛載上的檔案系統資訊到 buf 資料結構裡面。
- 根據開頭介紹過的 statfs 結構體,buf.f_blocks 表示開啟的檔案系統的總資料塊,buf.f_blocks-buf.f_bfree 表示已經使用的資料塊,buf.f_bavail 表示非超級使用者可用的剩餘資料塊,磁碟使用率就是前面列出過的計算表示式:(f_blocks- f_bfree)/ f_blocks*100%。通過子函式就可以打印出 df 需要顯示的所有資訊到標準輸出了。
小結
本文依次講述了 cp、rm、mkdir、tac、df 命令的主要功能實現程式碼,當然每個命令還有很多引數,我這個模擬實現程式碼甚至連主要功能的很多細節都沒有實現,比如 df 命令的輸出頭我沒有打印出來,這牽涉到列印頭和輸出格式化等很多細節。所以,從這裡我們就可以推斷出,真實的原始碼肯定是考慮得非常全面、嚴謹和健壯的。我這裡只是拋磚引玉,希望能給愛好 Linux 的朋友們提供一種理解 Linux 系統的思路。
相關主題
- 參考:《 Linux 作業系統下 c 語言程式設計入門》。
- 參考 developerWorks 文章:Linux 系統呼叫列表。
- 參考 developerWorks 文章:read 系統呼叫剖析。
- 參考 developerWorks 文章:記憶體管理內幕 -- 動態分配的選擇、折衷和實現。
- 在 developerWorks Linux 專區 尋找為 Linux 開發人員(包括 Linux 新手入門)準備的更多參考資料,查閱我們 最受歡迎的文章和教程。
- 在 developerWorks 上查閱所有 Linux 技巧 和 Linux 教程。