UNIX環境高級編程(5):文件I/O(1)

UNIX系統中的大多數文件I/O只需要用到5個函數:open、read、write、lseek以及close。本章說明的函數經常稱爲“不帶緩衝的I/0”,術語不帶緩衝指的是每個read和write都調用內核中的一個系統調用。這些不帶緩衝的I/O函數不是ISO C的組成部分,但是它們是POSIX.1和Single UNIX Specification的組成部分。

文件描述符:

對內核而言,所有打開的文件都通過文件描述符引用。文件描述符是個非負整數。按照慣例,UNIX系統shell使用文件描述符0與進程的標準輸入相關聯,文件描述符1與標準輸出相關聯,文件描述符2與標準出錯相關聯。這是各種shell以及許多應用程序使用的慣例,而與UNIX內核無關。在依從POSIX的應用程序中,幻數0、1、2應當替換成符號常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,這些常量都定義在<unistd.h>中。

文件描述符的變化範圍是0-OPEN_MAX。

open函數:

調用open函數可以打開或創建一個文件:

#include <fcntl.h>

int open(const char *pathname,  int flag,  .../* mode_t mode */);

返回值,若成功則返回文件描述符,若出錯則返回-1

pathname:是要打開或創建文件的名字,flag參數可以說明此函數的多個選項,用下列一個或多個常量進行或運算,構成flag參數。

  • O_RDONLY:只讀打開;
  • O_WRONLY:只寫打開;
  • O_RDWR:讀寫打開;
上面三個常量必須指定一個且只能指定一個,下列常量則是可選擇的:
  • O_APPEND:每次寫時都追加到文件的尾端;
  • O_CREAT:若文件不存在,則創建它。使用此選項時,需要第三個參數mode,用於指定新文件的訪問權限位。
  • O_EXCL:若同時指定了O_CREAT,而文件已經存在,則會出錯。用此可以測試一個文件是否存在,如果不存在,則創建此文件。這使得測試和創建成爲一個原子操作;
  • O_TRUNC:如果文件存在,而且爲只寫或讀寫成功打開,則將其長度截短爲0;
  • O_NOCTTY:如果pathname指定的是終端設備,則不將該設備分配作爲此進程的控制終端;
  • O_NONBLOCK:如果pathname指的是一個FIFO、一個塊特殊文件或一個字符特殊文件,則此選項爲文件的本次打開操作和後續I/O操作設置爲非阻塞模式;
下面三個標誌也是可選的,他們是SUS中同步輸入和輸出選項的一部分:
  • O_DSYNC:使每次write等物理I/O操作完成,但是如果寫操作並不影響讀取剛寫入的數據,則不等待文件屬性被更新;
  • O_SYNC:使每次write都等到物理I/O操作完成,包括由write操作引起的文件屬性更新所需的I/O(數據和屬性總是同步更新);
  • O_RSYNC:使每一個以文件描述符作爲參數的read操作等待,直至任何對文件同一部分的未決寫操作都完成;

open函數返回的文件描述符一定是最小的未用描述符數值。這一點被某些應用程序用來在標準輸入、標準輸出或標準錯誤上打開新的文件。

文件名與路徑名截短:

在POSIX.1中,常量_POSIX_NO_TRRUNC決定了是否要截短過長的文件名或路徑名,還是返回一個出錯。根據文件系統類型,此值可變。若_POSIX_NO_TRUNC有效,則在整個路徑名超過PATH_MAX,或路徑名中的任意文件名超過NAME_MAX時,返回出錯狀態,並將errno設置爲ENAMETOOLONG。

creat函數:

也可以調用creat函數創建一個新文件:

#include <fcntl.h>

int creat(const char *pathname, mode_t mode);

返回值:若成功,則返回只寫打開的文件描述符,若出錯則返回-1。

此函數等效於:open(pathname,O_WRONLY | O_CREAT | O_TRUNC, mode)。creat函數的一個不足之處是它以只寫方式打開所創建的文件。

close函數:

可調用close函數關閉一個打開的文件:

#include <unistd.h>

int close(int fieldes)

返回值:若成功則返回0,若出錯則返回-1。

關閉一個文件時,還會釋放該進程加在該文件上的所有記錄鎖。當一個進程終止時,內核自動關閉它所有打開的文件。很多程序都利用這一功能而不顯示地用close關閉打開文件。

lseek函數:

每個打開的文件都有一個與其相關聯的當前文件偏移量。它通常是個非負整數,用以度量從文件開始處計算的字節數。通常,讀寫操作都從當前文件偏移量處開始,並使偏移量增加所讀寫的字節數。

按照系統默認情況,當打開一個文件時,除非指定O_APPEND選項,否則該偏移量設置爲0。可調用lseek函數顯式地爲一個打開的文件設置其偏移量:

#include <unistd.h>

off_t lseek(int fieldes, off_t offset, int whence);

返回值:若成功,則返回新的文件偏移量,若出錯則返回-1。

對參數offset的解釋與參數whence的值有關:

  • 若whence是SEEK_SET,則將該文件的偏移量設置爲距文件開始處offset個字節;
  • 若whence是SEEK_CUR,則將該文件的偏移量設置爲其當前值加offset,offset可爲正或負。
  • 若whence是SEEK_END,則將該文件的偏移量設置爲文件長度加offset,offset可爲正或負。

因此可用如下方式確定打開文件的當前偏移量:

offset currpos

currpos = lseek(fd, 0, SEEK_CUR);

用上述方法還可以確定所涉及的文件是否可以設置偏移量。如果文件描述符引用的是一個管道,FIFO或網絡套接字,則leek返回-1,並將errno設置爲ESPIPE。

下列程序用於測試能否對其標準輸入設置偏移值:

/*
 * Copyright (C) [email protected]
 */


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>


int
main(void)
{
	if (lseek(STDIN_FILENO, 0, SEEK_CUR) < 0) {
		printf("can't seek\n");
	} else {
		printf("seek ok\n");
	}
	exit(0);
}

通常文件的當前偏移量應當是個非負整數,但是某些設備也可能允許負的偏移量。但是對於普通文件,其偏移量必須是非負值。lseek僅將當前的文件偏移量記錄在內核中,它並不引起任何I/O操作。

文件偏移量可以大於文件的當前長度,這種情況下,對該文件的下一次寫將加長該文件,並在文件中構成一個空洞。位於文件中但沒有寫過的字節都被讀爲0。文件中的空洞並不要求在磁盤上佔用存儲區,具體處理方式與文件系統的實現有關。

下列程序用於創建一個具有空洞的文件:

/*
 * Copyright (C) [email protected]
 */


#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>


#define FILE_NAME "file.hole"


int
main(void)
{
	char buf_one[] = "abcdefghij";
	char buf_two[] = "ABCDEFGHIJ";
	int fd;

	if ( (fd = creat(FILE_NAME, S_IRUSR | S_IWUSR)) < 0) {
		printf("create file %s error: %s\n", FILE_NAME, strerror(errno));
		exit(1);
	} 
	
	if (write(fd, buf_one, 10) != 10) {
		printf("write error: %s\n", strerror(errno));
		exit(2);
	}
	
	if (lseek(fd, 16384, SEEK_SET) == -1) {
		printf("lseek error: %s\n", strerror(errno));
		exit(3);
	}

	if (write(fd, buf_two, 10) != 10) {
		printf("write error: %s\n", strerror(errno));
		exit(4);
	}

	exit(0);
}

read函數:

調用read函數從打開文件中讀取數據:

#include <unistd.h>

ssize_t read(int fields, void *buf, size_t nbytes);

返回值:若成功則返回讀到的字節數,若已到文件結尾則返回0,若出錯返回-1。

ssize_t和size_t都是基本系統數據類型,ssize_t爲帶符號的整數,size_t爲不帶符號的整數。

有多種情況可使實際讀到的字節數少於要求讀的字節數:

  • 在讀普通文件時,在讀到要求的字節數之前已經到達了文件尾端;
  • 從終端設備讀時,通常一次最多讀一行;
  • 當從網絡讀時,網絡中的緩衝機構可能造成返回值小於所要求讀的字節數;
  • 當從管道或FIFO讀時,如果管道包含的字節少於所需的數量,那麼read將只返回實際可用的字節數;
  • 當從某些面向記錄的設備(例如磁帶)讀時,一次最多返回一個記錄;
  • 當某一信號造成中斷,而已經讀了部分數據量時;

write函數:

調用write函數向打開的文件寫數據:

#include <unistd.h>

sszie_t write(int fields,const void *buf, size_t nbytes)

若成功則返回已寫的字節數,若出錯則返回-1。

其返回值通常與參數nbytes的值相同,否則表示出錯。write出錯的一個常見原因是:磁盤已寫滿,或者超過了一個給定進程的文件長度限制。

I/O的效率:

下列程序使用read和write函數複製一個文件:

/*
 * Copyright (C) [email protected]
 */


#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>


#define BUFSIZE 4096


int
main(void)
{
	int n;
	char buf[BUFSIZE];

	while ( (n = read(STDIN_FILENO, buf, BUFSIZE)) > 0) {
		if (write(STDOUT_FILENO, buf, n) != n) {
			printf("write error: %s\n", strerror(errno));
			exit(1);
		}
	}

	if (n < 0) {
		printf("read error: %s\n", strerror(errno));
		exit(2);
	}

	exit(0);
}

不同的BUFSIZE對程序的運行時間有非常大的影響。系統CPU時間的最小值出現在BUFFSIZE爲4096處(一個block的大小),繼續增大緩衝區長度對此時間幾乎沒有影響。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章