应用需要通过Linux内核与硬件交互。
内核本质也是应用,运行的时候也需要CPU资源、内存资源。用户应用也在消耗这些资源。
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
寻址空间:
无论内核还是用户应用,都无法直接访问物理内存,而是分配虚拟的内存空间,映射到不同的物理内存空间。内核和用户应用,再去访问虚拟内存空间,就需要有对应的虚拟地址(无符号的整数)
示例:32位系统,带宽32,地址的最大值就是2的32次方,也就是寻址的范围从0到2的32次方,也就是4GB.
IO在用户空间和内核空间切换的整体流程:
访问流程图
顾名思义,阻塞IO就是两个阶段都必须阻塞等待。
顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。
可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但是性能并没有得到提高。而且忙等机制导致CPU空转,CPU使用率暴增。
无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案:
比如服务端处理客户端Socket请求时,在单线程情况下,只能依次处理每一个socket,如果正在处理的soket恰好未就绪(数据不可读或不可写),线程就会被阻寒,所有其它客户端socket都必须等待,性能自然会很差。
这就像服务员给顾客点餐,分两步:
1、顾客思考要吃什么(等待数据就绪);
2、顾客想好了,开始点餐(读取数据)。
第一步要提高效率的几种方法:
1、方案一:增加更多服务员(多线程)
2、方案二:不排队,谁想好了吃什么(数据就绪了),服务员就给谁点餐(用户应用就去读取数据)
那么问题来了:用户进程如何知道内核中数据是否就绪呢?
文件描述符(File Descriptor)
:简称FD,是一个从0 开始递增的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字 (Socket)。
IO多路复用
:是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
监听FD的方式、通知的方式又有多种实现,常见的有:
1、select
2、poll
3、epoll
差异:
(1)select和poll只会通知用户进程有FD就绪,但不确定具体是哪个FD,需要用户进程逐个遍历FD来确认;
(2)epoll则会在通知用户进程FD就绪的同时,把已就绪的FD写入用户空间
select是Linux中最早的I/O多路复用时限方案:
poll模式对select模式进行了简单改进,但性能提升不明显。
epoll模式是对select和poll的改进,它提供了三个函数:
1、调用epoll_create(1),创建epoll实例
2、调用epoll_ctl(…),添加要监听的FD,关联callback,当callback触发时,把对应的FD加入到链表list_head中。
3、epoll_wait(…, events)等待FD就绪
1、select模式存在的三个问题:
当FD有数据可读时,我们调用epoll_wait就可以得到通知。事件通知的模式有两种:
区别:
拷贝数据之前,会将链表中的fd从list_head中断开连接,然后拷贝。
假如数据没有处理完,
当采用ET时,直接删掉fd,再次调用epoll_wait,list_head中没有数据;
当采用LT,会再次添加到 list_head,再次调用epoll_wait,list_head中有数据;
结论:
基于epoll模式的web服务的基本流程图:
信号驱动IO是与内核建立SIGIO的信号关联并设置回调,当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可
以执行其它业务,无需阻塞等待,
当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出。
而且内核空间与用户空间的频繁信号交互性能也较低。
IO操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作),也就是阶段二是同步还是异步:
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库API库AE:
ae.c文件判断服务器类型,选择执行哪个ae.c文件
Redis单线程网络模型的整个流程:
整个流程:
Redi6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令、写响应结果时采用了多线程。核心的命令执行、IO多路复用模块依然是由主线程执行。