原文首更地址,阅读效果更佳!
Redis原理 - IO详解 | CoderMast编程桅杆https://www.codermast.com/database/redis/redis-IO.html
任何Linux 系统的发行版,其系统内核都是 Linux 。我们的应用都需要通过 Linux 内核与硬件交互。
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
32 位的操作系统,寻址地址就为 0 ~ 2322 ^ {32}232
当进程运行在用户空间时称为用户态,运行在内核空间时称为内核态。
Linux 系统为了提高 IO 效率,会在用户空间和内核空间都加入缓冲区:
5种IO模型
顾名思义,阻塞 IO 就是在等待数据和拷贝数据到用户空间两个阶段过程中都必须阻塞等待。
可以看到,阻塞IO模型中,用户进程在两个阶段都是阻塞状态。
非阻塞 IO 的 recvfrom 操作会立即返回结果,而不是阻塞用户进程。
可以看到,非阻塞 IO 模型中,用户进程在第一个阶段是非阻塞的,在第二个阶段是阻塞的。虽然是非阻塞的,但是性能并没有得到提高,而且忙等机制会导致 CPU 空转,CPU 使用率暴增。
无论是阻塞 IO 还是非阻塞 IO,用户应用在一阶段都是需要调用 recvfrom 来获取数据,差别在于无数据时的处理方式:
比如服务端处理客户端Socket 请求时,在单线程情况下,只能依次处理每一个 Socket,如果正在处理 socket 恰好未就绪(数据不可读或者不可写),线程就会被阻塞,所有其它客户端 socket 都必须等待,性能自然很差。
文件描述符(File Descriptor):简称FD,是一个从 0 开始递增的无符号整数,用来关联 Linux 中的一个文件。在 Linux 中一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)
IO多路复用:是利用单个线程来同时监听多个 FD ,并在某个 FD 可读、可写时得到通知,从而避免无效的等待,充分利用 CPU 资源。
实现 IO 多路复用的技术有三种方式:
差异:
select 是 Linux 中最早的 I/O 多路复用的实现方案:
// 定义类型别名 __fd_mask,本质是 long int
typedef long int __fd_mask;
/* fd_set 记录要监听的fd集合,及其对应状态 */
typedef struct {
// fds_bits是long类型数组,长度为 1024/32 = 32
// 共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
// ...
} fd_set;
// select函数,用于监听多个fd的集合
int select(
int nfds,// 要监视的fd_set的最大fd + 1
fd_set *readfds,// 要监听读事件的fd集合
fd_set *writefds,// 要监听写事件的fd集合
fd_set *exceptfds, // 要监听异常事件的fd集合
// 超时时间,nulT-永不超时;0-不阻塞等待;大于0-固定等待时间
struct timeval *timeout
);
具体流程如下:
select模式存在的问题:
poll 模式对 select 模式做了简单改进,但是性能提升并不明显,部分关键代码如下:
// pollfd 中的事件类型
#define POLLIN //可读事件
#define POLLOUT //可写事件
#define POLLERR //错误事件
#define POLLNVAL //fd未打开
// pollfd结构
struct pollfd{
int fd; // 要监听的 fd
*short int events; // 要监听的事件类型:读、写、异常
short int revents; // 实际发生的事件类型
}
// poll函数
int poll(
struct pollfd xfds, // pollfd数组,可以自定义大小
nfds_t nfds, // 数组元素个数
int timeout // 超时时间
);
IO 流程:
与 SELECT 比较:
epoll 模式是对 select 和 poll 模式的改进, 提供了三个函数:
struct eventpoll{
//...
struct rb_root rbr; // 一颗红黑树,记录要监听的fd
struct list_head rdlist; // 一个链表,记录就绪的 FD
//...
}
// 1.会在内核创建eventpolL结构体,返回对应的句柄epfd
int epoll create(int size);
// 2.将一个FD添加到epol的红黑树中,并设置ep_poli_calLback
// calTback触发时,就把对应的FD加入到rdlist这个就绪列表中
int epoll _ctl(
int epfd, // epoll实例的句柄
int op, // 要执行的操作,包括:ADD、MOD、DEL
int fd, // 要监听的 FD
struct epoll_event *event // 要监听的事件类型: 读、写、异常等
);
// 3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量
int epoll wait(
int epfd, // eventpoll 实例的句柄
struct epoll_event *events, // 空event 数组,用于接收就绪的 FD
int maxevents, // events 数组的最大长度
int timeout // 超时时间,-1永不超时;0不阻塞;大于0为阻塞时间
);
当 FD 有数据可读时,我们调用 epoll_wait 就可以得到通知,但是时间通知的模式有两种:
举个例子
结论
基于 epoll 模式的 web 服务的基本流程图:
select 模式的存在的三个问题:
poll 模式的问题:
epoll 模式中如何解决这些问题:
信号驱动 IO 是与内核建立 SIGIO 的信号关联并设置回调,当内核有 FD 就绪时,会发出 SIGIO 信号通知用户,期间用户应用可以执行其他业务,无需阻塞等待。
当有大量 IO 操作时,信号较多,SIGIO 处理函数不能及时处理可能导致信号队列溢出。
而且内核空间与用户空间的频繁信号交互性能也较低。
异步 IO 的整个过程都是非阻塞的,用户进程调用完异步 API 后就可以去做其他事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。
在异步 IO 模型中,用户进程在两个阶段都是非阻塞的状态。
异步 IO 模型虽然很简单,但是在高并发的访问下,内核中会处理大量请求,容易导致内核崩溃。
IO 操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作),也就是阶段二是同步还是异步: