程序运行时,为了避免用户应用发生冲突,甚至发生系统冲突,用户应用和内核是分开的:
进程的寻址空间会划分为两部分:内核空间、用户空间。
当进程运行在用户空间时,我们称之为用户态,当进程运行在内核空间的时候,我们称之为内核态。
Linux
为了提高IO
效率会在用户空间和内核空间都加入缓冲区:
其中读数据的过程如下所示:
读数据主要分为两部分:
阻塞IO指的是在 等待数据阶段 和 由内核拷贝数据到用户空间阶段 都阻塞等待的IO。
等待数据阶段: 用户进程尝试读取数据,此时数据尚未到达,内核需要等待数据,此时用户线程也处于阻塞状态。
拷贝数据阶段: 数据到达并拷贝到内核缓冲区,代表已经就绪;将内核数据拷贝到用户缓冲区;拷贝过程中,用户进程依然阻塞等待;拷贝完成,用户进程阻塞解除,处理数据。
用户应用调用recvfrom读取数据,数据未准备好,用户应用阻塞等待,直到1,2都完成才能解除阻塞等待。
非阻塞IO
的recvfrom
操作会立即返回结果而不是阻塞用户进程。
等待数据阶段:
用户进程尝试读取数据,此时数据尚未到达,内核需要等待数据,返回异常给用户进程,用户进程拿到error之后,再次读取失败,之后不停调用recvfrom
,直到数据就绪。
数据拷贝阶段:
用户进程阻塞等待,直到拷贝完成,用户进程解除阻塞,处理数据。
虽然第二个阶段是非阻塞状态,但是性能并没有得到提高,而且忙等机制会导致CPU空转,CPU使用率暴增。
文件描述符(File Descriptor
)FD
, 是一个从0开始的无符号整数,用来关联Linux
中的一个文件。在Linux
中一切皆文件,例如常规文件、视频、硬件设备等,也包括网络套接字。
IO多路复用是利用单个线程来同时监听多个FD,并在FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
等待数据阶段:用户进程调用select
,指定要监听的FD
集合,内核监听FD
对应的多个socket
,任意一个或多个socket
数据就绪就返回readable
,该过程中用户线程阻塞。
数据拷贝阶段:用户线程找到就绪的socket
,依次调用recvfrom
读取数据,内核将数据拷贝到用户空间,用户进程处理数据。
IO多路复用中监听FD
的方式,有多种实现,常见的有:select
、poll
、epoll
其中:
select
和poll
只会通知用户进程有FD
就绪,但不确定是哪个FD
,需要用户进程遍历FD
来确认。epoll
则会在通知用户进程FD
就绪的同时,把已经就绪的FD
写入用户空间。select
的部分代码如下:
// 定义类型别名 __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_set,也就是多个fd的集合
int select(
int nfds, // 要监视的fd_set的最大fd + 1
fd_set *readfds, // 要监听读事件的fd集合
fd_set *writefds,// 要监听写事件的fd集合
fd_set *exceptfds, // // 要监听异常事件的fd集合
// 超时时间,null-用不超时;0-不阻塞等待;大于0-固定等待时间
struct timeval *timeout
);
其中,存储FD
的数据结构为fd_set
, fd_set
中有一个数组fds_bits
,其大小为32,fds_bits
数组的类型为long int
占4
字节,因此总共为32x4x8=1024
位,每个bit位代表一个fd
,0代表未就绪,1代表就绪
。
其过程如下:
select模式存在的问题:
fd_set
从用户空间拷贝到内核空间,select
结束还要再次拷贝回用户空间select
无法得知具体是哪个fd
就绪,需要遍历整个fd_set
fd_set
监听的fd
数量不能超过1024poll的部分代码如下:
// 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 *fds, // pollfd数组,可以自定义大小
nfds_t nfds, // 数组元素个数
int timeout // 超时时间
);
IO
流程:
pollfd
数组,向其中添加关注的fd
信息,数组大小自定义poll
函数,将pollfd
数组拷贝到内核空间,转链表存储,无上限fd
,判断是否就绪pollfd
数组到用户空间,返回就绪fd
数量n
n
是否大于0
0
则遍历pollfd
数组,找到就绪的fd
与select
对比:
select
模式中的fd_set
大小固定为1024
,而pollfd
在内核中采用链表,理论上无上限fd
数量无限制,但是监听FD
越多,每次遍历消耗时间也越久,性能反而会下降epoll模式是对select和poll的改进,它提供了三个函数:
struct eventpoll {
//...
struct rb_root rbr; // 一颗红黑树,记录要监听的FD
struct list_head rdlist;// 一个链表,记录就绪的FD
//...
};
// 1.创建一个epoll实例,内部是event poll,返回对应的句柄epfd
int epoll_create(int size);
// 2.将一个FD添加到epoll的红黑树中,并设置ep_poll_callback
// callback触发时,就把对应的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, // epoll实例的句柄
struct epoll_event *events, // 空event数组,用于接收就绪的FD
int maxevents, // events数组的最大长度
int timeout // 超时时间,-1用不超时;0不阻塞;大于0为阻塞时间
);
select模式存在的三个问题:
FD
最大不超过1024
select
都需要把所有要监听的FD都拷贝到内核空间FD
来判断就绪状态`poll模式的问题:
poll
利用链表解决了select
中监听FD
上限的问题,但依然要遍历所有FD
,如果监听较多,性能会下降epoll模式:
epoll
实例中的红黑树保存要监听的FD
,理论上无上限,而且增删改查效率都非常高epoll_ctl
添加到红黑树,以后每次epol_wait
无需传递任何参数,无需重复拷贝FD
到内核空间ep_poll_callback
机制来监听FD
状态,无需遍历所有FD
,因此性能不会随监听的FD
数量增多而下降信号驱动IO是与内核建立SIGIO
的信号关联并设置回调,当内核有FD
就绪时,会发出SIGIO
信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。
等待数据阶段:
sigaction
,注册信号处理函数FD
数据拷贝阶段:
SIGIO
回调信号recvfrom
,读取
当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也较低。
异步IO的整个过程都是非阻塞的,用户进程调用完异步API后就可以去做其它事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。
等待数据阶段:
aio_read
,创建信号回调函数数据拷贝阶段:
aio_read
中的回调函数异步IO模型中,用户进程在两个阶段都是非阻塞状态。