Linux之IO多路复用

一、什么是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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值