【Linux】文件IO詳解

Linux文件結構

文件描述符

  文件描述符已經不陌生了,在一個進程中一個打開的文件就是用一個文件描述符所表徵的,可以看作是一個句柄,就是所謂的遙控器。但是這個遙控器到底怎麼來控制具體的文件呢?接下來會依此講解文件描述符背後的在UNIX環境下實現相關的數據結構。

UNIX環境下的文件共享

  文件描述符用來表徵一個文件,但是爲什麼操作系統要用這麼一個整數來表徵一個文件呢?這就操作系統底層實現有莫大的關係。
  在進程PCB中有着這麼一個部分,IO狀態信息,說的再具體點,在PCB中存在着一張表,我們可以叫它文件描述符表也可以叫做打開文件描述符表,這張表每個進程都會有且爲進程獨有,所以它是進程級的。這張表上的每一個表項都有兩個部分組成,文件描述符標誌以及一個文件指針。其中文件描述符標誌也就是我們所使用的文件描述符fd,當然我們也可以將其看做是這張表的下標。這張表長這樣。
文件系統
  這張表中每一項都有一個文件指針,那麼這個指針又指向哪裏呢?這就要提到另一張表打開文件表,注意這張表由操作系統管理,且系統中只有唯一一張這樣的表,因此這張表是系統級的。這張表中的每一項都存儲着一個進程與這個文件相關的一些信息,其中主要分爲三個部分:文件狀態標誌,文件當前偏移量,v-node結點指針
  文件狀態標誌就是文件在打開時的狀態標誌,例如可讀,可寫,可讀寫,阻塞等都會記錄在其中,這些狀態標誌也可以使用fcntl函數修改。
  文件當前偏移量就是文件指針當前在文件中指向的位置,我們可以用lseek函數修改。
  v-node結點指針我們稍後再談,現在我們要詳細講講這張表的工作過程。這張表屬於系統級的,系統中任何進程打開任何文件都會在其中添加一個記錄項,按照一般情況下來說兩個不同的進程打開相同的文件也會在表中創建兩個不同的表項,因此兩個進程對同一個文件可以有不同的狀態標誌以及文件當前偏移量,一個進程中不同的文件描述符所代表的文件描述符表項中的文件指針也該指向不同的打開文件表項,但是在某些情況下文件描述符表中不同表項的指針卻又有可能指向系統級打開文件表中的同一個表項。例如我們在fork子進程時,子進程複製父進程PCB中的大部分信息包括IO狀態信息時會複製文件描述符表,因此兩個不同的進程此時就會打開同一個文件,並且文件指針的指向也不會改變會指向相同的打開文件表表項;在使用dup函數重定向時一個進程中不同文件描述符表項中的文件指針也會指向同一個打開文件表中的表項。
  這張表中的每個表項長這樣。
文件系統
  最後還剩一個問題,這個v-node結點指針幹嘛用的?v-node節點指針當然指向v-node節點的啊。那麼什麼是v-node節點?說到v-node就不得不提起i-node節點,在UNIX操作系統中操作系統管理文件的方式是通過使用v-nodei-node節點的方式進行管理的,每個文件都會有這樣的節點用於保存相關的文件信息,例如v-node節點上保存了文件類型,對這個文件進行操作的函數指針以及對應的i-node節點的指針;而i-node節點上保存了文件長度,文件數據存儲在磁盤的位置,文件所屬者等。這些文件信息平時存儲在磁盤上,當一個文件倍打開時系統會將這些信息讀入內存,並且相同的文件的i-nodev-node節點在內存中只會存在一份。這兩個節點長這樣。
文件系統
  那麼爲什麼要用兩個節點保存這些信息呢?這是爲了在一個操作系統上對多文件系統進行支持。把與文件系統無關的文件信息存儲在v-node節點上,其餘信息存在i-node上,分開存儲,這樣的系統也叫做虛擬文件系統。而Linux比較特殊,他其中沒有v-node節點而是用了兩個不同的i-node節點,但是結果而言大同小異。
  綜上所述,把以上集中數據結構連接起來就構成了一個進程對文件進行控制的完整脈絡,進程也就得到了和文件控制有關的所有信息,可見並不是所有文件信息都保存在PCB中的。
文件系統
  對於兩個不同的進程打開同一個文件,他們的文件指針可能指向不同的打開文件表項,但是最終都會指向同一個v-nodei-node節點,正如之前所說,相同文件的有關信息在內存中只會存在一份。如下圖。
文件系統

打開關閉文件

open()

  open()函數用於打開一個文件,函數聲明如下。

int open(const char *pathname, int flags, mode_t mode);
 Arguments:
 path:打開文件或創建文件的名字,
 flags:表示選項,用|連接多個選項
 flags選項宏的定義文件在每個系統中有所不同,Linux中定義在fcntl-linux.h文件中
 mode參數僅在使用部分選項時纔用到,例如O_CREAT在mode中需要給定文件初始權限
 Return Value:打開的文件描述符,失敗返回-1

  返回的文件描述符符合最小未使用分配原則。
  其中flags選項有幾個比較常用選項,介紹如下:

//以下這五個選項只能五選其一
O_RDONLY:只讀
O_WRONLY:只寫
O_RDWR:可讀可寫
O_EXEC:可執行
O_SEARCH:只搜索(應用於目錄)
//剩下這些選項可以同時存在多個
O_APPEND:追加寫,打開文件時將文件當前偏移量置爲文件長度,建議要是要想像文件末尾追加數據都加上這個選項,原因後面解釋。
O_CREAT:文件不存在則創建,全線由mode給出
O_CLOEXEC:當前進程如果發生進程替換,自動關閉當前文件
O_DIRECTORY:打開的不是目錄則報錯
O_EXCL:同時存在O_CREAT時如果文件存在則報錯
O_NOFOLLOW:如果打開的是一個符號鏈接則出錯
O_NONBLOCK:非阻塞打開文件
O_SYNC:非延遲寫,即同步寫,每次都等待物理寫磁盤成功後再返回
O_TRUNC:打開文件則截斷文件

  以上這些宏定義在fcntl.h中,但是根據操作系統不同具體定義的位置也各不相同。

openat()

  openat()open()參數及功能和返回值都很類似,其函數聲明和主要區別如下:

int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
openat函數與open函數功能類似,唯獨多出dirfd參數用以區分功能
openat函數解決的主要問題是
1、可以讓同一進程下的多個線程之間擁有不同的當前工作目錄,從而可以使用同樣的相對路徑打開絕對路徑可能不同的文件
2、解決TOCTTOU(Time Of Check To Time Of Use)。如果兩個文件相關的系統調用互相依賴,則這個系統是脆弱的
openat函數主要特性
1、如果pathname是絕對路徑,那麼此時dirfd參數毫無意義,功能則與open一致
2、如果pathname是相對路徑,且dirfd參數不是特殊宏AT_FDCWD,則將dirfd所在的目錄作爲此時的當前工作目錄,以此打開相對路徑的文件
3、如果pathname是相對路徑,且dirfd參數未特殊宏AT_FDCWD,則就以當前工作目錄打開相對路徑的文件,功能與open無異

文件名截斷

  在我們利用open()函數創建新文件時如果文件名過長會怎樣呢?
  在UNIX系統中,有這麼一個宏提前定義在系統中即NAME_MAX,它標識了當前系統中一個文件名最大的字符長度。假設一個系統中此值爲255但是我們想要創建一個文件名爲256長度的文件時操作系統會怎麼處理呢?此時有兩種做法。
  第一種做法爲截斷。即既然只支持最長255那麼你可以創建新文件,但是我只截取前255個字符作爲新文件的文件名,文件照樣會創建成功。這種處理方法在DOS系統上十分常見。
  第二種做法爲報錯返回-1。即文件名超出系統限制,那麼文件創建失敗返回-1並且修改errno爲ENAMETOOLONG,這種做法在BSD系統和Linux上十分常見。
  如果我們想要知道我們當前系統對於這個問題的處理方法是怎樣的我們可以使用pathconf()函數或者fpathconf()查看系統系統限制,如果返回爲真則表示當前系統的處理爲出錯返回而不是截斷。

int main()
{                            
  //測試文件是否截斷                             
  //如果使用一個系統調用如open創建一個新文件的文件名大於NAME_MAX,有的系統會選擇截斷,而有的系統選擇返回-1報錯
  //如果_POSIX_NO_TRUNC值爲真則返回-1報錯,爲假則對文件名進行截斷,並且成功創建
  std::cout << pathconf(".", _PC_NO_TRUNC) << std::endl;         
  //Linux操作系統下的處理爲報錯返回,並將errno置爲ENAMETOOLONG          
}

1

creat()

  這個函數可以用於創建一個文件,因爲在早期版本中open()函數的選項並沒有現在這樣豐富,無法創建文件,所以出現了這個函數。函數原型及介紹如下:

int creat(const char *pathname, mode_t mode);
用於新建一個文件,功能與open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode)完全一致
成功返回文件描述符,失敗返回-1

close()

  close()用於關閉一個文件,函數介紹及原型如下:

int close(int fd);
close用於關閉一個文件,成功返回0,失敗返回-1

lseek()

文件當前偏移量

  每個文件在打開後都會一個文件當前偏移量的概念存在,也可以叫做文件指針,它指向文件中某一位置,並且之後的讀寫操作會全部從此處開始,一般來說文件當前偏移量一般來說是一個非負整數,但是在某些情況下我們獲取的偏移量有可能爲負值或者大於文章長度。每個文件當前偏移量存儲在系統級的打開文件表當中。
  當一個文件被打開時文件當前偏移量被置爲0,如果使用了O_APPEND選項則置爲文章長度,方便追加。

lseek()

基本使用

  lseek()用於修改文件當前偏移量,函數原型及介紹如下:

off_t lseek(int fd, off_t offset, int whence);
Arguments:
fd:操作的文件描述符
whence:可以有三種參數,SEEK_SET,SEEK_CUR,SEEK_END,分別代表相對位置文件開頭,當前文件偏移量,文件結尾位置
offset:表示移動距離,offset可正可負
Return Value:成功返回更改後的文件當前偏移量,失敗返回-1,如果當前fd是一個管道,套接字等不可修改的會將errno置爲ESPIPE

  注意其中的off_t類型,這個類型爲偏移量類型,雖然偏移量爲非負,但是這裏的類型卻是個有符號整型,因此它可正可負,並且它也代表了一個文件的最大長度,如果它是32位的,則文章最大長度爲2^31 - 1,在Linux操作系統下它是8個字節的即64位,但是否能創建一個大於2G的文件取決於底層文件系統。
  我們也可以使用lseek()獲取當前文件偏移量,方式如下:

void PrintOffset()
{
  int fd = open("test.txt", O_CREAT | O_TRUNC | O_RDWR, 0664);
  if(fd == -1)
  {
    perror("error:");
    return;
  }
  //通過以下這種方法可以獲取當前的偏移量
  off_t curOffset = -1;
  curOffset = lseek(fd, 0, SEEK_CUR);
  //打印0,可知文件默認打開偏移量爲0
  std::cout << curOffset << std::endl;
}
int main()
{
  PrintOffset();
}


0

  以上方法還可以用來檢測一個文件支不支持更改偏移量,例如管道套接字等文件不支持更改,則會返回-1,errno置爲ESPIPE
  還要注意一點,lseek()只更改文件當前偏移量,不涉及IO。

lseek()引起空洞

  之前有提到過文件當前偏移量是可以大於當前文件長度的,如果在這種情況下還進行文件寫入是允許的,但是會形成文件空洞。空洞的部分用\0代替,但是空洞並不佔用磁盤塊。

文件讀寫

read()

  函數聲明及介紹如下:

size_t read(int fildes, void *buf, size_t nbyte);
從文件中讀取數據
Arguments:
fildes:讀取的文件描述符
buf:數據存放的目標緩衝區
nbyte:最多讀取的數據長度,16位無符號整型,一次讀取最多爲65535個字節
Return Value:
返回實際讀取的數據長度,大部分情況下目標文件中有多少數據則讀取多少數據並且返回讀取長度;
如果是管道或者套接字目前暫無數據則會阻塞
如果是普通文件,讀到文件結尾返回0
可以設置非阻塞讀取,如果暫無數據則不會阻塞而回返回-1並將errno置爲EAGAIN
返回值ssize_t是一個帶符號整形

  read()函數讀取出錯返回-1,對於不同類型的文件有着不同的處理。

write()

  函數聲明及介紹如下:

ssize_t write(int fildes, const void *buf, size_t nbyte);
向文件中寫入數據
Argumentes:
fildes:文件描述符
buf:寫入數據存放的緩衝區
nbyte:寫入的最長數據長度
Return Value:
返回實際寫入的數據長度,如果數據長度小於nbyte則在後補'\0';如果文件剩餘容量小於nbyte則返回能寫入的最大數據長度

  同樣的,write()函數讀取出錯返回-1,對於不同類型的文件有着不同的處理。

void Test1()
{                                                             
  int fd = open("test.txt", O_CREAT | O_RDWR | O_TRUNC, 0664);
  if(fd < 0)
  {
    perror("error:");
    return;
  }
  //末尾補\0
  int ret = write(fd, "Misaki", 7);
  std::cout << ret << std::endl;
  lseek(fd, 0, SEEK_SET);
  char buf[1024] = {0};
  ret = read(fd, buf, 1024);
  std::cout << ret << std::endl;
  std::cout << buf << std::endl;
  //可以發現末尾確實補了\0
  for(int i = 0; i < ret; i++)
  {
    std::cout << (int)buf[i] << " ";
  }                
}  

int main()   
{  
  Test1();   
}


7
7
Misaki
77 105 115 97 107 105 0 

IO效率問題

  首先看以下一段讀取文件常用的代碼:

#define BUFSIZE 4096
bool Test2()
{
  int n = 0;
  char buf[BUFSIZE];
  while((n = read(STDIN_FILENO, buf, 4096)) > 0)
  {
    if(write(STDOUT_FILENO, buf, n) != n)
    {
      perror("write error:");
      return false;
    }
  }
  if(n < 0)
  {
    perror("read error:");
    return false;
  }
  return true;
}

int main()
{
  if(Test2() == false)
  {
    std::cerr << "copy error" << std::endl;
    return -1;
  }                                        
}

  這段代碼是很普通的一段從標準輸入讀取數據寫入標準輸出的文件讀取寫入代碼,但是其中有一個重要的問題,我們在用文件讀取的時候往往需要在程序內開闢一塊緩衝區用作數據暫存,問題來了,這塊buffer開多大呢?
  這裏跟文件系統相關了,我們都知道數據在磁盤上是按照扇區讀取的,但是操作系統讀取磁盤數據的最小單位是磁盤塊,也就是說我們每次讀取數據最小都要讀取一個磁盤塊大小的數據,如果讀取數據長度小於磁盤塊操作系統也要把整個磁盤塊數據先讀進來然後再拿其中一部分剩下的丟掉,這樣就導致一個問題如果我們讀取的數據小於一個磁盤塊就會導致效率低下,造成性能浪費,而在Linux操作系統上一個磁盤塊大小爲4K,所以我們一次讀取數據大於等於4K並且爲4K整數倍的話效率是最高的。不過現在的操作系統爲了提高效率使用了預讀技術,這使得不帶緩衝的文件IO在使用較小緩衝區讀取大的連續存儲的文件時也能有較高地效率,我們可以從下圖看出:
IO效率

原子性操作

  考慮這麼一種場景,兩個不同的進程同時打開了一個文件,要對文件進行追加寫,但是問題來了,兩個進程這裏都使用了lseek的方式將當前文件偏移量置爲文件末尾處再寫,這樣的操作並不是一個原子性操作,很有可能導致兩個進程同時先將偏移量移到末尾,然後一個寫文件結束,另一個再繼續在之前的偏移量接着寫,這時的偏移量並不在文章末尾,會導致將第一個進程寫的數據覆蓋。舉個例子,假設一個文件目前長度爲1500,進程都將偏移量置爲了1500,然後第一個線程先寫400的數據,之後第二個進程接着準備寫400數據,但是第二個進程的偏移量還在1500處,並沒有更新爲1900,此時再寫入數據就會把之前進程寫入的數據覆蓋。
  以上的問題想要解決也很容易,有兩個辦法,第一個就是使用O_APPEND選項,在打開文件加入這個選項後,每次寫入數據都會自動將偏移量置爲文件末尾處再寫,不用lseek保證了原子性;第二個辦法就是使用pread()pwrite()函數,這兩個函數與read()write()幾乎無異,不同的是它多了一個參數,可以原子性的幫助我們在讀寫之前修改文件偏移量,但是要注意這裏的文件偏移量修改只對這一次操作有效,以下是函數聲明及介紹:

ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
ssize_t pwrite(int fildes, const void *buf, size_t nbyte,off_t offset);
這兩個函數與read和write參數功能以及返回值一致,主要區別在第四個參數offset
這兩個函數會將偏移量置爲offset在進行讀寫操作,期間是原子性的,並且不可打斷。操作完成後也不會修改原有的偏移量的值。


int main()
{                                                            
  int fd = open("test.txt", O_CREAT | O_RDWR, 0664);         
  if(fd < 0)                                   
  {                                             
    perror("open error:");                
    return -1;
  }
  char buf[4096];
  lseek(fd, 1, SEEK_SET);
  //從這裏可以看出pread是將偏移量置爲offset,而不是加上offset
  int ret = pread(fd, buf, 4096, 1);
  std::cout << ret << std::endl;
  buf[ret] = '\0';        
  std::cout << buf;
  //事實打印出來的當前偏移量並沒有發生改變
  std::cout << lseek(fd, 0, SEEK_CUR) << std::endl;;
} 


7
Misaki
1

重定向

dup()

  dup()函數用於重定向,傳入一個描述符,系統會將當前最小未使用的文件描述符中的文件指針指向這個描述符所指向的系統級打開文件表項,因此在重定向後新文件描述符將和舊文件描述符擁有相同的文件狀態和當前文件偏移量以及v-node節點指針,因爲這些信息都是存儲在系統級打開文件表中的。
  函數介紹及聲明如下:

int dup(int oldfd);
Arguments:
oldfd:舊文件描述符
Return Value:成功返回新文件描述符,失敗返回-1


int main()
{
  //一個進程執行時自動打開0,1,2三個文件描述符,作爲標準輸入標準輸出標準錯誤
  //在這裏我們重定向的新文件描述符自動更新爲3
  //並且文件描述符重定向舊文件描述符依然可以用,且指向文件不變
  int newfd = dup(1);
  std::cout << newfd << std::endl;
  write(newfd, "123\n", 4);
  write(STDOUT_FILENO, "123\n", 4);
}


3
123
123

dup2()

  功能比dup()更加強大,傳入兩個參數,可以指定讓新文件描述符中的文件指針拷貝爲舊文件描述符的文件指針,也就是在dup()的基礎上我們可以指定將哪個文件描述符作爲新新文件描述符,而不是最小未使用。如果新文件描述符已經打開文件則將其關閉。
  函數聲明及介紹如下:

int dup2(int oldfd, int newfd);
讓文件描述符表中newfd的項中的文件表項指針更改爲oldfd項中文件表項指針
若newfd原先有指向文件並且已經打開則關閉,若newfd == oldfd則直接返回newfd
Arguments:
oldfd:舊文件描述符
newfd:新文件描述符
Return Value:成功返回新文件描述符,失敗返回-1
dup和dup2都是原子性的,其相當於調用了close函數和fcntl函數

  dupdup2的功能相當於調用了functl實現,實現方法如下:

dup:
fcntl(oldfd, F_DUPFD, 0);
dup2:
close(newfd);
fcntl(oldfd, F_DUPFD, newfd);

內核緩衝與同步寫

內核緩衝

  即使我們說文件IO是沒有緩衝區的,但是其實並不盡然,我們應該說文件IO是不會維護進程緩衝區,但是Unix操作系統爲了提高讀寫效率會在內核中存在一塊緩衝區,我們稱之爲內核緩衝。以寫爲例,我們每次調用write()寫數據到達文件的時候並不是直接將數據寫入文件,因爲如果要寫入的數據非常多則會因爲IO佔用非常多的時間,導致阻塞嚴重。系統在這裏的處理時先將要寫入的數據寫入每個文件的內核緩衝區,然後隨後再將它們真正寫入文件,這樣的寫入模式稱之爲延遲寫
  但是這樣會導致問題,對一些需要即時寫入即時使用的數據來說會導致文件數據與緩衝區的數據不相符,原因是緩衝區中的數據還沒來得及更新,於是這裏牽扯到了同步寫

sync()

  sync()通常由系統利用守護進程update每隔30秒週期性調用,它的作用是將系統中所有修改過的內核緩衝區加入寫隊列,從而讓其可以更新到真正的文件中,但是這個函數並不等待真正的寫入就會返回。

void sync(void)

同步寫

  因爲系統默認是不會等待真正數據寫入文件,對於要求立即寫入文件的程序來說這樣並不靠譜,於是系統也爲我們提供了可以同步寫的方法。同步寫一般應用於數據庫文件當中。

fsync()

  fsync()會傳入一個文件描述符,並且等待此文件緩衝區中的數據真正寫入到磁盤後纔會返回,達到同步寫。

int fsync(int fd);

fdatasync()

  fdatasync()fsync()類似,唯一不同的是它只等待文件數據更新到磁盤上即可返回,並不文件的屬性信息更新,因此調用它返回會更快。

int fdatasync(int fd);

修改文件屬性

  在打開文件時可以指定文件狀態以及一些附加選項,當然既然可以指定那麼就可以修改,而fcntl()就向我們提供了這一功能。

fcntl()

  函數聲明及介紹:

int fcntl(int fd, int cmd, ...);
fcntl函數可以更改已經打開的文件的屬性 
Arguments:
fd:文件描述符      
cmd:執行命令
...:不定參數,後面有可能會根據cmd的不同有着不同的需要傳遞的參數
Return Value:返回值根據cmd的不同也不同,但是失敗都會返回-1,大部分設置爲主的模式成功會返回0
常用cmd:  
F_DUPFD:賦值文件描述符,dup底層就是用這個參數進行實現的,它會將第三個參數起最小未使用的描述符複製爲fd所指文件
F_DUPFD_CLOEXEC:這裏涉及一個文件描述符標誌,FD_CLOEXEC,這也是唯一一個文件描述符標誌,當被定義了這個文件描述符標誌的文件
噹噹前進程在exec進程替換時會自動關閉這個文件,防止子進程一直佔用,多用於只需要父進程可以使用這個文件而子進程關閉這個文件的文件上
FD_CLOEXEC也可以通過F_SETFD模式進行設置,F_DUPFD_CLOEXEC則與F_DUPFD功能以及參數類似,不同的是會自動爲newfd設置FD_CLOEXEC標誌
F_GETFD:獲得對應於fd的文件描述符標誌FD_CLOEXEC作爲返回值返回
F_SETFD:對於fd設置新的文件描述符標誌,新標誌作爲第三個參數傳入
F_GETFL:對於fd獲得文件狀態標誌,例如O_RDWR之類的稱爲文件狀態標誌,但是一個文件中的O_RDWR,O_WRONLY,O_RDONLY,O_EXEC,o_SEARCH是互斥的
因此可以使用O_ACCMODE獲得訪問方式屏蔽位
F_SETFL:對於fd設置文件狀態標誌

  這裏要區分兩個概念,文件描述符標誌文件狀態標誌。目前文件描述符標誌最常用的標誌就一個FD_CLOEXEC標誌,這個標誌也可以在打開文件時加上,也可以通過fcntl()FD_SETFD選項加上這個標誌。這個標誌的作用就是當前進程在放生進程替換時會自動關閉有這個標誌的文件,主要解決的問題是父進程創建子進程,子進程拷貝了父進程文件描述符表因此有着和父進程相同的打開文件以及偏移量,如果子進程發生進程替換,可能會導致文件無意篡改的情況,所以關閉不用的描述符防止數據無意篡改。文件狀態標誌就是一些文件相關的狀態信息及打開時的選項,常見有如下選項:
文件狀態
  以下是使用示例:

//打印文件狀態標誌
void GetState(int fd)
{
  int flags = fcntl(fd, F_GETFL);
  if(flags < 0)
  {
    perror("fcntl error:");
    return;
  }
  //用屏蔽字獲取當前狀態標誌
  switch(flags & O_ACCMODE)
  {
    case O_WRONLY:
      std::cout << "write only" << std::endl;
      break;
    case O_RDONLY:
      std::cout << "read only" << std::endl;
      break;
    case O_RDWR:
      std::cout << "read write" << std::endl;
      break;
    default:
      std::cerr << "unknow mode" << std::endl;
      break;
  }
  if(flags & O_NONBLOCK)
  {
    std::cout << "nonblock" << std::endl;
  }
  if(flags & O_APPEND)
  {
    std::cout << "append" << std::endl;
  }
  //一個文件描述符就算設置了SYNC同時寫系統也不一定一定會按照預期進行同時寫,因此程序員有必要調用fsync()函數
  if(flags & O_SYNC)
  {
    std::cout << "sync" << std::endl;
  }
}

int main(int argc, char* argv[])
{
  if(argc != 2)
  {
    std::cerr << "use ./main <fd> << std::endl";
    return -1;
  }
  GetState(atoi(argv[1]));
}          


[misaki@localhost BaseIO]$ ./main 0 < /dev/tty
read only
[misaki@localhost BaseIO]$ ./main 1 > test.txt
[misaki@localhost BaseIO]$ cat test.txt 
write only
[misaki@localhost BaseIO]$ ./main 2 2>>test.txt
write only
append
[misaki@localhost BaseIO]$ ./main 5 5<>test.txt
read write

  這裏要注意的一個文件狀態是O_SYNCO_DSYNC同步寫狀態,這裏就算給一個文件加上了這個標誌,操作系統爲了優化也不一定會同步寫,要想百分百同步寫還是需要調用fsync()fdatasync()函數。

dev/fd

  打開dev/fd中的文件描述符意義基本上等同於重新打開一份已經在進程中打開過的文件,基本上和dup(fd)的作用是差不多的,我們可以這樣使用:

int fd = open("/dev/fd/0", mode);

  這樣的寫法等同於

int fd = dup(0);

  並且要求mode必須是事先打開文件選項的子集,但是Linux平臺是個例外,在Linux上打開/dev/fd的文件等同於打開了一份新的文件,與原來打開的文件無關。

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

int main()
{
  int fd = open("test.txt", O_CREAT | O_RDWR, 0664);
  if(fd < 0)
  {
    perror("open error:");
    return -1;
  }
  write(fd, "Misaki0", 7);
  dup2(fd, 1);
  std::cout << "Misaki1" << std::endl;
  int stdOut = open("/dev/fd/1", O_RDWR);
  if(stdOut < 0)
  {
    perror("open stdOut error:");
    return -1;
  }
  dup2(stdOut, 1);
  lseek(1, 14, SEEK_DATA);                               
  std::cout << "Misaki2" << std::endl;
  close(stdOut);
  close(fd);
}


[misaki@localhost BaseIO]$ cat test.txt 
Misaki0Misaki1Misaki2

  這個例子可以看出來利用/dev/fd打開的文件有着自己獨有的文件偏移量。
  /dev/fd這種用法一般在shell語句中用於獲取一個進程的標準輸入標準輸出比較多。

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