當從一個文件描述符進行讀寫操做時,accept、read、write這些函數會阻塞I/O。在這種會阻塞I/O的操做好處是不會佔用cpu寶貴的時間片,可是若是須要對多個描述符操做時,阻塞會使同一時刻只能處理一個操做,從而使程序的執行效率大大下降。一種解決辦法是使用多線程或多進程操做,可是這浪費大量的資源。另外一種解決辦法是採用非阻塞、忙輪詢,這種辦法提升了程序的執行效率,缺點是須要佔用更多的cpu和系統資源。因此,最終的解決辦法是採用IO多路轉接技術。linux
IO多路轉接是先構造一個關於文件描述符的列表,將要監聽的描述符添加到這個列表中。而後調用一個阻塞函數用來監聽這個表中的文件描述符,直到這個表中有描述符要進行IO操做時,這個函數返回給進程有哪些描述符要進行操做。從而使一個進程能完成對多個描述符的操做。而函數對描述符的檢測操做都是由系統內核完成的。數組
linux下經常使用的IO轉接技術有:select、poll和epoll。服務器
select:多線程
頭文件:#include <sys/select.h>、#include <sys/time.h>、#include <sys/types.h>、#include <unistd.h>socket
函數:函數
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);ui
nfds:要檢測的文件描述符中最大的fd+1,nfds最大值爲1024。select最多隻能檢測1024個文件描述符。spa
readfds:讀集合。讀緩衝區中有數據時,readfds寫入數據。fd_set文件描述符集類型,具體實現見下面。線程
writefds:寫集合。一般設爲NULL。code
exceptfds:異常集合。一般設爲NULL。
timeout:設置超時返回。爲NULL時只有檢測到fd變化時返回。struct timeval a; a.tv_sec=10; a.tv_usec=0;
返回值:成功返回要操做的描述符個數,超時返回0,失敗返回-1。
select最多隻能檢測1024個文件描述符,是因爲fd_set在內核代碼中的設置所限制
1 //部分fd_set的內核代碼 2 3 #define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS) 4 #define __FD_SETSIZE 1024 5 #define __NFDBITS (8 * sizeof(unsigned long)) 6 typedef __kernel_fd_set fd_set; 7 typedef struct { 8 unsigned long fds_bits [__FDSET_LONGS]; 9 } __kernel_fd_set;
void FD_CLR(int fd, fd_set *set); 從set集合中刪除文件描述符fd。
int FD_ISSET(int fd, fd_set *set); 判斷文件描述符fd是否在set集合中。
void FD_SET(int fd, fd_set *set); 將fd添加到set集合中。
void FD_ZERO(fd_set *set); 清空set集合。
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <sys/socket.h> 5 #include <arpa/inet.h> 6 #include <string.h> 7 #include <unistd.h> 8 #include <sys/select.h> 9 #include <sys/time.h> 10 #include <stdlib.h> 11 int main() 12 { 13 int fd=socket(AF_INET,SOCK_STREAM,0); 14 struct sockaddr_in serv; 15 memset(&serv,0,sizeof(serv)); 16 serv.sin_addr.s_addr=htonl(INADDR_ANY); 17 serv.sin_port=htons(8888); 18 serv.sin_family=AF_INET; 19 bind(fd,(struct sockaddr*)&serv,sizeof(serv)); 20 21 listen(fd,20); 22 23 struct sockaddr_in client; 24 socklen_t cli_len=sizeof(client); 25 int maxfd=fd; 26 fd_set reads, temp; 27 FD_ZERO(&reads); 28 FD_SET(fd,&reads); 29 while(1) 30 { 31 temp=reads; 32 int ret=select(maxfd+1,&temp,NULL,NULL,NULL); 33 if(-1==ret) 34 { 35 perror("select error"); 36 exit(1); 37 } 38 //客戶端發起鏈接 39 if(FD_ISSET(fd,&temp)) 40 { 41 //接受鏈接 42 int cfd=accept(fd,(struct sockaddr*)&client,&cli_len); 43 if(cfd==-1) 44 { 45 perror("accept error"); 46 exit(1); 47 } 48 FD_SET(cfd,&reads); 49 //更新最大文件描述符 50 maxfd=maxfd<cfd?cfd:maxfd; 51 52 } 53 for(int i=fd+1;i<=maxfd;++i) 54 { 55 if(FD_ISSET(i,&temp)) 56 { 57 char buf[1024]={0}; 58 int len=recv(i,buf,sizeof(buf),0); 59 if(len==-1) 60 { 61 perror("recv error"); 62 exit(1); 63 64 } 65 else if(len==0) 66 { 67 printf("客戶端斷開鏈接\n"); 68 close(i); 69 70 FD_CLR(i,&reads); 71 } 72 else 73 { 74 printf("recv buf: %s\n",buf); 75 send(i,buf,strlen(buf)+1,0); 76 } 77 } 78 } 79 } 80 close(fd); 81 return 0; 82 }
poll:
頭文件:#include <poll.h>
函數:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:數組地址。內核檢測fds中的文件描述符。
nfds:數組的最大長度,數組中最後有效元素的下標+1。
timeout:超時返回,-1永久阻塞,0不阻塞調用後當即返回,>0等待的時長,單位毫秒。
返回值:成功返回要操做的個數,失敗返回-1。
struct pollfd { int fd; /*文件描述符*/ short events; /*等待的事件*/ short revents; /*實際發生的事件,內核給的反饋*/ }
pollfd經常使用事件:讀事件,POLLIN;寫事件,POLLOUT;錯誤事件,POLLERR(不能做爲events的值);
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 #include <string.h> 6 #include <sys/socket.h> 7 #include <arpa/inet.h> 8 #include <ctype.h> 9 #include <poll.h> 10 11 #define SERV_PORT 8989 12 13 int main(int argc, const char* argv[]) 14 { 15 int lfd, cfd; 16 struct sockaddr_in serv_addr, clien_addr; 17 int serv_len, clien_len; 18 19 // 建立套接字 20 lfd = socket(AF_INET, SOCK_STREAM, 0); 21 // 初始化服務器 sockaddr_in 22 memset(&serv_addr, 0, sizeof(serv_addr)); 23 serv_addr.sin_family = AF_INET; // 地址族 24 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽本機全部的IP 25 serv_addr.sin_port = htons(SERV_PORT); // 設置端口 26 serv_len = sizeof(serv_addr); 27 // 綁定IP和端口 28 bind(lfd, (struct sockaddr*)&serv_addr, serv_len); 29 30 // 設置同時監聽的最大個數 31 listen(lfd, 36); 32 printf("Start accept ......\n"); 33 34 // poll結構體 35 struct pollfd allfd[1024]; 36 int max_index = 0; 37 // init 38 for(int i=0; i<1024; ++i) 39 { 40 allfd[i].fd = -1; 41 } 42 allfd[0].fd = lfd; 43 allfd[0].events = POLLIN; 44 45 while(1) 46 { 47 int i = 0; 48 int ret = poll(allfd, max_index+1, -1); 49 if(ret == -1) 50 { 51 perror("poll error"); 52 exit(1); 53 } 54 55 // 判斷是否有鏈接請求 56 if(allfd[0].revents & POLLIN) 57 { 58 clien_len = sizeof(clien_addr); 59 // 接受鏈接請求 60 int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len); 61 printf("============\n"); 62 63 // cfd添加到poll數組 64 for(i=0; i<1024; ++i) 65 { 66 if(allfd[i].fd == -1) 67 { 68 allfd[i].fd = cfd; 69 break; 70 } 71 } 72 // 更新最後一個元素的下標 73 max_index = max_index < i ? i : max_index; 74 } 75 76 // 遍歷數組 77 for(i=1; i<=max_index; ++i) 78 { 79 int fd = allfd[i].fd; 80 if(fd == -1) 81 { 82 continue; 83 } 84 if(allfd[i].revents & POLLIN) 85 { 86 // 接受數據 87 char buf[1024] = {0}; 88 int len = recv(fd, buf, sizeof(buf), 0); 89 if(len == -1) 90 { 91 perror("recv error"); 92 exit(1); 93 } 94 else if(len == 0) 95 { 96 allfd[i].fd = -1; 97 close(fd); 98 printf("客戶端已經主動斷開鏈接。。。\n"); 99 } 100 else 101 { 102 printf("recv buf = %s\n", buf); 103 for(int k=0; k<len; ++k) 104 { 105 buf[k] = toupper(buf[k]); 106 } 107 printf("buf toupper: %s\n", buf); 108 send(fd, buf, strlen(buf)+1, 0); 109 } 110 111 } 112 113 } 114 } 115 116 close(lfd); 117 return 0; 118 }
select和poll雖然沒有前面幾種方法的缺點,可是select和poll只返回個數,不會告訴進程具體是哪幾個描述符要操做, 並且select和poll最多隻能檢測1024個。select每次調用時,都須要把fd集合從用戶態和內核態之間相互拷貝,這在fd不少時會消耗大量資源。
epoll檢測的個數沒有限制,它在內部構造維護了紅黑樹,減小了資源的消耗。
epoll:
頭文件:#include <sys/epoll.h>
函數:
int epoll_create(int size); 生成epoll專用的文件描述符,size:epoll上能關注的最大描述符個數。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create生成的文件描述符。
op:選項,EPOLL_CTL_ADD 註冊,EPOLL_CTL_MOD 修改,EPOLL_CTL_DEL 刪除。
fd:關聯的文件描述符。
event:告訴內核要監聽的事件
返回值:成功返回0,失敗返回-1。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 等待IO事件發生,能夠設置阻塞。
epfd:要檢測的句柄。
events:回傳待處理的數組。
maxevents:events的大小。
timeout:超時返回。-1永久阻塞;0當即返回;>0超時時間。
1 typedef union epoll_data { 2 void *ptr; 3 int fd; 4 uint32_t u32; 5 uint64_t u64; 6 } epoll_data_t; 7 8 struct epoll_event { 9 uint32_t events; /* Epoll events */ 10 epoll_data_t data; /* User data variable */ 11 };
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <sys/types.h> 5 #include <string.h> 6 #include <sys/socket.h> 7 #include <arpa/inet.h> 8 #include <ctype.h> 9 #include <sys/epoll.h> 10 11 12 int main(int argc, const char* argv[]) 13 { 14 if(argc < 2) 15 { 16 printf("eg: ./a.out port\n"); 17 exit(1); 18 } 19 struct sockaddr_in serv_addr; 20 socklen_t serv_len = sizeof(serv_addr); 21 int port = atoi(argv[1]); 22 23 // 建立套接字 24 int lfd = socket(AF_INET, SOCK_STREAM, 0); 25 // 初始化服務器 sockaddr_in 26 memset(&serv_addr, 0, serv_len); 27 serv_addr.sin_family = AF_INET; // 地址族 28 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽本機全部的IP 29 serv_addr.sin_port = htons(port); // 設置端口 30 // 綁定IP和端口 31 bind(lfd, (struct sockaddr*)&serv_addr, serv_len); 32 33 // 設置同時監聽的最大個數 34 listen(lfd, 36); 35 printf("Start accept ......\n"); 36 37 struct sockaddr_in client_addr; 38 socklen_t cli_len = sizeof(client_addr); 39 40 // 建立epoll樹根節點 41 int epfd = epoll_create(2000); 42 // 初始化epoll樹 43 struct epoll_event ev; 44 ev.events = EPOLLIN; 45 ev.data.fd = lfd; 46 epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); 47 48 struct epoll_event all[2000]; 49 while(1) 50 { 51 // 使用epoll通知內核fd 文件IO檢測 52 int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1); 53 54 // 遍歷all數組中的前ret個元素 55 for(int i=0; i<ret; ++i) 56 { 57 int fd = all[i].data.fd; 58 // 判斷是否有新鏈接 59 if(fd == lfd) 60 { 61 // 接受鏈接請求 62 int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len); 63 if(cfd == -1) 64 { 65 perror("accept error"); 66 exit(1); 67 } 68 // 將新獲得的cfd掛到樹上 69 struct epoll_event temp; 70 temp.events = EPOLLIN; 71 temp.data.fd = cfd; 72 epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp); 73 74 // 打印客戶端信息 75 char ip[64] = {0}; 76 printf("New Client IP: %s, Port: %d\n", 77 inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)), 78 ntohs(client_addr.sin_port)); 79 80 } 81 else 82 { 83 // 處理已經鏈接的客戶端發送過來的數據 84 if(!all[i].events & EPOLLIN) 85 { 86 continue; 87 } 88 89 // 讀數據 90 char buf[1024] = {0}; 91 int len = recv(fd, buf, sizeof(buf), 0); 92 if(len == -1) 93 { 94 perror("recv error"); 95 exit(1); 96 } 97 else if(len == 0) 98 { 99 printf("client disconnected ....\n"); 100 // fd從epoll樹上刪除 101 ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); 102 if(ret == -1) 103 { 104 perror("epoll_ctl - del error"); 105 exit(1); 106 } 107 close(fd); 108 109 } 110 else 111 { 112 printf(" recv buf: %s\n", buf); 113 write(fd, buf, len); 114 } 115 } 116 } 117 } 118 119 close(lfd); 120 return 0; 121 }
epoll三種工做模式:
水平觸發:epoll默認工做模式,只要fd對應的緩衝區有數據,epoll_wait就會返回。epoll_wait調用次數越多,系統開銷越大。
邊沿觸發:fd默認是阻塞的,客戶端發送一次數據epoll_wait就返回一次,無論數據是否讀完。若是要讀完數據,能夠循環讀取,可是recv會阻塞,解決方法是將fd設置爲非阻塞。
邊沿非阻塞觸發:將fd設置爲非阻塞(open下設置O_NONBLOCK,或者利用fcntl()函數)。效率最高,能夠將緩衝區數據徹底讀完。