1. 程式人生 > >UNIX環境高階程式設計(3) 第三章

UNIX環境高階程式設計(3) 第三章

3 檔案I/O

3.1 引言

3.2 檔案描述符

檔案描述符是一個標示,非負整數,類似於windows裡的控制代碼,為了與標準C保持一致(標準C裡的檔案的讀寫都是通過File Pointer)UNIX採用了這樣的三級結構,我混淆於檔案描述標誌和檔案狀態標誌,還是看英文來的有效,fd 的flag = close_on_exec。是在一個檔案在某程序中的標示,而由於檔案可以被多個程序開啟,因此這個file status flag能被很多個程序訪問到,它表示的是這個檔案在此刻的讀寫等標示。

對開啟檔案的處理與每個描述符的執行時關閉(close_on_exec)標誌值有關,程序中每個開啟描述符都有一個執行時關閉標誌。若設定了此標誌,則在執行exec時關閉該描述符;否則該描述符人開啟。除非特地用fcntl設定了該執行時關閉標誌,否則系統預設是在exec後保持描述符開啟狀態。

3.3 函式open和openat

<fcntl.h>
    int open(const char *path, int oflag, .../* mode_t mode */);
    int openat(int fd, const char *path, int oflag, .../* mode_t mode */);
    返回值:若成功,返回檔案描述符;若出錯,返回-1.

oflag:以怎樣的方式來開啟或建立檔案。 mode:賦予檔案讀寫許可權,在第四章中st_mode會有介紹。

O_RDONLY    只讀方式開啟
O_WRONLY    只寫方式開啟
O_RDWR      讀寫開啟
O_EXEC      只執行開啟
O_SEARCH    只搜尋開啟(應用於目錄)
以上必須且只能指定其中一個,下面的常量是可選的:
O_APPEND    每次寫時追加到末尾
O_CLOEXEC   把FD_CLOEXEC常量設定為檔案描述符標誌
O_CREAT     若此檔案不存在,則建立它,使用時,open的第3個選項mode以指定新檔案的訪問許可權位
O_DIRCTORY  如果path引用的不是目錄,則出錯
O_EXCL      如果同時指定了O_CREAT,而檔案已經存在,則出錯。可測試一個檔案是否存在。
O_NOCTTY    如果path引用的是終端裝置,則不將該裝置分配作為此程序的控制終端。
O_NOFOLLOW  如果path引用的是一個符號連結,則出錯
O_NONBLOCK  如果path引用的是一個FIFO、一個塊特殊檔案或一個字元特殊檔案,則選項為檔案的本次開啟操作和後續的I/O操作設定非阻塞方式。
O_SYNC      使每次write等待物理I/O操作完成,包括由該write操作引起的檔案屬性更新所需的I/O.
O_TRUNC     如果此檔案存在,而且為只寫或讀-寫成功開啟,則將其長度截斷為0。
O_TTY_INIT  如果開啟一個還未開啟的終端裝置,設定非標準termios引數值,使符合Single UNIX specification.
下面兩個也是可選,是Single UNIX specification中同步輸入和輸出選項的一部分。
O_DSYNC     使每次write要等待物理I/O操作完成,但是如果該寫操作並不影響讀取剛寫入的資料,則不需等待檔案屬性被更新。
O_RSYNC     使每一個以檔案描述符作為引數進行的read操作等待,直至所有對檔案同一部分掛起的寫操作都完成。

fd引數將兩者區分開,當path為相對路徑,fd指出開始地址或當前工作目錄(AT_FDCWD)

3.4 函式create

#include <fcntl.h>  
int creat(const char *path, mode_t mode);
    返回值:成功返回只寫開啟的檔案描述符,出錯返回-1
    下面方式可建立、寫、讀檔案:open(path,O_RDWR|O_CREAT|O_TRUNC,mode);
關閉一個開啟的檔案

3.5 函式close

#include <unistd.h>
    int close(int fd);
    返回值:成功返回0
;出錯返回-1 當一個程序終止時,核心自動關閉它開啟的所有檔案(隱式關閉)。

3.6 函式lseek

顯示地為一個開啟檔案設定偏移量

<unistd.h>
    off_t lseek(int fd, off_t offset, int whence);
    返回值:成功返回新的檔案偏移量;出錯返回-1

whence:

SEEK_SET:將該檔案的偏移量設定為距檔案開始處offset個位元組。
SEEK_CUR:將該檔案的偏移量設定為當前值加offset,offset可正可負。
SEEK_END:將該檔案的偏移量設定為檔案長度加offset,可正可負。

如果檔案描述符指向的是一個管道、FIFO或網路套接字,則lseek返回-1,並將errno設定為ESPIPE。

3.7/8 函式read和write

<unistd.h>
    ssize_t read(int fd, void *buf, size_t nbytes);
    返回值:讀到的位元組數,若到檔案結尾,返回0;出錯返回-1。
    ssize_t write(int fd, const void *buf, size_t nbytes);
    返回值:成功返回已寫的字元數;出錯返回-1

3.9 I/O的效率

Richard Steven通過程式實驗發現系統CPU時間的最小值在緩衝空間大小為4096個位元組以後,繼續增加緩衝區長度對讀操作所用時間幾乎沒有影響。

3.10 檔案共享

UNIX系統支援不同程序間共享開啟檔案。首先介紹核心用於所有I/O的資料結構:

核心使用3種資料結構表示開啟檔案,他們之間的關係決定了檔案共享方面一個程序對另一個程序可能產生的影響: (1)每個程序在程序表中都有一個記錄項,記錄項中包含一張開啟檔案描述符表,可將其視為一個向量,每個描述符佔用一項,與滅一個檔案描述符相關聯的是:

a,檔案描述標誌(close_on_exec,參見圖3-7和3-14)
b,指向一個檔案表項的指標。

(2)核心為所有開啟檔案維護昂檔案表,每一檔案表項包含:

1) 檔案狀態標誌(讀、寫、添寫、同步和非阻塞等)
2) 當前檔案偏移量
3) 指向該檔案v節點表項的指標。

(3)每個開啟檔案(或裝置)都有一個v節點(v-node)結構。v節點包含了檔案型別和對此檔案進行各種操作函式的指標。還包含了該檔案的i節點(i-node,索引節點),這些資訊實在開啟檔案時從磁碟上讀入記憶體的,所以,檔案的所有相關資訊都是隨時可用的。例如,i節點包含檔案所有者、檔案長度、只想檔案實際資料塊 在磁碟上所在位置的指標等。

這裡書上有幾張幫助理解程序表項、檔案表項及v節點表項的圖,筆記格式原因就沒有放,總的圖我會做在後面的筆記中。

3.11 原子操作

#include <unistd.h>
    ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
    返回值:讀到的位元組數,若已到檔案尾,返回0;若出錯,返回-1
    ssize_t pwrite(int fd, const void *buf,size_t nbytes, off_t offset);
    返回值:成功,返回已寫的位元組數;若出錯,返回-1

呼叫pread相當於呼叫lseek後再呼叫read,具體程式碼如下:

if (lseek(fd, OL, 2) < 0)   /* position to EOF */
    err_sys("lseek error");
if (read(fd, buf, 100) != 100)
    err_sys("read error");

單程序上面沒有問題,但是多程序若同時使用上方法,就會產生偏移量錯亂的問題,所以pread與上方法呼叫有重要區別:

1,呼叫pread是,無法中斷其定位和讀操作;
2,不更新當前檔案偏移量。

這樣保證每個程序執行時是安全的,pwrite函式也是類似。

3.12 函式dup和dup2

複製一個現有的檔案描述符

#include <unistd.h>
    int dup(int fd);
    int dup2(int fd,int fd2);
    返回值:成功返回新的檔案描述符;出錯返回-1

3.13 函式sync、fsync和fdatasync

延遲寫:核心通常先將資料複製到緩衝區,然後排入佇列,晚些時候在寫入磁碟。 為了保證磁碟上實際檔案系統與緩衝區中的內容的一致性,UNIX系統提供了sync、fsync和fdatasync三個函式

#include <unistd.h>
    int fsync(int fd);
    int fdatasync(int fd);
    返回值:成功返回0;出錯返回-1
    void sync(void);

sync只是將所有修改過得塊緩衝區排入寫佇列,然後就返回,他並不等實際寫磁碟操作結束。通常稱為Update的系統守護程序週期性的呼叫(一般30s)sync函式,這就保證定期沖洗(flush)核心的塊緩衝區。

fsync函式只對檔案描述符fd指定的一個檔案起作用,並且等待寫磁碟操作結束才返回。

fdatasync函式類似於fsync,但他隻影響檔案的資料部分。而除資料外,fsync還會同步更新檔案的屬性。

3.14 函式fcntl

#include <fcntl.h>
    int fcntl(int fd, int cmd,... /* int arg */);
    返回值:成功,則依賴於cmd;出錯返回-1

第三個引數則是指向一個結構的指標。 5種功能:

1,複製一個已有的描述符:(cmd=F_DUPFD或F_DUPFD_CLOEXEC)
2,獲取/設定檔案描述符標誌:(cmd=F_GETFD或F_SETFD)
3,獲取/設定檔案狀態的標誌:(cmd=F_GETFL或F_SETFL)
4,獲取/設定非同步I/O所有權:    (cmd=F_GETOWN或F_SET).
5,獲取/設定記錄鎖:     (cmd=F_GETLK、F_SETLK或F_SETLKW)

cmd的前8種,與程序表項中各檔案描述符相關聯的 檔案描述符標誌 以及每個檔案表項中的 檔案狀態標誌。

F_DUPFD         複製檔案描述符fd。新檔案描述符作為函式值返回。
F_DUPFD_CLOEXEC 複製檔案描述符,設定與新描述符關聯的F_CLOEXEC檔案描述符標誌的值,返回新檔案描述。
F_GETFD 對應於fd的檔案描述符標誌作為函式值返回。當前只定義了一個檔案描述符標誌F_CLOEXEC
F_SETFD 對於fd設定檔案描述符標誌。新標誌按第3個引數設定。
F_GETFL 對應於fd的檔案狀態標誌作為函式值返回。
F_SETFL     將檔案狀態標誌設定為第3個引數的值。
F_GETOWN    獲取但前接收SIGIO和SIGURG訊號的程序ID或程序組ID。
F_SET       設定接收SIGIO和SIGURG訊號的程序ID或程序組ID,+arg指定一個程序ID,-arg表示等於arg絕對值的一個程序組ID。
F_GETLK 判斷由flockptr所描述的鎖是否會被另外一把鎖所排斥(阻塞),如果存在一把鎖,他阻止建立由flockptr所描述的鎖,則把該現存的鎖的資訊寫到flockptr指向的結構中。如果不存在,則除了將l_type設定為F_UNLCK之外,flockptr所指向結構中的其他資訊保持不變。
F_SETLK 設定由flockptr所描述的鎖。如果試圖建立一把讀鎖(l_type設為F_RDLCK)或寫鎖,而按上述相容性規則不能允許,則fcntl立即出錯返回,此時errno設定為EACCES或EAGAIN。
F_SETLKW    這是F_SETLK的阻塞版本(W表示wait)。如果因為當前在所請求區間的某個部分另一個程序已經有一把鎖,應而按相容性規則由flockptr所請求的鎖不能被建立,則是呼叫程序休眠。如果請求建立的鎖已經可用,或者休眠由訊號中斷,則該程序被喚醒。

在open函式中說明了檔案狀態標誌:

檔案狀態標誌 說明
O_RDONLY 只讀方式開啟
O_WRONLY 只寫方式開啟
O_RDWR 讀寫開啟
O_EXEC 只執行開啟
O_SEARCH 只搜尋開啟(應用於目錄)
O_APPEND 每次寫時追加到末尾
O_NONBLOCK 非阻塞模式
O_DSYNC 等待寫完成(僅資料)
O_RSYNC 同步讀和寫
O_SYNC 等待寫完成(資料和屬性)
O_ASYNC 非同步I/O(僅FreeBSD和Mac OS X)

遺憾的是,5個訪問方式標誌(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC、O_SEARCH)並不各佔1位(由於歷史原因,前3個標誌的值分別是0,1,2.這5個值互斥,一個檔案訪問方式只能取其一)。因此首先必須用遮蔽字O_ACCMODE取得訪問方式,然後將結果與這5個值分別比較。

int fcntl(int fd, int cmd,… /* struct flock flockptr /); 第三個引數flockptr指向flock結構的指標:

struct flock{
short   l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */
off_t       l_start;    /* offset in bytes, relative to l_whence */
short   l_whence; /* SEEK_SET, SEEK_CUR, or SEEEK_END */
off_t   l_len;  /* length, in bytes; 0 means lock to EOF */
pid_t   l_pid;  /* returned with F_GETLK */
};

3.15 函式ioctl

ioctl函式是裝置驅動程式中對裝置的I/O通道進行管理的函式。所謂對I/O通道進行管理,就是對裝置的一些特性進行控制,例如串列埠的傳輸波特率、馬達的轉速等等。

#include <unistd.h> 
#include <sys/ioctl.h>
int ioctl(int fd, ind cmd, …); 
    return: other value, error:-1.

ioctl函式是檔案結構中的一個屬性分量,就是說如果你的驅動程式提供了對ioctl的支援,使用者就可以在使用者程式中使用ioctl函式來控制裝置的I/O通道。

ioctl的必要性: 如果不用ioctl的話,也可以實現對裝置I/O通道的控制,但那是蠻擰了。例如,我們可以在驅動程式中實現write的時候檢查一下是否有特殊約定的資料流通過, 如果有的話,那麼後面就跟著控制命令(一般在socket程式設計中常常這樣做)。但是如果這樣做的話,會導致程式碼分工不明,程式結構混亂,程式設計師自己也會頭昏眼花的。

所以,我們就使用ioctl來實現控制的功能。要記住,使用者程式所作的只是通過命令碼(cmd)告訴驅動程式它想做什麼,至於怎麼解釋這些命令和怎麼實現這些命令, 這都是驅動程式要做的事情。

“幻數”是一個字母,資料長度也是8,用一個特定的字母來標明裝置型別,這和用一個數字是一樣的,只是更加利於記憶和理解。

cmd引數如何得出 這裡確實要說一說,cmd引數在使用者程式端由一些巨集根據裝置型別、序列號、傳送方向、資料尺寸等生成,這個整數通過系統呼叫傳遞到核心中的驅動程式,再由驅動程式使用解碼巨集從這個整數中得到裝置的型別、序列號、傳送方向、資料尺寸等資訊,然後通過switch{case}結構進行相應的操作。

小結 : ioctl其實沒有什麼很難的東西需要理解,關鍵是理解cmd命令碼是怎麼在使用者程式裡生成並在驅動程式裡解析的,程式設計師最主要的工作量在switch{case}結構中,因為對裝置的I/O控制都是通過這一部分的程式碼實現的。

3.16 /dev/fd

某些系統提供路徑名/dev/stdin/dev/stdout/dev/stderr,這些等效於/dev/fd/0/dev/fd/1/dev/fd2.

/dev/fd檔案主要由shell使用,它允許使用路徑名作為呼叫引數的程式,能用處理其他路徑名的相同的方式處理標準輸入輸出。