文章目录
五种IO模型
同步阻塞IO
同步模型也是比较简单的模型,指的是当我们去调用相应的系统调用时,会导致在用户态的进程处于阻塞状态,等待在内核态处理任务完成后拿到想要的数据才会继续往下执行的模型。所以在从用户态到内核态,在回到用户态的时间段内。进程是需要阻塞等待的。就像去楼下拿外卖,外卖小哥在下楼这段时间是不能去送其他东西,处于一个等待阻塞的状态。
非阻塞IO
非阻塞也就顾名思义,他不会有阻塞IO中一直等待的那个状态,进程在执行系统调用,进程不会处于阻塞状态,进程会不断的询问缓存区中需要的数据是否准备好了。如果没有则会返回EWOULDBLOCK状态,这时进程知道了自己想要的东西还没有准备好,会继续对需求的数据进行询问。
异步IO(asynchronous IO)
异步的特性是相对于同步的,进程执行系统调用后会立即返回,进程不会阻塞,会继续执行接下来的逻辑。当进程需要的数据准备好后,内核态会将数据放入用户缓冲区后给进程通知,然后进程可以直接去做拿到相应数据的那些逻辑。
信号驱动式IO(signal-driven IO)
信号驱动IO和异步IO很像。当进程执行相应的系统调用开始后,进程依然可以正常执行,不会被阻塞。当数据被准备好后,会通过信号通知给相应的进程,进程在收到通知后,在从内核态拿数据到用户缓冲区的这个过程中,进程会处于阻塞状态。这也是信号驱动和异步IO的一个区别。
多路复用IO(multiplexing IO)
在这种模型下,进程会受阻于select,poll或epoll函数。这两个函数能够阻塞多个I/O操做,能够同时对多个读操做和多个写操做进行检测,直到数据变成可读或可写时,即数据已经加载到buffer中。才真正调用I/O操做函数,而后再将buffer中的数据复制到用户进程缓存区中,而后进程读取其数据,最后返回给客户端。
SElECT
select函数主要使用了轮询的方式来取监听多个事件是否发生的。select采用了位图这样的数据结构,来反映哪些事件需要被监听,哪些事件已经就绪。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// 第一个参数nfds是监视的文件描述符中最大值加一,
//可以理解为我们需要多大的位图。
//fd_set 则是相应事件需要监督的文件描述符集合 也就是一个位图,
//分别代表读事件,写事件,异常事件,
//结构体timeval则是用力啊设置select的等待时间。timeout的不同值也有不同意义
// ==NULL 意味着如果没有事件就绪,select将被阻塞,一直轮询。
// == 0 则代表轮询后无论是否有事件发生都会立即返回。
// == 某个值, 则代表select 需要等待这么久,如果时间内没有事件发生则会返回。
关于结构体fd_set 也有一组现成的函数帮助我们完成对结构体的操作,而在timeval中有两个达标事件的变量,分别代表秒和微秒。
void FD_CLR(int fd, fd_set *set); // 用来清除set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除set的全部位置0
使用select时,需要我们自己去利用fd_set设置好我们想要监听的对应事件的位图,select在轮询时,会去扫描我们的位图,然后判断事件是否完成。如果有相应的事件完成,select会将位图的对应位置1。当轮询结束后,我们去检查位图,就可以知道相应事件是否准备就绪。
但是select会受限于fd_set的大小,会有一个监听上限。
同时,因为每次轮询结束后,我们设置好的监听事件的位图都会被改写,作为一个输出参数输出。这就导致我们每次都需要去重新写一遍位图。
这也是select的一些缺点,虽然支持了多路复用,但是效率并不会特别高,尤其是并发量很大时,每次的轮询依然是一个巨大的开销。
POLL
poll则是在select的基础上做了一些进化。函数原型为
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//fds 是需要监听的事件结构列表
// nfds 是指的 前者pollfd列表的长度
// timeout 超时时间, 这里使用了int 但是单位是毫秒
因为pollfd的数量是可以控制的,所以没有了select中因为位图对上限的限制。
而在一个pollfd中, 除了包含了相应的文件描述符, 有需要监听的事件和准备就绪事件两部分,这样的好处也就是意味着我们不需要每次轮询结束后都去重新细写一遍我们需要监听的内容。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
//事件有具体的一些宏定义来帮助我们编写更具有可读性的代码
// 例如 POLLIN 表示数据读 POLLOUT 表示数据写
// 而在events 和revents中表示的就是相应的事件
// 这样的宏定义了十来个,但是我也没有完全记住,有需要的大家可以查一查这个取值表
}
但是 poll 和 select都是通过轮询这样的方式来完成多路复用的,虽然能达到目的,但是当我们的需求量级过高时,轮询的效率依旧不会特别高。依然会有一定的损失。
EPOLL
epoll则时在poll的基础上又完成了一次进化。不同于前两者的是,epoll是有一组函数组的。
int epoll_create(int size); // 创建一个epoll的句柄, size参数是可以省略的
//epoll_ctl 是向对应的epoll句柄中华,增加一个需要监听的事件
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
// epfd则是对应的epoll句柄
// op则是代表不同的动作
// EPOLL_CTL_ADD 添加一个新的fd
// EPOLL_CTL_MOD 修改一个已有的fd
// EPOLL_CTL_DEL 删除一个fd
//epoll_event 结构体则是fd监听事件的描述,在其中还有一个epoll_data的联合体
```cpp
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
}__EPOLL_PACKED;
//events 依然是一个可选宏
int epoll_wait(int epfd,struct epoll_event *event,
int maxevents,int timeout);
// 去拿在被监控的事件中已经准备好的事件。
// 输入参数分别为 对应之前形同的内容
// maxevent 则是对event的事件列表大小
// timeout 超时时间。
而实际使用epoll 是没有轮询的。操作系统会维护相应的一个红黑树和就绪队列。也会建立相应的回调机制,来保证我们每次去拿准备就绪的事件时,事件复杂度始终是O(1) 。同时在触发回调时,也会有LT和ET两种模式。详细的我会在下篇文章继续补充吧,给自己先立一个flag。