I/O复用总结

来源:互联网 发布:js隐藏手机号码中间 编辑:程序博客网 时间:2024/05/29 15:18

一.什么是I/O复用?

假设我们拥有1000个线程进行读写操作,但是我们大部分时间都耗费在等待的时间上,同时1000个线程对资源占用和CPU的开销很大,因此使用一个线程来监控1000个读写操作符的状态,当其中某一个描述符状态就绪时,用一个线程去处理已经就绪的状态,这样对资源占用和CPU的开销就会十分少了。

I/O多路复用:多路网络连接复用一个IO线程。同时需要指出I/O复用虽然可以监听多个文件描述符,但它本身是阻塞的。且当多个文件描述符同时就绪,如果不采取额外措施,程序只能按顺序依次执行,这使得服务器程序看起来是串行工作的,如果想要实现并发只能采用多线程编程。

这里写图片描述

从上图可以看出通过一个线程对IO流的状态监控,来实现同时管理多个线程。

二.select、poll、epoll详解

1.select

#include<sys/select.h>#include<sys/time.h>int select(int maxfdp1, fd_set *readset, fd_set *writest, fd_set *exceptest, const struct timeval *timeout);//返回:若有就绪描述符,则返回值为其数目,若超时则为0,出错则为-1并设置errno。//如果select在等待期间程序接收到信号,则select返回-1且同时设置errno为EINTR。struct timeval {    long tv_sec; //seconds    long tv_usec; //microseconds};

这里写图片描述

select函数的作用是告诉内核那些文件描述(读、写、异常条件)就绪。

//fd_set结构体的定义如下:#icnlude<typesizes.h>#define __FD_SETSIZE 1024#include<sys/select.h>#define FD_SETSIZE  __FD_SETSIZE  //1024typedef long int __fd_mask; //4 #undef __NFDBITS#define __NFDBITS  (8*(int)sizeof(__fd_mask)) //__NFDBITS 32typedef struct {#ifdef __USE_XOPEN    __fd_mask fds_bits[__FD_SETSIZE/__NFDBITS]    //1024/32 = 32个元素 一个元素4个字节,所以总共128字节,得到1024位    #define __FDS_BITS(set)  ((set)->fds_bits)#else    __fd_mask __fds_bits[__FD_SETSIZE/__NFDBITS];    #define __FDS_BITS(set) ((set)->__fds_bits)#endif}fd_set;//从结构体的定义可以看出fd_set仅包含一个整形数组,该数组每个元素的每一位标记一个文件描述符。文件描述符的数量由FD_SETSIZE指定,这就限制了文件描述符的总量。
//由于位操作过于繁琐,所以使用的四个宏来访问fd_set结构体:void FD_ZERO(fd_set *fdset); //清空整个文件描述符集合void FD_SET(int fd, fd_set *fdset); //设置监听我们关心的文件描述符void FD_CLR(int fd, fd_set *fdset);//清除我们监听的文件描述符void FD_ISSET(int fd, fd_set *fdset);//判断描述符是否设置为1;

需要注意的一点是select的最大描述符数

在设计select函数时,操作系统就对每个进程可用的最大描述符设置了上限,在文件中定义的FD_SIZE的常量值为1024,而select使用了相同的限制值。
我们无法使用在包括含有FD_SIZE的头文件前把FD_SIZE设置为一个更大的值。如果想要增大FD_SIZE的值,则必须在内核中进行修改,重新编译内核。不编译内核而改变其值是不够的。

这里写图片描述

注意:

使用 select时要注意每次重新在fd_set中设置文件描述符,因为时间一旦发生后,文件描述符集合就会被内核修改,这也是因为fd_set是一个值-结果参数。

2.poll

poll的调用和select类似,也是在指定时间内轮询文件描述符。

#include<poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);//若poll发生错误,则返回-1。若定时器到时之前没有任何描述符就绪,则返回0,否则返回就绪描述符个数。struct pollfd {    int fd; //如果fd为-1,那么poll则不会监听fd    short events; //注册的事件    short revents; //实际发生的时间,由内核来填写};//events成员告诉poll监听fd上那些事件,它是一系列事件的按位或;revents成员则由内核来修改,以通知fd上实际发生了那些事件。

pollfd相比fd_set的区别

因为有了events和revents,所以events作为一个调用者,revents作为一个返回结果。这样避免了值-结果参数的出现,这也使我们操作变的更加方便,不用每一次去重新注册监听的文件描述符。

这里写图片描述

timeout值 说明(timeout的单位为ms) INFTIM(通常为一个负值) 永远等待 0 立即返回 大于0 等待指定数目的毫秒数

关于poll函数的最大描述符数目

相比于select函数的FD_SETSIZE来限制了最大描述符数量,对于poll函数就不存在那样的问题了,因为分配一个pollfd结构的数组并把该数组中元素的数目通知给内核成了调用者的责任(nfds的设置)。内核不需要类似fd_set的固定大小的数据类型。

3.epoll

epoll是Linux特有的I/O复用函数。

首先,epoll使用一组函数来完成任务,而不是单个函数。
其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一表示内核中的时间表。

#include<sys/epoll.h>int epoll_create(int size);//size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大,且epoll_create()函数返回值为一个文件描述符,将用作其他epoll系统的第一个参数。
//下面的函数用来操作epoll内核事件表#include<sys/epoll.h>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

这里写图片描述

struct epoll_event {    __uint32_t events;//epoll的事件    epoll_data_t data;//用户数据};//其中epoll_data_t是一个联合体,其含有4个成员,我们一般使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员用来指定与fd相关的用户数据。如果想要将文件描述符和用户数据关联起来,则应该使用ptr。
#include<sys/epoll.h>int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//该函数失败返回-1,并设置errno。成功返回就绪文件描述符的个数。

这里写图片描述

epoll_wait函数如果检测到事件,则将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。这个数组只输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。

LT和ET模式

epoll对文件描述符的操作有两种:
1. LT(电平触发),LT是默认的工作模式,这种情况下epoll相当于一个高效的poll。当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,当下一次调用epoll_wait时,epoll_wait还会继续向应用程序通告此事件,直到该事件被处理。
2. ET(边沿触发),当往epoll内核事件表中注册一个EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll高效工作模式。当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件了。
3 .ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率高于LT模式。且ET模式必须是非阻塞的。