多路IO转接

IO复用的典型应用场合

1) 客户需要处理多个描述符(通常是交互式输入和网络套接字)
2) 一个客户同时处理多个套接字
3) 一个TCP服务器既要处理监听套接字,又要处理已连接套接字
4) 一个服务器既要处理TCP,又要处理UDP
5) 一个服务器要处理多个服务或者多个协议

无论哪种IO模型,输入数据时都涉及两个步骤

1) 等待数据准备好
2) 从内核向用户复制数据

IO模型

阻塞式IO

工作方式:在内核将数据准备好之前,系统调用会移一直等待,所有的套接字,默认都是该方式。
在这里插入图片描述

非阻塞式IO

工作方式:如果内核还未将数据准备好,系统调用仍会直接返回,并且返回EWOULDBLOCK错误码。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程叫做轮训,这对于CPU而言是一种较大的浪费,一般只有特定场景下才使用
在这里插入图片描述

IO复用

工作方式:虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接同时等待多个文件描述符的就绪状态
在这里插入图片描述

信号驱动式IO

工作方式:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
优点:等待数据期间,进程不会被阻塞
在这里插入图片描述

异步IO

工作方式:由内核在数据拷贝完成时,通知应用程序,进程不会阻塞。
信号驱动式IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成

在这里插入图片描述

同步IO和异步IO

1) 同步IO操作导致请求进程阻塞,直到IO操作完成(前4种IO模型都是同步IO)
2) 异步IO操作不导致请求进程阻塞(最后一种是异步IO)

阻塞和非阻塞

阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
非阻塞调用指在不能得到结果之前,该调用不会阻塞当前线程

select函数

函数原型:

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

函数功能

系统提供select函数来实现多路复用输入/输出模型,程序会停在select这里等待,直到被监听的文件描述符有一个或多个发生了状态变化

形式参数

timeout

1) 永远等待,设置为NULL,仅当描述符准备好IO操作时才返回
2) 等待一段固定时间 当有描述符准备好IO时返回,但是不超过由该参数指定的时间
3) 根本不等待 检查描述符后立即返回,将该结构体参数中都设置为0
4) select函数并不能支持所有的timeval

nfds

1) 我们可以直接设置成1024(线性表,线性遍历),但是这样会增加内核的负担,应该也大不了多少,毕竟标志位上好多都是0
2) 一般为已知的最大fd+1,这是因为内核在遍历的时候从0开始的,所以,要到最大文件描述符加一
3) 内核中是fd_set的内部实现是一个数组,大小是1024,他需要从0遍历到该参数
4) 数组中存放的是标志位,每个文件描述符只占用一位
5) 如果想要修改最大文件描述符个数,那么只能是去修改内核源代码了
6) windows下随便整个数就行了

文件描述符集合

readfds: 读集合 (让内核去检查读缓冲区)
writefds: 写集合(让内核去检查写缓冲区)
exceptfds: 异常集合

返回值

跨所有描述符集的已就绪的总位数,如果在任何描述符就绪之前定时器到时,返回0,返回-1表示出错,此时readfds、writefds、exceptfds和timeout的值变为不可预测。

错误值可能是
EBADF   文件描述符无效或该文件已经关闭
EINTR   此调用被信号中断
EINVAL  参数N为负值
ENOMEM  核心内存不足

读就绪条件

1) 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小,TCP和UDP默认是1。此时可以非阻塞读该文件描述符,并且返回值大于0
2) 该连接读半部关闭(接受到FIN的TCP连接),对于这样的套接字进行读取不会阻塞,并返回0
3) 该套接字是一个监听套接字并且已完成的连接数不为0,对该套接字执行accept通常不会阻塞
4) 套接字上有错误待处理,对该套接字读取将会返回-1,同时把errno设置为确切的错误

写就绪条件

1) 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小且已经建立连接或者不用建立连接,对于TCP和UDP来说,默认低水位标记为2048。此时可以非阻塞读该文件描述符,并且返回值大于0
2) 该连接的写半部关闭,对于这样的套接字写操作将产生SIGPIPE信号
3) socket使用非阻塞connect连接成功或失败之后
4) 套接字上有错误待处理,对该套接字读取将会返回-1,同时把errno设置为确切的错误

异常条件

1) 某个套接字的带外数据到达(存在带外数据或者仍拥有带外标记)
2) 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息
3) 当套接字上发生错误时,它将会由select标记为可读可写

select缺点

  1. 每次调用select,都需要手动设置fd集合,从接口使用角度来说也非常不便
  2. 每次调用select,都需要把fd从用户态拷贝到内核态,这个开销在fd很多时会很大
  3. 每次调用select,都需在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  4. select支持的文件描述符数量太小

文件描述符操作函数

全部清空

void FD_ZERO(fd_set *set);

从集合中删除某一项

void FD_CLR(int fd, fd_set *set);

将某个文件描述符添加到集合

void FD_SET(int fd, fd_set *set);

判断某个文件描述符是否在集合中

int FD_ISSET(int fd, fd_set *set);
设置了的话是1, 没有设置0

接收低水位标记和发送低水位标记存在的目的

允许应用进程控制在select返回可读可写条件之前有多少数据可读或者有多大空间可写。

shutdown函数

函数作用

终止网络连接通常的方法是调用close函数,不过close有两个限制,可以使用shutdown函数来避免
1) close函数把描述符的引用计数减1,仅在该计数变为0时,才关闭套接字,而shutdown可以不管引用计数就激发TCP的正常连接终止序列
2) close终止读和写两个方向的数据传送,shutdown可以终止其中一个方向的数据传输或者两个方向的数据传输。

howto参数

1) SHUT_RD
        a) 关闭连接的读功能-套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数,对一个TCP套接字这样调用shutdown后,有该套接字接收的来自对端的任何数据都被确认,然后偷摸丢弃
2) SHUT_WR
        a) 关闭连接的写功能-对于TCP套接字而言,这称为半关闭,当前留在套接字发送缓冲区中的数据将被发送,后跟TCP的正常终止序列。
3) SHUT_RDWR
        a) 关闭连接的读写功能,相当于调用了两次shutdown函数

拒绝服务型攻击

内涵

当一个服务器在处理多个客户时,它绝对不能阻塞于单个客户相关的某个函数调用,否则可能导致服务器被挂起,拒绝为所有其他客户提供服务。这就是所谓的拒绝服务型攻击

解决方法:

1) 使用非阻塞IO
2) 让每个客户由单独的控制线程提供服务
3) 为IO操作提供一个超时时间

pselect函数

1) pselect函数使用timespec结构,而不使用timeval结构
2) pselect函数的第六个参数允许调用该函数时临时阻塞一个信号集,该信号集与进程环境中的阻塞信号集无关。
3) select可能会修改时间参数,但是pselect不会修改时间参数。

POLL函数

函数原型

 int poll(struct pollfd *fds, nfds_t nfds, int timeout);

形式 参数

       fds是 一个poll函数监听的结构列表,每一个元素中包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合
       nfds表示fds数组的长度
       timeout表示poll函数的超时时间,单位是毫秒
struct pollfd{
int fd;// 文件描述符
short events;// 请求的事件
short revents;// 返回的事件
};

返回值

返回值小于0,表示出错
返回值等于0,表示poll函数等待超时
返回值大于0,表示poll由于监听的文件描述符就绪而返回

poll优点

  1. 不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。pollfd结构包含了要监视的event和发生的event,不在使用select“参数-值”传递方式,接口使用比select更方便
  2. poll没有最大数量的限制(但是数量过大后性能也是会降低)

poll的缺点

       poll中监听的文件描述符数目增多时和select一样,poll返回后,需要轮训pollfd来获取就绪的描述符,每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核态中。同时连接的大量客户在一时刻可能只有很少的处于就绪状态,因此随着监视描述符数量的增长,其效率也会线性下降。

epoll

epoll是什么?按照man手册的说法是为处理大批量句柄而作了改进的poll。公认性能最好的多路IO就绪通知方法。

函数原型

epoll模型创建函数

int epoll_create(int size);
创建一个epoll的句柄,自从linux2.6.8之后,size参数是被忽略的,用完之后需要使用close关闭

epoll的事件注册函数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  • 第一个参数是epoll_create的返回值(epoll的句柄)
  • 第二个参数表示动作,用三个宏来表示
	EPOLL_CTL_ADD 注册新的fd到epfd中
	EPOLL_CTL_MOD 修改已经注册的fd的监听事件
	EPOLL_CTL_DEL 从epfd中删除一个fd
  • 第三个参数是需要监听的fd
  • 第四个参数是告诉内核需要监听什么事件

struct epoll_event结构如下

typedef union epoll_data
{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t
struct epoll_event
{
	uint32_t events;
	epoll_data_t data;
}__EPOLL_PACKED;

events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll等待函数

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

作用:收集在epoll监控的事件中已经发生的事件。
参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件复制到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
参数maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size。
参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。

工作原理

在这里插入图片描述
        当某个进程调用epoll_create方法时,linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关

struct eventpoll
{
	/* 红黑树的根节点,这颗树存储着所有添加到epoll中的需要监控的事件*/
	struct rb_root rbr;
	/* 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
	struct list_head rdlist;}

        每个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn(n为节点个数)),并且所有添加到epoll中的事件都会与设备驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,他会将发生的事件添加到rdlist双链表中。
在epoll中,对于一个事件,都会建立一个epitem结构体

struct epitem
{
	struct rb_node rbn;//红黑树节点
	struct list_head rdllink;// 双向链表节点
	struct epoll_filefd ffd;//事件句柄信息
	struct eventpoll *ep;// 指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}

        当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件的数量返回给用户,这个操作的时间复杂度是O(1)

epoll的优点(相对select而言)

  1. 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离
  2. 数据拷贝轻量:只在合适的时候调用EPOLL_CTL_ADD将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  3. 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作是O(1),即使文件描述符数据很多,效率也不会受到影响。
  4. 没有数量限制:文件描述符数目无上限

epoll使用方式

水平触发工作模式

        epoll默认状态下就是LT工作模式
        当epoll检测到socket上事件就绪的时候,可以不立即进行处理,或者只处理一部分,在第二次调用epoll_wait时,epoll_wait仍然会立即返回并通知socket读事件就绪。直到缓冲区上所有数据都被处理完,epoll_wait才不会立即返回。支持阻塞读写和非阻塞读写
        水平触发的主要特点是,如果用户在监听epoll事件,当内核有事件的时候,会拷贝给用户态事件,但是如果用户只处理了一次,,那么剩下没有处理的会在下一次epoll_wait再次返回该事件
        这样如果用户永远不处理这个事件,就导致每次都会有该事件从内核到用户拷贝,耗费性能,但是水平触发相对安全,最起码事件不会丢掉,除非用户处理完毕

边沿触发工作模式

        如果我们在将socket添加到epoll描述符的时候使用了EPOLLET标识,epoll进入ET工作模式
        当epoll检测到socket上事件就绪时,必须立刻处理,如果有剩余数据,第二次调用epoll_wait的时候,epoll_wait不会再返回了,即ET模式下,文件描述符上的事件就绪后,只有一次处理机会。ET的性能比LT的性能更高(epoll_wait返回的次数减少了很多),Nginx默认采用ET模式使用epoll。支持非阻塞的读写。
        其实select和poll也是工作在LT模式下,epoll既可以支持LT,也可以支持ET。
        边沿触发,相对跟水平出发相反,当内核有事件到达,只会通知用户一次,至于用户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能,但是相对来说,如果用户马虎忘记处理,将会产生事件丢失的情况

epoll的使用场景

        epoll的高性能,是有一定的特定场景的,如果场景不适宜,epoll的性能可能适得其反。对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll。例如,典型的一个需要处理上万个客户端的服务器就比较适合使用epoll,如果是系统内部,服务器和服务器之间进行通信,只有少数几个连接,这种情况下epoll就不适合。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值