文章目录
什么是IO??
IO分两步:等+数据拷贝
举例:
读IO就是:等待读事件就绪+将内核数据拷贝至用户空间
写IO就是:等待写事件就绪+将用户空间数据拷贝至内核空间
五种IO模型的基本概念
- 阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
- 非阻塞IO:如果内核还未将数据准备好,系统调用直接返回,并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员轮询检测读写的文件描述符。这对CPU来说是较大的浪费,一般只有特定场景才使用。 - 信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
- IO多路转接:IO多路转接可以等待多个文件描述符的就绪状态。
- 异步IO:不需要发起IO的进程亲自参与数据拷贝,而是由内核在数据拷贝完成时,通知应用程序。
非阻塞IO
fcntl
一个文件描述符,默认时阻塞IO。
函数原型:
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd,int cmd,.../* arg */);
fcntl 函数的五种功能:
- 复制一个现有的文件描述符(cmd=F_DUPFD)
- 获得/设置文件描述符标记(cmd=FD_GETFD或F_SETFD)
- 获得/设置文件状态标记(cmd=F_SETFL或F_SETFL)
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)
我们利用fcntl函数的第三个功能 ,将一个文件描述符设置为非阻塞
状态。
实现函数SetNoBlock
void SetNoBlock(int fd)
{
int fl=fcntl(fd,F_GETFL);
if(fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)
- 然后再使用F_SETFL将文件描述符设置回去。设置回去的同时加上一个O_NONBLOCK参数
IO多路转接(高级IO/高效IO)
高级IO的本质:减少等的比重
。
select
- select 系统调用的作用,可以让我们监视多个文件描述符的状态变化;
- 系统会在select这里等待,直到被监视的文件描述符有一个或多个发生状态改变。
select函数原型
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数解释:
nfds是你要监视的最大的文件描述符+1;
readfds,writefds,exceptfds 分别对应需要检测的可读文件描述符的集合,可写文件描述符的集合和异常文件描述符的集合;
timeout : 结构timeval,用来设置select()的等待时间
timeout取值:
NULL 表示没有timeout,select一直被阻塞,直到某个文件描述符上事件发生
0 :非阻塞,仅检测描述符集合的状态,然后立即返回;
特定时间值:表示多长时间轮询检测一次;
返回值:
> 0 表示有多少个文件描述符就绪
== 0 表示超时返回
-1 表示调用出错(错误码在errno中)
fd_set结构:
其实这个结构既是一个“位图”
为了方便操作fd_set,提供了如下接口:
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的全部位
select 就绪条件
读就绪
- socket内核中,接收缓冲区中的字节数, 大于等于
低水位标记
SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0; - socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪
- socket内核中, 发送缓冲区中的可用字节数(
发送缓冲区的空闲位置大小
), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0; - socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
select特点
- 可监控的文件描述符个数取决于sizeof(fd_set)的值,我的服务器上sizeof(fd_set)=128,每个比特位表示一个文件描述符,所以我服务器上支持的最大文件描述符是128*8=1024;
- 将fd加入select监控集的同时,还要使用一个数据结构array保存放到select监控集中的fd,
一是用于select返回后,array作为原数据和fd_set进行FD_ISSET判断。
二是select返回后会把以前加入但是并无事件发生的fd清空(输入输出型参数),则每次开始select前都要重新设置fd_set集合,扫描array的同时得到fd最大值,用于设置select的第一个参数。
poll
函数接口
#include<poll.h>
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
// pollfd 结构
struct pollfd{
int fd;
short events;
short revents;
};
参数说明:
fds是一个poll函数监听的结构列表,每一个元素中包含了三个部分:文件描述符,监听的事件集合,返回的事件集合
nfds表示fds数组的长度
timeout:poll函数的超时时间(ms)
返回值:
> 0 表示有多少个文件描述符就绪
== 0 表示超时返回
-1 表示调用出错
events和revents的取值:
epoll
为处理大量句柄而作了改进的poll
相关的系统调用
int epoll_create(int size);
创建一个epoll的句柄
- 自从linux 2.6.8之后,size参数是被忽略的
- y用完之后必须调用close()关闭
int epoll_ctl(int epfd,int op, int fd,struct epoll_event *event);
epoll的事件注册函数:
- 注册要监听的事件类型
- epfd:epoll_create()的返回值
- op:表示动作,用三个宏表示(EPOLL_CTL_ADD:注册新的fd到epfd中; EPOLL_CTL_MOD:修改已注册到fd中的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd)
- fd:需要监听的fd
- 第四个参数是告诉内核要监听哪个事件
struct epoll_event结构如下:
events重点关注以下两个:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
- events:输出型参数,获取并存放已经就绪的事件
- maxevents:告诉内核这个events多大,maxevents不能大于epoll_create()中size的大小
- timeout:(ms,0 立即返回,-1 阻塞)
返回值:
> 0 表示有多少个文件描述符就绪
== 0 表示超时返回
-1 表示调用出错
epoll原理
总结select,poll,epoll的优缺点
这三者都是高级IO,在IO体系中就负责等,也叫就绪事件通知方案
。他们的高效体现在一次可以等待多个文件描述符
.但是它们也是各有特点:
select
缺点 :
- 等的文件描述符是有上限的;
- 每次调用select都需要把fd集合从用户态拷贝至内核态。(设置fd集合),当文件描述符很多时开销会很大;
- 同时每次调用select都需要在内核中轮询检测传递进来的文件描述符,这个当fd很多时开销也是很大的;
- 还有就是select的输入输出型参数是在一起的,每次调用select都要手动设置fd集合,非常不方便
poll
poll解决了select的两个问题:
- poll等待的文件描述符无上限
- 它将输入输出型参数分离开来,每次调用不用从新设置fd集合
但是它也没有解决描述符越多,检测时间就越长的问题和要把大量的fd从用户拷贝到内核的问题。
epoll
它解决了select和poll的问题:
-
- 接口使用方便,不用每次从新设置fd集合,也将输入输出参数分离了
-
- 数据拷贝轻量:只是在合适的时候调用EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中。
- 事件回调机制:避免了轮询遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列当中,epoll_wait 返回直接访问就绪队列就可以直到哪些文件描述符就绪。时间复杂度O(1);
- 同时也没有文件描述符上限;
使用场景
适用于长连接的情况:
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.