I/O多路複用詳解

1、五種I/O模型javascript

一、阻塞I/O模型java

最流行的I/O模型是阻塞I/O模型,缺省情形下,全部套接口都是阻塞的。咱們以數據報套接口爲例來說解此模型(咱們使用UDP而不是TCP做爲例子的緣由在於就UDP而言,數據準備好讀取的概念比較簡單:要麼整個數據報已經收到,要麼尚未。然而對於TCP來講,諸如套接口低潮標記等額外變量開始活動,致使這個概念變得複雜)。編程

進程調用recvfrom,其系統調用直到數據報到達且被拷貝到應用進程的緩衝區中或者發生錯誤才返回,期間一直在等待。咱們就說進程在從調用recvfrom開始到它返回的整段時間內是被阻塞的。服務器

二、非阻塞I/O模型網絡

進程把一個套接口設置成非阻塞是在通知內核:當所請求的I/O操做非得把本進程投入睡眠才能完成時,不要把本進程投入睡眠,而是返回一個錯誤。也就是說當數據沒有到達時並不等待,而是以一個錯誤返回。app

三、I/O複用模型less

調用select或poll,在這兩個系統調用中的某一個上阻塞,而不是阻塞於真正I/O系統調用。 阻塞於select調用,等待數據報套接口可讀。當select返回套接口可讀條件時,調用recevfrom將數據報拷貝到應用緩衝區中。異步

四、信號驅動I/O模型socket

首先開啓套接口信號驅動I/O功能, 並經過系統調用sigaction安裝一個信號處理函數(此係統調用當即返回,進程繼續工做,它是非阻塞的)。當數據報準備好被讀時,就爲該進程生成一個SIGIO信號。隨便可以在信號處理程序中調用recvfrom來讀數據報,井通知主循環數據已準備好被處理中。也能夠通知主循環,讓它來讀數據報。函數

五、異步I/O模型

告知內核啓動某個操做,並讓內核在整個操做完成後(包括將數據從內核拷貝到用戶本身的緩衝區)通知咱們。這種模型與信號驅動模型的主要區別是:
信號驅動I/O:由內核通知咱們什麼時候能夠啓動一個I/O操做,
異步I/O模型:由內核通知咱們I/O操做什麼時候完成。

2、I/O複用的典型應用場合:

一、當客戶處理多個描述字(一般是交互式輸入和網絡套接口)時,必須使用I/O複用。

二、若是一個服務器要處理多個服務或者多個協議(例如既要處理TCP,又要處理UDP),通常就要使用I/O複用。

3、支持I/O複用的系統調用

目前支持I/O複用的系統調用有select、pselect、poll、epoll:

一、select函數

該函數容許進程指示內核等待多個事件中的任何一個發生,並僅在有一個或多個事件發生或經歷一段指定的時間後才喚醒它。

格式爲:

1 #include <sys/select.h>
2 #include <sys/time.h>
3
4 int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
5
6 返回:就緒描述字的正數目,0-超時,-1-出錯

咱們從該函數的最後一個參數開始介紹,它告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。

struct timeval{

long tv_sec; //seconds

long tv_usec; //microseconds

};

這個參數有三種可能:

(1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。爲此,咱們把該參數設置爲空指針。

(2)等待一段固定時間:在有一個描述字準備好I/O時返回,可是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。

(3)根本不等待:檢查描述字後當即返回,這稱爲輪詢。爲此,該參數必須指向一個timeval結構,並且其中的定時器值必須爲0。

中間的三個參數readset、writeset和exceptset指定咱們要讓內核測試讀、寫和異常條件的描述字。若是咱們對某一個的條件不感興趣,就能夠把它設爲空指針。struct fd_set能夠理解爲一個集合,這個集合中存放的是文件描述符,可經過如下四個宏進行設置:

void FD_ZERO(fd_set *fdset); //清空集合

void FD_SET(int fd, fd_set *fdset); //將一個給定的文件描述符加入集合之中

void FD_CLR(int fd, fd_set *fdset); //將一個給定的文件描述符從集合中刪除

int FD_ISSET(int fd, fd_set *fdset); // 檢查集合中指定的文件描述符是否能夠讀寫 ?

目前支持的異常條件只有兩個:

(1)某個套接口的帶外數據的到達。

(2)某個已置爲分組方式的僞終端存在可從其主端讀取的控制狀態信息。

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

一個應用select的例子:

001 /**
002 *TCP回射服務器客戶端程序
003 */
004 #include <stdio.h>
005 #include <stdlib.h>
006 #include <unistd.h>
007 #include <sys/socket.h>
008 #include <sys/types.h>
009 #include <netinet/in.h>
010 #include <netdb.h>
011 #include <string.h>
012 #include <math.h>
013 #include <sys/select.h>
014 #include <sys/time.h>
015
016 #define SERVER_PORT 3333 //服務器端口號
017
018 void str_cli(FILE *fp, int sockfd)
019 {
020 int maxfdp1, stdineof;
021 fd_set rset;
022 char buf[BUFSIZ];
023 int n;
024
025 stdineof = 0;
026 FD_ZERO(&rset);
027
028 while(1)
029 {
030 if( stdineof == 0 )
031 FD_SET(fileno(fp),&rset);
032 FD_SET(sockfd, &rset);
033
034 maxfdp1 = ((fileno(fp) > sockfd) ? fileno(fp) : sockfd) + 1;
035
036 select(maxfdp1, &rset, NULL, NULL, NULL);
037
038 if( FD_ISSET(sockfd, &rset) )
039 {
040 if( (n = read(sockfd, buf, BUFSIZ)) == 0 )
041 if( stdineof == 1 )
042 return;
043 else
044 perror("server terminated prematurely");
045 write(fileno(stdout), buf, n);
046 }
047
048 if( FD_ISSET(fileno(fp), &rset))
049 {
050 if( (n = read(fileno(fp), buf, BUFSIZ)) == 0 )
051 {
052 stdineof = 1;
053 shutdown(sockfd, SHUT_WR);
054 FD_CLR(fileno(fp), &rset);
055 continue;
056 }
057 write(sockfd, buf, n);
058 }
059 }
060 }
061
062 int main(int argc, char *argv[])
063 {
064 int sockfd[5];
065 struct sockaddr_in servaddr;
066 struct hostent *hp;
067 char buf[BUFSIZ];
068
069 if( argc != 2 )
070 {
071 printf("Please input %s <hostname>\n", argv[0]);
072 exit(1);
073 }
074
075 int i;
076 for(i = 0; i < 5; ++i)
077 {
078
079 //建立socket
080 if( (sockfd[i] = socket(AF_INET, SOCK_STREAM,0)) < 0 )
081 {
082 printf("Create socket error!\n");
083 exit(1);
084 }
085
086 //設置服務器地址結構
087 bzero(&servaddr, sizeof(servaddr));
088 servaddr.sin_family = AF_INET;
089 if( (hp = gethostbyname(argv[1])) != NULL )
090 {
091 bcopy(hp->h_addr, (struct sockaddr*)&servaddr.sin_addr, hp->h_length);
092 }
093 else if(inet_aton(argv[1], &servaddr.sin_addr) < 0 )
094 {
095 printf("Input Server IP error!\n");
096 exit(1);
097 }
098 servaddr.sin_port = htons(SERVER_PORT);
099
100 //鏈接服務器
101 if( connect(sockfd[i],(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0 )
102 {
103 printf("Connect server failure!\n");
104 exit(1);
105 }
106 }
107 str_cli(stdin, sockfd[0]);
108
109 exit(0);
110 }

注:本章內容摘自<Unix 網絡編程>第六章。

二、pselect函數

pselect函數是由POSIX發明的,現在許多Unix變種都支持它。

1 #include <sys/select.h>
2 #include <signal.h>
3 #include <time.h>
4
5 int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);
6 返回:就緒描述字的個數,0-超時,-1-出錯

pselect相對於一般的select有兩個變化:

一、pselect使用timespec結構,而不使用timeval結構。timespec結構是POSIX的又一個發明。

struct timespec{

time_t tv_sec; //seconds

long tv_nsec; //nanoseconds

};

這兩個結構的區別在於第二個成員:新結構的該成員tv_nsec指定納秒數,而舊結構的該成員tv_usec指定微秒數。

二、pselect函數增長了第六個參數:一個指向信號掩碼的指針。該參數容許程序先禁止遞交某些信號,再測試由這些當前被禁止的信號處理函數設置的全局變量,而後調用pselect,告訴它從新設置信號掩碼。

關於第二點,考慮下面的例子,這個程序的SIGINT信號處理函數僅僅設置全局變量intr_flag並返回。若是咱們的進程阻塞於select調用,那麼從信號處理函數的返回將致使select返回EINTR錯誤。然而調用select時,代碼看起來大致以下:

相關文章
相關標籤/搜索