[单刷 APUE 系列] 第十四章——高级 I/O
非阻塞I/O
在最前面,我們講過IO分成帶緩沖的IO和不帶緩沖的IO,但是實(shí)際上,這個(gè)區(qū)別并不是很大,因?yàn)榫彌_區(qū)并沒有影響到實(shí)際的讀寫。我們知道,系統(tǒng)調(diào)用實(shí)際上分成兩種,高速的系統(tǒng)調(diào)用和低速的系統(tǒng)調(diào)用,換句話說,低速的調(diào)用會(huì)導(dǎo)致系統(tǒng)永久性阻塞,但是需要注意的是,并不是磁盤IO都是低速調(diào)用。比如open、read、write函數(shù),如果這些操作不能完成就會(huì)立刻出錯(cuò)返回,并不會(huì)導(dǎo)致系統(tǒng)阻塞。在前面的時(shí)候我們也學(xué)到過,如果在open的時(shí)刻,指定O_NONBLOCK,或者在一個(gè)已打開的文件描述符上調(diào)用fcntl函數(shù),附加上O_NONBLOCK參數(shù)。實(shí)際上雖然指定了參數(shù),但是在某些情況下很有可能丟失信息。在大量傳輸信息的時(shí)候容易出現(xiàn)系統(tǒng)調(diào)用大量失敗的情況。
記錄鎖
在很多情況下,我們需要面對(duì)多方一起操作文件的情況,這就是一個(gè)典型的資源競爭沖突,為了保證文件的正確讀寫,Unix系統(tǒng)提供了文件記錄鎖的機(jī)制,也就是上文中提到過的文件記錄鎖。為了提供這個(gè)功能,各個(gè)系統(tǒng)都自行實(shí)現(xiàn)了API,其中,POSIX1.x標(biāo)準(zhǔn)規(guī)定的是fcntl方法,而BSD系列則是規(guī)定flock方法,SystemV在fcntl方法的基礎(chǔ)上構(gòu)建了lockf函數(shù)
fcntl函數(shù)
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)).復(fù)制代碼前面也介紹過這個(gè)函數(shù),不過這次會(huì)講解記錄鎖的內(nèi)容,對(duì)于記錄所來說,cmd參數(shù)是F_GETLK、F_SETLK或者FSETLKW,第三個(gè)參數(shù)是一個(gè)紙箱flock結(jié)構(gòu)體的指針
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 */ };復(fù)制代碼基本上也不用講解了,注釋早已說明一切。這個(gè)結(jié)構(gòu)體就是通過指定文件區(qū)域和鎖的類型等參數(shù)鎖定文件。不過需要注意的是,l_type實(shí)際上是取值SEEK_SET、SEEK_CUR、或SEEK_END。并且上面提到的類型只有兩種:共享讀鎖和獨(dú)占寫鎖,實(shí)際上就是讀寫鎖。
- F_GETLK參數(shù)判斷flockptr參數(shù)所描述的鎖是否會(huì)被另一把鎖排斥
- F_SETLK參數(shù)設(shè)置由flockptr所描述的鎖
- F_SETLKW這是F_SETLK的阻塞版本
很容易想到,在開發(fā)中肯定是先用F_GETLK參數(shù)測試是否能建立一把鎖,而后使用F_SETLK或者F_SETLKW建立鎖,但是這兩者并不是原子操作,前面已經(jīng)講過,非原子操作很容易導(dǎo)致操作沖突。
在設(shè)置釋放鎖的時(shí)候,內(nèi)核是根據(jù)字節(jié)數(shù)維持鎖的范圍的,也就是說,實(shí)際上內(nèi)核只是維護(hù)了一個(gè)flock結(jié)構(gòu)體的鏈表,然后每次的鎖更改都會(huì)導(dǎo)致鏈表被遍歷并且合并。
對(duì)于記錄鎖的自動(dòng)繼承和釋放有3條規(guī)則:
其實(shí)鎖對(duì)數(shù)據(jù)庫這種大量讀寫IO的程序才是最有用的,所以基本上鎖就可以直接考慮數(shù)據(jù)庫的環(huán)境,如果數(shù)據(jù)庫的客戶端庫使用的是同一套鎖機(jī)制,那就能保證文件的共享訪問,但是建議性鎖無法保證其他有權(quán)限存取數(shù)據(jù)庫文件的進(jìn)程讀寫此文件。而強(qiáng)制性鎖則會(huì)讓進(jìn)程檢查每一個(gè)open、read和write函數(shù),驗(yàn)證調(diào)用進(jìn)程是否違背了正在訪問文件的鎖,這就是強(qiáng)制性鎖和建議性鎖的區(qū)別。
IO多路轉(zhuǎn)接
前面談到過,對(duì)于內(nèi)核來說,IO只有兩種方式:阻塞和非阻塞,阻塞IO會(huì)導(dǎo)致CPU等待IO從而浪費(fèi)等待時(shí)間,所以系統(tǒng)提供了非阻塞IO,但是非阻塞IO帶來的問題就是完整IO沒有完成,為了獲取完整的數(shù)據(jù),應(yīng)用程序需要重復(fù)調(diào)用IO操作來確認(rèn)是否完成,也就是輪詢。
當(dāng)從一個(gè)文件描述符讀,然后又寫到另一個(gè)描述符時(shí),通常會(huì)寫出以下代碼
這種循環(huán)獲取的形式就是輪詢,非常簡單,但是消耗了CPU資源,并且如果需要有更高的要求,比如必須從兩個(gè)文件描述符讀取。
典型的應(yīng)用就是網(wǎng)絡(luò)守護(hù)進(jìn)程,例如Nginx和Telnet,這里直接拿原著中的Telnet講解,telnet由于存在兩個(gè)輸入兩個(gè)輸出,所以不能使用阻塞式的IO函數(shù),開發(fā)者的第一反應(yīng),應(yīng)該是fork函數(shù),使用兩個(gè)進(jìn)程,每個(gè)進(jìn)程都負(fù)責(zé)一條讀寫通道,但是這就需要進(jìn)程同步,而多線程編程也同樣是這樣的問題。
另一個(gè)方法就是使用一個(gè)進(jìn)程,但是使用非阻塞IO讀取數(shù)據(jù)。其基本思想很簡單,兩個(gè)描述符都讀取,但是一直處于循環(huán),每次循環(huán)都查詢一次兩個(gè)文件描述符,如果沒有就立刻返回不阻塞,這種循環(huán)就是典型的輪詢,這是種非常常見的技術(shù),實(shí)際上卻是非常浪費(fèi)CPU資源的技術(shù),所以目前,基本開發(fā)以及不能也不推薦了。
還有幾種技術(shù)就是異步IO,這種技術(shù)實(shí)質(zhì)上就是類似通知,當(dāng)描述符準(zhǔn)備完畢后,進(jìn)程通知內(nèi)核,但是實(shí)際上目前原生API并不能做到移植,所以,目前大部分的開發(fā),包括Node.js等在內(nèi)的網(wǎng)絡(luò)服務(wù),基本都是使用第三方或者自己實(shí)現(xiàn)線程池。不過,目前Linux系統(tǒng)已經(jīng)有了名為AIO的原生異步IO。
現(xiàn)在目前大部分的使用方式就是IO多路轉(zhuǎn)接,系統(tǒng)構(gòu)造一張鏈表,里面存儲(chǔ)所有的文件描述符,然后調(diào)用函數(shù)偵聽,知道其中一個(gè)已經(jīng)準(zhǔn)備完畢的時(shí)候返回。poll、pselect和select三個(gè)函數(shù)就是這樣執(zhí)行的。
select和pselect函數(shù)
這連個(gè)函數(shù)是POSIX規(guī)定的
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);復(fù)制代碼第一個(gè)參數(shù)nfds的意思就是“最大文件描述符編號(hào)值+1”,因?yàn)槲募枋龇际菑?開始的,從后面readfds、writefds、errorfds中找出最大描述符編號(hào)值并+1就是這個(gè)參數(shù)的值,中間三個(gè)參數(shù)是指向描述符集的指針,使用fd_set數(shù)據(jù)結(jié)構(gòu)表示,實(shí)際上有下列五個(gè)函數(shù)
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);復(fù)制代碼是不是發(fā)現(xiàn)比原著多了一個(gè)FD_COPY函數(shù),實(shí)際上就是復(fù)制用的,無關(guān)緊要。最后一個(gè)參數(shù)就是制定愿意等待的時(shí)間長度,使用timeval結(jié)構(gòu)體,也就是可以指定秒和微妙單位。
select實(shí)際上和描述符本身阻塞無關(guān),它只是簡化了我們監(jiān)聽一堆文件描述符的繁瑣操作,除了select以外,上面還有一個(gè)select的變體pselect,pselect和select很像,但是select得超時(shí)值用timeval結(jié)構(gòu)體定義,pselect使用timespec結(jié)構(gòu),pselect可使用可選信號(hào)屏蔽字,如果sigmask為null,則兩者一樣,但是sigmask指向屏蔽字的時(shí)候,將以原子操作形式安裝屏蔽字。
poll函數(shù)
除了select以外,大家應(yīng)該還見過poll函數(shù)
int poll(struct pollfd fds[], nfds_t nfds, int timeout);復(fù)制代碼看起來poll函數(shù)相對(duì)于select更加簡潔易懂,select函數(shù)對(duì)三種類型都指定了參數(shù)用于構(gòu)造描述符集,但是poll函數(shù)使用的則是pollfd結(jié)構(gòu)體數(shù)組,pollfd結(jié)構(gòu)體如下
struct pollfd {int fd; /* file descriptor */short events; /* events to look for */short revents; /* events returned */ };復(fù)制代碼nfds參數(shù)指定了fds數(shù)組的大小,從上面的注釋中應(yīng)該也看得出來結(jié)構(gòu)體究竟是怎么構(gòu)造的,events是我們關(guān)心fd的事件,而revents則是內(nèi)核設(shè)置,返回的時(shí)候用于說明每個(gè)描述符發(fā)生了哪些事件。
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.復(fù)制代碼上面是兩個(gè)參數(shù)可取的值,每個(gè)系統(tǒng)實(shí)現(xiàn)可能存在偏差,所以需要自行嘗試。
異步I/O
前面講過,非阻塞IO帶來的就是輪詢,前面內(nèi)容包括前面的章節(jié)整合一下,可以歸納出以下主流輪詢技術(shù):
雖然輪詢滿足了非阻塞IO獲取完整數(shù)據(jù)的需求,但是依舊是同步的,也需要花費(fèi)CPU用于便利文件描述符或者休眠等待事件發(fā)生。所以就有了異步IO,目前據(jù)筆者所知,只有Linux下有AIO技術(shù)算是真正原生提供的API。
但是,實(shí)際上,是有模擬方式的,信號(hào)機(jī)構(gòu)提供了異步形式通知事件發(fā)生的方法,使用一個(gè)信號(hào)通知進(jìn)程,但是,由于信號(hào)是有限的,如果使用一個(gè)信號(hào),則進(jìn)程不知道是哪個(gè)文件描述符發(fā)生的事件,如果用多個(gè)信號(hào),文件描述符的數(shù)量可能遠(yuǎn)遠(yuǎn)超出信號(hào)的數(shù)量。
實(shí)際上,最容易想到的辦法就是多線程。讓部分線程進(jìn)行阻塞IO或者非阻塞IO加輪詢技術(shù)來完成數(shù)據(jù)獲取,讓另一個(gè)線程進(jìn)行計(jì)算,而后通過線程間通信將IO得到的數(shù)據(jù)進(jìn)行傳遞,就能輕松實(shí)現(xiàn)異步IO。
SystemV異步IO
SystemV中異步IO是歸屬給STREAMS系統(tǒng)的,他只能用于STREAMS設(shè)備和管道,異步IO信號(hào)是SIGPOLL。實(shí)際上由于這種機(jī)制本身的限制,目前已經(jīng)找不到Unix環(huán)境會(huì)去采用它了,所以這里也不需要再講解了。
BSD異步IO
對(duì)于BSD系列的系統(tǒng)來說,異步IO信號(hào)是SIGIO和SIGURG信號(hào)的組合,SIGIO是通用異步IO的信號(hào),SIGURG則是通知網(wǎng)絡(luò)連接的數(shù)據(jù)已經(jīng)到達(dá)。
POSIX異步IO
POSIX標(biāo)準(zhǔn)對(duì)不同類型文件異步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 */ };復(fù)制代碼上面是蘋果系統(tǒng)下的AIO控制塊實(shí)現(xiàn),實(shí)際上和POSIX規(guī)定幾乎一樣,它是繼承于FreeBSD3.0的AIO實(shí)現(xiàn),
從上面可以看出,每個(gè)字段究竟的意義,aio_fildes就是文件描述符,讀寫操作從aio_offset指定的偏移量位置開始,對(duì)于讀操作,會(huì)將數(shù)據(jù)復(fù)制到aio_buf的緩沖區(qū)內(nèi),對(duì)于寫操作,會(huì)從這個(gè)緩沖區(qū)寫入磁盤,aio_nbytes字段指定了讀寫的字節(jié)數(shù)。
除了上面4個(gè)字段以外,aio_reqprio就是異步IO請(qǐng)求的順序,aio_sigevent就是IO事件完成后如何通知,而aio_lio_opcode就是執(zhí)行的操作。
sigevent結(jié)構(gòu)體是歸屬于signal信號(hào)機(jī)制模型中的數(shù)據(jù)結(jié)構(gòu),其中sigev_notify字段是通知類型
- SIGEV_NONE 不通知進(jìn)程
- SIGEV_SIGNAL 異步IO完成后,產(chǎn)生sigev_signo指定的信號(hào),
- SIGEV_THREAD 異步請(qǐng)求完成后,由sigev_notify_function指定的函數(shù)被調(diào)用
在異步IO之前需要先初始化AIO控制塊,當(dāng)函數(shù)返回成功時(shí)候,異步IO請(qǐng)求就已經(jīng)被放在了等待處理隊(duì)列中。這些返回值與實(shí)際IO擦做的結(jié)果沒有任何關(guān)系,如果想要強(qiáng)制所有等待中的異步操作不等待直接寫入存儲(chǔ),則調(diào)用aio_fsync函數(shù)
當(dāng)然,好像aio_fsync函數(shù)并不是非常廣泛,所以在使用的時(shí)候記得運(yùn)行時(shí)檢查。
為了獲取一個(gè)異步讀寫的完成狀態(tài),可以調(diào)用aio_error函數(shù)
返回如下:
記住在aio_error檢查已經(jīng)成功之前,不要調(diào)用aio_return函數(shù),而且需要當(dāng)心每個(gè)異步操作只能調(diào)用一次aio_return函數(shù)。
如果在其他操作完成之后,異步操作還未完成,那可以使用
aio_suspend函數(shù)會(huì)阻塞當(dāng)前進(jìn)程直到操作完成,一般情況下很少會(huì)使用。
如果我們想要取消已經(jīng)處于進(jìn)行中的異步操作,可以使用如下函數(shù)
這個(gè)函數(shù)會(huì)返回4個(gè)返回值:
除了上述函數(shù)以外,還有一個(gè)函數(shù)也被包含在異步請(qǐng)求函數(shù)中,但是實(shí)際上很少見到,所以這里就不多做講解。
readv和writev函數(shù)
ssize_t readv(int d, const struct iovec *iov, int iovcnt); ssize_t writev(int fildes, const struct iovec *iov, int iovcnt);復(fù)制代碼這兩個(gè)函數(shù)用于在一次讀寫中讀寫多個(gè)非連續(xù)的緩沖區(qū),也就是說可以將傳統(tǒng)的多個(gè)函數(shù)讀寫調(diào)用壓縮到一個(gè),這連個(gè)函數(shù)第二個(gè)參數(shù)就是一個(gè)指向iovec結(jié)構(gòu)體的指針,實(shí)際上是一個(gè)指向數(shù)組的指針
struct iovec {char *iov_base; /* Base address. */size_t iov_len; /* Length. */ };復(fù)制代碼第三個(gè)參數(shù)就是數(shù)組的長度。iov數(shù)組中的元素最大值就是IOV_MAX。
存儲(chǔ)映射IO
存儲(chǔ)映射IO能將一個(gè)磁盤文件映射到存儲(chǔ)空間中的一個(gè)緩沖區(qū)上,于是,當(dāng)從緩沖區(qū)中讀取數(shù)據(jù)的時(shí)候,就等同于讀取文件。Unix系統(tǒng)提供了此類函數(shù)
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);復(fù)制代碼addr指定映射存儲(chǔ)區(qū)的起始地址。通常為0,也就是系統(tǒng)自動(dòng)分配區(qū)域。fd參數(shù)指定被映射文件的文件描述符,也就代表必須先打開這個(gè)文件。prot參數(shù)指定了映射存儲(chǔ)區(qū)的保護(hù)要求如下:
|prot|說明|
|----|---|
|PROT_READ|存儲(chǔ)區(qū)可讀|
|PROT_WRITE|存儲(chǔ)區(qū)可寫|
|PROT_EXEC|存儲(chǔ)區(qū)可執(zhí)行|
|PROT_NONE|存儲(chǔ)區(qū)不可訪問|
當(dāng)然,這個(gè)參數(shù)的指定必然是基于文件描述符的打開方式的,很容易明白,因?yàn)榇鎯?chǔ)映射IO技術(shù)本質(zhì)上還是基于文件描述符的,所以不可能繞過文件描述符的限制讀寫。
flag參數(shù)影響映射存儲(chǔ)區(qū)的多種屬性,如下就是可選值:
這就不講解了,原著上已經(jīng)講解的足夠清楚了。
調(diào)用mprotect可以更改現(xiàn)有映射的權(quán)限
也就是一個(gè)修改映射區(qū)域權(quán)限的函數(shù),當(dāng)頁已經(jīng)修改完畢,可以調(diào)用msync函數(shù)沖洗到被映射的文件中。
int msync(void *addr, size_t len, int flags);復(fù)制代碼基本就和fsync函數(shù)差不多,也不多說了,基本上都在Unix手冊(cè)上
當(dāng)進(jìn)程終止的之后,自然會(huì)自動(dòng)解除存儲(chǔ)區(qū)的映射,或者可以調(diào)用munmap函數(shù)解除
munmap函數(shù)刪除了指定地址的映射,如果繼續(xù)對(duì)其進(jìn)行讀寫會(huì)導(dǎo)致無效內(nèi)存引用。并且這個(gè)函數(shù)不會(huì)沖洗緩沖區(qū)內(nèi)容到文件,所以需要小心使用。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的[单刷 APUE 系列] 第十四章——高级 I/O的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [深入JUnit] 测试运行的入口
- 下一篇: atitit。wondows 右键菜单的