IO多路复用(一)

Wesley13
• 阅读 601

在上一篇博文中提到了五种IO模型,关于这五种IO模型可以参考博文IO模型浅析-阻塞、非阻塞、IO复用、信号驱动、异步IO、同步IO,本篇主要介绍IO多路复用的使用和编程。

IO多路复用的概念

多路复用是一种机制,可以用来监听多种描述符,如果其中任意一个描述符处于就绪的状态,就会返回消息给对应的进程通知其采取下一步的操作。

IO多路复用的优势

当进程需要等待多个描述符的时候,通常情况下进程会开启多个线程,每个线程等待一个描述符就绪,但是多路复用可以同时监听多个描述符,进程中无需开启线程,减少系统开销,在这种情况下多路复用的性能要比使用多线程的性能要好很多。

相关API介绍

在linux中,关于多路复用的使用,有三种不同的API,select、poll和epoll

Select介绍

select的使用需要引入sys/select.h头文件,API函数比较简单,函数原型如下:

int select (int __nfds, fd_set *__restrict __readfds,
           fd_set *__restrict __writefds,
           fd_set *__restrict __exceptfds,
           struct timeval *__restrict __timeout);

fd_set

其中有一个很重要的结构体fd_set,该结构体可以看作是一个描述符的集合,可以将fa_set看作是一个位图,类似于操作系统中的位图,其中每个整数的每一bit代表一个描述符,。

举个简单的例子,fd_set中元素的个数为2,初始化都为0,则fd_set中含有两个整数0,假设一个整数的长度8位(为了好举例子),则展开fd_set的结构就是 00000000 0000000,如果这个时候添加一个描述符为3,则对应fd_set编程 00000000 00001000,可以看到在这种情况下,第一个整数标记描述符07,第二个整数标记815,依次类推。

fd_set有四个关联的api

void FD_ZERO(fd_set *fdset) //清空fdset,将所有bit置为0
void FD_SET(int fd, fd_set *fdset) //将fd对应的bit置为1
void FD_CLR(int fd, fd_set *fdset) //将fd对应的bit置为0
void FD_ISSET(int fd, fd_set *fdset) //判断fd对应的bit是否为1,也就是fd是否就绪

select函数中存在三个fd_set集合,分别代表三种事件,__readfds表示读描述符集合,__writefds表示读描述符集合,__exceptfds表示读描述符集合,当对应的fd_set = NULL时,表示不监听该类描述符。

__nfds

__nfds是fd_set中最大的描述符+1,当调用select的时候,内核态会判断fd_set中描述符是否就绪,__nfds告诉内核最多判断到哪一个描述符。

timeval

struct timeval {
    long tv_sec;    //秒
    long tv_usec;    //微秒
}

参数__timeout指定select的工作方式:

  • __timeout= NULL,表示select永远等待下去,直到其中至少存在一个描述符就绪
  • __timeout结构体中秒或者微妙是一个大于0的整数,表示select等待一段固定的事件,若该短时间内未有描述符就绪则返回
  • __timeout= 0,表示不等待,直接返回

函数返回

select函数返回产生事件的描述符的数量,如果为-1表示产生错误

值得注意的是,比如用户态要监听描述符1和3的读事件,则将readset对应bit置为1,当调用select函数之后,若只有1描述符就绪,则readset对应bit为1,但是描述符3对应的位置为0,这就需要注意,每次调用select的时候,都需要重新初始化并赋值readset结构体,将需要监听的描述符对应的bit置为1,而不能直接使用readset,因为这个时候readset已经被内核改变了。

Poll介绍

select中,每个fd_set结构体最多只能标识1024个描述符,在poll中去掉了这种限制,使用poll需要引入头文件sys/poll.h,poll调用的API如下:

int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

pollfd

struct pollfd {
    int fd;                    // poll的文件描述符
    short int events;        // poll关心的事件类型
    short int revents;        // 发生的事件类型
  };

Poll使用结构体pollfd来指定一个需要监听的描述符,结构体中fd为需要监听的文件描述符,events为需要监听的事件类型,而revents为经过poll调用之后返回的事件类型,在调用poll的时候,一般会传入一个pollfd的结构体数组,数组的元素个数表示监控的描述符个数,所以pollfd相对于select,没有最大1024个描述符的限制。

事件类型有多种,在bits/poll.h中定义了多种事件类型,主要如下:

#define POLLIN        0x001        // 有数据可读
#define POLLPRI        0x002        // 有紧迫数据可读
#define POLLOUT        0x004        // 现在写数据不会导致阻塞

# define POLLRDNORM    0x040        // 有普通数据可读
# define POLLRDBAND    0x080        // 有优先数据可读
# define POLLWRNORM    0x100        // 写普通数据不会导致阻塞
# define POLLWRBAND    0x200        // 写优先数据不会导致阻塞

#define POLLERR        0x008        // 发生错误
#define POLLHUP        0x010        // 挂起
#define POLLNVAL    0x020        // 无效文件描述符

当一个文件描述符要同时监听读写事件时,可以写成 events = POLLIN | POLLOUT

可以看到,poll中使用结构体保存一个文件描述符关心的事件,而在select中,统一使用fd_set,一个fd_set中可以是所有需要监听读事件的文件描述符,也可以是所有需要写事件的文件描述符。

相比来说,poll比select更加的灵活,在调用poll之后,无需像select一样需要重新对文件描述符初始化,因为poll返回的事件写在了pollfd->revents成员中。

__fds

__fds的作用同select中的__nfds,表示pollfd数组中最大的下标索引

__timeout

  • __timeout = -1:poll阻塞直到有事件产生
  • __timeout = 0:poll立刻返回
  • __timeout != -1 && __timeout != 0:poll阻塞__timeout对应的时候,如果超过该时间没有事件产生则返回

函数返回

poll函数返回产生事件的描述符的数量,如果返回0表示超时,如果为-1表示产生错误

Epoll介绍

epoll中,使用一个描述符来管理多个文件描述符,使用epoll需要引入头文件sys/epoll.h,epoll相关的api函数如下:

int epoll_create (int __size);
int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);
int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);

epoll_event

typedef union epoll_data {
  void *ptr;     // 可以用改指针指向自定义的参数
  int fd;         // 可以用改成员指向epoll所监控的文件描述符
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event {
  uint32_t events;        // epoll事件
  epoll_data_t data;    // 用户数据
} __EPOLL_PACKED;

epoll_event结构体中,首先是一个events的整型变量,类似于pollfd->events,表示要监控的事件,events支持的事件类型在sys/epoll.h的头文件中,跟pollfd中的事件类型基本移植,如下,这里只写出一部分:

enum EPOLL_EVENTS {
    EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN     // 有数据可读
    EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI     // 有紧迫数据可读
    EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT     // 现在写数据不会导致阻塞
    EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM        // 有普通数据可读
    EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND        // 有优先数据可读
    EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM        // 写普通数据不会导致阻塞
    EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND        // 写优先数据不会导致阻塞
    ...
    EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR    // 发生错误
    EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP    // 挂起
    EPOLLRDHUP = 0x2000,
       ...
  };

epoll_event中的data指向一个共用体结构,可以用该共用体保存自定义的参数,或者指向被监控的文件描述符。

epoll_create

int epoll_create (int __size);

epoll_create函数创建一个epoll实例并返回,该实例可以用于监控__size个文件描述符

epoll_ctl

int epoll_ctl (int __epfd, int __op, int __fd, struct epoll_event *__event);

该函数用来向epoll中注册事件函数,其中__epfd为epoll_create返回的epoll实例,__op表示要进行的操作,__fd为要进行监控的文件描述符,__event要监控的事件。

__op可用的类型定义在sys/epoll.h头文件中,如下:

#define EPOLL_CTL_ADD 1        // 添加文件描述符
#define EPOLL_CTL_DEL 2        // 删除文件描述符
#define EPOLL_CTL_MOD 3        //    修改文件描述符(指的是epoll_ctl中传入的__event)

该函数如果调用成功返回0,否则返回-1。

epoll_wait

int epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);

epoll_wait类似与select中的select函数、poll中的poll函数,等待内核返回监听描述符的事件产生,其中__epfd是epoll_create创建的epoll实例,__events数组为epoll_wait要返回的已经产生的事件集合,其中第i个元素成员的__events[i]->data->fd表示产生该事件的描述符,__maxevents为希望返回的最大的事件数量(通常为__events的大小),__timeout和poll中的__timeout相同。该函数返回已经就绪的事件的数量,如果为-1表示出错。

select、poll、epoll比较

select和poll的机制基本相同,只不过poll没有select最大文件描述符的限制,在具体使用的时候,有如下缺点:

  • 每次调用select或者poll,都需要将监听的fd_set或者pollfd发送给内核态,如果需要监听大量的文件描述符,这样的效率是很低下的
  • 在内核态中,每次需要对传入的文件描述符进行轮询,查询是否有对应的事件产生。

epoll的高效在于将这些分开,首先epoll不是在每次调用epoll_wait的时候,将描述符传送给内核,而是在epoll_ctl的时候传送描述符给内核,当调用epoll_wait的收,不用每次都接收

不像select和poll使用一个单独的API函数,在epoll中,使用epoll_create创建一个epoll实例,然后当调用epoll_ctl新增监听描述符的时候,这个时候才将用户态的描述符发送到内核态,因为epoll_wait调用的频率肯定要比epoll_create的频率要高,所以当epoll_wait的时候无需传送任何描述符到用户态;

关于第二点,在内核态中,使用一个描述符就绪的链表,当描述符就绪的时候,在内核态中会使用回调函数,该函数会将对应的描述符添加入就绪链表中,那么当epoll_wait调用的时候,就不需要遍历所有的描述符查看是否有就绪的事件,而是直接查看链表是否为空。

总结

可以使用一个生活中的场景来对三者的区别做个总结,仍然接着笔者的上一篇博文IO模型浅析-阻塞、非阻塞、IO复用、信号驱动、异步IO、同步IO中吃饭的例子:

在这个例子中,服务员和餐厅代表内核,客户“你”就是用户态进程,可能觉得这个例子写的不好,在这里写下加深记忆。

select和poll:你去餐厅请客吃饭,你是个豪爽的人,点了很多菜,你告诉服务员对应种类的菜有多少上多少,服务员将菜名一一写在纸上。然后你开始问服务员饭菜有好了么,服务员看着你的菜单一大串,头皮发麻,于是按着菜单的顺序去厨房查看饭菜有没有好,如果菜没有好就划掉菜单中对应的菜,终于找出了所有已经烧好的饭菜,服务员把饭菜端给了你。可是这个时候菜单上只能看到已经准备好的菜了,没准备好的菜看不清了,你觉得这个服务员做事很傻逼,没办法将就点,谁让你性格好呢,于是你重新写了一份菜单(可能这个过程中你又想点一些新的菜或者删除一些菜)。接下来你又去问饭菜好没好,服务员又开始按照菜单的顺序去厨房查看饭菜有没有好。。。(select和poll的主要区别就在于,select中的菜单是有限的,而poll中的菜单是无限的,你可以点任意种类的菜)

epoll:你去餐厅请客吃饭,你是个豪爽的人,点了很多菜,你告诉服务员对应种类的菜有多少上多少,服务员将菜名一一录入到餐厅后台的菜单管理软件中,厨房的师傅烧好一道菜在管理软件中标记完成一下,然后在烧好的菜上挂上对应的桌号放在取菜区,这个时候你来问服务员饭菜有准备好的么,服务员于是查一下管理软件,有标记欸,于是从取菜区取出对应桌号的饭菜送给你,清空标记。过了段时间,你又想点一道新的菜,于是叫来服务员,服务员在菜单软件中添加一栏。接下来你又去问饭菜好没好,服务员又开始看菜单软件中是否有标记完成的信息。。。

另外关于epoll的高效还有很多细节,例如使用mmap将用户空间和内核空间的地址映射到同一块物理内存地址,使用红黑树存储要监听的事件等等,具体的细节可以参考博文select、poll、epoll之间的区别总结整理高并发网络编程之epoll详解Linux下的I/O复用与epoll详解彻底学会使用epoll(一)——ET模式实现分析等几篇文章。

接下来使用select、pol、epoll实现一个tcp反射程序

参考资料

UNIX网络变成卷1:套接字联网API

select、poll、epoll之间的区别总结整理

高并发网络编程之epoll详解

Linux下的I/O复用与epoll详解

作者:yearsj

转载请注明出处:https://www.cnblogs.com/yearsj/p/9647135.html segmentfault对应博文:https://segmentfault.com/a/1190000016400053

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
java并发编程实践 笔记 2017
\TOC\javaIO模型BIO:JDK1.4之前的IO,阻塞IONIO:linux多路复用技术(select模式)实现IO事件的轮询方式:同步非阻塞的模式,这种方式目前是主流的网络通信模式Mina,netty网络通信框架AIO:jdk1.7
Wesley13 Wesley13
3年前
IO模式与现实中的例子
前言关于IO模式的区别,网络上的文章一搜一大把,但每次阅读时总觉得相当晦涩而且老容易混淆,俗话说好记性不如烂笔头,所以干脆自己写一篇便于自己理解的文章以此帮助记忆和理解,不对之处还请轻喷,先谢谢!1)同步阻塞IO(BlockingIO)BIO2)同步非阻塞IO(NonblockingIO)3)IO多路复用(IOMultipl
Wesley13 Wesley13
3年前
IO模型(BIO,NIO,AIO)及其区别
BIO:同步阻塞IONIO:同步非阻塞IOAIO:异步非阻塞IO先弄清楚同步、异步,阻塞、非阻塞概念。io操作分为两部分,发起io请求,和io数据读写。阻塞、非阻塞主要是针对线程发起io请求后,是否立即返回来定义的,立即返回称为非阻塞io,否则称为阻塞io。同步、异步主要针对io数据读写来定义的,读写数据过程中不阻塞线程称为异步io
Wesley13 Wesley13
3年前
IO复用-阻塞IO-非阻塞IO-同步IO-异步IO
本文是对《UNIX网络编程卷1》第6章的总结。一、            什么是IO复用?它是内核提供的一种同时监控多个文件描述符状态改变的一种能力;例如当进程需要操作多个IO相关描述符时(例如服务器程序要同时查看监听socket和大量业务socket是否有数据到来),需要内核能够监控这许多描述符,一旦这些描述符有就绪(或者状态改变了)就告
Wesley13 Wesley13
3年前
BIO、NIO、AIO、多路复用IO的区别(图解)
原文地址:blog.csdn.net/lzb348110175/article/details/98941378学习之前,我们先来了解一下IO模型:    ①同步阻塞IO(BlockingIO):即传统的IO模型。    ②同步非阻塞IO(NonblockingIO):默认创建的socket都是阻塞的,非阻塞
Wesley13 Wesley13
3年前
NIO
1、简介1.1Java中的IO介绍1.BIO:BlockingIO,同步式阻塞式IO,即传统的IO,是java中最早期的流2.NIO:NonBlockingIO,又称NewIO,同步式非阻塞IO,是JDK1.4提供的流3.AIO:AsynchronousIO,异步是非阻塞IO,可以认为是NIO的二代版
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
3年前
Eureka Server 开启Spring Security Basic认证
!Desktop(https://uploadimages.jianshu.io/upload_images/98242475ce94f98ae00f42f.jpg?imageMogr2/autoorient/strip%7CimageView2/2/w/1240)文章共503字,阅读大约需要2分钟!概述