歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> 《Linux系統編程》第二章筆記(一)

《Linux系統編程》第二章筆記(一)

日期:2017/3/1 11:46:57   编辑:關於Linux

文件I/O

前言

文件概念對於Linux系統的重要性不言而喻,本章主要介紹了內核為文件的創建、讀、寫、定位等系統調用以及高效的I/O機制。Linux系統為文件操作提供了通用的系列系統調用,使開發人員能夠對所有“文件”做相同的操作,同時還提供了ioctl()系統調用對非通用文件操作,fcntl()系統調用對文件描述符做操作。
此外Linux內核為了彌補CPU運算速度和磁盤I/O速度的巨大差異,引入了頁緩存、頁回寫機制。簡單來說就是讀取文件時多讀取一部分並保存在內核緩沖區中,在下次收到讀取請求時直接將內存中的數據返回;寫入文件時只是在內核緩沖區中保存要寫入的數據,在合適的時機提交給磁盤(這樣在寫入之後的讀取請求是從內核緩沖區中獲取數據的,多個寫入請求合並為一次磁盤操作能夠提高效率)。當然,內核也提供了同步I/O機制來確保數據立即被寫入磁盤;直接I/O機制來確保數據不經由內核緩存,直接提交給磁盤。

文件相關知識

文件描述符與偏移量

Linux系統調用一般通過文件描述符來指定對哪個文件進行操作,文件描述符與文件直接的關系在系統中由三個不同層次的表來維護。
* 進程級的文件描述符表(open file descriptor)
系統為每個進程維護了一個表,該表中每條記錄保存了控制文件描述符操作的一組標志(目前僅有一個close-on-exec標志——使用exec()系列函數加載程序時自動關閉)和打開的文件句柄的引用
* 系統級的打開文件表(open file table/open file descriptor)
每個打開的文件都在該表中有一條對應數據,表中每條記錄稱為文件句柄(open file handle),一個文件句柄對應了一個打開的文件的全部信息,包括:當前文件偏移量;打開文件時使用的標志(open()的flags參數);文件訪問模式(讀寫模式);與信號驅動I/O相關的設置;inode節點的引用
* 文件系統級的inode表
inode表被文件系統保存,任何文件系統中的文件都擁有inode信息,一個inode節點包括:文件類型和訪問權限;指向文件持有的鎖的指針;文件的各種類型例如大小、時間戳等。
由此可見,文件描述符與打開的文件之間有如下關系:
1 文件的偏移量和文件句柄相關,和文件描述符無關,同一個文件句柄可能被不同的文件描述符或進程共享。每次調用open()時內核都會分配一個文件句柄。
2 同一進程的不同文件描述符指向同一個文件句柄。這種情況可能是dup()dup()2fcntl()等系統調用或重定向>>>導致的,盡管文件描述符值不同,但其指向的句柄是同一個。此時不同文件描述符的文件偏移量是通用的,因為文件句柄是同一個。
3 不同進程的相同文件描述符指向同一個文件句柄。這種情況可能是打開文件描述符後調用了fork()函數,此時文件描述符的文件偏移量是通用的,因為文件句柄是同一個(見2.1的例子)。
4 不同進程的不同文件描述符指向同一個文件句柄,這種情況是2和1的順序組合的結果,此時文件描述符的文件偏移量也是通用的。
5 同一個進程的文件描述符指向同一個文件(例如多次open()),這種情況下盡管一個進程多次打開同一個文件,資源描述符不相同,但是文件句柄也不相同,其文件偏移量必然不同:

#include   
#include    
#include 
#include 
using namespace std;
int main ()   
{   
    //test.txt存在,內容為
    //1234567890
    int fd1 = open("test.txt", O_RDWR);
    int fd2 = open("test.txt", O_RDWR);
    cout << "fd1: " << fld1 << " fld2: " << fld2 << endl;//fld1:3 fld2:4
    char arr1[2] = {0};
    char arr2[2] = {0};
    read(fld1, arr1, 1);//簡單起見,不再錯誤處理
    read(fld2, arr2, 1);
    cout << "arr1: " << arr1 << endl;//arr1:1,說明是從文件頭開始讀取的
    cout << "arr2: " << arr2 << endl;//arr2:1,說明fd2的偏移量不與fd1共享
    return 0;  
} 

/dev/fd目錄
內核為每個進程提供了一個/dev/fd/n的虛擬路徑,鏈接到對應該進程打開的文件描述符,例如對每個進程來說,/dev/fd/0代表標准輸入。在進程中打開該目錄,相當於復制了一個文件描述符,其指向同一個文件句柄。fd=open("/dev/fd/1")等於fd=dup(1)(dup()的作用見【重定向與復制文件描述符】)。在這種情況下open()傳遞的標准應該與對應的文件描述符一樣。
此外系統還提供了/dev/stdin/dev/stdout/dev/stderr三個文件來方便引用。
一個例子來演示如何操作該目錄下的文件:

#include    
#include     
#include  
#include  
using namespace std; 
int main ()    
{    
    //從標准輸入讀取信息並輸出到文件test.txt中
    //簡單起見不做錯誤處理
    int fd1 = open("/dev/stdin", O_RDONLY); 
    int fd2 = open("test.txt", O_WRONLY|O_CREAT);
    char buf[1024];
    int iCount = read(fd1, buf, 1024);
    if(iCount != -1)
        write(fd2, buf, iCount);
    return 0;   
}  

臨時文件
臨時文件是進程關閉時自動刪除的文件,glibc提供了類似

#include 
int mkstemp(char* template);
FILE* tempfile();

的函數來幫助建立臨時文件。
* mkstemp
template參數是一個字符數組,表示文件的模板,後六個字符必須是”X”,庫函數會修改該值並在成功時返回文件描述符。例如char tempname[]="/temp/fileXXXXXX",在成功後會創建tempname的值會被修改並創建同名文件,進程所有者對其有讀寫權限。一般程序不再使用該臨時文件時可以使用unlink()系統調用解除對該文件的引用,稍後在close()時該文件由於引用數為0,會被自動刪除。當然,如果不顯示close()的話,在進程結束時系統自動調用close()。

#include    
#include 
#include 
int main ()    
{    
    char tempFile[]="/root/cpproot/test/tempfileXXXXXX";
    int fd = mkstemp(tempFile);
    if(fd == -1)
    {
        perror("mkstemp");
        return -1;
    }
    unlink(tempFile);   
    char temp[32] = "hi?";
    write(fd, temp, sizeof(temp));
    lseek(fd, 0, SEEK_SET);
    char temp2[32];
    read(fd, temp2, sizeof(temp2));
    printf("read : %s\n", temp2);
    return 0;   
}  

unlink()系統調用作用是解除進程對文件描述符的引用(記得嗎,引用計數記錄在inode節點中),若在close(fd)時fd對應的文件引用數為0,系統會刪除該文件。在對文件進行操作時,unlink()行為與remove()行為一樣。

tempfile
創建一個可讀可寫的臨時文件並返回文件流,函數內部會調用unlink()保證流關閉時臨時文件被刪除。

2.1 打開文件

在對文件操作前需要打開文件,內核為每個進程維護了一個打開文件的列表。在打開文件後創建子進程,子進程會共享父進程打開的文件、當前文件位置等信息,子進程關閉文件不會對父進程有影響。

以下代碼驗證不同的進程中文件描述符是獨立的:

#include   
#include    
#include 
int main ()   
{   
    pid_t fpid; 
    fpid=fork();   
    if (fpid < 0)   
        printf("error in fork!");   
    //父進程返回子進程pid,子進程返回0
    else if (fpid == 0) {  
    int fd1 = open("test.txt", O_RDWR|O_CREAT );
    printf("child process fd %d\n",fd1 );    //3
    }  
    else {  
    int fd1 = open("test.txt", O_RDWR|O_CREAT );
    printf("parent process fd %d\n",fd1 );    //3
    }  
    return 0;  
} 

可以看出對於不同的進程而言,每個進程的文件描述符都是私有的,內核會為每個進程重新分配文件描述符。

以下代碼驗證子進程共享父進程文件描述符和文件當前位置:

#include 
#include 
#include 
int main ()
{
    pid_t fpid;
    int fd1 = open("test.txt", O_RDWR|O_CREAT );
    fpid=fork();
    if (fpid < 0)
        printf("error in fork!");
    //父進程返回子進程pid,子進程返回0
    else if (fpid == 0)
    {
        write(fd1, "123", 3);
    }
    else
    {
        write(fd1, "abc", 3);
    }
    return 0;  //文件最後是6個字符,如果父子進程分別向0位置寫的話,會有數據被覆蓋
}

可以看出在fork()之後,子進程會共享父進程的文件描述符和文件位置(即共享同一個文件句柄),相對的測試如下:

#include  
#include  
#include  
int main () 
{ 
    pid_t fpid;  
    fpid=fork(); 
    if (fpid < 0) 
        printf("error in fork!"); 
    //父進程返回子進程pid,子進程返回0 
    else if (fpid == 0) 
    { 
        int fd1 = open("test.txt", O_RDWR|O_CREAT );
        write(fd1, "123", 3); 
    } 
    else 
    { 
        int fd1 = open("test.txt", O_RDWR|O_CREAT );
        write(fd1, "abc", 3); 
    } 
    return 0;  //文件最後是3個字符,因為父子進程都是從文件頭開始寫的,有一個進程寫的內容會被另一個覆蓋
} 

2.1.1 open()系統調用

系統調用open()打開文件並返回系統分配的文件描述符,錯誤時返回-1並設置errno,錯誤碼取值可以通過man 2 open查看。

#include 
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode );

pathname-文件路徑
flags-讀寫模式,有O_RDONLY、O_WRONLY、O_RDWR三種,同時可以和以下至少一個選項做或運算:

取值 說明 O_APPEND 每次寫操作都寫入文件的末尾,該標志保證了寫文件的原子性,避免了多個進程寫入同一個文件時內容被覆蓋 O_ASYNC 文件可讀或可寫時產生一個信號(默認SIGIO),只能用於終端和套接字文件上,普通文件無法使用 O_CREAT 如果指定文件不存在,則創建這個文件 O_DIRECT 打開的文件用於直接I/O O_DIRECTORY 如果打開的文件不是目錄則返回錯誤,用於opendir()內部使用 O_LARGEFILE 打開文件時使用64位偏移量,這樣大於2G的文件也能打開,64位架構默認就有該標志 O_EXCL 如果要創建的文件已存在,則返回 -1,並且修改 errno 的值。該標識保證了創建文件的原子性,避免多個進程同時創建文件後都認為文件是自己創建的 O_TRUNC 如果文件存在,並且以只寫/讀寫方式打開,則清空文件全部內容 O_NOCTTY 如果路徑名指向終端設備,不要把這個設備用作控制終端 O_NOFOLLOW 如果文件是一個符號鏈接,則open()會失敗 O_NONBLOCK 如果路徑名指向 FIFO/塊文件/字符文件,則把文件的打開和後繼 I/O設置為非阻塞模式(nonblocking mode) O_DSYNC 等待物理 I/O 結束後再 write。在不影響讀取新寫入的數據的前提下,不等待文件屬性更新 O_RSYNC read 等待所有寫入同一區域的寫操作完成後再進行 O_SYNC 等待物理 I/O 結束後再 write,包括更新文件屬性的 I/O O_CLOEXEC 內核2.6.23開始支持,指定在子進程執行exec()系列函數時自動關閉文件描述符,該參數出現之前使用fcntl()實現相同功能 O_NOATIME 內核2.6.8開始支持,使用該標志時,讀取文件不會更新inode節點中的最新訪問時間,減少讀寫的數據量

mode-新創建文件時用於指定權限,在創建文件時不指定權限會產生未定義的行為。權限在第一次創建文件時不會檢查,因此第一次打開文件時權限是無效的。POSIX規定了以下參數可以用或運算同時指定:

取值 說明 S_IRWX(U/G/O) 所有者、所有組、其他用戶有讀寫執行權限 S_IX(USR/GRP/OTH) 所有者、所有組、其他用戶有執行權限 S_IR(USR/GRP/OTH) 所有者、所有組、其他用戶有可讀權限 S_IW(USR/GRP/OTH) 所有者、所有組、其他用戶有可寫權限

2.1.4 creat()系統調用

由於O_WRONLY|O_CREAT|O_TRUNC組合非常常見,有專門的函數來實現相同功能(某些架構上可能沒有該系統調用,使用的是庫函數):

#include 
int creat(const char* name, mode_t mode);

返回值與open()一樣。

2.2 用read()讀取文件

read()是系統調用,用於從fd代表的文件中讀取最多len個字節到buf中,正確時返回讀取的字節數,失敗時返回-1並設置errno,只讀到一個文件結束符(EOF)時返回0。

#include 
ssize_t read (int fd, void *buf, size_t len);

每次read()成功後內核保存的文件位置指針會向前移動len個字節長度。這裡的位置是內核記錄的位置,與庫調用時的位置不一樣——庫函數例如fgetc(),可能每次從內核讀取1024個字符,但是每次只返回一個字符,此時內核的位置應該是1024而不是1。
以下代碼驗證對於一個文件描述符,不同進程共享同一個文件位置:

#include 
#include 
#include 
int main ()
{
    pid_t fpid;
    //假設test.txt存在且內容為"abcd",父子進程讀取出來的是a和b而不是a和a
    int fd1 = open("test.txt", O_RDWR );
    fpid=fork();
    if (fpid < 0)
        printf("error in fork!");
    //父進程返回子進程pid,子進程返回0
    else if (fpid == 0)
    {
        char temp[1024] = {0};
        read(fd1, temp, 1);
        printf("child read:%s", temp);
    }
    else
    {
        char temp[1024] = {0};
        read(fd1, temp, 1);
        printf("parent read:%s", temp);
    }
    return 0;
}

當文件沒有數據可讀或未讀完len個字節時(並且沒讀取到EOF,例如網絡通信時)read()調用會阻塞,直到讀滿。

read()可能會有如下返回值對應的場景:
返回len,說明需要的數據長度全部讀取到內存中
返回小於len,說明讀取過程中有信號大端、讀取時發生了錯誤、讀取到EOF,再次讀取時將剩余數據讀到內存中
返回0,讀到EOF
調用阻塞,等待可讀的數據到來
調用返回-1,errno為EINTR,表示讀取之前收到信號,可以再次調用
調用返回-1,errno為EAGAIN,表示沒有可用數據,在非阻塞模式下發生
調用返回-1,errno為其他值,表示發生的其他嚴重錯誤

一個比較全面的代碼是這樣的:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) 
{
    if (ret == -1) {
        if (errno == EINTR)
            continue;
        perror (”read”);
        break;
    }
    len -= ret;
    buf += ret;
}

函數在讀取到len個字節或遇到EOF之前會一直嘗試從fd中讀取,在出現中斷時再次讀取,在出現錯誤時打印錯誤信息並退出循環。

2.2.3 非阻塞讀

read()系統調用在等待數據時會阻塞,在有些情況下我們不希望等待數據時阻塞,而是做一些其他操作——例如有多個文件要讀,阻塞在一個上面不如看看其他文件有沒有數據(當然有更好的辦法,見I/O多路復用);或者有大量數據要在後面的代碼中處理,而且數據不依賴於從文件中讀取的內容,此時可以使用非阻塞I/O模式。在沒有數據可讀時read()也立即返回-1,並將errno設置為EAGAIN。進入非阻塞I/O模式需要在open()時指定O_NONBLOCK參數。

2.2.5 read()大小限制

POSIX規定了size_t和ssize_t類型表示占用內存的大小,ssize_t是有符號版的size_t,32位操作系統上是int,64位系統是long int。size_t最大值是SIZE_MAX,ssize_t則為SSIZE_MAX。若len大於SSZIE_MAX,read()調用結果是未定義的。

2.3 用write()來寫

write()也是系統調用,表示從fd代表的當前位置讀取最多count個字符到buf中。讀取成功時返回真實的讀取字符數,否則返回-1且設置errno。當count比SSIZE_MAX還大,調用的結果未定義。

#include 
ssize_t write (int fd, const void *buf, size_t count);

2.3.1 部分寫

write()不太可能返回一個小於count的數,對於普通文件,發生部分寫時很可能出現了錯誤。因此對於普通文件,出現部分寫時不需要通過循環保證所有字符都寫入文件。但對於socket等特殊文件來說,最好使用循環來保證全部寫入文件。

ssize_t ret, nr;
while (len != 0 && (ret = write (fd, buf, len)) != 0)
{
  if (ret == -1)
  {
    if (errno == EINTR)
        continue;
    perror (”write”);
    break;
    }
  len -= ret;
  buf += ret;
}

2.3.2 追加模式

文件在open()時如果指定O_APPEND參數,則寫操作每次都從文件末尾開始。例如有多個進程向同一個文件寫入,追加模式保證了每個進程都不會覆蓋其他進程寫入的數據,因為系統能夠保證每次寫入都從尾部開始,對日志功能非常有用。

2.3.3 非阻塞寫

文件在open()時如果指定了O_NONBLOCK參數,則write()會正常阻塞時,調用會立即返回-1並設置errno為EAGAIN,普通文件不會出現這種情況。

2.3.4 其他錯誤碼

write()操作會產生的錯誤碼還有:

錯誤碼 描述 EBADF 給定的fd非法或不是以寫方式打開的 EFAULT 給定的buf不在進程地址空間內 EFBIG 寫操作使文件超出進程最大文件限制 EINVAL fd對應的對象無法進行寫操作 EIO 發生了一個底層I/O錯誤 ENOSPC 文件系統沒有足夠空間 EPIPE fd對應的管道或socket的讀端被關閉,同時進程會受到SIGPIPE信號

2.3.6 write()的行為

由於硬盤I/O速度與CPU處理速度相差較大,在程序執行時等待數據真正寫入硬盤對程序性能有較大影響,因此內核提供了內核緩沖區用於存放要寫入硬盤的數據。write()操作在數據從用戶空間拷貝到內核空間時即返回,內核會確保在某個合適的時機將數據寫入硬盤。
這種延遲寫入的方式不會改變交替讀寫的結果:當需要read()剛剛寫入硬盤的數據時,若此時數據在內核緩沖區中還未寫入硬盤,讀取的將是緩沖區的數據,減少了讀取硬盤的次數。
為了保證及時將輸入寫入硬盤,在/proc/sys/vm/dirty_expire_centiseconds中配置了最大時效,內核會在超出最大時效之前將數據寫入硬盤。內核緩沖區

大文件讀寫

系統變量類型是指一些系統實現細節相關的變量類型typedef成不暴露實現細節的變量類型,從而保證跨平台時源碼級別的兼容性。例如對於進程id,其系統變量類型是pid_t,常見的還有size_tsocklen_t等,在sys/types.h中定義。
32位Linux上,文件偏移量類型off_t類型為int,32位,即能夠尋址的文件最大為2G。若想實現32位系統上對超過2G的文件尋址,需要將_FILE_OEFFSET_BISTS宏設置為64,可以在包含其他頭文件之前#define _FILE_OEFFSET_BISTS 64或在makefile中添加-D_FILE_OEFFSET_BISTS=64,編譯時根據該條件會再次將off_t類型typedef成__off64_t,這個變量是64位長度的。通過這個宏,在不修改源碼的前提下將只支持最大2G操作的源碼擴展成支持最大2^63-1大小。
除此之外還能使用過度型API來指明對大文件讀寫,包括open64()lseek64()等。更推薦的是在32位Linux上使用_FILE_OEFFSET_BISTS宏。此外使用宏定義的方式對大文件做支持時要注意,所有與文件讀寫相關的模塊在編譯時都要使用該宏,避免類型不一致導致的問題。
64位Linux的off_t長度默認就是64位的,因此不需要定義上述的宏。

Copyright © Linux教程網 All Rights Reserved