3.1.1.應用編程框架介紹
3.1.1.1、什么是應用編程
(1)整個嵌入式linux核心課程包括5個點,按照學習順序依次是:裸機、C高級、uboot和系統移植、linux應用編程和網絡編程、驅動。
(2)典型的嵌入式產品就是基於嵌入式linux操作系統來工作的。典型的嵌入式產品的研發過程就是;第一步讓linux系統在硬件上跑起來(系統移植工作),第二步基於linux系統來開發應用程序實現產品功能。
(3)基於linux去做應用編程,其實就是通過調用linux的【系統API】來實現應用需要完成的任務。
3.1.1.2、本課程大綱規划
3.1.1.3、課程設計思路
(1)通過本課程9個小課程的學習,學會如何使用linux系統提供的API(和C庫函數)來實現一定的功能,通過學習對應用層的編程有所掌握來配合后面驅動的學習。
(2)如果希望深入學習linux應用尤其是網絡編程知識,可以去看一些專門介紹這一塊的書。
3.1.1.4、什么是文件IO
(1)IO就是input/output,輸入/輸出。文件IO的意思就是讀寫文件。
3.1.2.文件操作的主要接口API
3.1.2.1、什么是【操作系統API】
(1)API(應用程序接口)是一些【函數】,這些函數是由linux操作系統提供支持的,由應用層程序來使用。
(2)應用層程序通過調用API來調用操作系統中的各種功能,來干活。
(3)學習一個操作系統,其實就是學習使用這個操作系統的API。
(比如學習開車的時候,車的內部結構就像是操作系統給予我們的封裝,我們開車只要學會怎么踩油門 ,踩剎車等就行,相當於我們操作系統給予我們外部的api接口一樣)
(1)今天我們要使用linux系統來讀寫文件,手段就是學習linux系統API中和文件IO有關的幾個。linux有幾百個api。
3.1.2.2、linux常用文件IO接口
(1)open、close、write、read、lseek(移動文件指針)
3.1.2.3、文件操作的一般步驟
(1)在linux系統中要操作一個文件,一般是先open打開一個文件,得到一個【文件描述符】,然后對文件進行讀寫操作(或其他操作),最后close關閉文件即可
(2)強調一點:我們對文件進行操作時,一定要先打開文件,打開成功后才能去操作(如果打開本身失敗,后面就不用操作了);最后讀寫完成之后一定要close關閉文件,否則可能會造成文件損壞。
(3)文件平時是存在【塊設備】中的文件系統中的,我們把這種文件叫【靜態文件】。當我們去open打開一個文件時,linux內核做的操作包括:內核在進程中建立了一個打開文件的數據結構,記錄下我們打開的這個文件;內核在內存中申請一段內存,並且將靜態文件的內容從塊設備中讀取到內存中【特定地址,由操作系統分配】管理存放(叫動態文件)。
小結:靜態文件就是平時存儲在硬盤中沒有被【打開】時候的文件,動態文件就是從塊設備中讀取到內存中的文件
(4)【打開文件】后,以后對這個文件的讀寫操作,都是針對內存中這一份動態文件的,而並不是針對靜態文件的。當我們對動態文件進行讀寫后,此時內存中的動態文件和塊設備中的靜態文件就不同步了,當我們close關閉動態文件時,close內部內核將內存中的動態文件的內容去更新(同步)塊設備中的靜態文件。(這也就是為什么我們要定期保存的原因,【為了定時更新同步】)
(5)常見的一些現象:
第一個:打開一個大文件時比較慢(因為內核要把【整個文件】從塊設備讀取到內存中,並且建立數據結構對其進行管理)
第二個:我們寫了一半的文件,如果沒有點保存直接關機/斷電,重啟后文件內容丟失。
(6)為什么要這么設計(在內存中為什么要重新搞一份,而不是直接對塊設備中的文件進行讀寫操作)?
因為塊設備本身有讀寫限制(回憶NnadFlash、SD等塊設備的讀寫特征,只能讀不能寫),本身對塊設備進行操作非常不靈活(因為只能按照塊來讀寫)。而內存可以按【字節為單位】來操作,而且可以隨機操作(內存就叫RAM,random,可以任意隨機指定一個地址去操作),很靈活。所以內核設計文件操作時就這么設計了,增強了文件操作的靈活性。
3.1.2.4、重要概念:文件描述符
(1)文件描述符其實【實質是一個數字】,這個數字在一個進程中表示一個特定的含義,當我們open打開一個文件時,操作系統在內存中構建了一些【數據結構】來表示這個動態文件,然后【返回給應用程序】一個數字作為文件描述符,這個數字就和我們內存中維護這個動態文件的這些數據結構掛鈎綁定上了,以后我們應用程序如果要操作這一個動態文件,只需要用這個文件描述符進行區分(這個過程有點類似於我們的一個變量名和變量地址的相互綁定,文件描述符類似於一個文件標號)。
(2)一句話講清楚文件描述符:文件描述符就是用來區分一個應用程序打開的多個動態文件的。
(3)文件描述符的作用域就是【當前進程】,出了當前進程這個文件描述符就沒有意義了,比如我們用notepad++打開多個文件,這個是一個進程,出了這個進程/程序,這個文件描述符就沒有意義了。每一個進程就是一個應用程序,不同的應用程序有一套自己的文件管理描述符,含義在不同的作用域范圍內也是不同的。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
文件描述符補充:
1、內核(kernel)利用文件描述符(filedescriptor)來訪問文件。文件描述符是非負整數。打開現存文件或新建文件時,內核會返回應用程序一個文件描述符。讀寫文件也需要使用文件描述符來指定待讀寫的文件。
2、文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程(應用程序)返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於UNIX、Linux這樣的操作系統。
習慣上,標准輸入(standard input)的文件描述符是 0,標准輸出(standard output)是 1,標准錯誤(standard error)是2, 盡管這種習慣並非Unix內核的特性,但是因為一些 shell 和很多應用程序都使用這種習慣,因此,如果內核不遵循這種習慣的話,很多應用程序將不能使用。
POSIX 定義了 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 來代替 0、1、2。這三個符號常量的定義位於頭文件 unistd.h。
文件描述符的有效范圍是 0 到 OPEN_MAX。一般來說,每個進程最多可以打開 64 個文件(0 — 63)。對於 FreeBSD Mac OS X 10.3 和 Solaris 9 來說,每個進程最多可以打開文件的多少取決於系統內存的大小,int 的大小,以及系統管理員設定的限制。Linux 2.4.22 強制規定最多不能超過 1,048,576 。
文件描述符是由無符號整數表示的句柄,進程使用它來標識打開的文件。文件描述符與包括相關信息(如文件的打開模式、文件的位置類型、文件的初始類型等)的文件對象(描述一個文件的信息的數據結構)相關聯,這些信息被稱作文件的上下文。
3、進程(應用程序)獲取文件描述符最常見的方法是通過本機子例程open或create獲取或者通過從父進程繼承。后一種方法允許子進程同樣能夠訪問由父進程使用的文件。文件描述符對於每個進程一般是唯一的。當用fork子例程創建某個子進程時,該子進程會獲得其父進程所有文件描述符的副本,這些文件描述符在執行fork時打開。在由fcntl、dup和dup2子例程復制或拷貝某個進程時,會發生同樣的復制過程。
【對於每個進程操作系統內核在u_block結構中維護文件描述符表,所有的文件描述符都在該表中建立索引】
4、優點
文件描述符的好處主要有兩個:
基於文件描述符的I/O操作兼容POSIX標准。
在UNIX、Linux的系統調用中,大量的系統調用都是依賴於文件描述符。
例如,下面的代碼就示范了如何基於文件描述符來讀取當前目錄下的一個指定文件,並把文件內容打印至Console中。
此外,在Linux系列的操作系統上,由於Linux的設計思想便是把一切設備都視作文件。因此,文件描述符為在該系列平台上進行設備相關的編程實際上提供了一個統一的方法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
int fd;
int numbytes;
char path[] = "file";
char buf[256];
/*
* O_CREAT: 如果文件不存在則創建
* O_RDONLY:以只讀模式打開文件
*/
fd = open(path, O_CREAT | O_RDONLY, 0644);
if(fd < 0)
{
perror("open()");
exit(EXIT_FAILURE);
}
memset(buf, 0x00, 256);
while((numbytes = read(fd, buf, 255)) > 0)
{
printf("%d bytes read: %s", numbytes, buf);
memset(buf, 0x00, 256);
}
close(fd);
exit(EXIT_SUCCESS);
}
缺點
文件描述符的概念存在兩大缺點:
在非UNIX/Linux操作系統上(如Windows NT),無法基於這一概念進行編程。
由於文件描述符在形式上不過是個整數,當代碼量增大時,會使編程者難以分清哪些整數意味着數據,哪些意味着文件描述符。因此,完成的代碼可讀性也就會變得很差。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3.1.3.一個簡單的文件讀寫實例
3.1.3.1、打開文件與關閉文件
(1)linux中的文件描述符fd的合法范圍是0或者一個正整數,不可能是一個負數。
(2)open返回的fd程序必須記錄好,以后向這個文件的所有操作都要靠這個fd去對應這個文件,最后關閉文件時也需要fd去指定關閉這個文件。如果在我們關閉文件前fd丟掉了那就慘了,這個文件沒法關閉了也沒法讀寫了。
輸入型參數和輸出型參數:
輸入型參數是指這個參數的值已知,由外面傳給函數里使用.
輸出型參數是指這個參數的值未知,要通過函數傳出來.
如拷貝函數char *strcpy( char *strDestination, const char *strSource );
函數功能是把字符串strSource 拷給strDestination
這里strSource 是輸入型形參,strDestination是輸出型形參.
void main()
{
char *strSrc = "abcd";
char strDes[10];
strcpy(strDes,strSrc);//strDes未知,要通過調用函數后才能得到,strSrc已知,傳遞給函數使用.
}
3.1.3.2、實時查man手冊
(1)當我們寫應用程序時,很多API原型都不可能記得,所以要實時查詢,用man手冊
(2)man 1 xx查linux shell命令,man 2 xxx查linux系統API, man 3 xxx查C庫函數
(3)包含必要的系統頭文件
man 2 open
man 2 close
man 2 read
man 2 write
3.1.3.3、讀取文件內容
(1)ssize_t read(int fd, void *buf, size_t count); //void *buf是一個輸出型參數,被寫
fd表示要讀取哪個文件,fd一般由前面的open返回得到
buf是應用程序自己提供的一段內存緩沖區,用來【存儲】讀出的內容
count是我們要讀取的字節數
返回值ssize_t類型是linux內核用typedef重定義的一個類型(其實就是int,為了構建平台可移植性),返回值表示真正成功讀取的字節數。
在文件所有范圍內可以讀取任意個字節。
【read從參數fd指定的文件中讀取數據到大小為count的緩存buf中,然后返回讀取的實際讀取到的字節數】
3.1.3.4、向文件中寫入
ssize_t write(int fd,const void *buf, size_t count);//const void *buf是一個輸入型參數,里面的內容只能夠被讀,size_t count是一個要從緩沖區中讀出來的字節數,ssize_t代表寫成功了多少個字節
【write函數向參數fd指定的文件從緩存buf中拿出count個字節到文件中,返回值為實際寫入的字節數】
(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)我們很少使用只寫的方式。
0
3.1.4.2、打開【存在】並有內容的文件時:O_APPEND、O_TRUNC
(1)思考一個問題:當我們【打開】一個已經存在並且內部有內容的文件時會怎么樣?
可能結果1:新內容會替代原來的內容(原來的內容就不見了,丟了或者說是覆蓋掉了)
可能結果2:新內容添加在前面,原來的內容繼續在后面
可能結果3:新內容附加在后面,原來的內容還在前面
可能結果4:不讀不寫的時候,原來的文件中的內容保持不變
/*
在我們操作文件的時候,可以給這個文件的操作指定多個操作屬性,這些屬性之間用‘|’位或符號相連接起來。eg: fd=open("test.txt",O_RDWR | O_APPEND);
*/
(2)O_TRUNC屬性去打開文件(注意這里只是說打開一個文件,還沒有說是讀還是寫這個文件,僅僅在打開的時候就會把文件里有的內容丟棄 )時,如果這個文件中本來是有內容的,則原來的內容會被丟棄。這就對應上面的結果1
(3)O_APPEND屬性去打開文件時,如果這個文件中本來是有內容的,則新寫入的內容會接續到原來內容的后面,對應結果3
(4)默認不使用O_APPEND和O_TRUNC屬性時就是結果4:原來的文件中的內容保持不變。
(5)如果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之一。(具體用哪個要包含哪一個對應的頭文件,具體可以查man手冊)
3.1.5.open函數的flag詳解2
3.1.5.1、打開【不存在】的文件時:O_CREAT、O_EXCL
(1)思考:當我們去打開一個並不存在的文件時會怎樣?當我們open打開一個文件時如果這個文件名不存在則會打開文件錯誤。
(2)vi或者windows下的notepad++,都可以直接打開一個尚未存在的文件。【有點類似於創建了一個文件】
(3)open的flag O_CREAT就是為了應對這種打開一個並不存在的文件的。O_CREAT就表示我們當前打開的文件並不存在,我們就要去創建並且打開它。
(4)思考:當我們open使用了O_CREAT,但是文件已經存在的情況下會怎樣?
(5)結論:open中加入O_CREAT后,不管原來這個文件存在與否都能打開成功,如果原來這個文件不存在則創建一個【空的新文件】,如果原來這個文件存在則會重新創建這個文件,原來的內容會被消除掉(有點類似於先刪除原來的文件再創建一個新的)
(6)這樣可能帶來一個問題?我們本來是想去創建一個新文件的,但是把文件名搞錯了弄成了一個老文件名,結果老文件就被意外修改了。我們希望的效果是:如果我CREAT要創建的是一個已經存在的名字的文件,則給我報錯,不要去創建。
(7)這個效果就要靠O_EXCL標志和O_CREAT標志來結合使用。當這連個標志一起的時候,則沒有文件時創建文件,有這個文件時會報錯提醒我們已經有這個文件存在了。
(8)open函數在使用O_CREAT標志去創建文件時,可以使用第三個參數mode來指定要創建的文件的權限(可讀或者可寫)。mode使用4個數字來指定權限的,其中后面三個很重要,對應我們要創建的這個文件的權限標志。譬如一般創建一個可讀可寫不可執行的文件就用0666 比如:fd=open("tst.txt",O_RDWR | O_CREAT,0666);mode位就是和創建文件相互配套使用的。
3.1.5.2、O_NONBLOCK
(1)阻塞與非阻塞。如果一個函數是阻塞式的,則我們調用這個函數時當前進程有可能被卡住(阻塞住,實質是這個函數內部要完成的事情條件不具備,當前沒法做,要等待條件成熟,但是最終還是要執行,類似於銀行的排隊等待,不管前面有多少人排隊,我就要等待),【函數被阻塞住了就不能立刻返回】;如果一個函數是非阻塞式的那么我們調用這個函數后一定會立即返回,但是函數有沒有完成指定的任務不一定(有可能排隊的時候前面沒人直接完成任務,也有可能是排隊的人太多了,直接回來了)。
小結:阻塞式一定能返回一個完成的結果,但是時間是不確定的;而非阻塞式的能夠立即返回一個結果,但是這個結果不一定執行成功。
(2)阻塞和非阻塞是兩種不同的設計思路,並沒有好壞。總的來說,【阻塞式的結果有保障(一定會去做的)但是時間沒保障;非阻塞式的時間有保障但是結果沒保障(做不做不一定的)。】
(3)操作系統提供的API和由API封裝而成的庫函數,有很多本身就是被設計為阻塞式或者非阻塞式的,所以我們應用程度調用這些函數的時候心里得非常清楚。
(4)我們打開一個文件默認就是阻塞式的,如果你希望以非阻塞的方式打開文件,則open的flag中要加O_NONBLOCK標志。
(2)只用於設備文件(硬件器件之類,比如串口文件),而不用於普通文件。
3.1.5.3、O_SYNC
(1)write阻塞等待底層完成寫入才返回到應用層。
(2)無O_SYNC時write只是將內容寫入【底層緩沖區】即可返回,然后底層(操作系統中負責實現open、write這些操作的那些代碼,也包含OS中讀寫硬盤等底層硬件的代碼)【在合適的時候會】將buf中的內容一次性的同步到硬盤中。這種設計是為了提升硬件操作的性能和銷量,提升硬件壽命,避免反復操作硬盤硬件;但是有時候我們希望硬件不要等待,而是直接將我們的內容寫入硬盤中,這時候就可以在open的flag中用O_SYNC標志。
3.1.6.文件讀寫的一些細節
3.1.6.1、errno和perror
(1)errno就是error number,意思就是錯誤號碼。可以看作是linux系統中維護的一個變量,linux系統中對各種【常見錯誤】做了個編號,當函數執行錯誤時,函數會返回一個特定的errno編號來告訴我們這個函數到底哪里錯了,或者說是屬於哪一種錯誤(比如內存溢出,函數執行錯誤等)。 【類似於一個錯誤編號表。】
(2)errno是由OS來維護的一個【全局變量】,任何OS內部函數都可以通過設置errno來告訴上層調用者究竟剛才發生了一個什么錯誤。
(3)errno本身實質是一個int類型的數字,每個數字編號對應一種錯誤。當我們只看errno時只能得到一個錯誤編號數字(譬如-37),不適應於人看。 【有點類似於中斷中的中斷編號,也有點類似於一個數組】
解決方案就是linux系統提供了一個函數perror(意思print error),perror函數【內部,(不用給這個函數傳參)】會讀取errno並且將這個 成對應的錯誤信息字符串,然后print打印出來。 【eg:perror ("open: ");】
man 3 perror
3.1.6.2、read和write的count
(1)count和返回值的關系。count參數表示我們想要寫或者讀的字節數,返回值表示【實際完成的】要寫或者讀的字節數。實現的有可能等於想要讀寫的,也有可能小於(說明沒完成任務)
(2)count再和阻塞非阻塞結合起來,就會更加復雜。如果一個函數是阻塞式的,則我們要讀取30個,結果暫時只有20個時就會被阻塞住,等待剩余的10個可以讀。【比如在阻塞式的讀取的時候,文件只有50個字節,我們設置的count為大於50個字節,則程序就會阻塞住,因為它想要讀取完你設定的值,交給的任務】
(3)有時候我們寫正式程序時,我們要讀取或者寫入的是一個很龐大的文件(譬如文件有2MB),我們不可能把count設置為2*1024*1024,而應該去把count設置為一個合適的數字(譬如2048、4096),然后通過【多次循環讀取】來實現全部讀完。
3.1.6.3、文件IO效率和標准IO
(1)文件IO就指的是我們當前在講的【open、close、write、read】等API函數構成的一套用來讀寫文件的體系,這套體系可以很好的完成文件讀寫,但是效率並不是最高的。
(2)應用層C語言庫函數提供了一些用來做文件讀寫的函數列表,叫標准IO。標准IO由一系列的C庫函數構成(fopen、fclose、fwrite、fread),加一個f代表是庫函數,這些標准IO函數其實是由【系統提供的文件IOAPI】封裝而來的(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)文件平時都在存放在硬盤中的,硬盤中存儲的文件以一種固定的形式存放的,我們叫靜態文件。
硬盤屬於一種塊設備,這些塊設備中又有一個個的扇區,一個扇區一般的是512個字節,多個扇區(64或者32)又組成一個塊。整個設備里可能有好多個塊,比如一個設備里有10000個塊,一個塊里有300個扇區,一個扇區有512個字節,則這個設備的總存儲容量就是10000*300*512 B。
文件為什么一經過壓縮就會變小呢?因為壓縮后可以把在磁盤上不同位置存儲的不同文件放到一個相同的文件夾中,這個壓縮包里集中了各種各樣的文件。
(2)一塊硬盤中可以分為兩大區域:一個是硬盤內容存儲管理表項,另一個是真正存儲內容的區域。操作系統訪問硬盤時是先去讀取硬盤內容管理表(作為搜找一個文件存儲路徑的索引),從中找到我們要訪問的那個文件的扇區級別的信息,然后再通過這個信息去查詢真正存儲內容的區域,最后得到我們要的文件。
(3)操作系統最初拿到的信息是文件名,最終得到的是文件內容。第一步就是去查詢硬盤內容管理表,這個管理表中以文件為單位記錄了各個文件的各種信息,每一個文件有一個信息列表數據結構(我們叫inode結構體,i節點,其實質是一個結構體,這個結構體有很多元素,每個元素記錄了這個文件的一些信息,其中就包括文件名、文件在硬盤上對應的扇區號、塊號那些東西·····)
強調:硬盤管理的時候是以【文件】為基本單位的,每個文件一個inode結構體,每個inode有一個數字編號,對應一個結構體,結構體中記錄了各種信息。(我們操作系統拿到一個文件名后就會在硬盤內容管理表中利用文件名循環匹配硬盤內容管理表中的文件信息節點inode,怎么匹配呢?我覺得應該是拿文件名去匹配節點里的一個結構體指向元素)
小結:i節點就是我們操作系統中用來記錄文件各種信息(存儲、大小之類)的一種數據結構,這種數據結構能夠讓我們去管理硬盤上的存儲文件。
(4)聯系平時實踐,大家格式化硬盤(U盤)時發現有:快速格式化和底層格式化。快速格式化非常快,格式化一個32GB的U盤只要1秒鍾,普通格式化格式化速度慢。這兩個的差異?其實快速格式化就是只刪除了U盤中的硬盤內容管理表(其實就是inode),真正存儲的內容沒有動。這種格式化的內容是有可能被找回的(我們可以重新掃描各個扇區,然后對各個扇區的內容文件進行重新的管理組建,形成一個新的內容管理表)。
如圖所示:

3.1.7.2、內存中被打開的文件和vnode(v節點)
(1)一個程序的運行就是一個進程,我們在該程序中打開的文件就屬於某個進程。每個進程都有一個數據結構用來記錄這個進程的所有信息(叫進程信息表),進程信息表中有一個指針,該指針會指向一個文件管理表,文件管理表中記錄了當前進程打開的所有文件及其相關信息。文件管理表中用來索引各個打開的文件的index就是文件描述符fd,我們最終找到的就是一個已經被打開的文件的管理結構體vnode(也就是說vnode就是專門用來管理已經被打開的文件的)
(2)一個vnode中就記錄了一個被打開的文件的各種信息(比如文件大小,文件在【內存】中的位置),而且我們只要知道這個文件的fd,就可以很容易的找到這個文件的vnode進而對這個文件進行各種操作。
小結:在我們的動態文件中,我們的內存中有一個【所有的進程管理表】,我們打開一個文件的時候就要去這個進程管理表中找到對應的一個進程,這個進程里有一個該進程的信息管理表,其實是一個結構體,這個結構體中其中有一個元素指針指向保存着所有的打開的文件信息的表,叫做【文件管理表】,文件管理表通過【文件描述符fd】來找到某一個具體打開的文件,對於一個具體打開的文件,該文件的各種信息(在內存中存儲位置、存儲大小等)由【vnode管理】,vnode就是一個結構體,里面有文件指針這個元素,這個指針表示當前我們正在操作文件流的哪個位置。
如圖所示:

3.1.7.3、文件與【流】的概念
(1)流(stream)對應自然界的水流。文件操作中,文件類似是一個大包裹,里面裝了一堆字符,但是文件被讀出/寫入時都只能一個字符一個字符的進行,而不能一股腦兒的讀寫,那么一個文件中N多的個字符被挨個一次讀出/寫入時,這些字符就構成了一個字符流。
(2)流這個概念是動態的,不是靜態的。
(3)編程中提到流這個概念,一般都是IO相關的。所以經常叫IO流。文件操作時就構成了一個IO流。
---------------------------------------------------------------------
3.1.8.lseek詳解
3.1.8.1、lseek函數介紹
(1)文件指針:當我們要對一個文件進行讀寫時,一定需要先打開這個文件,所以我們讀寫的所有文件都是動態文件。動態文件在內存中的形態就是文件流的形式。
(2)文件流很長,里面有很多個字節。那我們當前正在操作的是哪個位置?GUI模式下的軟件用光標來標識這個當前正在操作的位置,這是給人看的。【光標就好像在我們的這個流里面打了一個截點,光標就是在我們的這個流里來回的動的】
(3)在內存里的動態文件中,我們會通過文件指針來表征這個正在操作的位置。所謂文件指針,就是我們文件信息管理表vnode這個結構體里面的一個指針。【所以文件指針其實是vnode中的一個元素】這個指針表示當前我們正在操作文件流的哪個位置。這個指針不能被直接訪問,linux系統用lseek函數來訪問這個文件指針。
(4)當我們打開一個空文件時,默認情況下文件指針指向文件流的開始。所以這時候去write時寫入就是從文件開頭位置開始的。write和read函數本身自帶移動文件指針的功能,所以當我write了n個字節后,【文件指針會自動依次向后移動n位。】如果需要人為的隨意更改文件指針(讓光標隨意移動),自由化,那就只能通過lseek函數了。
(5)read和write函數都是從【當前文件指針處】開始操作的,所以當我們用lseek顯式的將文件指針移動后,那么再去read/write時就是從【移動過后的位置】開始的。但是文件中的內容還是完整存在的。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);//fd就是代表當前文件,whence就是代表一個參考位置(用來表示文件開始處到文件當前位置的字節數)offset代表一個偏移量。也就是說從參考位置開始偏移了多少位置。off_t代表返回的是實際位置相對於開頭偏移的字節數(也就是最終位置在哪)。
函數原型
#include<unistd.h>
off_t lseek(int fildes,off_t offset ,int whence);
參數 offset 的含義取決於參數 whence:
1. 如果 whence 是 SEEK_SET,則返回的文件偏移量將被設置為 offset。
2. 如果 whence 是 SEEK_CUR,則返回的文件偏移量將被設置為 cfo 加上 offset,
offset 可以為正也可以為負。
3. 如果 whence 是 SEEK_END,則返回的文件偏移量將被設置為文件長度加上 offset,
offset 可以為正也可以為負。
1) 欲將讀寫位置移到文件開頭時:
lseek(int fildes,0,SEEK_SET);
2) 欲將讀寫位置移到文件尾時:
lseek(int fildes,0,SEEK_END);
3) 想要取得目前文件位置時:
lseek(int fildes,0,SEEK_CUR);
返回值:
當調用成功時則返回目前的【讀寫位置】,也就是距離文件開頭多少個字節。若有錯誤則返回-1,errno 會存放錯誤代碼。
(6)回顧前面一節中我們從空文件,先write寫了12字節,然后read時是空的(但是此時我們打開文件后發現12字節確實寫進來了,這個的原因就是把文件指針放到了文件末尾)。
3.1.8.2、用lseek計算文件長度
(1)linux中並沒有一個函數可以直接返回一個文件的長度(因為不需要)。但是我們做項目時經常會需要知道一個文件的長度,怎么辦?【自己利用lseek來寫一個函數得到文件長度即可。】
原理就是當我們新打開一個文件時,此時文件指針在文件最開頭處,我們用lseek函數把文件指針移動到文件末尾處,然后返回值就是我們文件的末尾指針距離文件開頭的偏移量,即文件的長度。
eg:return=lseek(fd,0,SEEK_END);//表示從文件末尾
3.1.8.3、用lseek構建空洞文件
(1)空洞文件就是這個文件中有一段是空的。
(2)普通文件中間是不能有空的,因為我們write時文件指針是依次從前到后去移動的,不可能繞過前面直接到后面。
(3)我們打開一個文件后,用lseek往后跳過一段,再write寫入一段,就會構成一個空洞文件(這個空洞文件就是有一段沒有內容)。
(4)空洞文件方法對多線程共同操作文件是及其有用的。有時候我們創建一個很大的文件(比如視頻文件),如果從頭開始依次構建時間很長。有一種思路就是將文件分為多段,然后多線程來操作每個線程負責其中一段的寫入。
【就像修100公里的高速公路,分成20個段來修,每個段就只負責5公里,就可以大大提高效率】
(5)空洞文件就是由lseek來構建的。
空洞文件構建簡單示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
char a[50]="abcdefg";
char b[50]="znm";
int fd=-1;
int k;
int i,j;
fd=open(FILEPATH,O_RDWR);
k=write(fd,&a,5);
i= lseek(fd,5,SEEK_CUR);
j=write(fd,&b,3);
close(fd);
}
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標志即可
3.1.9.4、O_APPEND的實現原理和其原子操作性說明
(1)O_APPEND為什么能夠將分別寫改為接續寫?關鍵的核心的東西是文件指針。分別寫的內部原理就是2個fd擁有不同的文件指針,並且彼此只考慮自己的位移。但是O_APPEND標志可以讓write和read函數內部多做一件事情,就是移動自己的文件指針的同時也去把別人的文件指針同時移動。(也就是說即使加了O_APPEND,fd1和fd2還是各自擁有一個獨立的文件指針,但是這兩個文件指針關聯起來了,一個動了會通知另一個跟着動)
(2)O_APPEND對文件指針的影響,對文件的讀寫是原子的。
(3)原子操作的含義是:整個操作一旦開始是不會被打斷的,必須直到操作結束其他代碼才能得以調度運行,這就叫原子操作。每種操作系統中都有一些機制來實現原子操作,以保證那些需要原子操作的任務可以運行。比如一個程序的執行過程不能被cpu的多個進程同時執行的過程打斷。
代碼示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
char a[50]="aa";
char b[50]="bb";
int fd1=-1;
int fd2=-1;
int k;
int i,j,l;
//進程一
fd1=open(FILEPATH,O_RDWR | O_APPEND);
j=write(fd1,&a,2);
k=close(fd1);
//進程二
fd2=open(FILEPATH,O_RDWR | O_APPEND);
l=write(fd2,&b,2);
i=close(fd2);
}
--------------------------------------------------------
3.1.10.文件共享的實現方式
3.1.10.1、什么是文件共享
(1)文件共享就是同一個文件(同一個文件指的是同一個inode,同一個pathname)被多個獨立的讀寫體(幾乎可以理解為多個文件描述符)去同時(一個打開尚未關閉的同時另一個去操作)操作。
(2)文件共享的意義有很多:譬如我們可以通過文件共享來實現多線程同時操作同一個大文件,以減少文件讀寫時間,提升效率。
3.1.10.2、文件共享的3種實現方式
(1)文件共享的核心就是怎么弄出來多個文件描述符指向同一個文件。
(2)常見的有3種文件共享的情況:第一種是同一個進程中多次使用open打開同一個文件,第二種是在不同進程中去分別使用open打開同一個文件(這時候因為兩個fd在不同的進程中,所以兩個fd的數字可以相同也可以不同),第三種情況是后面要學的,linux系統提供了dup和dup2兩個API來讓進程復制文件描述符。
(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。也就是標准輸入、標准輸出、標准錯誤,因此用戶進程得到的最小的fd就是3了。
(7)標准輸入一般對應的是鍵盤(可以理解為:0這個fd對應的是鍵盤的設備文件),標准輸出一般是LCD顯示器(可以理解為:1對應LCD的設備文件)
(8)printf函數其實就是默認輸出到標准輸出stdout上了。stdio中還有一個函數叫fpirntf,這個函數就可以指定輸出到哪個文件描述符中。
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配合進行文件的重定位。
代碼示例1:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
int fd=-1;
int i=-1; //實際向文件中寫入的字節數
int j=-1;//實際向文件中寫入的字節數
int k;//用dup復制后的文件描述符
char a[100]="aa";
char b[100]= "bb";
//打開一個文件操作
fd=open(FILEPATH, O_RDWR );
if(-1==fd)
{
printf("文件打開失敗!\n");
perror("open:");
_exit(-1);
}
else
{
printf("文件打開成功!fd=%d\n",fd);
}
k=dup(fd);
while(1)
{
//寫一個文件操作
i=write(fd,&a,2);
if(-1==i)
{
printf("寫入文件失敗\n");
_exit(-1);
}
//寫一個文件操作
j=write(k,&b,2);
if(-1==j)
{
printf("寫入文件失敗\n");
perror("寫入");
_exit(-1);
}
}
close(fd);
}
代碼示例2:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "1.txt"
void main(int argc,char *argv[])
{
int fd=-1;
int i=-1; //實際向文件中寫入的字節數
int j=-1;//實際向文件中寫入的字節數
int k;//用dup復制后的文件描述符
char b[100]= "bbbbbadfghgdfh";
//打開一個文件操作
fd=open(FILEPATH, O_RDWR );
if(-1==fd)
{
printf("文件打開失敗!\n");
perror("open:");
_exit(-1);
}
else
{
printf("文件打開成功!fd=%d\n",fd);
}
close(1); //關閉標准輸出文件描述符1
k=dup(fd); //實現重定位
j=write(k,&b,20);
if(-1==j)
{
printf("寫入文件失敗\n");
perror("寫入");
_exit(-1);
}
close(fd);
}
3.1.12.文件描述符的復制2
3.1.12.1、使用dup2進行文件描述符復制
(1)dup2和dup的作用是一樣的,都是復制一個新的文件描述符。但是dup2允許用戶指定新的文件描述符的數字。
(2)使用方法看man手冊函數原型即可。
int dup2(int oldfd, int newfd);
如果成功則返回一個我們指定的文件描述符,反之出錯返回-1.
3.1.12.2、dup2共享文件交叉寫入測試
(1)dup2復制的文件描述符,和原來的文件描述符雖然數字不一樣,但是這兩個個指向同一個打開的文件
(2)交叉寫入的時候,結果是接續寫(實驗證明的)。
代碼示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH "./a.txt"
void main(int argc,char *argv[])
{
int fd=-1;
int i=-1; //實際向文件中寫入的字節數
int j=-1;//實際向文件中寫入的字節數
int k;//用dup復制后的文件描述符
char a[100]="aa";
char b[100]= "bb";
//打開一個文件操作
fd=open(FILEPATH, O_RDWR );
if(-1==fd)
{
printf("文件打開失敗!\n");
perror("open:");
_exit(-1);
}
else
{
printf("文件打開成功!fd=%d\n",fd);
}
k=dup2(fd,5); //調用dup2函數
printf("k=%d\n",k);
while(1)
{
//第一次寫一個文件操作
i=write(fd,&a,2);
if(-1==i)
{
printf("寫入文件失敗\n");
_exit(-1);
}
//第二次寫一個文件操作
j=write(k,&b,2);
if(-1==j)
{
printf("寫入文件失敗\n");
perror("寫入");
_exit(-1);
}
}
close(fd);
}
3.1.12.3、命令行中重定位命令 >
這個命令的主要作用就是在終端輸出的東西重定位到一個文件中來輸出。
(1)linux中的shell命令執行后,打印結果都是默認進入stdout標准輸出的(本質上是因為這些命令譬如ls、pwd等都是調用printf進行打印的),所以我們可以在linux的終端shell中直接看到命令執行的結果。
(2)能否想辦法把ls、pwd等命令的輸出給重定位到一個文件中(譬如2.txt)去,實際上linux終端支持一個重定位的符號>很簡單可以做到這點。(eg: ls > 2.txt)
(3)這個>的實現原理,其實就是利用open+close+dup,open打開一個文件2.txt,然后close(1)關閉stdout,然后dup將1和2.txt文件關聯起來即可。
3.1.13.fcntl函數介紹
3.1.13.1、fcntl的原型和作用
(1)fcntl函數是一個多功能文件管理(根據文件描述符來操作文件的特性)的工具箱,接收2個參數+1個變參。第一個參數是fd表示要操作哪個文件,第二個參數是cmd表示要進行哪個命令操作。變參是用來傳遞參數的,要配合cmd來使用(供命令使用的參數)。
用法:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
參數:
fd:文件描述詞。
cmd:操作命令。
arg:供命令使用的參數。
lock:同上。
(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否則就會出錯,返回-1;但是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,API類似於一種接口,是由操作系統提供的。
(2)C語言庫函數是【由API封裝】而來的。C庫函數內部也是通過調用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
3.1.14.3、一個簡單的標准IO讀寫文件實例
FILE *fopen(const char *path, const char *mode);
代碼示例:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#define FILEPATH ".//a.txt"
void main(int argc,char *argv[])
{
FILE *file; //定義一個文件指針
int k;//用fclose函數后返回的值,若返回值不為0則表示關閉失敗
file=fopen(FILEPATH, "r+");
if(NULL!= file)
{
printf("文件打開成功!\n");
}
else
{
printf("文件打開失敗!\n");
perror("open");
_exit(-1);
}
k=fclose(file);
if(k==0)
{
printf("文件關閉成功!\n");
}
else
{
printf("文件關閉失敗!\n");
perror("close");
_exit(-1);
}
}