普通文件類型
理解了文件系統(tǒng)的結(jié)構(gòu)之后,我們來看一下文件的類型。
Linux以文件的形式對計(jì)算機(jī)中的數(shù)據(jù)和硬件資源進(jìn)行管理,也就是徹底的一切皆文件,反映在Linux的文件類型上就是:**普通文件、目錄文件(也就是文件夾)、設(shè)備文件、鏈接文件、管道文件、套接字文件(數(shù)據(jù)通信的接口)**等等。而這些種類繁多的文件被Linux使用目錄樹進(jìn)行管理, 所謂的目錄樹就是以根目錄(/)為主,向下呈現(xiàn)分支狀的一種文件結(jié)構(gòu)。
普通文件
從Linux的角度來說,類似mp4、pdf、html這樣應(yīng)用層面上的文件類型都屬于普通文件,Linux用戶可以根據(jù)訪問權(quán)限對普通文件進(jìn)行查看、更改和刪除。我們知道,文件的屬性,權(quán)限,大小,占用那些數(shù)據(jù)塊是存在inode當(dāng)中的。所以,這里注意一點(diǎn),inode 當(dāng)中并沒有存放文件名,至于為什么,我們接下來看目錄文件。
目錄文件
本質(zhì)上來說,目錄頁是文件,目錄文件inode除了存放一些目錄的權(quán)限,等屬性之外,目錄文件的內(nèi)容則是該目錄文件下文件名和其inode編號的一個(gè)映射關(guān)系。最簡單的保存格式就是列表,就是一項(xiàng)一項(xiàng)地將目錄下的文件信息(如文件名、文件 inode、文件類型等)列在表里。
文件目錄塊:

通常,第一項(xiàng)是「.」,表示當(dāng)前目錄,第二項(xiàng)是「…」,表示上一級目錄,接下來就是一項(xiàng)一項(xiàng)的文件名和 inode。
如果一個(gè)目錄有超級多的文件,我們要想在這個(gè)目錄下找文件,按照列表一項(xiàng)一項(xiàng)地找,效率就不高了。
于是,保存目錄的格式改成哈希表,對文件名進(jìn)行哈希計(jì)算,把哈希值保存起來,如果我們要查找一個(gè)目錄下面的文件名,可以通過名稱取哈希。如果哈希能夠匹配上,就說明這個(gè)文件的信息在相應(yīng)地塊里面。
Linux 系統(tǒng)的 ext 文件系統(tǒng)就是采用了哈希表,來保存目錄的內(nèi)容,這種方法的優(yōu)點(diǎn)是查找非常迅速,插入和刪除也比較簡單,不過需要一些預(yù)備措施來避免哈希沖突。
目錄查詢是通過在磁盤上反復(fù)搜索完成,需要不斷地進(jìn)行 I/O 操作,開銷較大。所以,為了減少 I/O 操作,把當(dāng)前使用的文件目錄緩存在內(nèi)存,以后要使用該文件時(shí)只要在內(nèi)存中操作,從而降低了磁盤操作次數(shù),提高了文件系統(tǒng)的訪問速度。
文件inode

文件操作
文件鏈接
- 硬鏈接
一般情況下,文件名和inode號碼是"一一對應(yīng)"關(guān)系,每個(gè)inode號碼對應(yīng)一個(gè)文件名。但是,Unix/Linux系統(tǒng)允許,多個(gè)文件名指向同一個(gè)inode號碼。
這意味著,可以用不同的文件名訪問同樣的內(nèi)容;對文件內(nèi)容進(jìn)行修改,會(huì)影響到所有文件名;但是,刪除一個(gè)文件名,不影響另一個(gè)文件名的訪問。這種情況就被稱為"硬鏈接"(hard link)。
其實(shí)原理很簡單,我們會(huì)在某個(gè)目錄下創(chuàng)建一個(gè)文件名,這個(gè)文件名和硬鏈接的文件inode 相同,并且會(huì)在這個(gè)inode的記錄中增加鏈接數(shù)量。
我們看到的就是兩個(gè)鏈接到同一個(gè)inode 的文件其實(shí)是一個(gè)文件和inode的映射。

ln命令可以創(chuàng)建硬鏈接:
root@CentOS7 lnDemo]# ln source source_ln
[root@CentOS7 lnDemo]# ls -ali
總用量 8
33575032 drwxr-xr-x. 2 root root 37 9月 30 17:35 .
33574977 dr-xr-x---. 4 root root 161 9月 30 17:34 ..
33575033 -rw-r--r--. 2 root root 7 9月 30 17:31 source
33575033 -rw-r--r--. 2 root root 7 9月 30 17:31 source_ln
[root@CentOS7 lnDemo]# stat source
文件:"source"
大?。? 塊:8 IO 塊:4096 普通文件
設(shè)備:fd00h/64768d Inode:33575033 硬鏈接:2
權(quán)限:(0644/-rw-r--r--) Uid:( 0/ root) Gid:( 0/ root)
環(huán)境:unconfined_u:object_r:admin_home_t:s0
最近訪問:2020-09-30 17:34:54.975711266 +0800
最近更改:2020-09-30 17:31:49.124074625 +0800
最近改動(dòng):2020-09-30 17:35:05.665345071 +0800
創(chuàng)建時(shí)間:-
[root@CentOS7 lnDemo]# stat source_ln
文件:"source_ln"
大?。? 塊:8 IO 塊:4096 普通文件
設(shè)備:fd00h/64768d Inode:33575033 硬鏈接:2
權(quán)限:(0644/-rw-r--r--) Uid:( 0/ root) Gid:( 0/ root)
環(huán)境:unconfined_u:object_r:admin_home_t:s0
最近訪問:2020-09-30 17:34:54.975711266 +0800
最近更改:2020-09-30 17:31:49.124074625 +0800
最近改動(dòng):2020-09-30 17:35:05.665345071 +0800
創(chuàng)建時(shí)間:-
根據(jù)我們上面的研究,我們發(fā)現(xiàn),硬鏈接有以下幾個(gè)問題:
- 目錄不允許硬鏈接
如果目錄允許硬鏈接,那么我們完全可以將兩個(gè)目錄鏈接起來,那么操作系統(tǒng)則在找文件的時(shí)候,就會(huì)在兩個(gè)目錄跳來跳去,形成死循環(huán)。
[root@CentOS7 ~]# ln lnDemo/ ./lnDemo2
ln: "lnDemo/": 不允許將硬鏈接指向目錄
[root@CentOS7 ~]#
- 不同分區(qū)不允許硬鏈接
由于硬鏈接是在本分區(qū)指向相同的inode,那么就意味著inode的命名空間需要一致,但是不同的分區(qū),inode的編號將會(huì)重置,所有不能通過inode映射同一個(gè)文件。

- 軟連接
而軟連接則不同,當(dāng)創(chuàng)建軟連接的時(shí)候,linux確實(shí)已經(jīng)創(chuàng)建了一個(gè)inode 和起對應(yīng)來的data block,只不過,在data block存放的是字符串,字符串的內(nèi)容則是 鏈接文件的地址。
[root@CentOS7 lnDemo]# ln -s source source_sln
[root@CentOS7 lnDemo]# ls
source source_sln
[root@CentOS7 lnDemo]# ls -alt
總用量 4
drwxr-xr-x. 2 root root 38 9月 30 18:08 .
lrwxrwxrwx. 1 root root 6 9月 30 18:08 source_sln -> source
dr-xr-x---. 5 root root 172 9月 30 17:55 ..
-rw-r--r--. 1 root root 7 9月 30 17:31 source
[root@CentOS7 lnDemo]# ls -ailt
總用量 4
33575032 drwxr-xr-x. 2 root root 38 9月 30 18:08 .
33575034 lrwxrwxrwx. 1 root root 6 9月 30 18:08 source_sln -> source
33574977 dr-xr-x---. 5 root root 172 9月 30 17:55 ..
33575033 -rw-r--r--. 1 root root 7 9月 30 17:31 source
我們發(fā)現(xiàn) source_sln 的文件類型為 l ,且inode和source 不同。大小很小,原因就是我們存放的是地址字符。

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【865977150】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!!

文件新建(復(fù)制)
- (1).讀取GDT,找到各個(gè)(或部分)塊組imap中未使用的inode號,并為待存儲(chǔ)文件分配inode號;
- ((2).在inode table中完善該inode號所在行的記錄;
- ((3).在目錄的data block中添加一條該文件的相關(guān)記錄;
- ((4).將數(shù)據(jù)填充到data block中。
注意,填充到data block中的時(shí)候會(huì)調(diào)用block分配器:一次分配4KB大小的block數(shù)量,當(dāng)填充完4KB的data block后會(huì)繼續(xù)調(diào)用block分配器分配4KB的block,然后循環(huán)直到填充完所有數(shù)據(jù)。也就是說,如果存儲(chǔ)一個(gè)100M的文件需要調(diào)用block分配器100*1024/4=25600次。 另一方面,在block分配器分配block時(shí),block分配器并不知道真正有多少block要分配,只是每次需要分配時(shí)就分配,在每存儲(chǔ)一個(gè)data block前,就去bmap中標(biāo)記一次該block已使用,它無法實(shí)現(xiàn)一次標(biāo)記多個(gè)bmap位。這一點(diǎn)在ext4中進(jìn)行了優(yōu)化。
- (5)填充完之后,去inode table中更新該文件inode記錄中指向data block的尋址指針。
文件刪除
刪除文件分為普通文件和目錄文件,知道了這兩種類型的文件的刪除原理,就知道了其他類型特殊文件的刪除方法。
對于刪除普通文件:
- (1)找到文件的inode和data block(根據(jù)前一個(gè)小節(jié)中的方法尋找);
- (1.5) 如果inode的硬鏈接是數(shù)量不是1 ,則將硬鏈接的數(shù)量-1, 否則執(zhí)行真正的刪除。
- (2)將inode table中該inode記錄中的data block指針刪除;
- (3)在imap中將該文件的inode號標(biāo)記為未使用;
- (4)在其所在目錄的data block中將該文件名所在的記錄行刪除,刪除了記錄就丟失了指向inode的指針(實(shí)際上不是真的刪除,直接刪除的話會(huì)在目錄data block的數(shù)據(jù)結(jié)構(gòu)中產(chǎn)生空洞,所以實(shí)際的操作是將待刪除文件的inode號設(shè)置為特殊的值0,這樣下次新建文件時(shí)就可以重用該行記錄);
- (5)將bmap中data block對應(yīng)的block號標(biāo)記為未使用。
對于刪除目錄文件:
- 找到目錄和目錄下所有文件、子目錄、子文件的inode和data block;
- 在imap中將這些inode號標(biāo)記為未使用;將bmap中將這些文件占用的 block號標(biāo)記為未使用;
- 在該目錄的父目錄的data block中將該目錄名所在的記錄行刪除。需要注意的是,刪除父目錄data block中的記錄是最后一步,如果該步驟提前,將報(bào)目錄非空的錯(cuò)誤,因?yàn)樵谠撃夸浿羞€有文件占用。
文件搜索
當(dāng)執(zhí)行"cat /var/log/messages"命令在系統(tǒng)內(nèi)部進(jìn)行了什么樣的步驟呢?
- 找到根文件系統(tǒng)的塊組描述符表所在的blocks,讀取GDT(已在內(nèi)存中)找到inode table的block號。
根文件系統(tǒng)是不需被引用的,因?yàn)樵诓僮飨到y(tǒng)加載到內(nèi)存當(dāng)中的時(shí)候,跟文件系統(tǒng)已經(jīng)存在,其中第inode編號也已經(jīng)注冊到了操作系統(tǒng)內(nèi)核當(dāng)中。根文件系統(tǒng)的GDT早已經(jīng)在內(nèi)存中了,在系統(tǒng)開機(jī)的時(shí)候會(huì)掛載根文件系統(tǒng),掛載的時(shí)候就已經(jīng)將所有的GDT放進(jìn)內(nèi)存中。
- 在inode table的block中定位到根"/“的inode,找出”/"指向的data block。
- 在"/"的datablock中記錄了var目錄名和var的inode號,找到該inode記錄,inode記錄中存儲(chǔ)了指向var的block指針,所以也就找到了var目錄文件的data block。
- 通過var目錄的inode號,可以尋找到var目錄的inode記錄,但是在尋找的過程中,還需要知道該inode記錄所在的塊組以及所在的inode table,所以需要讀取GDT,同樣,GDT已經(jīng)緩存到了內(nèi)存中。
- 在var的data block中記錄了log目錄名和其inode號,通過該inode號定位到該inode所在的塊組及所在的inode table,并根據(jù)該inode記錄找到log的data block。
- 在log目錄文件的data block中記錄了messages文件名和對應(yīng)的inode號,通過該inode號定位到該inode所在的塊組及所在的inode table,并根據(jù)該inode記錄找到messages的data block。
- 最后讀取messages對應(yīng)的datablock。
- 當(dāng)然,在每次定位到inode記錄后,都會(huì)先將inode記錄加載到內(nèi)存中,然后查看權(quán)限,如果權(quán)限允許,將根據(jù)block指針找到對應(yīng)的data block。
文件移動(dòng)
同文件系統(tǒng)下移動(dòng)文件實(shí)際上是修改目標(biāo)文件所在目錄的data block,向其中添加一行指向inode table中待移動(dòng)文件的inode指針,如果目標(biāo)路徑下有同名文件,則會(huì)提示是否覆蓋,實(shí)際上是覆蓋目錄data block中沖突文件的記錄,由于同名文件的inode記錄指針被覆蓋,所以無法再找到該文件的data block,也就是說該文件被標(biāo)記為刪除
所以在同文件系統(tǒng)內(nèi)移動(dòng)文件相當(dāng)快,僅僅在所在目錄data block中添加或覆蓋了一條記錄而已。也因此,移動(dòng)文件時(shí),文件的inode號是不會(huì)改變的。
對于不同文件系統(tǒng)內(nèi)的移動(dòng),相當(dāng)于先復(fù)制再刪除的動(dòng)作。
文件掛載
Linux 系統(tǒng)下,文件是虛擬文件系統(tǒng),當(dāng)我們ls / 的時(shí)候,linux 會(huì)吧所有磁盤,所有分區(qū)下的且掛載在根目錄下的所有目錄列出來。 掛載文件系統(tǒng)到某個(gè)目錄下,例如"mount /dev/cdrom /mnt",掛載成功后/mnt目錄中的文件全都暫時(shí)不可見了,且掛載后權(quán)限和所有者(如果指定允許普通用戶掛載)等的都改變了,知道為什么嗎?
下面就以通過"mount /dev/cdrom /mnt"為例,詳細(xì)說明掛載過程中涉及的細(xì)節(jié)。
在將文件系統(tǒng)/dev/cdrom(此處暫且認(rèn)為它是文件系統(tǒng))掛載到掛載點(diǎn)/mnt之前,掛載點(diǎn)/mnt是根文件系統(tǒng)中的一個(gè)目錄,"/"的data block中記錄了/mnt的一些信息,其中包括inode號inode_n,而在inode table中,/mnt對應(yīng)的inode記錄中又存儲(chǔ)了block指針block_n,此時(shí)這兩個(gè)指針還是普通的指針。

當(dāng)文件系統(tǒng)/dev/cdrom掛載到/mnt上后,/mnt此時(shí)就已經(jīng)成為另一個(gè)文件系統(tǒng)的入口了,因此它需要連接兩邊文件系統(tǒng)的inode和data block。

- 在根文件系統(tǒng)的inode table中,為/mnt重新分配一個(gè)inode記錄m,該記錄的block指針block_m指向文件系統(tǒng)/dev/cdrom中的data block。
- /mnt分配了新的inode記錄m,那么在"/"目錄的data block中,也需要修改其inode指針為inode_m以指向m記錄。
- 同時(shí),原來inode table中的inode記錄n就被標(biāo)記為暫時(shí)不可用。
block_m指向的是文件系統(tǒng)/dev/cdrom的data block,所以嚴(yán)格說起來,除了/mnt的元數(shù)據(jù)信息即inode記錄m還在根文件系統(tǒng)上,/mnt的data block已經(jīng)是在/dev/cdrom中的了。這就是掛載新文件系統(tǒng)后實(shí)現(xiàn)的跨文件系統(tǒng),它將掛載點(diǎn)的元數(shù)據(jù)信息和數(shù)據(jù)信息分別存儲(chǔ)在不同的文件系統(tǒng)上。
掛載完成后,將在/proc/self/{mounts,mountstats,mountinfo}這三個(gè)文件中寫入掛載記錄和相關(guān)的掛載信息,并會(huì)將/proc/self/mounts中的信息同步到/etc/mtab文件中,當(dāng)然,如果掛載時(shí)加了-n參數(shù),將不會(huì)同步到/etc/mtab。
而卸載文件系統(tǒng),其實(shí)質(zhì)是移除臨時(shí)新建的inode記錄(當(dāng)然,在移除前會(huì)檢查是否正在使用)及其指針,并將指針指回原來的inode記錄,這樣inode記錄中的block指針也就同時(shí)生效而找回對應(yīng)的data block了。由于卸載只是移除inode記錄,所以使用掛載點(diǎn)和文件系統(tǒng)都可以實(shí)現(xiàn)卸載,因?yàn)樗鼈兪锹?lián)系在一起的。
下面是分析或結(jié)論。
(1).掛載點(diǎn)掛載時(shí)的inode記錄是新分配的。
掛載前掛載點(diǎn)/mnt的inode號
[root@server2 tmp]# ll -id /mnt
100663447 drwxr-xr-x. 2 root root 6 Aug 12 2015 /mnt
[root@server2 tmp]# mount /dev/cdrom /mnt
# 掛載后掛載點(diǎn)的inode號
[root@server2 tmp]# ll -id /mnt
1856 dr-xr-xr-x 8 root root 2048 Dec 10 2015 mnt
由此可以驗(yàn)證,inode號確實(shí)是重新分配的。
(2).掛載后,掛載點(diǎn)的內(nèi)容將暫時(shí)不可見、不可用,卸載后文件又再次可見、可用。
在掛載前,向掛載點(diǎn)中創(chuàng)建幾個(gè)文件
[root@server2 tmp]# touch /mnt/a.txt
[root@server2 tmp]# mkdir /mnt/abcdir
# 掛載
[root@server2 tmp]# mount /dev/cdrom /mnt
掛載后,掛載點(diǎn)中將找不到剛創(chuàng)建的文件
[root@server2 tmp]# ll /mnt
total 636
-r--r--r-- 1 root root 14 Dec 10 2015 CentOS_BuildTag
dr-xr-xr-x 3 root root 2048 Dec 10 2015 EFI
-r--r--r-- 1 root root 215 Dec 10 2015 EULA
-r--r--r-- 1 root root 18009 Dec 10 2015 GPL
dr-xr-xr-x 3 root root 2048 Dec 10 2015 images
dr-xr-xr-x 2 root root 2048 Dec 10 2015 isolinux
dr-xr-xr-x 2 root root 2048 Dec 10 2015 LiveOS
dr-xr-xr-x 2 root root 612352 Dec 10 2015 Packages
dr-xr-xr-x 2 root root 4096 Dec 10 2015 repodata
-r--r--r-- 1 root root 1690 Dec 10 2015 RPM-GPG-KEY-CentOS-7
-r--r--r-- 1 root root 1690 Dec 10 2015 RPM-GPG-KEY-CentOS-Testing-7
-r--r--r-- 1 root root 2883 Dec 10 2015 TRANS.TBL
卸載后,掛載點(diǎn)/mnt中的文件將再次可見
[root@server2 tmp]# umount /mnt
[root@server2 tmp]# ll /mnt
total 0
drwxr-xr-x 2 root root 6 Jun 9 08:18 abcdir
-rw-r--r-- 1 root root 0 Jun 9 08:18 a.txt
之所以會(huì)這樣,是因?yàn)閽燧d文件系統(tǒng)后,掛載點(diǎn)原來的inode記錄暫時(shí)被標(biāo)記為不可用,關(guān)鍵是沒有指向該inode記錄的inode指針了。在卸載文件系統(tǒng)后,又重新啟用掛載點(diǎn)原來的inode記錄,"/"目錄下的mnt的inode指針又重新指向該inode記錄。
(3).掛載后,掛載點(diǎn)的元數(shù)據(jù)和data block是分別存放在不同文件系統(tǒng)上的。
(4).掛載點(diǎn)即使在掛載后,也還是屬于源文件系統(tǒng)的文件。
文件描述符
先看一段最文件描述符的官方說明
維基百科:文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開文件的記錄表。當(dāng)程序打開一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開。
- 作用
Linux 系統(tǒng)中,把一切都看做是文件,當(dāng)進(jìn)程打開現(xiàn)有文件或創(chuàng)建新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符,文件描述符就是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,用來指向被打開的文件,所有執(zhí)行I/O操作的系統(tǒng)調(diào)用都會(huì)通過文件描述符。
- 概念定義文件描述符 是 用來訪問資源(文件,輸入輸出設(shè)備等)的一種抽象指示符。文件描述符 是POSIX(Portable Operating System Interface)規(guī)范的組成部分文件描述符 通常是非負(fù)整數(shù),C 語言中使用int類型。
- FD 具體可以指向什么文件/目錄 files/directories輸入輸出源 input/output管道 pipes套接字 sockets其他 Unix 文件類型 other Unix files
- 默認(rèn)的fds每一個(gè) Unix 進(jìn)程中,通常會(huì)有三個(gè)預(yù)制的 FD。它們分別是標(biāo)準(zhǔn)輸入 Standard input 標(biāo)準(zhǔn)輸入 用于程序接受數(shù)據(jù)標(biāo)準(zhǔn)輸出 Standard output 標(biāo)準(zhǔn)輸出 用于程序輸出數(shù)據(jù)標(biāo)準(zhǔn)錯(cuò)誤(輸出) Standard error 標(biāo)準(zhǔn)錯(cuò)誤 用于程序輸出錯(cuò)誤或者診斷信息
- 文件描述符的意義 一個(gè) Linux 進(jìn)程啟動(dòng)后,會(huì)在內(nèi)核空間中創(chuàng)建一個(gè) PCB 控制塊,PCB 內(nèi)部有一個(gè)文件描述符表(File descriptor table),記錄著當(dāng)前進(jìn)程所有可用的文件描述符,也即當(dāng)前進(jìn)程所有打開的文件。
除了文件描述符表,系統(tǒng)還需要維護(hù)另外兩張表:
打開文件表(Open file table)
i-node 表(i-node table)
文件描述符表每個(gè)進(jìn)程都有一個(gè),打開文件表和 i-node 表整個(gè)系統(tǒng)只有一個(gè),它們?nèi)咧g的關(guān)系如下圖所示。

文件描述符的意義
首先,為什么不把文件位置干脆存放在索引節(jié)點(diǎn)中,而要多此一舉,設(shè)一個(gè)新的數(shù)據(jù)結(jié)構(gòu)呢?我們知道,Linux中的文件是能夠共享的,假如把文件位置存放在索引節(jié)點(diǎn)中,則如果有兩個(gè)或更多個(gè)進(jìn)程同時(shí)打開同一個(gè)文件時(shí),它們將去訪問同一個(gè)索引節(jié)點(diǎn),于是一個(gè)進(jìn)程的LSEEK操作將影響到另一個(gè)進(jìn)程的讀操作,這顯然是不允許也是不可想象的。
另一個(gè)想法是既然進(jìn)程是通過文件描述符訪問文件的,為什么不用一個(gè)與文件描述符數(shù)組相平行的數(shù)組來保存每個(gè)打開文件的文件位置?這個(gè)想法也是不能實(shí)現(xiàn)的,原因就在于在生成一個(gè)新進(jìn)程時(shí),子進(jìn)程要共享父進(jìn)程的所有信息,包括文件描述符數(shù)組。
我們知道,一個(gè)文件不僅可以被不同的進(jìn)程分別打開,而且也可以被同一個(gè)進(jìn)程先后多次打開。一個(gè)進(jìn)程如果先后多次打開同一個(gè)文件,則每一次打開都要分配一個(gè)新的文件描述符,并且指向一個(gè)新的file結(jié)構(gòu),盡管它們都指向同一個(gè)索引節(jié)點(diǎn),但是,如果一個(gè)子進(jìn)程不和父進(jìn)程共享同一個(gè)file結(jié)構(gòu),而是也如上面一樣,分配一個(gè)新的file結(jié)構(gòu),會(huì)出現(xiàn)什么情況了?讓我們來看一個(gè)例子:
假設(shè)有一個(gè)輸出重定位到某文件A的shell script(shell腳本),我們知道,shell是作為一個(gè)進(jìn)程運(yùn)行的,當(dāng)它生成第一個(gè)子進(jìn)程時(shí),將以0作為A的文件位置開始輸出,假設(shè)輸出了2K的數(shù)據(jù),則現(xiàn)在文件位置為2K。然后,shell繼續(xù)讀取腳本,生成另一個(gè)子進(jìn)程,它要共享shell的file結(jié)構(gòu),也就是共享文件位置,所以第二個(gè)進(jìn)程的文件位置是2K,將接著第一個(gè)進(jìn)程輸出內(nèi)容的后面輸出。如果shell不和子進(jìn)程共享文件位置,則第二個(gè)進(jìn)程就有可能重寫第一個(gè)進(jìn)程的輸出了,這顯然不是希望得到的結(jié)果。
查看文件描述符
lsof(list open files)是一個(gè)查看當(dāng)前系統(tǒng)文件的工具。在linux環(huán)境下,任何事物都以文件的形式存在,通過文件不僅僅可以訪問常規(guī)數(shù)據(jù),還可以訪問網(wǎng)絡(luò)連接和硬件。如傳輸控制協(xié)議 (TCP) 和用戶數(shù)據(jù)報(bào)協(xié)議 (UDP) 套接字等,系統(tǒng)在后臺(tái)都為該應(yīng)用程序分配了一個(gè)文件描述符,該文件描述符提供了大量關(guān)于這個(gè)應(yīng)用程序本身的信息。
lsof打開的文件可以是:
- 普通文件
- 目錄
- 網(wǎng)絡(luò)文件系統(tǒng)的文件
- 字符或設(shè)備文件
- (函數(shù))共享庫
- 管道,命名管道
- 符號鏈接
- 網(wǎng)絡(luò)文件(例如:NFS file、網(wǎng)絡(luò)socket,unix域名socket)
- 還有其它類型的文件,等等
我們用java 新寫一段代碼:
public static void main(String[] args) throws Exception {
String s ="/tmp/file.test";
FileOutputStream fileOutputStream = new FileOutputStream(new File(s));
System.in.read();
}
運(yùn)行上述代碼,并用jps找到其對應(yīng)的pid
利用lsof -i 命令來查看:
...
java 36562 lizhipeng mem REG 253,0 142144 50547 /usr/lib64/libpthread-2.17.so
java 36562 lizhipeng mem REG 253,0 163312 42066 /usr/lib64/ld-2.17.so
java 36562 lizhipeng mem REG 253,0 32768 51151094 /tmp/hsperfdata_lizhipeng/36562
java 36562 lizhipeng 0u CHR 136,4 0t0 7 /dev/pts/4
java 36562 lizhipeng 1u CHR 136,4 0t0 7 /dev/pts/4
java 36562 lizhipeng 2u CHR 136,4 0t0 7 /dev/pts/4
java 36562 lizhipeng 3r REG 253,0 73861866 33613070 /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el7_8.x86_64/jre/lib/rt.jar
java 36562 lizhipeng 4r REG 253,0 1027597 33613060 /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.262.b10-0.el7_8.x86_64/jre/lib/jfr.jar
java 36562 lizhipeng 5w REG 253,0 0 33671497 /tmp/file.test
PCB 進(jìn)程控制塊
為了描述控制進(jìn)程的運(yùn)行,系統(tǒng)中存放進(jìn)程的管理和控制信息的數(shù)據(jù)結(jié)構(gòu)稱為進(jìn)程控制塊(PCB Process Control Block),它是進(jìn)程實(shí)體的一部分,是操作系統(tǒng)中最重要的記錄性數(shù)據(jù)結(jié)構(gòu)。它是進(jìn)程管理和控制的最重要的數(shù)據(jù)結(jié)構(gòu),每一個(gè)進(jìn)程均有一個(gè)PCB,在創(chuàng)建進(jìn)程時(shí),建立PCB,伴隨進(jìn)程運(yùn)行的全過程,直到進(jìn)程撤消而撤消。 在linux中 PCB 用task_struct 數(shù)據(jù)結(jié)構(gòu)來表示
PCB

fs_struct
1、與進(jìn)程相關(guān)的文件
首先,文件必須由進(jìn)程打開,每個(gè)進(jìn)程都有它自己當(dāng)前的工作目錄和它自己的根目錄。task_struct的fs字段指向進(jìn)程的fs_struct結(jié)構(gòu),files字段指向進(jìn)程的files_struct結(jié)構(gòu)。
struct fs_struct {
atomic_t count;
rwlock_t lock;
int umask;
struct dentry * root, * pwd, * altroot;
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};
- count:共享這個(gè)表的進(jìn)程個(gè)數(shù)
- lock:用于表中字段的讀/寫自旋鎖
- umask:當(dāng)打開文件設(shè)置文件權(quán)限時(shí)所使用的位掩碼
- root:根目錄的目錄項(xiàng)
- pwd:當(dāng)前工作目錄的目錄項(xiàng)
files_struct
每個(gè)進(jìn)程用一個(gè) files_struct 結(jié)構(gòu)來記錄文件描述符的使用情況, 這個(gè) files_struct結(jié)構(gòu)稱為用戶打開文件表, 它是進(jìn)程的私有數(shù)據(jù)。 files_struct 結(jié)構(gòu)在include/linux/sched.h 中定義如下:
struct files_struct {
atomic_t count;
struct fdtable *fdt;
struct fdtable fdtab;
int next_fd;
struct embedded_fd_set close_on_exec_init;
struct embedded_fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};

ulimit
ulimit命令可以查看當(dāng)前shell下的文件描述符的數(shù)量。
ulimit 用于限制 shell 啟動(dòng)進(jìn)程所占用的資源,支持以下各種類型的限制:所創(chuàng)建的內(nèi)核文件的大小、進(jìn)程數(shù)據(jù)塊的大小、Shell 進(jìn)程創(chuàng)建文件的大小、內(nèi)存鎖住的大小、常駐內(nèi)存集的大小、打開文件描述符的數(shù)量、分配堆棧的最大大小、CPU 時(shí)間、單個(gè)用戶的最大線程數(shù)、Shell 進(jìn)程所能使用的最大虛擬內(nèi)存。同時(shí),它支持硬資源和軟資源的限制。
作為臨時(shí)限制,ulimit 可以作用于通過使用其命令登錄的 shell 會(huì)話,在會(huì)話終止時(shí)便結(jié)束限制,并不影響于其他 shell 會(huì)話。而對于長期的固定限制,ulimit 命令語句又可以被添加到由登錄 shell 讀取的文件中,作用于特定的 shell 用戶。
語法:
ulimit (選項(xiàng))
選項(xiàng):
-a:顯示目前資源限制的設(shè)定;
-c :設(shè)定core文件的最大值,單位為區(qū)塊;
-d <數(shù)據(jù)節(jié)區(qū)大小>:程序數(shù)據(jù)節(jié)區(qū)的最大值,單位為KB;
-f <文件大小>:shell所能建立的最大文件,單位為區(qū)塊;
-H:設(shè)定資源的硬性限制,也就是管理員所設(shè)下的限制;
-m <內(nèi)存大小>:指定可使用內(nèi)存的上限,單位為KB;
-n <文件數(shù)目>:指定同一時(shí)間最多可開啟的文件數(shù);
-p <緩沖區(qū)大小>:指定管道緩沖區(qū)的大小,單位512字節(jié);
-s <堆疊大小>:指定堆疊的上限,單位為KB;
-S:設(shè)定資源的彈性限制;
-t :指定CPU使用時(shí)間的上限,單位為秒;
-u <程序數(shù)目>:用戶最多可開啟的程序數(shù)目;
-v <虛擬內(nèi)存大小>:指定可使用的虛擬內(nèi)存上限,單位為KB。
實(shí)例:
來看一下具體的用法:
[root@Centos ~]# ulimit -a
core file size (blocks, -c) 0 #core文件的最大值為100 blocks。
data seg size (kbytes, -d) unlimited #進(jìn)程的數(shù)據(jù)段可以任意大。
scheduling priority (-e) 0
file size (blocks, -f) unlimited #文件可以任意大。
pending signals (-i) 3794 #最多有98304個(gè)待處理的信號。
max locked memory (kbytes, -l) 64 #一個(gè)任務(wù)鎖住的物理內(nèi)存的最大值為32KB。
max memory size (kbytes, -m) unlimited #一個(gè)任務(wù)的常駐物理內(nèi)存的最大值。
open files (-n) 1024 #一個(gè)任務(wù)最多可以同時(shí)打開1024的文件。
pipe size (512 bytes, -p) 8 #管道的最大空間為4096字節(jié)。
POSIX message queues (bytes, -q) 819200 #POSIX的消息隊(duì)列的最大值為819200字節(jié)。
real-time priority (-r) 0
stack size (kbytes, -s) 10240 #進(jìn)程的棧的最大值為10240字節(jié)。
cpu time (seconds, -t) unlimited #進(jìn)程使用的CPU時(shí)間。
max user processes (-u) 1024 #當(dāng)前用戶同時(shí)打開的進(jìn)程(包括線程)的最大個(gè)數(shù)為98304。
virtual memory (kbytes, -v) unlimited #沒有限制進(jìn)程的最大地址空間。
file locks (-x) unlimited #所能鎖住的文件的最大個(gè)數(shù)沒有限制。
Linux默認(rèn)的文件打開數(shù)是1024,現(xiàn)在設(shè)置打開數(shù)為2048.
[root@Centos ~]# ulimit -n --查看打開數(shù)為1024
1024
[root@Centos ~]# ulimit -n 2048 --設(shè)置打開數(shù)為2048
[root@Centos ~]# ulimit -n --再次查看
2048
特殊文件類型
- Linux設(shè)備驅(qū)動(dòng)程序工作原理
系統(tǒng)調(diào)用是操作系統(tǒng)內(nèi)核和應(yīng)用程序之間的接口,設(shè)備驅(qū)動(dòng)程序是操作系統(tǒng)內(nèi)核和機(jī)器硬件之間的接口。設(shè)備驅(qū)動(dòng)程序?yàn)閼?yīng)用程序屏蔽了硬件的細(xì)節(jié),這樣在應(yīng)用程序看來,硬件設(shè)備只是一個(gè)設(shè)備文件, 應(yīng)用程序可以象操作普通文件一樣對硬件設(shè)備進(jìn)行操作。設(shè)備驅(qū)動(dòng)程序是內(nèi)核的一部分,運(yùn)行在核心態(tài),它完成以下的功能:
1.對設(shè)備初始化和釋放.
2.把數(shù)據(jù)從內(nèi)核傳送到硬件和從硬件讀取數(shù)據(jù).
3.讀取應(yīng)用程序傳送給設(shè)備文件的數(shù)據(jù)和回送應(yīng)用程序請求的數(shù)據(jù).
4.檢測和處理設(shè)備出現(xiàn)的錯(cuò)誤.
在Linux操作系統(tǒng)下有三類主要的設(shè)備文件類型:字符設(shè)備、塊設(shè)備和網(wǎng)絡(luò)接口。 字符設(shè)備和塊設(shè)備的主要區(qū)別是:在對字符設(shè)備發(fā)出讀/寫請求時(shí),實(shí)際的硬件I/O一般就緊接著發(fā)生了塊設(shè)備則不然,它利用一塊系統(tǒng)內(nèi)存作緩沖區(qū),當(dāng)用戶進(jìn)程對設(shè)備請求能滿足用戶的要求,就返回請求的數(shù)據(jù),如果不能,就調(diào)用請求函數(shù)來進(jìn)行實(shí)際的I/O操作。這也就是進(jìn)程管理的Page cache的作用,塊設(shè)備是主要針對磁盤等慢速設(shè)備設(shè)計(jì)的,以免耗費(fèi)過多的CPU時(shí)間來等待。
換句話說, 當(dāng)發(fā)生塊設(shè)備的IO的時(shí)候, 操作系統(tǒng)實(shí)際是先寫到Page cache上,而 pageCashe 會(huì)有一個(gè)映射規(guī)則,映射到某個(gè)塊設(shè)備的具體地址,在發(fā)生操作以系統(tǒng)的IO的時(shí)候,比如說 寫某個(gè)文件 當(dāng)我們點(diǎn)擊保存的時(shí)候,實(shí)際是寫到了Page cache上, 此時(shí)操作系統(tǒng)將當(dāng)前pageCash標(biāo)記為臟頁,之后如何將臟頁刷新會(huì)磁盤就要看各個(gè)操作系統(tǒng)策略了。
字符設(shè)備、塊設(shè)備
每個(gè)設(shè)備文件都有其文件屬性(c/b),表示是字符設(shè)備還是塊設(shè)備, 另外每個(gè)文件都有兩個(gè)設(shè)備號,第一個(gè)是主設(shè)備號,標(biāo)識驅(qū)動(dòng)程序,第二個(gè)是從設(shè)備號,標(biāo)識使用同一個(gè)設(shè)備驅(qū)動(dòng)程序的不同的硬件設(shè)備,比如有兩個(gè)軟盤,就可以用 從設(shè)備號來區(qū)分他們。設(shè)備文件的的主設(shè)備號必須與設(shè)備驅(qū)動(dòng)程序在登記時(shí)申請的主設(shè)備號一致,否則用戶進(jìn)程將無法訪問到驅(qū)動(dòng)程序。
我可以通過 ls 來看一下
命令:ls -alti
輸出詳解:
# ===================
10605 (inode 編號)
brw-rw----. b 塊設(shè)備, c 字符設(shè)備
1
root (所屬用戶)
cdrom (用戶組)
11,(主設(shè)備號)
0(次設(shè)備號)
10月 12 17:41
sr0 設(shè)備名稱
# ===================
ex:
10445 brw-rw----. 1 root disk 8, 1 10月 12 17:41 sda1
10446 brw-rw----. 1 root disk 8, 2 10月 12 17:41 sda2
10444 brw-rw----. 1 root disk 8, 0 10月 12 17:41 sda
10449 brw-rw----. 1 root disk 8, 16 10月 12 17:41 sdb
8535 crw-------. 1 root root 247, 1 10月 12 17:41 usbmon1
10104 crw-------. 1 root root 246, 0 10月 12 17:41 hidraw0
8550 crw-------. 1 root root 247, 2 10月 12 17:41 usbmon2
- 文件操作的關(guān)鍵結(jié)構(gòu)
由于用戶進(jìn)程是通過設(shè)備文件同硬件打交道,對設(shè)備文件的操作方式不外乎就是一些系統(tǒng)調(diào)用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系統(tǒng)調(diào)用和驅(qū)動(dòng)程序關(guān)聯(lián)起來呢?這需要了解一個(gè)非常關(guān)鍵的數(shù)據(jù)結(jié)構(gòu) file_operations:
struct file_operations {
int (*seek) (struct inode * ,struct file *, off_t ,int);
int (*read) (struct inode * ,struct file *, char ,int);
int (*write) (struct inode * ,struct file *, off_t ,int);
int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);
int (*select) (struct inode * ,struct file *, int ,select_table *);
int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long);
int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);
int (*open) (struct inode * ,struct file *);
int (*release) (struct inode * ,struct file *);
int (*fsync) (struct inode * ,struct file *);
int (*fasync) (struct inode * ,struct file *,int);
int (*check_media_change) (struct inode * ,struct file *);
int (*revalidate) (dev_t dev);
}
這個(gè)結(jié)構(gòu)的每一個(gè)成員的名字都對應(yīng)著一個(gè)系統(tǒng)調(diào)用。用戶進(jìn)程利用系統(tǒng)調(diào)用在對設(shè)備文件進(jìn)行諸如read/write操作時(shí),系統(tǒng)調(diào)用通過設(shè)備文件的主設(shè)備號找到相應(yīng)的設(shè)備驅(qū)動(dòng)程序,然后讀取這個(gè)數(shù)據(jù)結(jié)構(gòu)相應(yīng)的函數(shù)指針,接著把控制權(quán)交給該函數(shù)。這是linux的設(shè)備驅(qū)動(dòng)程序工作的基本原理 這里不再詳細(xì)闡述。
鏈接文件
軟連接文件,詳情請看上文 文件鏈接
管道文件
- 什么是管道?
管道,英文為pipe。這是一個(gè)我們在學(xué)習(xí)Linux命令行的時(shí)候就會(huì)引入的一個(gè)很重要的概念。它的發(fā)明人是道格拉斯.麥克羅伊,這位也是UNIX上早期shell的發(fā)明人。他在發(fā)明了shell之后,發(fā)現(xiàn)系統(tǒng)操作執(zhí)行命令的時(shí)候,經(jīng)常有需求要將一個(gè)程序的輸出交給另一個(gè)程序進(jìn)行處理,這種操作可以使用輸入輸出重定向加文件搞定,比如:
[lizhipeng@CentOS7 ~]$ ls /etc/ > etc.txt
[lizhipeng@CentOS7 ~]$ wc -l etc.txt
但是這樣未免顯得太麻煩了。所以,管道的概念應(yīng)運(yùn)而生。目前在任何一個(gè)shell中,都可以使用“|”連接兩個(gè)命令,shell會(huì)將前后兩個(gè)進(jìn)程的輸入輸出用一個(gè)管道相連,以便達(dá)到進(jìn)程間通信的目的:
[lizhipeng@CentOS7 ~]$ ls -l /etc/ | wc -l
對比以上兩種方法,我們也可以理解為,管道本質(zhì)上就是一個(gè)文件,前面的進(jìn)程以寫方式打開文件,后面的進(jìn)程以讀方式打開。這樣前面寫完后面讀,于是就實(shí)現(xiàn)了通信。實(shí)際上管道的設(shè)計(jì)也是遵循UNIX的“一切皆文件”設(shè)計(jì)原則的,它本質(zhì)上就是一個(gè)文件。Linux系統(tǒng)直接把管道實(shí)現(xiàn)成了一種文件系統(tǒng),借助VFS給應(yīng)用程序提供操作接口。
雖然實(shí)現(xiàn)形態(tài)上是文件,但是管道本身并不占用磁盤或者其他外部存儲(chǔ)的空間。在Linux的實(shí)現(xiàn)上,它占用的是內(nèi)存空間。所以,Linux上的管道就是一個(gè)操作方式為文件的內(nèi)存緩沖區(qū)。
Linux上的管道分兩種類型:
- 匿名管道
- 命名管道
這兩種管道也叫做有名或無名管道。匿名管道最常見的形態(tài)就是我們在shell操作中最常用的”|”。它的特點(diǎn)是只能在父子進(jìn)程中使用,父進(jìn)程在產(chǎn)生子進(jìn)程前必須打開一個(gè)管道文件,然后fork產(chǎn)生子進(jìn)程,這樣子進(jìn)程通過拷貝父進(jìn)程的進(jìn)程地址空間獲得同一個(gè)管道文件的描述符,以達(dá)到使用同一個(gè)管道通信的目的。此時(shí)除了父子進(jìn)程外,沒人知道這個(gè)管道文件的描述符,所以通過這個(gè)管道中的信息無法傳遞給其他進(jìn)程。這保證了傳輸數(shù)據(jù)的安全性,當(dāng)然也降低了管道了通用性,于是系統(tǒng)還提供了命名管道。
我們可以使用mkfifo或mknod命令來創(chuàng)建一個(gè)命名管道,這跟創(chuàng)建一個(gè)文件沒有什么區(qū)別:
[lizhipeng@CentOS7 ~]$ mkfifo pip
[lizhipeng@CentOS7 ~]$ ls
prw-rw-r--. 1 lizhipeng lizhipeng 0 11月 17 13:24 pip
可以看到創(chuàng)建出來的文件類型比較特殊,是p類型。表示這是一個(gè)管道文件。有了這個(gè)管道文件,系統(tǒng)中就有了對一個(gè)管道的全局名稱,于是任何兩個(gè)不相關(guān)的進(jìn)程都可以通過這個(gè)管道文件進(jìn)行通信了。比如我們現(xiàn)在讓一個(gè)進(jìn)程寫這個(gè)管道文件:
[lizhipeng@CentOS7 ~]$ echo xxxxxxxxxxxxxx > pip
此時(shí)這個(gè)寫操作會(huì)阻塞,因?yàn)楣艿懒硪欢藳]有人讀。這是內(nèi)核對管道文件定義的默認(rèn)行為。此時(shí)如果有進(jìn)程讀這個(gè)管道,那么這個(gè)寫操作的阻塞才會(huì)解除:
[lizhipeng@CentOS7 ~]$ cat pip
xxxxxxxxxxxxxx
大家可以觀察到,當(dāng)我們cat完這個(gè)文件之后,另一端的echo命令也返回了。這就是命名管道
接下來我們來看一下匿名管道,我們需要用到 shell 的代碼塊 命令如下
[lizhipeng@CentOS7 ~]$ { echo $BASHPID; read x ; } | { cat ; echo $BASHPID; read y; }
37057
{} 花括號的代碼會(huì)先執(zhí)行,遇到管道后,會(huì)開啟另外一個(gè)進(jìn)程,兩個(gè)進(jìn)程實(shí)現(xiàn)通訊。此時(shí)父進(jìn)程輸出了父進(jìn)程的pid
且阻塞在了read x 這個(gè)代碼塊中,此時(shí)我們可以通過結(jié)果拿到父進(jìn)程的 pid 37057
我們通過pstree來驗(yàn)證一下我們的關(guān)系
[lizhipeng@CentOS7 ~]$ pstree -p
...
─sshd(36387)───bash(36388)─┬─bash(37057+
│ │ └─bash(37058+
│ └─sshd(36713)───sshd(36717)───bash(36718)───pstree(370+
...
我們看到了 37057 進(jìn)程生出了 37058的子進(jìn)程。我們來看一下管道的文件描述符:
[lizhipeng@CentOS7 fd]$ ls -alt /proc/37057/fd
總用量 0
lrwx------. 1 lizhipeng lizhipeng 64 11月 17 13:33 0 -> /dev/pts/4
l-wx------. 1 lizhipeng lizhipeng 64 11月 17 13:33 1 -> pipe:[253711]
lrwx------. 1 lizhipeng lizhipeng 64 11月 17 13:33 2 -> /dev/pts/4
lrwx------. 1 lizhipeng lizhipeng 64 11月 17 13:33 255 -> /dev/pts/4
dr-x------. 2 lizhipeng lizhipeng 0 11月 17 13:33 .
dr-xr-xr-x. 9 lizhipeng lizhipeng 0 11月 17 13:30 ..
[lizhipeng@CentOS7 fd]$ ls -alt /proc/37058/fd
總用量 0
lr-x------. 1 lizhipeng lizhipeng 64 11月 17 13:34 0 -> pipe:[253711]
lrwx------. 1 lizhipeng lizhipeng 64 11月 17 13:34 1 -> /dev/pts/4
lrwx------. 1 lizhipeng lizhipeng 64 11月 17 13:34 2 -> /dev/pts/4
lrwx------. 1 lizhipeng lizhipeng 64 11月 17 13:34 255 -> /dev/pts/4
dr-x------. 2 lizhipeng lizhipeng 0 11月 17 13:34 .
dr-xr-xr-x. 9 lizhipeng lizhipeng 0 11月 17 13:30 ..
由此我們可以看到,37057 通過重定向 1 號文件描述符來講管道 重定向到了 37058 的0號描述符。
這就是匿名管道。
評論