一、linux对网络通信的实现
1、linux网络IO模型
同步和异步:关注点在于调用方是否主动获取结果
同步:调用方需要主动等待结果返回;
异步:不需要主动等待结果返回,通过其它手段获取结果,如回调函数;
阻塞和非阻塞:关注点是等待结果返回调用方的状态;
阻塞:是指结果返回前,当前线程被挂起,不做任何事;
非阻塞:指结果返回前,线程可以做其它事情;
(*好好品味上述描述)
组合态的类型:
同步阻塞:最常见模型,去买东西,东西卖完了,你在店里一直等待,不做任何事情(干等),等到有货;效率很低;
同步非阻塞:同步非阻塞可以抽象为轮训模式,去买东西,东西卖完了,不需要傻傻的等着,你可以去买奶茶,但这期间你需要时不时的去问老板东西到货了吗;
异步阻塞:这个不常用,类似于你去买东西,东西卖完了,你把手机号留给老板,然后傻傻的什么也不做,等着老板告诉你有货了;
异步非阻塞:好比你去买东西,东西没了,你把手机号告诉老板,衣服到了联系你,这期间你可以去做任何事情;
2、linux下的五种I/O模型
a、阻塞I/O
b、非阻塞I/O
c、I/O复用(select、poll和epoll)
d、信号驱动I/O
d、异步I/O
总的来说,阻塞 IO 就是 JDK 里的 BIO 编程,IO 复用就是 JDK 里的 NIO 编程,Linux 下异
步 IO 的实现建立在 epoll 之上,是个伪异步实现,而且相比 IO 复用,没有体现出性能优势,使用不广。非阻塞 IO 使用轮询模式,会不断检测是否有数据到达,大量的占用 CPU 的时间,是绝不被推荐的模型。信号驱动 IO 需要在网络通信时额外安装信号处理函数,使用也不广泛。
阻塞I/O模型
I/O复用模型
比较上面两张图,IO 复用需要使用两个系统调用(select 和 recvfrom),而 blocking IO 只
调用了一个系统调用(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。
所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用
multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。select/epoll 的优势
并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
二、Linux下的IO多路复用
select、poll、epoll是Io多路复用机制,IO多路复用机制是一个进程可以监视多个描述符,一但某个描述符就绪(读或写就绪),能够通知响应程序进行响应的读写操作;但select、pollepoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,并等待读写完成;
1、文件描述符Fd
在linux上,可将一切都看做是文件,包括普通文件、目录文件、字符设备文件、快设备文件、套接字等;所有的一起都抽象成文件,提供统一接口,方便程序调用;
如程序需要打开或者新建一个文件时,内核会返回一个文件描述符用于打开或者新建文件,本质是一个整数,实际是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表;
2、I/O多路复用机制
select:函数监视的文件描述符分为三类,writefds、readfds和exceptfds。调用后select函数会阻塞,直到描述符就绪,或者超时返回空;当select函数返回后可以通过遍历fdset,来找到就绪的描述符;
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval
*timeout);
select 目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select 的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
提高效率的关键点在于将fd文件给内核处理;
缺点:1 bitmap只有1024大小,变大影响效率
2 存储fd的set不可重复利用
3 内存copy有较大的开销
4 内核判断是否有数据后,select又判断了一次
poll:不同与 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针实现。pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参数-值”传递的方
式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。 和 select 函数
一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。
int poll (struct pollfd *fds, unsigned int nfds, int timeou
poll与select最大的区别在于采用pollfd存储fd文件,消除了bitmap不可重复利用的问题,并且数组存储大小不会局限于1024;
epoll:不同与 select 使用三个位图来表示三个 fdset 的方式,poll 使用一个 pollfd 的指针现。pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select“参数-值”传递的方式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。 和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。
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);
epoll解决了select的几个问题:
1 bitmap只有1024大小,变大影响效率
2 存储fd的set不可重复利用
3 内存copy有较大的开销
4 内核判断是否有数据后,select又判断了一次
比较:
类型 | 最大连接数 | fd剧增的问题 | 消息传递方式 |
select | 单个进程所能打开的最大连接数有 FD_SETSIZE 宏定义,其大小是 32个整数的大小(在 32 位的机器上,大小就是 32*32,同理 64 位机器上FD_SETSIZE 为 32*64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响。 | 因为每次调用时都会对连接进行线性遍历,所以随着 FD 的增加会造 成遍历速度慢的“线性下降性能问题” | 内核需要将消息传递到用户空间,都需要内核拷贝动作 |
poll | poll 本质上和 select 没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 | 同上 | 同上 |
epoll | 连接数基本上只受限于机器的内存大小 | 因为 epoll 内核中实现是根据每个 fd 上的 callback 函数来实现的,只 有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下, 使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活跃的情况下,可能会有性能问题。 | epoll 通过内核和用户空间共享一块内存来实现的。 |
1、表面上看 epoll 的性能最好,但是在连接数少并且连接都十分活跃的情况下,select
和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。
2、select 低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过
良好的设计改善。
三、epoll高效原理和底层机制详解
1、计算机结构
了解epoll的本质我们要先了解硬件是如何接收网络数据的;
1 网卡接收数据 2 经过硬件电路传输最终将数据写到内存中的某个地址;这个过程涉及到DMA、IO通路选择等;我们只需要知道网卡最终会把己收到的数据写入内存;操作系统如何去读取它们;
2、如何知道接收了数据?
中断机制:所谓中断,可以理解为设备有数据到达时,会给CPU的相关引脚上触发一个电压变化值,以此通知CPU来处理数据;
计算机执行程序的过程中,有优先级的区分,当收到中断时,保存数据优先级较高,CPU应立即去保存数据;当CPU完成对硬件的响应后,再重新执行用户程序;
3、中断的上半部和下半部
由于网卡模块处理过程比较复杂耗时,因此中断处理分为上半部和下半部;上半部只做简单的处理,快速释放cpu,剩下的放到下半部慢慢处理;2.4版本后下半部采用软中断处理,由Ksoftirqd内核线程全权处理;软中断是通过给内存中的一个变量的二进制值以通知软中断程序处理;
4、内核收包过程
5、内核接收网络数据
进程在 recv 阻塞期间,计算机收到了对端传送的数据(步骤①)。数据经由网卡传送到内存(步骤②),然后网卡通过中断信号通知 cpu 有数据到达,cpu 执行中断程序(步骤③)。此处的中断程序主要有两项功能,先将网络数据写入到对应 socket 的接收缓冲区里
面(步骤④),再唤醒进程 A(步骤⑤),重新将进程 A 放入工作队列中。