天天看點

3.1.linux中的底層檔案IO

3.1.1.應用程式設計架構介紹

(1)整個嵌入式linux核心課程包括5個點,按照學習順序依次是:裸機、C進階、uboot和系統移植、linux應用程式設計和網絡程式設計、驅動。

(2)典型的嵌入式産品就是基于嵌入式linux作業系統來工作的。典型的嵌入式産品的研發過程就是;第一步讓linux系統在硬體上跑起來(系統移植工作),第二步基于linux系統來開發應用程式實作産品功能。

(3)基于linux去做應用程式設計,其實就是通過調用linux的系統API來實作應用需要完成的任務。

系統調用之底層檔案I/O操作

1、linux常用檔案IO接口

(1)open、close、write、read、lseek

2、檔案操作的一般步驟

(1)在linux系統中要操作一個檔案,一般是先open打開一個檔案,得到一個檔案描述符,然後對檔案進行讀寫操作(或其他操作),最後close關閉檔案即可

(2)強調一點:我們對檔案進行操作時,一定要先打開檔案,打開成功後才能去操作(如果打開本身失敗,後面就不用操作了);最後讀寫完成之後一定要close關閉檔案,否則可能會造成檔案損壞。

(3)檔案平時是存在塊裝置中的檔案系統中的,我們把這種檔案叫靜态檔案。

當我們去open打開一個檔案時,

linux核心做的操作包括:

1.核心在程序中建立了一個打開檔案的資料結構,記錄下我們打開的這個檔案;

2.核心在記憶體中申請一段記憶體,

3.将靜态檔案的内容從塊裝置中讀取到記憶體中特定位址管理存放(叫動态檔案)。

(4)打開檔案後,以後對這個檔案的讀寫操作,都是針對記憶體中這一份動态檔案的,而并不是針對靜态檔案的。

當我們對動态檔案進行讀寫後,此時記憶體中的動态檔案和塊裝置中的靜态檔案就不同步了,

當我們close關閉動态檔案時,close内部核心将記憶體中的動态檔案的内容去更新(同步)塊裝置中的靜态檔案。

(5)常見的一些現象:

第一個:打開一個大檔案時比較慢

第二個:我們寫了一半的檔案,如果沒有點儲存直接關機/斷電,重新開機後檔案内容丢失。

(6)為什麼要這麼設計?

以為塊裝置本身有讀寫限制(回憶NnadFlash、SD等塊裝置的讀寫特征),本身對塊裝置進行操作非常不靈活。

而記憶體可以按位元組為機關來操作,而且可以随機操作(記憶體就叫RAM,random),很靈活。是以核心設計檔案操作時就這麼設計了。

重要概念:檔案描述符

簡介:

​​核心​​(kernel)利用檔案描述符(file descriptor)來通路檔案。

1.檔案描述符是非負整數。打開現存檔案或建立檔案時,核心會傳回一個檔案描述符。讀寫檔案也需要使用檔案描述符來指定待讀寫的檔案。

2.實際上,它是一個索引值,指向​​核心​​​為每一個程序所維護的該程序打開檔案的記錄表。在程式設計中,一些涉及底層的程式編寫往往會圍繞着檔案描述符展開。但是檔案描述符這一概念往往隻适用于​​UNIX​​​、​​Linux​​這樣的作業系統。

3.習慣上,

标準輸入(standard input)的檔案描述符是 0,

标準輸出(standard output)是 1,

标準錯誤(standard error)是 2。

盡管這種習慣并非​​Unix​​核心的特性,但是因為一些 shell 和很多應用程式都使用這種習慣,是以,如果核心不遵循這種習慣的話,很多應用程式将不能使用。

4.檔案描述符的有效範圍是 0 到 OPEN_MAX。一般來說,每個程序最多可以打開 64 個檔案(0 — 63)。對于 FreeBSD 5.2.1、Mac OS X 10.3 和 Solaris 9 來說,每個程序最多可以打開檔案的多少取決于​​系統記憶體​​的大小,int 的大小,以及系統管理者設定的限制。Linux 2.4.22 強制規定最多不能超過 1,048,576 。

5.檔案描述符與包括相關資訊(如檔案的打開模式、檔案的位置類型、檔案的初始類型等)的檔案對象相關聯,這些資訊被稱作檔案的上下文。

如何建立檔案描述符

1.程序擷取檔案描述符最常見的方法是通過本機​​子例程​​​open或create擷取或者通過從​​父程序​​繼承。後一種方法允許子程序同樣能夠通路由父程序使用的檔案。

2.檔案描述符對于每個程序一般是唯一的。

3.當用fork子例程建立某個子程序時,該子程序會獲得其父程序所有檔案描述符的副本,這些檔案描述符在執行fork時打開。在由fcntl、dup和​​dup2​​子例程複制或拷貝某個程序時,會發生同樣的複制過程。

4.對于每個程序,作業系統​​核心​​在u_block結構中維護檔案描述符表,所有的檔案描述符都在該表中建立索引。

缺點:

1.在非UNIX/​​Linux作業系統​​上(如Windows NT),無法基于這一概念進行程式設計。

2.由于檔案描述符在形式上不過是個整數,當代碼量增大時,會使程式設計者難以厘清哪些整數意味着資料,哪些意味着檔案描述符。是以,完成的代碼可讀性也就會變得很差。

檔案描述符與檔案指針的差別

​​javascript:void(0)​​

(1)檔案描述符其實實質是一個數字,這個數字在一個程序中表示一個特定的含義,

當我們open打開一個檔案時,作業系統在記憶體中建構了一些資料結構來表示這個動态檔案,

然後傳回給應用程式一個數字作為檔案描述符,這個數字就和我們記憶體中維護這個動态檔案的這些資料結構挂鈎綁定上了,

以後我們應用程式如果要操作這一個動态檔案,隻需要用這個檔案描述符進行區分。

(2)一句話講清楚檔案描述符:檔案描述符就是用來區分一個程式打開的多個檔案的。

(3)注意:

1.檔案描述符的作用域就是目前程序,出了目前程序這個檔案描述符就沒有意義了

2.不同檔案在不同程序中打開,它的檔案描述符不一樣

3.1.3.一個簡單的檔案讀寫執行個體

3.1.3.1、打開檔案與關閉檔案

(1)linux中的檔案描述符fd的合法範圍是0或者一個正正數,不可能是一個負數。

(2)open傳回的fd程式必須記錄好,以後向這個檔案的所有操作都要靠這個fd去對應這個檔案,最後關閉檔案時也需要fd去指定關閉這個檔案。如果在我們關閉檔案前fd丢掉了那就慘了,這個檔案沒法關閉了也沒法讀寫了。

3.1.3.2、實時查man手冊

(1)當我們寫應用程式時,很多API原型都不可能記得,是以要實時查詢,用man手冊

(2)man 1 xx查linux shell指令,man 2 xxx查API, man 3 xxx查庫函數

3.1.3.3、讀取檔案内容

(1)ssize_t read(int fd, void *buf, size_t count);

fd表示要讀取哪個檔案,fd一般由前面的open傳回得到

buf是應用程式自己提供的一段記憶體緩沖區,用來存儲讀出的内容

count是我們要讀取的位元組數

傳回值ssize_t類型是linux核心用typedef重定義的一個類型(其實就是int),傳回值表示成功讀取的位元組數。

3.1.3.4、向檔案中寫入

(1)寫入用write系統調用,write的原型和了解方法和read相似

(2)注意const在buf前面的作用,結合C語言進階專題中的輸入型參數和輸出型參數一節來了解。

(3)注意buf的指針類型為void,結合C語言進階專題中void類型含義的講解

(4)剛才先寫入12位元組,然後讀出結果讀出是0(但是讀出成功了),這個問題的答案後面章節會講,大家先思考一下。

3.1.4.open函數的flag詳解1

3.1.4.1、讀寫權限:O_RDONLY O_WRONLY O_RDWR

(1)linux中檔案有讀寫權限,我們在open打開檔案時也可以附帶一定的權限說明

O_RDONLY 表示以隻讀方式打開,

O_WRONLY 表示以隻寫方式打開,

O_RDWR 表示以可讀可寫方式打開

(2)當我們附帶了權限後,打開的檔案就隻能按照這種權限來操作。

3.1.4.2、打開存在并有内容的檔案時:

(1)思考一個問題:當我們打開一個已經存在并且内部有内容的檔案時會怎麼樣?

可能結果1:先清空檔案内容,再寫新内容

可能結果2:新内容添加在前面,原來的内容繼續在後面

可能結果3:新内容附加在後面,原來的内容還在前面,一般這種情況不存在

可能結果4:将需要寫入的内容,在原來的檔案中的内容一個一個的覆寫原來的内容

O_TRUNC:

打開檔案時,如果這個檔案中本來是有内容的,則原來的内容會被丢棄。這就對應上面的結果1

O_APPEND

去打開檔案時,如果這個檔案中本來是有内容的,則新寫入的内容會接續到原來内容的後面,對應結果3

預設

不使用O_APPEND和O_TRUNC屬性時就是結果4

O_APPEND | O_TRUNC ?

如果O_APPEND和O_TRUNC同時出現會怎麼樣?

O_TRUNC 覆寫 O_APPEND

3.1.4.3、exit、_exit、_Exit退出程序

(1)當我們程式在前面步驟操作失敗導緻後面的操作都沒有可能進行下去時,應該在前面的錯誤監測中結束整個程式,不應該繼續讓程式運作下去了。

(2)我們如何退出程式?

第一種;在main用return,一般原則是程式正常終止return 0,如果程式異常終止則return -1。

第一種:正式終止程序(程式)應該使用exit或者_exit或者_Exit之一。

​​https://wenku.baidu.com/view/4e999b886529647d27285291.html?from=search​​

3.1.5.open函數的flag詳解2

3.1.5.1、打開不存在的檔案時:O_CREAT、O_EXCL

當我們open打開一個檔案時如果這個檔案名不存在則會打開檔案錯誤。

O_CREAT

open中加入O_CREAT後,不管原來這個檔案存在與否都能打開成功,

如果原來這個檔案不存在則建立一個空的新檔案,

如果原來這個檔案存在則不會有什麼影響,和普通打開方式一樣

O_CREAT | O_EXCL

如果原來這個檔案不存在則建立一個空的新檔案,

如果原來這個檔案存在則會報錯

open("a.txt", O_RDWR | O_CREAT, 0666) 這個數字是8進制

open函數在使用O_CREAT标志去建立檔案時,可以使用第三個參數mode來指定要建立的檔案的權限。mode使用4個數字來指定權限的,其中後面三個很重要,對應我們要建立的這個檔案的權限标志。譬如一般建立一個可讀可寫不可執行的檔案就用0666

3.1.5.2、O_NONBLOCK

O_NONBLOCK 非阻塞

預設 阻塞

如果一個函數是阻塞式的,則我們調用這個函數時目前程序有可能被卡住(阻塞住,實質是這個函數内部要完成的事情條件不具備,目前沒法做,要等待條件成熟),函數被阻塞住了就不能立刻傳回;

如果一個函數是非阻塞式的那麼我們調用這個函數後一定會立即傳回,但是函數有沒有完成任務不一定。

(2)阻塞和非阻塞是兩種不同的設計思路,并沒有好壞。

總的來說,

阻塞式的 結果有保障 但是 時間沒保障

非阻塞式的 時間有保障 但是 結果沒保障

(3)作業系統提供的API和由API封裝而成的庫函數,有很多本身就是被設計為阻塞式或者非阻塞式的,是以我們應用程度調用這些函數的時候心裡得非常清楚。

(4)我們打開一個檔案預設就是阻塞式的,如果你希望以非阻塞的方式打開檔案,則flag中要加O_NONBLOCK标志。

(2)隻用于裝置檔案,而不用于普通檔案。

3.1.5.3、O_SYNC 不要等待,直接寫到硬碟

(1)write阻塞等待底層完成寫入才傳回到應用層。

(2)無O_SYNC時write隻是将内容寫入底層緩沖區即可傳回,然後底層(作業系統中負責實作open、write這些操作的那些代碼,也包含OS中讀寫硬碟等底層硬體的代碼)在合适的時候會将buf中的内容一次性的同步到硬碟中。

這種設計是為了提升硬體操作的性能和銷量,提升硬體壽命;但是有時候我們希望硬體不要等待,直接将我們的内容寫入硬碟中,這時候就可以用O_SYNC标志。

3.1.6.檔案讀寫的一些細節

3.1.6.1、errno和perror

(1)errno就是error number,意思就是錯誤号碼。linux系統中對各種常見錯誤做了個編号,當函數執行錯誤時,函數會傳回一個特定的errno編号來告訴我們這個函數到底哪裡錯了。

(2)errno是由OS來維護的一個全局變量,任何OS内部函數都可以通過設定errno來告訴上層調用者究竟剛才發生了一個什麼錯誤。

(3)errno本身實質是一個int類型的數字,每個數字編号對應一種錯誤。當我們隻看errno時隻能得到一個錯誤編号數字(譬如-37),不适應于人看。

(4)linux系統提供了一個函數perror(意思print error),perror函數内部會讀取errno并且将這個不好認的數字直接給轉成對應的錯誤資訊字元串,然後print列印出來。

3.1.6.2、read和write的count

(1)count和傳回值的關系。count參數表示我們想要寫或者讀的位元組數,傳回值表示實際完成的要寫或者讀的位元組數。實作的有可能等于想要讀寫的,也有可能小于(說明沒完成任務)

(2)count再和阻塞非阻塞結合起來,就會更加複雜。如果一個函數是阻塞式的,則我們要讀取30個,結果暫時隻有20個時就會被阻塞住,等待剩餘的10個可以讀。

我覺得這裡有錯:

因為:我們預設打開的就是阻塞的,但是那麼多次實驗,我們都是讀20個,實際隻有5,但還是讀出來了,沒有等待那15寫入

(3)有時候我們寫正式程式時,我們要讀取或者寫入的是一個很龐大的檔案(譬如檔案有2MB),我們不可能把count設定為2*1024*1024,而應該去把count設定為一個合适的數字(譬如2048、4096),然後通過多次讀取來實作全部讀完。

3.1.6.3、檔案IO效率和标準IO

(1)檔案IO就指的是我們目前在講的open、close、write、read等API函數構成的一套用來讀寫檔案的體系,這套體系可以很好的完成檔案讀寫,但是效率并不是最高的。

為了解決這個效率,就在應用層建立一個buff

(2)應用層C語言庫函數提供了一些用來做檔案讀寫的函數清單,叫标準IO。标準IO由一系列的C庫函數構成(fopen、fclose、fwrite、fread),這些标準IO函數其實是由檔案IO封裝而來的(fopen内部其實調用的還是open,fwrite内部還是通過write來完成檔案寫入的)。标準IO加了封裝之後主要是為了在應用層添加一個緩沖機制,這樣我們通過fwrite寫入的内容不是直接進入核心中的buf,而是先進入應用層标準IO庫自己維護的buf中,然後标準IO庫自己根據作業系統單次write的最佳count來選擇好的時機來完成write到核心中的buf(核心中的buf再根據硬碟的特性來選擇好的實際去最終寫入硬碟中)。

3.1.7.linux系統如何管理檔案

3.1.7.1、硬碟中的靜态檔案和inode(i節點)

(1)檔案平時都在存放在硬碟中的,硬碟中存儲的檔案以一種固定的形式存放的,我們叫靜态檔案。

(2)一塊硬碟中可以分為兩大區域:

一個是硬碟内容管理表項

一個是真正存儲内容的區域

作業系統通路硬碟時是先去讀取硬碟内容管理表,從中找到我們要通路的那個檔案的扇區級别的資訊,然後再用這個資訊去查詢真正存儲内容的區域,最後得到我們要的檔案。

(3)作業系統最初拿到的資訊是檔案名,最終得到的是檔案内容。

第一步就是去查詢硬碟内容管理表,這個管理表中以檔案為機關記錄了各個檔案的各種資訊,每一個檔案有一個資訊清單(我們叫inode,i節點,其實質是一個結構體,這個結構體有很多元素,每個元素記錄了這個檔案的一些資訊,其中就包括檔案名、檔案在硬碟上對應的扇區号、塊号那些東西·····)

強調:硬碟管理的時候是以檔案為機關的,每個檔案一個inode,每個inode有一個數字編号,對應一個結構體,結構體中記錄了各種資訊。

(4)聯系平時實踐,大家格式化硬碟(U盤)時發現有:快速格式化和底層格式化。快速格式化非常快,格式化一個32GB的U盤隻要1秒鐘,普通格式化格式化速度慢。這兩個的差異?其實快速格式化就是隻删除了U盤中的硬碟内容管理表(其實就是inode),真正存儲的内容沒有動。這種格式化的内容是有可能被找回的。

3.1.7.2、記憶體中被打開的檔案和vnode(v節點)

(1)一個程式的運作就是一個程序,我們在程式中打開的檔案就屬于某個程序。

每個程序都有一個資料結構用來記錄這個程序的所有資訊(叫程序資訊表),表中有一個指針會指向一個檔案管理表,檔案管理表中記錄了目前程序打開的所有檔案及其相關資訊。

檔案管理表中用來索引各個打開的檔案的index就是檔案描述符fd,我們最終找到的就是一個已經被打開的檔案的管理結構體vnode

(2)一個vnode中就記錄了一個被打開的檔案的各種資訊,而且我們隻要知道這個檔案的fd,就可以很容易的找到這個檔案的vnode進而對這個檔案進行各種操作。

一個表項就是一個vnode,表項的編号就是fd

vnode用于記憶體表示,Inode用于硬碟表示

3.1.7.3、檔案與流的概念

(1)流(stream)對應自然界的水流。檔案操作中,檔案類似是一個大包裹,裡面裝了一堆字元,但是檔案被讀出/寫入時都隻能一個字元一個字元的進行,而不能一股腦兒的讀寫,那麼一個檔案中N多的個字元被挨個一次讀出/寫入時,這些字元就構成了一個字元流。

(2)流這個概念是動态的,不是靜态的。

(3)程式設計中提到流這個概念,一般都是IO相關的。是以經常叫IO流。檔案操作時就構成了一個IO流。

3.1.8.lseek詳解

3.1.8.1、lseek函數介紹

off_t lseek( int fd, off_t offset, int whence);

whence:

SEEK_SET

The offset is set to offset bytes.

SEEK_CUR

The offset is set to its current location plus offset bytes.

SEEK_END

The offset is set to the size of the file plus offset bytes.

off_t offset:偏移量

傳回值:檔案指針的位置

(1)檔案指針:我認為叫檔案内部指針更好

當我們要對一個檔案進行讀寫時,一定需要先打開這個檔案,是以我們讀寫的所有檔案都是動态檔案。動态檔案在記憶體中的形态就是檔案流的形式。

(2)檔案流很長,裡面有很多個位元組。那我們目前正在操作的是哪個位置?GUI模式下的軟體用光标來辨別這個目前正在操作的位置,這是給人看的。

(3)在動态檔案中,我們會通過檔案指針來表征這個正在操作的位置。所謂檔案指針,就是我們檔案管理表這個結構體裡面的一個指針。是以檔案指針其實是vnode中的一個元素。這個指針表示目前我們正在操作檔案流的哪個位置。這個指針不能被直接通路,linux系統用lseek函數來通路這個檔案指針。

(4)當我們打開一個空檔案時,預設情況下檔案指針指向檔案流的開始。是以這時候去write時寫入就是從檔案開頭開始的。write和read函數本身自帶移動檔案指針的功能,是以當我write了n個位元組後,檔案指針會自動向後移動n位。如果需要人為的随意更改檔案指針,那就隻能通過lseek函數了

(5)read和write函數都是從目前檔案指針處開始操作的,是以當我們用lseek顯式的将檔案指針移動後,那麼再去read/write時就是從移動過後的位置開始的。

(6)回顧前面一節中我們從空檔案,先write寫了12位元組,然後read時是空的(但是此時我們打開檔案後發現12位元組确實寫進來了)。

3.1.8.2、用lseek計算檔案長度

(1)linux中并沒有一個函數可以直接傳回一個檔案的長度。但是我們做項目時經常會需要知道一個檔案的長度,怎麼辦?自己利用lseek來寫一個函數得到檔案長度即可。

ret = lseek( fd, 0, SEEK_END);

3.1.8.3、用lseek建構空洞檔案

(1)空洞檔案就是這個檔案中有一段是空的。

空洞的那一段用亂碼表示:

(2)普通檔案中間是不能有空的,因為我們write時檔案指針是依次從前到後去移動的,不可能繞過前面直接到後面。

(3)我們打開一個檔案後,用lseek往後跳過一段,再write寫入一段,就會構成一個空洞檔案。

(4)空洞檔案方法對多線程共同操作檔案是及其有用的。有時候我們建立一個很大的檔案,如果從頭開始依次建構時間很長。有一種思路就是将檔案分為多段,然後多線程來操作每個線程負責其中一段的寫入。

空洞檔案有什麼用?

空洞檔案作用很大,例如迅雷下載下傳檔案,在未下載下傳完成時就已經占據了全部檔案大小的空間,這時候就是空洞檔案。下載下傳時如果沒有空洞檔案,多線程下載下傳時檔案就都隻能從頭一點一點寫入,這就不是多線程了。如果有了空洞檔案,可以從不同的位址寫入,就完成了多線程的優勢任務。

3.1.9.多次打開同一檔案與O_APPEND

3.1.9.1、重複打開同一檔案讀取

(1)一個程序中兩次打開同一個檔案,然後分别讀取,看結果會怎麼樣

(2)結果無非2種情況:一種是fd1和fd2分别讀,第二種是接續讀。經過實驗驗證,證明了結果是fd1和fd2分别讀。

(3)分别讀說明:我們使用open兩次打開同一個檔案時,fd1和fd2所對應的檔案指針是不同的2個獨立的指針。檔案指針是包含在動态檔案的檔案管理表中的,是以可以看出linux系統的程序中不同fd對應的是不同的獨立的檔案管理表。

3.1.9.2、重複打開同一檔案寫入

(1)一個程序中2個打開同一個檔案,得到fd1和fd2.然後看是分别寫還是接續寫?

(2)正常情況下我們有時候需要分别寫,有時候又需要接續寫,是以這兩種本身是沒有好壞之分的。關鍵看使用者需求

(3)預設情況下應該是:分别寫(實驗驗證過的)

3.1.9.3、加O_APPEND解決覆寫問題

(1)有時候我們希望接續寫而不是分别寫?辦法就是在open時加O_APPEND标志即可

分别寫ab cd

3.1.9.4、O_APPEND的實作原理和其原子操作性說明

(1)O_APPEND為什麼能夠将分别寫改為接續寫?關鍵的核心的東西是檔案指針。

分别寫的内部原理就是2個fd擁有不同的檔案指針,并且彼此隻考慮自己的位移。

但是O_APPEND标志可以讓write和read函數内部多做一件事情,就是移動自己的檔案指針的同時也去把别人的檔案指針同時移動。(也就是說即使加了O_APPEND,fd1和fd2還是各自擁有一個獨立的檔案指針,但是這兩個檔案指針關聯起來了,一個動了會通知另一個跟着動)

加O_APPEND的效果和dup(),dup2()相同

(2)O_APPEND對檔案指針的影響,對檔案的讀寫是原子的。

(3)原子操作的含義是:整個操作一旦開始是不會被其他程序打斷的,必須直到該程序操作結束,

其他程序才能得以排程運作,這就叫原子操作。每種作業系統中都有一些機制來實作原子操作,以保證那些需要原子操作的任務可以運作。

3.1.10.檔案共享的實作方式

3.1.10.1、什麼是檔案共享

(1)檔案共享就是同一個檔案(同一個檔案指的是同一個inode,同一個pathname)被多個獨立的讀寫體(幾乎可以了解為多個檔案描述符)去同時(一個打開尚未關閉的同時另一個去操作)操作。

檔案共享的意義:

1.譬如我們可以通過檔案共享來實作多線程同時操作同一個大檔案,以減少檔案讀寫時間,提升效率。

2.可以實作程序間通信

3.1.10.2、檔案共享的3種實作方式

(1)檔案共享的核心就是怎麼弄出來多個檔案描述符指向同一個檔案。

(2)常見的有3種檔案共享的情況:

第一種是同一個程序中多次使用open打開同一個檔案,

第二種是在不同程序中去分别使用open打開同一個檔案(這時候因為兩個fd在不同的程序中,是以兩個fd的數字可以相同也可以不同),

第三種情況是後面要學的,linux系統提供了dup和dup2兩個API來讓程序複制檔案描述符。

第四種:在一個程序中,直接指定檔案描述符,這種結果和第三種一樣,就是接續寫

fd1 = open(FILENAME, O_RDWR);

fd2 = fd1;

(3)我們分析檔案共享時的核心關注點在于:分别寫/讀還是接續寫/讀

3.1.10.3、再論檔案描述符

(1)檔案描述符的本質是一個數字,這個數字本質上是程序表中檔案描述符表的一個表項,程序通過檔案描述符作為index去索引查表得到檔案表指針,再間接通路得到這個檔案對應的檔案表。

(2)檔案描述符這個數字是open系統調用内部由作業系統自動配置設定的,作業系統配置設定這個fd時也不是随意配置設定,也是遵照一定的規律的,我們現在就要研究這個規律。

(3)作業系統規定,fd從0開始依次增加。fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,是以當時一個程序最多允許打開20個檔案。linux中檔案描述符表是個數組(不是連結清單),是以這個檔案描述符表其實就是一個數組,fd是index,檔案表指針是value

(4)當我們去open時,核心會從檔案描述符表中挑選一個最小的未被使用的數字給我們傳回。也就是說如果之前fd已經占滿了0-9,那麼我們下次open得到的一定是10.(但是如果上一個fd得到的是9,下一個不一定是10,這是因為可能前面更小的一個fd已經被close釋放掉了)

(5)fd中0、1、2已經預設被系統占用了,是以使用者程序得到的最小的fd就是3了。

(6)linux核心占用了0、1、2這三個fd是有用的,當我們運作一個程式得到一個程序時,内部就預設已經打開了3個檔案,這三個檔案對應的fd就是0、1、2。這三個檔案分别叫stdin、stdout、stderr。也就是标準輸入、标準輸出、标準錯誤。

(7)标準輸入一般對應的是鍵盤(可以了解為:0這個fd對應的是鍵盤的裝置檔案),标準輸出一般是LCD顯示器(可以了解為:1對應LCD的裝置檔案)

實驗驗證:close(1)後直接界面不輸出了

但是close(1) 這句代碼以後的程式

的 輸出内容 會輸入到 新打開的檔案中,這個檔案的描述符為1

(8)printf函數其實就是預設輸出到标準輸出stdout上了。stdio中還有一個函數叫fpirntf,這個函數就可以指定輸出到哪個檔案描述符中。

檔案描述符與檔案指針的差別

​​javascript:void(0)​​

3.1.11.檔案描述符的複制1

3.1.11.1、dup和dup2函數介紹

3.1.11.2、使用dup進行檔案描述符複制

(1)dup系統調用對fd進行複制,會傳回一個新的檔案描述符(譬如原來的fd是3,傳回的就是4)

(2)dup系統調用有一個特點,就是自己不能指定複制後得到的fd的數字是多少,而是由作業系統内部自動配置設定的,配置設定的原則遵守fd配置設定的原則。

(3)dup傳回的fd和原來的oldfd都指向oldfd打開的那個動态檔案,操作這兩個fd實際操作的都是oldfd打開的那個檔案。實際上構成了檔案共享。

(4)dup傳回的fd和原來的oldfd同時向一個檔案寫入時,結果是分别寫還是接續寫?

3.1.11.3、使用dup的缺陷分析

(1)dup并不能指定配置設定的新的檔案描述符的數字,dup2系統調用修複了這個缺陷,是以平時項目中實際使用時根據具體情況來決定用dup還是dup2.

3.1.11.4、練習

(1)之前課程講過0、1、2這三個fd被标準輸入、輸出、錯誤通道占用。而且我們可以關閉這三個

(2)我們可以close(1)關閉标準輸出,關閉後我們printf輸出到标準輸出的内容就看不到了

(3)然後我們可以使用dup重新配置設定得到1這個fd,這時候就把oldfd打開的這個檔案和我們1這個标準輸出通道給綁定起來了。這就叫标準輸出的重定位。

(4)可以看出,我們可以使用close和dup配合進行檔案的重定位。

3.1.12.檔案描述符的複制2

3.1.12.1、使用dup2進行檔案描述符複制

(1)dup2和dup的作用是一樣的,都是複制一個新的檔案描述符。但是dup2允許使用者指定新的檔案描述符的數字。

(2)使用方法看man手冊函數原型即可。

3.1.12.2、dup2共享檔案交叉寫入測試

(1)dup2複制的檔案描述符,和原來的檔案描述符雖然數字不一樣,但是這連個指向同一個打開的檔案

(2)交叉寫入的時候,結果是接續寫(實驗證明的)。

3.1.12.3、指令行中重定位指令 >

(1)linux中的shell指令執行後,列印結果都是預設進入stdout的(本質上是因為這些指令譬如ls、pwd等都是調用printf進行列印的),是以我們可以在linux的終端shell中直接看到指令執行的結果。

(2)能否想辦法把ls、pwd等指令的輸出給重定位到一個檔案中(譬如2.txt)去,實際上linux終端支援一個重定位的符号>很簡單可以做到這點。

(3)這個>的實作原理,其實就是利用open+close+dup,

open打開一個檔案2.txt,得到檔案描述符4(假設)

然後close關閉stdout,檔案描述符1就會空着

然後fd2 = dup(4)

着就會将1和2.txt檔案關聯起來即可。

檔案鎖:

3.1.13.fcntl函數介紹

3.1.13.1、fcntl的原型和作用

(1)fcntl函數是一個多功能檔案管理的工具箱,接收2個參數+1個變參。

第一個參數是fd表示要操作哪個檔案,

第二個參數是cmd表示要進行哪個指令操作。

變參是用來傳遞參數的,要配合cmd來使用。

(2)cmd的樣子類似于F_XXX,不同的cmd具有不同的功能。學習時沒必要去把所有的cmd的含義都弄清楚(也記不住),隻需要弄明白一個作為案例,搞清楚它怎麼看怎麼用就行了,其他的是類似的。其他的當我們在使用中碰到了一個fcntl的不認識的cmd時再去查man手冊即可。

3.1.13.2、fcntl的常用cmd

(1)F_DUPFD這個cmd的作用是複制檔案描述符(作用類似于dup和dup2),這個指令的功能是從可用的fd數字清單中找一個比arg大或者和arg一樣大的數字作為oldfd的一個複制的fd,和dup2有點像但是不同。dup2傳回的就是我們指定的那個newfd否則就會出錯,但是F_DUPFD指令傳回的是>=arg的最小的那一個數字。

3.1.13.3、使用fcntl模拟dup2

3.1.14.标準IO庫介紹

3.1.14.1、标準IO和檔案IO有什麼差別

(1)看起來使用時都是函數,但是:标準IO是C庫函數,而檔案IO是linux系統的API

(2)C語言庫函數是由API封裝而來的。庫函數内部也是通過調用API來完成操作的,但是庫函數因為多了一層封裝,是以比API要更加好用一些。

(3)庫函數比API還有一個優勢就是:API在不同的作業系統之間是不能通用的,但是C庫函數在不同作業系統中幾乎是一樣的。是以C庫函數具有可移植性而API不具有可移植性。

(4)性能上和易用性上看,C庫函數一般要好一些。譬如IO,檔案IO是不帶緩存的,而标準IO是帶緩存的,是以标準IO比檔案IO性能要更高。

3.1.14.2、常用标準IO函數介紹

(1)常見的标準IO庫函數有:fopen、fclose、fwrite、fread、ffulsh、fseek

繼續閱讀