进程寻址空间分为两部分:内核空间、用户空间
- 内核空间:可以执行特权命令(Ring1),调用一切系统资源
- 用户空间:只能执行受限的命令(Ring3),不能直接调用系统资源,必须通过内核提供的接口来访问
一、五种IO模型
《Unix网络编程》中归纳了五种IO模型:
阻塞IO(Blocking IO)、非阻塞IO(NonBlocking IO)、IO多路服用(IO Multiplexing)、信号驱动IO(Signal Driven IO)、异步IO(Asynchonous IO)
读取数据过程:
下面是读取数据的基本过程:
- 同步/异步的区别:关键在于第二阶段是同步还是异步
1、阻塞IO
等待数据阶段、读取数据阶段,都需要阻塞等待
2、非阻塞IO
recvfrom操作会立即返回结果,而不是阻塞线程
- 性能上没有得到提升,且
忙等待机制
会导致CPU空转,CPU的使用率暴增
3、IO多路复用
利用单个线程来同时监听多个
FD
,并在某个FD可读/可写
时得到通知,从而避免无效的等待,充分利用CPU资源
- 文件描述符(File Descriptor):简称FD,是一个从0开始递增的无符号整数,用来关联linux中的一个文件
4、信号驱动IO
与内核建立
Signal-driven I/O信号
关联,并设置回调
,当FD
就绪,会发出Signal-driven I/O信号
通知用户,期间用户可以执行其他业务,无需阻塞。
- 存在问题:有大量IO操作时,信号较多。
Signal-driven I/O信号
处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也较低
5、异步IO
整个过程都是非阻塞的
,用户进程调用异步API
后,就可以做其他事情,内核等待数据就绪并拷贝到用户空间后,才会递交信号,通知用户进程
二、SELECT、POLL、EPOLL
select、poll、epoll
是IO多路复用中的三种常见实现
差异:select
和poll
只会通知用户进程有FD就绪,但不确定具体是哪个FD就绪,需要用户进程逐个遍历FD
来确认。epoll
会通知用户进程FD就绪的同时,把已就绪的FD写入用户进程
1、SELECT
缺点:
- 1、需要将整个fd_set从用户空间拷贝到内核空间,setlect结束还要再次拷贝回用户空间
- 2、select无法得知具体是哪一个fd就绪,需要遍历整个fd_set
- 3、fd_set监听的fd数量不能操过1024
数据结构
//定义类型别名 __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的集合
* @param nfds 要监听的fd_set的最大fd + 1
* @param readfds 要监听的读事件的fd集合
* @param writefds 要监听的写事件的fd集合
* @param exceptfds 要监听的异常时间的fd集合
* @param timeout 超时时间,null-永不超时,0-不阻塞等待,大于0-固定等待时间
**/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
IO流程
2、POLL
相比与select,poll监听fd的大小理论上无上限。但是监听的fd越多,每次遍历消耗时间也越久,性能反而下降
数据结构
//pollfd 中的事件类型
#define POLLIN //可读事件
#define POLLOUT //可写事件
#define POLLERR //错误事件
#define POLLNVAL //fd未打开
//pollfd结构
struct pollfd {
int fd; //要监听的fd
short int events; // 要监听的事件类型:读、写、异常
short int revents; //实际发生的事件类型
}
/**
* poll函数
* @param fds // pollfd数组,可以自定义大小
* @param nfds // 数组元素个数
* @param timeout // 超时时间
**/
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
IO流程
- 1、创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
- 2、调用poll函数,将pollfd拷贝到内核空间,转链表存储无上线
- 3、内核遍历fd,判断是否就绪
- 4、数据就绪/超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
- 5、用户进程判断n是否大于0
- 6、大于0则遍历pollfd数组,找到就绪fd
3、EPOLL
对select和poll进行了改进,提供三个函数(epoll_create、epoll_ctl、epoll_wait)
数据结构
struct eventpoll{
//...
struct rb_root rbr; // 一颗红黑树,记录要监听的FD
struct list_head rdlist; // 一个链表, 记录就绪的FD
//...
}
/**
* 1、在内核创建eventpoll结构体,返回对应的句柄epfd
* @param size
**/
int epoll_create(int size);
/**
* 2、将一个FD添加到epoll的红黑树中,并设置ep_poll_callback
* callback触发时,就把对应的FD加入到relist这个就绪列表中
* @param epfd epoll实例的句柄
* @param op 要执行的操作,包括:ADD,MOD,DEL
* @param fd 要监听的FD
* @param event 要监听的事件类型: 读、写、异常等
**/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/**
* 3、检查relist列表是否为空,不为空则返回就绪的FD的数量
* @param epfd epoll实例的句柄
* @param events 空event数组,用于接收就绪的FD
* @param maxevents events数组的最大长度
* @param timeout 超时时间:-1不超时,0 不阻塞,大于0为阻塞时间
**/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
IO流程
相比于select和poll。 epoll减少了拷贝次数,减少了拷贝的数量,仅将就绪的FD返回,减少了遍历
事件通知机制
当FD有数据可读时,调用epoll_wait就可以得到通知。有如下两种模式:
- LevelTriggered:简称LT,默认模式。会重复通知多次,直至数据处理完成
- EdgeTriggered:简称ET。只会通知一次,不管数据是否处理完成。避免了LT模式可能出现的惊群现象,最好是结合非阻塞IO读取FD数据,相比LT会复杂一些
WEB服务流程
三、Redis中的网络模型
redis通过
IO多路复用
来提高网络性能,并支持各种不同的多路复用实现,并将这些实现进行封装,提供了统一的高性能事件库:AE库
Redis6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令、写入响应结果时采用了多线程。核心的命令执行、IO多路复用模块
依然是由主线程执行。