1. 程式人生 > >Unix程式設計:檔案I/O操作及檔案描述符

Unix程式設計:檔案I/O操作及檔案描述符

Unix系統中大多數檔案I/O需要用到以下五個函式:open,read,write,lseek以及close。這些函式通常被稱為不帶緩衝的I/O(這些函式都是在核心中執行,它們直接對核心快取區進行讀寫)。

檔案描述符

對於核心而言,所有開啟的檔案都通過檔案描述符引用,檔案描述符是一個非負整數,當開啟一個現有檔案或建立一個新檔案時,核心向程序返回一個檔案描述符。

open 函式:呼叫open函式可以開啟或建立一個檔案

#include <fcntl.h>
int open(const char *pathname, int oflag, .../*mode_t mode*/);
//返回值:若成功則返回檔案描述符,若出錯則返回-1
/*
pathname:要開啟或建立檔案的名字
oflags:檔案開啟的方式
第三個引數僅當open建立新檔案時才使用
*/

已開啟的檔案在核心中,用file結構體表示.

open函式具有處理符號連結(指向一個檔案的間接指標,也就是檔案的索引的索引)的功能,如果用open開啟檔案時,如果傳遞給open函式的路徑名指定了一個符號連結,那麼open函式跟隨連結到達你所指定的檔案,若此符號連結所指向的檔案並不存在,則open返回出錯。

creat 函式:建立一個新檔案

#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
//返回值:若成功則返回為只寫開啟的檔案描述符,若出錯則返回-1
/*該函式等效於 open(pathname, O_RDWR|O_CREAT|O_TRUNC, mode);
creat 函式存在不足,只能以只寫方式開啟所建立的檔案,現在一般呼叫open函式來建立*/

close 函式:關閉一個已經開啟的檔案

#include <unistd.h>
int close(int filedes);
//返回值:若成功則返回0,若出錯則返回-1
lseek 函式

每個開啟的檔案都有一個與其相關聯的“當前檔案偏移量”。用以度量從檔案開始處計算的位元組數。通常,讀、寫操作都是從當前檔案偏移量處開始,並使偏移量增加所讀寫的位元組數。系統預設開啟一個檔案,偏移量指定為零,除非指定O_APPEND選項

我們可以呼叫lseek 顯式地為一個開啟的檔案設定其偏移量

#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence);
//返回值:若成功則返回新的檔案偏移量,若出錯則返回-1
/*
引數offset 的解釋與引數whence的值有關,偏移量可以為負值,whence的值:
SEEK_SET:距檔案開始處offset 個位元組
SEEK_CUR:當前值加offset
SEEK_END:檔案長度加offset
*/
lseek 僅將當前的檔案偏移量記錄在核心中,它並不引起任何I/O操作,然後,該偏移量用於下一個讀或寫操作。

read 函式:從開啟檔案中讀資料

#include <unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
//返回值:若成功則返回讀到的位元組數,若已到檔案結尾則返回0,若出錯則返回-1
/*從filedes關聯的檔案中讀取nbytes的位元組數到buf中,然後返回實際讀的位元組數*/
讀操作從檔案的當前偏移量處開始,在成功返回之前,該偏移量將增加實際讀到的位元組數,read 有時實際讀到的位元組數會少於要求讀的位元組數。

write 函式:向開啟的檔案寫資料

#include <unistd.h>
ssize_t write(int filedes, const void *buf, size_t nbytes);
//返回值:若成功則返回已寫的位元組數,若出錯則返回-1
/*將buf中nbytes個位元組寫入到filedes關聯的檔案中,返回實際寫入的位元組數*/
對於普通檔案,寫操作從檔案的當前偏移量處開始,成功寫之後,該檔案偏移量增加實際寫的位元組數。

上面教科書式的介紹了這幾個常用的I/O函式。下面通過幾個簡單程式來理解這幾個函式的應用。

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFSIZE 4096

int main(void)
{
	int fd;
	int n;
	char buf[BUFFSIZE];
	char data[] = "sunburn";
	off_t offset;
	
	if((fd = open("examplefiles", O_RDWR)) == -1)
		perror("open error\n");
	printf("fd = %d\n", fd);

	offset = lseek(fd, 0, SEEK_CUR);
	printf("offset = %d\n", offset);

	if((n = read(fd, buf, BUFFSIZE)) == -1)
		perror("read error\n");

	if(write(STDOUT_FILENO, buf, n) != n)//標準輸出
	    perror("write error\n");
	
	offset = lseek(fd, -5, SEEK_CUR);//當前偏移量前移
	printf("offset = %d\n", offset);

	lseek(fd, 100, SEEK_SET);//設定新的偏移量
	write(fd, data, sizeof(data)/sizeof(char));//基於新的偏移量寫入資料
	
	close(fd);
	return 0;
}
下面是程式驗證結果


最終的examplefiles 檔案存在空洞

揭祕檔案描述符

核心支援的檔案描述符資料結構如下圖


右側的表稱為 i 節點表,整個系統只有一張。該表可以視為結構體陣列,該陣列的一個元素對應於一個物理檔案。

中間的表稱為檔案表,整個系統只有一張,該表可以視為結構體陣列,一個結構體中有很多欄位,其中有3個欄位比較重要:

  • file status flags:用於記錄檔案被開啟時採用的選項,其實記錄的就是open 呼叫中使用者指定的第 2個引數
  • current file offset:用於記錄檔案的當前讀寫位置(指標)正是由於此欄位的存在,使得一個檔案被開啟並讀取後,下一次讀取將從上一次讀取的字元後開始讀取
  • v-node ptr:該欄位是指標,指向右側表的一個元素,從而關聯了物理檔案

右側的表稱為檔案描述符表,每個程序有且只有一張,該表可以視為指標陣列,陣列的元素指向檔案表的一個元素,最重要的是陣列元素的下標就是檔案描述符。

open函式的實際操作就是:新建一個 i 節點表元素,讓其對應開啟的物理檔案,新建一個檔案表的元素,根據open的第二個引數設定檔案狀態標誌,將當前檔案偏移量置0,將v節點指標指向剛建立的i節點表元素,在檔案描述符表中尋找一個上未使用的元素,在該元素中填入一個指標值,讓其指向剛建立的檔案表元素,該元素下標作為open的返回值,也就是檔案描述符。

這樣一來,當呼叫readwrite)函式時,根據傳入的檔案描述符,系統可以找到對應的檔案描述符元素,進而找到檔案表的元素,找到i節點表元素,從而完成對物理檔案的讀寫。


參考資料:

《Unix 高階環境程式設計》

《Linux下C語言應用程式設計》