Linux下的典型IO模型以及IO多路复用

阻塞IO

在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。就像去钓鱼,鱼没有上钩,就一直等待。

特点:

  • 效率低,阻塞IO的方式,等待的时长取决于内核
  • 在等待过程中执行流是被挂起的,对CPU利用率是很低的
  • 实时性比较好,在IO就绪和数据拷贝之间是没有浪费时间的

非阻塞IO

如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。非阻塞IO 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。 这对CPU来说是较大的浪费,一般只有特定场景下才使用。

特点:

  • 对CPU的利用率较阻塞IO充分
  • 实时性较差。当IO调用返回的时候,执行流可能去处理其他的就绪事件
  • 需要轮训的处理,对CPU造成浪费

信号驱动IO

内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

特点:

  • 实时性比较好
  • 资源利用充分
  • 流程最为复杂
  • 资源消耗高

异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作

特点:

  • 实时性比较好
  • 资源利用更加充分

多路转接IO

替进程监控大量的描述符,发生了哪些事,这时候进程就可以针对发生了事件的描述符进行相应操作,IO多路转接能够同时等待多个文件描述符的就绪状态(可读、可写、异常)。

select

函数原型

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

// nfds是需要监视的最大的文件描述符值+1
// readfds,writefds,exceptfds分别对应于需要检测的
// 可读文件描述符的集合,可写文件描述符的集 合及异常文
// 件描述符的集合
//timeout为结构timeval,用来设置select()的等待时间
  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件
  • 0:将select设置为非阻塞
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回

select执行过程

  1. 定义指定事件的集合。
    用户想要监控的描述符集合。可读事件、可写时间、异常事件。
  2. 初始化集合。
    void FD_ZERO(fd_set* set); 清空指定描述符集合
  3. 将需要监控的事件的描述符添加到集合中。
    集合fd_set结构体中只有一个成员,就是一个数组,被当做一个位图使用。描述符就是一个数字,向集合中添加一个描述符,就是在这个数字相应的比特位置1,表示这个描述符被添加到集合中。这个数组中有多少个比特位或者说select能够监控多少描述符,取决于宏 _FD_SETSIZE ,默认是1024.
  4. 发起调用。
    将集合中的数据拷贝到内核中进行监控,监控采用轮训遍历判断方式进行。
    int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  5. 调用返回。
    select调用返回后,进程遍历哪些描述符还在哪些集合中,就可以知道哪些描述符就绪了哪些事件,进而进行对应操作。
    int FD_ISSET(int fd, fd_set* set) – 判断fd描述符是否在集合中
    void FD_CLR(int fd, fd_set* set) – 从集合中删除某个描述符,解除监控

socket就绪条件

读就绪:

  • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞的读该文件描述符,并且返回值大于0
  • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0
  • 监听的socket上有新的连接请求
  • socket上有未处理的错误

写就绪:

  • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
  • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或失败之后
  • socket上有未读取的错误

select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 假如服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则服务器上支持的最大文件描述符是512*8=4096
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。一是用于在select 返回后,array作为源数据和 fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取 得fd最大值maxfd,用于select的第一个参数

select的缺点

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

poll

poll函数原型

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; // 要监控的描述符
short events; // 描述符想要监控的事件POLLIN(输入)/POLLOUT(输出)
short revents; // 实际就绪的事件
};
  • fds是一个poll函数监听的结构列表。每一个元素中, 包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合。
  • nfds表示fds数组的长度。
  • timeout表示poll函数的超时时间,单位是毫秒(ms)。

poll执行过程

  1. 定义描述符事件结构体数组,将需要监控的描述符以及对应的事件填充到数组中
  2. 发起监控调用poll,将数组中数据拷贝到内核中进行轮训遍历监控,有描述符就绪或者超时后返回,返回时将这个描述符实际就绪的事件放到结构体的revents中(如果描述符没有就绪,revents中的数据为0)
  3. 监控调用返回后,程序员遍历数组中每个节点的revents,确定当前就绪了什么事件,做相应的操作

poll的优缺点

优点:

  • poll通过使用事件结构体进行监控,简化了select三种集合的操作流程
  • poll所能监控的描述符数量,不做最大数量的限制
  • 不需要每次重新定义节点,不需要每一次添加描述符信息

缺点:

  • 无法跨平台移植
  • 监控的原理是在内核中轮训遍历,会随着监控的描述符增多而性能下降
  • 返回后还是需要进行遍历才知道就绪了什么事件

epoll :Linux下最好用的多路转接模型

epoll执行流程

  1. 在内核中创建epollevent结构体,返回一个描述符作为代码中的操作句柄。
    int epoll_create(int size)
    size:要监控的描述符的最大数量。但在Linux2.6.8之后被忽略,只要大于0即可。
  2. 对需要监控的描述符,组织事件结构体,将描述符以及对应的事件结构添加到内核的epollevent结构体中
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)
    epfd:操作句柄
    op:针对描述符的监控信息进行添加/删除/修改操作。EPOLL_CTL_ADD / EPOLL_CTL_DEL / EPOLL_CTL_MOD
    fd:要进行监控的描述符
    event:内核需要监听什么事件。EPOLLIN / EPOLLOUT……
struct epoll_event{
	uint32_t events; // 表示要监控的事件,以及监控调用返回后的实际就绪事件 EPOLLIN/EPOLLOUT
	union epoll_data{
		int fd;// 描述符
		void *ptr;
	}data;
}
  1. 开始监控,当有描述符就绪,或者等待超时后调用返回
    int epoll_wait(int epfd, struct epoll_event* evs, int maxevents, int timeout)
    epfd:epoll的操作句柄,通过这个句柄,找到内核中指定的eventpoll结构体
    evs:epoll_event描述符的事件结构体数组首地址,用于获取就绪的描述符对应的事件结构体
    maxevents:evs数组的结点数量,主要防止就绪太多,向evs中放置的时候越界
    timeout:超时等待时间 – 单位毫秒
    返回值:返回值大于零表示就绪的描述符的数量,等于零表示等待超时,小于零表示监控出错

epoll工作原理

当某一进程调用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_wait()调用之后,并没有立即返回,而是每隔一会看看rdlist是否为空,确定是否有描述符就绪。

若rdlist不为空,则表示有继续,将描述符的结构信息添加到epoll_wait传入的evs数组中。

epoll的工作方式

水平触发LT(Level Triggered):
epoll默认状态下就是LT工作模式
假如说现在缓冲区有2KB的数据,已经读取了1KB的数据,再次调用epoll_wait()接口,仍然会返回。也就是说,只要缓冲区有数据,调用epoll_wait()接口就返回,知道缓冲区没有数据。支持阻塞读写和非阻塞读写。
边缘触发ET(Edge Triggered):
假如现在缓冲区有2KB的数据,已经读取了1KB,那么再次调用epoll_wait()接口,就不会再返回。也就是说,只有新数据到来时,才会调用epoll_wait()接口。
只支持非阻塞的读写。
select和poll其实也是工作在LT模式下。epoll既可以支持LT,也可以支持ET

epoll的优缺点

  • 采用事件结构体的方式,简化了多种监控集合的操作流程
  • 所监控的描述符没有了最大上限
  • 不需要每次监控的时候重新添加监控信息
  • 每次监控依然需要将数据拷贝到内核
  • 监控原理依然是轮询遍历,性能随着描述符的增多而下降
  • 不能跨平台移植

多路转接模型进行服务器并发处理与多进程/多线程并发并行处理有什么区别?

多路转接模型进行服务器并发处理:指的是在单执行流中进行轮训处理就绪的描述符
若就绪的描述符较多,则很难做到负载均衡(最后一 个描述符要很长时间后,前边描述符处理完了才能处理到) 所以作出规定每个描述符只能读取指定数量的数据,读取了就进行下一一个描述符。(用户态完成的负载均衡)

多路转接模型仅适用于:有大量描述符需要监控,但是同一-时间只有少量活跃的场景

多进程 / 多线程多执行流进行并发 / 并行:指的是操作系统通过轮训调度执行流实现每个执行流中描述符的处理

系统内核层面的负载均衡,不需要用户态做过多操作

基于两种方式的特点:通常两者可以搭配一起使用

多路转接模型监控大量描述符,哪个描述符就绪有事件了,再去创建执行流进行处理,防止直接为描述符创建执行流,但是描述符没有事件到来,空耗资源。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值