今天我們總結一下,linux中常用文件I/O操作。
首先讓我們看一下,什么是文件I/O:
所謂文件I/O就是:對于I/O就是input/output,輸入/輸出。文件IO的意思就是讀寫文件。
1、linux給我們留的常用文件I/O接口。
1、open close write read lseek
2、文件操作的一般步驟:
1、在linux中要操作一個文件,一般是先open打開一個文件,得到文件描述符,然后對文件進行讀寫操作(或其他操作),最后是close關閉文件即可。
2、強調一點:我們對文件進行操作時,一定要先打開文件,打開成功之后才能操作,如果打開失敗,就不用進行后邊的操作了,最后讀寫完成后,一定要關閉文件,否則會造成文件損壞。
3、文件平時是存放在塊設備中的文件系統文件中的,我們把這種文件叫靜態文件,當我們去open打開一個文件時,linux內核做的操作包括:內核在進程中建立一個打開文件的數據結構,記錄下我們打開的這個文件;內核在內存中申請一段內存,并且將靜態文件的內容從塊設備中讀取到內核中特定地址管理存放(叫動態文件)。
4、打開文件以后,以后對這個文件的讀寫操作,都是針對內存中的這一份動態文件的,而并不是針對靜態文件的。當然我們對動態文件進行讀寫以后,此時內存中動態文件和塊設備文件中的靜態文件就不同步了,當我們close關閉動態文件時,close內部內核將內存中的動態文件的內容去更新(同步)塊設備中的靜態文件。
5、為什么這么設計,不直接對塊設備直接操作。
塊設備本身讀寫非常不靈活,是按塊讀寫的,而內存是按字節單位操作的,而且可以隨機操作,很靈活。
3、重要概念:
文件描述符:
1、對于內核而言,所有打開文件都由文件描述符引用。文件描述符是一個非負整數。當打開一個現存文件或者創建一個新文件時,內核向進程返回一個文件描述符。當讀寫一個文件時,用open和creat返回的文件描述符標識該文件,將其作為參數傳遞給read和write。
按照慣例,UNIX shell使用文件描述符0與進程的標準輸入相結合,文件描述符1與標準輸出相結合,文件描述符2與標準錯誤輸出相結合。STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO這幾個宏代替了0、1、2這幾個魔數。
2、文件描述符,這個數字在一個進程中表示一個特定含義,當我們open一個文件時,操作系統在內存中構建了一些數據結構來表示這個動態文件,然后返回給應用程序一個數字作為文件描述符,這個數字就和我們內存中維護的這個動態文件的這些數據結構綁定上了,以后我們應用程序如果要操作這個動態文件,只需要用這個文件描述符區分。
3、文件描述符的作用域就是當前進程,出了這個進程文件描述符就沒有意義了。
open函數打開文件,打開成功返回一個文件描述符,打開失敗,返回-1。
補充:這里我們補充一點,
1、學習linux過程中注意學會使用man手冊,查詢幫助文檔。
2、man 1 xx查linux shell命令,man 2 xxx查API, man 3 xxx查庫函數
4、常用文件I/O操作的使用:
1、open函數(打開文件操作)
需要用到的頭文件 #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> 函數原型: int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); 返回值:若成功返回文件描述符,若出錯返回 -1
對于open函數而言,僅當創建新文件時才使用第三個參數。namepath是要打開或創建的文件的名字。oflag參數可以用來說明函數的多個選項。用下列一個或者多個常數進行或運算構成oflag參數(這些參數定義在<fcntl.h>頭文件中):
open函數flags參數詳解:
讀寫權限:O_RDONLY O_WRONLY O_RDWR
O_RDONLY 只讀打開
O_WRONLY 只寫打開
O_RDWR 可讀可寫打開
當我們附帶了權限后,打開的文件就只能按照這種權限來操作。以上這三個常數中應當只指定一 個。下列常數是可選擇的:
O_CREAT 若文件不存在則創建它。使用此選項時,需要同時說明第三個參數mode,用其說明該新文件的存取許可權限。
O_EXCL 如果同時指定了OCREAT,而文件已經存在,則出錯。這可測試一個文件是
否存在,如果不存在則創建此文件成為一個原子操作。3 . 11節將較詳細地說明原子操作。
O_APPEND 每次寫時都加到文件的尾端。
O_TRUNC 屬性去打開文件時,如果這個文件中本來是有內容的,而且為只讀或只寫成功打開,則將其長度截短為0。
重點:
一: 打開存在并有內容的文件時:O_APPEND、O_TRUNC
(1)思考一個問題:當我們打開一個已經存在并且內部有內容的文件時會怎么樣?
可能結果1:新內容會替代原來的內容(原來的內容就不見了,丟了)
可能結果2:新內容添加在前面,原來的內容繼續在后面
可能結果3:新內容附加在后面,原來的內容還在前面
可能結果4:不讀不寫的時候,原來的文件中的內容保持不變
(2)O_TRUNC屬性去打開文件時,如果這個文件中本來是有內容的,則原來的內容會被丟棄。這就對應上面的結果1
(3)O_APPEND屬性去打開文件時,如果這個文件中本來是有內容的,則新寫入的內容會接續到原來內容的后面,對應結果3
(4)默認不使用O_APPEND和O_TRUNC屬性時就是結果4
(5)如果O_APPEND和O_TRUNC同時出現會會清空文件。
二:打開不存在的文件時: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
O_NOCTTY 如果pathname指的是終端設備,則不將此設備分配作為此進程的控制終端。9.6節將說明控制終端。
O_NONBLOCK 如果pathname指的是一個FIFO、一個塊特殊文件或一個字符特殊文件,則此選擇項為此文件的本次打開操作和后續的I/O操作設置非阻塞方式。(只用于設備文件,而不用于普通文件。)
(1)阻塞與非阻塞。如果一個函數是阻塞式的,則我們調用這個函數時當前進程有可能被卡?。ㄗ枞?,實質是這個函數內部要完成的事情條件不具備,當前沒法做,要等待條件成熟),函數被阻塞住了就不能立刻返回;如果一個函數是非阻塞式的那么我們調用這個函數后一定會立即返回,但是函數有沒有完成任務不一定。
(2)阻塞和非阻塞是兩種不同的設計思路,并沒有好壞??偟膩碚f,阻塞式的結果有保障但是時間沒保障;非阻塞式的時間有保障但是結果沒保障。
(3)操作系統提供的API和由API封裝而成的庫函數,有很多本身就是被設計為阻塞式或者非阻塞式的,所以我們應用程度調用這些函數的時候心里得非常清楚。
(4)我們打開一個文件默認就是阻塞式的,如果你希望以非阻塞的方式打開文件,則flag中要加O_NONBLOCK標志。
O_SYNC 使每次write都等到物理I/O操作完成
(1)write阻塞等待底層完成寫入才返回到應用層。
(2)無O_SYNC時write只是將內容寫入底層緩沖區即可返回,然后底層(操作系統中負責實現open、write這些操作的那些代碼,也包含OS中讀寫硬盤等底層硬件的代碼)在合適的時候會將buf中的內容一次性的同步到硬盤中。這種設計是為了提升硬件操作的性能和銷量,提升硬件壽命;但是有時候我們希望硬件不好等待,直接將我們的內容寫入硬盤中,這時候就可以用O_SYNC標志。
2、creat函數(也可用creat函數創建一個新文件)
需要用到的頭文件 #include<sys/types.h> #include<sys/stat.h> #include<fcutl.h> 函數原型: int creat(const char *pathname, mode_t mode); 返回:若成功為只寫打開的文件描述符,若出錯為-1
此函數等效于 open(pathname,O_WRONLY|O_CRAT|O_TRUNC,mode);
3、read 函數(用read函數從打開的文件中讀取數據)
需要的頭文件: #include<unistd.h> 函數原型: ssize_t read(int filedes,void *buffer,size_t nbytes); 返回值:讀取到字節數,若已到文件尾0,則返回-1如果read成功,則返回讀取到字節數。如已到文件結尾返回0;
有多種情況可使實際讀到的字節數少于要求讀字節數:
1、讀取普通文件時,在讀到要求字節數之前已經到達了文件結尾。例如,若在到達文件尾端之前還有30個字節,而要求讀100個字節,則read返回30,下一次調用read時,它將返回0(文件尾端)。
2、當從終端設備讀時,通常一次最多讀一行
3、當從網絡讀時,網絡中緩存機構可能造成返回值小于所要求讀的字節數。
4、某些面向記錄的設備,例如磁帶,一次最多返回一個記錄。
4、write函數(用write函數向打開的文件寫數據)
需要用到的頭文件 #include<unistd.h> 函數原型: ssize_t write(int filedes,const void buffer,size_t nbytes); 返回值:若成功返回已寫字節數,若出錯返回-1 其返回值通常與參數nbyte的值不同,否則表示出錯。write出錯的一個常見原因是:磁盤已寫滿,或者超過了對一個給定進程的文件長度限制。
寫入用write系統調用,write的原型和理解方法和read相似
5、close函數(可以用close函數關閉一個打開是文件)
需要用到的頭文件: #include<unistd.h> 函數原型: int close(int filedes); 返回值:成功返回0,若出錯返回-1
當一個進程終止時,它所有的打開的文件都是由內核自動關閉。
6、lseek函數
每個打開文件都有一個與其相關聯的“當前文件位移量”。它是一個非負整數,用以度量從文件開始處計算的字節數。
需要用到的頭文件 #include<sys/types.h> #include<unistd.h> 函數原型: off_t lseek(int filedes,off_t offset,int whence); 返回值:若成功為新的文件的位移,若出錯位-1
對參數offset 的解釋與參數whence的值有關。
若whence是SEEK_SET,則將該文件的位移量設置為距文件開始處offset 個字節。
若whence是SEEK_CUR,則將該文件的位移量設置為其當前值加offset, offset可為正或負。
若whence是SEEK_END,則將該文件的位移量設置為文件長度加offset, 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字節確實寫進來了)。
lseek函數幾個用途:
1、用lseek計算文件長度
(1)linux中并沒有一個函數可以直接返回一個文件的長度。但是我們做項目時經常會需要知道一個文件的長度,怎么辦?自己利用lseek來寫一個函數得到文件長度即可。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
typedef int file_t;
#define MAXLENG 1024
int main(int argc,char *argv[])
{
file_t fd = -1;
ssize_t ret = -1;
if(2 != argc)
{
fprintf(stdout,"usage: %s filename \n",argv[0]);
_exit(-1);
}
char buffer[MAXLENG] = {0};
fd = open(argv[1],O_RDONLY);
//文件打開成功,文件指針指向文件開頭
if(-1 == fd)
{
perror("open file error:");
_exit(-1);
}
else
{
fprintf(stdout,"文件打開成功\n");
ret = lseek(fd,0,SEEK_END);
}
fprintf(stdout,"文件長度是: %d\n",ret);
return 0;
}2、用lseek構建空洞文件
(1)空洞文件就是這個文件中有一段是空的。
(2)普通文件中間是不能有空的,因為我們write時文件指針是依次從前到后去移動的,不可能繞過前面直接到后面。
(3)我們打開一個文件后,用lseek往后跳過一段,再write寫入一段,就會構成一個空洞文件。
(4)空洞文件方法對多線程共同操作文件是及其有用的。有時候我們創建一個很大的文件,如果從頭開始依次構建時間很長。有一種思路就是將文件分為多段,然后多線程來操作每個線程負責其中一段的寫入。
補充:
1、exit、_exit、_Exit退出進程
(1)當我們程序在前面步驟操作失敗導致后面的操作都沒有可能進行下去時,應該在前面的錯誤監測中結束整個程序,不應該繼續讓程序運行下去了。
(2)我們如何退出程序?
第一種;在main用return,一般原則是程序正常終止return 0,如果程序異常終止則return -1。
第一種:正式終止進程(程序)應該使用exit或者_exit或者_Exit之一。
2、文件讀寫的一些細節
<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打印出來。
<2>、read和write的count
(1)count和返回值的關系。count參數表示我們想要寫或者讀的字節數,返回值表示實際完成的要寫或者讀的字節數。實現的有可能等于想要讀寫的,也有可能小于(說明沒完成任務)
(2)count再和阻塞非阻塞結合起來,就會更加復雜。如果一個函數是阻塞式的,則我們要讀取30個,結果暫時只有20個時就會被阻塞住,等待剩余的10個可以讀。
(3)有時候我們寫正式程序時,我們要讀取或者寫入的是一個很龐大的文件(譬如文件有2MB),我們不可能把count設置為2*1024*1024,而應該去把count設置為一個合適的數字(譬如2048、4096),然后通過多次讀取來實現全部讀完。
<3>、文件IO效率和標準IO
(1)文件IO就指的是我們當前在講的open、close、write、read等API函數構成的一套用來讀寫文件的體系,這套體系可以很好的完成文件讀寫,但是效率并不是最高的。
(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、linux系統如何管理文件
<1>、硬盤中的靜態文件和inode(i節點)
(1)文件平時都在存放在硬盤中的,硬盤中存儲的文件以一種固定的形式存放的,我們叫靜 態文件。
(2)一塊硬盤中可以分為兩大區域:一個是硬盤內容管理表項,另一個是真正存儲內容的區域。操作系統訪問硬盤時是先去讀取硬盤內容管理表,從中找到我們要訪問的那個文件的扇區級別的信息,然后再用這個信息去查詢真正存儲內容的區域,最后得到我們要的文件。
(3)操作系統最初拿到的信息是文件名,最終得到的是文件內容。第一步就是去查詢硬盤內容管理表,這個管理表中以文件為單位記錄了各個文件的各種信息,每一個文件有一個信息列表(我們叫inode,i節點,其實質是一個結構體,這個結構體有很多元素,每個元素記錄了這個文件的一些信息,其中就包括文件名、文件在硬盤上對應的扇區號、塊號那些東西·····)
強調:硬盤管理的時候是以文件為單位的,每個文件一個inode,每個inode有一個數字編號,對應一個結構體,結構體中記錄了各種信息。
(4)聯系平時實踐,大家格式化硬盤(U盤)時發現有:快速格式化和底層格式化??焖俑袷交浅??,格式化一個32GB的U盤只要1秒鐘,普通格式化格式化速度慢。這兩個的差異?其實快速格式化就是只刪除了U盤中的硬盤內容管理表(其實就是inode),真正存儲的內容沒有動。這種格式化的內容是有可能被找回的。
<2>、內存中被打開的文件和vnode(v節點)
(1)一個程序的運行就是一個進程,我們在程序中打開的文件就屬于某個進程。每個進程都有一個數據結構用來記錄這個進程的所有信息(叫進程信息表),表中有一個指針會指向一個文件管理表,文件管理表中記錄了當前進程打開的所有文件及其相關信息。文件管理表中用來索引各個打開的文件的index就是文件描述符fd,我們最終找到的就是一個已經被打開的文件的管理結構體vnode
(2)一個vnode中就記錄了一個被打開的文件的各種信息,而且我們只要知道這個文件的fd,就可以很容易的找到這個文件的vnode進而對這個文件進行各種操作。
<3>、文件與流的概念
(1)流(stream)對應自然界的水流。文件操作中,文件類似是一個大包裹,里面裝了一堆字符,但是文件被讀出/寫入時都只能一個字符一個字符的進行,而不能一股腦兒的讀寫,那么一個文件中N多的個字符被挨個一次讀出/寫入時,這些字符就構成了一個字符流。
(2)流這個概念是動態的,不是靜態的。
(3)編程中提到流這個概念,一般都是IO相關的。所以經常叫IO流。文件操作時就構成了一個IO流。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。