(46)LINUX應用編程和網絡編程之一Linux應用編程框架

3.1.1.應用編程框架介紹
3.1.1.一、什麼是應用編程
(1)整個嵌入式linux核心課程包括5個點,按照學習順序依次是:裸機、C高級、uboot和系統移植、linux應用編程和網絡編程、驅動。
(2)典型的嵌入式產品就是基於嵌入式linux操做系統來工做的。典型的嵌入式產品的研發過程就是;第一步讓linux系統在硬件上跑起來(系統移植工做),第二步基於linux系統來開發應用程序實現產品功能。
(3)基於linux去作應用編程,其實就是經過調用linux的【系統API】來實現應用須要完成的任務。
 
3.1.1.二、本課程大綱規劃
 
3.1.1.三、課程設計思路
(1)經過本課程9個小課程的學習,學會如何使用linux系統提供的API(和C庫函數)來實現必定的功能,經過學習對應用層的編程有所掌握來配合後面驅動的學習。
(2)若是但願深刻學習linux應用尤爲是網絡編程知識,能夠去看一些專門介紹這一塊的書。
 
3.1.1.四、什麼是文件IO
(1)IO就是input/output,輸入/輸出。文件IO的意思就是讀寫文件。
 
 
3.1.2.文件操做的主要接口API
3.1.2.一、什麼是【操做系統API】
(1)API(應用程序接口)是一些【函數】,這些函數是由linux操做系統提供支持的,由應用層程序來使用。
(2)應用層程序經過調用API來調用操做系統中的各類功能,來幹活。
(3)學習一個操做系統,其實就是學習使用這個操做系統的API。
(好比學習開車的時候,車的內部結構就像是操做系統給予咱們的封裝,咱們開車只要學會怎麼踩油門 ,踩剎車等就行,至關於咱們操做系統給予咱們外部的api接口同樣)
(1)今天咱們要使用linux系統來讀寫文件,手段就是學習linux系統API中和文件IO有關的幾個。linux有幾百個api。
3.1.2.二、linux經常使用文件IO接口
(1)open、close、write、read、lseek(移動文件指針)
 
3.1.2.三、文件操做的通常步驟
(1)在linux系統中要操做一個文件,通常是先open打開一個文件,獲得一個【文件描述符】,而後對文件進行讀寫操做(或其餘操做),最後close關閉文件便可
(2)強調一點:咱們對文件進行操做時,必定要先打開文件,打開成功後才能去操做(若是打開自己失敗,後面就不用操做了);最後讀寫完成以後必定要close關閉文件,不然可能會形成文件損壞。
(3)文件平時是存在【塊設備】中的文件系統中的,咱們把這種文件叫【靜態文件】。當咱們去open打開一個文件時,linux內核作的操做包括:內核在進程中創建了一個打開文件的數據結構,記錄下咱們打開的這個文件;內核在內存中申請一段內存,而且將靜態文件的內容從塊設備中讀取到內存中【特定地址,由操做系統分配】管理存放(叫動態文件)。
 
小結:靜態文件就是平時存儲在硬盤中沒有被【打開】時候的文件,動態文件就是從塊設備中讀取到內存中的文件
 
(4)【打開文件】後,之後對這個文件的讀寫操做,都是針對內存中這一份動態文件的,而並非針對靜態文件的。當咱們對動態文件進行讀寫後,此時內存中的動態文件和塊設備中的靜態文件就不一樣步了,當咱們close關閉動態文件時,close內部內核將內存中的動態文件的內容去更新(同步)塊設備中的靜態文件。(這也就是爲何咱們要按期保存的緣由,【爲了定時更新同步】)
(5)常見的一些現象:
第一個:打開一個大文件時比較慢(由於內核要把【整個文件】從塊設備讀取到內存中,而且創建數據結構對其進行管理)
第二個:咱們寫了一半的文件,若是沒有點保存直接關機/斷電,重啓後文件內容丟失。
(6)爲何要這麼設計(在內存中爲何要從新搞一份,而不是直接對塊設備中的文件進行讀寫操做)?
由於塊設備自己有讀寫限制(回憶NnadFlash、SD等塊設備的讀寫特徵,只能讀不能寫),自己對塊設備進行操做很是不靈活(由於只能按照塊來讀寫)。而內存能夠按【字節爲單位】來操做,並且能夠隨機操做(內存就叫RAM,random,能夠任意隨機指定一個地址去操做),很靈活。因此內核設計文件操做時就這麼設計了,加強了文件操做的靈活性。
 
3.1.2.四、重要概念:文件描述符
(1)文件描述符其實【實質是一個數字】,這個數字在一個進程中表示一個特定的含義,當咱們open打開一個文件時,操做系統在內存中構建了一些【數據結構】來表示這個動態文件,而後【返回給應用程序】一個數字做爲文件描述符,這個數字就和咱們內存中維護這個動態文件的這些數據結構掛鉤綁定上了,之後咱們應用程序若是要操做這一個動態文件,只須要用這個文件描述符進行區分(這個過程有點相似於咱們的一個變量名和變量地址的相互綁定,文件描述符相似於一個文件標號)。
(2)一句話講清楚文件描述符:文件描述符就是用來區分一個應用程序打開的多個動態文件的。
(3)文件描述符的做用域就是【當前進程】,出了當前進程這個文件描述符就沒有意義了,好比咱們用notepad++打開多個文件,這個是一個進程,出了這個進程/程序,這個文件描述符就沒有意義了。每個進程就是一個應用程序,不一樣的應用程序有一套本身的文件管理描述符,含義在不一樣的做用域範圍內也是不一樣的。
 
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
文件描述符補充:
一、內核(kernel)利用文件描述符(filedescriptor)來訪問文件。文件描述符是非負整數。打開現存文件或新建文件時,內核會返回應用程序一個文件描述符。讀寫文件也須要使用文件描述符來指定待讀寫的文件。
二、文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程(應用程序)返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。
習慣上,標準輸入(standard input)的文件描述符是 0,標準輸出(standard output)是 1,標準錯誤(standard error)是2, 儘管這種習慣並不是Unix內核的特性,可是由於一些 shell 和不少應用程序都使用這種習慣,所以,若是內核不遵循這種習慣的話,不少應用程序將不能使用。
POSIX 定義了 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 來代替 0、一、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 。
文件描述符是由無符號整數表示的句柄,進程使用它來標識打開的文件。文件描述符與包括相關信息(如文件的打開模式、文件的位置類型、文件的初始類型等)的文件對象(描述一個文件的信息的數據結構)相關聯,這些信息被稱做文件的上下文。
三、進程(應用程序)獲取文件描述符最多見的方法是經過本機子例程open或create獲取或者經過從父進程繼承。後一種方法容許子進程一樣可以訪問由父進程使用的文件。文件描述符對於每一個進程通常是惟一的。當用fork子例程建立某個子進程時,該子進程會得到其父進程全部文件描述符的副本,這些文件描述符在執行fork時打開。在由fcntl、dup和dup2子例程複製或拷貝某個進程時,會發生一樣的複製過程。
【對於每一個進程操做系統內核在u_block結構中維護文件描述符表,全部的文件描述符都在該表中創建索引】
四、優勢
文件描述符的好處主要有兩個:
基於文件描述符的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)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.二、實時查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.三、讀取文件內容
(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.四、向文件中寫入
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.一、打開文件時對文件讀寫權限的要求:O_RDONLY (只讀方式)   O_WRONLY (只寫方式)    O_RDWR(可讀可寫)
(1)linux中文件有讀寫權限,咱們在open打開文件時也能夠附帶必定的權限說明(譬如O_RDONLY就表示以只讀方式打開,O_WRONLY表示以只寫方式打開,O_RDWR表示以可讀可寫方式打開)
(2)當咱們附帶了權限後,打開的文件就只能按照這種權限來操做。
(3)咱們不多使用只寫的方式。
0
3.1.4.二、打開【存在】並有內容的文件時: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.三、exit、_exit、_Exit退出進程
(1)當咱們程序在前面步驟操做失敗致使後面的操做都沒有可能進行下去時,應該在前面的錯誤監測中結束整個程序,不該該繼續讓程序運行下去了。
(2)咱們如何退出程序?
第一種;在main用return,通常原則是程序正常終止return 0,若是程序異常終止則return -1。
第二種:正式終止進程(程序)應該使用exit或者_exit或者_Exit之一。(具體用哪一個要包含哪個對應的頭文件,具體能夠查man手冊)
 
 
3.1.5.open函數的flag詳解2
3.1.5.一、打開【不存在】的文件時: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.二、O_NONBLOCK
(1)阻塞與非阻塞。若是一個函數是阻塞式的,則咱們調用這個函數時當前進程有可能被卡住(阻塞住,實質是這個函數內部要完成的事情條件不具有,當前無法作,要等待條件成熟,可是最終仍是要執行,相似於銀行的排隊等待,無論前面有多少人排隊,我就要等待),【函數被阻塞住了就不能馬上返回】;若是一個函數是非阻塞式的那麼咱們調用這個函數後必定會當即返回,可是函數有沒有完成指定的任務不必定(有可能排隊的時候前面沒人直接完成任務,也有多是排隊的人太多了,直接回來了)。
小結:阻塞式必定能返回一個完成的結果,可是時間是不肯定的;而非阻塞式的可以當即返回一個結果,可是這個結果不必定執行成功。
(2)阻塞和非阻塞是兩種不一樣的設計思路,並無好壞。總的來講,【阻塞式的結果有保障(必定會去作的)可是時間沒保障;非阻塞式的時間有保障可是結果沒保障(作不作不必定的)。】
(3)操做系統提供的API和由API封裝而成的庫函數,有不少自己就是被設計爲阻塞式或者非阻塞式的,因此咱們應用程度調用這些函數的時候內心得很是清楚。
(4)咱們打開一個文件默認就是阻塞式的,若是你但願以非阻塞的方式打開文件,則open的flag中要加O_NONBLOCK標誌。
(2)只用於設備文件(硬件器件之類,好比串口文件),而不用於普通文件。
 
3.1.5.三、O_SYNC
(1)write阻塞等待底層完成寫入才返回到應用層。
(2)無O_SYNC時write只是將內容寫入【底層緩衝區】便可返回,而後底層(操做系統中負責實現open、write這些操做的那些代碼,也包含OS中讀寫硬盤等底層硬件的代碼)【在合適的時候會】將buf中的內容一次性的同步到硬盤中。這種設計是爲了提高硬件操做的性能和銷量,提高硬件壽命,避免反覆操做硬盤硬件;可是有時候咱們但願硬件不要等待,而是直接將咱們的內容寫入硬盤中,這時候就能夠在open的flag中用O_SYNC標誌。
 
 
3.1.6.文件讀寫的一些細節
3.1.6.一、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.二、read和write的count
(1)count和返回值的關係。count參數表示咱們想要寫或者讀的字節數,返回值表示【實際完成的】要寫或者讀的字節數。實現的有可能等於想要讀寫的,也有可能小於(說明沒完成任務)
(2)count再和阻塞非阻塞結合起來,就會更加複雜。若是一個函數是阻塞式的,則咱們要讀取30個,結果暫時只有20個時就會被阻塞住,等待剩餘的10個能夠讀。【好比在阻塞式的讀取的時候,文件只有50個字節,咱們設置的count爲大於50個字節,則程序就會阻塞住,由於它想要讀取完你設定的值,交給的任務】
(3)有時候咱們寫正式程序時,咱們要讀取或者寫入的是一個很龐大的文件(譬如文件有2MB),咱們不可能把count設置爲2*1024*1024,而應該去把count設置爲一個合適的數字(譬如204八、4096),而後經過【屢次循環讀取】來實現所有讀完。
 
3.1.6.三、文件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.一、硬盤中的靜態文件和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.二、內存中被打開的文件和vnode(v節點)
(1)一個程序的運行就是一個進程,咱們在該程序中打開的文件就屬於某個進程。每一個進程都有一個數據結構用來記錄這個進程的全部信息(叫進程信息表),進程信息表中有一個指針,該指針會指向一個文件管理表,文件管理表中記錄了當前進程打開的全部文件及其相關信息。文件管理表中用來索引各個打開的文件的index就是文件描述符fd,咱們最終找到的就是一個已經被打開的文件的管理結構體vnode(也就是說vnode就是專門用來管理已經被打開的文件的)
(2)一個vnode中就記錄了一個被打開的文件的各類信息(好比文件大小,文件在【內存】中的位置),並且咱們只要知道這個文件的fd,就能夠很容易的找到這個文件的vnode進而對這個文件進行各類操做。
 
小結:在咱們的動態文件中,咱們的內存中有一個【全部的進程管理表】,咱們打開一個文件的時候就要去這個進程管理表中找到對應的一個進程,這個進程裏有一個該進程的信息管理表,實際上是一個結構體,這個結構體中其中有一個元素指針指向保存着全部的打開的文件信息的表,叫作【文件管理表】,文件管理表經過【文件描述符fd】來找到某一個具體打開的文件,對於一個具體打開的文件,該文件的各類信息(在內存中存儲位置、存儲大小等)由【vnode管理】,vnode就是一個結構體,裏面有文件指針這個元素,這個指針表示當前咱們正在操做文件流的哪一個位置。
如圖所示:
3.1.7.三、文件與【流】的概念
(1)流(stream)對應天然界的水流。文件操做中,文件相似是一個大包裹,裏面裝了一堆字符,可是文件被讀出/寫入時都只能一個字符一個字符的進行,而不能一古腦兒的讀寫,那麼一個文件中N多的個字符被挨個一次讀出/寫入時,這些字符就構成了一個字符流。
(2)流這個概念是動態的,不是靜態的。
(3)編程中提到流這個概念,通常都是IO相關的。因此常常叫IO流。文件操做時就構成了一個IO流。
 
---------------------------------------------------------------------
3.1.8.lseek詳解
3.1.8.一、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.二、用lseek計算文件長度
(1)linux中並無一個函數能夠直接返回一個文件的長度(由於不須要)。可是咱們作項目時常常會須要知道一個文件的長度,怎麼辦?【本身利用lseek來寫一個函數獲得文件長度便可。】
原理就是當咱們新打開一個文件時,此時文件指針在文件最開頭處,咱們用lseek函數把文件指針移動到文件末尾處,而後返回值就是咱們文件的末尾指針距離文件開頭的偏移量,即文件的長度。
eg:return=lseek(fd,0,SEEK_END);//表示從文件末尾
 
3.1.8.三、用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)一個進程中兩次打開同一個文件,而後分別讀取,看結果會怎麼樣
(2)結果無非2種狀況:一種是fd1和fd2分別讀,第二種是接續讀。通過實驗驗證,證實告終果是fd1和fd2分別讀。
(3)分別讀說明:咱們使用open兩次打開同一個文件時,fd1和fd2所對應的文件指針是不一樣的2個獨立的指針。文件指針是包含在動態文件的文件管理表中的,因此能夠看出linux系統的進程中不一樣fd對應的是不一樣的獨立的文件管理表。
 
3.1.9.二、重複打開同一文件寫入
(1)一個進程中2個打開同一個文件,獲得fd1和fd2.而後看是分別寫仍是接續寫?
(2)正常狀況下咱們有時候須要分別寫,有時候又須要接續寫,因此這兩種自己是沒有好壞之分的。關鍵看用戶需求
(3)默認狀況下應該是:分別寫(實驗驗證過的)
 
3.1.9.三、加O_APPEND解決覆蓋問題
(1)有時候咱們但願接續寫而不是分別寫?辦法就是在open時加O_APPEND標誌便可
 
3.1.9.四、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)文件共享就是同一個文件(同一個文件指的是同一個inode,同一個pathname)被多個獨立的讀寫體(幾乎能夠理解爲多個文件描述符)去同時(一個打開還沒有關閉的同時另外一個去操做)操做。
(2)文件共享的意義有不少:譬如咱們能夠經過文件共享來實現多線程同時操做同一個大文件,以減小文件讀寫時間,提高效率。
 
3.1.10.二、文件共享的3種實現方式
(1)文件共享的核心就是怎麼弄出來多個文件描述符指向同一個文件。
(2)常見的有3種文件共享的狀況:第一種是同一個進程中屢次使用open打開同一個文件,第二種是在不一樣進程中去分別使用open打開同一個文件(這時候由於兩個fd在不一樣的進程中,因此兩個fd的數字能夠相同也能夠不一樣),第三種狀況是後面要學的,linux系統提供了dup和dup2兩個API來讓進程複製文件描述符。
(3)咱們分析文件共享時的核心關注點在於:分別寫/讀仍是接續寫/讀
 
3.1.10.三、再論文件描述符
(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、一、2已經默認被系統佔用了,所以用戶進程獲得的最小的fd就是3了。
(6)linux內核佔用了0、一、2這三個fd是有用的,當咱們運行一個程序獲得一個進程時,內部就默認已經打開了3個文件,這三個文件對應的fd就是0、一、2。這三個文件分別叫stdin、stdout、stderr。也就是標準輸入、標準輸出、標準錯誤,所以用戶進程獲得的最小的fd就是3了。
(7)標準輸入通常對應的是鍵盤(能夠理解爲:0這個fd對應的是鍵盤的設備文件),標準輸出通常是LCD顯示器(能夠理解爲:1對應LCD的設備文件)
(8)printf函數其實就是默認輸出到標準輸出stdout上了。stdio中還有一個函數叫fpirntf,這個函數就能夠指定輸出到哪一個文件描述符中。
 
 
3.1.11. 文件描述符的複製1
3.1.11.一、dup和dup2函數介紹
 
3.1.11.二、使用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.三、使用dup的缺陷分析
(1)dup並不能指定分配的新的文件描述符的數字,dup2系統調用修復了這個缺陷,因此平時項目中實際使用時根據具體狀況來決定用dup仍是dup2.
3.1.11.四、練習
(1)以前課程講過0、一、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.一、使用dup2進行文件描述符複製
(1)dup2和dup的做用是同樣的,都是複製一個新的文件描述符。可是dup2容許用戶指定新的文件描述符的數字。
(2)使用方法看man手冊函數原型便可。
   int dup2(int oldfd, int newfd);
若是成功則返回一個咱們指定的文件描述符,反之出錯返回-1.
3.1.12.二、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.三、命令行中重定位命令   >
這個命令的主要做用就是在終端輸出的東西重定位到一個文件中來輸出。
(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.一、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.二、fcntl的經常使用cmd
(1)F_DUPFD這個cmd的做用是複製文件描述符(做用相似於dup和dup2),這個命令的功能是從可用的fd數字列表中找一個比arg大或者和arg同樣大的數字做爲oldfd的一個複製的fd,和dup2有點像可是不一樣。dup2返回的就是咱們指定的那個newfd不然就會出錯,返回-1;可是F_DUPFD命令返回的是>=arg的最小的那一個數字。
 
3.1.13.三、使用fcntl模擬dup2
 
 
3.1.14.標準IO庫介紹
3.1.14.一、標準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.二、經常使用標準IO函數介紹
(1)常見的標準IO庫函數有:fopen、fclose、fwrite、fread、ffulsh(刷新標準庫函數的緩存,直接寫進操做系統的緩衝區中)、fseek
 
3.1.14.三、一個簡單的標準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);
    }
}
相關文章
相關標籤/搜索