首先我们要明确的是在任何IO操作中,均包含两个步骤,等待和拷贝,而在实际的业务中,等待所消耗的时间往往大于拷贝的时间,因此,让IO操作更高效,核心的方法就是将等待的时间缩短,而这两个过程是在内核当中的过程。
*
低阶IO:是指类似于将用户输入的内容读取到某个变量中,将变量中的值打印在屏幕上等等,简单来说就是对C库自己所维护的缓冲区进行I/O操作。
*
高阶IO:通常应用于网络Socket编程,对UDP(TCP)所维护的发送缓冲区和接收缓冲区进行I/O操作。并且高阶IO分为同步IO和异步IO,同步IO又分为阻塞IO、非阻塞IO、信号驱动IO和多路转接IO。
信号驱动IO:当内核将数据准备好之后,或者说告诉应用进程何时才可以开始拷贝数据,会给应用进程发送一个SIGIO的信号,通知其进行IO操作。当应用程序接收到该信号之后,证明数据已经准备好了,接下来就会调用系统调用函数对其进行相应的IO操作。
由内核在数据拷贝完成时, 通知应用程序进行相关操作(而信号驱动是当内核中数据准备好了就通知应用程序)。
注:为了性能和效率的优先,C++默认采用的是异步IO的方式。
内核帮我们监控了多个文件描述符,当某一个或者若干个文件描述符就绪的时候,就会通知调用者,调用者调用系统调用函数针对就绪的文件描述符进行操作。
首先我们要知道多路复用函数的作用是什么,其本质上就是让内核帮助程序员监控多个文件描述符的IO事件,一旦监控的某个文件描述符对应的事件产生(IO就绪),就会通知调用者,也就是说可以并行的处理多条客户端的请求,换句话说就是实现了高并发。
作用:监控多个文件描述符,就绪之后,通知调用者。
select的函数原型如下: /#include <sys/select.h>
int select(int nfds, fd_set / readfds, fd_set / writefds,fd_set /* exceptfds, struct timeval timeout);**
nfds:<select监控事件集合(fd_set)的范围,范围是从[0,1023]之间去选择范围;
*
**nfds的取值为:**监控的最大文件描述符数值+1
*
**fd_set:**事件集合类型
*
**readfds:**可读事件集合
*
**writefds:**可写事件集合
**exceptfds:**异常事件集合
*
timeout:
阻塞方式:传递NULL
非阻塞方式:传递0
带有超时时间的方式:
使用vim打开/usr/include/sys/select.h路径下查看源码:
内核在使用该数组的时候采用的是位图的方式,一共有16 / 8 / 8=1024个比特位**
fd_set事件集合占用比特位的个数和宏_FD_SETSIZE强相关,即,_FDSETSIZE多大,fd_set事件集合就有多少个比特位。
其实fd_set结构就是一个整数数组,更严格的是,是一个“位图”,使用位图中对应的位来表示要监控的文件描述符,如下图所示:
提供了一组操作fd_set的接口, 来比较方便的操作位图:
从事件集合当中删除一个文件描述符
void FD_CLR(int fd, fd_set /*set);
判断文件描述符是否在某一个事件集合当中
int FD_ISSET(int fd, fd_set /*set);
设置文件描述符到事件集合当中
void FD_SET(int fd, fd_set /*set);
清空事件集合,将所有的比特位全部置为0
void FD_ZERO(fd_set /*set);
imeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
优点:
elect遵循的是POSIX标准,说明select函数是一个跨平台的函数,既可以在Windows当中运行,也可以在Linux当中运行。
select在带有超时时间监控的时候,超时时间单位可以是微秒。。
缺点:
监控文件描述符个数的上限为1024。
随着文件描述符的增多,select监控效率在下降(本质是select在轮询进行监控)。
可读、可写、异常这些事件需要单独的添加到不同的事件集合当中。
当select监控成功之后,会从事件集合当中去除掉未就绪的文件描述符,这使程序下一次调用select时,还需要重新添加文件描述符。
在每次select进行监控的时候,都会将准备好的事件集合拷贝到内核空间,select返回的时候都会将内核空间拷贝给用户空间。
我们使用select对0号文件描述符(读缓冲区)进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来。
1 #include<iostream>
2 #include<sys/select.h>
3 #include<unistd.h>
4 #include<stdio.h>
5 using namespace std;
6 int main()
7 {
8 fd_set readfds;
9
10 FD_ZERO(&readfds);
11 FD_SET(0,&readfds);
12
13 while(1)
14 {
15 int ret=select(1,&readfds,NULL,NULL,NULL);
16
17 if(ret<0)
18 {
19 perror("select");
20 return 0;
21 }
22
23 char buf[1024]={0};
24 read(0,buf,sizeof(buf)-1);
25
26 cout<<buf<<endl;
27 }
28 return 0;
29 }
前提:均是监控多个文件描述符,就绪之后,然后通知调用者。与select相比并不支持跨平台,与epoll相比,没有epoll的效率高。
struct pollfd:事件结构
0:带有超时时间,单位:秒
==0:非阻塞
<0:阻塞
利用poll函数对系统的0号文件描述符(读缓冲区)进行监控,一旦监控到读的事件,则将其读入的内容打印到屏幕上。
1 #include<iostream>
2 #include<stdio.h>
3 #include<poll.h>
4 #include<unistd.h>
5
6 using namespace std;
7 int main()
8 {
9 struct pollfd pf;
10
11 pf.fd=0;
12 pf.events=POLLIN;
13
14 while(1)
15 {
16 int ret=poll(&pf,1,-1);
17 if(ret<0)
18 {
19 perror("poll");
20 }
21 else if(ret==0)
22 {
23 cout<<"TimeOut!"<<endl;
24 sleep(1);
25 continue;
26 }
27 char buf[1024]={0};
28 read(0,buf,sizeof(buf)-1);
29 cout<<buf<<endl;
30
31 }
32 return 0;
33 }
epoll函数是目前世界上公认在Linux下,多路转接监控效率最高的模型。
① 创建epoll操作句柄
② 注册待要监控的文件描述符
**epfd:**epoll操作句柄。
*
**op:**告诉epoll要做什么是事。
① EPOLL_CTL_ADD:添加一个文件描述符对应的事件结构到epoll当中。
② EPOLL_CTL_MOD:修改一个文件描述符的事件结构。
③ EPOLL_CTL_DEL:从epoll当中删除一个文件描述符对应的事件结构。
**fd:**待处理(添加、修改、删除)的文件描述符。
*
**event:**文件描述符对应的事件结构。
*
epoll_event结构体
返回值
③ epoll的等待接口
当某一个进程调用epoll_create函数时,LInux内核会创建一个eventpoll的结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
当调用epoll_create函数时,会在内核创建一个eventpoll结构体,在该结构体中有一个rdlist成员和rbr成员,它两分别是一个双向链表和红黑树,而调用epoll_ctl函数添加、修改、删除文件描述符对应的事件集合其实是对红黑树中的节点进行相应的添加、修改、删除操作,而所有添加到epoll的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当文件描述符准备就绪后,内核会回调ep_poll_callback函数,将准备就绪的事件集合添加到rdlist双向链表中,而当调用epoll_wait进行监控的时候,如果双向链表为空,则表明当前没有就绪的事件发生,如果不为空,则将双向链表中的内容复制到用户态,并返回将事件数量返回给用户。
【注意】:这里的双向链表其实实现的是一个队列,虽然是一个双向链表,但是他只支持先进先出(FIFO),是队列的特性。每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。
我们使用epoll对0号文件描述符进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来。
1 #include<iostream>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<sys/epoll.h>
5
6 using namespace std;
7 int main()
8 {
9 int epfd=epoll_create(3);
10 if(epfd<0)
11 {
12 perror("epoll_create");
13 return 0;
14 }
15
16 struct epoll_event ee;
17 ee.events=EPOLLIN;
18 ee.data.fd=0;
19 epoll_ctl(epfd,EPOLL_CTL_ADD,0,&ee);
20
21 while(1)
22 {
23 struct epoll_event arr[2];
24 int ret=epoll_wait(epfd,arr,sizeof(arr)/sizeof(arr[0]),-1);
25 if(ret<0)
26 {
27 perror("epoll_wait");
28 continue;
29 }
30
31 for(int i=0;i<ret;i++)
32 {
33 if(arr[i].events==EPOLLIN)
34 {
35 char buf[1024]={0};
36
37 read(arr[i].data.fd,buf,sizeof(buf)-1);
38 cout<<buf<<endl;
39 }
40 }
41 }
42 return 0;
43 }
select、poll、epoll对比:
① LT(Level Triggered) 水平触发工作模式
② ET(Edge Triggered) 边缘触发工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式。
在ET模式下,当epoll检测到事件就绪的时候,会立即进行处理,并且只会处理一次,换句话说就是文件描述符上的事件就绪之后,只有一次处理机会。 简单来说就是如果事件来了,不管来了几个,你若不处理或者没有处理完,除非下一个事件到来,否则epoll将不会再通知你。ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll。只支持非阻塞的读写。
LT模式存在的问题:
如果可读或者可写事件未进行处理,会频繁反复的激活未处理事件。
LT模式存在的问题解决方法:
在不想处理某个事件的时候就将它从epoll中移除,需要时再添加上。
ET模式存在的问题:
如果可读或者可写事件没有全部处理,会有老数据残留,需要等待新数据的到来才会被处理。
ET模式存在的问题解决方法:
循环读取或者写入数据,直至返回值未EAGAIN或者EWOULDBLOCK(循环调用)。
读取或写入数据后,通过epoll_ctl设置EPOLL_CTL_MOD,激活未处理事件(相当于将当前未处理事件设置未新事件)。
💬 总结
以上就是今天要讲的内容,本文详细介绍了Linux高级IO中的5中ION模型的原理及用法,高级IO提供了大量的方法供我们使用,非常的便捷,我们务必掌握。希望大家多多支持!另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。加油啊!
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/qq_44918090/article/details/119838006
内容来源于网络,如有侵权,请联系作者删除!