IO多路复用之select、poll以及epoll

进程的阻塞

处理运行态的进程(获得CPU资源),由于需要等待一些事件的发送而不能继续执行时,就会祖东的转为阻塞状态,这是,他是不占用CPU资源的。因此也只有处于运行态的进程,才能够转为阻塞状态。这时候调度器会切换到其他进程,一旦这个进程等待的事件发生了,那么就会重新唤醒这个进程,重新等待被调度。一旦被调度器调度,那么就会恢复到切换之前的状态,继续执行,由阻塞状态切换到运行态。

文件描述符

一个用于对文件引用的抽象化概念。形式上就是一个非负整数,一个索引值,指向内核为每个进程维护的进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件的时候,内核就会向进程返回一个文件描述符。在linux中,将所有的外围设备也抽象为文件,所以在驱动程序开发中,打开一个设备,首先就需要获得这个设备对应的文件描述符,然后持有这个描述符,就可以打开并操作这个设备了。

缓存IO

其实就是标准IO,在linux中,当进程需要访问文件中数据时,会先将数据从磁盘文件拷贝到内核的缓冲区,然后再从内核缓冲区拷贝到进程的地址空间中。这样做的好处是实现了数据的共享,和保证数据在缓冲区中的时间尽可能的长。为什么这么说呢,因为从磁盘中读取一个文件的速度和从内存中拷贝一个数据的速度相差是100倍,一旦发生了一次数据的共享,那么这么一次周转的开销就是非常值得的。如果缓冲区中没有,再从磁盘中去拷贝,这个过程进行了多次数据的拷贝操作,带来的CPU和内存的开销是非常大的。

IO模式

当一个read操作发生时,会经历两个阶段:
1.数据从磁盘中拷贝到内核缓冲区
2.数据从内核缓冲区拷贝到进程地址空间。
正是由于这两个阶段,linux产生了下面五种网络模式的方案。

  • 阻塞I/O
  • 非阻塞I/O
  • I/O多路复用
  • 异步I/O
  • 信号驱动I/O

阻塞I/O(blocking IO)

默认情况中所有的socket都是阻塞的,当用户进程调用了recvfrom这个系统调用,然后kernel就会进入上述的两个阶段,这时候进程都是被阻塞的。当kernel一直等到数据准备好了,并拷贝到了用户进程地址空间,然后kernel返回结果,用户进程才会解除block的状态,重新运行起来。

非阻塞I/O

可以将socket设置为非阻塞的。当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,特点就是用户进程需要不断的主动询问kernel数据是否准备好。

IO多路复用(IO multiplexing)

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在於单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

异步I/O

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

I/O 多路复用之select、poll、epoll详解

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select

select函数监视的文件描述符有3类,writefd,readfd,exceptfd.调用select函数会阻塞,直到有描述符就绪,或者超时,函数会返回。返回之后,可以通过遍历fdset,找出就绪的描述符,然后再调用recvfrom函数将数据从缓冲区拷贝到进程地址空间。
缺点在於单个进程能够监测的文件描述符的数量存在最大限制,linux上一般为1024。通过重新编译内核或者修改宏定义可以提升这个限制,但是会造成效率的降低。

poll

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

int epoll_create(int size);//创建一个epoll的句柄,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);
  1. int epoll_create(int size);
    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
    当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    函数是对指定描述符fd执行op操作。

    • epfd:是epoll_create()的返回值。
    • op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
    • fd:是需要监听的fd(文件描述符)
    • epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待epfd上的io事件,最多返回maxevents个事件。
    参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

     epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
      LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
      ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章