目录
五种基本IO模型
read/write recv/send:都是阻塞/非阻塞(阻塞或非阻塞取决于描述符属性)
是否具备可读写条件----等待----完成数据的拷贝功能
-
阻塞IO:为完成一个功能发起调用,如果不具备完成条件,那么调用就不返回,一直等待直到条件具备完成功能或返回
(被动挂起等待) -
非阻塞IO:为了完成一个功能发起调用,如果不具备完成条件,直接报错返回(阻塞与非阻塞区别:发起一个调用之后,是否会立即返回)
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询 -
信号驱动IO:提前设定IO相关信号(SIGIO),当进程收到信号时,就意味着当前已经具备IO条件,接下里我们直接发起调用,拷贝数据
-
多路转接IO:我们发起调用的时候,并不清楚当前是否具备IO条件,所以一旦发起调用就有可能无法立即返回,因此IO多路转接的几种模型就实现了帮我们监控当前是否具备条件,如果具备则返回,然后发起IO调用即可。不具备它就再这里等待(一直监控)(核心在于多路转接能够同时等待多个文件描述符的就绪状态)
看效果,阻塞IO和多路转接都在不具备条件时等待,但是多路转接这种技术可以一次对很多个描述符进行监控。而阻塞IO只是自己在等待。(目的就是将串行化的IO等待给并行化,节省时间,提高效率)
-
异步IO:为了完成一个功能发起调用,如果当前不具备完成条件则直接返回,当具备条件时并且完成了拷贝操作之后通过信号或者其他方式来通知我们功能已经完成(由内核在数据拷贝完成时,通知应用程序(信号驱动是告诉应用程序合适可以开始拷贝数据))
aio----Linux下的异步IO aio_write/aio_read(需要我们指定写入的文件,位置,长度) -
(对比补充)同步IO:为了完成一个根功能发起调用,如果当前不具备完成条件时则一直等待,直到完成功能后返回
总结:
任何IO过程都包含两个步骤:第一是等待,第二是拷贝
而在实际过程中往往等待远远高于拷贝,所以让IO更高效最核心的办法就是让等待时间尽量减少
同步异步
- 同步,就是在发一个调用时,没有结果就不返回,一旦返回就得到返回值(调用者主动等待)
(A等B去吃饭,B不来就一直等) - 异步:调用发出后,调用者就直接返回,所以没有返回结果
(当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用后,被调用者通过状态和通知告诉调用者)
(A告诉B吃饭走,不管B去不去回应不回应,直接直接去)
阻塞非阻塞
- 阻塞调用就是调用结果返回之前,当前的线程会被挂起,调用线程只有在得到结果后才返回
- 非阻塞就是不能立刻得到结果之前,该调用不会阻塞当前线程
同步与异步的区别:发起调用之后能否立即完成功能后返回
同步异步与阻塞非阻塞的区别:
(完成功能的时间不同)同步异步说的是发起调用后是否能够立即完成功能
(返回的时间不同)阻塞与非阻塞说的是发起调用之后能否立即返回
非阻塞IO
fcntl
一个文件描述符,默认都是阻塞IO,我们通过第三种功能获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞
文件控制函数 fcntl -- file control
头文件:
#include <unistd.h>
#include <fcntl.h>
函数原型:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
描述:
fcntl()针对(文件)描述符提供控制.参数fd是被参数cmd操作(如下面的描述)的描述符.
针对cmd的值,fcntl能够接受第三个参数(arg)
fcntl函数有5种功能:
- 复制一个现有的描述符(cmd=F_DUPFD).
- 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
- 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
- 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
- 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
void setNonBlock(int fd)
{
int fl = fcntl(fd , F_GETFD);
if(fl < 0)
{
perror("fcntl");
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
setNonBlock(0);
char* buff[1024] = {0};
while(1)
{
char* buff[1024] = {0};
ssize_t s = read(0, buff,sizeof(buff));
printf("buff:%sfd:%d\n",buff,s);
sleep(1);
}
return 0;
}
阻塞时不输入一直等待,非阻塞则输出-1
重定向dup/dup2系统调用
这两个函数都可以来复制一个现有的文件描述符,他们的声明如下:
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd 2);
- dup,当我们调用它的时候,dup会返回一个新的描述符,这个描述一定是当前可用文件描述符中的最小值。我们知道,一般的0,1,2描述符分别被标准输入、输出、错误占用,所以在程序中如果close掉标准输出1后,调用dup函数,此时返回的描述符就是1。
- dup2,可以用fd2指定新描述符的值,如果fd2本身已经打开了,则会先将其关闭。如果fd等于fd2,则返回fd2,并不关闭它。
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd = open("./log.text",O_WRONLY|O_CREAT,0644);
if(fd < 0)
{
perror("open");
}
dup2(fd,1);
int i = 0;
while(i++ < 5)
{
printf("ha:%d\n",i);
fflush(stdout);
}
return 0;
}
int dup2(int oldfd, int newfd)
dup2() makes newfd be the copy of oldfd
new是复制old,完成重定向
多路转接
主要涉及的几个模型----select/poll/epoll----就绪事件通知机制
以前的程序不是阻塞在accept就是阻塞在recv为什么?
- socketfd是默认阻塞的
- 因为我们不知道数据什么时候来,所以只能在固定的地方进行accept和recv,但当我们accept和recv没有数据是就会卡在这里。假如我们知道数据什么时候到来,那么我们就可以在这个时候再发起调用,肯定有数据可读,不会阻塞。我们就可以同时处理多个客户端的数据(多路复用的几个模型就是专门干这个的,帮我们来监控什么时候那个描述符条件就绪)
就绪:IO条件是否满足(可读可写异常)
select在单个描述符的监控上的使用频率非常高
多路转接技术(select/poll/epoll)都是只使适用于大量客户连接,但是同一时间只有少量用户使用的情况
select
select:循环不断的对所监控的描述符进行就绪判断,如果没有描述就绪,就继续等!
1:select的监控性能会随着描述符的增多而降低
2:select的描述符是向fd_set集合中添加,然而fd_set这个结构体决定select最多只能监听1024个描述符
//#include <sys/time.h>早期标准
//#include <sys/types.h>
//#include <unistd.h>
#include <sys/select.h>/* According to POSIX.1-2001 */
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- nfds:当前监控最大的描述符+1(将描述符添加到哪个集合,就代表这个描述符监控的是什么条件)
- readfds:监控可读事件的描述符集合(只关心读)
- writefds:监控可写事件的描述符集合(只关心写)
- exceptfd:监控异常事件的描述符集合(只关心写)
- timeout:用来设置等待时间 NULL:永久等待,直到有描述符事件就绪
- 输入代表我关心那些文件描述符上的那些事件
- 输出代表内核告诉操作系统上那些文件描述符就绪
fd_set
其实fd_set结构是一个整数数组,更严格来说就是一个位图
使用位图中对的位来表示要监视的文件描述符,操作fd_set的接口如下:
- void FD_CLR(int fd, fd_set *set); 从集合中移除(清除描述词组set中相关fd位)
- int FD_ISSET(int fd, fd_set *set); 判断描述符是否在集合中(测试描述符词组相关fd位是否为真)
- void FD_SET(int fd, fd_set *set); 向集合添加描述符(设置描述符词组set中的fd位)
- void FD_ZERO(fd_set *set); 清空集合(清除描述符词组set中的全部位)
返回值
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回
返回值:
- 执行成功返回文件描述符状态以改变的个数
- 如果返回0代表以及超过timeout时间
- 当有错误返回-1
执行过程
select模型关键在于理解fd_set,假设fd_set为一字节,每一bit位都对应一个文件描述符fd,则一字节可以对应八个
- 执行fd_set,则位表示0000,0000
- 若fd=5,执行后变为0001,0000(第5位置置1)
- 若fd=1,则0001,0001
- 执行select,阻塞等待
- 若5上都有发生可读事件,则select返回
- 此时set变为0001,0000,没有发生事件的1被清空
- select当有描述符就绪时,会从集合中剔除那些没有就绪的描述符
- 意味着当select返回值 >0 时,集合中留下的描述符就都是就绪的(位图置1)
- 但是我们用户不知道到底哪个就绪了,不知道该对那个描述符就绪IO
- 所以一般情况下,我们会将所以的描述符记录下来,然后一一对比
- 判断是否任然存在,如果在的话就表示这个描述符就绪了
- select一旦返回,并且不是超时或者出错这时候留在集合中的都是就绪的描述符
创建一个数组专门用于存放所有的描述符
将数组中的元素全部置-1
将监听的描述符全部添加到数组中
不管集合中有什么数据都清空一下,重新添加描述符
将数组中不是-1的元素添加到select监控集合(因为每次都将未发生事件的描述符清除,所以每次都需要添加)
select优缺点
select缺点:
- select能够支持监控的描述符个数是有上限的(因为是位图),有FD_SETSIZE来确定(1024)
- select是原理是轮询判断就绪,描述符越多循环越多,因此性能会因为描述符增多而下降
- select并不会直接告诉用户那个描述符就绪,与我们需要遍历所有的描述符哪一个就绪,因此编码复杂,并且随着描述符增多性能会降低
- select每次有就绪事件发生都会修改我们的描述符集合(把没有就绪的剔除出去),因此我们要监控的描述符每次循环都需要重新添加(编码麻烦)
- 每次都需要将用户态的监控集合拷贝到内核态(性能较低)
优点:
- 监控的超时时间比较精细(精确到微妙)(epoll是毫秒)
- :select可以跨平台(window也可以)(epoll仅用于linux)
socket就绪条件
读就绪
- socket内核中,接收缓冲区中的字节数,大于等于低水位标记(SO_RCVLOWAT)此时可以无阻塞的读该文件描述符,并且返回值大于0
- socket TCP通信中,对端关闭连接,此时对socket读,则返回0
- 监听socket上有新的连接请求
- socket上有未读取的错误
读就绪
- socket内核中,接收缓冲区中的字节数,大于等于低水位标记(SO_SNDLOWAT)此时可以无阻塞的读该文件描述符,并且返回值大于0
- socket写操作被关闭,对一个写操作关闭的socket进行操作,会触发SIFPIPE信号
- socket使用非阻塞connect连接成功或失败之后
- socket上有未读取的错误
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
timeout:毫秒时间
0:非阻塞
-1:永远阻塞
返回值:与select 一样
- fds是一个poll函数的监听结构列表,每一个元素包含上内容:文件描述符,监听事件结集合,返回事件集合
- nfds表示fds数组长度
- timeout表示poll函数超时时间,单位是毫秒
优点
不同与select用位图来表示fdset,poll使用一个pollfd的指针实现
- poll没有最大数量限制(上限取决于硬件资源),因此第一个参数传递的是一个事件的集合(事件结构体数组),具体数据节点有多少,由用户决定
- polllfd结构包含了要监视的事件和发生的事件,不再使用select“参数-值”的方式,接口使用更方便
- poll不需要每次都重新添加事件符
缺点
- 性能随着描述符增多而下降(也是等同于select轮询判断)
- 也不会明确的告诉我们具体就绪的描述符也需要用户自己遍历,找出就绪的事件之后处理(避免麻烦,性能较低)
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中
被淘汰了!因为linux有了更好的epoll解决的所有缺点
epoll
epoll:是linux中最常用的多路转接模型,为了处理大批量句柄而改进了poll
因为select中都有各种各样的缺点,但是epoll将这些缺点全部克服了
epoll系统调用
epoll_creat:创建一个epoll结果会返回一个epoll描述符(创建红黑树,就绪队列,回调机制)
epoll_ctl: 注册函数,向内核中添加我们关心的epoll_event事件结构(用户告诉内核你帮我要关心那些文件描述符上的那些事件,内核采用红黑树的方式维护)
epoll_wait:开始对事件进行监控,我们需要传入一个事件的结构体数组地址,用于获取就绪的那些事件(内核告诉用户,那个就绪了)
)
epoll_creat
int epoll_creat(int size)
size:现在已经忽略了,大于0即可
返回;epoll句柄(文件描述符)
epoll_ctl
向内核添加事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll句柄
op:当前操作
EPOLL_CTL_ADD 添加事件
EPOLL_CTL_MOD事件修改
EPOLL_CTL_DEL删除事件
fd:所监控的描述符
event:
EPOLLIN:可读事件
EPOLLOUT:可写事件
EPOLLET:边缘触发属性
struct epoll_event结构
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 */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。
参数events用来从内核得到事件的集合
maxevents告诉内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size
参数timeout是超时时间该函数返回需要处理的事件数目,如返回0表示已超时。
优缺点
优点
- 描述符数目无上限,取决于硬件
- 性能不会随着描述符增多而下降,使用红黑树的数据结构来管理所有需要监控的文件描述符,采用就绪队列的方式管理
一旦触发事件,就会采用事件回调方式,迅速激活这个文件描述符 - 维护就绪队列:当文件描述符就绪,就会被放在一个就绪队列中,调用epoll_wait获取就绪文件描述符的时候
只需要去取队列中的元素即可,(时间复杂度永远为O(1)) - 当有事件描述就绪,epoll会直接告诉我们那些就绪了。所以性能更高
- (监控的事件只向内核添加一次,不需要每次都添加,因此复杂度低,性能较高
并不会在原来的事件基础上进行修改,因此不需要重复添加)
缺点:
不能跨平台,仅可用于linux
epoll两种工作方式
水平触发(LT)和边缘触发(ET)
select和poll其实是工作在LT模式下的,epoll两者都可以
通知模式:
LT模式时,事件就绪时,假设对事件没做处理,内核会反复通知事件就绪
ET模式时,事件就绪时,假设对事件没做处理,内核不会反复通知事件就绪
事件通知的细节:
1.调用epoll_ctl,ADD或者MOD事件EPOLLIN
LT:如果此时缓存区没有可读数据,则epoll_wait不会返回EPOLLIN,如果此时缓冲区有可读数据,则epoll_wait会持续返回EPOLLIN
ET:如果此时缓存区没有可读数据,则epoll_wait不会返回EPOLLIN,如果此时缓冲区有可读数据,则epoll_wait会返回一次EPOLLIN
2.调用epoll_ctl,ADD或者MOD事件EPOLLOUT
LT:如果不调用epoll_ctl将EPOLLOUT修改为EPOLLIN,则epoll_wait会持续返回EPOLLOUT(前提条件是写缓冲区未满)
ET:epoll_wait只会返回一次EPOLLOUT
如何说明与描述符是读就绪的?
可读事件就是缓冲区是否有数据可读
只要缓冲区有数据就一直提醒,(每次epoll_wait都会触发事件就绪)