Linux 复习 六

问:什么是I/O复用?

答:I/O复用就是让一个进程/或者线程,操作多个I/O,保证不会阻塞到某个特定的I/O.即一个进程可以处理多个请求,常见的I/O有select,poll,epoll(Linux特有)。

select:

#include <sys/select.h>
#include >sys/time.h>

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

返回值:就绪的描述符数目,==0  超时,==-1,出错

int maxfdp1  : 表示指定测试的文件描述符数目+1,(从0开始)   

   fd_set *readset

fd_set *writeset

fd_set *exceptset

表示我们想让内核帮我们检测的描述字:读事件,写事件,异常,如果对某个事件不敢兴趣,至为NULL ,struct  fd_set可以理解为一个集合,这个集合中的文件描述符可以用以下宏进行设置

void FD_ZERO(fd_set *fdset);           
//清空集合

void FD_SET(int fd, fd_set *fdset);   
//将一个给定的文件描述符加入集合之中

void FD_CLR(int fd, fd_set *fdset);   
//将一个给定的文件描述符从集合中删除

int FD_ISSET(int fd, fd_set *fdset);   
// 检查集合中指定的文件描述符是否可以读写

const struct timeval *timeout

struct timeval{

               long tv_sec;   //seconds

               long tv_usec;  //microseconds

   };

select函数调用过程:

  1. 使用copy__from _user 将fd_set 拷贝到内核空间
  2. 注册回掉函数_pollwait,
  3. 遍历所有的fd ,调用对应的poll方法,返回一个bitmask掩码,告诉select是否有读写事件,没有进程睡眠,一只等到有资源可读,或者timeout
  4. 把fd_set 拷贝到用户空间

select缺点:

  •       每次调用select都会将fd_set拷贝到内核空间,这个在fd 很多时开销很大
  •       内核需要遍历所有的fd ,当fd很多时,开销也很大
  •       找到就绪描述符,又要将fd_set 拷贝到用户空间,又要遍历一遍fd 找出是哪个文件描述符
  •        select 支持的文件描述符有上限,Linux一般为1024 个

pool 在机制上和select相似,也是进行轮询查找,但是poll 没有最大文件描述符限制

#include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

 pollfd结构体定义如下:

struct pollfd {
    int fd;               /* 文件描述符 */
    short events;         /* 等待的事件 */
    short revents;        /* 实际发生了的事件 */
} ;

 合法的事件如下

  •       POLLIN         有数据可读。
  •   POLLRDNORM      有普通数据可读。
  •   POLLRDBAND      有优先数据可读。
  •   POLLPRI         有紧迫数据可读。
  •   POLLOUT       写数据不会导致阻塞。
  •   POLLWRNORM      写普通数据不会导致阻塞。
  •   POLLWRBAND      写优先数据不会导致阻塞。
  •   POLLMSGSIGPOLL     消息可用。

 此外,revents域中还可能返回下列事件:

  •      POLLER   指定的文件描述符发生错误。
  •    POLLHUP   指定的文件描述符挂起事件。  
  •    POLLNVAL  指定的文件描述符非法。 

 unsigned int nfds,

nfds_t类型的参数,用于标记数组fds中的结构体元素的总数量

int timeout

      timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。
  timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生
  timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
返回值:

      成功时,poll()返回结构体中revents域不为0的文件描述符个数。 
  如果在超时前没有任何事件发生,poll()返回0。 
  失败时,poll()返回-1,并设置errno为下列值之一:

总结:poll 和 select 本质上一致,也是将用户传入的数组传入到内核空间,轮询遍历,如果有就绪,修改revents ,继续遍历,如果没有就绪,挂起该进程,直到有就绪,或者超时,再次遍历 ,然后拷贝给用户空间

select与poll比较:

select需要为读、写、异常事件分别创建一个描述符集合,最后轮询的时候,需要分别轮询这三个集合。而poll只需要一个集合,在每个描述符对应的结构上分别设置读、写、异常事件,最后轮询的时候,可以同时检查三种事件。
它没有最大连接数的限制,原因是它是基于链表来存储的。
epoll :
epoll 不会因为文件描述符的fd的增加导致效率下降,epoll会将用户有关系的文件描述符,在内核中创建一个事件表,这样用户空间到内核空间的拷贝只有一次。
 
epoll 的接口:
#include <sys/epoll.h>

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);
  1.  首先通过epoll_create()创建一个epoll对象,参数size表示处理的最大描述符数量
  2. epoll_ctl 可以操作上面的epoll,可以将某个fd 添加到时间表中,或者删除某个文件描述符
  3. epoll_wait调用时,如果有事件发生,就返回给用户态、

epoll_create()的返回值本身要占用一个fd,因此最后要将这个fd,close(),否则会导致fd被消耗殆尽,

epoll_ctl :的参数

  1. epfd,就是创建的epoll文件描述符
  2. op 表示动作,用宏表示
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;
  3. 第三个是要监听的fd,
  4. 第四个告诉内核需要监听什么事
    struct epoll_event {
      __uint32_t events;  /* Epoll events */
      epoll_data_t data;  /* User data variable */
    };
    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的文件描述符可以写;
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

epoll_wait():

参数maxevents 告诉内核接收内核能得到事件的集合数量  参数struct epoll_event *  接收内核返回的就绪事件  timeout 超时时间  毫秒。

epoll 实现机制

       epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket。
  这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。
  这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层。
  简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
  epoll的高效就在于,当我们调用epoll_ ctl往里塞入百万个句柄时,epoll_ wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。
  这是由于我们在调用epoll_ create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.
  当epoll_ wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
  而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。
  那么,这个准备就绪list链表是怎么维护的呢?
  当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。
  所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
  如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。
  执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪      链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

ET模式和LT模式

LT:默认水平触发模式,当调用epoll_wait时,返回的socket 未被处理,则下一次调用epoll_wait时,将再次返回这个就绪事件,

ET:沿边触发模式,当调用epoll_wait时,返回的就绪事件未被处理,则下次调用的epoll_wait 不会在返回这个就绪队列,直到这个文件描述符有了新的事件时,才会返回

原理,如果是LT模式,返回后清空了就绪链表,将未处理的再次添加到list链表中,而ET 必须要等到新的中断,才会再次epoll_wait 返回

epoll 的优点:

  1. 支持的打开的文件描述符很大  跟内存大小有关
  2. IO效率不会因为fd的增多而下降
  3. 提高了和用户空间消息传递的效率 (mmap  同一块内存实现) 

select 和epoll的使用场景

对于并发量低的场景,select 不会在内核建立起负责的文件系统,所以效率不一定比epoll 慢