(三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录
.
.
.
.
.
目錄
(一) 一起學 Unix 環境高級編程 (APUE) 之 標準IO
(二) 一起學 Unix 環境高級編程 (APUE) 之 文件 IO
(三) 一起學 Unix 環境高級編程 (APUE) 之 文件和目錄
(四) 一起學 Unix 環境高級編程 (APUE) 之 系統數據文件和信息
(五) 一起學 Unix 環境高級編程 (APUE) 之 進程環境
(六) 一起學 Unix 環境高級編程 (APUE) 之 進程控制
(七) 一起學 Unix 環境高級編程 (APUE) 之 進程關系 和 守護進程
(八) 一起學 Unix 環境高級編程 (APUE) 之 信號
(九) 一起學 Unix 環境高級編程 (APUE) 之 線程
(十) 一起學 Unix 環境高級編程 (APUE) 之 線程控制
(十一) 一起學 Unix 環境高級編程 (APUE) 之 高級 IO
(十二) 一起學 Unix 環境高級編程 (APUE) 之 進程間通信(IPC)
(十三) [終篇] 一起學 Unix 環境高級編程 (APUE) 之 網絡 IPC:套接字
?
?
前面兩篇博文講了文件 IO 的基本操作,但是它們操作的都是文件本身所存儲的有效數據。而文件系統保存文件的時候不僅僅要存儲文件內的數據,還要存儲許多亞數據,即文件屬性和其它特征數據。這篇博文 LZ 就帶領大家討論文件系統亞數據的操作。
1.stat(2)
1 stat, fstat, lstat - get file status 2 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 #include <unistd.h> 6 7 int stat(const char *path, struct stat *buf); 8 int fstat(int fd, struct stat *buf); 9 int lstat(const char *path, struct stat *buf);?
stat(2) 函數族是專門用來獲取文件的亞數據信息的。系統中 stat(1) 命令就是利用這個函數實現的。
它們會根據文件的路徑(path)或是已打開的文件的文件描述符(fd)得到該文件的亞數據,并將他們回填到 struct stat 類型的結構體中供調用者使用。
下面介紹下 struct stat 的字段含義:
1 struct stat { 2 dev_t st_dev; /* ID of device containing file */ 3 ino_t st_ino; /* inode 號 */ 4 mode_t st_mode; /* 權限和文件類型,位圖,權限位9位,類型3位,u+s 1位,g+s 1位,粘滯位(T位)1位。 5 位圖是用一位或幾位數據表示某種狀態。許多要解決看似不可能的問題的面試題往往需要從位圖著手。*/ 6 nlink_t st_nlink; /* 硬鏈接數量 */ 7 uid_t st_uid; /* 文件屬主 ID */ 8 gid_t st_gid; /* 文件屬組 ID */ 9 dev_t st_rdev; /* 設備號,只有設備文件才有 */ 10 off_t st_size; /* 總大小字節數,編譯時需要指定宏 -D_FILE_OFFSIZE_BITES=64,否則讀取大文件可能導致溢出 */ 11 blksize_t st_blksize; /* 文件系統塊大小 */ 12 blkcnt_t st_blocks; /* 每個 block 占用 512B,則整個文件占用的 block 數量。這個值是文件真正意義上所占用的磁盤空間 */ 13 // 下面三個成員都是大整數,實際使用時需要先轉換 14 time_t st_atime; /* 文件最后訪問時間戳 */ 15 time_t st_mtime; /* 文件最后修改時間戳 */ 16 time_t st_ctime; /* 文件亞數據最后修改時間戳 */ 17 }在 Linux 系統中,一個文件實際占用了多大的磁盤空間要看 st_blocks 的數量,而不是看 st_size 的大小。
一般情況下文件系統的一個 block 的大小為 4KB,而每個 st_blocks 是 512B,所以一個有效文件站用磁盤空間最小的大小為 8 個 st_blocks。
下面我們用前面學過的函數舉個栗子來說明 blocks 這個東西。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <fcntl.h> 4 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 8 int main (void) 9 { 10 int fd = -1; 11 12 fd = open("tmp", O_RDWR | O_CREAT, 0664); 13 lseek(fd, 1024UL*1024UL*1024UL*5UL-1UL, SEEK_SET); 14 write(fd, "", 1); 15 close(fd); 16 17 return 0; 18 }?
>$ gcc -Wall lseek.c -o lseek
>$ ls -lh tmp
-rw-rw-r-- 1 yuhuashi yuhuashi 5.0G 4月 15 23:26 tmp
>$ stat tmp
File: ‘tmp’
Size: 5368709120 Blocks: 8 IO Block: 4096 regular file
Device: 13h/19d Inode: 222778 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/yuhuashi) Gid: ( 1000/yuhuashi)
Access: 2015-04-15 23:28:38.532203798 +0800
Modify: 2015-04-15 23:28:38.532203798 +0800
Change: 2015-04-15 23:28:38.532203798 +0800
Birth: -
這段代碼的功能是生成一個 5GB 大小的空洞文件(即內容全部為 \0 的文件)。
實現方法很簡單,首先創建一個文件,然后將文件位置指針向后偏移 5GB - 1byte,使用 UL 是為了防止數據類型溢出,然后用系統調用寫入最后一字節,關閉文件,完成。
為什么最后要使用系統調用 write(2) 函數寫入一個字節在上一篇博文中已經解釋過了,如果不發生系統調用,則生成的文件 blocks 為 0,即不占用實際的磁盤空間。
用 ls(1) 命令可以看到,文件的大小為 5GB,但是用 stat(1) 命令查看,雖然 Size 是?5368709120,但 Blocks 卻只有 8,這說明文件實際只占用了磁盤空間的 4KB。
那么既然 st_size 表示的不是實際的文件大小,那它又有什么用途呢?它其實只是文件系統記錄的一個屬性而已,并沒有什么特別之處。
類似 stat(2)、fstat(2)、lstat(2) 函數族這種命名的函數在 Linux 系統中有很多這一點值得大家注意,總結出了這樣的規律,以后大家在見到新的函數的時候就能大致做到“見名知義”。
stat(2):一般函數族中函數名稱不帶前綴的表示普通的用法,比如 stat(2) 函數通過文件路徑 path 參數讀取并解析一個文件的亞數據。
fstat(2):一般函數族中某個函數名稱以 f 開頭的表示解析的文件來源不再是文件路徑 path,而是已打開的文件對應的文件描述符了(fd)。
lstat(2):一般函數族中某個函數名稱以 l 開頭的表示如果 path 參數指定的文件是一個符號鏈接則不要展開它,而是直接處理符號鏈接文件本身。上面兩個函數如果拿到的是一個符號鏈接,則會展開它,讀取符號鏈接所指向的真實文件的亞數據。
2.文件類型
通過 struct stat 結構體的 st_mode 成員可以獲得文件類型信息。
Linux 系統中的文件共分為 7 種類型:dcb-lsp
d 目錄
c 字符設備文件
b 塊設備文件
- 普通文件
l 符號鏈接文件
s 套接字文件
p 管道文件
是不是很奇怪這些符號是什么?其實當你使用 ls(1) 命令查看文件詳細信息的時候就應該注意到了,權限位的第一個符號就是上面這些符號,也就是下面標紅的部分。
1 >$ touch file1.txt 2 >$ mkdir files 3 >$ mkfifo fifo 4 >$ sudo mknod fileb b 10 20 5 >$ sudo mknod filec c 10 21 6 >$ ln -s /etc/services services 7 >$ nc -Ul socet 8 >$ ls -l 9 total 0 10 prw-rw-r-- 1 yuhuashi yuhuashi 0 4月 16 16:46 fifo 11 -rw-rw-r-- 1 yuhuashi yuhuashi 0 4月 16 16:24 file1.txt 12 brw-r--r-- 1 root root 10, 20 4月 16 16:30 fileb 13 crw-r--r-- 1 root root 10, 21 4月 16 16:31 filec 14 drwxrwxr-x 2 yuhuashi yuhuashi 40 4月 16 16:24 files 15 lrwxrwxrwx 1 yuhuashi yuhuashi 13 4月 16 16:31 services -> /etc/services 16 srwxrwxr-x 1 yuhuashi yuhuashi 0 4月 16 16:39 socet?
st_mode 是使用位圖的形式來保存文件的類型和權限信息的,那么位圖是什么?
位圖就是用某一位或幾位來表示不同狀態的一種手段。如果某一位為 1 則認為某個功能是使能的,為 0 則認為對應的功能是 disable 的。
圖1 st_mode 位圖
其中 0-2 位表示 Other 權限,3-5 位表示 Group 權限,6-8 位表示 Owner 權限,9 位表示粘著位(T位),10 位表示 G+S 位,11 位表示 U+S 位,12-14 位表示上面所述的7種文件類型,15 位預留。
好吧其實畫完了才發現這圖的高低位畫反了,本圖的左側為高位,右側為低位。
st_mode 位圖介紹完了,下面我們來說說其中的文件類型位。
系統提供了一些宏來操作這個位圖,其中下面的八個宏專門用于提取 12-14 位。
S_IFMT ? ? ? ?0170000 bit mask for the file type bit fields 從 st_mode 中提取出文件類型位,其它位清零
S_IFSOCK ??0140000 socket ?套接字文件
S_IFLNK ? ? ?0120000 symbolic link 符號鏈接文件
S_IFREG ? ? ?0100000 regular file 普通文件
S_IFBLK ? ? ? 0060000 block device 塊設備文件
S_IFDIR ? ? ? 0040000 directory 目錄
S_IFCHR ? ? ?0020000 character device 字符設備
S_IFIFO ? ? ? ?0010000 FIFO 管道文件
?
系統也提供了七個帶參數的宏,可以幫我們直接判斷文件的類型:
S_ISREG(m) ? ? ? ?is it a regular file??? ? ? ??? ? ? ??? ? ? ??? ? ? ?? ? ? ? 是否為普通文件
S_ISDIR(m)? ? ? ? ?directory??? ? ? ??? ? ? ??? ? ? ??? ? ? ??? ? ? ??? ? ? ?? ? ?是否為目錄
S_ISCHR(m)?? ? ? ?character device??? ? ? ??? ? ? ??? ? ? ??? ? ? ?? ? ? ? 是否為字符設備文件
S_ISBLK(m)?? ? ? ?block device??? ? ? ??? ? ? ??? ? ? ??? ? ? ??? ? ? ?? ? ? ? 是否為塊設備文件
S_ISFIFO(m)?? ? ? FIFO (named pipe)??? ? ? ??? ? ? ??? ? ? ??? ? ? ?? ? ?是否為管道文件
S_ISLNK(m)?? ? ? ?symbolic link? (Not in POSIX.1-1996.)?? ? ? ??是否為符號鏈接文件
S_ISSOCK(m)?? ? socket? (Not in POSIX.1-1996.)?? ? ? ??? ? ? ?? ? 是否為套接字文件
在使用這些宏的時候只需要將 st_mode 作為參數傳入即可,如果是某個類型的文件,它們會返回 1,否則返回 0,可以方便的在 if 語句中使用。
?
3.設置用戶 ID 和設置組 ID
這一節說的是 st_mode 位圖中的 U+S 位和 G+S 位。
可以使用下面的宏獲得這兩位的狀態,相信大家都能看得懂,用法和上面介紹的文件類型位是一樣的,具體的就不再贅述了。
S_ISUID 0004000 set-user-ID bit
S_ISGID 0002000 set-group-ID bit (see below)
4.文件訪問權限
文件訪問權限就是 st_mode 位圖中的低 9 位。
大家都知道,在 Linux 系統中文件的權限分為 3 個組:文件屬主權限、文件屬組權限、其它用戶權限,而每個組又分為 4 種權限:讀取(r)、寫入(w)、執行(x)、無權(-)。
所以在使用 ls(1) -l 命令時可以得到類似 -rwx-r-xr-x 的權限標志,這個標志就是這樣來的。
圖2 權限標志
?
就像文件類型一樣系統提供了宏供我們方便的從 st_mode 中得到對應的權限位:
S_IRWXU ? ? ? 00700?? ? ? ?mask for file owner permissions ?當 st_mode & 這個宏時,計算結果將只保留 st_mode 中的文件屬主權限,其它位全部被清零。注意只是將計算結果中其它位清零,并不是把 st_mode 本身的數據清零。
S_IRUSR?? ? ? ?00400 ?? ? ??owner has read permission
S_IWUSR?? ? ? 00200??? ? ??owner has write permission
S_IXUSR?? ? ? ?00100 ? ? ? ?owner has execute permission
S_IRWXG?? ? ? 00070 ? ? ? ?mask for group permissions
S_IRGRP?? ? ? ?00040 ? ? ? ?group has read permission
S_IWGRP?? ? ? 00020 ? ? ? ?group has write permission
S_IXGRP?? ? ? ?00010 ? ? ? ?group has execute permission
S_IRWXO?? ? ? 00007 ?? ? ? mask for permissions for others (not in group)
S_IROTH?? ? ? ?00004 ?? ? ? others have read permission
S_IWOTH?? ? ? 00002 ?? ? ? others have write permission
S_IXOTH?? ? ? ?00001 ?? ? ? others have execute permission
?
5.access(2)
1 access - check real user's permissions for a file 2 3 #include <unistd.h> 4 5 int access(const char *pathname, int mode);?
測試當前進程對 pathname 文件是否具有 mode 權限,成功返回 0,失敗返回 -1 并設置具體的 errno。
如果 pathname 是符號鏈接,則不會展開,而是測試符號鏈接文件本身。
mode 可以選擇:
F_OK:檢測文件是否存在;
R_OK:檢測是否具有讀權限;
W_OK:檢測是否具有寫權限;
X_OK:檢測是否具有執行權限;
?
6.umask(2)
1 umask - set file mode creation mask 2 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 6 mode_t umask(mode_t mask);?
用于設定進程文件模式的掩碼(又稱屏蔽字),并返回之前的值。umask 值越大,權限越低。umask(1) 命令就是用這個函數封裝的。
參數 mask 由以下位構成,使用按位或運算符指定多個模式:
| st_mode 屏蔽 | 含義 |
| S_IRUSR S_IWUSR S_IXUSR | 屬主讀 屬主寫 屬主執行 |
| S_IRGRP S_IWGRP S_IXGRP | 屬組讀 屬組寫 屬組執行 |
| S_IROTH S_IWOTH S_IXOTH | 其他讀 其他寫 其他執行 |
表1 st_mode 掩碼
?
7.chmod(2)
1 chmod, fchmod - change permissions of a file 2 3 #include <sys/stat.h> 4 5 int chmod(const char *path, mode_t mode); 6 int fchmod(int fd, mode_t mode);?
chmod(2) 函數族的函數用于更改現有文件的訪問權限。看到這兩個文件的命名方式是不是覺得似曾相識?沒錯,上面我們講 stat(2) 系統調用的時候就說過這種命名約定,fun()、ffun()、lfun()等等見名知義,系統中還有很多函數都是遵循這樣的約定的。
chmod(1) 命令就是使用 chmod(2) 函數族封裝的,mode 參數位圖與 表1 中的掩碼是一致的,另外還支持?S_ISUID (U+S)、S_ISGID?(G+S) 和 S_ISVTX?(T位)。多個位使用按位或的方式進行計算再傳參。
?
8.粘著位
粘滯位是在早期的 Unix 系統中為常用的可執行程序(bin)設置的,這樣可以將使用頻繁的程序駐留在內存中。現在 Unix 使用了 Page Cache 技術,可以使常用的數據塊駐留在內存中了,所以粘滯位的作用越來越弱化了。
?
9.chown(2)
1 chown, fchown, lchown - change ownership of a file 2 3 #include <unistd.h> 4 5 int chown(const char *path, uid_t owner, gid_t group); 6 int fchown(int fd, uid_t owner, gid_t group); 7 int lchown(const char *path, uid_t owner, gid_t group);?
chown(2) 函數族用來修改文件所有者。修改成功返回 0,修改失敗返回 -1 并設置 errno。
修改文件所有者這件事在 Linux 中只有超級用戶能做,這是為什么呢?舉個簡單的栗子:
假設系統中有 A 和 B 兩個用戶,而系統對用戶磁盤進行了配額限制。如果 chown(2) 這件事任何人都可以做,那么當 A 用戶的磁盤配額不夠用的時候,他就可以創建一個具有 0777 權限的文件夾,然后將這個文件夾的所有者修改為 B 用戶,這樣自己依然可以使用這個文件夾,但是卻繞過了磁盤配額的限制,將帳算在了 B 用戶的頭上。
所以為了安全起見, chown(2) 只能由超級用戶來做,普通用戶不僅無法修改別人文件的所有者,也無權修改自己文件的所有者。
?
10.truncate(2)
1 truncate, ftruncate - truncate a file to a specified length 2 3 #include <unistd.h> 4 #include <sys/types.h> 5 6 int truncate(const char *path, off_t length); 7 int ftruncate(int fd, off_t length);?
truncate(2) 函數族的文件用于截斷 path 所指定的文件到 length 個字節。
如果想要清空一個文件,可以在 open(2) 的時候指定 flags 為 O_TRUNC。
如果 length 參數小于文件之前的長度,則 length 個字節后面的數據將被丟棄。
如果 length 參數大于文件之前的長度,則在文件末尾用 '\0' 填充,使文件達到 length 指定的長度,也就是在文件的尾部創建了一個空洞。
?
11.古老的 UFS 文件系統
FAT 是大家所熟悉的 Dos 文件系統,它是順序存儲的單鏈表結構,所以單鏈表的缺點就是 FAT 文件系統的缺點。只能從前往后訪問,不能反向訪問,而且無法管理大文件。
UFS 文件系統是一個與 FAT 同時代的 Unix 文件系統,但是 UFS 文件系統卻是與 FAT 完全不同的文件系統,它可以很好的支持大文件,但小文件的管理卻是它的弱點。
下面我們介紹一下 UFS 文件系統。
目錄也是一個文件, 它存儲的內容是一個個的目錄項,而每一個目錄項記錄的是目錄中文件的 inode 和文件名等信息,如圖 3 所示。
圖3 目錄
所以目錄中并不是存儲文件的數據,而是存儲文件的信息。
而 UFS 文件系統是如何管理磁盤的呢?見下圖,圖4 是參照 APUE 第三版 91 頁圖4-13 畫的。
圖4 磁盤、分區和文件系統
畫圖工具無法輸入中文,只能畫成英文的了,而且畫上去的東西撤銷掉有時候會留下痕跡,我也是醉了。。
上圖中的藍框部分是我要詳細介紹的部分。什么?你說你看不到藍框?好吧,圖貼上去才發現藍框不明顯,我錯了,但是我不想用這個讓我抓狂的工具重新畫一次了。藍框就是倒數第二行的那個框。
下面我把藍框的部分單獨畫一幅圖出來詳細說明一下,它是每一個柱面組的組成部分,柱面組就是上圖第二行的 cylinder。
圖5 柱面組
想當年小時候在紙上畫畫的時候我就經常被墨水弄得手上、紙上都臟兮兮的一片,沒想到 N 年后用了這款逼(cao)真(dan)的畫圖軟件,居然在電腦上能畫得跟在紙上畫得一樣臟兮兮的。。
現在我解釋一下圖上畫的都是什么東西。
上面橫著的表格:
data block:文件數據的邏輯存儲塊,每個塊的大小是 4KB 的倍數。
data block bitmap:這是一個位圖,為了提高數據塊的查找速度而設計的。位圖中的每一位對應 data block 中的一個塊,當某一位為 1 時表示對應的塊被占用,為 0 時表示對應的塊可用。
inode:包含文件的所有信息,除了文件名,因為文件名是包含在目錄文件中的。
inode bitmap:與 block 位圖一樣,也是為了提高 inode 的訪問速度而設計的。位圖中的每一位對應 inode 區域的一塊數據,當某一位為 1 時表示對應的 inode 被占用,為 0 時表示對應的 inode 可用。
圖下半部分的表格:
inode:inode 中記錄數據存儲位置的部分。是一個包含 15 個磁盤塊地址的數組,其中前 12 個地址是直接地址,它們直接指向 data block 的數據塊。如果使用 12 個數據邏輯存儲塊無法容納下一個文件,那么就會啟用數組中第 13 個元素,它不直接指向數據塊,而是指向一級間接塊的。
1 level indirect block:一級間接塊。包含 256 個磁盤塊地址的數組,每個元素指向 data block 區域的一個數據邏輯存儲塊。如果依然無法容納下一個文件,那么將啟用 inode 中的第14個元素,它指向二級間接塊。
2 level indirect block:二級間接塊。包含 256 個磁盤塊地址的數組,每個元素指向一個一級間接塊數組。如果依然無法容納下一個文件,那么將啟用 inode 中的第15個元素,它指向三級間接塊。
3 level indirect block:三級間接塊。包含 256 個磁盤塊地址的數組,每個元素指向一個二級間接塊數組。
?
這回知道為什么 UFS 文件系統不懼怕大文件了吧?但是——UFS 文件系統不善于管理小文件,這是為什么呢?
答案其實很簡單,因為文件系統中數據塊的數量遠比 inode 塊多得多,所以一個 inode 塊才能夠對應多個 data block。而如果小文件太多,會導致在 data block 還有大量剩余空間的情況下就把 inode 耗盡了。
有些人認為 UFS 無法管理小文件,其實這是不正確的,只是 UFS 的設計策略使它不善于管理小文件而已。
現在知道為什么同時代的文件系統,*nix 的要遠比 win 的先進了吧。
?
關于上面這幅圖我有一點需要補充說明一下,圖畫得有不明確的地方:
左側 inode 數組中第 12 個元素指向的右側粉色標識的一級間接塊數組,而圖上中間紫色標識的二級間接塊數組中的成員也指向粉色的一級間接塊數組。其實紫色二級間接塊數組中的成員并非同樣指向了 inode 數組中第 12 個元素所指向的位置,圖上的意思是二級間接塊數組中的每個成員都指向了一個一級間接塊數組,而并非是指向了與 inode[12] 的指向相同的地址!左側黃色標識的三級間接塊同理。
?
12.函數 link(2)、unlink(2) 和 remove(3)
1 link - make a new name for a file 2 3 #include <unistd.h> 4 5 int link(const char *oldpath, const char *newpath); 6 7 8 unlink - delete a name and possibly the file it refers to 9 10 #include <unistd.h> 11 12 int unlink(const char *pathname); 13 14 15 remove - remove a file or directory 16 17 #include <stdio.h> 18 19 int remove(const char *pathname);?
Linux 系統中 ln(1) 命令是用來創建文件鏈接的。
ln 產生硬鏈接。
ln -s 產生符號鏈接,s 是 symbol,不是 soft,所以不是軟鏈接而是符號鏈接。
硬鏈接和符號鏈接有什么區別呢?
硬鏈接的 inode 號沒有改變,inode 號是文件的唯一標識。所以創建硬鏈接沒有產生新文件,硬鏈接就是目錄項的同義詞,實際上就是在當前的目錄項上多寫了一條記錄。
硬鏈接不能跨分區,不能為目錄文件建立硬鏈接。
符號鏈接產生了新的 inode 號,說明產生了新的文件,但并不分配磁盤塊(block)。
符號鏈接可以跨分區,可以為目錄文件建立符號鏈接。
link(2)、unlink(2) 函數用于創建和刪除符號鏈接。
remove(2) 相當于 rm(1) 命令,它是使用 unlink(2) 、rmdir(2) 函數封裝的。它在刪除文件的時候其實并沒有立即將文件的數據塊從磁盤上移除,而是在被刪除的文件沒有任何進程引用的時候才將它的數據塊釋放。
?
13.rename(2)
1 rename - change the name or location of a file 2 3 #include <stdio.h> 4 5 int rename(const char *oldpath, const char *newpath);?
rename(2) 函數用于重命名文件或目錄。
?
14.utime(2)
1 utime - change file last access and modification times 2 3 #include <sys/types.h> 4 #include <utime.h> 5 6 int utime(const char *filename, const struct utimbuf *times);?
utime(2) 函數用于修改文件的最后訪問時間戳和最后修改時間戳。
個人感覺這個函數沒什么實際用途,除非是在干壞事的時候想要擦除腳印,避免被別人發現。否則有什么理由能去修改一個文件的最后訪問時間和最后修改時間呢?
?
15.mkdir(2)、rmdir(2)
1 mkdir - create a directory 2 3 #include <sys/stat.h> 4 #include <sys/types.h> 5 6 int mkdir(const char *pathname, mode_t mode); 7 8 9 rmdir - delete a directory 10 11 #include <unistd.h> 12 13 int rmdir(const char *pathname);?
與 mkdir(1) 和 rmdir(1) 命令一樣,用于創建和刪除目錄。但是 rmdir(2) 只能刪除空白目錄,如果想要刪除非空目錄需要自行遞歸實現。
?
16.chdir(2)
1 chdir, fchdir - change working directory 2 3 #include <unistd.h> 4 5 int chdir(const char *path); 6 int fchdir(int fd);?
用于改變進程的工作目錄,參數 path 和 fd 表示要修改到的目標目錄。cd(1) 命令就是用這個函數封裝的。這個函數在后面我們講守護進程的時候會再次用到。
有人認為 chdir(2) 這個函數接口不應該被公開,因為它可以穿透假根技術(chroot(1)),帶來一定的安全隱患。
?
17.getcwd(3)
1 getcwd - get current working directory 2 3 #include <unistd.h> 4 5 char *getcwd(char *buf, size_t size);?
用于獲取進程當前工作路徑的絕對路徑,pwd(1) 命令是用該函數封裝的。
?
18.讀目錄
所謂讀目錄其實是讀取出目錄中的文件列表,對目錄的訪問分為兩種方式:glob(3) 和 xxxxdir(3) 函數族。
我們先來說說使用 glob(3) 的方式訪問目錄。
1 glob, globfree - find pathnames matching a pattern, free memory from glob() 2 3 #include <glob.h> 4 5 int glob(const char *pattern, int flags, 6 int (*errfunc) (const char *epath, int eerrno), 7 glob_t *pglob); 8 void globfree(glob_t *pglob);?
glob(3) 函數參數列表
pattern:匹配文件路徑的表達式,可以是通配符。比如輸入 "/home/*",glob(3) 函數會解析到 /home/ 目錄下所有的文件列表。
flags:特殊要求。
很多函數都支持特殊要求的設置,在遇到可以設定特殊要求的函數時,如果沒有特殊要求,如果它是一個位圖就傳入0,如果它是一個指針就傳入 NULL。
glob(3) 函數有很多特殊要求,常用的只有以下幾個:
GLOB_NOSORT:不排序,解析到哪個就是哪個。默認是排序的,使用這個選項可以提高效率。
GLOB_APPEND:將多次調用 glob(3) 解析到的結果追加到一起。但是第一次調用的時候不能傳入這個參數,否則會導致 glob(3) 動態內存分配失敗。
GLOB_NOCHECK:不檢查是否匹配到文件,與?GLOB_APPEND 參數一同使用時,可以把 pglob?參數看作是一個像 argv 一樣的可變長數組,可以存儲任何字符串。
errfunc:當 glob(3) 函數出錯的時候的回調函數,如果不需要異常處理可以傳入 NULL。
pglob:將解析的結果回填到這個參數中。
1 typedef struct { 2 size_t gl_pathc; /* Count of paths matched so far */ 3 char **gl_pathv; /* List of matched pathnames. */ 4 size_t gl_offs; /* Slots to reserve in gl_pathv. */ 5 } glob_t;?大家看 gl_pathc 和 gl_pathv 有木有覺得眼熟?是不是跟 argc 和 argv 很像?其實它們的用法和很相似。
gl_pathv 中存放的是根據 pattern 參數解析出來的所有文件列表的路徑,而 gl_pathc 就是 gl_pathv 的數量。
一個目錄中存放的文件的數量是不確定的,所以 gl_pathv 中的成員數量一定也是不確定的,那么它一定用到了 malloc(3) 函數族使用動態內存來保存解析出來的數據。根據“誰申請,誰釋放”的原則, glob(3) 還提供了一個釋放它所申請的資源的函數:globfree(3)。
globfree(3) 函數的使用就很明了了,將 glob(3) 函數產生在?pglob 參數中所申請的內存釋放掉。
下面寫個簡單的栗子,用于獲取某個目錄下所有文件占用磁盤的空間(blocks / 2),模仿 du(1) 命令的實現。du(1) 命令是做了權限處理的,而我們的 mydu 沒有處理文件權限,所以得出的結果可能略有差異。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <glob.h> 4 #include <unistd.h> 5 #include <string.h> 6 7 #include <sys/types.h> 8 #include <sys/stat.h> 9 10 #define BUFSIZE 1024 11 12 int path_noloop (const char *path) { 13 // 去除 . 和 .. 14 char *pos = strrchr(path, '/'); 15 if (pos) { 16 if ((!strcmp("/.", pos)) 17 || (!strcmp("/..", pos))) { 18 return 0; 19 } 20 } else if ((!strcmp(".", path)) || (!strcmp("..", path))) { 21 return 0; 22 } 23 24 return 1; 25 } 26 27 int mydu(const char *path) 28 {// /a/b/c/d/f/g 29 static char str[BUFSIZE] = ""; 30 glob_t globt; 31 int i = 0, ret = 0; 32 struct stat buf; 33 34 lstat(path, &buf); 35 36 // path為目錄文件 37 if (S_ISDIR(buf.st_mode)) 38 { 39 // 非隱藏文件 40 snprintf(str, BUFSIZE, "%s/*", path); 41 glob(str, 0, NULL, &globt); 42 43 // 隱藏文件,將兩次解析的結果追加到一塊,所以特殊要求使用 GLOB_APPEND 44 snprintf(str, BUFSIZE, "%s/.*", path); 45 glob(str, GLOB_APPEND, NULL, &globt); 46 47 ret = buf.st_blocks; 48 49 for (i = 0; i < globt.gl_pathc; i++) { 50 // 遞歸目錄的時候要注意,目錄并不是一個典型的樹狀結構,它是具有回路的,所以向下遞歸時遇到 . 和 .. 的時候不要進行遞歸 51 if (path_noloop(globt.gl_pathv[i])) { 52 ret += mydu(globt.gl_pathv[i]); 53 } 54 } 55 56 // 用完了不要忘記釋放資源 57 globfree(&globt); 58 } else { // path 為非目錄文件 59 ret = buf.st_blocks; 60 } 61 62 return ret; 63 } 64 65 int main(int argc, char **argv) 66 { 67 printf("%d\n",mydu(argv[1]) / 2); 68 69 exit(0); 70 } 71這里補充一點關于遞歸的優化:
遞歸會將在代碼段定義的局部變量重復的壓棧,所以減少局部變量的數量和占用的空間則可以增加壓棧的層數。
所以只在遞歸點之前或遞歸點之后使用的局部變量可以定義為 static,這樣可以節省每次遞歸棧幀的空間;橫跨遞歸點的變量不能定義為 static。
?
?
訪問目錄的第二種方式是通過 xxxxdir(3) 函數族的函數。下面我們一一介紹它們。
1 opendir, fdopendir - open a directory 2 3 #include <sys/types.h> 4 #include <dirent.h> 5 6 DIR *opendir(const char *name); 7 DIR *fdopendir(int fd); 8 9 10 readdir - read a directory 11 12 #include <dirent.h> 13 14 struct dirent *readdir(DIR *dirp); 15 16 17 closedir - close a directory 18 19 #include <sys/types.h> 20 #include <dirent.h> 21 22 int closedir(DIR *dirp); 23 24 25 telldir - return current location in directory stream 26 27 #include <dirent.h> 28 29 long telldir(DIR *dirp); 30 31 32 seekdir - set the position of the next readdir() call in the directory stream. 33 34 #include <dirent.h> 35 36 void seekdir(DIR *dirp, long loc);看到它們的名字,是不是覺得和文件的訪問函數族很像?其實他們的使用比文件的操作簡單得多。
opendir(3) 函數用于打開一個目錄,并返回一個指向目錄的結構體指針:DIR。
readdir(3) 函數用于讀取目錄項。每次調用會返回一個目錄項,循環調用就可以讀取出一個目錄中的所有目錄項,返回 NULL 表示目錄中的目錄項讀取完畢了。
注意系統調用也有一個 readdir(2) 函數,不要弄混了喲,這里說的是標準庫的 readdir(3)。
closedir(3) 函數用于回收資源,它和 opendir(3) 函數是成對使用的。在前兩篇博文中學習各種 IO 的過程中,相信小伙伴們對于這種方式出現的函數已經不陌生了。
而 telldir(3) 和 seekdir(3) 函數是用來定位目錄項位置指針的,使用場景很少。
下面這個栗子也是模仿 du(1) 命令,運行結果和上面的栗子是一樣的,不過這次是用 xxxxdir(3) 函數族的函數實現的。
1 #define NEWPATHSIZE 1024 2 3 int64_t mydudir (const char *path) 4 { 5 int64_t sum = 0; 6 struct stat buf; 7 struct dirent *de; 8 struct stat subbuf; 9 char newpath[NEWPATHSIZE]; 10 11 if (lstat(path, &buf) < 0) 12 { 13 perror("lstat"); 14 exit(1); 15 } 16 17 if (S_ISDIR(buf.st_mode)) // 文件夾 18 { 19 sum = buf.st_blocks; 20 DIR *dirp = opendir(path); 21 if (NULL == dirp) 22 { 23 perror("opendir"); 24 //exit(1); 25 return sum; 26 } 27 28 while (NULL != (de = readdir(dirp))) 29 { 30 if (NULL != de) 31 { 32 snprintf(newpath, NEWPATHSIZE, "%s/%s", path, de->d_name); 33 if (DT_DIR == de->d_type) // 文件夾 34 { 35 if (strcmp(".", de->d_name) && strcmp("..", de->d_name)) 36 { 37 strncat(newpath, "/", NEWPATHSIZE); 38 sum += mydudir(newpath); 39 } 40 } 41 else // 文件 42 { 43 if (lstat(newpath, &subbuf) < 0) 44 { 45 perror("lstat-sub"); 46 //exit(1); 47 return sum; 48 } 49 sum += subbuf.st_blocks; 50 } 51 } 52 } 53 closedir(dirp); 54 } 55 else // 文件 56 { 57 sum = buf.st_blocks; 58 } 59 60 return sum; 61 }?
?
?
好了,到這里文件和目錄部分也結束了,有什么問題歡迎小伙伴們在評論中進行討論。
總結
以上是生活随笔為你收集整理的(三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ColorUI开发手册(适用于后端同学使
- 下一篇: Linux 系统应用编程——进程间通信(