IO多路复用

IO多路复用

1、五种IO模型
  • 阻塞IO(BIO)

    服务器端创建socket文件描述符,调用bind将socket文件描述符与端口连接之后,就会调用listen等待客户端的连接,阻塞IO就是在调用listen以及连接客户端之后的read操作时,进程会一直阻塞,等待客户端连接或者客户端发送数据。

    特点:进程阻塞后,不会占用资源,可以响应其它操作,适用于并发量较小的网络应用

  • 非阻塞IO(NIO)

    与BIO相同创建socket,通过fcntl将socket设置为非阻塞,之后进入listen阶段,如果有客户端连接进来,服务端会将其加入一个list中保存,如果没有客户端连接也会继续执行,其通过轮询的方式查看list中的客户端有无数据发来。

    特点:不会阻塞在listen或read,但是轮询调用比较消耗CPU资源

  • 信号驱动IO

    应用程序通过向操作系统注册感兴趣的IO事件,然后进程就返回去做自己的事情,当操作系统这边的IO事件就绪时,就会发送信号告诉应用程序来取。

    特点:使用上有所限制,开发难度较大

  • 异步IO

    当进程发起一个I/O操作的时候,进程直接返回,当内核把整个I/O处理完之后,则会通知进程结果,如果I/O操作成功则直接获得数据

    特点:非常适合高性能高并发应用

  • I/O复用模型

    当拿到一堆文件描述符的时候,就调用某个函数告诉内核,等这些文件描述符中可以进行I/O读写操作的时候再返回,当这个函数返回后,直接就能知道哪些文件可以进行I/O文件操作

2、IO多路复用
2.1 什么是多路复用
  • 多路:多个socket网络连接
  • 复用:使用一个线程来检查多个文件描述符的就绪状态
  • 三种多路复用技术:select、poll、epoll
2.2 select复用技术
#include <sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);

1)nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值为1,应为文件描述符是从0开始计数的。
2)readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。
3)timeout参数用来设置select函数的超时时间

成功则返回就绪文件描述符的总数,若超时时间内都没任何文件描述符就绪,则返回0,失败则返回-1

readfds、writefds和exceptfds本质上是只包含一个整形数组的结构体,该数组的每一位标记了一个文件描述符,其通过以下函数来对该结构体进行操作:

FD_ZERO(fd_set *fdset);//清楚fdset的所有位
FD_SET(int fd,fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);//清楚fdset的位fd
int FD_ISSET(int fd,fd_set *fdset);//测试fdset的位fd是否被设置

select会将整个fdset传给操作系统,当有文件描述符需要进行I/O的时候,会将所有fdset返回,需要应用程序自己进行遍历。

程序示例

//socket文件描述符
int connfd;

fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);

while(1)
{
	FD_SET(connfd,&read_fds);
	FD_SET(connfd,&exception_fds);
	ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL);
	if(FD_ISSET(connfd,&read_fds))
	{
		ret = recv(connfd,buf,sizeof(buf)-1,0);
		if(ret <= 0)
		{
			break;
		}
		cout<<get bytes<<endl;
	}
	else if(FD_ISSET(connfd,&exception_fds))
	{
		ret = recv(connfd,buf,sizeof(buf)-1,MSG_OOB);
		if(ret <= 0)
		{
			break;
		}
		cout<<get bytes from oob<<endl;
	}
}

存在的问题:

1、每次调用select,都需要将被监控的fds集合从用户态拷贝到内核态,在高并发的场景下,消耗的资源很大

2、能监听的端口有限,32位机默认1024个,64位机默认2048个

3、在被监控的fds集合中,只要有一个数据可读,整个socket集合就会被遍历一次。

2.3 poll复用技术

poll和select技术类似,但是其用pollfd结构体替代了poll中的fd集合,因此解决了select中的问题2。pollfd结构体的定义如下:

struct pollfd
{
	int fd;//文件描述符
    short events;//监控的事件
    short revents;//监控中满足条件返回的事件
}
int poll(struct pollfd *fds,unsigned long nfds,int timeout);

1)fds参数指定struct pollfd类型的数组, 存储了待检测的文件描述符
2)nfds描述数组fds的大小
3)timeout参数用来设置select函数的超时时间

成功返回检测的集合中已经就绪的文件描述符的总数,失败返回-1

虽然poll解决了select的问题2,但是依然没有解决问题1和3。

2.4 epoll复用技术

epoll与select和poll不同,select和poll使用单个函数来完成任务,但是epoll使用一组函数来完成任务,它不用像select和poll一样需要反复将监控的文件描述符从内核态和用户态之间相互传递,其在内核创建一个事件表,并用一个文件描述符来标识这个事件表,其使用epoll_create函数来创建这个事件表:

int epoll_create(int size);
//size代表事件表需要多大

​ 然后通过epoll_ctl函数来对这个事件表进行操作,可以选择往事件表上注册事件、修改注册的事

件、删除注册的事件,其函数声明如下:

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *events);
//fd代表要操作的文件描述符
//op代表操作类型
//events的作用与pollfd类似

​ 最后就使用epoll_wait进行等待就行了,其声明如下:

int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);

epoll_wait函数如果检测到了事件,就会将就绪的事件全部复制到events指向的数组中,这个数组内只储存就绪的事件,不像poll和select将没就绪的事件也一起储存在一起,极大的提高了效率。

程序示例:

int ret = epoll_wait(epollfd,events,MAX_EVENTS_NUMBER,-1);
for(int i = 0;i < ret;i++)
{
	int sockfd = events[i].data.fd;
	//处理socket
}
2.5 epoll边缘触发和水平触发
  • 水平触发(LT)

    只要读缓冲区不为空,写缓冲区不满,每次epoll_wait都会返回就绪,其是epoll的默认工作方式

  • 边缘触发(ET)

    只有缓冲区的数据有变化,epoll_wait才会返回就绪,因此在ET模式下,每次需要将缓冲区的数据全部读完,不然下次如果缓冲区没有发生数据变化,就一直不会返回就绪了。

2.6 epoll、select和poll的区别

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值