sylar-from-scratch----IO协程调度模块(iomanager)

416 阅读7分钟

图片2.png

基础知识

IO多路复用

I/O模型指网络I/O模型;

  • 服务器如何管理连接,如何请求连接的措施,是用一个进程管理一个连接(PPC)、一个线程管理一个连接(TPC),亦或者是一个进程管理多个连接(Reactor);
  • 多路是指多个TCP连接(或多个channel),复用即指复用一个或者少量线程
  • 多个网络I/O复用一个或少量线程来处理这些连接;

多路复用的原理

I/O多路复用模型的基本思路: fd文件描述符:监控多个网络请求的线程,所有网络请求看作是一个fd来标识,这样只需要几个线程来完成数据状态的询问操作,当数据准备就绪后再分配随影的线程去读取数据,从而节省出大量的线程资源出来。 其实现方式就是通过select、poll、epoll来监控多个fd,应用线程通过调用select函数就可以同时监控多个fd,只要监控的fd中有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求无读取数据。从而达到不必为每个fd创建一个对应的监控线程,减少线程资源创建的目的。 高性能:单机性能(单服务器的性能发挥到极致,关键在于服务器采取的网络编程模型),多机性能(设计服务器集群方案);
管理连接、处理请求:I/O模型(阻塞、非阻塞、同步、异步)、进程模型(单进程、多进程、多线程)。

常见I/O模型

同步与异步

指内核通知用户线程方式;用户线程/进程和内核以传输层为分割线,传输层以上为用户进程,以下(包括传输层)为内核(处理所有通信细节,发送数据,等待确认,给无序到达的数据排序等,这四层是操作系统内核的一部分)。

  • 同步:用户线程发起I/O请求后需要等待轮询内核I/O操作,完成后才能继续执行;
  • 异步:用户线程发起I/O请求后仍然继续执行操作,当内核I/O操作完成后会通知用户线程,或会调用用户线程注册的回调函数。

阻塞与非阻塞

用户线程调用内核I/O操作的方式
阻塞:I/O操作需要彻底完成后才能返回用户空间;
非阻塞:I/O操作被调用后立即返回给用户一个状态值,无需等待I/O操作彻底返回。

(1)同步阻塞I/O

最简单的I/O模型,用户线程在内核进行I/O操作时被阻塞。用户线程通过调用read发起I/O读操作,由用户空间转至内核空间,内核等到数据包到达后,然后将接受的数据拷贝到用户空间,完成read操作。整个I/O请求过程中用户线程是被阻塞的,对CPU 的利用率不够。

(2)同步非阻塞I/O

在同步基础上,将socket设置为NONBLOCK,用户线程可以发起I/O请求后立即返回,此时并未读到数据,需要不断地轮询,发起I/O请求,直到数据到达后才能读到真正的数据,然后处理。不断轮询、重复请求的过程消耗了大量的CPU资源,实际用处不大。

(3)异步非阻塞式I/O

在I/O多路复用中,时间循环文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而异步I/O中,当用户线程收到通知时,数据已经被啮合读取完毕并存放在用户线程指定的缓冲区内,内核在I/O完成后通知用户线程直接使用就行,因此这种模型需要操作系统更强的支持,即把read操作从用户线程转移到了内核。
select、poll、epoll都是I/O多路复用机制,本质上是同步I/O,因为它们需要在续写石金就绪后自己负责进行读写,即读写过程是阻塞的,真正意义上的I/O无需自己负责读写。

select

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:n为最大的文件描述符,监视的文件描述符有三类,readfds读文件描述符集合,writefds写文件描述符集合,exceptfds异常信息; 功能:调用后函数会阻塞,直到有描述符就绪(有数据读、写、或者有except),或者超时(timeout为指定时间,返回null),函数返回;
当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
缺点:单个进程能够监视的文件描述符的数量存在最大限制(linux上位1024);
每次调用select都需要将fd集合从用户态拷贝到内核态,开销较大; 对socket扫描是线性扫描,采用轮询的方法,效率较低。

poll

int poll(struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd{
  int fd;
  short events;
  short revents;
};

使用pollfd指针实现,指针中包含要监视的时间和发生的事件;
pollfd并没有最大数量的限制; poll返回后需要遍历pollfd来获取就绪的描述符。
缺点:每次调用poll,都需要将fd集合从用户态拷贝到内核态,开销较大; 对socket扫描时线性扫描,轮询的方法效率较低。

epoll

epoll通过内核与用户空间红黑树。
不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射到物理地址,这块物理地址对内核和用户都是可见的。以此减少用户态和内核态之间数据交换的复制开销。 socket与文件描述符之间的关系
套接字也是文件,其数据传输流程为: 用户端监听到有连接时,应用程序会请求内核创建socket,socket创建好之后会返回一个文件描述符给应用程序;当数据包过来网卡时,网卡会通过数据包的源端口、目的端口和源ip等在内核维护一个双向链表找到对应的socket,将数据包赋值到socket缓冲区中;当应用程序请求读取socket中数据时,内核会将数据拷贝到应用程序的内存空间,完成读取socket数据。

epoll的3个API

epoll_create():epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像seect和poll那样每次调用都要重复传入文件描述符或事件集。那么也就需要一个额外的文件描述符来表述事件表,这个文件描述符由epoll_create函数来创建。
epoll_ctl():epoll可以实现删除或添加所有待监控的连接。所有添加进来的事件都会被放在红黑树的某个节点中,重复添加无效且删除插入的事件复杂度低。
epoll_wailt():能够返回活跃链接,即使用epoll_wait检查rdlist双向链表中是否由存在注册的事件。

epoll的核心数据结构

红黑树存储socket
当添加或删除一个套接字时,在红黑树上处理(红黑树本身插入和删除性能比较好,时间复杂度低),添加进来的时间会被放在红黑树的某个节点内,重复添加是无效的。 数据结构体定义为:

struct eventpoll
{
    spin_lock_t lock;            //对本数据结构的访问
    struct mutex mtx;            //防止使用时被删除
    wait_queue_head_t wq;        //sys_epoll_wait() 使用的等待队列
    wait_queue_head_t poll_wait; //file->poll()使用的等待队列
    struct list_head rdllist;    //事件满足条件的链表
    struct rb_root rbr;          //用于管理所有fd的红黑树
    struct epitem *ovflist;      //将事件到达的fd进行链接起来发送至用户空间
}


struct epitem
{
    struct rb_node rbn;            //用于主结构管理的红黑树
    struct list_head rdllink;       //事件就绪队列
    struct epitem *next;           //用于主结构体中的链表
    struct epoll_filefd ffd;         //每个fd生成的一个结构
    int nwait;
    struct list_head pwqlist;     //poll等待队列
    struct eventpoll *ep;          //该项属于哪个主结构体
    struct list_head fllink;         //链接fd对应的file链表
    struct epoll_event event;  //注册的感兴趣的事件,也就是用户空间的epoll_event
 }

juejin.cn/post/724184… blog.csdn.net/JMW1407/art…