?五种IO模型
????????在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
这也是最常见的IO模型,阻塞流程按上图所示
? ? ? ? 如果内核还未准备好数据报,也不会阻塞而是直接返回,并且返回EWOULDBLOCK错误码?
非阻塞IO往往需要循环的去尝试读取文件描述符,这个过程称为轮询,但这种循环方式对CPU来说是较大的浪费,在特定场景下使用.?
??
? ? ? ? 内核将数据准备好的时候,会使用SIGIO信号通知应用程序进行IO操作.?
????????从流程图上看跟阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
TIP:这就相当于一个钓鱼佬,带了很多把鱼竿,同时等待多把鱼竿看看哪个上钩就收哪个.. )
由内核在数据拷贝完成时,同时?
- 任何IO过程中,都包含了两个步骤,一个是等待,一个是拷贝。
- 在实际应用场景中,往往等待的时间都远高于拷贝的时间,所以让IO更高效,最核心的办法就是让等待的时间尽量少?
高级IO的重要概念?
- 同步即是在发出一个调用时,在没有得到结果前,该调用就不返回,但是一旦调用返回了,就得到了返回值
- 异步则是相反,调用在发出之后,这个调用就直接返回的,所以没有返回结果。当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用?
tips:这里的同步跟进程间同步是八竿子打不着的
阻塞和非阻塞关注的时程序在等待调用结果(消息,返回值)时的状态?
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
- 非阻塞调用指在不能立即得到结果之前,该调用不会阻塞当前线程
其他的高级IO:非阻塞IO,记录锁,系统V流机制,IO多路转接,readv和writev函数以及存储映射IO(mmap)
这里我们讨论的是IO多路转接?
非阻塞IO?
一个文件描述符,默认都是阻塞IO
函数原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
根据传入的cmd的不同,后面追加的参数也不相同.
fcntl函数有5种功能:
这里我们使用第三个功能,设置文件状态标记,就可以将一个文件描述符设置为非阻塞?
基于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);
}
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
void SetNoBlock(int fd)
{
int fl = fcntl(fd,F_GETFL);
if(!fl){
perror("fcntl");
return ;
}
fcntl(fd,F_SETFL, fl | O_NONBLOCK);
}
int main()
{
SetNoBlock(0);
while(true){
char buf[1024] = {0};
ssize_t read_size = read(0,buf,sizeof(buf)-1);
if(!read_size) {
perror("read");
continue;
}
sleep(2);
printf("input:%s\n",buf);
}
return 0;
}
I/O多路转接之select?
系统提供select函数来实现多路复用输入/输出模型
#include<sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,
struct timeval *timeout);
参数timeout的取值:
关于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的全部位
?关于timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则翻书返回,返回值为0
?
函数返回值:?
?错误值可能为:
① 执行fd_set set;FD_ZERO(&set);则现在用位表示是0000 0000
②若fd = 5执行FD_SET(fd,&set)后set变为0001 0000
③若再加入fd = 2,fd = 1 则set变成0001 0011
④执行select(6,&set,0,0,0)阻塞等待
⑤若fd = 1,fd = 2上都发生可读时间,则select返回,此时set变成0000,0011
注意:这时候没有事件发生的fd = 5就会被清空
?I/O多路转接之poll
poll函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
events和revents的取值:
socket的就绪条件和上面select的一样?
对比selsect
不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现
使用poll监控标准输入示例:
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
struct pollfd poll_fd;
poll_fd.fd = 0;
poll_fd.events = POLLIN;
for (;;)
{
int ret = poll(&poll_fd, 1, 1000);
if (ret < 0)
{
perror("poll");
continue;
}
if (ret == 0)
{
printf("poll timeout\n");
continue;
}
if (poll_fd.revents == POLLIN)
{
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("stdin:%s", buf);
}
}
return 0;
}
I/O多路转接之epoll?
按照man手册的说法:是为了处理大批量句柄而做了改进的poll?
它几乎几倍了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法
epoll_create:?
int epoll_create(int size);
创建一个epoll的句柄
epoll_ctl:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数
第二个参数op的取值--三个宏
struct epoll_event的结构如下:
events可以是以下几个宏的集合:
epoll_wait:?
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
下面就是那两个重要成员:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
总结使用epoll三部曲:
epoll有2种工作方式,水平触发(LT),边缘触发(ET)?
?举个例子:
epoll默认状态下就是LT工作模式
如果在第一步将socket添加到epoll描述符中使用了EPOLLET标志,epoll进入ET工作模式
select和poll其实也是工作在LT模式下,epoll即可以支持LT 也可以支持 ET
LT是epoll的默认行为,使用ET能够减少epoll触发的次数,但是代价就是要求程序员一次响应就绪过程中就要把所有的数据都处理好
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到
每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
另一方面, ET 的代码复杂程度更高了
?
?假设一个场景:服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 则不会发送第二个10k请求.
正常流程应该这样:
此时如果服务端的代码是阻塞式的read,并且一次只read 1K的数据的话(read不能保证一次就读出所有的数据,参考man手册可能被信号打断),剩下的9k数据就会呆在缓冲区
此时由于epoll是ET模式,并不会认为文件描述符就绪。epoll_wait就不会再次返回。剩下的9K会一直在缓冲区中,直到下一次客户端再给服务器写数据,epoll_wait才能返回
但是:
所以,为了解决上述问题(阻塞read不能一下把完整的请求读完),于是就可以使用非阻塞轮询方式来读缓冲区,保证一定能把完整的请求都读出来。
而LT就没这个问题,如果LT下缓冲区没读完,就能够放epoll_wait返回的文件描述符读事件就绪,让read下次还会读取?
?epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反
例如:一个需要处理上完个客户端的服务器--互联网APP的入口服务器,这样就很适合epoll
如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll就并不合适(大炮打蚊子),具体要根据需求和场景特点来决定使用哪种IO模型
背景:在Linux 下使用epoll编写socket的服务端程序,并且使用了多线程/进程来epoll_wait监听socket。
在多线程或者多进程的环境下,部分为了提高程序的稳定性,往往会让多个线程或者多个进程同时在epoll_wait监听socket描述符。当一个新的连接请求到达时,操作系统并不知道选择哪个线程或者进程来处理此事件,就会将其中的几个线程或进程唤醒,然而实际上却只有一个线程或进程能够成功处理accept事件,其他的线程或进程都将失败,且errno错误码是EAGAIN。
这种现象称为惊群效应,会带来资源的小号和性能的影响,那么该如何解决该问题:
让其中的一个线程单独epoll_wait监听socket,当有新的连接请求到达,用该线程来调用accept事件来建立新连接,之后的数据的读写操作则交给其他工作线程去处理,这样就可以避免多线程下epoll_wait惊群效应
目前很多的开源软件,例如lighttpd,nginx等都才用了master/worders的模式提高软件的吞吐和并发能力,在nginx中还采用了负载均衡技术,在某个子进程的处理能力达到一定负载的时候,由其他负载轻的子进程负责epoll_wait调用。
lighttpd的解决思路就是无视惊群效应,仍然采用master/workers模式,每个子进程仍然在自己监听的socket上调用epoll_wait,当有新的连接请求发生,操作系统唤醒部分子进程来处理,只有一个子进程能够成功处理此事件,其他被惊醒的子进程捕捉EAGAIN错误,然后无视掉
nginx的解决思路:在同一时刻,永远都只有一个子进程在监视socket上的epoll_wait,它的做法就是创建一个全局的phread_mutex_t,在子进程进行epoll_wait前,先获取锁。也就是我们说的对临界资源的加锁,就能保证同一时刻只有一个进程能够执行epoll_wait就能够避免惊群效应
并且在代码实现上,nginx是子进程在负载在一定范围下才会去争取锁,也就是说如果一个线程的epoll_wait连接的数量达到一定数值(nginx是达到7/8时)就不会去争取锁,它的目标就是好好招待现在已经accept的事件请求.
总结:?
本章讲述了五种IO模型,并且展开讲述了IO多路转接,在Linux下使用IO转接有主要有select、poll、epoll三种当中最广泛好用的当属epoll,epoll支持ET以及LT(默认LT),但也不能无脑使用epoll。
epoll其网络模式是事件驱动,事件驱动的本质还是IO事件,应用程序在多个IO句柄之间快速切换,实现异步IO。事件驱动的服务器最适合就是IO密集型的工作,例如反向代理,在客户端和web服务端中间起到一个数据中转的作用,基本上就是纯纯IO并不涉及复杂技术,所以用epoll这种事件驱动,单进程单线程就能搞定。
至于那些cpu密集型的服务,比如跑图形图像,科学计算,或者数据库读写这样的,就不好使用单进程单线程了,还是开多线程好,这样之间互不影响(cpu密集型的处理速度一般都不会快,即使很快但是有那么一点点卡顿,数量大了之后一样效率下降很多),可以说只要有阻塞的话,那么事件IO就没有优势,这时候不如开多进程/线程去跑