天天看點

深入了解計算機系統——系統級I/O

一、UNIX I/O

    在UNIX系統中有一個說法,一切皆檔案。所有的I/O裝置,如網絡、磁盤都被模型化為檔案,而所有的輸入和輸出都被當做對相應檔案的讀和寫來執行。這種将裝置映射為檔案的方式,允許UNIX核心引出一個簡單、低級的應用接口,稱為UNIX I/O,這使得所有的輸入和輸出都能以一種統一且一緻的方式來執行。

  • 打開檔案 打開檔案操作完成以後才能對檔案進行一些列的操作,打開完成過以後會傳回一個檔案描述符,它在後續對此檔案的所有操作中辨別這個檔案,核心記錄有關這個打開檔案的所有資訊。
  • 改變目前的檔案位置。
  • 讀寫檔案
  • 關閉檔案 應用完成了對檔案的通路之後,就通知核心關閉這個檔案,核心釋放檔案打開時建立的資料結構,并将這個描述符恢複到可用的描述符池中。程序終止,核心也會關閉所有打開的檔案并釋放他們的存儲器資源。

二、打開和關閉檔案

    關于打開檔案的基本操作,這裡就不再累述,就是關于幾個函數的解釋,在上面的三篇文章中有解釋。

    int open(char *filename,int flags,mode_t mode);

    其中打開标志flags有三種基本标志:O_RDONLY、O_WRONLY、O_RDWR。也可以和其他三種(O_CREAT、O_TRUNC、O_APPEND)組合使用。mode參數指定了新檔案的通路權限位。(這次終于看到完全的mode參數的使用方法了)

深入了解計算機系統——系統級I/O

三、讀和寫檔案

在系統I/O中讀寫檔案用的系統函數為read()和write()函數來執行。

#include <unistd.h>

ssize_t read(int fd,void * buf,size_t n);

ssize_t write(int fd,void *buf,size_t n);      

    read函數從描述符為fd的目前檔案位置拷貝最多n個位元組到存儲器位置buf。傳回值-1表示一個錯誤,而傳回值0表示EOF。否則,傳回值表示的是實際傳送的位元組數量。而write函數從存儲器位置buf拷貝至多n個位元組到描述符fd的目前檔案位置。傳回值要麼為-1要麼為寫入的位元組數目。

/* $begin cpstdin */
#include "csapp.h"

int main(void) 
{
    char c;

    while(Read(STDIN_FILENO, &c, 1) != 0) 
	Write(STDOUT_FILENO, &c, 1);
    exit(0);
}
/* $end cpstdin */
      

    關于在檔案中定位使用的函數為lseek,在I/O庫中使用的函數為fseek。

    (ps:size_t和ssize_t的差別,前者是unsigned int,而後者是int)

    有些情況下,read和write傳送的位元組比應用程式要求的要少,出現這種情況的原因如下:

  • 讀時遇到EOF。此時read傳回0來發出EOF信号。
  • 從終端讀文本行。如果打開檔案是與終端相關聯,那麼每個read函數将以此傳送一個文本行,傳回的不足值等于文本行的大小。
  • 讀和寫網絡套接字。可能會出現阻塞現象。(我一定會在程序間通信的時候弄清楚這個事情的前前後後,後後前前!!!)

    實際上,除了EOF,在讀磁盤檔案時,将不會遇到不足值,而且在寫磁盤檔案時,也不會遇到不足值。然而,如果你想建立健壯的網絡應用,就必須反複調用read和write處理不足值,直到所有需要的位元組都傳送完畢。(這一點在UNIX網絡程式設計中已經領略過了!!)

四、用RIO包健壯地讀寫

    這個包會處理上面的不足,RIO提供了友善、健壯和高效的I/O。提供了兩類不同的函數:

  • 無緩沖的輸入輸出函數 直接在存儲器和檔案之間傳送資料,沒有應用級緩沖,它們對将二進制資料讀寫到網絡和從網絡讀寫二進制資料尤其有用。
  • 帶緩沖的輸入函數
ssize_t rio_readn(int fd,void *usrbuf,size_t n);

ssize_t rio_writen(int fd,void *usrbuf,size_t n);
      

    對同一個描述符,可以任意交錯地調用rio_readn和rio_writen。一個問本行的末尾都有一個換行符,那麼像讀取一個文本中的行數怎麼辦,使用read讀取換行符這個方法不是很妥當,可以調用一個包裝函數(rio_readineb),它從一個内部讀緩沖區拷貝一個文本行,當緩沖區為空時,會自動地調用read重新填滿緩沖區。也就是說,這些函數都是緩沖區操作而言的。

五、讀取檔案中繼資料

    應用程式能夠通過調用stat和fstat函數檢索到關于檔案的資訊(有時也稱為檔案的中繼資料)

#include <sys/stat.h>

#include <unistd.h>

int stat(const char *filename,struct stat *buf);

int fstat(int fd,struct stat *buf);      

    若成功,傳回0,若出錯則為-1.stat以一個檔案名為輸入,并且填充buf結構體。fstat函數隻不過是以檔案描述符而不是檔案名作為輸入。

struct stat {
#if defined(__ARMEB__)
	unsigned short st_dev;
	unsigned short __pad1;
#else
	unsigned long  st_dev;
#endif
	unsigned long  st_ino;
	unsigned short st_mode;
	unsigned short st_nlink;
	unsigned short st_uid;
	unsigned short st_gid;
#if defined(__ARMEB__)
	unsigned short st_rdev;
	unsigned short __pad2;
#else
	unsigned long  st_rdev;
#endif
	unsigned long  st_size;
	unsigned long  st_blksize;
	unsigned long  st_blocks;
	unsigned long  st_atime;
	unsigned long  st_atime_nsec;
	unsigned long  st_mtime;
	unsigned long  st_mtime_nsec;
	unsigned long  st_ctime;
	unsigned long  st_ctime_nsec;
	unsigned long  __unused4;
	unsigned long  __unused5;
};      

    其中st_size成員包含了檔案的位元組大小。st_mode為檔案通路許可位。UNIX提供的宏指令根據st_mode成員來确定檔案的類型:S_ISREG(),這是一個普通檔案麼;S_ISDIR(),這是一個目錄檔案麼;S_ISSOCK()這是一個網絡套接字麼。使用一下這個函數

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	int fd,size;
	struct stat buf_stat;
	memset(&buf_stat,0x00,sizeof(buf_stat));
	fd=stat("stat.c",&buf_stat);
		printf("%d\n",(int)buf_stat.st_size);
	return 0;
	}
      

六、共享檔案

  核心用三個相關的資料結構來表示打開的檔案:

  • 描述符表(descriptor table)每個程序都有它獨立的描述符表,它的表項是由程序打開的檔案描述符來索引的。每個打開的描述符表項指向檔案表中的一個表項。
  • 檔案表(file table)  打開檔案的描述符表項指向問價表中的一個表項。所有的程序共享這張表。每個檔案表的表項組成包括由目前的檔案位置、引用計數(既目前指向該表項的描述符表項數),以及一個指向v-node表中對應表項的指針。關閉一個描述符會減少相應的檔案表表項中的應用計數。核心不會删除這個檔案表表項,直到它的引用計數為零。
  • v-node表(v-node table)同檔案表一樣,所有的程序共享這張v-node表,每個表項包含stat結構中的大多數資訊,包括st_mode和st_size成員。

   下面看幾張圖。

深入了解計算機系統——系統級I/O

   描述符1和4通過不同的打開檔案表表項來引用兩個不同的檔案。這是典型的情況,沒有共享檔案,并且每個描述符對應一個不同的檔案。

深入了解計算機系統——系統級I/O

    多個描述符也可以通過不同的檔案表表項來應用同一個檔案。如果同一個檔案被open兩次,就會發生上面的情況。關鍵思想是每個描述符都有它自己的檔案位置,是以對不同描述符的讀操作可以從檔案的不同位置擷取資料。

深入了解計算機系統——系統級I/O

    父子程序也是可以共享檔案的,在調用fork()之前,父程序如第一張圖,然後調用fork()之後,子程序有一個父程序描述符表的副本。父子程序共享相同的打開檔案表集合,是以共享相同的檔案位置。一個很重要的結果就是,在核心删除相應檔案表表項之前,父子程序必須都關閉了他們的描述符。

下圖展示了檔案描述符、打開的檔案句柄以及i-node之間的關系,圖中,兩個程序擁有諸多打開的檔案描述符。

深入了解計算機系統——系統級I/O

    在程序A中,檔案描述符1和30都指向了同一個打開的檔案句柄(标号23)。這可能是通過調用dup()、dup2()、fcntl()或者對同一個檔案多次調用了open()函數而形成的。

    程序A的檔案描述符2和程序B的檔案描述符2都指向了同一個打開的檔案句柄(标号73)。這種情形可能是在調用fork()後出現的(即,程序A、B是父子程序關系),或者當某程序通過UNIX域套接字将一個打開的檔案描述符傳遞給另一個程序時,也會發生。再者是不同的程序獨自去調用open函數打開了同一個檔案,此時程序内部的描述符正好配置設定到與其他程序打開該檔案的描述符一樣。

    此外,程序A的描述符0和程序B的描述符3分别指向不同的打開檔案句柄,但這些句柄均指向i-node表的相同條目(1976),換言之,指向同一個檔案。發生這種情況是因為每個程序各自對同一個檔案發起了open()調用。同一個程序兩次打開同一個檔案,也會發生類似情況。

七、I/O重定向

      函數為:

深入了解計算機系統——系統級I/O

    函數解釋:

深入了解計算機系統——系統級I/O

(即:讓描述符oldfd實作newfd的功能)

  eg,dup2(field,1)      将标準描述符輸出重定向到field描述符

假設在調用dup2(4,1)之前,我們的狀态圖10-11所示,其中描述符1(标準輸出)對應于檔案A(比如一個終端),描述符4對應于檔案B(比如一個磁盤檔案)。A和B的引用計數都等于1。圖10-14顯示了調用dup2(4,1)之後的情況。兩個描述符現在都指向了檔案B;檔案A已經被關閉了,并且它的檔案表和v-node表表項也已經被删除了;檔案B的引用計數已經增加了。從此之後,任何寫到标準輸出的資料都被重定向到檔案B。

    解析圖如下:

深入了解計算機系統——系統級I/O

八、I/O使用的抉擇方法

深入了解計算機系統——系統級I/O