Linux系统下典型的网络IO模型

前言

IO指的就是输入输出, 所谓的输入输出就是数据流在内存和硬盘之间的相互传输。并且输入输出都是相对于内存说的,数据从硬盘传输到内存属于输入,而数据从内存传输到硬盘属于输出。

IO的过程其实就是发起IO调用后等待IO就绪条件,进行数据拷贝

Linux系统下有几种典型的IO模型,我们就来一起探索一下有关阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路转接IO的相关知识。

一:阻塞IO模型

发起IO调用,若IO就绪条件不满足,则一直等待。

例如:在Linux默认情况下,所有套接字都是阻塞的。进程调用一个recvfrom请求,但它不能立刻收到回复,直到数据返回,然后将数据从内核空间拷贝到程序空间。

阻塞IO模型在IO执行的两个阶段中,进程都处于blocked(阻塞)状态,在等待数据返回的过程中不能做其它的工作,只能阻塞等待

优点:

  1. 实时性高,响应及时无延时

缺点:

  1. 无法充分利用资源
  2. 效率低

二:非阻塞IO模型

发起IO调用,若IO就绪条件不满足,则立即报错返回。

例如:在Linux下可以通过设置socket套接字选项使其变为非阻塞。进程调用一个recvfrom请求,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以处理其他的业务逻辑,过会儿再发起recvfrom系统调用。采用轮询的方式检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理

优点:

  1. 对资源的利用较为充分(利用数据未就绪的时间进行其他操作)

缺点:

  1. IO不够实时,任务完成的响应延迟增大了,导致整体数据吞吐量的降低
  2. 轮寻对于CPU来说是较大的浪费

三:信号驱动IO模型

发起IO调用,当IO就绪条件满足时,操作系统才会给进程发送SIGIO信号,进程才会中断当前操作去处理数据。

定义IO就绪信号处理方式,发起IO调用后进程不需要被阻塞且不需要轮询检查内核数据,进程可以进行其它操作,当IO就绪后,进程会收到一个SIGIO信号,可以在信号处理函数中调用IO操作函数处理数据

优点:

  1. 实时性高
  2. 资源利用更加充分

缺点:

  1. 流程较为复杂,既有主控流程又有信号处理流程,涉及到信号处理以及可靠性

四:异步IO模型

IO顺序不确定,并且IO过程(等待+数据拷贝)由操作系统完成,不是进程自己进行。

进程进行aio_read系统调用之后,进行其它的操作,无论内核数据是否准备好,都会直接返回给用户进程,不会对进程造成阻塞。等到数据准备好了,内核直接复制数据到进程空间,然后从内核向进程发送通知,此时数据已经在用户空间了,可以对数据进行处理了。

优点:

  1. 资源利用最为充分
  2. 效率高

缺点:

  1. 流程最为复杂
  2. 资源消耗高

五:多路转接IO模型

多路转接IO模型用于大量IO就绪事件的监控,实现进程只针对IO就绪事件进行IO操作。

IO就绪事件:

  1. 可读事件:一个描述符对应的缓冲区当前是否有数据可读
  2. 可写事件:一个描述符对应的缓冲区当前是否可以写入数据
  3. 异常事件:一个描述符当前是否发生了异常(挂起、无效请求、错误条件、连接关闭)

只针对IO就绪事件处理的好处:

  1. 避免对没有就绪的描述符进行操作而导致进程阻塞
  2. 避免对大量没有就绪的描述符遍历而导致的效率降低

多路转接IO模型的使用场景:

  1. 对描述符有可读,可写,异常事件监控的需求
  2. 对大量描述符进行监控,但是同一时间只有少量的描述符活跃(并发)
5.1 select模型
5.1.1 select模型的工作原理

select模型对大量描述符进行可读、可写、异常事件监控,让用户能够仅仅针对事件就绪的描述符进行操作。

对就绪事件的判断主要有以下几个标准:

1、可读事件:接收缓冲区中数据大小大于等于低水位标记(默认一个字节)。
2、可写事件:发送缓冲区中空闲空间的大小大于等于低水位标记(默认一个字节)。
3、异常事件:描述符是否发生了某些异常。
5.1.2 select模型的操作流程

1.定义某个事件的描述符集合

定义可读事件描述符集合,可写事件描述符集合,异常事件描述符集合。

struct fd_set 成员中只有一个数组当做二进制位图使用,添加描述符就是将描述符的值对应的比特位置1

fd_set fds;

因此select能够监控的描述符数量,取决于二进制位图中的比特位的多少,而比特位多少取决于宏_FD_SETSIZE,默认等于1024bit。

2.初始化清空描述符集合

void FD_ZERO(fd_set* set);

3.添加描述符到相应事件的描述符集合中

void FD_SET(int fd, fd_set* set) -- 将fd描述符添加到set集合中

4.调用select将集合中的数据拷贝到内核中进行监控

在内核中采用轮询遍历判断的方式进行监控

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

nfds : 当前监控集合中最大描述符+1,减少遍历次数,提高效率(监听对象缺省,位置必须保留)

readfds / writefds / exceptfds: 可读 / 可写 / 异常 三种事件的描述符集合

timeout: struct timeval {tv_ sec, tv_ usec;} 时间结构体,通过时间决定select阻塞,非阻塞,限制超时的阻塞。

  1. 若timeout为NULL,则表示阻塞监控,直到有描述符就绪,或者监控出错才会返回
  2. 若timeout中的成员数据为0,则表示非阻塞,监控的时候若没有描述符就绪,则立即超时返回
  3. 若timeout中的成员数据不为0,则在指定的时间内,没有描述符就绪则超时返回

注:select会在返回前将集合中没有就绪的描述符从集合中移除

5.调用返回

返回给我们就绪的描述符集合,遍历哪一个描述符还在哪个集合中,就是就绪了哪个事件,进而进行相应操作。

int FD_ISSET(int fd, fd_set* set) -- 判断fd描述符是否在集合中

注:select返回时会修改集合,所以在每一次监控时都要重新添加描述符

6.对描述符解除监控

void FD_CLR(int fd, fd_set* set) -- 从set集合中删除描述符

5.1.3 select模型的优缺点分析

优点:

  1. 遵循posix标准,跨平台的移植性比较好。

缺点:

  1. select对描述符进行监控有最大数量上限,取决于宏 _FD_SETSIZE,默认大小是1024。
  2. 在内核中进行监控,是通过轮询遍历的,性能会随着描述符的增多而下降。
  3. 只能返回就绪的集合,需要进程进行轮询的遍历判断才能得知哪个描述符就绪了哪个事件。
  4. 每一次监控都要重新添加描述符到集合中,每一次监控都需要将集合重新拷贝到内核中。
5.2 poll模型
5.2.1 poll模型的工作原理

poll采用事件结构的方式对描述符进行事件监控,只需要一个事件结构数组,将要响应的描述符提交到数组的每一个结构节点的fd中,以及将用户关心的事件添加到响应节点revents中,即可进行监控。

5.2.2 poll模型的操作流程

1.定义监控的描述符事件结构体数组

定义监控的描述符事件结构体数组,将需要的描述符以及事件标识信息,添加到数组的各个节点中。

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

struct pollfd结构体:

struct pollfd {
	int fd; // 要监控的描述符
	short evects; // 要监控的事件 POLLIN(输入)/POLLOUT(输出) 多个事件
	short revents; // 调用返回时,填充的就绪事件
}

array_fds: 事件的结构体数组(填充要监控的描述符以及事件信息)

nfds: 数组中的有效节点个数(数组有可能很大,但是需要监控的节点只有前nfds个)

timeout: 监控的超时等待时间 - 单位 : 毫秒

2. 发起调用

发起调用,开始监控,将描述符事件结构体数组,拷贝到内核中进行轮询遍历判断,若有就绪或等待超时则调用返回。并且在每一个描述符对应的事件结构体中,标识当前就绪的事件。

3. 调用返回

当事件数组中有描述符事件就绪 / 等待超时 则poll返回,poll返回时在每一个描述符对应的事件结构体revents中,标识当前就绪的事件。

轮询遍历数组,判断数组中每个节点中的就绪事件是哪个事件,决定是否就绪以及如何对描述符进行操作。

5.2.3 poll模型的优缺点分析

优点:

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

缺点:

  1. 跨平台移植性差
  2. 每一次监控依然向内核中拷贝监控数据
  3. 在内核中监控采取轮询遍历的方式,性能会随描述符的增多而下降。
5.3 epoll模型

epoll模型是为了处理大批量句柄而做了改进的poll

定位:Linux下最好用的,性能最高的多路转接模型

5.3.1 epoll模型的工作原理

epoll监控是一个异步阻塞操作。对描述符的监控由操作系统完成,当描述符就绪之后,则将就绪的描述符对应的epoll_event结构添加到双向链表list中,而当前进程只是每隔一段时间判断以下list是否为空,即可知道是否有描述符就绪。

操作系统完成监控,对于每一个描述符所关心的事件都定义了一个事件回调,当描述符就绪事件的时候就会调用回调函数,这个回调函数负责将事件结构信息即struct epoll_event添加到双向链表中。

epoll_wait会自动检测list双向链表,检测到链表list不为空,表示有就绪事件,则将这个链表中这些epoll_event放到用户的events数组中返回出去。

5.3.2 epoll模型的操作流程

1. 在内核中创建epoll句柄epollevent结构体

epollevent结构体包含很多的信息,红黑树+双向链表。

int epoll_create(int size) -- 创建epoll句柄

size: 表示监控描述符的最大数量,在linux2.6.2之后就被忽略掉了,size大于0即可。

返回值: 文件描述符 – epoll的操作句柄。

2. 将描述符以及对应事件结构添加到内核中的epollevent结构体中(用红黑树存储)

int epoll_ctl(int epfd, int cmd, int fd, struct epoll_event* ev)

epfd: epoll的操作句柄,找到内核中指定的epollevent结构体。

cmd: 针对fd描述符的监控信息进行操作(添加、删除、修改 )

操作: EPOLL_CTL_ADD / EPOLL_CTL_DEL / EPOLL_CTL_MOD

fd: 要进行监控操作的描述符。

ev: fd描述符要监控的事件结构体。

struct epoll_event{
	uint32_t events; // 对fd描述符监控的事件 - EPOLLIN(写入事件)/EPOLLOUT(读出事件)
	union{
		int fd;
		void * ptr;
	}data;  //就绪后,要填充的描述符信息(通常与fd一致)
};

注意:每一个需要监控的描述符都会有一个对应的事件结构,当描述符就绪了监控的事件后,就会将这个事件结构体返回给程序员。

3. 开始监控

int epoll_wait(int epfd, struct epoll_event* evs, int max_event, int timeout)

epfd: epoll的操作句柄,找到内核中指定的epollevent结构体。

evs: 就绪事件结构体数组的首地址,用于接收就绪描述符对应的事件结构体信息。

max_event: 本次监控想要获取就绪事件的最大数量,不大于evs数组中节点的个数,防止越界。

timeout: 等待超时时间,毫秒。

开始异步阻塞监控,操作系统为红黑树中每一个需要监控的描述符设置了事件回调,一旦描述符就绪了监控的事件则触发回调,系统将就绪描述符的对应事件结构体信息添加到双向链表中。

进程通过判断双向链表是否为空,判断是否有就绪,有就将就绪的事件结构返回给进程。

进程只需要根据就绪事件的结构体中的事件信息决定对事件结构中的fd描述符进行那个相应操作即可。

4. 调用返回

进程直接对就绪的事件结构体中的描述符成员进行操作即可。

5.3.3 epoll模型的优缺点分析

优点:

  1. 没有描述符监控数量的上线。
  2. 监控信息只需要向内核中添加一次。
  3. 监控使用异步阻塞操作完成,性能不会随着描述符的增多而下降(只需要判断双向链表是否为空)。
  4. 直接向用户返回就绪事件信息(包含描述符在内),进程直接对描述符以及事件进行操作,不需要判断是否就绪。

缺点:

  1. 跨平台移植性差。
5.3.4 epoll的触发模式

触发IO就绪事件的方式

1.EPOLLLT水平触发

水平触发是触发IO就绪事件的默认方式。

水平触发对于可读事件来说,只要接收缓冲区中的数据大小高于低水位标记就会触发事件。对于可写事件来说,只要发送缓冲区中剩余空间高于低水位标记就会触发事件。总结来说就是只要缓冲区满足可读或可写要求就会触发事件。

举个例子,如果你在读取缓冲区,一次没有读完,只要缓冲区中还有数据那么下一次epoll会继续触发事件告诉你有东西可读。

2.EPOLLET边缘触发

边缘触发对于可读和可写事件来说都是只有当缓冲区中内容改变即新数据接收到接收缓冲区或发送缓冲区数据被发送走时才会触发事件

举个例子,如果想要读取接收缓冲区的内容,一次没有读取完,虽然缓冲区中还有数据但是下一次epoll不会触发事件让你继续读取,直到有新的数据接收到接收缓冲区中,此时你可以连着上次的数据和新数据一起读取。

新数据到来时最好能一次性将所有数据读出,否则epoll边缘触发不会触发第二次事件而一直等待,只有等到下一次新数据到来时才会触发事件。

无法得知缓冲区内有多少数据,因此只能采取循环读取的方式进行所有数据的读取,但recv读完数据后会阻塞,所以我们就会将recv的flag设置为MSG_DONTWAIT或直接将描述符的属性设置为非阻塞(fcntl接口中arg设置为O_NONBLOCK)

边缘触发每次内核只会通知一次,大大减少了内核资源的浪费,提高了效率,并且系统就不会触发一些你不关心的就绪文件描述符。

六:多路转接IO的适用场景

多路转接模型进行服务器并发处理:在单执行流中轮询处理就绪的描述符。(用户态完成负载均衡)
多线程多进程进行服务器并发处理:操作系统通过轮询调度多个执行流实现每个执行流中描述符的处理。(内核态完成负载均衡)

若大量描述符就绪,则多路转接模型很难做到负载均衡,那么我们就得在用户态进行约束,规定每个描述符只能读取指定数量的数据,完成后进行下一个描述符的操作。

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

我们通常将多路转接模型与多进程多线程结合使用,多路转接模型用来监控大量描述符,当描述符就绪事件后再创建执行流进行处理,防止直接为描述符创建执行流,但描述符没有时间到来而空耗资源的情况发生

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值