【I/O多路复用】

I/O

I/O复用是基于一个单进程或单线程的一个执行流当中监控多个输入输出流的技术(网络套接字或者文件描述符进行监控)。单进程或单线程,允许多个用户对单进程发起连接进行I/O事件的处理。不在需要每一个连接创建一个独立的进程或线程单独服务,减少了操作系统的资源消耗和提高了运行效率。让操作系统进行资源的等待,一旦某一个文件描述符的读或写事件就绪,就执行相应的I/O事件。减少等待的时间来提高效率。再非阻塞的情况下,即使I/O条件不满足,也返回一个错误码EWOULDBLOCK,然后进程继续执行其他的任务,并不会进行阻塞。

实现I/O多路复用

实现I/O多路有三种方法,select,poll,和epoll。实现的原理都是将多个描述符进行监听,当某一个描述符的读或写事件就绪时,OS就会进行回调,用户层通过对所有的文件描述符进行循环遍历,执行对应的I/O事件。

select

select使用一个fd_set的数据类型来表示多个描述符。每个比特位表示描述符的数字,比特位的0和1表示事件的是否就绪,相当于一个位图。select可以关心三种事件进行关心,读,写和异常事件(超时)的关心。对于select这三种事件都有各自的fe_set数据类型所关心的事件变量。当有事件就绪时,便返回到用户层,用户层通过遍历保存了描述符的数组进行遍历,执行对应的I/O事件。当有新的网络连接到来时,连接先不会进行I/O的处理,而是先将新连接的套接字给select进行事件监控,不在需要等待对方发送数据到自己时这段时间回阻塞,一旦事件就绪了,系统调用就会通知上层。一旦accept成功,不能直接进行I/O处理,因为数据可能没有就绪,如果直接调用recv或read可能会导致阻塞或者其他连接无法通信。服务器调用select函数后,会进入无限循环,监测两种事件,一种是对端发起新的连接,一种是已经连接好的描述符事件已经就绪。每次有新连接的描述符,就添加到用户层管理的数组和内核态fd_set类型的关心读写或异常的变量中。每当有一个连接关闭,就把相应的描述符关闭然后置为无效。

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

nfds表示描述符中最大的描述符值加1。
3个fd_set的输入输出型参数对应读事件,写事件,异常事件。
timeout是设置select对某个描述符得等待时长,如果位nullptr,则阻塞等待事件,直到有事件就绪。如果timeout设置{0,0}表示非阻塞的轮询等待。{num,0}表示在0到num时间内没有事件到来则进行返回。返回值是大于一个位0的数,则表示有多少个描述符读写或异常数据就绪了,0为表示超时发送,事件的状态没有方式改变。-1表示出错。

//处理描述机会的宏
void FD_ZERO(fd_set *set);/*将事件的描述符集全部清空*/
void FD_SET(int fd, fd_set *set);/*将文件描述符设置到集合中,表示对该文件描述符的事件关心*/
void FD_CLR(int fd, fd_set *set);/*将对应的文件描述符在集合中清理*/
int  FD_ISSET(int fd, fd_set *set);/*判断该文件描述符是否在集合中*/

在这里插入图片描述

事件就绪的条件

事件就绪的条件有多种。
1.接收缓冲区中的字节数大于或等于低水位标记(SO_RCVLOWAT),此时可以无阻塞地读取内核文件描述符,并且返回值大于0。
2.发送缓冲区中的可用字节数大于或等于低水位标记(SO_SNDLOWAT),可以无阻塞地写,并且返回值大于0。
3.套接字是一个监听套接字,并且完成连接数不为0,这样的套接字的accept不会阻塞。
4.连接的读半关闭,也就是收到FIN的TCP连接,对这样的套接字的读操作不阻塞,并返回0。写半关闭的时候,写操作将产生SIGPIPE的信号。
5.socket使用非阻塞connect连接成功或失败之后。

优缺点

优点:可以对多个描述符进行事件的监控,提高I/O效率。
缺点:因为是使用一个fd_set的数据类型对于描述符的,该类型的能关心的事件只有1024个描述符,有上限的问题。每次有新的事件到来,都要用户层对事件进行重新设置,就要重用户态转到内核态。
内核态需要遍历描述符集,每次事件就绪,内核态就会返回一个已经就绪的文件描述符数量给用户态,然后用户态遍历文件描述符集找到哪个已经就绪。这造成效率低下。因为select要轮询的监听描述符的事件,造成OS底层对这些事件的关心,还有用户态和内核态的频繁切换,所以select监听的事件不宜过多。内核硬编码和效率的原因限制监听的事件有限。
说到底,select要关心文件描述符集合,就要轮询的方式来关心。

poll

poll也可以对多个描述符进行监控等待事件的就绪。在网络连接中,当有listen套接字创建成功的时候,可以对pollfd类型的数据进行填充。poll的相较于select,不在需要用户层在对事件的重新设置,事件的关心由一个结构体pollfd关心。且没有监控事件的上限。

 struct pollfd
 {
   int fd;			/* File descriptor to poll.  */
   short int events;		/* Types of events poller cares about.  */
   short int revents;		/* Types of events that actually occurred.  */
 };
int poll(struct pollfd *fds, int nfds, int timeout);

pollfd由一个整形文件描述符,和两个short int类型的事件组成,events和revents,表示内核关心的事件和内核返回给用户的事件。*fds表示的是一个数组的首元素,nfds表示个数,timeout为等待时间。因为poll的事件关心分离了,不像select需要每次都需要重置对事件的关心。

优缺点

优点:没有监控数量的限制,可以一点程度提高效率,对事件不再需要对poll的参数进行重置。
缺点:
在poll模型中,用户层和内核态都需要对事件进行遍历,关心事件是否有就绪。
每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。
当监听的数量很多,但就绪的事件很少,遍历的时候会是线性的时间,效率会有所降低。
说到底,poll要关心文件描述符集合,就要轮询的方式来关心。

epoll

epoll模型则可以管理在内存大小允许数量的文件描述符,epoll模型有两个重要的数据构,一个是红黑树,一个是队列,红黑树对文件描述符进行管理监听,当某个文件描述符的事件就绪时,就将文件描述符添加到队列,队列的都是事件就绪的文件描述符,操作系统对就绪队列的事件进行执行。

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//添加文件描述符到epoll模型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//监听epoll模型的文件描述符事件是否就绪

epoll_create是创建epoll模型。创建成功返回文件描述符,如果失败返回-1,错误码被设置。epoll模型是进程创建的,然后会有一个struct file* fd_array[]分配一个文件描述符,作为这个epoll模型的一个标识和该函数调用的返回值,让后续要监听的文件描述符通过这个返回值添加到epoll模型中。

epoll_create是对epoll模型的文件描述符进行操作,epfd是指的是epoll模型的第一个文件描述符,op是指三个操作的其中一个EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL。fd是指要操作的那一个文件描述符,events是指要epoll模型关心文件fd的事件。用户态转到内核态。函数成功返回0,错误返回-1,错误码被设置。

epoll_wait收集epoll模型中就绪的事件 。epfd是指的是epoll模型的第一个文件描述符,epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).。
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败。
epoll_wait会返回就绪文件的个数,然后会按文件描述符的大小复制到应用层的epoll_event(内核态拷贝数据到用户态),此时,只需要遍历就绪的文件描述符即可,提高效率,不会像select和poll那样遍历整个描述符集的数组。

epoll原理

epoll模型中,有一个红黑树和队列的数据结构帮助epoll模型I/O多路复用,将需要监听事件的文件描述符添加到红黑树中,这样就不会有文件描述符的限制影响,一旦某一个文件描述符的事件就绪,就将该文件描述符添加到一个就绪队列中,该队列中,所有的文件描述符都是已经事件就绪的,就等到被调度,有了这个就绪队列,事件是否有就绪可以在O(1)的时间复杂度知道,不会在像select和poll模型那样,内核态和用户态遍历文件描述符集合的数组,然后才知道有事件就绪,提高了效率。但是要知道哪些事件就绪,依旧会是O(N)的时间复杂度,epoll需要把就绪队列的时间全部拷贝在epoll_event类型的数组中进行遍历执行,这是无法避免的。

当网卡这个硬件有数据时,会触发硬件中断的信号。在epoll模型中,添加要监控的描述符到epoll模型的红黑树中时,底层的网卡驱动会有回调函数,一旦红黑树中的某个事件就绪时,就会调用回调函数ep_poll_callback,然后将事件就绪的文件描述符添加到就绪队列中,让epoll_wait进行获取。

  • epoll_create创建epoll模型
  • epoll_ctl将要监听文件描述符和要关心的事件添加到红黑树中
  • epoll_wait获取就绪队列的事件

在这里插入图片描述
epoll模型通过对红黑树就绪的事件进行回调,不再需要操作系统进行遍历来知道哪一个数据就绪,这让就提高了效率。因为红黑树采用的近似平衡的二叉树而且是有key-value的关系,所以文件描述符就可以作为键值对使用红黑树进行管理。在操作系统中,可以存在多个epoll模型,然后使用链表这样的数据结构进行管理。

优点

没有最大并发限制:epoll所支持的FD(文件描述符)上限是最大可以打开文件的数目。这使得epoll能够轻松处理成千上万的并发连接,而不会像select或poll那样受到文件描述符数量的限制。

高效处理大量并发连接:epoll通过红黑树管理所有的socket描述符,并且只返回那些活跃的、即准备就绪进行I/O操作的事件。这避免了遍历整个文件描述符集合的需求,从而显著提高了效率。

事件驱动机制:epoll采用事件驱动的方式来处理I/O事件,这意味着它只会在有事件发生时才通知应用程序。这与传统的轮询机制相比,极大地减少了无效的检查和等待时间,提高了系统的响应速度和吞吐量。

注意事项

  • 默认情况下,只要底层有就绪事件没有处理,epoll也会一直通知用户,也就是调用epoll_wait会一直成功返回,并将就绪的事件拷贝到我们传入的数组当中。
  • 需要注意的是,所谓的事件处理并不是调用epoll_wait将底层就绪队列中的就绪事件拷贝到用户层,比如当这里的读事件就绪后,我们应该调用accept获取底层建立好的连接,或调用recv读取客户端发来的数据,这才算是将读事件处理了。
  • 如果我们仅仅是调用epoll_wait将底层就绪队列当中的事件拷贝到应用层,那么这些就绪事件实际并没有被处理掉,底层注册的回调函数会被再次调用,将就绪的事件重新添加到就绪队列当中,本质原因就是我们实际并没有对底层就绪的数据进行读取,然后一直就绪。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值