姓名:謝煥彬 學號:19020100303
一、Linux文件I/O概述
1、POSIX規(guī)范
POSIX(Portable Operating System Interface,可移植操作系統(tǒng)接口規(guī)范)標準最初由IEEE(Institute of Electrical and Electronics Engineers,電氣和電子工程師協(xié)會,是目前最大的全球性非營利性專業(yè)技術學會)制定,目的是提高UNIX環(huán)境下程序的可移植性。通俗來講,為一個兼容POSIX標準的操作系統(tǒng)編寫的應用程序,可以在任何其他兼容POSIX標準的操作系統(tǒng)上編譯執(zhí)行而無需修改代碼。常見的Linux與UNIX系統(tǒng)都支持POSIX標準。
2、虛擬文件系統(tǒng)VFS
Linux系統(tǒng)的一個成功的關鍵因素是它具有與其他操作系統(tǒng)共存的能力。Linux的文件系統(tǒng)由兩層結構搭建:上面的虛擬文件系統(tǒng)VFS(Virtual File System),和下面的各種不同的具體文件系統(tǒng)(例如Ext、FAT32、NFS等)。
VFS將各種具體的文件系統(tǒng)的公共部分抽取出來形成一個抽象層,位于用戶的程序與具體需要使用的系統(tǒng)中間,并提供系統(tǒng)調用接口。這樣我們只需針對VFS提供的系統(tǒng)調用進行文件操作而無需具體考慮底層細節(jié)。VFS屏蔽了用戶對底層細節(jié)的描述使得編程簡化。
可以使用指令:
cat /proc/filesystems
查看當前操作系統(tǒng)支持哪些具體文件系統(tǒng)。
3、文件與文件描述符
Linux操作系統(tǒng)是基于文件概念搭建起來的操作系統(tǒng)(“萬物皆文件”),基于這一點,所有的I/O設備都可以直接當做文件來處理。因此操作普通文件的操作函數(shù)與操作設備文件的操作函數(shù)是相同的,這樣大大簡化了系統(tǒng)對不同設備、不同文件的處理,提高了效率。
那么對于內核而言,內核是如何區(qū)分不同的文件呢?內核使用文件描述符來索引打開的文件。文件描述符是一個非負整數(shù),每當打開一個存在的文件或創(chuàng)建一個新文件的時候,內核會向進程返回一個文件描述符,當對文件進行相應操作的時候,使用文件描述符作為參數(shù)傳遞給相應的函數(shù)。
通常一個進程啟動時,都會打開三個流:標準輸入、標準輸出、標準錯誤輸出,這三個流的文件描述符分別是0、1、2,對應的宏定義是STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO??梢圆榭搭^文件unistd.h查看相關定義。
流的名稱 文件描述符 宏定義
標準輸入 0 STDIN_FILENO
標準輸出 1 STDOUT_FILENO
標準錯誤輸出 2 STDERR_FILENO
基于文件描述符的I/O操作雖然不能直接移植到諸如Windows系統(tǒng)等之外的操作系統(tǒng)上,但對于某些底層的I/O操作(例如驅動程序、網(wǎng)絡連接等)是唯一的操作途徑。
4、標準I/O與文件I/O的區(qū)別:
1.文件I/O又稱為低級磁盤I/O,遵循POSIX標準。任何兼容POSIX標準的操作系統(tǒng)都支持文件I/O。標準I/O又稱為高級磁盤I/O,遵循ANSI C相關標準。只要開發(fā)環(huán)境有標準C庫,標準I/O就可以使用。
在Linux系統(tǒng)中使用GLIBC標準,它是標準C庫的超集,既支持ANSI C中定義的函數(shù)又支持POSIX中定義的函數(shù)。因此Linux下既可以使用標準I/O,也可以使用文件I/O。
2.通過文件I/O讀寫文件時,每次操作都會執(zhí)行相關系統(tǒng)調用。這樣的好處是直接讀寫實際文件,壞處是頻繁的系統(tǒng)調用會增加系統(tǒng)開銷。標準I/O在文件I/O的基礎上封裝了緩沖機制,每次先操作緩沖區(qū),必要時再訪問文件,從而減少了系統(tǒng)調用的次數(shù)。
3.文件I/O使用文件描述符打開操作一個文件,可以訪問不同類型的文件(例如普通文件、設備文件和管道文件等)。而標準I/O使用FILE指針來表示一個打開的文件,通常只能訪問普通文件。
二、文件I/O編程
1、打開文件
函數(shù)open()
需要頭文件:#include<sys/stat.h>
#include<fcntl.h>
函數(shù)原型:int open(const char *pathname,int flags,int perms);
函數(shù)參數(shù):pathname:打開文件名(可以包含具體路徑名)
flags:打開文件的方式,具體見下
perms:新建文件的權限,可以使用宏定義或者八進制文件權限碼,具體見下
函數(shù)返回值:成功:文件描述符
失?。?1
參數(shù)2flags具體可用參數(shù)(若使用多個flags參數(shù)可以使用|組合):
O_RDONLY:以只讀方式打開文件
O_WRONLY:以只寫方式打開文件
O_RDWR:以可讀可寫方式打開文件
O_CREAT:如果文件不存在,就創(chuàng)建這個文件,并使用參數(shù)3為其設置權限
O_EXCL:如果使用O_CREAT創(chuàng)建文件時文件已存在則返回錯誤信息。使用這個參數(shù)可以測試文件是否已存在
O_NOCTTY:若打開的是一個終端文件,則該終端不會成為當前進程的控制終端
O_TRUNC:若文件存在,則刪除文件中全部原有數(shù)據(jù)并設置文件大小為0
O_APPEND:以添加形式打開文件,在對文件進行寫數(shù)據(jù)操作時數(shù)據(jù)添加到文件末尾
注意:O_RDONLY與O_WRONLY與O_RDWR三個參數(shù)互斥,不可同時使用
若在參數(shù)2的位置有多個參數(shù)進行組合,注意使用按位或(|)運算符。
/** 可查看/usr/include/i386-linux-gnu/bits/fcntl.h文件看到具體的宏定義 **/
參數(shù)3perms表示新建文件的權限,可以使用宏定義或八進制文件權限碼。其中宏定義的格式是:S_I(R/W/X)(USR/GRP/OTH),其中R/W/X代表可讀/可寫/可執(zhí)行,USR/GRP/OTH代表文件所有者/文件組/其他用戶。例如:
S_IRUSR|S_IWUSR表示設置文件所有者具有可讀可寫權限,即0600。(一般情況下該參數(shù)都直接使用八進制文件權限碼因為使用宏定義的形式太復雜)。
2、關閉文件
函數(shù)close()
需要頭文件:#include<unistd.h>
函數(shù)原型:int close(int fd);
函數(shù)參數(shù):fd:文件描述符
函數(shù)返回值:成功:0
失敗:-1
示例:使用open()與close()打開文件和關閉文件
include<stdio.h>
include<stdlib.h>
include<unistd.h>
include<sys/stat.h>
include<fcntl.h>
int main()
{
int fd;
if((fd=open("hello.txt",O_RDWR|O_CREAT|O_TRUNC,0666))<0)
{
perror("fail to open file");
exit(0);
}
close(fd);
return 0;
}
練習:說明以下在標準I/O中打開文件的模式所對應的在文件I/O中的模式(即flags的參數(shù)組合),其中文件名使用命令行傳參的形式
例:w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)
r
r+
w
w+
a
a+
答案:
r -----> open(argv[1],O_RDONLY)
r+ ----> open(argv[1],O_RDWR)
w -----> open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666)
w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)
a -----> open(argv[1],O_WRONLY|O_CREAT|O_APPEND,0666)
a+ ----> open(argv[1],O_RDWR|O_CREAT|O_APPEND,0666)
3、文件讀寫
函數(shù)read()
需要頭文件:#include<unistd.h>
函數(shù)原型:int read(int fd,void *buf,size_t count);
函數(shù)參數(shù):fd:文件描述符
buf:讀取出的數(shù)據(jù)存放的緩沖區(qū)(內存地址)
count:指定讀取的字節(jié)數(shù)
函數(shù)返回值:成功:讀到的字節(jié)數(shù) 或 0(表示文件已結尾)
失?。?1
函數(shù)write()
需要頭文件:#include<unistd.h>
函數(shù)原型:ssize_t write(int fd,void *buf,size_t count);
函數(shù)參數(shù):fd:文件描述符
buf:待寫入的數(shù)據(jù)存放的緩沖區(qū)
count:指定寫入的字節(jié)數(shù)
函數(shù)返回值:成功:已寫的字節(jié)數(shù)
失敗:-1
示例:使用read()和write()函數(shù),先向文件中寫入一些數(shù)據(jù),之后讀取出來
include<stdio.h>
include<stdlib.h>
include<strings.h>
include<unistd.h>
include<sys/stat.h>
include<fcntl.h>
define MAX 128
int main(int argc,char *argv[])
{
int fdread,fdwrite;
char readbuffer[MAX]={0},writebuffer[MAX]; //相當于自己設置了兩塊緩沖區(qū)
if(argc<2)
{
perror("arguments are too few");
exit(0);
}
//先打開文件寫入數(shù)據(jù)
if((fdwrite=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)
{
perror("fail to open file");
exit(0);
}
printf("請輸入寫入的內容:");
scanf("%[^\n]",writebuffer);
write(fdwrite,writebuffer,MAX);
close(fdwrite);
//再打開文件讀取剛寫入的內容
int n=0,sum=0;
if((fdread=open(argv[1],O_RDONLY))<0)
{
perror("fail to open file");
exit(0);
}
while((n=read(fdread,readbuffer,MAX))>0)
{
sum += n;
printf("%s",readbuffer);
bzero(readbuffer,MAX);
}
printf("共讀取到%d個字節(jié)\n",sum);
close(fdread);
return 0;
}
執(zhí)行示例程序后查看文件可以發(fā)現(xiàn)文件除了寫入的字符外,還有部分亂碼字符。這是因為writebuffer[]數(shù)組沒有初始化,存儲了部分隨機數(shù),未被數(shù)據(jù)覆蓋掉的部分也同時被寫入了文件中。
思考:如何解決這個問題?
練習:使用文件I/O的read()/write()函數(shù)實現(xiàn)文件的復制
include<stdio.h>
include<stdlib.h>
include<unistd.h>
include<sys/stat.h>
include<fcntl.h>
define MAX 128
int main(int argc,char *argv[])
{
int fdread,fdwrite;
char buffer[MAX]={0};
int n=0,sum=0;
if(argc<3)
{
printf("arguments are too few, Usage:%s <src_file> <dst_file>\n",argv[0]);
exit(0);
}
if((fdread=open(argv[1],O_RDONLY))<0)
{
perror("fail to open file");
exit(0);
}
if((fdwrite=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)
{
perror("fail to open file");
exit(0);
}
while((n=read(fdread,buffer,MAX))>0)
{
sum += n;
write(fdwrite,buffer,n);
}
printf("復制文件成功,共操作%d字節(jié)\n",sum);
close(fdread);
close(fdwrite);
return 0;
}
4、文件定位
函數(shù)lseek()
需要頭文件:#include<unistd.h>
#include<sys/types.h>
函數(shù)原型:off_t lseek(int fd,off_t offset,int whence);
函數(shù)參數(shù):fd:文件描述符
offset:相對于基準點whence的偏移量,正數(shù)表示向前移動,負數(shù)表示向后移動,0表示不移動
whence:基準點(取值同標準I/O內fseek()函數(shù)第三個參數(shù))
函數(shù)返回值:成功:當前讀寫位置
失?。?1
其中第三個參數(shù)whence的取值如下:
SEEK_SET:代表文件起始位置,數(shù)字表示為0
SEEK_CUR:代表文件當前的讀寫位置,數(shù)字表示為1
SEEK_END:代表文件結束位置,數(shù)字表示為2
lseek()僅將文件的偏移量記錄在內核內而不進行任何I/O操作。
注意:lseek()函數(shù)僅能操作常規(guī)文件,一些特殊的文件(例如socket文件、管道文件等)無法使用lseek()函數(shù)。
示例:讀取文件的最后10個字節(jié)的數(shù)據(jù)
include<stdio.h>
include<stdlib.h>
include<unistd.h>
include<sys/stat.h>
include<fcntl.h>
define MAX 10
int main(int argc,char *argv[])
{
int fd;
char buffer[MAX]={0};
int n=0,sum=0;
if(argc<2)
{
printf("arguments are too few\n",argv[0]);
exit(0);
}
if((fd=open(argv[1],O_RDONLY))<0)
{
perror("cannot open file");
exit(0);3
}
lseek(fd,-10,SEEK_END);
if(read(fd,buffer,MAX)>0)
printf("讀到的數(shù)據(jù):%s\n",buffer);
else
printf("讀取出錯!\n");
close(fd);
return 0;
}
思考:若將基準點設置為SEEK_END但是偏移量是正數(shù)(即從文件末尾再向后偏移),會產生什么情況?
/*******************文件空洞******************/
若將lseek()函數(shù)的基準點設置為SEEK_END但是偏移量是正數(shù)(即從文件末尾再向后偏移),則會產生“文件空洞”的情況。#但是不能從文件頭向前偏移#
文件的偏移量是從文件開始位置開始計算的,若文件的偏移量大于了文件的實際數(shù)據(jù)長度,則會延長該文件,形成空洞。
示例:創(chuàng)建一個有空洞的文件。故意在文件結尾偏移好多個字節(jié),然后再寫入數(shù)據(jù)
include<stdio.h>
include<stdlib.h>
include<string.h>
include<unistd.h>
include<sys/stat.h>
include<fcntl.h>
int main(int argc,char *argv[])
{
int fd;
int n;
char buf1[]="LiLaoShiZhenShuai!";
char buf2[]="ABCDEFG";
if(argc<2)
{
printf("arguments are too few\n");
exit(0);
}
if((fd=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666))<0)
{
perror("cannot open file");
exit(0);
}
write(fd,buf1,strlen(buf1));//首先寫入某些數(shù)據(jù)
n=lseek(fd,0,SEEK_END);//返回文件末尾位置求出文件大小
printf("原先的文件大小是%d\n",n);
n=lseek(fd,987654321,SEEK_END);//在文件末尾向后偏移很多字節(jié)
printf("此時偏移量是%d\n",n);
write(fd,buf2,strlen(buf2));//寫入buf內數(shù)據(jù)
n=lseek(fd,0,SEEK_END);
printf("空洞后文件大小是%d\n",n);
close(fd);
return 0;
}
程序執(zhí)行后,使用vim查看該文件,會發(fā)現(xiàn)在兩段數(shù)據(jù)之間有一段亂碼數(shù)據(jù),并且使用ls -l指令查看,文件的大小也變大了。
在UNIX/Linux文件操作中,文件位移量可以大于文件的當前長度,在這種情況下,對該文件的下一次寫將延長該文件,并在文件中構成一個空洞,這一點是允許的。位于文件中但沒有寫過的字節(jié)都被設為0,用read讀取空洞部分讀出的數(shù)據(jù)是0。
空洞文件作用很大,例如迅雷下載文件,在未下載完成時就已經(jīng)占據(jù)了全部文件大小的空間,這時候就是空洞文件。下載時如果沒有空洞文件,多線程下載時文件就都只能從一個地方寫入,這就不是多線程了。如果有了空洞文件,可以從不同的地址寫入,就完成了多線程的優(yōu)勢任務。
/*******************文件空洞end***************/
5、文件鎖(選學)
通過之前的open()/close()/read()/write()/lseek()函數(shù)已經(jīng)可以實現(xiàn)文件的打開、關閉、讀寫等基本操作,但是這些基本操作是不夠的。對于文件的操作而言,“鎖定”操作是對文件(尤其是對共享文件)的一種高級的文件操作。當某進程在更新文件內數(shù)據(jù)時,期望某種機制能防止多個進程同時更新文件從而導致數(shù)據(jù)丟失,或者防止文件內容在未更新完畢時被讀取并引發(fā)后續(xù)問題,這種機制就是“文件鎖”。
對于共享文件而言,不同的進程對同一個文件進行同時讀寫操作將極有可能出現(xiàn)讀寫錯誤、數(shù)據(jù)亂碼等情況。在Linux系統(tǒng)中,通常采用“文件鎖”的方式,當某個進程獨占資源的時候,該資源被鎖定,其他進程無法訪問,這樣就解決了共享資源的競爭問題。
文件鎖包括建議性鎖(又名“協(xié)同鎖”)和強制性鎖兩種。建議性鎖要求每個相關進程訪問文件的時候檢查是否已經(jīng)有鎖存在并尊重當前的鎖(也可以不尊重)。一般情況下不建議使用建議性鎖,因為無法保證每個進程都能自動檢測是否有鎖,Linux內核與系統(tǒng)總體上都堅持不使用建議性鎖。而強制性鎖是由內核指定的鎖,當一個文件被加強制性鎖的過程中,直至該所被釋放之前,內核將阻止其他任何進程對該文件進行讀或寫操作,每次讀或寫操作都得檢測鎖是否存在。當然,采用強制性鎖對內核的性能影響較大,每次內核在操作文件的時候都需要檢查是否有強制性鎖。
在Linux內核提供的系統(tǒng)調用中,實現(xiàn)文件上鎖的函數(shù)有l(wèi)ockf()和fcntl(),其中l(wèi)ockf()用于對文件加建議性鎖,這里不再講解。fcntl()函數(shù)既可以加建議性鎖,也可以加強制性鎖。同時,fcntl()還能對文件某部分上記錄鎖。所謂記錄鎖,其實就是字節(jié)范圍鎖,它能鎖定文件內某個特定區(qū)域,當然也可鎖定整個文件。
記錄鎖又分為讀鎖和寫鎖兩種。其中讀鎖又稱為共享鎖,它用來防止進程讀取的文件記錄被更改。記錄內可設置多個讀鎖,但當有一個讀鎖存在的時候就不能在該記錄區(qū)域設置寫鎖。寫鎖又稱為排斥鎖,在任何時刻只能有一個程序對文件的記錄加寫鎖,它用來保證文件記錄被某一進程更新數(shù)據(jù)的時候不被其他進程干擾,確保文件數(shù)據(jù)的正確性,同時也避免其他進程“弄臟”數(shù)據(jù)。文件記錄一旦被設置寫鎖,就不能再設置任何鎖直至該寫鎖解鎖。
本節(jié)只簡單講述fcntl()對文件施加讀鎖和寫鎖并查看兩種鎖的效果,有關函數(shù)fcntl()的更詳細用法請查閱fcntl()手冊(man fcntl)。
函數(shù)fcntl()
需要頭文件:#include<unistd.h>
#include<sys/types.h>
#include<fcntl.h>
函數(shù)原型:int fcntl(int fd,int cmd,struct flock *lock_set);
函數(shù)參數(shù):fd:文件描述符
cmd:檢測鎖或設置鎖
lock_set:結構體類型指針,結構體struct flock需要事先設置,與第二個參數(shù)連用
函數(shù)返回值:成功:0
失敗:-1
第二個參數(shù)cmd表示該操作對文件的命令,若該命令是對文件檢測鎖或施加鎖,則需要第三個參數(shù):
F_GETLK:檢測文件鎖狀態(tài),檢測結果存放在第三個參數(shù)的結構體的l_type內
F_SETLK:對文件進行鎖操作,鎖操作類型存放在第三個參數(shù)的結構體的l_type內
F_SETLKW:同F(xiàn)_SETLK,不過使用該參數(shù)時若不能對文件進行鎖操作則會阻塞直至可以進行鎖操作為止(W即wait,等待)
(更多參數(shù)請參閱fcntl()函數(shù)的使用手冊)
第三個參數(shù)是對文件施加鎖操作的相關參數(shù)設置的結構體
注意:必須定義struct flock類型結構體并初始化結構體內的數(shù)據(jù),然后使用地址傳遞的方式傳遞參數(shù),不允許直接定義struct flock* 類型指針直接傳參
關于struct flock的成員如下:
struct flock
{
short l_type; //文件鎖類型:F_RDLCK, F_WRLCK, F_UNLCK
short l_whence; //起始位置:SEEK_SET, SEEK_CUR, SEEK_END
off_t l_start;
off_t l_len;
pid_t l_pid; //將文件鎖定的進程PID(僅在F_GETLK時有效)
}
結構體成員說明:
l_type:有三個參數(shù)
F_RDLCK:讀鎖(共享鎖)
F_WRLCK:寫鎖(排斥鎖)
F_UNLCK:無鎖/解鎖
l_whence:相對于偏移量的起點,參數(shù)等同于fseek()與lseek()中的whence參數(shù)
SEEK_SET:位置為文件開頭位置
SEEK_CUR:位置為文件當前讀寫位置
SEEK_END:位置為文件結尾位置
l_start:加鎖區(qū)域在文件中的相對位移量,與l_whence的值共同決定加鎖區(qū)域的起始位置
l_len:加鎖區(qū)域的長度,若為0則表示直至文件結尾EOF
l_pid:具有阻塞當前進程的鎖,其持有的進程號會存放在l_pid中,僅由F_GETLK返回
思考:如何設置該結構體內的成員使得加鎖的范圍為整個文件?
答案:設置l_whence為SEEK_SET,l_start為0,l_len為0即可。
示例:使用fcntl()函數(shù)對文件進行鎖操作。首先初始化結構體flock中的值,然后調用兩次fcntl()函數(shù)。第一次參數(shù)設定為F_GETLK判斷是否可以執(zhí)行flock內所描述的鎖操作;第二次參數(shù)設定為F_SETLK或F_SETLKW對該文件進行鎖操作。
注意:需要至少兩個終端運行該程序才能看到效果
include<stdio.h>
include<stdlib.h>
include<unistd.h>
include<fcntl.h>
include<sys/types.h>
int lock_set(int fd,int type)
{
struct flock lock;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;//三個參數(shù)設置鎖的范圍是全文件
lock.l_type = type;//type的參數(shù)由主調函數(shù)傳參而來
lock.l_pid = -1;
//第一次操作,判斷該文件是否可以上鎖
fcntl(fd,F_GETLK,&lock);
if(lock.l_type!=F_UNLCK)//如果l_type得到的返回值不是F_UNLCK則證明不能加鎖,需判斷原因
{
if(lock.l_type==F_RDLCK)
{
printf("This is a ReadLock set by %d\n",lock.l_pid);
}
else if(lock.l_type==F_WRLCK)
{
printf("This is a WriteLock set by %d\n",lock.l_pid);
}
}
//第二次操作,對文件進行相應鎖操作
lock.l_type = type;
if((fcntl(fd,F_SETLKW,&lock))<0)
{
printf("Lock Failed:type = %d\n",lock.l_type);
return -1;
}
switch(lock.l_type)
{
case F_RDLCK:
printf("ReadLock set by %d\n",getpid());break;
case F_WRLCK:
printf("WriteLock set by %d\n",getpid());break;
case F_UNLCK:
printf("ReleaseLock by %d\n",getpid());
return 1;
break;
}
return 0;
}
int main(int argc, const char *argv[])
{
int fd;
if((fd=open("hello.txt",O_RDWR))<0)
{
perror("fail to open hello.txt");
exit(0);
}
printf("This pid_no is %d\n",getpid());
//給文件上鎖
lock_set(fd,F_WRLCK);
printf("Press ENTER to continue...\n");
getchar();
//給文件解鎖
lock_set(fd,F_UNLCK);
close(fd);
return 0;
}
這里標注比較隨意,可以看我的印象筆記:文件IO 第二天 (文件IO)
————————————————
版權聲明:本文為CSDN博主「nan_lei」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權協(xié)議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/nan_lei/article/details/81488349