[單刷 APUE 系列] 第十四章——高級 I/O

非阻塞I/O

在最前面,咱們講過IO分紅帶緩衝的IO和不帶緩衝的IO,可是實際上,這個區別並非很大,由於緩衝區並無影響到實際的讀寫。咱們知道,系統調用實際上分紅兩種,高速的系統調用和低速的系統調用,換句話說,低速的調用會致使系統永久性阻塞,可是須要注意的是,並非磁盤IO都是低速調用。好比open、read、write函數,若是這些操做不能完成就會馬上出錯返回,並不會致使系統阻塞。在前面的時候咱們也學到過,若是在open的時刻,指定O_NONBLOCK,或者在一個已打開的文件描述符上調用fcntl函數,附加上O_NONBLOCK參數。實際上雖然指定了參數,可是在某些狀況下頗有可能丟失信息。在大量傳輸信息的時候容易出現系統調用大量失敗的狀況。javascript

記錄鎖

在不少狀況下,咱們須要面對多方一塊兒操做文件的狀況,這就是一個典型的資源競爭衝突,爲了保證文件的正確讀寫,Unix系統提供了文件記錄鎖的機制,也就是上文中提到過的文件記錄鎖。爲了提供這個功能,各個系統都自行實現了API,其中,POSIX1.x標準規定的是fcntl方法,而BSD系列則是規定flock方法,SystemV在fcntl方法的基礎上構建了lockf函數java

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_GETLKF_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_SETSEEK_CUR、或SEEK_END。而且上面提到的類型只有兩種:共享讀鎖和獨佔寫鎖,實際上就是讀寫鎖。編程

  • F_GETLK參數判斷flockptr參數所描述的鎖是否會被另外一把鎖排斥
  • F_SETLK參數設置由flockptr所描述的鎖
  • F_SETLKW這是F_SETLK的阻塞版本

很容易想到,在開發中確定是先用F_GETLK參數測試是否能創建一把鎖,然後使用F_SETLK或者F_SETLKW創建鎖,可是這二者並非原子操做,前面已經講過,非原子操做很容易致使操做衝突。
在設置釋放鎖的時候,內核是根據字節數維持鎖的範圍的,也就是說,實際上內核只是維護了一個flock結構體的鏈表,而後每次的鎖更改都會致使鏈表被遍歷而且合併。
對於記錄鎖的自動繼承和釋放有3條規則:數組

  1. 鎖和進程、文件相關聯,換言之,一個進程結束的時候,全部的鎖所有釋放,這其實是exit函數作清理的,第二就是文件描述符關閉的時候,該文件全部的鎖都會關閉
  2. fork產生的子進程不繼承父進程的鎖。由於鎖是用於限制多個進程讀寫同一個文件的,若是fork能繼承鎖,那就起不到約束做用了
  3. 執行exec後,新程序繼承原執行程序的鎖,可是close_on_exec則會不同。

其實鎖對數據庫這種大量讀寫IO的程序纔是最有用的,因此基本上鎖就能夠直接考慮數據庫的環境,若是數據庫的客戶端庫使用的是同一套鎖機制,那就能保證文件的共享訪問,可是建議性鎖沒法保證其餘有權限存取數據庫文件的進程讀寫此文件。而強制性鎖則會讓進程檢查每個open、read和write函數,驗證調用進程是否違背了正在訪問文件的鎖,這就是強制性鎖和建議性鎖的區別。網絡

IO多路轉接

前面談到過,對於內核來講,IO只有兩種方式:阻塞和非阻塞,阻塞IO會致使CPU等待IO從而浪費等待時間,因此係統提供了非阻塞IO,可是非阻塞IO帶來的問題就是完整IO沒有完成,爲了獲取完整的數據,應用程序須要重複調用IO操做來確認是否完成,也就是輪詢。
當從一個文件描述符讀,而後又寫到另外一個描述符時,一般會寫出如下代碼數據結構

while ((n = read(STDIN_FILENO, buf, BUFSIZE)) > 0)
    if (write(STDOUT_FILENO, buf, n) != n)
        err_sys("write error");複製代碼

這種循環獲取的形式就是輪詢,很是簡單,可是消耗了CPU資源,而且若是須要有更高的要求,好比必須從兩個文件描述符讀取。
典型的應用就是網絡守護進程,例如Nginx和Telnet,這裏直接拿原著中的Telnet講解,telnet因爲存在兩個輸入兩個輸出,因此不能使用阻塞式的IO函數,開發者的第一反應,應該是fork函數,使用兩個進程,每一個進程都負責一條讀寫通道,可是這就須要進程同步,而多線程編程也一樣是這樣的問題。
另外一個方法就是使用一個進程,可是使用非阻塞IO讀取數據。其基本思想很簡單,兩個描述符都讀取,可是一直處於循環,每次循環都查詢一次兩個文件描述符,若是沒有就馬上返回不阻塞,這種循環就是典型的輪詢,這是種很是常見的技術,實際上倒是很是浪費CPU資源的技術,因此目前,基本開發以及不能也不推薦了。
還有幾種技術就是異步IO,這種技術實質上就是相似通知,當描述符準備完畢後,進程通知內核,可是實際上目前原生API並不能作到移植,因此,目前大部分的開發,包括Node.js等在內的網絡服務,基本都是使用第三方或者本身實現線程池。不過,目前Linux系統已經有了名爲AIO的原生異步IO。
如今目前大部分的使用方式就是IO多路轉接,系統構造一張鏈表,裏面存儲全部的文件描述符,而後調用函數偵聽,知道其中一個已經準備完畢的時候返回。poll、pselect和select三個函數就是這樣執行的。多線程

select和pselect函數

這連個函數是POSIX規定的app

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結構體,也就是能夠指定秒和微妙單位。

  1. timeout == NULL,永遠等待
  2. timeout->tv_sec == 0 && timeout->tv_usec == 0,不等待
  3. timeout->tv_sec != 0 || timeout->tv_usec != 0,等待指定時間

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 POLLHUP
                    and 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帶來的就是輪詢,前面內容包括前面的章節整合一下,能夠概括出如下主流輪詢技術:

  1. read,最原始,性能最低的一種,重複檢查IO狀態來完成完整數據的讀取,也就是前面一小節的開頭代碼
  2. select,在read基礎上改進的方案,經過對文件描述符上的事件狀態判斷
  3. poll,使用鏈表做爲文件描述符的存儲方式,和select相似
  4. epoll,目前Linux下最高效的IO事件通知機制,進入輪詢時候若是沒有檢查到IO事件就會休眠,直到事件將其喚醒
  5. queue,和epoll相似,不過是FreeBSD下的

雖然輪詢知足了非阻塞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就是執行的操做。

struct sigevent {
        int                             sigev_notify;                           /* Notification type */
        int                             sigev_signo;                            /* Signal number */
        union sigval    sigev_value;                            /* Signal value */
        void                    (*sigev_notify_function)(union sigval);   /* Notification function */
        pthread_attr_t  *sigev_notify_attributes;       /* Notification attributes */
};複製代碼

sigevent結構體是歸屬於signal信號機制模型中的數據結構,其中sigev_notify字段是通知類型

  • SIGEV_NONE 不通知進程
  • SIGEV_SIGNAL 異步IO完成後,產生sigev_signo指定的信號,
  • SIGEV_THREAD 異步請求完成後,由sigev_notify_function指定的函數被調用
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);複製代碼

在異步IO以前須要先初始化AIO控制塊,當函數返回成功時候,異步IO請求就已經被放在了等待處理隊列中。這些返回值與實際IO擦作的結果沒有任何關係,若是想要強制全部等待中的異步操做不等待直接寫入存儲,則調用aio_fsync函數
固然,好像aio_fsync函數並非很是普遍,因此在使用的時候記得運行時檢查。
爲了獲取一個異步讀寫的完成狀態,能夠調用aio_error函數

int aio_error(const struct aiocb *aiocbp);複製代碼

返回以下:

  1. 返回值爲0,異步操做成功,使用aio_return函數得到返回值
  2. 返回值爲-1,對aio_error操做失敗
  3. 返回值爲EINPROGRESS,讀寫操做仍處於等待狀態
ssize_t aio_return(struct aiocb *aiocbp);複製代碼

記住在aio_error檢查已經成功以前,不要調用aio_return函數,並且須要小心每一個異步操做只能調用一次aio_return函數。
若是在其餘操做完成以後,異步操做還未完成,那可使用

int aio_suspend(const struct aiocb *const list[], int nent, const struct timespec *timeout);複製代碼

aio_suspend函數會阻塞當前進程直到操做完成,通常狀況下不多會使用。
若是咱們想要取消已經處於進行中的異步操做,可使用以下函數

int aio_cancel(int fildes, struct aiocb *aiocbp);複製代碼

這個函數會返回4個返回值:

  1. AIO_ALLDONE,全部操做已經完成
  2. AIO_CANCELED,全部操做已經取消
  3. AIO_NOtCANCELED,至少有一個請求沒有取消
  4. -1,函數自己失敗

除了上述函數之外,還有一個函數也被包含在異步請求函數中,可是實際上不多見到,因此這裏就很少作講解。

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參數影響映射存儲區的多種屬性,以下就是可選值:

MAP_ANONYMOUS     Synonym for MAP_ANON.

     MAP_ANON          Map anonymous memory not associated with any specific file.  The offset argument is ignored.  Mac OS X specific: the file descriptor used
                       for creating MAP_ANON regions can be used to pass some Mach VM flags, and can be specified as -1 if no such flags are associated with the
                       region.  Mach VM flags are defined in <mach/vm_statistics.h> and the ones that currently apply to mmap are:

                       VM_FLAGS_PURGABLE   to create Mach purgable (i.e. volatile) memory

                       VM_MAKE_TAG(tag)    to associate an 8-bit tag with the region
                       <mach/vm_statistics.h> defines some preset tags (with a VM_MEMORY_ prefix).  Users are encouraged to use tags between 240 and 255.  Tags
                       are used by tools such as vmmap(1) to help identify specific memory regions.

                       VM_FLAGS_SUPERPAGE_SIZE_*     to use superpages for the allocation.  See <mach/vm_statistics.h> for supported architectures and sizes (or
                       use VM_FLAGS_SUPERPAGE_SIZE_ANY to have the kernel choose a size).  The specified size must be divisible by the superpage size (except for
                       VM_FLAGS_SUPERPAGE_SIZE_ANY), and if you use MAP_FIXED, the specified address must be properly aligned. If the system cannot satisfy the
                       request with superpages, the call will fail. Note that currently, superpages are always wired and not inherited by children of the process.

     MAP_FILE          Mapped from a regular file.  (This is the default mapping type, and need not be specified.)

     MAP_FIXED         Do not permit the system to select a different address than the one specified.  If the specified address cannot be used, mmap() will fail.
                       If MAP_FIXED is specified, addr must be a multiple of the pagesize.  If a MAP_FIXED request is successful, the mapping established by
                       mmap() replaces any previous mappings for the process' pages in the range from addr to addr + len.  Use of this option is discouraged.

     MAP_HASSEMAPHORE  Notify the kernel that the region may contain semaphores and that special handling may be necessary.

          MAP_PRIVATE       Modifications are private (copy-on-write).

     MAP_SHARED        Modifications are shared.

     MAP_NOCACHE       Pages in this mapping are not retained in the kernel's memory cache.  If the system runs low on memory, pages in MAP_NOCACHE mappings will
                       be among the first to be reclaimed.  This flag is intended for mappings that have little locality and provides a hint to the kernel that
                       pages in this mapping are unlikely to be needed again in the near future.複製代碼

這就不講解了,原著上已經講解的足夠清楚了。
調用mprotect能夠更改現有映射的權限

int mprotect(void *addr, size_t len, int prot);複製代碼

也就是一個修改映射區域權限的函數,當頁已經修改完畢,能夠調用msync函數沖洗到被映射的文件中。

int msync(void *addr, size_t len, int flags);複製代碼

基本就和fsync函數差很少,也很少說了,基本上都在Unix手冊上
當進程終止的以後,天然會自動解除存儲區的映射,或者能夠調用munmap函數解除

int munmap(void *addr, size_t len);複製代碼

munmap函數刪除了指定地址的映射,若是繼續對其進行讀寫會致使無效內存引用。而且這個函數不會沖洗緩衝區內容到文件,因此須要當心使用。

相關文章
相關標籤/搜索