日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

[单刷 APUE 系列] 第十四章——高级 I/O

發布時間:2023/12/4 编程问答 59 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [单刷 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條規則:

  • 鎖和進程、文件相關聯,換言之,一個進程結束的時候,所有的鎖全部釋放,這實際上是exit函數做清理的,第二就是文件描述符關閉的時候,該文件所有的鎖都會關閉
  • fork產生的子進程不繼承父進程的鎖。因為鎖是用于限制多個進程讀寫同一個文件的,如果fork能繼承鎖,那就起不到約束作用了
  • 執行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規定的

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

  • timeout == NULL,永遠等待
  • timeout->tv_sec == 0 && timeout->tv_usec == 0,不等待
  • 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 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帶來的就是輪詢,前面內容包括前面的章節整合一下,可以歸納出以下主流輪詢技術:

  • read,最原始,性能最低的一種,重復檢查IO狀態來完成完整數據的讀取,也就是前面一小節的開頭代碼
  • select,在read基礎上改進的方案,通過對文件描述符上的事件狀態判斷
  • poll,使用鏈表作為文件描述符的存儲方式,和select類似
  • epoll,目前Linux下最高效的IO事件通知機制,進入輪詢時候如果沒有檢查到IO事件就會休眠,直到事件將其喚醒
  • 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);復制代碼

    返回如下:

  • 返回值為0,異步操作成功,使用aio_return函數獲得返回值
  • 返回值為-1,對aio_error操作失敗
  • 返回值為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個返回值:

  • AIO_ALLDONE,所有操作已經完成
  • AIO_CANCELED,所有操作已經取消
  • AIO_NOtCANCELED,至少有一個請求沒有取消
  • -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 usedfor 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 theregion. 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) memoryVM_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. Tagsare 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 (oruse VM_FLAGS_SUPERPAGE_SIZE_ANY to have the kernel choose a size). The specified size must be divisible by the superpage size (except forVM_FLAGS_SUPERPAGE_SIZE_ANY), and if you use MAP_FIXED, the specified address must be properly aligned. If the system cannot satisfy therequest 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 bymmap() 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 willbe among the first to be reclaimed. This flag is intended for mappings that have little locality and provides a hint to the kernel thatpages 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函數刪除了指定地址的映射,如果繼續對其進行讀寫會導致無效內存引用。并且這個函數不會沖洗緩沖區內容到文件,所以需要小心使用。

    創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的[单刷 APUE 系列] 第十四章——高级 I/O的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。