一、什么是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;
}
仓促成文,不当之处,尚祈方家和读者批评指正。联系邮箱1772348223@qq.com