在说五种高级IO模型之前,不得不先说一下IO,任何的IO过程都包括两种,等待和数据拷贝。一般来说,等待的时候远大于数据拷贝的时间,所以想要优化一个程序的性能,就得从它的的等待入手,所以我们就得了解各种IO的原理以及使用。
五种IO模型:
- 同步IO (synchronous IO)
1.阻塞IO
在发起调用,如果没有完成则一直等待。这个模型的流程非常简单,但是一个IO的结束才能执行下一个IO,对资源的利用率很低。
2.非阻塞IO
发起调用,如果当前不具备IO条件,则立即返回,每隔一段时间,都会进行轮询遍历。这个模型虽然在一定程度上解决的资源利用率的问题,但是IO的操作不够实时。
3.信号驱动IO
用信号处理函数sigaction对SIGIO信号进行处理,一旦IO准备就绪,就会通知进程进行IO操作,IO的效率和实时性虽然在一定程度上得以解决。但是这种方式,一旦信号被触发,其他线程就会被挂起。
4.多路转接IO
对大量的描述符进行IO事件的监控,它会告诉进程有哪些描述符的哪些事件准备就绪,然后进程对就绪的描述符对应的事件,进行相应的IO操作。这种模型,很好的解决的实时性以及资源利用的问题。多路转接IO的实现模型有三种:select、poll、epoll,在下文会详细涉及。
- 异步IO (asynchronous IO)
通过异步IO调用,完成内核数据的拷贝之后,通知进程。
阻塞: 阻塞调用是指,当前调用如果没有完成,则线程会被挂起,函数在只有得到返回值之后,才会返回。
非阻塞:非阻塞与阻塞相对立,如果不能立即得到结果,则不会阻塞当前线程,直接返回
阻塞和非阻塞的区别:发起一个调用之后,如果不具备条件是否直接返回
同步:在发起一个功能调用的时候,功能由自己来完成,而且一个完成之后才可以完成下一个
异步:在发起一个功能调用,然后去完成其他的事情,等内核完成之后,会通知这个操作已经完成,处理的顺序不定。
同步和异步的区别:一个操作是不是由进程来完成,且是否为顺序处理
IO多路转接之select
- 操作流程:
1.程序员定义某个事件的描述符(可读/可写/异常事件的描述符集合)
初始化清空以及描述符的添加
2.将集合拷贝到内核中进行监控,监控的原理是轮询遍历判断
可写就绪、可读事件、异常事件
3.监控调用的返回
监控出错、监控就绪、监控超时
并且返回调用的时候,将事件监控的描述符集合中的未就绪描述符从集合中移除
所以,因为修改了描述符,因此下一次需要重新添加描述符
4.程序员轮询判断哪个描述符仍然还在哪个集合中,就确定了描述符就绪了某个时间,然后进行对应时间的操作即可
注意:select不会直接返回描述符操作,所以需要用户自己判断
- 代码操作:
1.定义集合
struct fd_set 成员只有一个数组,位图原理,将相应比特位置为1
监控的描述符数量,取决于一个宏 __FD_SETSIZE 默认为1024
void FD_ZERO(fd_set *set) 清空
void FD_SET(int fd,fd_set *set)添加
2.监控操作
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* excptfds,struct timeval* timeout);
nfds:当前描述符最大值加一,为了提高遍历效率
readfds/writefds/excptfds 可读可写异常三种事件的描述符集合
timeout:时间结构体
struct timeval{ tv_sec; tv_usec}; 通过这个时间决定select为阻塞/非阻塞/限制超时的阻塞
若timeout为NULL,标识为阻塞
timeout的数据为0.标识非阻塞,立即返回
timeout的数据不为0,没有数据超时返回
返回值大于0则表示就绪的描述符个数,等于0没有描述符就绪,返回值小于0表示监控出错
3.调用返回,返回给程序员,就绪的描述符集合,用户遍历判断哪个描述符还在集合中,就是就绪了哪个事件
FD_ISSET(int fd,fd_set* set)
4.对描述符不想进行监控,则从描述符中移除描述符
void FD_CLR(int fd,fd_set* set) 从set集合中删除描述符fd
- 优缺点分析:
缺点:
1.监控的数量有上限
2.在内核中进行轮询遍历判难,性能受到描述符数量影响
3.需要对集合再进一次判断,才可以知道这个描述符准备就绪
4.每次监控重新添加描述符,到集合中,然后将集合拷贝到内核中
优点:
遵循posix标准,跨平台移植性很好
IO多路转接之epoll
- 操作流程:
1.在内核中创建epoll句柄epollevent结构体
2.对内核中的epollevent结构体添加/删除/修改所监控的描述符监控信息
3.开始监控,在内核中采用异步阻塞操作实现监控,等待超时/有描述符就绪了事件调用返回,返回给用户就绪描述符的时间结构体信息
4.进程通过对一个双向链表的访问,得到事件结构体中的描述符成员,然后直接进行操作即可
- 代码操作:
1.int epoll_creat(int size)创建epoll句柄
size:在早期用来确定监控的描述符数量
返回值:文件描述符—epoll的操作句柄
2.int epoll_ctl(innt epfd, int cmd, int fd, struct epoll_event* ev)
epfd:返回的操作句柄
cmd:对fd进行的操作 添加删除修改 EPOLL_CTL_ADD
fd:要监控操作的描述符
ev:fd描述符对应的事件结构体信息
struct epoll_event{
uint32_t events; //表示fd没描述符要监控的事件 EPOLLIN/EPOLLOUT 可读可写
union { int fd; void* ptr}data;//要填充的描述符信息
}
3.开始监控
int epoll_wait(int epfd,struct epoll_event* evs,int max_event,int timeout)
epfd:epoll操作句柄
evs:struct_event结构体数组的首地址,用于接收就绪描述符对应的事件结构体信息
max_event:本次监控想要获取的就绪时间的最大数量,不大于evs数组的节点个数
timeout:超时等待时间
返回值大于0表示就绪的事件个数,等于0等待超时,小于0监控出错
- 优缺点分析:
优点:
1.没有监控的数量有上限
2.只需要想内核添加一次监控信息
3.监控使用异步阻塞操作完成,性能不会随着描述符增多而下降
4.直接向用户返回就绪事件信息,不需要在进行判断是否就绪,直接操作描述符以及时间即可
缺点:
跨平台移植性差