深刻理解計算機系統——系統級I/O

1、UNIX I/O

    在UNIX系統中有一個說法,一切皆文件。全部的I/O設備,如網絡、磁盤都被模型化爲文件,而全部的輸入和輸出都被當作對相應文件的讀和寫來執行。這種將設備映射爲文件的方式,容許UNIX內核引出一個簡單、低級的應用接口,稱爲UNIX I/O,這使得全部的輸入和輸出都能以一種統一且一致的方式來執行。node

  • 打開文件 打開文件操做完成之後才能對文件進行一些列的操做,打開完成過之後會返回一個文件描述符,它在後續對此文件的全部操做中標識這個文件,內核記錄有關這個打開文件的全部信息。
  • 改變當前的文件位置。
  • 讀寫文件
  • 關閉文件 應用完成了對文件的訪問以後,就通知內核關閉這個文件,內核釋放文件打開時建立的數據結構,並將這個描述符恢復到可用的描述符池中。進程終止,內核也會關閉全部打開的文件並釋放他們的存儲器資源。

2、打開和關閉文件

    關於打開文件的基本操做,這裏就再也不累述,就是關於幾個函數的解釋,在上面的三篇文章中有解釋。編程

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

    其中打開標誌flags有三種基本標誌:O_RDONLY、O_WRONLY、O_RDWR。也能夠和其餘三種(O_CREAT、O_TRUNC、O_APPEND)組合使用。mode參數指定了新文件的訪問權限位。(此次終於看到徹底的mode參數的使用方法了)數據結構

3、讀和寫文件

在系統I/O中讀寫文件用的系統函數爲read()和write()函數來執行。app

#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)ui

 

    有些狀況下,read和write傳送的字節比應用程序要求的要少,出現這種狀況的緣由以下:spa

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

    實際上,除了EOF,在讀磁盤文件時,將不會遇到不足值,並且在寫磁盤文件時,也不會遇到不足值。然而,若是你想建立健壯的網絡應用,就必須反覆調用read和write處理不足值,直到全部須要的字節都傳送完畢。(這一點在UNIX網絡編程中已經領略過了!!)指針

4、用RIO包健壯地讀寫

    這個包會處理上面的不足,RIO提供了方便、健壯和高效的I/O。提供了兩類不一樣的函數:code

  • 無緩衝的輸入輸出函數 直接在存儲器和文件之間傳送數據,沒有應用級緩衝,它們對將二進制數據讀寫到網絡和從網絡讀寫二進制數據尤爲有用。
  • 帶緩衝的輸入函數
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從新填滿緩衝區。也就是說,這些函數都是緩衝區操做而言的。

5、讀取文件元數據

    應用程序可以經過調用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;
	}

 

6、共享文件

  內核用三個相關的數據結構來表示打開的文件:

  • 描述符表(descriptor table)每一個進程都有它獨立的描述符表,它的表項是由進程打開的文件描述符來索引的。每一個打開的描述符表項指向文件表中的一個表項。
  • 文件表(file table)  打開文件的描述符表項指向問價表中的一個表項。全部的進程共享這張表。每一個文件表的表項組成包括由當前的文件位置、引用計數(既當前指向該表項的描述符表項數),以及一個指向v-node表中對應表項的指針。關閉一個描述符會減小相應的文件表表項中的應用計數。內核不會刪除這個文件表表項,直到它的引用計數爲零。
  • v-node表(v-node table)同文件表同樣,全部的進程共享這張v-node表,每一個表項包含stat結構中的大多數信息,包括st_mode和st_size成員。

   下面看幾張圖。


   描述符1和4經過不一樣的打開文件表表項來引用兩個不一樣的文件。這是典型的狀況,沒有共享文件,而且每一個描述符對應一個不一樣的文件。


    多個描述符也能夠經過不一樣的文件表表項來應用同一個文件。若是同一個文件被open兩次,就會發生上面的狀況。關鍵思想是每一個描述符都有它本身的文件位置,因此對不一樣描述符的讀操做能夠從文件的不一樣位置獲取數據。


    父子進程也是能夠共享文件的,在調用fork()以前,父進程如第一張圖,而後調用fork()以後,子進程有一個父進程描述符表的副本。父子進程共享相同的打開文件表集合,所以共享相同的文件位置。一個很重要的結果就是,在內核刪除相應文件表表項以前,父子進程必須都關閉了他們的描述符。

下圖展現了文件描述符、打開的文件句柄以及i-node之間的關係,圖中,兩個進程擁有諸多打開的文件描述符。
    在進程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()調用。同一個進程兩次打開同一個文件,也會發生相似狀況。

7、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。

    解析圖以下:

                

 

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

    上圖中展示了幾種I/O的關係模式,在應用程序中應該使用哪些函數呢?標準I/O函數是磁盤和終端設備I/O的首選。可是對網絡套接字上儘可能使用健壯的RIO或者系統I/O

小結:

Unix內核使用三個相關的數據結構來表示打開的文件。描述符表中的表項指向打開文件表中的表項,而打開文件表中的表項又指向v-node表中的表項。每一個進程都有它本身單獨的描述符表,而全部的進程共享同一打開文件表和v-node表。

相關文章
相關標籤/搜索