天天看點

Linux IO模型:阻塞/非阻塞/IO複用 同步/異步 Select/Epoll/AIO

IO概念

Linux的核心将所有外部裝置都可以看做一個檔案來操作。那麼我們對與外部裝置的操作都可以看做對檔案進行操作。我們對一個檔案的讀寫,都通過調用核心提供的系統調用;核心給我們傳回一個file descriptor(fd,檔案描述符)。而對一個socket的讀寫也會有相應的描述符,稱為socketfd(socket描述符)。描述符就是一個數字,指向核心中一個結構體(檔案路徑,資料區,等一些屬性)。那麼我們的應用程式對檔案的讀寫就通過對描述符的讀寫完成。

linux将記憶體分為核心區,使用者區。linux核心給我們管理所有的硬體資源,應用程式通過調用系統調用和核心互動,達到使用硬體資源的目的。應用程式通過系統調用read發起一個讀操作,這時候核心建立一個檔案描述符,并通過驅動程式向硬體發送讀指令,并将讀的的資料放在這個描述符對應結構體的核心緩存區中,然後再把這個資料讀到使用者程序空間中,這樣完成了一次讀操作;

但是大家都知道I/O裝置相比cpu的速度是極慢的。linux提供的read系統調用,也是一個阻塞函數。這樣我們的應用程序在發起read系統調用時,就必須阻塞,就程序被挂起而等待檔案描述符的讀就緒,那麼什麼是檔案描述符讀就緒,什麼是寫就緒?

讀就緒:就是這個檔案描述符的接收緩沖區中的資料位元組數大于等于套接字接收緩沖區低水位标記的目前大小;

寫就緒:該描述符發送緩沖區的可用空間位元組數大于等于描述符發送緩沖區低水位标記的目前大小。(如果是socket fd,說明上一個資料已經發送完成)。

接收低水位标記和發送低水位标記:由應用程式指定,比如應用程式指定接收低水位為64個位元組。那麼接收緩沖區有64個位元組,才算fd讀就緒;

綜上所述,一個基本的IO,它會涉及到兩個系統對象,一個是調用這個IO的程序對象,另一個就是系統核心(kernel)。當一個read操作發生時,它會經曆兩個階段:

  • 通過read系統調用想核心發起讀請求。
  • 核心向硬體發送讀指令,并等待讀就緒。
  • 核心把将要讀取的資料複制到描述符所指向的核心緩存區中。
  • 将資料從核心緩存區拷貝到使用者程序空間中。

IO模型

在linux系統下面,根據IO操作的是否被阻塞以及同步異步問題進行分類,可以得到下面五種IO模型:

1、阻塞I/O模型

最流行的I/O模型是阻塞I/O模型,預設情形下,所有檔案操作都是阻塞的。我們以套接口為例來講解此模型。在程序空間中調用recvfrom,其系統調用直到資料報到達且被拷貝到應用程序的緩沖區中或者發生錯誤才傳回,期間一直在等待。我們就說程序在從調用recvfrom開始到它傳回的整段時間内是被阻塞的。

Linux IO模型:阻塞/非阻塞/IO複用 同步/異步 Select/Epoll/AIO

image

2、非阻塞I/O模型

程序把一個套接口設定成非阻塞是在通知核心:當所請求的I/O操作不能滿足要求時候,不把本程序投入睡眠,而是傳回一個錯誤。也就是說當資料沒有到達時并不等待,而是以一個錯誤傳回。

Linux IO模型:阻塞/非阻塞/IO複用 同步/異步 Select/Epoll/AIO

3、I/O複用模型

linux提供select/poll,程序通過将一個或多個fd傳遞給select或poll系統調用,阻塞在select;這樣select/poll可以幫我們偵測許多fd是否就緒。但是select/poll是順序掃描fd是否就緒,而且支援的fd數量有限。linux還提供了一個epoll系統調用,epoll是基于事件驅動方式,而不是順序掃描,當有fd就緒時,立即回調函數rollback;

Linux IO模型:阻塞/非阻塞/IO複用 同步/異步 Select/Epoll/AIO

4、信号驅動異步I/O模型

首先開啟套接口信号驅動I/O功能, 并通過系統調用sigaction安裝一個信号處理函數(此系統調用立即傳回,程序繼續工作,它是非阻塞的)。當資料報準備好被讀時,就為該程序生成一個SIGIO信号。随即可以在信号處理程式中調用recvfrom來讀資料報,井通知主循環資料已準備好被進行中。也可以通知主循環,讓它來讀資料報。

Linux IO模型:阻塞/非阻塞/IO複用 同步/異步 Select/Epoll/AIO

5、異步I/O模型

告知核心啟動某個操作,并讓核心在整個操作完成後(包括将資料從核心拷貝到使用者自己的緩沖區)通知我們。這種模型與信号驅動模型的主要差別是:信号驅動I/O:由核心通知我們何時可以啟動一個I/O操作;異步I/O模型:由核心通知我們I/O操作何時完成。

Linux IO模型:阻塞/非阻塞/IO複用 同步/異步 Select/Epoll/AIO

非阻塞IO詳解

通過上面,我們知道,所有的IO操作在預設情況下,都是屬于阻塞IO。盡管上圖中所示的反複請求的非阻塞IO的效率底下(需要反複在使用者空間和程序空間切換和判斷,把一個原本屬于IO密集的操作變為IO密集和計算密集的操作),但是在後面IO複用中,需要把IO的操作設定為非阻塞的,此時程式将會阻塞在select和poll系統調用中。把一個IO設定為非阻塞IO有兩種方式:在建立檔案描述符時,指定該檔案描述符的操作為非阻塞;在建立檔案描述符以後,調用fcntl()函數設定相應的檔案描述符為非阻塞。

建立描述符時,利用open函數和socket函數的标志設定傳回的fd/socket描述符為O_NONBLOCK。

int sd=socket(int domain, int type|O_NONBLOCK, int protocol);
int fd=open(const char *pathname, int flags|O_NONBLOCK);
           

建立描述符後,通過調用fcntl函數設定描述符的屬性為O_NONBLOCK

#include <unistd.h>#include <fcntl.h> 
int fcntl(int fd, int cmd, ... /* arg */ ); 
  //例子
  if (fcntl(fd, F_SETFL, fcntl(sockfd, F_GETFL, 0)|O_NONBLOCK) == -1) {       
     return -1;
  } 
  return 0;
}
           

IO複用詳解

在IO程式設計過程中,當需要處理多個請求的時,可以使用多線程和IO複用的方式進行處理。上面的圖介紹了整個IO複用的過程,它通過把多個IO的阻塞複用到一個select之類的阻塞上,進而使得系統在單線程的情況下同時支援處理多個請求。和多線程/程序比較,I/O多路複用的最大優勢是系統開銷小,系統不需要建立新的程序或者線程,也不必維護這些線程和程序。IO複用常見的應用場景:

  1. 客戶程式需要同時處理互動式的輸入和伺服器之間的網絡連接配接。
  2. 用戶端需要對多個網絡連接配接作出反應。
  3. 伺服器需要同時處理多個處于監聽狀态和多個連接配接狀态的套接字
  4. 伺服器需要處理多種網絡協定的套接字。

目前支援I/O複用的系統調用有select、pselect、poll、epoll,下面幾小結分别來學習一下select和epoll的使用。

Linux 2.6之前是select、poll,2.6之後是epoll,Windows是IOCP。

select

#include <sys/select.h> 
int select(int maxfdps, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); 
struct timeval{
  long tv_sec; //秒
  long tv_usec; //微秒
};
void FD_CLR(int fd, fd_set *set); //将一個給定的檔案描述符從集合中删除
int  FD_ISSET(int fd, fd_set *set);  // 檢查集合中指定的檔案描述符是否可以讀寫 ?
void FD_SET(int fd, fd_set *set); //将一個給定的檔案描述符加入集合之中
void FD_ZERO(fd_set *set);//清空集合           傳回:就緒描述字的正數目,0-逾時,-1-出錯
           

struct timeval *time結構體告知核心等待所指定描述字中的任何一個就緒可花多少時間。參數取值:

(1)(struct timeval *)0:永遠等待下去,僅在有一個描述字準備好I/O時才傳回。

(2)struct timeval *time:在有一個描述字準備好I/O時傳回,但是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。如果超過時間,沒有描述字準備好,那就傳回0。如果秒=微秒=0,檢查描述字後立即傳回,此時相當于輪詢。

中間的三個參數readset、writeset和exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果我們對某一個的條件不感興趣,就可以把它設為空指針。fd_set可以了解為一個集合,這個集合中存放的是檔案描述符,可通過上面的四個宏進行設定(注意,fd_set不是struct fd_set。。。 剛剛開始調試程式就犯錯誤,傳回storage size of 'fds' isn't known)。

第一個參數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(是以我們把該參數命名為maxfdp1),描述字0、1、2...maxfdp1-1均将被測試。

下面是一個從網上看到的一個比較好的測試程式,太長了,移到空間代碼功能裡面了,

click me to see
Linux IO模型:阻塞/非阻塞/IO複用 同步/異步 Select/Epoll/AIO
http://my.csdn.net/colzer/code/detail/26460 奮鬥 http://my.csdn.net/colzer/code/detail/26460

pselect

#include <sys/select.h> 
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,   const sigset_t *sigmask);
struct timespec{    
  time_t tv_sec; //秒    
  long tv_nsec; //納秒
};
           

比較select和pselect函數,我們發現在原型上面有兩個不同:

1、pselect使用timespec結構,而不使用timeval結構。timespec結構是POSIX的又一個發明。 這兩個結構的差別在于第二個成員:新結構的該成員tv_nsec指定納秒數,而舊結構的該成員tv_usec指定微秒數。

2、pselect函數增加了第六個參數:一個指向信号掩碼的指針。該參數允許程式先禁止遞交某些信号,再測試由這些目前被禁止的信号處理函數設定的全局變量,然後調用pselect,告訴它重新設定信号掩碼。

首先我們看看信号掩碼的概念:在POSIX下,每個程序有一個信号掩碼(signal mask)。簡單地說,信号掩碼是一個“位圖”,其中每一位都對應着一種信号。如果位圖中的某一位為1,就表示在執行目前信号的處理程式期間相應的信号暫時被“屏蔽”,使得在執行的過程中不會嵌套地響應那種信号。為什麼對某一信号進行屏蔽呢?我們來看一下對CTRL_C的處理。大家知道,當一個程式正在運作時,在鍵盤上按一下CTRL_C,核心就會向相應的程序發出一個SIGINT 信号,而對這個信号的預設操作就是通過do_exit()結束該程序的運作。但是,有些應用程式可能對CTRL_C有自己的處理,是以就要為SIGINT另行設定一個處理程式,使它指向應用程式中的一個函數,在那個函數中對CTRL_C這個事件作出響應。但是,在實踐中卻發現,兩次CTRL_C事件往往過于密集,有時候剛剛進入第一個信号的處理程式,第二個SIGINT信号就到達了,而第二個信号的預設操作是殺死程序,這樣,第一個信号的處理程式根本沒有執行完。為了避免這種情況的出現,就在執行一個信号處理程式的過程中将該種信号自動屏蔽掉。所謂“屏蔽”,與将信号忽略是不同的,它隻是将信号暫時“遮蓋”一下,一旦屏蔽去掉,已到達的信号又繼續得到處理。有關信号相關的知識參考我另外一個文章:《

linux基礎程式設計:程序通信之信号

》。在我們開發過程中,如果程序阻塞與select函數,此時該阻塞被信号所打斷,select傳回-1,errno=EINTR的錯誤。由于這個原因,我們才有了pselect函數在信号上的優化處理。是以在信号和select都被使用的系統裡,pselect有其作用。否則和select沒有任何差別。

poll

#include <poll.h> 
int poll(struct pollfd *fds, nfds_t nfds, int timeout); 
struct pollfd { 
  int fd; /* file descriptor */ 
  short events; /* requested events to watch */ 
  short revents; /* returned events witnessed */    
}; 
           

和select()不一樣,poll()沒有使用低效的三個基于位的檔案描述符set,而是采用了一個單獨的結構體pollfd數組,由fds指針指向這個數組的第一個元素,其中nfds表示該數組的大小。每一個pollfd 結構體指定了一個被監視的檔案描述符,每個結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。revents域是檔案描述符的操作結果事件掩碼。核心在調用傳回時設定這個域。events域中請求的任何事件都可能在revents域中傳回,而部分事件不能出現在events中,詳細如下表。

| 常量 | 說明 |

| POLLIN | 普通或優先級帶資料可讀 |

| POLLRDNORM | 普通資料可讀 |

| POLLRDBAND | 優先級帶資料可讀 |

| POLLPRI | 高優先級資料可讀 |

| POLLOUT | 普通資料可寫 |

| POLLWRNORM | 普通資料可寫 |

| POLLWRBAND | 優先級帶資料可寫 |

| POLLERR | 發生錯誤 |

| POLLHUP | 發生挂起 |

| POLLNVAL | 描述字不是一個打開的檔案 |

注意:後三個隻能作為描述字的傳回結果存儲在revents中,而不能作為測試條件用于events中。

最後一個參數timeout是指定poll函數傳回前等待多長時間。它的取值如下:

| timeout值 | 說明 |

| INFTIM | 永遠等待 |

| 0 | 立即傳回,不阻塞程序 |

| >0 | 等待指定數目的毫秒數 |

成功時,poll()傳回結構體中revents域不為0的檔案描述符個數;如果在逾時前沒有任何事件發生,poll()傳回0;失敗時,poll()傳回-1,并設定errno為下列值之一:

EBADF:一個或多個結構體中指定的檔案描述符無效。EFAULT:fds指針指向的位址超出程序的位址空間。EINTR:請求的事件之前産生一個信号,調用可以重新發起。EINVAL:nfds參數超出PLIMIT_NOFILE值。ENOMEM:可用記憶體不足,無法完成請求。

關鍵代碼如下:

client[0].fd = listenfd;         /*将數組中的第一個元素設定成監聽描述字*/    
client[0].events = POLLIN;       
while(1)    {        
  nready = poll(client, maxi+1,INFTIM); 
  //将程序阻塞在poll上       
  if( client[0].revents & POLLIN/*POLLRDNORM*/ )
  /*先測試監聽描述字*/
           

epoll

在linux的網絡程式設計中,很長的一段時間都在使用select來做事件觸發。然而select逐漸暴露出了一些缺陷,使得linux不得不在新的核心中尋找出替代方案,那就是epoll。其實,epoll與select原理類似,隻不過,epoll作出了一些重大改進,即:

1.支援一個程序打開大數目的socket描述符(FD)

select 最不能忍受的是一個程序所打開的FD是有一定限制的,由FD_SETSIZE設定,預設值是2048。對于那些需要支援的上萬連接配接數目的IM伺服器來說顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯核心,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多程序的解決方案(傳統的 Apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上線程間同步的高效,是以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關系很大。

2.IO效率不随FD數目增加而線性下降

傳統的select/poll另一個緻命弱點就是當你擁有一個很大的socket集合,不過由于網絡延時,任一時間隻有部分的socket是"活躍"的,但是select/poll每次調用都會線性掃描全部的集合,導緻效率呈現線性下降。但是epoll不存在這個問題,它隻會對"活躍"的socket進行操作---這是因為在核心實作中epoll是根據每個fd上面的callback函數實作的。那麼,隻有"活躍"的socket才會主動的去調用 callback函數,其他idle狀态socket則不會,在這點上,epoll實作了一個"僞"AIO,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll并不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模拟WAN環境,epoll的效率就遠在select/poll之上了。

3.使用mmap加速核心與使用者空間的消息傳遞。

這點實際上涉及到epoll的具體實作了。無論是select,poll還是epoll都需要核心把FD消息通知給使用者空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心于使用者空間mmap同一塊記憶體實作的。

4.核心微調

這一點其實不算epoll的優點了,而是整個linux平台的優點。也許你可以懷疑linux平台,但是你無法回避linux平台賦予你微調核心的能力。比如,核心TCP/IP協定棧使用記憶體池管理sk_buff結構,那麼可以在運作時期動态調整這個記憶體pool(skb_head_pool)的大小 --- 通過echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手的資料包隊列長度),也可以根據你平台記憶體大小動态調整。更甚至在一個資料包面數目巨大但同時每個資料包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。

epoll api函數比較簡單,包括建立一個epoll描述符,添加監聽事件,阻塞等待所監聽的事件發生,關閉epoll描述符,如下:

#include <sys/epoll.h>
#include <unistd.h> 
int epoll_create(int size); //epoll描述符
int close(int fd);//關閉epoll描述符
           

建立一個epoll的句柄,size用來告訴核心這個監聽的數目一共有多大。需要注意的是,當建立好epoll句柄後,epoll本身就占用一個fd值,是以用完後必須調用close()關閉,以防止fd被耗盡。

#include <sys/epoll.h>
#include <unistd.h> 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//添加監聽事件 typedef union epoll_data {    
  void        *ptr;    
  int          fd;    
  uint32_t     u32;    
  uint64_t     u64;
} 
epoll_data_t; 
struct epoll_event {    
  uint32_t     events;      /* Epoll events */    
  epoll_data_t data;        /* User data variable */
};
           

epoll_ctl為事件注冊函數,第一個參數是epoll_create()的傳回值,第二個參數表示動作,用三個宏來表示:

  • EPOLL_CTL_ADD:注冊新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
  • EPOLL_CTL_DEL:從epfd中删除一個fd;

第三個參數是需要監聽的描述符fd,第四個參數是告訴核心需要監聽什麼事件,struct epoll_event結構如上所示,其中events為需要注冊的事件,可以為下面幾個宏的集合:

  • EPOLLIN:表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
  • EPOLLOUT:表示對應的檔案描述符可以寫;
  • EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
  • EPOLLERR:表示對應的檔案描述符發生錯誤;
  • EPOLLHUP:表示對應的檔案描述符被挂斷;
  • EPOLLET:将EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水準觸發(Level Triggered)來說的。詳細見下面描述
  • EPOLLONESHOT:隻監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裡。

作業系統對被注冊的檔案描述符的事件發送以後有兩種處理方式,分别有LT和ET模式,預設情況下為LT,如果需要設定為ET模式,設定struct epoll_event.events|EPOLLET。

  • LT(level triggered)是預設的工作方式,并且同時支援block和no-block socket.在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,是以,這種模式程式設計出錯誤可能性要小一點,傳統的select/poll都是這種模型的代表。
  • ET (edge-triggered)是高速工作方式,隻支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,并且不會再為那個檔案描述符發送更多的就緒通知,直到你做了某些操作導緻那個檔案描述符不再為就緒狀态了(比如,你在發送,接收或者接收請求,或者發送接收的資料少于一定量時導緻了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(進而導緻它再次變成未就緒),核心不會發送更多的通知(only once),不過在TCP協定中,ET模式的加速效用仍需要更多的benchmark确認。

struct epoll_event.data為一個epoll_data_t類型的聯合體,詳細結構如上,其中可以儲存指針,描述符,32/64位的整數。如果在監聽過程中,該描述符上面有相應的事件發生,系統将會把該字段傳回。先看監聽函數吧。看完就知道

#include <sys/epoll.h>
#include <unistd.h> 
int epoll_wait(int epfd, struct epoll_event *events,  int maxevents, int timeout);//阻塞等待所監聽的事件發生
           

阻塞監聽函數,類似于select()調用。參數events用來從核心得到事件的集合,傳回的結構也是struct epoll_event,其中event為相應的事件,data為注冊時,設定的值(常見情況,data設定為注冊的描述符,這樣就可以對相應的描述符進行IO操作)。maxevents告之核心這個events有多大,這個maxevents的值不能大于建立epoll_create()時的size,參數timeout是逾時時間(毫秒,0會立即傳回,-1将不确定,也有說法說是永久阻塞)。該函數傳回需要處理的事件數目,如傳回0表示已逾時。

struct epoll_event ev, *events; int kdpfd = epoll_create(100); 
ev.events = EPOLLIN | EPOLLET; 
// 注意這個EPOLLET,指定了邊緣觸發 
ev.data.fd =listener; 
epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev); 
for(;;) {   
nfds = epoll_wait(kdpfd, events, maxevents, -1);    
for(n = 0; n < nfds; ++n)    {      
if(events[n].data.fd == listener)       {                   
client = accept(listener, (struct sockaddr *) &local, &addrlen);                    if(client < 0){                         
perror("accept");                           
continue;                   }                   
setnonblocking(client);                  
ev.events = EPOLLIN | EPOLLET;                   
ev.data.fd = client;                    
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)           {                           fprintf(stderr, "epoll set insertion error: fd=%d0, client);                            return -1;                  
}  }else{           
do_use_fd(events[n].data.fd);       
}                        
} }
           

最後我們說一下,用來克服select/poll缺點的方法不隻有epoll。epoll隻是一種linux 的方案,在freeBSD下有kqueue,而dev/poll 是最古老的Solaris的方案,使用難度依次遞增。 kqueue 是freebsd 的寵兒,kqueue 實際上是一個功能相當豐富的kernel 事件隊列,它不僅僅是select/poll 的更新,而且可以處理 signal 、目錄結構變化、程序等多種事件。Kqueue 是邊緣觸發的。/dev/poll 是Solaris的産物,是這一系列高性能API 中最早出現的。Kernel 提供一個特殊的裝置檔案/dev/poll 。應用程式打開這個檔案得到操縱fd_set 的句柄,通過寫入pollfd 來修改它,一個特殊ioctl 調用用來替換select 。由于出現的年代比較早,是以/dev/poll 的接口現在看上去比較笨拙可笑。

信号驅動異步IO詳解

上面介紹了不管是阻塞/非阻塞或者IO複用,在應用程式層面都需要進行阻塞輪詢指定的IO上面是否有相應的事件發生。本節我們将将介紹一個全新的模型:異步模型,異步則意味着不需要使用者層進行輪詢,在特定IO或者事件發生時候,會主動通知應用程式IO就緒,本節介紹的通知機制即信号,我們把這個模型叫着信号驅動的異步I/O。在《

》文章中,我們詳細介紹信号操作。

Unix上有定義了許多信号,源自Berkeley的實作使用的是SIGIO信号來支援套接字和終端裝置上的信号驅動IO。在套接字IO中,信号驅動IO模型主要是在UDP套接字上使用,在TCP套接字上幾乎是沒有什麼使用的(在TCP上,由于TCP是雙工的,它的信号産生過于平凡,并且信号的出現幾乎沒有告訴我們發生了什麼事情。是以對于TCP套接字,SIGIO信号是沒有什麼使用的)。

使用信号驅動異步IO主要有下面幾個步驟:

為了讓套接字描述符可以工作于信号驅動I/O模式,應用程序必須完成如下三步設定:

  • 1.注冊SIGIO信号處理程式。(安裝信号處理器)
  • 2.使用fcntl的F_SETOWN指令,設定套接字所有者。
  • 3.使用fcntl的F_SETFL指令,置O_ASYNC和O_NONBLOCK标志,允許套接字信号驅動I/O。

注意,必須保證在設定套接字所有者之前,向系統注冊信号處理程式,否則就有可能在fcntl調用後,信号處理程式注冊前核心向應用傳遞SIGIO信号,導緻應用丢失此信号。

代碼片段如下:

struct sigaction sigio_action;
memset(&sigio_action, 0, sizeof(sigio_action));
sigio_action.sa_flags = 0;sigio_action.sa_handler = do_sigio;
//信号發生時的處理函數sigaction(SIGIO, &sigio_action, NULL); 
fcntl(listenfd1, F_SETOWN, getpid());
int flags;flags = fcntl(listenfd1, F_GETFL, 0);
flags |= O_ASYNC | O_NONBLOCK;fcntl(listenfd1, F_SETFL, flags);
           

異步IO詳解

Linux 2.6核心包含對一套新的異步IO,即AIO 的支援為使用者空間提供統一的異步I/O 接口。目前好像隻支援本地IO。等有時間再去學習吧。

繼續閱讀