UNIX環境高階程式設計(14-高階I/O)
本章主要介紹幾種高階I/O功能,主要有非阻塞I/O、記錄鎖、I/O多路轉接、非同步I/O、readv/writev函式和儲存對映I/O。
非阻塞I/O
某些系統呼叫可能會使程序永遠阻塞,一般稱其為低速系統呼叫。而使用非阻塞I/O,可以使open
、read
和write
這類I/O操作不會阻塞,如果不能完成這些操作時,會立即出錯返回。
有兩種方法將其指定為非阻塞I/O:
-
呼叫
open
時指定O_NONBLOCK
標誌。 -
通過
fcntl
函式開啟O_NONBLOCK
檔案狀態標誌。#include <fcntl.h> // Returns: depends on cmd if OK (see following), −1 on error int fcntl(int fd, int cmd, ... /* int arg */ );
記錄鎖
記錄鎖的主要功能是阻止多個程序同時修改檔案的某一檔案區。記錄鎖可以對整個檔案加鎖,也可以只針對檔案的一部分進行加鎖。
鎖的型別
主要有共享讀鎖和獨佔性寫鎖這兩種。
加讀/寫鎖時,檔案描述符必須是讀/寫開啟。
任意多個程序在給定的位元組上可以有一把共享的讀鎖,但是隻能有一個程序有一把獨佔寫鎖。如果已經有一把或多把讀鎖,則不能再加上寫鎖;如果已經有一把寫鎖,則不能再對它加任何讀鎖。
對於同一個程序而言,如果嘗試在同一個檔案區間再加一把鎖,無論之前是哪種型別的鎖,新的鎖都會覆蓋舊的鎖。
fcntl記錄鎖
記錄鎖也是通過fcntl
函式進行操作的,其cmd引數可選項為F_GETLK
,F_SETLK
F_SETLKW
。第三個引數是一個指向flock結構的指標flockptr
,用於描述鎖。
struct flock { short l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */ short l_whence; /* SEEK_SET, SEEK_CUR, or SEEK_END */ off_t l_start; /* offset in bytes, relative to l_whence */ off_t l_len; /* length, in bytes; 0 means lock to EOF */ pid_t l_pid; /* returned with F_GETLK */ };
l_pid
變數返回的是持有鎖的程序的pid。
注意:
-
鎖可以在檔案尾端或者越過尾端處開始,但是不能在起始位置之前開始。
-
將起始偏移量指向檔案起始處(如l_whence=SEEK_SET,l_start=0),且l_len設定為0,即可對整個檔案加鎖。
在設定或釋放檔案上的一把鎖時,系統按要求組合或分裂相鄰區。
一塊大的加鎖區域,解鎖其中的一部分,系統會自動將剩餘加鎖區域分裂為兩個加鎖區域,並各自維護一把鎖;如果對兩塊加鎖區域的中間未加鎖部分加鎖,則3個相鄰區域會合併成一個加鎖區域。如上圖14.4所示,100-199間解鎖150,則分成兩塊區域;之後重新加鎖150,則又會變為上半部分的狀態。
加鎖和解鎖
上面提到的3個命令對應於3種加解鎖方式,具體如下:
- F_GETLK:判斷是否會被其他鎖阻塞。如果flockptr描述的鎖被阻塞,則現有鎖的資訊會重寫flockptr指向的內容;如果沒有被阻塞,則將
l_type
設定為F_UNLCK
,其餘flockptr指向的資訊不變。 - F_SETLK:設定flockptr所描述的鎖。如果嘗試獲得讀鎖/寫鎖,但是系統無法給這把鎖,那麼會立即出錯返回,並將
errno
設定為EACCES
或EAGAIN
。如果將型別設定為F_UNLCK
,那麼此命令會清除flockptr指定的鎖。 - F_SETLKW:F_SETLK的阻塞版本。不能獲取鎖的時候,程序會被休眠,直到鎖可用或者被訊號喚醒。
繼承與釋放
-
鎖與程序和檔案兩者相關聯。即(a)當一個程序終止時,它建立的鎖全部釋放;(b)關閉一個描述符時,引用的檔案上的該程序的所有鎖都會釋放(無論該檔案是否還有其他的描述符)。
如圖14.8所示,當父程序關閉fd1、2或3中任意一個時,與之關聯的鎖都會釋放。系統會逐個檢查lockf連結串列中的各項,釋放呼叫程序持有的鎖。
-
由fork產生的子程序不繼承父程序所設定的鎖。
-
在執行exec後,新程式可以繼承原程式的鎖。
I/O多路轉接
對於需要同時對多個檔案進行操作的場景,比如從兩個描述符中讀取資料並全部存入另一個檔案中,無法通過阻塞讀(read)來讀取這兩個描述符,因為當一個描述符被讀操作阻塞時,另一個描述符可能有資料可以讀取。
通過I/O多路轉接技術,可以構建一張描述符表,呼叫一個函式,直到列表中的一個描述符準備好後該函式返回。omv-confdbadm populate
select
#include <sys/select.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int select(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
struct timeval *restrict tvptr);
-
maxfdp1
指定搜尋的最大描述符,該值應該是3個描述符集中的最大值+1。 -
readfds
,writefds
和exceptfds
是指向描述符集的指標,分別表示我們關心的可讀、可寫或處於異常狀態的描述符集合。// Returns: nonzero if fd is in set, 0 otherwise int FD_ISSET(int fd, fd_set *fdset); void FD_CLR(int fd, fd_set *fdset); void FD_SET(int fd, fd_set *fdset); void FD_ZERO(fd_set *fdset);
描述符集支援以上4中操作,宣告一個描述符集後,必須首先使用
FD_ZERO
將其置為0,之後再通過SET和CLR函式設定各個描述符位。 -
tvptr
為等待時間(該值在返回時可能被改變)。- 設定為NULL表示永遠等待。捕捉到訊號(函式返回-1且errno設定為EINTR)或有描述符準備好後才返回。
- 時間設定為0則表示不等待,測試完所有描述符後立即返回。
- 時間不為0,則等待對應的時間。超時(返回0)或有描述符準備好後即返回,另外也會被訊號打斷。
-
該函式的返回值>0則表示有描述符已經準備好了,此返回值是3個描述符集中準備好的描述符之和,因此,如果描述符集中有相同的描述符,則該描述符會被多次計數。描述符集中仍舊開啟的位是準備好的描述符,可以通過
FD_ISSET
來測試。
當3個描述符集都設定為NULL時,select就變成了一個延時函式。
另外還有一個變體函式pselect
。
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int pselect(int maxfdp1, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
const struct timespec *restrict tsptr,
const sigset_t *restrict sigmask);
與select
函式主要有以下不同:
- 等待時間使用的資料結構不同。
- 超時時間不會被改變。
- 多了一個訊號遮蔽字
sigmask
。當不為NULL時,呼叫pselect
函式會原子地安裝該訊號遮蔽字,在返回時復原。
poll
#include <poll.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor to check, or <0 to ignore */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};
使用pollfd結構的陣列代替了select
函式中的3個描述符集。nfds
即為陣列中的元素個數。其中,events
的可選值見圖14.17,可以選擇多個;返回時,revents
說明了描述符發生的事件。
timeout
指定等待時間,單位是毫秒。
非同步I/O
本節主要討論POSIX中的非同步I/O介面。
AIO控制塊
非同步介面使用AIO控制塊來描述I/O操作,其主要結構如下:
struct aiocb {
int aio_fildes; /* file descriptor */
off_t aio_offset; /* file offset for I/O */
volatile void *aio_buf; /* buffer for I/O */
size_t aio_nbytes; /* number of bytes to transfer */
int aio_reqprio; /* priority */
struct sigevent aio_sigevent; /* signal information */
int aio_lio_opcode; /* operation for list I/O */
};
其中,aio_buf
作為讀寫操作的緩衝區,在操作完成前必須始終有效且不能複用。
如果檔案開啟方式為追加模式O_APPEND,向其寫入資料時,偏移量aio_offset
會被忽略。
aio_lio_opcode
指定該操作是讀(LIO_READ)、寫(LIO_WRITE)還是空(LIO_NOP)操作,該引數僅在基於列表的非同步I/O操作lio_listio時有效。
aio_sigevent
結構如下,它表示在I/O事件完成後,如何通知程式:
struct sigevent {
int sigev_notify; /* notify type */
int sigev_signo; /* signal number */
union sigval sigev_value; /* notify argument */
void (*sigev_notify_function)(union sigval); /* notify function */
pthread_attr_t *sigev_notify_attributes; /* notify attrs */
};
sigev_notify
控制通知型別,有如下3中取值:
- SIGEV_NONE: 不通知程序。
- SIGEV_SIGNAL:產生
sigev_signo
指定的訊號。如果程式捕獲該訊號,並設定SA_SIGINFO標誌(通過sigaction設定),那麼訊號處理程式得到的siginfo結構中的si_value被設定為sigev_value
。 - SIGEV_THREADS:呼叫
sigev_notify_function
指定的函式,且傳入的引數為sigev_value
。預設情況下該函式通過一個單獨的分離執行緒執行,除非sigev_notify_attributes
設定了執行緒引數。
介面函式
#include <aio.h>
// Both return: 0 if OK, −1 on error
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);
讀寫函式返回時,非同步I/O請求被放入等待處理佇列,返回值與讀寫操作的結果無關。
// Returns: 0 if OK, −1 on error
int aio_fsync(int op, struct aiocb *aiocb);
如果希望等待中的非同步操作不等待而直接寫入,可以呼叫aio_fsync函式,同樣的,該函式也僅僅是傳送一個請求,而不會等待操作結束。
op
引數設定為O_DSYNC,則執行起來與fdatasync類似;如果設定為O_SYNC,則與fsync類似。
int aio_error(const struct aiocb *aiocb);
獲取非同步讀/寫或同步操作的完成狀態,返回值有以下4種情況:
- 0:非同步操作成功完成。
- -1:函數出錯,可以通過errno查看出錯資訊。
- EINPROGRESS:操作仍在等待。
- 其他:相關的非同步操作失敗返回的錯誤碼。
ssize_t aio_return(const struct aiocb *aiocb);
獲取非同步操作的返回值,如果上面的aio_error返回0時,可以呼叫該函式檢視非同步操作的返回值。函式返回-1表示出錯,會設定errno;其餘情況為非同步操作的結果。
注意:
非同步操作完成前不要呼叫該函式,並且對每個非同步操作僅呼叫一次該函式。因為呼叫該函式後,作業系統就可以釋放掉包含了I/O操作返回值的資訊。
// Returns: 0 if OK, −1 on error
int aio_suspend(const struct aiocb *const list[], int nent,
const struct timespec *timeout);
阻塞程序等待非同步操作完成。
list
引數是指向SIO控制塊陣列的指標,nent
為陣列的條目數。timeout
設定為NULL可以不設時間限制。
如果被訊號中斷,則返回-1且errno設定為EINTR;如果超時則返回-1且errno設定為EAGAIN。任何一個操作完成都會使該函式返回0。
int aio_cancel(int fd, struct aiocb *aiocb);
取消非同步操作。
fd
為未完成操作的檔案的檔案描述符。aiocb
為檔案上的某個指定的非同步操作,如果設定為NULL,則會取消檔案上所有未完成的非同步操作。該函式無法保證能夠取消正在程序中的操作。
返回值:
- AIO_ALLDONE:所有操作在取消前就已經完成。
- AIO_CANCELED:所要求的操作已被取消。
- AIO_NOTCANCELED:至少一個請求的操作沒有被取消。
- -1:呼叫失敗,錯誤碼在errno中。
對被取消的操作呼叫aio_error
會返回錯誤ECANCELED。
int lio_listio(int mode, struct aiocb *restrict const list[restrict],
int nent, struct sigevent *restrict sigev);
該函式提交一系列由一個AIO控制塊列表描述的I/O請求。
mode
引數決定該函式是否是非同步的。如果被設定為LIO_WAIT
,那麼函式將在列表中的所有操作完成後返回;如果設定為LIO_NOWAIT
,那麼函式將在I/O請求入隊後返回,並在所有操作結束後,按照sigev
的設定被非同步地通知(無需通知則設為NULL)。sigev
通知不同於AIO控制塊本身的通知,它是額外的,且只會在所有操作完成後才會傳送。
readv和writev
這兩個函式用於在一次函式呼叫中讀、寫多個非連續緩衝區,也稱之為散佈讀和聚集寫。
#include <sys/uio.h>
// Both return: number of bytes read or written, −1 on error
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /* starting address of buffer */
size_t iov_len; /* size of buffer */
};
writev
按照iov[0]、iov[1]直至iov[iovcnt-1]的順序輸出資料,且返回輸出的總位元組數。
readv
則將讀入的資料按照上面的順序依次存入各個緩衝區,返回讀到的總位元組數。如果遇到檔案尾端,則返回0。
儲存對映I/O
該技術將一個磁碟檔案對映到儲存空間的一個緩衝區上,從緩衝區讀寫資料就相當於向檔案讀寫資料。可以在不使用read/write函式的情況下執行I/O。
對映與解除
#include <sys/mman.h>
// Returns: starting address of mapped region if OK, MAP_FAILED on error
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off );
addr
指定對映儲存區的起始地址。設定為0則由系統自動分配。
prot
引數指定對映儲存區的保護要求,如下表所示:
prot | 說明 |
---|---|
PROT_READ | 對映區可讀 |
PROT_WRITE | 對映區可寫 |
PROT_EXEC | 對映區可執行 |
PROT_NONE | 對映區不可訪問 |
表中前三項可以任意組合(按位或),但是保護要求不能超過檔案本身的訪問許可權。
flag
引數指定了對映區的各類屬性:
-
MAP_FIXED:返回值必須等於
addr
。即要求核心必須將儲存區的起始地址設定為addr
,如果沒有此標誌且addr
非0,核心僅將addr
的值視為一種建議。 -
MAP_SHARED:表示儲存操作會修改對映檔案,即相當於呼叫write。
-
MAP_PRIVATE:表示儲存操作會建立改對映檔案的副本,所有的儲存操作不會修改真實檔案。
注意:
off
和addr
的值一般要求是虛擬系統儲存頁長度的倍數。
對於一些對映區不是頁長整數倍的情況,系統會分配更多的對映區以滿足此要求。如檔案長為12位元組,頁長512位元組,則系統會提供512位元組的對映區。可以修改後面500位元組的內容,但是不會作用到原檔案上。
// Returns: 0 if OK, −1 on error
int munmap(void *addr, size_t len);
程序終止或者呼叫munmap
都會解除對映區。但是關閉檔案描述符並不會解除對映,並且呼叫munmap
也不會使對映區的內容寫到磁碟檔案上。
其他
// Returns: 0 if OK, −1 on error
int mprotect(void *addr, size_t len, int prot);
該函式可以更改一個現有對映的許可權。
對於通過MAP_SHARED方式進行的對映,所作的修改不會立即寫回到檔案中。
// Returns: 0 if OK, −1 on error
int msync(void *addr, size_t len, int flags);
改函式將修改的頁沖洗到檔案中去。
如果將flags
引數指定為MS_ASYNC,則僅僅是請求一個寫入操作;如果指定為MS_SYNC,那麼在返回之前會等待寫操作完成。這兩個選項必選其一。
另外,還可以指定MS_INVALIDATE,來告訴作業系統丟棄與底層儲存器沒有同步的頁。