1. 程式人生 > 實用技巧 >UNIX環境高階程式設計(14-高階I/O)

UNIX環境高階程式設計(14-高階I/O)

本章主要介紹幾種高階I/O功能,主要有非阻塞I/O、記錄鎖、I/O多路轉接、非同步I/O、readv/writev函式和儲存對映I/O。

非阻塞I/O

某些系統呼叫可能會使程序永遠阻塞,一般稱其為低速系統呼叫。而使用非阻塞I/O,可以使openreadwrite這類I/O操作不會阻塞,如果不能完成這些操作時,會立即出錯返回。

有兩種方法將其指定為非阻塞I/O:

  • 呼叫open時指定O_NONBLOCK標誌。

  • 通過fcntl函式開啟O_NONBLOCK檔案狀態標誌。

    #include <fcntl.h>
    // Returns: depends on cmd if OK (see following), −1 on error
    int fcntl(int fd, int cmd, ... /* int arg */ );
    

記錄鎖

記錄鎖的主要功能是阻止多個程序同時修改檔案的某一檔案區。記錄鎖可以對整個檔案加鎖,也可以只針對檔案的一部分進行加鎖。

鎖的型別

主要有共享讀鎖獨佔性寫鎖這兩種。

加讀/寫鎖時,檔案描述符必須是讀/寫開啟。

任意多個程序在給定的位元組上可以有一把共享的讀鎖,但是隻能有一個程序有一把獨佔寫鎖。如果已經有一把或多把讀鎖,則不能再加上寫鎖;如果已經有一把寫鎖,則不能再對它加任何讀鎖。

對於同一個程序而言,如果嘗試在同一個檔案區間再加一把鎖,無論之前是哪種型別的鎖,新的鎖都會覆蓋舊的鎖。

fcntl記錄鎖

記錄鎖也是通過fcntl函式進行操作的,其cmd引數可選項為F_GETLKF_SETLK

F_SETLKW。第三個引數是一個指向flock結構的指標flockptr,用於描述鎖。

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

l_pid變數返回的是持有鎖的程序的pid。

注意:

  • 鎖可以在檔案尾端或者越過尾端處開始,但是不能在起始位置之前開始。

  • 將起始偏移量指向檔案起始處(如l_whence=SEEK_SET,l_start=0),且l_len設定為0,即可對整個檔案加鎖。

在設定或釋放檔案上的一把鎖時,系統按要求組合或分裂相鄰區。

一塊大的加鎖區域,解鎖其中的一部分,系統會自動將剩餘加鎖區域分裂為兩個加鎖區域,並各自維護一把鎖;如果對兩塊加鎖區域的中間未加鎖部分加鎖,則3個相鄰區域會合併成一個加鎖區域。如上圖14.4所示,100-199間解鎖150,則分成兩塊區域;之後重新加鎖150,則又會變為上半部分的狀態。

加鎖和解鎖

上面提到的3個命令對應於3種加解鎖方式,具體如下:

  • F_GETLK:判斷是否會被其他鎖阻塞。如果flockptr描述的鎖被阻塞,則現有鎖的資訊會重寫flockptr指向的內容;如果沒有被阻塞,則將l_type設定為F_UNLCK,其餘flockptr指向的資訊不變。
  • F_SETLK:設定flockptr所描述的鎖。如果嘗試獲得讀鎖/寫鎖,但是系統無法給這把鎖,那麼會立即出錯返回,並將errno設定為EACCESEAGAIN。如果將型別設定為F_UNLCK,那麼此命令會清除flockptr指定的鎖。
  • F_SETLKW:F_SETLK的阻塞版本。不能獲取鎖的時候,程序會被休眠,直到鎖可用或者被訊號喚醒。

繼承與釋放

  1. 鎖與程序和檔案兩者相關聯。即(a)當一個程序終止時,它建立的鎖全部釋放;(b)關閉一個描述符時,引用的檔案上的該程序的所有鎖都會釋放(無論該檔案是否還有其他的描述符)。

    如圖14.8所示,當父程序關閉fd1、2或3中任意一個時,與之關聯的鎖都會釋放。系統會逐個檢查lockf連結串列中的各項,釋放呼叫程序持有的鎖。

  2. 由fork產生的子程序不繼承父程序所設定的鎖。

  3. 在執行exec後,新程式可以繼承原程式的鎖。

I/O多路轉接

對於需要同時對多個檔案進行操作的場景,比如從兩個描述符中讀取資料並全部存入另一個檔案中,無法通過阻塞讀(read)來讀取這兩個描述符,因為當一個描述符被讀操作阻塞時,另一個描述符可能有資料可以讀取。

通過I/O多路轉接技術,可以構建一張描述符表,呼叫一個函式,直到列表中的一個描述符準備好後該函式返回。omv-confdbadm populate

select

#include <sys/select.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int select(int maxfdp1, fd_set *restrict readfds,
         fd_set *restrict writefds, fd_set *restrict exceptfds,
         struct timeval *restrict tvptr);
  • maxfdp1指定搜尋的最大描述符,該值應該是3個描述符集中的最大值+1

  • readfdswritefdsexceptfds是指向描述符集的指標,分別表示我們關心的可讀、可寫或處於異常狀態的描述符集合。

    // Returns: nonzero if fd is in set, 0 otherwise
    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);
    

    描述符集支援以上4中操作,宣告一個描述符集後,必須首先使用FD_ZERO將其置為0,之後再通過SET和CLR函式設定各個描述符位。

  • tvptr為等待時間(該值在返回時可能被改變)。

    • 設定為NULL表示永遠等待。捕捉到訊號(函式返回-1且errno設定為EINTR)或有描述符準備好後才返回。
    • 時間設定為0則表示不等待,測試完所有描述符後立即返回。
    • 時間不為0,則等待對應的時間。超時(返回0)或有描述符準備好後即返回,另外也會被訊號打斷。
  • 該函式的返回值>0則表示有描述符已經準備好了,此返回值是3個描述符集中準備好的描述符之和,因此,如果描述符集中有相同的描述符,則該描述符會被多次計數。描述符集中仍舊開啟的位是準備好的描述符,可以通過FD_ISSET來測試。

當3個描述符集都設定為NULL時,select就變成了一個延時函式。

另外還有一個變體函式pselect

// Returns: count of ready descriptors, 0 on timeout, −1 on error
int pselect(int maxfdp1, fd_set *restrict readfds,
          fd_set *restrict writefds, fd_set *restrict exceptfds,
          const struct timespec *restrict tsptr,
          const sigset_t *restrict sigmask);

select函式主要有以下不同:

  • 等待時間使用的資料結構不同。
  • 超時時間不會被改變。
  • 多了一個訊號遮蔽字sigmask。當不為NULL時,呼叫pselect函式會原子地安裝該訊號遮蔽字,在返回時復原。

poll

#include <poll.h>
// Returns: count of ready descriptors, 0 on timeout, −1 on error
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

struct pollfd {
  int   fd;      /* file descriptor to check, or <0 to ignore */
  short events;  /* events of interest on fd */
  short revents; /* events that occurred on fd */
};

使用pollfd結構的陣列代替了select函式中的3個描述符集。nfds即為陣列中的元素個數。其中,events的可選值見圖14.17,可以選擇多個;返回時,revents說明了描述符發生的事件。

timeout指定等待時間,單位是毫秒

非同步I/O

本節主要討論POSIX中的非同步I/O介面。

AIO控制塊

非同步介面使用AIO控制塊來描述I/O操作,其主要結構如下:

struct aiocb {
  int             aio_fildes;     /* file descriptor */
  off_t           aio_offset;     /* file offset for I/O */
  volatile void  *aio_buf;        /* buffer for I/O */
  size_t          aio_nbytes;     /* number of bytes to transfer */
  int             aio_reqprio;    /* priority */
  struct sigevent aio_sigevent;   /* signal information */
  int             aio_lio_opcode; /* operation for list I/O */
};

其中,aio_buf作為讀寫操作的緩衝區,在操作完成前必須始終有效且不能複用。

如果檔案開啟方式為追加模式O_APPEND,向其寫入資料時,偏移量aio_offset會被忽略。

aio_lio_opcode指定該操作是讀(LIO_READ)、寫(LIO_WRITE)還是空(LIO_NOP)操作,該引數僅在基於列表的非同步I/O操作lio_listio時有效。

aio_sigevent結構如下,它表示在I/O事件完成後,如何通知程式:

struct sigevent {
  int             sigev_notify;                /* notify type */
  int             sigev_signo;                 /* signal number */
  union sigval    sigev_value;                 /* notify argument */
  void (*sigev_notify_function)(union sigval); /* notify function */
  pthread_attr_t *sigev_notify_attributes;     /* notify attrs */
};

sigev_notify控制通知型別,有如下3中取值:

  • SIGEV_NONE: 不通知程序。
  • SIGEV_SIGNAL:產生sigev_signo指定的訊號。如果程式捕獲該訊號,並設定SA_SIGINFO標誌(通過sigaction設定),那麼訊號處理程式得到的siginfo結構中的si_value被設定為sigev_value
  • SIGEV_THREADS:呼叫sigev_notify_function指定的函式,且傳入的引數為sigev_value。預設情況下該函式通過一個單獨的分離執行緒執行,除非sigev_notify_attributes設定了執行緒引數。

介面函式

#include <aio.h>
// Both return: 0 if OK, −1 on error
int aio_read(struct aiocb *aiocb);
int aio_write(struct aiocb *aiocb);

讀寫函式返回時,非同步I/O請求被放入等待處理佇列,返回值與讀寫操作的結果無關。

// Returns: 0 if OK, −1 on error
int aio_fsync(int op, struct aiocb *aiocb);

如果希望等待中的非同步操作不等待而直接寫入,可以呼叫aio_fsync函式,同樣的,該函式也僅僅是傳送一個請求,而不會等待操作結束。

op引數設定為O_DSYNC,則執行起來與fdatasync類似;如果設定為O_SYNC,則與fsync類似。

int aio_error(const struct aiocb *aiocb);

獲取非同步讀/寫或同步操作的完成狀態,返回值有以下4種情況:

  • 0:非同步操作成功完成。
  • -1:函數出錯,可以通過errno查看出錯資訊。
  • EINPROGRESS:操作仍在等待。
  • 其他:相關的非同步操作失敗返回的錯誤碼。
ssize_t aio_return(const struct aiocb *aiocb);

獲取非同步操作的返回值,如果上面的aio_error返回0時,可以呼叫該函式檢視非同步操作的返回值。函式返回-1表示出錯,會設定errno;其餘情況為非同步操作的結果。

注意:

非同步操作完成前不要呼叫該函式,並且對每個非同步操作僅呼叫一次該函式。因為呼叫該函式後,作業系統就可以釋放掉包含了I/O操作返回值的資訊。

// Returns: 0 if OK, −1 on error
int aio_suspend(const struct aiocb *const list[], int nent,
              const struct timespec *timeout);

阻塞程序等待非同步操作完成。

list引數是指向SIO控制塊陣列的指標,nent為陣列的條目數。timeout設定為NULL可以不設時間限制。

如果被訊號中斷,則返回-1且errno設定為EINTR;如果超時則返回-1且errno設定為EAGAIN。任何一個操作完成都會使該函式返回0。

int aio_cancel(int fd, struct aiocb *aiocb);

取消非同步操作。

fd為未完成操作的檔案的檔案描述符。aiocb為檔案上的某個指定的非同步操作,如果設定為NULL,則會取消檔案上所有未完成的非同步操作。該函式無法保證能夠取消正在程序中的操作。

返回值:

  • AIO_ALLDONE:所有操作在取消前就已經完成。
  • AIO_CANCELED:所要求的操作已被取消。
  • AIO_NOTCANCELED:至少一個請求的操作沒有被取消。
  • -1:呼叫失敗,錯誤碼在errno中。

對被取消的操作呼叫aio_error會返回錯誤ECANCELED。

int lio_listio(int mode, struct aiocb *restrict const list[restrict],
             int nent, struct sigevent *restrict sigev);

該函式提交一系列由一個AIO控制塊列表描述的I/O請求。

mode引數決定該函式是否是非同步的。如果被設定為LIO_WAIT,那麼函式將在列表中的所有操作完成後返回;如果設定為LIO_NOWAIT,那麼函式將在I/O請求入隊後返回,並在所有操作結束後,按照sigev的設定被非同步地通知(無需通知則設為NULL)。sigev通知不同於AIO控制塊本身的通知,它是額外的,且只會在所有操作完成後才會傳送。

readv和writev

這兩個函式用於在一次函式呼叫中讀、寫多個非連續緩衝區,也稱之為散佈讀和聚集寫。

#include <sys/uio.h>
// Both return: number of bytes read or written, −1 on error
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

struct iovec {
  void  *iov_base; /* starting address of buffer */
  size_t iov_len;  /* size of buffer */
};

writev按照iov[0]、iov[1]直至iov[iovcnt-1]的順序輸出資料,且返回輸出的總位元組數。

readv則將讀入的資料按照上面的順序依次存入各個緩衝區,返回讀到的總位元組數。如果遇到檔案尾端,則返回0。

儲存對映I/O

該技術將一個磁碟檔案對映到儲存空間的一個緩衝區上,從緩衝區讀寫資料就相當於向檔案讀寫資料。可以在不使用read/write函式的情況下執行I/O。

對映與解除

#include <sys/mman.h>
// Returns: starting address of mapped region if OK, MAP_FAILED on error
void *mmap(void *addr, size_t len, int prot, int flag, int fd, off_t off );

addr指定對映儲存區的起始地址。設定為0則由系統自動分配。

prot引數指定對映儲存區的保護要求,如下表所示:

prot 說明
PROT_READ 對映區可讀
PROT_WRITE 對映區可寫
PROT_EXEC 對映區可執行
PROT_NONE 對映區不可訪問

表中前三項可以任意組合(按位或),但是保護要求不能超過檔案本身的訪問許可權。

flag引數指定了對映區的各類屬性:

  • MAP_FIXED:返回值必須等於addr。即要求核心必須將儲存區的起始地址設定為addr,如果沒有此標誌且addr非0,核心僅將addr的值視為一種建議。

  • MAP_SHARED:表示儲存操作會修改對映檔案,即相當於呼叫write。

  • MAP_PRIVATE:表示儲存操作會建立改對映檔案的副本,所有的儲存操作不會修改真實檔案。

注意:

offaddr的值一般要求是虛擬系統儲存頁長度的倍數。

對於一些對映區不是頁長整數倍的情況,系統會分配更多的對映區以滿足此要求。如檔案長為12位元組,頁長512位元組,則系統會提供512位元組的對映區。可以修改後面500位元組的內容,但是不會作用到原檔案上。

// Returns: 0 if OK, −1 on error
int munmap(void *addr, size_t len);

程序終止或者呼叫munmap都會解除對映區。但是關閉檔案描述符並不會解除對映,並且呼叫munmap也不會使對映區的內容寫到磁碟檔案上。

其他

// Returns: 0 if OK, −1 on error
int mprotect(void *addr, size_t len, int prot);

該函式可以更改一個現有對映的許可權。

對於通過MAP_SHARED方式進行的對映,所作的修改不會立即寫回到檔案中。

// Returns: 0 if OK, −1 on error
int msync(void *addr, size_t len, int flags);

改函式將修改的頁沖洗到檔案中去。

如果將flags引數指定為MS_ASYNC,則僅僅是請求一個寫入操作;如果指定為MS_SYNC,那麼在返回之前會等待寫操作完成。這兩個選項必選其一。

另外,還可以指定MS_INVALIDATE,來告訴作業系統丟棄與底層儲存器沒有同步的頁。