1. 程式人生 > >第4章 檔案IO:通用的IO模型

第4章 檔案IO:通用的IO模型

我們現在正式看一下系統呼叫API。檔案是一個很好的起點,因為它們是UNIX的核心。本章的重點是用於執行檔案輸入和輸出的系統呼叫。 我們會介紹 檔案描述符(file descriptor) 的概念,然後看一下I/O模型中用到的系統呼叫。這些系統呼叫用於開啟和關閉檔案,讀取和寫入資料。 我們主要介紹磁碟檔案的I/O。但是這裡涉及到的大部分知識點與後面章節相關,因為執行I/O的系統呼叫適用於所有檔案型別,如pipes和終端。 第5章對本章的檔案I/O進行更深入細緻的討論。主要講檔案I/O的緩衝,緩衝足夠的複雜,所以值得專門拿出一個章節進行討論。第13章講核心中和stdio庫中的I/O緩衝。 在這裡插入圖片描述

4.1 概述 (Overview)

所有用於執行I/O的系統呼叫都會涉及到使用 檔案描述符file descriptor 來開啟檔案。檔案描述符是一個(很小的)非負整數。檔案描述符適用於開啟檔案的所有型別,包括管道、FIFOs、socket、終端和普通檔案。每個程序都有自己的一組檔案描述符。 按照慣例,大部分程式都應該可以使用 Table 4.1中列出的三種標準檔案描述符。程式會繼承shell的檔案描述符的拷貝。如果在一個命令列中指定了I/O重定向(rediresction),那麼shell會確保程式啟動之前檔案描述符都會被合適的修改。

當在程式中涉及到這些檔案描述符時,我們可是使用數字(0,1,或2)或者更好的是使用在 <unistd.h>

中定義的 POSIX標準名稱

儘管變數stdin、stdout和stder剛開始是指程序的標準輸入、輸出和錯誤,但是使用freopen()庫函式,可以將它們改成指向任何檔案。freopen()可能改變檔案描述符,例如在stdout上使用freopen(),就不能認為檔案描述符仍然是1(可能已經變成其他值)。

以下是執行檔案I/O的四個核心系統呼叫(程式語言和軟體包,一般只有通過I/O庫間接採用這些呼叫):

  • fd = open(pathname, flags, mode) 開啟pathname中指定的檔案,返回一個檔案描述符,用於在隨後的呼叫中,指代開啟的檔案。如果檔案不存在,open()會根據flags引數,決定是否建立它。flags引數還指定了將要開啟的檔案是用於讀的、寫的還是同時可讀寫的。如果該檔案是這個呼叫新建立的,mode引數用於設定該檔案的許可權。如果該檔案不是新建立的,mode引數不起作用或者可以被忽略。
  • numread = read(fd, buffer, count) 用於從fd所指向的已開啟的檔案中讀取最多count個位元組,並將這些位元組儲存到buffer中。read()這個呼叫返回實際讀取位元組的個數。如果沒有更多位元組可以讀取(例如,遇到end-of-file,EOF),那麼read()就返回0。
  • numwritten = write(fd, buffer, count) 用於將count個位元組從buffer中寫到fd所指向的已開啟的檔案中。write()這個呼叫返回實際寫入的位元組個數,可能小於count。
  • status = close(fd) 在所有I/O完成的情況下呼叫,用於釋放檔案描述符fd和它相關的核心資源。

在我們講解這些系統呼叫的細節之前,我們使用Listing 4-1來簡短地展示一下這些系統呼叫的用法。

#include <sys/stat.h>
#include <fcntl.h>
#include "lib/tlpi_hdr.h"

#ifndef BUF_SIZE        /* Allow "cc -D" to override definition */
#define BUF_SIZE 1024
#endif

int main(int argc, char *argv[])
{
    int inputFd, outputFd, openFlags;
    mode_t filePerms;
    ssize_t numRead;
    char buf[BUF_SIZE];

    if (argc != 3 || strcmp(argv[1], "--help") == 0)
        usageErr("%s old-file new-file\n", argv[0]);

    /* Open input and output files */
    inputFd = open(argv[1], O_RDONLY);
    if (inputFd == -1)
    	errExit("opening file %s", argv[1]);

    openFlags = O_CREAT | O_WRONLY | O_TRUNC;
    filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
                S_IROTH | S_IWOTH;      /* rw-rw-rw- */
    outputFd = open(argv[2], openFlags, filePerms);
    if (outputFd == -1)
        errExit("opening file %s", argv[2]);

    /* Transfer data until we encounter end of input or an error */
    while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
        if (write(outputFd, buf, numRead) != numRead)
            fatal("couldn't write whole buffer");
    if (numRead == -1)
        errExit("read");

    if (close(inputFd) == -1)
        errExit("close input");
    if (close(outputFd) == -1)
        errExit("close output");

    exit(EXIT_SUCCESS);
}

4.2 I/O的通用性 (Universality of I/O)

UNIX I/O模型的一個顯著性的特徵是I/O的通用性(universality)。這意味著以下四種系統呼叫–open()、read()、write()和close()可用於所有型別檔案的I/O,包括裝置(如終端)。如果我們使用這些系統呼叫寫一個程式,這個程式可用於所有型別的檔案。 之所以I/O具有通用性,是因為每個檔案系統和裝置驅動器的實現使用了相同的一組I/O系統呼叫。因為檔案系統或裝置的細節是由核心內部處理的,所以我們在寫應用程式時一般可以忽略特定裝置的因素。當需要訪問系統或裝置的特定特徵時,程式可以使用ioctl()系統呼叫,它提供了超出I/O模型通用性範圍的特徵。

4.3 使用 open() 開啟檔案 (opening a File: open())

open() 系統呼叫可以開啟一個已存在的檔案或者建立並開啟一個新檔案。

#include <sys/stat.h>
#include <fcntl.h>
// Returns file descriptor on success, or -1 on error
int open(const char *pathname, int flags, ... /*mode_t mode*/);

被開啟的檔案的路徑是由pathname引數確定的。如果pathname是一個符號連結,會被間接引用(dereferenced)(譯者注:即引用到目標檔案)。如果執行成功,open()會返回一個檔案描述符,用於指向這個檔案,隨後的系統呼叫就可以使用這個檔案描述符來訪問這個檔案。如果執行時出現錯誤,open()就會返回 -1,並且會賦予 errno 相應的值。 flags引數是一個 用於指定檔案的 訪問模式(access mode), 使用Table4-2的的其中一個常量。

早期的UNIX實現使用數字0、1和2,而不是Table4-2中的名稱。大部分現代的UNIX定義了這些常量和對應的值。因此,O_RDWR與O_RDONLY | O_WRONLY不相等。後面的組合是一種邏輯上的錯誤。

當open()建立一個新檔案時,mode位遮蔽引數用於給檔案賦予許可權(mode是一個mode_t資料型別,它是SUSv3中定義的整型型別)。如果open()呼叫的flags中不指定O_CREATE,mode就會被忽略。 在這裡插入圖片描述

我們會在章節15.4詳細描述檔案許可權。隨後,我們可以看到新檔案上設定的許可權不僅僅跟mode引數有關,而且還跟process umask(章節15.4.6)、父級目錄的預設訪問控制列表(access control list)有關。同時,我們注意到mode引數可以被設定成一個數字(一般是八進位制)或者更好的是使用Table15-4中列出的由或操作(|)連線的位遮蔽常量。 Listing 4-2 的例子中使用了open()以及剛才簡要描述的flags位:

//Listing 4-2: Examples of the use  of open()
fd = open("startup", O_RDONLY);
if (fd == -1)
	errExit("open");
/* open new or existing file for reading and writing, truncating to zero
bytes; file permssions read+write for woner, nothing for all others
*/
fd = opne("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1)
	errExit("open");

/* Open new or existing file for writing; writes should always append to end of file */
fd = open("w.log", O_WRONLY | O_CREATE | O_TRUNC | O_APPEND, S_IRUSR, S_IWUSR);

if (fd == -1)
	errExit("open");

File descriptor number returned by open()

SUSv3中規定:如果 open() 執行成功,它會確保使用該程序中未使用的最小檔案描述符。我們可以使用這個特點來確保使用特定的檔案描述符來開啟一個檔案。例如,以下程式碼確保使用標準輸入(檔案描述符0)來開啟一個檔案:

int open_file_with_df_0()
{
    /* Close file descriptor 0. if not close, the result will be 3 */
    if (close(STDIN_FILENO) == -1)
        errExit("close", O_RDONLY);
    int fd = open("/data/work/luciuschina/c_study/main.c", O_RDONLY);
    if (fd == -1)
        errExit("open");
    printf("%d",fd);
    return 0;
}

因為檔案描述符0未被使用,open()就會使用描述符0來開啟這個檔案。在章節5.5,我們會使用dup2()和fcntl()來實現相似的結果,但是會使用更加靈活的方式來對檔案描述符進行控制。在那節中,還會展示一個例子,告訴我們為什麼對檔案描述符的控制是很有用的。

4.3.1 The *open() flags * Argument

在Listing 4-2的例子中,我們介紹了flags中的一些位(O_CREAT, O_TRUNC和O_APPEND)以及檔案訪問模式。現在我們更加詳細的看下flags引數。Table 4-3總結了flags中可用於位或操作( | )的所有常量。最後一列表示這些常量是在SUSv3還是SUSv4中標準化的。 在這裡插入圖片描述 Table 4-3中的常量可以分為以下組:

  • 檔案訪問模式標誌 (file access mode flags):它們是O_RDONLY、O_WRONLY和O_RDWR標誌(flags)。它們可以使用 fcntl() F_GETFL操作恢復(章節5.3)。
  • 檔案建立標誌 (file creation flags):它們是Table 4-3的第二部分描述的flags。它們用於控制open()呼叫的各個方面以及隨後I/O操作的選項。這些操作不能被恢復或改變。
  • 開啟檔案狀態標誌(Open file status flags) : 這是Table 4-3剩下的flags。它們可以使用fcntl()的F_GETFL和F_SETFL操作(Section 5.3)來恢復和修改。這種flags有時簡稱為檔案狀態標誌(file status flags)

自從Kernel 2.6.22開始,可以通過 /proc/PID/fdinfo 目錄下的Linux的特定檔案來讀取系統中任意程序的檔案描述符資訊。每個程序開啟的檔案描述符都可以在這個目錄中找到相應的檔案,該檔案的名稱與檔案描述符的數字相同 。該檔案中的 pos 欄位表示當前檔案的偏移量(offset)(章節4.7)。flags欄位是一個八進位制數字,表示檔案訪問模式標誌(file access mode flags)和開啟檔案狀態標誌(open file status flags)。(想要對這個數字解碼,需要檢視C庫標頭檔案中關於這些標誌的數值) 在這裡插入圖片描述

flags常量的詳細介紹如下:

  • O_APPEND: 寫入的資料追加到檔案的末尾。我們會在章節(5.1)討論這個flag的重要性。
  • O_ASYNC: 當open()返回的檔案描述符可以進行I/O時,產生一個signal。這個特點被稱為 訊號驅動I/O(signal-drivern I/O) ,只有特定檔案型別(例如終端、FIFOs和sockets)才有。在Linux中呼叫open()時指定O_ASYNC不會產生影響。想要開啟訊號驅動I/O功能,必須使用fcntl() F_SETFL操作設定這個flag(章節5.3)。
  • O_CLOEXEC (從Linux2.6.23開始):為新的檔案識別符號開啟close-on-exec flag(FD_CLOEXEC)。我們會在章節27.4描述FD_CLOEXEC flag。使用O_CLOEXEC flag可以避免程式進行額外的fcntl() F_SETFD和F_SETFD操作來設定close-on-exec flag。使用後者技術可以避免多執行緒程式競態(race condition)的發生。這種競態會發生在:當一個程式開啟一個檔案描述符,然後嘗試將它標識為close-on-exec,同時另一執行緒進行fork(),然後一個任意程式的exec() (假設第二個執行緒在第一個執行緒開啟檔案描述符和使用fcntl()設定close-on-exec flag期間同時管理fork()和exec())。這種競態會導致開啟的檔案描述符會無意地傳入不安全的程式 (我們會在章節5.1對競態有更多的描述)。
  • O_CREAT:如果檔案不存在,就會建立一個新的、空的檔案。該flag是高效的,即使被開啟的檔案只用於讀取。如果我們指定O_CREAT,那麼必須在open()呼叫中提供一個mode引數。否則,新檔案的許可權會被設定成棧中的一個隨機值。
  • O_DIRECT: 允許檔案I/O繞過buffer快取。這個特點會在章節13.6中描述。
  • O_DIRECTORY:如果pathname不是一個目錄,返回一個錯誤(errno等於ENOTDIR)。該flag是專門為實現 opendir() 而設計的。
  • O_DSYNC (從Linux 2.6.33開始):根據同步I/O資料完整性完成的需求執行檔案寫入。請看章節13.3中核心I/O快取。
  • O_EXCL:該flag用於結合O_CREAT來表示如果該檔案已經存在,就不應該被開啟。如果檔案已經存在了,Open就會失敗,errno被設定成EEXIST。換句話說,該呼叫確保是該程序建立了這個檔案。檔案存在的檢查和建立是原子的(atomically)。我們會在章節5.1討論原子(atomicity)的概念。如果O_CREAT和O_EXCL都在flags中被設定,如果pathname是一個符號連結,那麼Open()就會失敗(errno是EEXIST)。SUSv3需要這種行為,這樣特權應用可以在一個已知的地方建立一個檔案,而不會存在符號連結引起的在不同位置(例如一個系統目錄)建立檔案,引起安全隱患的可能性。
  • O_LARGEFILE: 支援開啟一個很大的檔案。該flag在32位系統中使用,為的是可以開啟大檔案。儘管不在SUSv3中指定,但是在某些UNIX實現中可以找到O_LARGEFILE flag。在64位Linux系統中,這個flag不會產生影響。更多資訊請看章節5.10。
  • O_NOATIME (從Linux 2.6.8開始):當從檔案讀取資料時,不更新檔案的最近訪問時間 (在章節15.1中會描述st_atime欄位)。想要使用該flag,呼叫程序的 有效使用者ID(effective user ID) 必須與檔案的擁有者相匹配,或者該程序必須是特權的(CAP_FOWNER);否則open()會失敗,錯誤是EPERM。
  • O_NOCTTY: 如果被開啟的檔案是一個終端裝置,防止它稱為控制終端(controlling termianl)。控制終端會在章節34.4中討論。如果被開啟的檔案不是一個終端,該flag不產生效果。
  • O_NOFOLLOW:通常情況下,如果open()的是一個符號連結,會間接引用到目標檔案。但是如果指定O_NOFOLLOW flag,那麼open()的是符號連結時就會失敗(errno被設成ELOOP)。這個flag很有用,特別是在具有特權的程式中,會確保open()不會開啟一個符號連結。
  • O_NONBLOCK:以非阻塞模式開啟檔案。請看章節5.9。
  • O_SYNC:為同步的 (synchronous) I/O開啟檔案。請看章節13.3中核心I/O快取的討論。
  • O_TRUNC:如果已經存在的檔案是一個普通檔案,那麼刪除存在的資料(清空檔案),檔案長度變為零。在Linux中,無論是為了讀或寫而開啟檔案,truncation可能發生。

4.3.2 Errors from open()

如果嘗試開啟檔案的過程中發生錯誤,open()會返回-1,並且errno會標識錯誤的原因。下面是一些可能發生的錯誤:

  • EACCES:在flags指定的模式下,檔案許可權不允許呼叫程序開啟檔案。另外,因為目錄許可權,檔案不能被訪問,或者檔案不存在,且不能被建立。
  • EISDIR:想要開啟的檔案是一個目錄,呼叫者嘗試開啟檔案並寫入。這是不被允許的。
  • EMFILE:開啟檔案描述符的數量達到了程序的資源限制 (章節36.3描述的RLIMIT_NOFILE)。
  • ENFILE:開啟檔案描述符的數量達到了系統範圍(system-wide)的限制。
  • ENOENT:想要開啟的檔案不存在,並且沒有指定O_CREAT,或者指定了O_CREAT,但是pathname的目錄不存在,或者是一個指向不存在的pathname的符號連結(a dangling link)
  • EROFS:想要開啟的檔案是檔案系統中的只讀檔案,但是呼叫者嘗試開啟檔案並用於寫。
  • ETXTBSY:想要開啟的檔案是一個可執行檔案(一個程式),並且當前正在執行。與正在執行程式相關的檔案不允許被修改(想要修改必須先終止這個程式)。 當我們隨後描述其他的系統呼叫或者庫函式時,我們一般不會列出上述的這些可能存在的錯誤。上面的列表是不完整的,open()方法失敗的更多原因可以通過open(2)手冊頁檢視。

4.3.3 The creat() System Call

在早期的UNIX系統實現中,open()只有兩個引數,並且不能用於建立新檔案。而creat()系統呼叫用於建立和開啟一個新檔案。

#include <fcntl.h>
//返回一個檔案描述符,發生錯誤時返回-1
int creat(const char *pathname, mode_t mode);

creat()系統呼叫以給定的pathname,建立和開啟一個新的檔案,如果這個檔案已經存在,開啟檔案,並且清空檔案中的資料。creat()返回一個檔案描述符,可以在隨後的系統呼叫中使用。呼叫creat()與下面的open()等價:

fd = open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)

因為open() flag引數提供了對開啟檔案的更強大的控制(例如,使用O_RDWR而不是O_WRONLY)。creat()現在已經是過期的了,儘管在舊的程式中仍然可以看到 。

4.4 Reading from a File:read()

read()系統呼叫從描述符fd指向的開啟的檔案中讀取資料。

# include <unistd.h>
//返回讀取的位元組數,EOF時返回0,發生錯誤時返回-1
ssize_t read(int fd, void *buffer, size_t count);

count 引數指定了可以讀取的最大位元組數。(size_t資料型別是一種無符號整數型別)buffer引數提供了輸入資料儲存到的buffer記憶體的地址。這塊buffer的長度至少是count個位元組。

系統呼叫不會為為我們分配buffer。我們必須傳入一個指向之前分配過記憶體的具有正確size的buffer地址。 一次成功的呼叫read()會返回實際讀取到的位元組數,如果遇到end-of-file就返回0。報錯的時候通常返回-1。ssize_t資料型別是一個有符合的整型型別,用於持有一個位元組的count或者錯誤時的-1。 一次呼叫read()讀取到的資料可能小於設定的位元組數(count)。對於普通檔案,產生這種情況的可能原因是快接近檔案末尾了。 當read()應用於其他檔案型別–例如管道(pipe)、FIFOs、sockets或者終端–在多種環境下讀取到的位元組會小於設定的位元組數。例如,預設情況下,從終端進行一次read(),只會將這一行讀取完畢(遇到\n字元)。在隨後的章節中,當遇到其他的這種情況,我們再說。

4.5 Writing to a File:write()

write()系統呼叫將資料寫入到一個開啟的檔案。

#include <unistd.h>
//返回寫入的位元組數,當發生錯誤時返回-1
ssize_t write(int fd, void *buffer, size_t count);

write()的引數與read()的相似:buffer是將要被寫入資料的地址;count是從buffer中寫入的位元組數,fd指向資料將要寫入檔案的檔案描述符。 執行成功時,write()返回實際寫入的位元組數,這可能小於count。對於磁碟檔案來說,可能的原因是partial write時磁碟滿了或者是達到了file sizes這個程序資源限制(章節36.3中描述的RLIMIT_FSIZE)。 當在一個磁碟檔案執行I/O時,從write()成功返回不能確保資料已經被寫入到磁碟中了,因為核心執行了 磁碟I/O緩衝 ,為的是減少磁碟活躍性和加快write()呼叫 (我們會在第13章來考慮這些細節)。

4.6 Closing a File: close()

close() 系統呼叫關閉一個開啟的檔案描述符,釋放檔案描述符,供給程序隨後重用。當程序終止時,所有開啟的檔案描述符會自動關閉。

# include <unistd.h>
//成功時返回0,出錯時返回-1
int close(int fd);

明確地關閉不再需要的檔案描述符是一個很好的習慣,因為這使得我們的程式碼可讀性更高,程式可靠性更高。此外,檔案描述符是一種消耗性資源,所以關閉檔案描述符失敗可能會導致程序耗盡描述符資源。當程式是一個長時間存活的,且會處理很多檔案的程式時,這個問題尤其嚴重。 像其他系統呼叫那樣,對close()的呼叫應該加上錯誤檢查程式碼,例如:

if (close(fd) == -1)
	errExit("close");

4.7 Changing the File Offset: lseek()

對於每個開啟的檔案,核心會記錄一個 檔案偏移量(file offset) ,有時也稱為 讀寫偏移量read-write offset 或者 指標(pointer)。這是檔案中的位置,指向下一次read()或者write()開始的位置。檔案的偏移量表示成有序的位元組的位置,檔案中第一個位元組的偏移量是0。 當開啟檔案時,檔案的偏移量會指向檔案的起始位置。後面的每一次read()或者write()都會自動調整偏移量。所以在讀取或者寫入位元組後,檔案偏移量會指向下一個位元組。因此,連續的read()和write()呼叫會在檔案中按序執行。 lseek()系統呼叫 可以根據 offsetwhence 指定的值調整 fd 檔案描述符所指向檔案的偏移量。

#include <unistd.h>
//如果執行成功,返回檔案偏移量,發生錯誤則返回-1
off_t lseek(int fd, off_t offset, int whence);

offset 引數用於指定一個值(off_t資料型別是有符號的整型)。whence 引數有以下值:

  • SEEK_SET:檔案偏移量被條成為檔案起始位置(檔案偏移量為0)加上offset
  • SEEK_CUR:檔案偏移量被調整為當前檔案偏移量加上offset
  • SEEK_END: 檔案偏移量被調整為檔案的size加上offst。

Figure 4-1 展示了whence引數如何被解析。

在早期的UNIX系統實現中,使用整數0、1和2,而不是文中的SEEK_*常量。BSD的老的版本使用不同的名稱表示這些值:L_SET、L_INCR和L_XTND。

在這裡插入圖片描述 如果whence是SEEK_CUR或者SEEK_END,offset可能是負數的或者正數的;對於SEEK_SET,offset必須是非負數。 一次成功的lseek()呼叫會返回一個新的偏移量。下面語句會得到檔案偏移量的當前位置,而不會修改它:

curr = lseed(fd, 0, SEEK_CUR)

一些UNIX系統實現(但不是Linux)具有非標準的 tell(fd) 函式,該函式與上面的lseek()呼叫功能相同。

以下是一些lseek()呼叫的例子,以及相應的註釋,表示檔案偏移量會移動到哪裡:

lseek(fd, 0, SEEK_SET)/*檔案起始位置*/
lseek(fd, 0, SEEK_END)/*檔案末尾的下一個位元組*/
lseek(fd, -1, SEEK_END)/*檔案的最後一個位元組*/
lseek(fd, -10, SEEK_CUR);                       /*當前位置的往前移10個位元組的位置*/
lseek(fd, 10000, SEEK_END);                     /*檔案最後一個位元組的位置再往後移10001個位元組*/

呼叫lseek() 僅僅調整了核心中的與檔案描述符相關的檔案偏移量記錄。不會引起物理裝置的訪問。 在5.4節中我們會更詳細地描述檔案偏移量、檔案描述符和開啟檔案之間的關係。 lseek()不能運用於所有的檔案型別。不能在pipe、FIFO、socket或者terminal上使用lseek()。lseek()失敗時,errno被設定成ESPIPE。另外,在裝置上使用lseek()是可能的。例如,可以在磁碟上尋找(seek)到特定的位置。

lseek()中的l的意思是offset引數和返回值都是long型。早期的UNIX系統實現提供了seek()系統呼叫,返回的值是int型

File holes (檔案洞)

如果一個程式seek超過了檔案的末尾會發生什麼,仍舊會執行I/O嗎?呼叫read()會返回0,表示檔案末尾。令人奇怪的是,在超過檔案末尾的任意點,都可能寫入資料。 從檔案末尾到新寫入位元組之間的 空間(space) 被稱為 file hole(檔案洞) 。從程式設計的角度看,hole中的位元組是存在的,從hole中讀取會返回包含0位元組(null位元組)的緩衝。 然而,File holes不佔用任何磁碟空間。檔案系統不會為hole分配任何磁碟塊(disk blocks)直到在後面的某個時刻有資料被寫入到檔案中。File holes的主要優點是一個稀疏的填充檔案(a sparsely populated flie)會消耗較少的磁碟空間,因為null bytes不佔用任何磁碟空間。核心的dump檔案(章節22.1)是一種包含大量holes的檔案。 holes的存在意味著一個檔名義上的大小(nominal size)可能比它使用的磁碟儲存的量要大。將位元組寫入file hole的中間會降低可用磁碟空間的數量,因為核心分配塊(block)來填補hole,儘管檔案的大小可能不會改變。這種場景是不常見的,但是需要值得注意。 章節 14.4 會描述檔案中的holes是如何表示的。在章節15.1會描述stat()系統呼叫,它會告訴我們一個檔案當前的大小,以及實際為這個檔案分配的塊的數量。

Example program

Listing 4-3 展示了lseek() 以及read()和write()的用法。該程式的第一個命令列引數是將要開啟的檔案的名稱。剩下的引數指定了將要在檔案上執行的I/O操作。

#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include "../lib/tlpi_hdr.h"
// fileio/seek_io.c
int main(int argc, char *argv[])
 {
    size_t len;
    off_t offset;
    int fd, ap, j;
    char *buf;
    ssize_t numRead, numWritten;
    if (argc < 3 || strcmp(argv[1], "--help") == 0)
    usageErr("%s file {r<length> | R<length> | w<string> | s<offset>}...\n", argv[0]);
    fd = open(argv[1], O_RDWR | O_CREAT ,
              S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); /*rw-rw-rw-*/
    if (fd == -1)
        errExit("open");
    for (ap = 2; ap < argc; ap++)
    {
        switch (argv[ap][0]) {
            //Display bytes at current offset, as text
            case 'r':

            //Display bytes at current offset, in hex
            case 'R':
                len = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
                buf = malloc(len);
                if (buf == NULL)
                    errExit("malloc");
                numRead = read(fd, buf, len);
                if (numRead == -1)
                    errExit("read");
                if (numRead == 0)
                {
                    printf("%s: end-of-file\n", argv[ap]);
                }
                else
                {
                    printf("%s:", argv[ap]);
                    for (j = 0; j < numRead; j++)
                    {
                        if (argv[ap][0] == 'r')
                            printf("%c", isprint((unsigned char) buf[j]) ? buf[j] : '?');
                        else
                            printf("%02x ", (unsigned int) buf[j]);
                    }
                    printf("\n");
                }
                free(buf);
                break;

            // Write string at current offset
            case 'w':
                numWritten = write(fd, &argv[ap][1], strlen(&argv[ap][1]));
                if (numWritten == -1)
                    errExit("write");
                printf("%s: wrote %ld bytes\n", argv[ap], (long) numWritten);
                break;
            case 's':
                offset = getLong(&argv[ap][1], GN_ANY_BASE, argv[ap]);
                if (lseek(fd, offset, SEEK_SET) == -1)
                    errExit("lseek");
                printf("%s: seek succeeded\n", argv[ap]);
                break;
            default:
                cmdLineErr("Argument must start with [rRws]: %s\n", argv[ap]);\
        }
    }
    return 0;
}

上面程式碼編譯後的檔名為seek_io,我們看一下從file hole中讀取位元組會發生什麼:

$ touch tfile                                    Create new,empty file $ ./seek_io tfile s100000 wabc     Seek to offset 100000,write “abc” s100000: seek succeeded wabc: wrote 3 bytes $ ls -l tfile                                       Check size of file -rw-r–r-- 1 mtk users 100003 Feb 10 10:35 tfile $ ./seek_io tfile s10000 R5            Seek to offset 10,000, read 5 bytes from hole s10000: seek succeeded R5: 00 00 00 00 00                          Bytes in the hole contain 0

4.8 Operations Outside the Universal I/O Model:ioctl()

系統呼叫是一種用於執行 檔案裝置 操作的通用機制,但是正如本章之前提到的,它不具有I/O模型的通用性(即有些檔案型別沒有這個系統呼叫)。

#include <sys/ioctl.h>
//根據request,返回正確的結果值,發生錯誤時返回-1
int ioctl(int fd, int request, ... /*argp*/);

fd引數是檔案或裝置的檔案描述符,需要執行的控制操作在request中指定。特定裝置(device-specific)的標頭檔案中定義的常量可以傳入到request引數中。 正如標準的C省略符號(…)所示,ioctl()中的第三個引數argp可以是任意型別。request引數中的值使得ioctl()能夠知道argp這個值的型別。一般來說,argp是一個指向整型或者結構體(structure)的指標。在有些情況下,不需要使用到這個引數。 在後面的章節,我們可以看到ioctl()的一些用處(例如章節15.5)。

SUSv3中唯一為ioctl()指定的規範是對STREAMS裝置的控制操作。本書中提到的其他的ioctl()操作都沒有在SUSv3中指定。但是ioctl()呼叫從很早開始就已經稱為UNIX系統的一部分。我們描述的多種ioctl()操作在很多UNIX系