I/O複用
I/O多路複用是爲了解決進程或線程阻塞到某個I/O系統調用而出現的技術,使進程不再阻塞於某個特定的I/O系統調用。
利用select、poll、epoll實現多個端口通信。I/O複用使得程序能同時監聽多個文件描述符,當某個文件描述符就緒時,能夠通知程序進行相應的讀寫操作,但select()、poll()、epoll()本身是阻塞的,直到出現就緒的文件描述符。並且當多個文件描述符同時就緒時,如果不採取額外的措施,程序就只能按順序依次處理其中的每一個文件描述符,這使得服務器程序看起來像是串形工作的。如果要實現併發,只能使用多進程或多線程的手段。
與多進程和多進程相比,I/O多路複用的最大優勢是系統開銷小,系統不需要建立新的進程或線程,也不必維護進程和線程。
文件描述符的就緒條件
滿足下列條件之一,套接字準備好讀:
- 套接字接收緩衝區當中的數據字節數大於等於套接字接收緩衝區中設置的最小值。(對於TCP和UDP來說默認值爲1)。此時可以無阻塞地讀該socket,並且讀操作返回的字節數大於0。
- socket通信的對方關閉連接。此時對該socket的讀操作將返回0。
- 監聽socket上有新的連接請求。
- socket上有未處理的錯誤。此時我們可以使用getsockopt來讀取和清除該錯誤。
滿足下列條件之一,套接字準備好寫:
- 該套接字發送緩衝區中可用空間的大小大於等於套接字發送緩衝區中設置的最小值時,此時我們可以無阻塞地寫該socket,並且寫操作返回的字節數大於0;
- socket的寫操作被關閉。對寫操作被關閉的socket執行寫操作將觸發一個SIGPIPE的信號。
- socket使用非阻塞connect連接成功或者失敗(超時)之後
- socket有未處理的錯誤。
select
#include<sys/time.h>
#include<sys/select.h>
#include<sys/type.h>
#include<unistd.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exception,struct timeval *timeout);
功能:select監視readfds、writefds、exception三個參數作爲自己感興趣的文件描述符;應用程序在調用select時,函數會阻塞,直到有文件描述符就緒(有數據可讀、可寫、或者有錯誤異常)、或者超時,select函數將調用返回,內核會修改他們來通知應用程序那些文件描述符已經就緒,可以遍歷fd_set來找到就緒的文件描述符。
參數:
nfds:監聽的最大文件描述符+1;文件描述符存放在fd_set中,fd_set是一個long類型的數組,並且數組中的每一位表示一個文件描述符,所以最多可維護1024個文件描述符(0~1023文件描述符從0開始);
readfds:監視可讀的文件描述符集合,只要有文件描述符即將進行讀操作,這個文件描述符就存儲到這;
writefds:監視可寫的文件描述符集合;
exception:監視的錯誤異常文件描述符的集合;
這三個參數是我們指定讓內核檢測讀、寫、異常條件的文件描述符。如果不需要的則設置爲NULL。結構體fd_set就是存放文件描述符的集合,可以通過以下四個宏進行設置:
FD_ZERO(fd_set *fdset); /*清除fdset的所有位*/
FD_SET(int fd,fd_set *fdset); /*設置fdset的位fd*/
FD_CLE(int fd,fd_set *fdset); /*清除fdset的位fd*/
int FD_ISSET(int fd,fd_set *fdset) /*測試fdset的位fd是否被設置*/
timeout:設置select函數的超時時間,它告知內核等待所指定描述字中的任何一個就緒可花多少時間,其結構如下:
struct timeval
{
time_t tv_sec;//秒
suseconds_t tv_usec;//微妙
}
這個參數有以下幾種可能:
(1)永遠等待下去,僅在有一個描述符準備好I/O時才返回。爲此,把該參數設置爲NULL;
(2)等待固定時間,在指定的固定時間內,在有一個描述字準備好I/O時返回,如果時間到了,沒有文件描述符發生變化,這個函數會返回0;
(3)根本不等待,檢查描述字後立即返回,這稱爲輪詢。爲此,struct timeval變量的時間指定爲0秒0微秒,文件描述符屬性無變化返回0,有變化返回準備好的描述符數量;
返回值:
出錯:返回-1
超時:返回0
成功:返回就緒文件描述符的個數
簡單實例select函數:
將標準輸入文件描述符0存放在集合中,利用select監聽此描述符,代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/select.h>
#include <netinet/in.h>
#define STDIN 0//標準輸入的文件描述符爲0
int main()
{
fd_set readfd;//文件描述符的集合
int fd = STDIN;
while(1)
{
FD_ZERO(&readfd);
FD_SET(fd,&readfd);
struct timeval vl = {5,0};//時間設置爲5秒
int n = select(fd+1,&readfd,NULL,NULL,&vl);
if(n == -1)
{
perror("select error\n");
continue;
}
else if(n == 0)
{
printf("time out\n");
continue;
}
else
{
if(FD_ISSET(fd,&readfd))//檢測就緒的文件描述符
{
char buff[128] = {0};
read(fd,buff,127);
printf("read:%s",buff);
//close(fd);//不能關閉fd。如果關閉,fd是無效的文件描述符,將一直打印出timeout
}
}
}
}
結果:
使用多客戶端的網絡通信實例(服務器利用select函數實現I/O複用)
服務器端:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int maxfd = -1;//初始化最大描述符
int sockfds[1024];//存放文件描述符的集合,最大爲1024個
void InitFds()//對文件描述符的集合初始化
{
int i = 0;
for(;i < 1024;++i)
{
sockfds[i] = -1;
}
}
int AddFd(int fd)//添加文件描述符
{
int i = 0;
for(;i < 1024;i++)
{
if(sockfds[i] == -1)
{
sockfds[i] = fd;
return 1;
}
}
return 0
}
int Delete(int fd)//刪除文件描述符
{
int i = 0;
for(;i < 1024;i++)
{
if(sockfds[i] == fd)
{
sockfds[i] = -1;
return 1;
}
}
return 0;
}
int CreateSocket(int port,char *p)//服務器端創建一個文件描述符
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(port);
ser.sin_addr.s_addr = inet_addr(p);
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
AddFd(sockfd);
return sockfd;
}
int main()
{
InitFds();//對文件描述符的集合初始化
int sockfd = CreateSocket(6000,"127.0.0.1");//服務器端創建一個文件描述符
fd_set readfds;
while(1)
{
FD_ZERO(&readfds);
int i = 0;
for(;i < 1024;i++)
{
if(sockfds[i] != -1)
{
FD_SET(sockfds[i],&readfds);//將文件描述符設置到readfds相應的位中
if(sockfds[i] > maxfd)
{
maxfd = sockfds[i];//找到最大的文件描述符
}
}
}
int n = select(maxfd+1,&readfds,NULL,NULL,NULL);//設置爲永久阻塞,直到有就緒的文件描述符
if(n <= 0)//出錯
{
printf("error\n");
exit(0);
}
for(i = 0;i < 1024;i++)
{
int fd = sockfds[i];
if(fd != -1 && FD_ISSET(fd,&readfds))
{
if(fd == sockfd)//sockfd的處理,監聽套接字
{
struct sockaddr_in cli;
int len = sizeof(cli);
int c = accept(sockfd,(struct sockaddr*)&cli,&len);
if(c == -1)
{
printf("Link error\n");
continue;
}
AddFd(c);
printf("accept:%d\n",c);
}
else //c的處理,鏈接套接字
{
char buff[128] = {0};
int n = recv(fd,buff,127,0);
if(n <= 0)
{
close(fd);
Delete(fd);
continue;
}
printf("recv(%d):%s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
}
}
客戶端:
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INT,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connet(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
while(1)
{
printf("please input:");
char buff[128] = {0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)
{
close(sockfd);
break;
}
printf("recvbuff:%s\n",recvbuff);
}
}
運行結果:
select()的補充:
- 記錄每種事件的結構,在數組按位記錄關注的文件描述符上的事件
- 每次最多可以監聽1024個文件描述符,並且最大值1023。
- select函數返回時,通過傳遞的結構體變量(fd_set)將結果帶回(就緒的和未就緒的)所以:
(1)每次都必須循環探測那些文件描述符就緒 時間複雜度O(n)
(2)每次調用select之前必須重新設置三個結構體變量 - select函數第一個參數最大的文件描述符值+1,可以提高底層效率
select()的優點:目前幾乎在所有的平臺上支持,其良好跨平臺支持
select()的缺點:
- 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大;
- 文件描述符就緒時,內核會修改readfds、writefds、exception結構,所以每次調用select之前,必須重新將文件描述符註冊一遍
- 每次調用select都需要在內核遍歷傳遞進來的所有的fd,這個開銷在fd很多時,也很大
- 單個進程能夠監視的文件描述符存在最大的限制
poll
select()和poll()系統調用的本質一樣,前者在BSD UNIX中引入的,後者在Sysem V中引入的。poll()的機制和select()類似,管理多個描述符也是輪詢,根據描述符的狀態進行處理,但是poll()沒有最大文件描述符數量的限制(但是數量過大後性能也是會下降的),並且poll的底層是用鏈表實現的,而select底層是數組。
#include <poll.h>
int poll(struct pollfd *fds,int nfds,int timeout);
功能:監視並等待多個文件描述符的屬性變化
參數:
fds:fds是指向polled這一結構體數組,結構體中包括用戶關注的文件描述符(fd),用戶關注的事件(events),調用後實際發生的事件,也就是由內核修改的事件,放在revents中;如下
struct polled
{
int fd; /*用戶關注的文件描述符*/
short events; /*用戶關注的事件,由用戶設置*/
short reventd /*由內核修改,表示發生了那些事件*/
}
poll事件類型:ndfs:指的是第一個參數數組元素的個數,也就是用戶關注的文件描述符的個數。
timeout:指定等待的毫秒。當等待時間爲0時,poll()函數立即返回,爲-1時poll()一直阻塞直到一個事件發生
返回值:成功時,poll()返回結構體中revents域不爲0的文件描述符個數,如果在超時前沒有任何事件發生,poll()返回0;
失敗時,返回-1;
使用多客戶端的網絡通信實例(服務器利用poll函數實現I/O複用)
服務器端:
#define _GNU_SOURCE//必須放在首行
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <assert.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#define MAXFD 10
int Createsockfd(int port,char *ip)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in ser;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(port);
ser.sin_addr.s_addr = inet_addr(ip);
int res = bind(socket,(struct sockaddr*)&ser,sizeof(ser));
if(res == -1)
{
return -1;
}
listen(sockfd,5);
return sockfd;
}
void Init(struct pollfd fds[])
{
int i = 0;
for(;i < MAXFD;i++)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revens = 0;
}
}
void add_fds(struct pollfd fds[],int sockfd)
{
int i = 0;
for(;i < MAXFD;i++)
{
if(fds[i].fd == -1)
{
fds[i].fd = sockfd;
fds[i].events = POLLIN|POLLRDHUP;
fds[i].revents = 0;
break;
}
}
}
void DeleteFd(struct pollfd fds[],int fd)
{
int i = 0;
for(;i < MAXFD;i++)
{
if(fds[i].fd == fd)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
break;
}
}
}
int main()
{
int sockfd = Createsockfd(6000,"127.0.0.1");
assert(sockfd != -1);
struct polled fds[MAXFD];
Init(fds);
add_fds(fds,sockfd);
while(1)
{
int n = poll(fds,MAXFD,-1);//永久阻塞,直到事件發生
if(n < 0)
{
perror("poll error\n");
}
else if(n == 0)
{
printf("time out\n");
}
else
{
int i = 0;
for(;i < MAXFD;i++)
{
if(fds[i].fd == -1)
{
continue;
}
if(fds[i].revents & POLLRDHUP)
{
close(fds[i].fd);
DeleteFd(fds,fds[i].fd);
}
if(fds[i].revent & POLLIN)
{
if(fds[i].fd == sockfd)
{
struct sockaddr_in cli;
int len = sizeof(cli);
int c = accept(fds[i].fd,(struct sockaddr*)&cli,&len);
if(c < 0)
{
continue;
}
add_fds(fds,c);
}
else
{
char buff[128] = {0};
int num = recv(fds[i].fd,buff,127,0);
if(num > 0)
{
printf("recv(%d) = %s\n",fds[i].fd,buff);
send(fds[i].fd,"OK",2,0);
}
}
}
}
}
}
}
客戶端同select客戶端代碼;
結果:
poll的優點:
- 將用戶關注的文件描述符的事件單獨表示,可關注更多的事件類型。
- 將用戶傳遞的和內核修改的分開,每次調用poll之前,不需要重新設置。
- poll函數沒有最大文件描述符的限制。
poll的缺點:
- 每次調用都需要將用戶空間數組拷貝到內核空間。
- 每次返回都需要將所有的文件描述符拷貝到用戶空間數組中,無論是否就緒。
- 返回的是所有就緒和未就緒的文件描述符,搜索就緒文件描述符的時間複雜度爲O(n)。
epoll
epoll是在2.6內核中提出的,是select()和poll()的增強版本,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的複製只需一次。epoll底層是用紅黑樹和鏈表實現的。
#include <sys/epoll.h>
int epoll_create(size);
功能:該函數創建內核事件表;返回epoll專用的文件描述符
參數:
size:用來告訴內核這個監聽的數目一共有多大,參數size並不是限制了epoll所監聽的文件描述符的最大值個數,只是對內核初始內部數據結構的一個建議。自從linux2.6.8之後,size參數是被忽略的。
返回值:
成功:epoll專用的文件描述符
失敗:-1
#include <sys/epoll.h>
int epoll_ctl(int epollfd,int cmd,int fd,struct epoll_event *event);
功能:epoll的事件註冊函數,對內核事件表中的文件描述符對應的事件進行增刪改。
參數:
epollfd:epoll的專用文件描述符,epoll_create()的返回值。
cmd:表示需要設置的操作,用以下三個宏表示:
EPOLL_CTL_ADD 添加新的文件描述符
EPOLL_CTL_MOD 修改已經註冊的文件描述符的監聽事件
EPOLL_CTL_DEL 刪除一個文件描述符
fd:需要監聽的文件描述符。
event:作用是指定事件,結構如下:
struct epoll_event
{
_uint32_t events; /*epoll事件*/
epoll_data_t data; /*用戶數據*/
};
typedef union epoll_data
{
void* ptr;
int fd; /*用戶關注的事件*/
uint32_t u32;
uint64_t u64;
}epoll_data_t;
返回值:
成功:0
失敗:-1
#include <sys/epoll.h>
int epoll_wait(int epollfd,struct epoll_event *revent,int maxevents,int timeout);
功能:此函數如果檢測到事件,就將所有就緒事件從內核事件表中(由epollfd中的參數指定)複製到它的第二個參數revent指定的數組中,這個數組只輸出epoll_wait檢測出的就緒事件。所以,搜索就緒文件描述符的時間複雜度爲O(1)。
參數:
epollfd:epoll專用的文件描述符,epoll_create()的返回值。
revent:分配好的epoll_event結構體數組,epoll將會把發生的事件賦值到revent數組中(revent不可以是空指針,內核只負責把數據複製到這個events數組中,不會幫助我們在用戶態中分配內存)。
maxevents:maxevents告知內核這個revent有多大。
timeout:超時時間,單位爲毫秒,爲-1時,函數爲阻塞。
返回值:
成功:返回需要處理事件數目,如返回0,表示超時
失敗:返回-1
文件描述符的操作有兩種模式:LT和ET。LT模式時默認模式。區別如下:
LT模式:epoll_wait檢測到就緒事件,將其通知應用程序,應用程序可以不立即處理或處理不完,下一次epoll_wait依舊會通知這一事件。
ET模式:epoll_wait檢測到就緒事件,將其通知應用程序,應用程序必須立即處理並且必須將事件處理完,如果未處理或者未處理完成,則下一次epoll_wait並不會通知這個就緒事件。
ET模式比LT模式高效的原因:
- 同一個事件ET只會通知一次,LT會多次通知,epoll_wait函數調用多次。epoll_wait調用需要消耗時間。
- LT模式下。epoll_wait因爲上一個時間未處理完而直接返回,造成對後續事件的延遲處理。
- ET模式內核實現時,將rdlist中的就緒的文件描述符通過txlist拷貝給用戶空間,並且rdlist會被清空。
LT模式內核實現時,將rdlist中的就緒文件描述符通過txlist拷貝給用戶空間,rdlist也會被清空,但是會將未處理的或處理未完成的文件描述符又返回給rdlist
而epoll採用高效的ET模式,通過將文件描述符設置成非阻塞而實現ET模式。
使用多客戶端的網絡通信實例(服務器利用poll函數實現I/O複用)
服務器端:
#define _GNU_SOURCE
#include <stdio.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <string.h>
#define MAXFD 10
Createfd(int port,char *ip)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
return -1;
}
struct sockaddr_in ser;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(port);
ser.sin_addr.s_addr = inet_addr(ip);
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
if(res == -1)
{
return -1;
}
listen(sockfd,5);
return sockfd;
}
void epoll_add(int epfd,int fd)
{
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = epfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
{
perror("EPOLL_CTL_ADD ERROR\n");
}
}
void epoll_del(int epfd,int fd)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
{
perror("EPOLL_CTL_DEL ERROR\n");
}
}
int main()
{
int sockfd = Createfd(6000,"127.0.0.1");
assert(sockfd != -1);
int epfd = epoll_create(MAXFD);
struct epoll_event events[MAXFD];
epoll_add(epfd,sockfd);
while(1)
{
int n = epoll_wait(epfd,events,MAXFD,5000);
if(n == -1)
{
perror("epoll_wait error\n");
}
else if(n == 0)
{
printf("time out\n");
continue;
}
else
{
int i = 0;
for(;i < n;i++)
{
int fd = events[i].data.fd;
if(fd & EPOLLRDHUP)
{
close(fd);
epoll_del(epfd,fd);
}
if(fd == sockfd)
{
struct sockaddr_in cli;
int len = sizeof(cli);
int c = accept(sockfd,(struct sockaddr*)&cli,&len);
if(c < 0)
{
continue;
}
printf("accept = %d\n",c);
epoll_add(epfd,c);
}
else
{
char buff[128] = {0};
int num = recv(fd,buff,127,0);
if(num <= 0)
{
printf("one client over!");
epoll_del(epfd,fd);
close(fd);
}
else
{
printf("recv(%d) = %s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
}
}
}
客戶端同select客戶端代碼;
結果:
epoll的優點:
- 監視的描述符數量不受限制
- 時間類型更多
- 用戶關注的事件由內核維護,每次調用epoll_wait時,不需要將用戶空間數據拷貝到內核空間
- 每次epoll只返回就緒的文件描述符
- 用戶程序檢測就緒文件描述符的效率O(1)
- epoll的內核比select和poll高效,select和poll採用輪詢的方式,而epoll回調的方式
- epoll支持高效的ET模式
select、poll、epoll總結
- select通過三個結構分別表示可讀、可寫、異常事件;poll和epoll用一個short類型的變量表示關注的事件,事件類型更多
- select通過long類型的數組按位記錄文件描述符,最多關注1024個文件描述符,並且範圍是0-1023;poll和epoll都是通過一個int類型的fd表示文件描述符,poll通過用戶數組記錄所有文件描述符,epoll通過內核事件表來記錄,一般能達到系統允許打開的最大文件描述符
- select通過三個結構傳遞傳遞用戶關注的文件描述符,也是通過其返回就緒的和未就緒的文件描述符,所以每次調用select都必須重新設置三個結構體,而poll和epoll則不需要。poll將用戶關注的事件和內核反饋發生的事件分開表示,epoll通過數組返回就緒的內核事件表。
- select和poll返回的就緒和未就緒的文件描述符,檢測就緒文件描述符的時間複雜度爲O(n),epoll直接通過數組僅僅返回所有就緒的文件描述符,檢測就緒文件描述符的時間複雜度爲O(1)。
- select和poll採用輪詢的方式,epoll採用回調的方式
- select內核通過數組,poll內核使用鏈表,epoll內核是紅黑樹+鏈表
- select和poll僅僅支持LT模式,epoll支持高效的ET模式。
- select和poll都是單獨的函數,epoll是一組函數。
- select、poll每次調用都要把文件描述符集合從用戶態往內核態拷貝一次,而epoll只要拷貝一次,這也能節省不少的開銷