10.1 文件I/O操作概述
在Linux系統中,文件I/O操作可以分為兩類,一類是基于文件描述符的I/O操作,另一類是基于數據流的I/O操作。
10.1.1 文件描述符簡介
在文件操作一章中,也經常提到文件描述符這個概念。所謂文件描述符,就是進程與打開的文件的一個橋梁,通過這個橋梁,才可以在進程中對這個文件進行讀寫等操作。
在Linux環境下,每打開一個磁盤文件,都會在內核中建立一個文件表項,文件表項中存儲著文件的狀態信息、存儲文件內容的緩沖區和當前文件的讀寫位置。如果同一磁盤文件打開了3次,就會創建3個這樣的文件表項(a,b和c),讀寫文件時,只會改變該文件表項中的文件讀寫位置。這3個文件表項存儲在一個文件表數組table[3]中,在這里table[0]=a,table[1]=b,table[2]=c。這個文件表的下標就稱之為文件描述符,將這個文件描述符存儲在一個數組中des[3]={0,1,2},那么,在進程中就可以通過這個des數組下標引用文件表項。也就是說,通過文件描述符就可以訪問到這個磁盤文件。
10.1.2 數據流概述
從數據操作方式這個角度來說,Linux系統中的文件(無論是普通文件還是設備文件)可以看做是數據流。對文件進行操作之前,必須先調用標準I/O庫函數fopen()將數據流打開。打開數據流之后,就可以對數據流進行輸入和輸出的操作。
標準I/O庫函數是C語言中所特有的用于高級接口的函數,這些庫函數存放在C語言的stdio.h頭文件中,因此這些用于數據流的I/O操作函數不僅適用于Linux系統,還適用于其他的操作系統。由此可見,此庫函數的引用大大增加了程序的移植性。
要對數據流進行讀寫操作時,需要標準I/O庫函數和FILE類型的文件指針一起來實現。這個文件指針石達開數據流時返回的指針,該指針用來表示要操作的數據流。
當執行程序時,有3個數據流不需要特定的函數進行打開的操作,他們會自動打開。這3個數據流是標準輸入、標準輸出和標準錯誤輸出。他們是自動打開的,當不使用時,也會自動關閉。
然而,調用標準I/O庫函數fopen()打開的數據流,在對數據流進行操作后,需要調用fclose()函數將其關閉。fclose()函數在關閉數據流之前,會清空在操作過程中分配的緩沖區并保存數據信息。
10.2 基于文件描述符的I/O操作
10.2.1 文件的打開與關閉
1)open()函數
#include
#include
#include
int open(const char *pathname, int flags)
int open(const char *pathname, int flags, mode_t mode)
int creat(const char *pathname, mode_t mode)
上述的兩個open()函數和一個creat()函數在調用成功時,都會返回其新分配的文件描述符;否則返值為-1,并設置適當的error。
2)close()函數
#incldue
int close(int fd)
當一個進程終止時,內核對該進程的所有尚未關閉的文件描述符調用close()函數關閉,所以即使用戶程序不調用close()函數,在終止時內核也會自動關閉它打開的所有文件。但是,對于網絡服務器這種一直運行的程序,文件描述符一定要及時關閉,否則隨著打開的文件越來越多,會占用大量文件描述符和系統資源。
有函數open()返回的文件描述符一定是該進程尚未使用的最小描述符。由于程序啟動時自動打開標準輸入、標準輸出和標準錯誤輸出,因此文件描述符0,1,2會存在,那么第一次調用open()函數打開文件時返回的文件描述符通常會是3,再調用open()函數就會返回4.可以利用這一點在標準輸入、標準輸出或標準錯誤輸出上打開一個新文件,是想重定向的功能。例如,首先調用close()函數關閉文件描述符1,然后調用open()函數打開一個常規文件,則一定會返回文件描述符1,這是標準輸出就不再是終端,而是一個常規文件了,再調用printf()函數就不打印到屏幕上,而是寫到這個文件中了。在文件操作一章中講到的dup2()函數就是另外一種在指定的文件描述符上打開文件的方法。
10.2.2 文件的讀寫操作
1)read()函數
#include
ssize_t read(int fd, void *buf, size_t count)
2)write()函數
#include
ssize_t write(int fd, const void *buf, size_t count)
10.2.3 文件的定位
每個文件都記錄著當前讀寫位置,打開文件時讀寫位置是0,表示文件開頭,通常讀寫多少個自己就會將讀寫位置往后移多少個字節。
以O_APPEND方式打開文件,每次寫操作都會在文件末尾追加數據,然后間讀寫位置移動到新的文件末尾。
1)lseek()函數
lseek()函數可以移動當前讀寫位置,通常稱為偏移量,該函數的定義形式如下:
#include
#include
off_t lseek(int fildes, off_t offset, int whence)
10.3 基于數據流的I/O操作
10.3.1 文件的打開與關閉
在操作文件之前要用fopen()函數打開文件,操作結束后,要用fclose()函數關閉文件。
1)fopen()函數
#include
FILE *foepn(cosnt char *path, cosnt char *mode)
2)fclose()函數
#include
int fclose(FILE *fp)
10.3.2 字符輸入/輸出
1)fgetc()函數
fgetc()函數從指定的文件中讀一個字節,該函數的定義形式如下:
#include
int fgetc(FILE *stream)
在程序中,偶爾會遇到getchar()函數,也是用于讀取一個字節,但它是從標準輸入讀一個字節。在程序中調用getchar()函數相當于調動fgetc(stdin)
在使用fgetc()函數時需要注意一下幾點:
①調用fgetc()函數時,指定的文件的打開方式必須是可讀的。
②函數fgetc()調用成功時,返回的是讀到的字節,應該為unsigned char,但fgetc()函數在原型中返回值類型時int,原因在于函數調用出錯或讀到文件末尾時fgetc()會返回EOF,即-1,保存在int型的返回值是0xffffffff,如果讀到字節0xff,由unsigned char型轉換int 型時0x000000ff,只有規定返回值是int型才能把這種情況區分開,如果規定返回值是unsigned char型,那么當返回值是0xff時則無法區分到底是EOF還是字節0xff。
2)fputc()函數
#include
int fputc(int c, FILE *stream)
10.3.3 字符串輸入/輸出
1)fgets()函數
#include
char *fgets(char *s, int size, FILE *stream)
對于fgets()函數而言,'\n'是一個特別的字符,作為結束符;而'\0'并無任何特別之處,只用作普通字符串讀入。正因為'\0'作為一個普通的字符串,因此無法判斷緩沖區中的'\0'究竟是從文件讀上來的字符還是有fgets()函數自主添加的結束符,所以fgets()函數只用于讀文本文件而不提倡讀二進制文件,并且文本文件中的所有字符串不能有'\0'。
2)fputs()
#include
int fputs(const char *s, FILE *stream)
緩沖區s中保存的是以'\0'結尾的字符串,fputs()將該字符串寫入文件stream,但并不寫入結尾的'\0',且字符串中可以有'\n',也可以沒有'\n'。
10.3.4 數據塊輸入/輸出
1)fread()和fwrite()函數
#include
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
10.3.5 格式化輸入/輸出
所謂格式化輸入/輸出,就是按照一定的格式將數據進行輸入/輸出操作。在程序中經常用到的printf()函數和scanf()函數是用于對終端設備文件的讀寫操作,這兩個函數被稱為格式化輸入/輸出,因為在使用這兩個函數時,需要制定讀寫數據的數據類型并按照一定的格式進行讀寫。
1)格式化輸入函數
#include
int printf(const char *format,...)
int fprintf(FILE *stream, const char *format)
int sprintf(char *str, size_t size, const char *format,...)
2)格式化輸出函數
#include
int scanf(const char *format,...)
int fscanf(FILE *stream, const char *format,...)
int sscanf(const char *str, const char *format,...)
10.3.6 操作讀寫位置的函數
1)fseek()函數
#inlcude
int fseek(FILE *stream, long offset, int whence)
函數fseek()的作用是用來移動文件內部位置指針。
2)ftell()函數
#inlcude
long ftell(FILE *stream)
ftell()函數的作用是得到stream指定的流式文件中的位置。
3)rewind()函數
void rewind(FILE *stream)
rewind()函數的作用是使位置指針重新返回文件的開頭,該函數沒有返回值。
10.3.7 C標準的I/O緩沖區
C標準庫在調用fopen()函數時,都會給此文件分配一個I/O緩沖區,可以加速讀寫操作,原因在于用戶程序需要調用C標準I/O庫函數(如fread()、fwrite()等基于文件流I/O操作)讀寫文件,當緩沖區裝滿后,再由系統調用的I/O函數(如read()、write()等基于文件描述符的I/O操作)把讀寫請求傳給內核,最終由內核驅動磁盤或設備完成I/O操作。
由此看來,為文件分配的內存緩沖區大小,直接影響到實際操作外村設備的次數,內存中為未見分配的緩沖區越大,操作外存的次數會越小,因此讀寫數據的速度會越來越快,效率就會隨之增高。
然而,有時用戶程序等不及將緩沖區都裝滿之后再傳給內核,進行I/O操作,而是希望把I/O緩沖區中的數據立刻傳給內核,讓內核寫回設備,這種行為叫做flush操作,對應的庫函數是fflush()。
C標準庫的I/O緩沖區有全緩沖、行緩沖和無緩沖3種類型。
1)全緩沖:如果緩沖區寫滿了,就寫回內核。普通文件通常是全緩沖的。
2)行緩沖:如果用戶程序寫的數據中有'\n',就把這一行寫回內核,或者緩沖區寫滿后就寫回內核。標準輸入和標準輸出對應中斷設備時通常是行緩沖。
3)無緩沖:用戶程序每次調庫函數做寫操作都要通過系統調用寫回內核。標準錯誤輸出通常是無緩沖的。這樣用戶程序產生的錯誤信息就可以盡快輸出到設備。
使用緩沖區時,會使用到如下兩類操作,一個是設置緩沖區屬性,另外一個是清空緩沖區。
4)設置緩沖區屬性
#include
void setbuf(FILE *stream, char *buf)
void setbuffer(FILE *stream, char *buf, size_t size)
void setlinebuf(FILE *stream)
int setvbuf(FILE *stream, char *buf, int mode, size_t size)
setbuf()函數主要實現了為參數buf所指定的緩沖區設置大小。此函數中,設定緩沖區大小的值只有兩個,一個是常數BUFSIZ,另一個是NULL。當定義值為BUFSIZ時,代表設置緩沖區為全緩沖;若為NULL,則代表設置緩沖區為無緩沖形式。
setbuffer()與setbuf()功能相同,只是setbuffer()函數可以任意指定緩沖區大小為size
setlinebuf()函數實現了將stream指向的緩沖區設置為行緩沖。
setvbuf()函數融合了上述3種函數的功能,既可以設置緩沖區的任意大小size,也可以設置緩沖區的任意類型,如mode參數取值為_IOFBF(全緩沖類型)、_IOLBF(行緩沖類型)或_IONBF(無緩沖類型)。
5)清空緩沖區
#include
int fflush(FILE *stream)
fflush()函數實現將緩沖區中的尚未寫入文件的數據強制性地寫進stream所指定的文件中,然后清空緩沖區。如果stream為NULL,此函數會將所有打開的文件數據更新。
10.4 小結
本章主要介紹了Linux系統下的文件I/O操作。在Linux系統下存在兩種文件I/O操作,一種是基于文件描述符的I/O操作,這里面的I/O操作都是Linux系統中提供并直接作用于內核的,是非緩沖的I/O操作;另一種I/O操作是基于數據流的I/O操作,是由C語言的stdio庫所提供的,需要在內存中開辟一塊緩沖區,在緩沖區中進行快速地讀寫操作。本章主要結合典型實例介紹了上述兩種I/O操作方式對文件的打開、關閉、讀、寫、文件定位等操作。
注:感覺本文10.3.5之前的內容對我有用,讓我大概明白了基于數據流的文件I/O和基于文件描述符的文件I/O操作之間的區別。但是如果在給出其中的從操作函數之后給出一些實例就更好了。