[单刷 APUE 系列] 第十四章——高级 I/O
非阻塞I/O
在最前面,我們講過IO分成帶緩沖的IO和不帶緩沖的IO,但是實際上,這個區別并不是很大,因為緩沖區并沒有影響到實際的讀寫。我們知道,系統調用實際上分成兩種,高速的系統調用和低速的系統調用,換句話說,低速的調用會導致系統永久性阻塞,但是需要注意的是,并不是磁盤IO都是低速調用。比如open、read、write函數,如果這些操作不能完成就會立刻出錯返回,并不會導致系統阻塞。在前面的時候我們也學到過,如果在open的時刻,指定O_NONBLOCK,或者在一個已打開的文件描述符上調用fcntl函數,附加上O_NONBLOCK參數。實際上雖然指定了參數,但是在某些情況下很有可能丟失信息。在大量傳輸信息的時候容易出現系統調用大量失敗的情況。
記錄鎖
在很多情況下,我們需要面對多方一起操作文件的情況,這就是一個典型的資源競爭沖突,為了保證文件的正確讀寫,Unix系統提供了文件記錄鎖的機制,也就是上文中提到過的文件記錄鎖。為了提供這個功能,各個系統都自行實現了API,其中,POSIX1.x標準規定的是fcntl方法,而BSD系列則是規定flock方法,SystemV在fcntl方法的基礎上構建了lockf函數
fcntl函數
int fcntl(int fildes, int cmd, ...);The commands available for advisory record locking are as follows:F_GETLK Get the first lock that blocks the lock description pointed to by the third argument, arg, taken as a pointer to a struct flock (see above). The information retrieved overwrites the information passed to fcntl in the flock structure. If no lock is found that would prevent this lock from being created, the structure is left unchanged by this function call except for the lock type which is set to F_UNLCK.F_SETLK Set or clear a file segment lock according to the lock description pointed to by the third argument, arg, taken as a pointer to a struct flock (see above). F_SETLK is used to establish shared (or read) locks (F_RDLCK) or exclusive (or write) locks, (F_WRLCK), as well as remove either type of lock (F_UNLCK). If a shared or exclusive lock cannot be set, fcntl returns immediately with EAGAIN.F_SETLKW This command is the same as F_SETLK except that if a shared or exclusive lock is blocked by other locks, the process waits until the request can be satisfied. If a signal that is to be caught is received while fcntl is waiting for a region, the fcntl will be interrupted if the signal han-dler has not specified the SA_RESTART (see sigaction(2)).復制代碼前面也介紹過這個函數,不過這次會講解記錄鎖的內容,對于記錄所來說,cmd參數是F_GETLK、F_SETLK或者FSETLKW,第三個參數是一個紙箱flock結構體的指針
struct flock {off_t l_start; /* starting offset */off_t l_len; /* len = 0 means until end of file */pid_t l_pid; /* lock owner */short l_type; /* lock type: read/write, etc. */short l_whence; /* type of l_start */ };復制代碼基本上也不用講解了,注釋早已說明一切。這個結構體就是通過指定文件區域和鎖的類型等參數鎖定文件。不過需要注意的是,l_type實際上是取值SEEK_SET、SEEK_CUR、或SEEK_END。并且上面提到的類型只有兩種:共享讀鎖和獨占寫鎖,實際上就是讀寫鎖。
- F_GETLK參數判斷flockptr參數所描述的鎖是否會被另一把鎖排斥
- F_SETLK參數設置由flockptr所描述的鎖
- F_SETLKW這是F_SETLK的阻塞版本
很容易想到,在開發中肯定是先用F_GETLK參數測試是否能建立一把鎖,而后使用F_SETLK或者F_SETLKW建立鎖,但是這兩者并不是原子操作,前面已經講過,非原子操作很容易導致操作沖突。
在設置釋放鎖的時候,內核是根據字節數維持鎖的范圍的,也就是說,實際上內核只是維護了一個flock結構體的鏈表,然后每次的鎖更改都會導致鏈表被遍歷并且合并。
對于記錄鎖的自動繼承和釋放有3條規則:
其實鎖對數據庫這種大量讀寫IO的程序才是最有用的,所以基本上鎖就可以直接考慮數據庫的環境,如果數據庫的客戶端庫使用的是同一套鎖機制,那就能保證文件的共享訪問,但是建議性鎖無法保證其他有權限存取數據庫文件的進程讀寫此文件。而強制性鎖則會讓進程檢查每一個open、read和write函數,驗證調用進程是否違背了正在訪問文件的鎖,這就是強制性鎖和建議性鎖的區別。
IO多路轉接
前面談到過,對于內核來說,IO只有兩種方式:阻塞和非阻塞,阻塞IO會導致CPU等待IO從而浪費等待時間,所以系統提供了非阻塞IO,但是非阻塞IO帶來的問題就是完整IO沒有完成,為了獲取完整的數據,應用程序需要重復調用IO操作來確認是否完成,也就是輪詢。
當從一個文件描述符讀,然后又寫到另一個描述符時,通常會寫出以下代碼
這種循環獲取的形式就是輪詢,非常簡單,但是消耗了CPU資源,并且如果需要有更高的要求,比如必須從兩個文件描述符讀取。
典型的應用就是網絡守護進程,例如Nginx和Telnet,這里直接拿原著中的Telnet講解,telnet由于存在兩個輸入兩個輸出,所以不能使用阻塞式的IO函數,開發者的第一反應,應該是fork函數,使用兩個進程,每個進程都負責一條讀寫通道,但是這就需要進程同步,而多線程編程也同樣是這樣的問題。
另一個方法就是使用一個進程,但是使用非阻塞IO讀取數據。其基本思想很簡單,兩個描述符都讀取,但是一直處于循環,每次循環都查詢一次兩個文件描述符,如果沒有就立刻返回不阻塞,這種循環就是典型的輪詢,這是種非常常見的技術,實際上卻是非常浪費CPU資源的技術,所以目前,基本開發以及不能也不推薦了。
還有幾種技術就是異步IO,這種技術實質上就是類似通知,當描述符準備完畢后,進程通知內核,但是實際上目前原生API并不能做到移植,所以,目前大部分的開發,包括Node.js等在內的網絡服務,基本都是使用第三方或者自己實現線程池。不過,目前Linux系統已經有了名為AIO的原生異步IO。
現在目前大部分的使用方式就是IO多路轉接,系統構造一張鏈表,里面存儲所有的文件描述符,然后調用函數偵聽,知道其中一個已經準備完畢的時候返回。poll、pselect和select三個函數就是這樣執行的。
select和pselect函數
這連個函數是POSIX規定的
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout); int pselect(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, const struct timespec *restrict timeout, const sigset_t *restrict sigmask);復制代碼第一個參數nfds的意思就是“最大文件描述符編號值+1”,因為文件描述符都是從0開始的,從后面readfds、writefds、errorfds中找出最大描述符編號值并+1就是這個參數的值,中間三個參數是指向描述符集的指針,使用fd_set數據結構表示,實際上有下列五個函數
void FD_CLR(fd, fd_set *fdset); void FD_COPY(fd_set *fdset_orig, fd_set *fdset_copy); void FD_ISSET(fd, fd_set *fdset); void FD_SET(fd, fd_set *fdset); void FD_ZERO(fd_set *fdset);復制代碼是不是發現比原著多了一個FD_COPY函數,實際上就是復制用的,無關緊要。最后一個參數就是制定愿意等待的時間長度,使用timeval結構體,也就是可以指定秒和微妙單位。
select實際上和描述符本身阻塞無關,它只是簡化了我們監聽一堆文件描述符的繁瑣操作,除了select以外,上面還有一個select的變體pselect,pselect和select很像,但是select得超時值用timeval結構體定義,pselect使用timespec結構,pselect可使用可選信號屏蔽字,如果sigmask為null,則兩者一樣,但是sigmask指向屏蔽字的時候,將以原子操作形式安裝屏蔽字。
poll函數
除了select以外,大家應該還見過poll函數
int poll(struct pollfd fds[], nfds_t nfds, int timeout);復制代碼看起來poll函數相對于select更加簡潔易懂,select函數對三種類型都指定了參數用于構造描述符集,但是poll函數使用的則是pollfd結構體數組,pollfd結構體如下
struct pollfd {int fd; /* file descriptor */short events; /* events to look for */short revents; /* events returned */ };復制代碼nfds參數指定了fds數組的大小,從上面的注釋中應該也看得出來結構體究竟是怎么構造的,events是我們關心fd的事件,而revents則是內核設置,返回的時候用于說明每個描述符發生了哪些事件。
The event bitmasks in events and revents have the following bits:POLLERR An exceptional condition has occurred on the device or socket. This flag is output only, and ignored if present in the input events bitmask.POLLHUP The device or socket has been disconnected. This flag is output only, and ignored if present in the input events bitmask. Note that POLLHUPand POLLOUT are mutually exclusive and should never be present in the revents bitmask at the same time.POLLIN Data other than high priority data may be read without blocking. This is equivalent to ( POLLRDNORM | POLLRDBAND ).POLLNVAL The file descriptor is not open. This flag is output only, and ignored if present in the input events bitmask.POLLOUT Normal data may be written without blocking. This is equivalent to POLLWRNORM.POLLPRI High priority data may be read without blocking.POLLRDBAND Priority data may be read without blocking.POLLRDNORM Normal data may be read without blocking.POLLWRBAND Priority data may be written without blocking.POLLWRNORM Normal data may be written without blocking.復制代碼上面是兩個參數可取的值,每個系統實現可能存在偏差,所以需要自行嘗試。
異步I/O
前面講過,非阻塞IO帶來的就是輪詢,前面內容包括前面的章節整合一下,可以歸納出以下主流輪詢技術:
雖然輪詢滿足了非阻塞IO獲取完整數據的需求,但是依舊是同步的,也需要花費CPU用于便利文件描述符或者休眠等待事件發生。所以就有了異步IO,目前據筆者所知,只有Linux下有AIO技術算是真正原生提供的API。
但是,實際上,是有模擬方式的,信號機構提供了異步形式通知事件發生的方法,使用一個信號通知進程,但是,由于信號是有限的,如果使用一個信號,則進程不知道是哪個文件描述符發生的事件,如果用多個信號,文件描述符的數量可能遠遠超出信號的數量。
實際上,最容易想到的辦法就是多線程。讓部分線程進行阻塞IO或者非阻塞IO加輪詢技術來完成數據獲取,讓另一個線程進行計算,而后通過線程間通信將IO得到的數據進行傳遞,就能輕松實現異步IO。
SystemV異步IO
SystemV中異步IO是歸屬給STREAMS系統的,他只能用于STREAMS設備和管道,異步IO信號是SIGPOLL。實際上由于這種機制本身的限制,目前已經找不到Unix環境會去采用它了,所以這里也不需要再講解了。
BSD異步IO
對于BSD系列的系統來說,異步IO信號是SIGIO和SIGURG信號的組合,SIGIO是通用異步IO的信號,SIGURG則是通知網絡連接的數據已經到達。
POSIX異步IO
POSIX標準對不同類型文件異步IO提供了可移植的模型,異步IO使用AIO控制塊來描述IO操作。
struct aiocb {int aio_fildes; /* File descriptor */off_t aio_offset; /* File offset */volatile void *aio_buf; /* Location of buffer */size_t aio_nbytes; /* Length of transfer */int aio_reqprio; /* Request priority offset */struct sigevent aio_sigevent; /* Signal number and value */int aio_lio_opcode; /* Operation to be performed */ };復制代碼上面是蘋果系統下的AIO控制塊實現,實際上和POSIX規定幾乎一樣,它是繼承于FreeBSD3.0的AIO實現,
從上面可以看出,每個字段究竟的意義,aio_fildes就是文件描述符,讀寫操作從aio_offset指定的偏移量位置開始,對于讀操作,會將數據復制到aio_buf的緩沖區內,對于寫操作,會從這個緩沖區寫入磁盤,aio_nbytes字段指定了讀寫的字節數。
除了上面4個字段以外,aio_reqprio就是異步IO請求的順序,aio_sigevent就是IO事件完成后如何通知,而aio_lio_opcode就是執行的操作。
sigevent結構體是歸屬于signal信號機制模型中的數據結構,其中sigev_notify字段是通知類型
- SIGEV_NONE 不通知進程
- SIGEV_SIGNAL 異步IO完成后,產生sigev_signo指定的信號,
- SIGEV_THREAD 異步請求完成后,由sigev_notify_function指定的函數被調用
在異步IO之前需要先初始化AIO控制塊,當函數返回成功時候,異步IO請求就已經被放在了等待處理隊列中。這些返回值與實際IO擦做的結果沒有任何關系,如果想要強制所有等待中的異步操作不等待直接寫入存儲,則調用aio_fsync函數
當然,好像aio_fsync函數并不是非常廣泛,所以在使用的時候記得運行時檢查。
為了獲取一個異步讀寫的完成狀態,可以調用aio_error函數
返回如下:
記住在aio_error檢查已經成功之前,不要調用aio_return函數,而且需要當心每個異步操作只能調用一次aio_return函數。
如果在其他操作完成之后,異步操作還未完成,那可以使用
aio_suspend函數會阻塞當前進程直到操作完成,一般情況下很少會使用。
如果我們想要取消已經處于進行中的異步操作,可以使用如下函數
這個函數會返回4個返回值:
除了上述函數以外,還有一個函數也被包含在異步請求函數中,但是實際上很少見到,所以這里就不多做講解。
readv和writev函數
ssize_t readv(int d, const struct iovec *iov, int iovcnt); ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);復制代碼這兩個函數用于在一次讀寫中讀寫多個非連續的緩沖區,也就是說可以將傳統的多個函數讀寫調用壓縮到一個,這連個函數第二個參數就是一個指向iovec結構體的指針,實際上是一個指向數組的指針
struct iovec {char *iov_base; /* Base address. */size_t iov_len; /* Length. */ };復制代碼第三個參數就是數組的長度。iov數組中的元素最大值就是IOV_MAX。
存儲映射IO
存儲映射IO能將一個磁盤文件映射到存儲空間中的一個緩沖區上,于是,當從緩沖區中讀取數據的時候,就等同于讀取文件。Unix系統提供了此類函數
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);復制代碼addr指定映射存儲區的起始地址。通常為0,也就是系統自動分配區域。fd參數指定被映射文件的文件描述符,也就代表必須先打開這個文件。prot參數指定了映射存儲區的保護要求如下:
|prot|說明|
|----|---|
|PROT_READ|存儲區可讀|
|PROT_WRITE|存儲區可寫|
|PROT_EXEC|存儲區可執行|
|PROT_NONE|存儲區不可訪問|
當然,這個參數的指定必然是基于文件描述符的打開方式的,很容易明白,因為存儲映射IO技術本質上還是基于文件描述符的,所以不可能繞過文件描述符的限制讀寫。
flag參數影響映射存儲區的多種屬性,如下就是可選值:
這就不講解了,原著上已經講解的足夠清楚了。
調用mprotect可以更改現有映射的權限
也就是一個修改映射區域權限的函數,當頁已經修改完畢,可以調用msync函數沖洗到被映射的文件中。
int msync(void *addr, size_t len, int flags);復制代碼基本就和fsync函數差不多,也不多說了,基本上都在Unix手冊上
當進程終止的之后,自然會自動解除存儲區的映射,或者可以調用munmap函數解除
munmap函數刪除了指定地址的映射,如果繼續對其進行讀寫會導致無效內存引用。并且這個函數不會沖洗緩沖區內容到文件,所以需要小心使用。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的[单刷 APUE 系列] 第十四章——高级 I/O的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [深入JUnit] 测试运行的入口
- 下一篇: atitit。wondows 右键菜单的