《UNIX環境高級編程》第14章 高級IO

14.1 引言

本章涵蓋衆多概念和函數,將是後幾章的基礎。

14.2 非阻塞IO

10.5節中曾將系統調用分成兩類:“低速”系統調用和其他。低速系統調用是可能會使進程永遠阻塞的一類系統調用,包括:

  • 如果某些文件類型的數據並不存在,該操作可能會使調用者永遠阻塞;
  • 如果數據不能被相同的文件類型立即接受,寫操作可能會使調用者永遠阻塞;
  • 在某種條件發生之前打開某些文件類型可能會發生阻塞;
  • 對已經加上強制性記錄鎖的文件進行讀寫;
  • 某些ioctl操作;
  • 某些進程間通信函數。
    非阻塞IO使我們可以發出open、read和write這樣的IO操作,並使這些操作不會永遠阻塞。如果這種操作不能完成,則調用立即出錯返回,表示這些操作如果繼續執行將阻塞。
    對於一個給定的描述符,有兩種爲其制定非阻塞IO的方法。

    1. 如果調用open獲得描述符,則可指定O_NONBLOCK標誌。
    2. 對於已經打開的一個描述符,則可調用fcntl,由該函數打開O_NONBLOCK文件狀態標誌。

14.3 記錄鎖

當兩個人同時編輯一個文件時,其後果將如何呢?在大多數UNIX系統中,該文件最後狀態取決於寫該文件的最後一個進程。但是對於有些應用程序,如數據庫,進程有時需要確保它正在單獨寫一個文件。爲了向進程提供這種功能,商用UNIX系統提供了記錄鎖機制。
記錄鎖(record locking)的功能是:當一個進程正在讀或修改文件的某個部分時,使用記錄鎖可以阻止其他進程修改同一文件區。“記錄”這個詞是一種吳用,更適合的術語可能是“字節範圍鎖”(byte-range locking),因爲它鎖定的只是文件中的一個區域(也可能是整個文件)。
1.歷史
早起的UNIX並不支持對部分文件加鎖。
2.fcntl記錄鎖

#include <fcntl.h>
int fcntl(int fd,int cmd,.../*struct flock *flockptr*/);

低於記錄鎖,cmd是F_GETLK、F_SETLK、F_SETLKW.第3個參數是指向flock結構的指針。

struct flock {
short l_type;   //F_RDLCK/F_WRLCK,or F_UNLCK
short l_whence; //SEEK_SET,SEEK_CUR,SEEK_END
off_t l_start;  //offset in bytes,relative to l_whence
off_t l_len;    //length,in bytes;0 menas to EOF
pid_t l_pid;    //returned with F_GETLK
};

對flock結構說明如下:
- 所希望的鎖類型:F_RDLCK(共享讀鎖)、F_WRLCK(獨佔性寫鎖)或F_UNLCK(解鎖)。
- 要加鎖或解鎖區域的起始字節偏移量(l_start和l_whence)。
- 區域的字節長度(l_len).
- 進程的ID(l_pid)持有的鎖能阻塞當前進程。


總的來說記錄鎖實現的是對文件局部加鎖的功能。

14.4 IO多路轉接

如果從多個輸入讀取,就不能使用阻塞IO的實行處理輸入,因爲有可能被阻塞在其中一個輸入上,而不能得到另一個IO的數據。例如telnet應用:
telnet進程讀取用戶輸入,並通過網絡送給遠端主機的telnetd進程,telnetd守護進程將數據送給shell處理,並將shell返回的數據通過網絡傳給telnet進程,telnet進程再送到標準輸出。
這樣,telnet進程同時讀取標準輸入和來自遠端telnetd進程的網絡輸入。因此使用阻塞IO讀取其中一個都不恰當。
可以考慮使用多線程或多進程來分別處理兩個IO輸入,但這並不是最優的方案。
一種比較好的技術是使用IO多路轉接(IO multiplexing)。爲了使用這種技術,先構造一張我們感興趣的描述符的列表,然後調用一個函數,直到這些描述符中的一個已準備好進行IO時,該函數才返回。
poll、pselect和select這3個函數使我們能夠執行IO多路轉接。在這些函數返回時,進程會被告知哪些描述符已經準備好可以進程IO了。

14.4.1 函數select和pselect

在所有POSIX兼容平臺上,select函數使我們可以執行IO multiplexing ,傳遞給select的參數告訴內核:

  • 我們所關心的描述符;
  • 對於每個描述符我們所關心的條件(是否想從一個給定的描述符讀,是否想寫一個給定的描述符寫,是否關心一個給定描述符的異常條件);
  • 願意等待多長時間(可以用於等待、等待一個固定的時間或者根本不等待)。
    從select返回時,內核告訴我們:

  • 已準備好的描述符的總數量;

  • 對於讀、寫或異常這3個條件中的每一個,哪些描述符已經準備好。
    使用這種返回信息,就可以調用相應的IO函數(一般是read或write),並且確知該函數不會阻塞。
#include <sys/select.h>
int select(int maxfdpl,fd_set *restrict readfds,fd_set *restrict write,fd_set *restrict exceptfds,struct timeval *restrict tvptr );

參數tvptr,它指定願意等待的時間長度,單位爲秒和微秒。

  • tvptr ==NULL,永遠等待,若捕捉到一個信號則中斷此無限等待。
  • tvptr ->tv_sec==0&&tvptr ->tv_usec==0,根本不等待。測試所有指定的描述符並立即返回。這是輪詢系統找到多個描述符狀態而不阻塞select函數的方法。
  • tvptr ->tv_sec!=0&&tvptr ->tv_usec!=0,等待指定的秒數和微秒。當指定的描述符之一已準備好,或當指定的時間值已經超過時立即返回。
    中間3個參數readfds、writefds、exceptfds是指向描述符集的指針。這3個描述符集說明了我們關心的可讀、可寫或處於異常條件的描述符集合。每個描述符集存儲在一個fd_set數據類型中。
#include <sys/select.h>
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);        //將整個描述符集置0

第一個參數maxfdpl的意思是“最大文件描述符編號值加1”。

select函數有3個返回值:
1. 返回值-1表示出錯。例如,在指定的描述符一個都沒有準備好時捕捉到了一個信號。在此種情況下,一個描述符集都不修改。
2. 返回值0,表示沒有描述符準備好。若指定的描述符一個都沒有準備好,指定的時間就過完了,那麼就會發生這種情況。此時所有描述符集都會置0。
3. 一個正返回值說明了已經準備好的描述符數。該值是3個描述符集中已經準備好的描述符數之和,所有如果同一描述符已經準備好讀和寫,那麼在返回值中會對其計數兩次。在這種情況下,3個描述符集中仍舊打開的位對應於已經準備好的描述符。


POSIX也定義了一個select的變體,稱爲pselect。

14.4.2 函數poll

poll函數類似於select,但是程序員接口有所不同。poll函數可用於任何類似文件描述符。

#include <poll.h>
int poll(struct pollfd fdarray[].nfds_t nfds,int timeout);

與select函數不同,poll不是爲每個條件構造一個描述符集,而是構造一個pollfd結構的數組,每個數組元素指定一個描述符編號以及我們對該描述符感興趣的條件

struct pollfd{
int fd;
short events;
short revents;
};

這裏寫圖片描述
poll函數中:
events成員設置爲上面所示的一個或幾個,通過這些值告訴內核我們關心的是每個描述符的哪些事件。
revents成員由內核設置,用於說明每個描述符發生了哪些事件。

14.5 異步IO

使用上一節說明的select和poll可以實現異步形式的通知。關於描述符的狀態,系統並不主動告訴我們任何信息,我們需要進行查詢(調用select或poll)。如在第10章中所述,信號機構提供了一種以異步形式通知某種事件已經發生的方法。
在我們瞭解使用異步IO的不同方法之前,需要先討論一下成本。在用異步IO的時候,要通過選擇來靈活處理多個併發操作,這會使應用程序的設計複雜化。更簡單的做法可能是使用多線程,使用同步模型來編寫程序,並讓這些線程以異步的方法運行。

14.5.1 System V異步IO

在System V中,異步IO是STREAMS系統的一部分,它只對STREAMS設備和STREAMS管道起作用。System V的異步IO信號是SIGPOLL.
爲了對一個STREAMS設備啓動異步IO,需要調用ioctl,將它的第二個參數(request)設置成I_SETSIG.
處理調用ioctl指定產生SIGPOLL信號的條件以外,還應爲信號建立信號處理程序。對於SIGPOLL的默認動作是終止該進程,所以應當在調用ioctl之前建立信號處理程序。

14.5.2 BSD異步IO

在BSD派生的系統中,異步IO是信號SIGIO和SIGURG的組合。SIGIO是通用異步IO信號,SIGURG則只用來通知進程網絡連接上的帶外數據已經到達。

14.5.3 POSIX異步IO

POSIX異步IO接口對不同類型的文件進行異步IO提供了一套一致的方法。
這些異步IO接口使用AIO控制塊來描述IO操作。aiocb結構定義了AIO控制塊。該結構至少包含以下字段:

struct aiocb{
int         aio_fildes; //被打開用來讀或寫的文件描述符
off_t       aio_offset; //讀或寫的操作從aio_offset指定的偏移量開始
volatile    void *aio_buf;  //對於讀操作,數據複製到該緩衝區
size_t      aio_nbytes; //包含了要讀或寫的字節數。
int         aio_reqprio;    //異步IO請求提示順序(系統對此順序只有有限的控制力,並不一定按照此順序)
struct      sigevent aio_sigvent;   //此字段控制在IO事件完成後,如何通知應用程序。
int     aio_lio_opcode;     //僅用於基於列表的異步IO
};

aio_sigevent字段控制在IO事件完成後,如何通知應用程序,這個字段通過sigevent結構來描述。

struct sigevent{
int sigev_notify;
int sigev_signo;
unio sigval sigev_value;
void(*sigev_notify_function)(union sigval);
pthread_attr_t  *sigev_notify_attributrs;
};

sigev_notify 字段控制通知的類型。取值可能是以下3箇中的一個:
- SIGEV_NONE 異步IO請求完成後,不通知進程。
- SIGEV_SIGNAL 異步IO請求完成後,產生由sigev_signo字段指定的信號
- SIGEV_THREAD 當異步IO請求完成時,由sigev_notify_function字段指定的函數被調用sigev_value字段被傳入作爲它唯一參數。除非sigev_notify_attributes字段被設定爲pthread屬性結構的地址,且該結構指定了一個另外的線程屬性,否則該函數將在分離狀態下的一個單獨的線程中執行

在進行異步IO之前需要先初始化AIO控制塊,調用aio_read函數來進一步進行異步讀操作,或調用aio_write函數來進行異步寫操作:

#include <aio.h>
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);

當這些函數返回時,異步io請求便已經被操作系統放入等待處理的隊列中了。


要想強制所有等待中的異步操作不等待而寫入持久化的存儲中(直接flush,不緩衝),可以設立一個AIO控制塊,並調用aio_fsync函數。

#include <aio.h>
int aio_fsync(int op,struct aiocb *aiocb);

AIO控制塊中的aio_fildes字段指定了其異步寫操作被同步的文件。如果op參數設定爲O_DSYNC,那麼操作執行起來就像調用了fdatasync一樣,否砸,如果op參數設定爲O_SYNC那麼操作執行起來就會像調用了fsync一樣。


爲了獲知一個異步讀、寫或者同步操作的完成狀態,需要調用aio_error函數。

#include <aio.h>
int aio_error(const struct aiocb *aiocb);

返回值爲下面4種情況中的一種。
0 異步操作成功完成。需要調用aio_return函數獲取操作返回值。
-1 對aio_error調用失敗,這時候errno會告訴我們爲什麼。
EINPROGRESS 異步讀、寫或同步操作仍在等待。
其他情況 其他任何返回值是相關的異步操作失敗返回的錯誤碼。


爲了獲得一個異步讀、寫或者同步操作的完成狀態,需要調用aio_return函數。

#include <aio.h>
int aio_return(const struct aiocb *aiocb);

注意,直到異步操作完成之前,都需要小心不要調用aio_return函數。操作完成之前的結果是爲定義的。還需要小心對每個異步操作只調用一次aio_return,一旦調用了該函數,操作系統就可以釋放掉包含io操作返回值的記錄。


執行IO操作時,如果還有其他事物需要處理而不想被IO操作阻塞,就可以使用異步IO。然而,如果在完成了所有事物時,還有異步操作未完成時,可以調用aio_suspend函數來阻塞進程,直到操作完成。

#include <aio.h>
int aio_suspend(const struct aiocb *const list[],int nent,const struct timespec *timeout);
//const struct aiocb *const list[],第一個const表示指針指向的值不可變,第二個const指指針本身不可變;
  • list參數是一個指向AIO控制塊數組的指針,
  • nent參數表明了數組中的條目數。
  • timeout參數表明了阻塞時間限制。
    ai_suspend可能會返回三種情況中的一種:

  • 如果我們被一個信號中斷,它將會返回-1,並將errno設置爲EINTR.

  • 如果沒有任何IO操作完成的情況下,阻塞時間超過了函數中可選的timeout參數指定的時間限制,那麼返回-1,並將errno設置爲EAGAIN。(若不想設置任何時間限制的話,可以給timeout空指針)
  • 如果有任何IO操作完成,ai_suspend將返回0.
    如果所有的異步IO操作都已完成,那麼aio_suspend將在不阻塞的情況下直接返回。

當還有我們不想再完成的等待中的異步IO操作時,可以嘗試使用aio_cancel函數來取消它們:

#include <aio.h>
int aio_suspend(int fd,const struct aiocb *aiocb);

fd參數指定了那個未完成的異步IO操作的文件描述符。如果aiocb參數設置爲NULL,系統將會嘗試取消所有該文件上未完成的異步IO操作。之所以是“嘗試”,是因爲無法保證系統能夠取消正在進程中的任何操作。
aio_calcel函數可能會返回一下4個值中的一個:

  • AIO_ALLDOWN 所有操作在嘗試取消它們之前已經完成。
  • AIO_CANVELED 所有要求的操作已被取消。
  • AIO_NOTCANCELED 至少有一個要求的操作沒有被取消。
  • -1 對aio_cancel的調用失敗,錯誤碼存儲在errno中。
    如果異步IO操作被成功取消,對相應的AIO控制塊調用aio_error函數將會返回錯誤ECANCELED。如果操作不能被取消,那麼相應的AIO控制塊不會因爲對aio_cancel的調用而被修改。

還有一個函數也被包含在異步IO接口中,儘管它既能以同步的方式來使用,又能以異步的方式來使用,這個函數就是lio_listio。該函數提交一系列由一個AIO控制塊列表描述的IO請求。

#include <aio.h>
int lio_listio(int mode,struct aiocb *restrict const list[restrict],int nent,struct sigevent *restrict sigev);
//方括號中的restrict是什麼意思???
  • mode參數決定了IO是否真的是異步的。如果改參數爲LIO_WAIT,函數將在所有由列表指定的IO操作完成後返回。這種情況下,sigev參數將被忽略。如果設定爲LIO_NOWAIT,函數將在IO請求入隊後立即返回。進程將在所有IO操作完成後,安裝sigev參數指定的,被異步地通知。如果不想被通知可以把sigev設置爲NULL。
  • list參數指向AIO控制塊列表,該列表指定了要運行的IO操作。列表中可以包含NULL,這些條目會被忽略。
  • nent參數指定來數組中的元素個數。

每個AIO控制塊中,aio_lio_opcode地段指定了該操作是一個讀操作(LIO_READ)、寫操作(LIO_WRITE),還是被忽略的空操作(LIO_NOP)。
引入POSIX異步操作IO接口的初衷是爲實時應用提供一種方法,避免在執行IO操作時阻塞進程。

14.6 函數readv和writev

readv和writev函數用於在一次函數調用中讀、寫多個非連續緩衝區。有時也將這兩個函數稱爲散佈讀(scatter read)和聚集寫(gather write)。

#include <sys/uio.h>
ssize_t readv(int fd,const struct iovec *iov,int iovcnt);
ssize_t writev(int fd,const struct iovec *iov,int iovcnt);

這兩個函數的第二個參數是指向iovec結構數組的一個指針;

struct iovec {
void *iov_base; //buf起始地址
size_t iov_len; //buf大小
};

iov數組中的元素由iovcnt指定,其最大值受限於IOV_MAX.

14.7 函數readn和writen

管道、FIFO以及某些設備(特別是終端和網絡)有下列兩種性質:

  1. 一次read操作所返回的數據可能少於所要求的數據,即使還沒達到文件尾端也可能是這樣。這不是一個錯誤,應當繼續讀該設備。
  2. 一次write操作的返回值也可能少於指定輸出的字節數。這可能是由某個因素造成的,例如,內核輸出緩衝區滿。這也不是錯誤,應當繼續寫餘下的數據。
    通常在讀、寫一個管道、網絡設備或終端時,需要考慮這些特性。readn和writen函數的功能分別是讀、寫指定的N字節數據,並處理返回值可能小於要求值的情況。這兩個函數只是按需多次調用read和write直至讀、寫了N字節數據。
#include <sys/uio.h>
ssize_t readn(int fd,void *buf,size_t nbytes);
ssize_t writen(int fd,void *buf,size_t nbytes);

14.8 存儲映射IO

存儲映射IO(memory-mapped IO)能將一個磁盤文件映射到存儲空間中的一個緩衝區上,於是,當從緩衝區中取數據時,就相當於讀文件中的相應字節。與此類似,將數據存入緩衝區時,相應字節就自動寫入文件。這樣就可以在不使用read和write的情況下執行IO。

爲了使用這種功能,應首先告訴內核將一個給定的文件映射到一個存儲區域中。這是由mmap函數實現的。

#include <sys/mman.h>
void *mmap(void *addr,size_t len,int port,int flag,int fd,off_t off);
  • addr參數用於指定映射存儲區的起始地址。通常設置爲0,這表示由系統選擇該映射區的起始地址。此函數的返回值是映射區的起始地址。
  • len參數是映射的字節數。
  • port 參數指定了映射存儲區的保護要求。可以是可讀(PORT_READ)、可寫(PORT_WRITE)、可執行(PORT_EXEC)、不可訪問(PORT_NONE).
  • flag參數影響映射存儲區的多種屬性。
  • fd參數是指定要被映射的文件描述符。在文件映射到地址空間之前,必須先打開該文件。
  • off是要映射字節在文件中的起始偏移量。
  • -

子進程通過fork繼承存儲映射區(因爲子進程複製父進程地址空間,而存儲區映射是該地址空間的一部分)。新程序則不能通過exec繼承存儲映射區。


mprotect函數可以更改一個現有的映射權限:

#include <sys/mman.h>
void mprotect(void *addr,size_t len,int prot);

prot的合法值與mmap中prot參數一樣。注意,地址參數addr的值必須是系統頁長的整數倍。


如果共享映射中的頁已經修改,那麼可以調用msync將該頁沖洗到被映射的文件中。msync函數類似於fsync,但用於存儲映射區。

#include <sys/mman.h>
void msync(void *addr,size_t len,int flags);

當進程終止時,會自動解除映射區的映射,或者直接調用munmap函數也可以解除映射區。關閉映射存儲區使用的文件描述符並不解除映射區。

#include <sys/mman.h>
int munmap(void *addr,size_t len);

munmap並不影響被映射的對象,也就是說,調用munmap並不會使映射區的內容寫到磁盤文件上。

14.9 小結

本章介紹了很多高級IO功能。

  • 非阻塞IO;
  • 記錄鎖;
  • IO多路轉接;
  • readv和writev函數。
  • 存儲映射IO(mmap)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章