一、什麼是IO多路複用
關於這部分內容請移步到下面的鏈接進行學習,本文主要介紹select,poll和epoll三種多路複用的實現。
https://www.zhihu.com/question/32163005
二、select的實現
2.1 select的原理:
user space創建一張存放文件描述符的表,(該表的大小爲1024個bit,受限於能打開文件個數),並且將需要監測的文件描述符在表內對應的bit置1,(如下圖,需要監測描述符值爲3和1020的兩個文件描述符,我們將監測表內的第3和第1020個bit置1,其他的都爲0),當調用select函數的時候,select函數會將該表拷貝到kernel space,然後進行輪訓操作,當某些文件不能進行IO操作時,kernel space會把傳來表內的位置0,能進行IO操作的則保留原來的1,然後通過select函數將表返回給user space,user space遍歷返回的表就能找到那個文件能進行IO操作。然後調用對應函數進行操作即可。
2.2 select函數的基本流程
使用select函數的基本流程:
(1)創建監聽描述符表
(2)把需要監聽的描述符放到表中(將相應的位置一)
(3)計算出被監聽的描述符中的最大值
(4)調用select函數
(5)select函數返回之後,遍歷被監聽表,找到能進行操作的文件,並調用相應函數
(6)重複調用用2、3、4、5步
2.3 相關函數介紹
select函數:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數含義:
nfds:指定測試的描述符個數,它的值是衆多待測試描述符中值最大的那個再加1。
readfds、writefds、exceptfds:是指定我們讓內核測試讀寫和異常測試條件的描述表,若無需監測某一條件,將其設置爲NULL即可。
timeout:設置內核等待時間。設置爲NULL時爲阻塞等待。
對文件描述符表操作函數:
void FD_ZERO(fd_set *fdset); //清空文件描述符監聽表
void FD_SET(int fd, fd_set *fdset); //將指定文件描述符添加到表內,表內對應位置1
void FD_CLR(int fd, fd_set *fdset); //將指定文件描述符從表內移除,表內對應位置0
void FD_ISSET(int fd, fd_set *fdset); //判斷指定文件是否可進行操作,能操作返回1,否則返回0
2.4 具體實現代碼
代碼爲socket一個服務端同時和多個客戶機進行通訊的實例。
客戶端代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SPORT 12000 //主機端口號
#define SIZE 200
#define ADDR "127.0.0.1"
int CreateConnect(const char* ip, int port);
int main(int argc, const char* argv[])
{
int sockfd = 0;
int len = 0;
struct sockaddr_in addr;
char str[SIZE] = {0};
sockfd = CreateConnect(ADDR, SPORT);
if ( 0 > sockfd)
{
perror("sockInit is failed");
return -1;
}
while (1)
{
fgets(str, SIZE - 1, stdin);
if (strncmp(str, "quit", 4) == 0)
{
break;
}
send(sockfd, str, strlen(str), 0);
if (0 < recv(sockfd, str, SIZE - 1, 0))
{
printf("get server data %s\n", str);
}
}
close(sockfd);
return 0;
}
int CreateConnect(const char* ip, int port)
{
int sockfd = 0;
int len = 0;
struct sockaddr_in addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("socket is failed");
return -1;
}
printf("sockfd is ok\n");
memset(&addr, 0, sizeof(len));
addr.sin_family = AF_INET;
addr.sin_port = htons(SPORT);
addr.sin_addr.s_addr = inet_addr(ADDR);
if (0 > connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)))
{
perror("connect is failed");
return -1;
}
printf("connect is ok\n");
return sockfd;
}
服務端代碼:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#define SPORT 12000
#define SIZE 200
#define ADDR "127.0.0.1"
int SockfdInit(const char* ip, int port);
int SelectTask(int fd, int* numfd, fd_set* readfds, fd_set* tempfds);
int main(int argc, const char* argv[])
{
int ret = 0;
int sockfd = 0;
int maxfd = 0;
fd_set rdfs;
fd_set tempfds;
sockfd = SockfdInit(ADDR, SPORT);
if (0 > sockfd)
{
perror("socket is failed");
return -1;
}
FD_ZERO(&rdfs); //初始化監聽表
FD_ZERO(&tempfds); //初始化備份表
FD_SET(sockfd, &rdfs); //將socket添加到表內進行監聽
FD_SET(sockfd, &tempfds);
maxfd = (maxfd > sockfd) ? (maxfd) : (sockfd); //獲取最大文件描述符的值
while (1)
{
printf("調用sele\n");
rdfs = tempfds; //注意select每次調用都會改變傳入參數,所以需要將參數進行備份
ret = select(maxfd + 1, &rdfs, NULL, NULL, NULL);
if (0 > ret)
{
perror("select is failed");
break;
}
else if (0 == ret)
{
perror("timeout");
break;
}
SelectTask(sockfd, &maxfd, &rdfs, &tempfds);
}
close(sockfd);
return 0;
}
int SockfdInit(const char* ip, int port)
{
if (NULL == ip)
{
printf("請輸入正確的ip地址");
return -1;
}
int ret = 0;
int len = 0;
int sockfd = 0;
struct sockaddr_in addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("sockfd is failed");
return -1;
}
printf("socket is ok\n");
int on = 1;
ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); //設置端口複用
if (0 > ret)
{
perror("setsockopt is failed");
return -1;
}
printf("setsockopt is ok\n");
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if (0 > bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)))
{
perror("bind is failed");
return -1;
}
printf("bind is ok\n");
if (0 > listen(sockfd, 5))
{
perror("listen is failed");
return -1;
}
printf("listen is ok\n");
return sockfd;
}
int SelectTask(int fd, int* numfd, fd_set* readfds, fd_set* tempfds)
{
int i = 0;
int len = 0;
int ret = 0;
int newfd = 0;
char str[SIZE] = {0};
struct sockaddr_in addr;
if (FD_ISSET(fd, readfds)) //先判斷是否爲socket能操作,因爲tcp通信等待連接有時間限制
{
len = sizeof(addr);
newfd = accept(fd, (struct sockaddr*)&addr, &len);
if (0 > newfd)
{
perror("accept is failed");
close(newfd);
return -1;
}
printf("accept is ok port = %d\n", ntohs(addr.sin_port));
FD_SET(newfd, tempfds); //將新連接的客戶端也添加到表內進行監聽
*numfd = (*numfd > newfd) ? (*numfd) : (newfd); //找到最大文件描述符
}
for (i = 0; i <= *numfd; i++) //遍歷返回的表,並將數據返回到對應的客戶端
{
if (FD_ISSET(i, readfds) && i != fd)
{
memset(str, 0, sizeof(str));
ret = recv(i, str, SIZE - 1, 0);
if (ret < 0)
{
perror("recv failed");
FD_CLR(i , tempfds);
close(i);
continue;
}
else if (ret == 0)
{
FD_CLR(i , tempfds);
close(i);
continue;
}
printf("get %d client data %s\n", i, str);
send(i, str, strlen(str), 0);
}
}
return 0;
}
這裏我們發現服務器端創建了兩個監聽表,rdfs和tempfds,調用selecet函數之前,先將tempfds表的數據傳給rdfs表,然後將rdfs表傳遞到select函數去調用select函數,並且在SelectTask函數中,每次對錶進行操作的時候,都是對tempfds表操作,這是因爲select函數每次調用的時候會改變傳入函數中的參數。我們需要將數據備份。
三、poll的原理
3.1 poll的原理
user space創建一個結構體數組,數組中每一個成員由文件描述符、當前監聽的事件、返回的事件三個元素構成(數組大小自己設置,每個元素就是一個文件),在調用poll之前,把前兩個元素寫好,然後調用poll,這個函數會拷貝表到kernel space,輪詢,在poll返回時,如果某個文件可以進行IO操作,那麼,它對應的“返回的事件”將被修改,user space再次遍歷返回的數組,查看數組中每一個成員對應在的“返回的事件”,然後,調用相應的函數進行操作。
struct pollfd
{
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
監聽事件events自己設置,判斷IO是否能操作時,就是判斷revents是否與我們設置的events相同即可。
往監聽數組中添加監聽文件和移除監聽文件流程如下圖所示:
3.2 poll的流程
使用poll函數的基本流程:
(1)創建監聽數組
(2)把需要監聽的文件放到數組中,並且設置好監聽事件
(3)計算監聽數組中,監聽事件存放在最大的索引值
(4)調用poll函數
(5)poll函數返回之後,遍歷被監聽數組,找到能進行操作的文件,並調用相應函數
(6)重複調用用4、5步
3.3 相關函數
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數:
fds:監聽數組
nfds:監聽數組的長度
timeout:等待時間時間的,單位爲ms
3.4 具體實現代碼
代碼爲socket一個服務端同時和多個客戶機進行通訊的實例。
客戶端代碼與上文中select舉例一致。
服務端代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <poll.h>
#define SPORT 12000
#define SIZE 200
#define ADDR "127.0.0.1"
int SockfdInit(const char* ip, int port);
int PollTask(struct pollfd *pfd, int* index, int maxnum);
int main(int argc, const char* argv[])
{
int i = 0;
int ret = 0;
int index = 0;
int sockfd = 0;
struct pollfd pfd[SIZE];
sockfd = SockfdInit(ADDR, SPORT);
if (0 > sockfd)
{
perror("socket is failed");
return -1;
}
for (i = 0; i < SIZE; i++) //初始化poll表
{
pfd[i].fd = -1;
pfd[i].events = POLLIN; //默認監聽時間都爲讀事件
}
pfd[0].fd = sockfd;
index++; //監聽數組內存放監聽事件的最大索引值
while (1)
{
printf("調用poll\n");
ret = poll(pfd, SIZE, 3000); //等待事件觸發事件爲3000ms
if (0 > ret)
{
perror("select is failed");
break;
}
else if (0 == ret)
{
perror("timeout");
continue;
}
PollTask(pfd, &index, SIZE);
}
close(sockfd);
return 0;
}
int SockfdInit(const char* ip, int port)
{
if (NULL == ip)
{
printf("請輸入正確的ip地址");
return -1;
}
int ret = 0;
int len = 0;
int sockfd = 0;
struct sockaddr_in addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("sockfd is failed");
return -1;
}
printf("socket is ok\n");
int on = 1;
ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if (0 > ret)
{
perror("setsockopt is failed");
return -1;
}
printf("setsockopt is ok\n");
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if (0 > bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)))
{
perror("bind is failed");
return -1;
}
printf("bind is ok\n");
if (0 > listen(sockfd, 5))
{
perror("listen is failed");
return -1;
}
printf("listen is ok\n");
return sockfd;
}
int PollTask(struct pollfd *pfd, int* index, int maxnum)
{
int i = 0;
int len = 0;
int ret = 0;
int newfd = 0;
char str[SIZE] = {0};
struct sockaddr_in addr;
if (pfd[0].revents & POLLIN)
{
len = sizeof(addr);
newfd = accept(pfd[0].fd, (struct sockaddr*)&addr, &len);
if (0 > newfd)
{
perror("accept is failed");
close(newfd);
return -1;
}
printf("accept is ok port = %d\n", ntohs(addr.sin_port));
if (*index >= maxnum) //計算監聽數組中存放事件的最大索引值
{
printf("polltable is full \n");
close(newfd);
}
else
{
for (i = 0; i < maxnum; i++)
{
if (pfd[i].fd == -1)
{
pfd[i].fd = newfd;
(*index)++;
break;
}
}
}
}
for (i = 1; i < maxnum; i++) //遍歷能操作的文件,並將數據原路返回
{
if (pfd[i].revents & POLLIN)
{
memset(str, 0, sizeof(str));
ret = recv(pfd[i].fd, str, SIZE - 1, 0);
if (ret < 0)
{
perror("recv failed");
close(pfd[i].fd);
pfd[i].fd = -1;
(*index)--;
break;
}
else if (ret == 0)
{
printf("client %d is closed\n", pfd[i].fd);
close(pfd[i].fd);
pfd[i].fd = -1;
(*index)--;
continue;
}
printf("get %d client data %s\n", i, str);
send(pfd[i].fd, str, strlen(str), 0);
}
}
return 0;
}
四、epoll的原理
創建epoll相關鏈表,返回epollFd,把需要監聽的文件描述符,及監聽的事件用結構體進行組合,把這個結構體加入到epollFd對應的鏈表中;開始監聽等待, 在kernel space輪詢時,發現某些文件可以進行IO操作時,kernel space會將這些文件放入到一個數組中,epoll等待返回時,把數組拷貝到user space,user space遍歷返回的數組,查看數組中每一個成員的“事件”,然後,調用相應的函數進行操作。
使用epoll的時候,我們創建一個文件描述符鏈表,每次調用epoll函數的時候,系統會遍歷這個鏈表,查看那個文件可以進行操作,並且將可以操作的文件存放到一個數組裏面,並且返回一個數組長度的值,然後我們遍歷該數組執行相應函數即可。
4.1 epoll流程
(1) 創建一個epoll相關的鏈表,返回epoll的文件描述符
(2)將監聽的文件描述符放在鏈表中
(3) 啓動epoll的監聽
(4) epoll返回時,將所有可以進行IO操作的文件放在一個數組中,用戶遍歷該數組進行相應的IO操作
(5)重複3、4步
4.2相關函數
int epoll_create(int size);
函數參數size無意義,可以隨意填寫。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函數參數:
epfd:監聽鏈表描述符
op:需要執行的操作 EPOLL_CTL_ADD給監聽鏈表中添加文件,EPOLL_CTL_DEL從監聽鏈表中移除文件
fd:需要操作的文件
event:需要監聽的事件,EPOLLIN文件輸入,EPOLLIN文件輸出
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函數參數:
epfd:監聽鏈表
events:用於存放能操作文件的數組
maxevents:存放能操作文件的數組最大長度
timeout:等待時間發生時間,單位ms
函數返回值爲能操作的事件的數量。
4.3 具體實現代碼
代碼爲socket一個服務端同時和多個客戶機進行通訊的實例。
客戶端代碼與上文中select舉例一致。
服務端代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/epoll.h>
#define SPORT 12000
#define SIZE 200
#define ADDR "127.0.0.1"
int SockfdInit(const char* ip, int port);
int EpollTask(int sofd, int epfd);
int getLink(int sofd, int epfd);
int main(int argc, const char* argv[])
{
int i = 0;
int ret = 0;
int sockfd = 0;
int epollfd = 0;
struct epoll_event event;
sockfd = SockfdInit(ADDR, SPORT);
if (0 > sockfd)
{
perror("socket is failed");
return -1;
}
epollfd = epoll_create(1);
if ( 0 > epollfd)
{
perror("epoll_create is failed");
return -1;
}
event.events = EPOLLIN;
event.data.fd = sockfd;
ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event); //將socket添加到監聽鏈表當中
if (0 > ret)
{
close(epollfd);
close(sockfd);
perror("epoll_ctl is failed");
return -1;
}
while (1)
{
printf("調用epoll_wait\n");
struct epoll_event epoll_arr[10]; //自己根據實際情況設置監聽數組的大小
ret = epoll_wait(epollfd, epoll_arr, 10, 15000);
if (0 > ret)
{
perror("epoll is failed");
break;
}
else if (0 == ret)
{
perror("timeout");
break;
}
for (i = 0; i < ret; i++)
{
if (EPOLLIN == epoll_arr[i].events)
{
if (epoll_arr[i].data.fd == sockfd) //有新的客戶端連接
{
getLink(epoll_arr[i].data.fd, epollfd);
}
else //接收到客戶端的數據,原路返回
{
EpollTask(epoll_arr[i].data.fd, epollfd);
}
}
}
}
close(sockfd);
return 0;
}
int SockfdInit(const char* ip, int port)
{
if (NULL == ip)
{
printf("請輸入正確的ip地址\n");
return -1;
}
int sockfd = 0;
struct sockaddr_in addr;
int len = sizeof(addr);
int ret = 0;
struct timeval tm =
{
.tv_sec = 3,
};
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("socket is failed");
return -1;
}
printf("sockfd is ok\n");
int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tm, sizeof(tm));
memset(&addr, 0, len);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if (0 > bind(sockfd, (struct sockaddr*)&addr, len))
{
perror("bind is failed");
return -1;
}
printf("bind is ok\n");
if (0 > listen(sockfd, 5))
{
perror("listen is failed");
return -1;
}
printf("listen is ok\n");
return sockfd;
}
int getLink(int sofd, int epfd)
{
int i = 0;
int len = 0;
int ret = 0;
int newfd = 0;
struct sockaddr_in addr;
struct epoll_event event;
len = sizeof(addr);
newfd = accept(sofd, (struct sockaddr*)&addr, &len);
if (0 > newfd)
{
perror("accept is failed");
close(newfd);
return -1;
}
printf("accept is ok port = %d\n", ntohs(addr.sin_port));
event.events = EPOLLIN;
event.data.fd = newfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &event);
return 0;
}
int EpollTask(int sofd, int epfd)
{
int i = 0;
int ret = 0;
char str[SIZE] = {0};
struct epoll_event event;
memset(str, 0, sizeof(str));
ret = recv(sofd, str, SIZE - 1, 0);
if (ret < 0)
{
perror("recv failed");
goto end;
}
else if (ret == 0)
{
goto end;
}
printf("get %d client data %s\n", sofd, str);
send(sofd, str, strlen(str), 0);
return 0;
end:
epoll_ctl(epfd, EPOLL_CTL_ADD, sofd, NULL);
close(sofd);
return -1;
}
倉促成文,不當之處,尚祈方家和讀者批評指正。聯繫郵箱[email protected]