【Linux从青铜到王者】第二十二篇:Linux高级IO

【Linux从青铜到王者】第二十二篇:Linux高级IO_第1张图片

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、五种IO模型
      • 1.阻塞IO
      • 2.非阻塞IO
      • 3.信号驱动IO
      • 4.异步IO
      • 5.IO多路转接
  • 二、高级IO重要概念
      • 1.同步通信vs异步通信
      • 2.阻塞vs非阻塞
  • 三、I/O多路转接之select
      • 1.select函数的作用
      • 2.select函数的原型
      • 3.fd_set结构解释
      • 4.关于timeval结构体
      • 5.select函数返回值
      • 6.select函数优缺点
      • 7.select函数监控代码
  • 四、I/O多路转接之poll
      • 1.函数原型及参数解释
      • 2.pool的优缺点
      • 3.poll的代码验证
  • 五、I/O多路转接之epoll
      • 1.epoll相关系统调用
      • 2.epoll的原理
      • 3.epoll的优点
      • 4.epoll的简单代码
      • 5.epoll的工作方式
      • 6.对比ET和LT
        • 1.epoll的LT的代码
        • 2.epoll的ET的代码
      • 7.epoll的LT模式的tcp代码
      • 8.epoll的ET模式的tcp代码
  • 总结


前言


【Linux从青铜到王者】第二十二篇:Linux高级IO_第2张图片

一、五种IO模型

【Linux从青铜到王者】第二十二篇:Linux高级IO_第3张图片
首先我们要明确的是在任何IO操作中,均包含两个步骤,等待和拷贝,而在实际的业务中,等待所消耗的时间往往大于拷贝的时间,因此,让IO操作更高效,核心的方法就是将等待的时间缩短,而这两个过程是在内核当中的过程。

  • 低阶IO:是指类似于将用户输入的内容读取到某个变量中,将变量中的值打印在屏幕上等等,简单来说就是对C库自己所维护的缓冲区进行I/O操作。

  • 高阶IO:通常应用于网络Socket编程,对UDP(TCP)所维护的发送缓冲区和接收缓冲区进行I/O操作。并且高阶IO分为同步IO和异步IO,同步IO又分为阻塞IO、非阻塞IO、信号驱动IO和多路转接IO。

1.阻塞IO

【Linux从青铜到王者】第二十二篇:Linux高级IO_第4张图片

  • 阻塞IO:在内核将数据准备好之前(等待+拷贝),系统调用会一直进行等待。并且所有的套接字默认均是阻塞方式。
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第5张图片

2.非阻塞IO

【Linux从青铜到王者】第二十二篇:Linux高级IO_第6张图片

  • 非阻塞IO:若当前内核没有将数据准备好,则系统调用会直接返回,并返回一个EWOULDBLOCK错误码。
  • 非阻塞IO一般都是搭配循环来使用的(也叫轮询),这对系统资源是较大的浪费,一般都是在特定的场景下使用。
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第7张图片

3.信号驱动IO

【Linux从青铜到王者】第二十二篇:Linux高级IO_第8张图片

信号驱动IO:当内核将数据准备好之后,或者说告诉应用进程何时才可以开始拷贝数据,会给应用进程发送一个SIGIO的信号,通知其进行IO操作。当应用程序接收到该信号之后,证明数据已经准备好了,接下来就会调用系统调用函数对其进行相应的IO操作。
【Linux从青铜到王者】第二十二篇:Linux高级IO_第9张图片

4.异步IO

【Linux从青铜到王者】第二十二篇:Linux高级IO_第10张图片

由内核在数据拷贝完成时, 通知应用程序进行相关操作(而信号驱动是当内核中数据准备好了就通知应用程序)。
【Linux从青铜到王者】第二十二篇:Linux高级IO_第11张图片
注:为了性能和效率的优先,C++默认采用的是异步IO的方式。

5.IO多路转接

内核帮我们监控了多个文件描述符,当某一个或者若干个文件描述符就绪的时候,就会通知调用者,调用者调用系统调用函数针对就绪的文件描述符进行操作。【Linux从青铜到王者】第二十二篇:Linux高级IO_第12张图片

二、高级IO重要概念

1.同步通信vs异步通信

  • 同步和异步关注的是消息通信机制。
  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果。
  • 异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第13张图片
  • 另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念。
  • 进程/线程同步也是进程/线程之间直接的制约关系。
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候 。
  • 我们以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步。

2.阻塞vs非阻塞

  • 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
    在这里插入图片描述

三、I/O多路转接之select

【Linux从青铜到王者】第二十二篇:Linux高级IO_第14张图片
首先我们要知道多路复用函数的作用是什么,其本质上就是让内核帮助程序员监控多个文件描述符的IO事件,一旦监控的某个文件描述符对应的事件产生(IO就绪),就会通知调用者,也就是说可以并行的处理多条客户端的请求,换句话说就是实现了高并发。

1.select函数的作用

作用:监控多个文件描述符,就绪之后,通知调用者。

2.select函数的原型

select的函数原型如下: #include

int select(int nfds, fd_set * readfds, fd_set * writefds,fd_set * exceptfds, struct timeval timeout);

  • 参数解释:
  • 参数nfds是需要监视的最大的文件描述符值+1。
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
  • 参数timeout为结构timeval,用来设置select()的等待时间。
  • 参数timeout取值:
  • NULL:则表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件。
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。

【Linux从青铜到王者】第二十二篇:Linux高级IO_第15张图片

  • nfds:

  • nfds的取值为:监控的最大文件描述符数值+1

  • fd_set:事件集合类型

  • readfds:可读事件集合

  • writefds:可写事件集合
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第16张图片

  • exceptfds:异常事件集合

  • timeout:
        阻塞方式:传递NULL
        非阻塞方式:传递0
        带有超时时间的方式:
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第17张图片

3.fd_set结构解释

【Linux从青铜到王者】第二十二篇:Linux高级IO_第18张图片
使用vim打开/usr/include/sys/select.h路径下查看源码:

【Linux从青铜到王者】第二十二篇:Linux高级IO_第19张图片
【Linux从青铜到王者】第二十二篇:Linux高级IO_第20张图片
【Linux从青铜到王者】第二十二篇:Linux高级IO_第21张图片
【Linux从青铜到王者】第二十二篇:Linux高级IO_第22张图片
内核在使用该数组的时候采用的是位图的方式,一共有16 * 8 * 8=1024个比特位
【Linux从青铜到王者】第二十二篇:Linux高级IO_第23张图片
fd_set事件集合占用比特位的个数和宏_FD_SETSIZE强相关,即,_FDSETSIZE多大,fd_set事件集合就有多少个比特位。

其实fd_set结构就是一个整数数组,更严格的是,是一个“位图”,使用位图中对应的位来表示要监控的文件描述符,如下图所示:
【Linux从青铜到王者】第二十二篇:Linux高级IO_第24张图片

  • 如上图所示。
  • 如果关心某个文件描述符对应的某个事件,则将文件描述符添加到对应的事件集合当中。eg:关心3号文件描述符的可读事件,则将3号文件描述符添加到read_fds当中。
  • 添加文件描述符到事件集合的时候,是将文件描述符对应的比特位设置为1。
  • 如果一个文件描述符关心多种事件(可读,可写,异常),则将文件描述符添加到不同的事件集合当中。
  • select的监控效率会随着监控文件描述符的增多而下降,本质原因是由于监控轮询的范围变大了。

提供了一组操作fd_set的接口, 来比较方便的操作位图:
【Linux从青铜到王者】第二十二篇:Linux高级IO_第25张图片
从事件集合当中删除一个文件描述符

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);
在这里插入图片描述

4.关于timeval结构体

【Linux从青铜到王者】第二十二篇:Linux高级IO_第26张图片

imeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
【Linux从青铜到王者】第二十二篇:Linux高级IO_第27张图片

5.select函数返回值

【Linux从青铜到王者】第二十二篇:Linux高级IO_第28张图片
在这里插入图片描述

  • 监控成功:返回就绪的文件描述符个数,会将事件集合当中未就绪的文件描述符去掉。
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第29张图片
  • 监控失败:当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测。
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。
  • 【注意】:select返回之后,需要重新添加文件描述符。

6.select函数优缺点

【Linux从青铜到王者】第二十二篇:Linux高级IO_第30张图片

  • 优点:
  • elect遵循的是POSIX标准,说明select函数是一个跨平台的函数,既可以在Windows当中运行,也可以在Linux当中运行。
  • select在带有超时时间监控的时候,超时时间单位可以是微秒。。
  • 缺点:
  • 监控文件描述符个数的上限为1024。
  • 随着文件描述符的增多,select监控效率在下降(本质是select在轮询进行监控)。
  • 可读、可写、异常这些事件需要单独的添加到不同的事件集合当中。
  • 当select监控成功之后,会从事件集合当中去除掉未就绪的文件描述符,这使程序下一次调用select时,还需要重新添加文件描述符。
    • 在每次select进行监控的时候,都会将准备好的事件集合拷贝到内核空间,select返回的时候都会将内核空间拷贝给用户空间。

7.select函数监控代码

【Linux从青铜到王者】第二十二篇:Linux高级IO_第31张图片


【Linux从青铜到王者】第二十二篇:Linux高级IO_第32张图片
【Linux从青铜到王者】第二十二篇:Linux高级IO_第33张图片
我们使用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	}

【Linux从青铜到王者】第二十二篇:Linux高级IO_第34张图片

四、I/O多路转接之poll

【Linux从青铜到王者】第二十二篇:Linux高级IO_第35张图片
前提:均是监控多个文件描述符,就绪之后,然后通知调用者。与select相比并不支持跨平台,与epoll相比,没有epoll的效率高。

1.函数原型及参数解释

【Linux从青铜到王者】第二十二篇:Linux高级IO_第36张图片

struct pollfd:事件结构
【Linux从青铜到王者】第二十二篇:Linux高级IO_第37张图片

  • 想让poll监控多个文件描述符,只需要在定义事件结构数组的时候,多传递几个元素
  • eg:struct pollfd arr[10];
      arr[0].fd = 0;
      arr[0].events = POLLIN;
  • nfds:事件结构数组中有效元素的个数
  • timeout:
      >0:带有超时时间,单位:秒
      ==0:非阻塞
      <0:阻塞
  • 返回值:就绪文件描述符的个数

2.pool的优缺点

【Linux从青铜到王者】第二十二篇:Linux高级IO_第38张图片

  • 优点:
  • 提出了事件结构的方式,在给poll函数传递参数的时候,不需要分别添加到“事件集合”中。
  • 事件结构数组的大小可以根据程序员自己进行定义,并没有上限要求。
  • 不用在监控到就绪文件描述符之后,重新添加文件描述符。
  • 缺点:
  • 不支持跨平台。
  • 内核对事件结构数组的监控也是采用轮询遍历的方式,即随着监控文件描述符的增多,监控效率会下降。
  • 每次调用poll都需要把大量的pollf结构从用户态拷贝到内核态,poll返回的时候,会将内核空间拷贝给用户空间(从内核态到用户态会调用do_signal会有开销)。

3.poll的代码验证

利用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	}

【Linux从青铜到王者】第二十二篇:Linux高级IO_第39张图片

五、I/O多路转接之epoll

在这里插入图片描述

epoll函数是目前世界上公认在Linux下,多路转接监控效率最高的模型。

1.epoll相关系统调用

【Linux从青铜到王者】第二十二篇:Linux高级IO_第40张图片
① 创建epoll操作句柄

【Linux从青铜到王者】第二十二篇:Linux高级IO_第41张图片

  • size自从Linux2.6.8之后,size参数是被忽略的;但不要传递小于0的数字
  • 返回值:返回epoll的操作句柄

② 注册待要监控的文件描述符
【Linux从青铜到王者】第二十二篇:Linux高级IO_第42张图片
在这里插入图片描述

  • epfd:epoll操作句柄。

  • op:告诉epoll要做什么是事。
     ① EPOLL_CTL_ADD:添加一个文件描述符对应的事件结构到epoll当中。
     ② EPOLL_CTL_MOD:修改一个文件描述符的事件结构。
     ③ EPOLL_CTL_DEL:从epoll当中删除一个文件描述符对应的事件结构。
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第43张图片

  • fd:待处理(添加、修改、删除)的文件描述符。

  • event:文件描述符对应的事件结构。

  • epoll_event结构体
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第44张图片

  • 返回值
    在这里插入图片描述

③ epoll的等待接口
【Linux从青铜到王者】第二十二篇:Linux高级IO_第45张图片
在这里插入图片描述

  • epfd:epoll的操作句柄。
  • events:时间结构数组(集合),从epoll当中获取就绪的事件结构。
  • maxevents:最多一次获取多少个事件结构。
  • timeout:
      0:带有超时事件
      ==0:非阻塞
      <0:阻塞
  • 返回值:就绪的文件描述符个数。

2.epoll的原理

当某一个进程调用epoll_create函数时,LInux内核会创建一个eventpoll的结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

【Linux从青铜到王者】第二十二篇:Linux高级IO_第46张图片
当调用epoll_create函数时,会在内核创建一个eventpoll结构体,在该结构体中有一个rdlist成员和rbr成员,它两分别是一个双向链表和红黑树,而调用epoll_ctl函数添加、修改、删除文件描述符对应的事件集合其实是对红黑树中的节点进行相应的添加、修改、删除操作,而所有添加到epoll的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当文件描述符准备就绪后,内核会回调ep_poll_callback函数,将准备就绪的事件集合添加到rdlist双向链表中,而当调用epoll_wait进行监控的时候,如果双向链表为空,则表明当前没有就绪的事件发生,如果不为空,则将双向链表中的内容复制到用户态,并返回将事件数量返回给用户。

【注意】:这里的双向链表其实实现的是一个队列,虽然是一个双向链表,但是他只支持先进先出(FIFO),是队列的特性。每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。

3.epoll的优点

  • 事件回调机制,当文件描述符就绪之后,会调用回调函数将事件结构复制到双向链表中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响。
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)。
  • 没有数量限制: 文件描述符数目无上限。
  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开。
    【Linux从青铜到王者】第二十二篇:Linux高级IO_第47张图片

4.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	}

【Linux从青铜到王者】第二十二篇:Linux高级IO_第48张图片
select、poll、epoll对比:
【Linux从青铜到王者】第二十二篇:Linux高级IO_第49张图片

5.epoll的工作方式

【Linux从青铜到王者】第二十二篇:Linux高级IO_第50张图片

  • 举个例子:当你在中午饭点玩游戏的时候,如果这个时候饭刚好做好了。
  • LT:家里人第一次通知的时候,你没有动,那他们还会通知第二次、第三次…
  • ET:家里人在第一次通知的时候,你没有动,那么他们就不会在通知你了。

① LT(Level Triggered) 水平触发工作模式

  • epoll默认状态下就是LT工作模式。
  • 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分。
  • 如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪。
  • 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回。
  • 支持阻塞读写和非阻塞读写。

在LT模式下,当epoll检测到事件就绪的时候,可以不处理或处理一部分,但是可以连续多次调用epoll_wait对事件进行处理,简单点来说的话就是如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你

② 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从青铜到王者】第二十二篇:Linux高级IO_第51张图片

6.对比ET和LT

【Linux从青铜到王者】第二十二篇:Linux高级IO_第52张图片
【Linux从青铜到王者】第二十二篇:Linux高级IO_第53张图片

1.epoll的LT的代码
     1	/*================================================================
     2	  ================================================================*/
     3	
     4	#include <stdio.h>
     5	#include <unistd.h>
     6	#include <sys/epoll.h>
     7	#include <fcntl.h>
     8	#include <errno.h>
     9	#include <string>
    10	/*
    11	 * 1.监控0号文件描述符对应的可读事件
    12	 */
    13	
    14	using namespace std;
    15	
    16	int main()
    17	{
    18	    //1.创建epoll操作句柄
    19	    int epfd = epoll_create(5);
    20	    if(epfd < 0)
    21	    {
    22	        perror("epoll_create:");
    23	        return 0;
    24	    }
    25	
    26	    //2.添加0号文件描述符对应的事件结构到epoll当中
    27	
    28	    struct epoll_event ee;
    29	    ee.events = EPOLLIN ;
    30	    //ee.data.fd = 0 : 当监控成功之后, 从双向链表当中将事件结构拷贝到用户空间的事件结构数组当中之后
    31	    //程序员可以通过该结构当中的fd, 知道该事件结构属于哪一个文件描述符
    32	    ee.data.fd = 0;
    33	
    34	    epoll_ctl(epfd, EPOLL_CTL_ADD,  0,  &ee);
    35	
    36	    //epoll等待
    37	    int count = 0;
    38	    while(1)
    39	    {
    40	        struct epoll_event arr[10];
    41	        //int ret = epoll_wait(epfd, arr, 10, -1);
    42	        //int ret = epoll_wait(epfd, arr, 10, 1000);
    43	        int ret = epoll_wait(epfd, arr, 10, 0);
    44	        if(ret < 0)
    45	        {
    46	            perror("epoll_wait:");
    47	            return 0;
    48	        }
    49	        else if(ret == 0)
    50	        {
    51	            //printf("timeout: %d\n", count++);
    52	            continue;
    53	        }
    54	
    55	
    56	        char buf[2] = {0};
    57	        read(arr[0].data.fd, buf, sizeof(buf) - 1);
    58	        //printf("ret : %d\n", ret);
    59	        //a b c 
    60	        //
    61	        printf("%s ", buf);
    62	        printf("count: %d\n", count++);
    63	
    64	    }
    65	    return 0;
    66	}

【Linux从青铜到王者】第二十二篇:Linux高级IO_第54张图片

2.epoll的ET的代码
    11	#include <stdio.h>
    12	#include <unistd.h>
    13	#include <sys/epoll.h>
    14	#include <fcntl.h>
    15	#include <errno.h>
    16	#include <string>
    17	/*
    18	 * 1.监控0号文件描述符对应的可读事件
    19	 */
    20	
    21	using namespace std;
    22	
    23	int main()
    24	{
    25	    int flag = fcntl(0, F_GETFL);
    26	    fcntl(0, F_SETFL, flag | O_NONBLOCK);
    27	
    28	
    29	    //1.创建epoll操作句柄
    30	    int epfd = epoll_create(5);
    31	    if(epfd < 0)
    32	    {
    33	        perror("epoll_create:");
    34	        return 0;
    35	    }
    36	
    37	    //2.添加0号文件描述符对应的事件结构到epoll当中
    38	
    39	    struct epoll_event ee;
    40	    ee.events = EPOLLIN | EPOLLET;
    41	    //ee.data.fd = 0 : 当监控成功之后, 从双向链表当中将事件结构拷贝到用户空间的事件结构数组当中之后
    42	    //程序员可以通过该结构当中的fd, 知道该事件结构属于哪一个文件描述符
    43	    ee.data.fd = 0;
    44	
    45	    epoll_ctl(epfd, EPOLL_CTL_ADD,  0,  &ee);
    46	
    47	    //epoll等待
    48	    int count = 0;
    49	    while(1)
    50	    {
    51	        struct epoll_event arr[10];
    52	        //int ret = epoll_wait(epfd, arr, 10, -1);
    53	        //int ret = epoll_wait(epfd, arr, 10, 1000);
    54	        int ret = epoll_wait(epfd, arr, 10, 0);
    55	        if(ret < 0)
    56	        {
    57	            perror("epoll_wait:");
    58	            return 0;
    59	        }
    60	        else if(ret == 0)
    61	        {
    62	            //printf("timeout: %d\n", count++);
    63	            continue;
    64	        }
    65	
    66	       
    67	        string str;
    68	        while(1)
    69	        {
    70	            char buf[2] = {0};
    71	            int ret = read(arr[0].data.fd, buf, sizeof(buf) - 1);
    72	            //printf("ret : %d\n", ret);
    73	            if(ret < 0)
    74	            {
    75	                if(errno == EAGAIN || errno == EWOULDBLOCK)
    76	                {
    77	                   break;
    78	                }
    79	            }
    80	            //a b c 
    81	            //
    82	            printf("%s ", buf);
    83	            str.append(buf);
    84	            //printf("count: %d\n", count++);
    85	        }
    86	
    87	        printf("str : %s\n", str.c_str());
    88	    }
    89	    return 0;
    90	}

【Linux从青铜到王者】第二十二篇:Linux高级IO_第55张图片

7.epoll的LT模式的tcp代码

在这里插入图片描述

     1	/*================================================================
     2	*
     3	================================================================*/
     4	
     5	#include <stdio.h>
     6	#include <unistd.h>
     7	#include <string.h>
     8	#include <sys/epoll.h>
     9	#include <sys/socket.h>
    10	#include <arpa/inet.h>
    11	#include <netinet/in.h>
    12	
    13	/*
    14	 * 1.实现单线程的tcp服务端
    15	 *
    16	 *
    17	 * 2.在单线程tcp服务端基础上, 添加epoll代码(让epoll监控两种文件描述符 侦听套接字 & 新连接套接字)
    18	 *   epoll监控的文件描述符 = 1个侦听套接字 + 若干个新连接的套接字
    19	 *        侦听套接字(读事件): 一旦读事件就绪, 表示当前有新连接三次握手建立连接了, 调用accept函数处理读事件
    20	 *        新连接的套接字(读事件) : 一旦读事件就绪了,表示当前客户端给服务端发送消息了, 调用recv函数处理读事件
    21	 * */
    22	
    23	int main()
    24	{
    25	    int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    26	    if(listen_sockfd < 0){
    27	        perror("socket:");
    28	        return 0;
    29	    }
    30	
    31	    struct sockaddr_in addr;
    32	    addr.sin_family = AF_INET;
    33	    addr.sin_port = htons(45678);
    34	    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    35	
    36	    int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
    37	    if(ret < 0){
    38	        perror("bind:");
    39	        return 0;
    40	    }
    41	
    42	    ret = listen(listen_sockfd, 5);
    43	    if(ret < 0){
    44	        perror("listen: ");
    45	        return 0;
    46	    }
    47	
    48	    /*
    49	     * 1.创建epoll操作句柄
    50	     * 2.添加侦听套接字, 让epoll进行监控
    51	     * 3.监控到侦听套接字的读事件之后, 调用accept函数处理读事件(接收新连接)
    52	     *  区分到底是新连接套接字还是侦听套接字, 分别处理
    53	     *    3.1 将新连接的套接字添加到epoll当种进行监控
    54	     *
    55	     *
    56	     *  如果说是新连接套接字的读事件产生, 则接收数据
    57	     *
    58	     * */
    59	
    60	
    61	    int epoll_fd = epoll_create(5);
    62	    if(epoll_fd < 0){
    63	        perror("epoll_create");
    64	        return 0;
    65	    }
    66	
    67	    struct epoll_event ee;
    68	    ee.events = EPOLLIN;
    69	    ee.data.fd = listen_sockfd;
    70	    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sockfd, &ee);
    71	
    72	    while(1){
    73	        struct epoll_event arr[10];
    74	        int ret = epoll_wait(epoll_fd, arr, 10, -1);
    75	        if(ret < 0){
    76	            perror("epoll_wait:");
    77	            continue;
    78	        }else if(ret == 0){
    79	            continue;
    80	        }
    81	        //一定是由epoll监控的文件描述符, 有对应的就绪事件产生了
    82	        for(int i = 0; i < ret; i++){
    83	            if(arr[i].data.fd == listen_sockfd){
    84	                //accept
    85	                int new_sockfd = accept(listen_sockfd, NULL, NULL);
    86	                if(new_sockfd < 0){
    87	                    perror("accept:");
    88	                    continue;
    89	                }
    90	
    91	                struct epoll_event new_ee;
    92	                new_ee.events = EPOLLIN;
    93	                new_ee.data.fd = new_sockfd;
    94	                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_sockfd, &new_ee);
    95	            }else{
    96	                int new_sockfd = arr[i].data.fd;
    97	                char buf[1024] = {0};
    98	                ssize_t recv_size = recv(new_sockfd, buf, sizeof(buf), 0);
    99	                if(recv_size < 0){
   100	                    perror("recv:");
   101	                    continue;
   102	                }else if(recv_size == 0){
   103	                    printf("peershutdown connect\n");
   104	                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, new_sockfd, NULL);
   105	                    close(new_sockfd);
   106	                    continue;
   107	                }
   108	                printf("buf: %s\n", buf);
   109	                memset(buf, '\0', sizeof(buf));
   110	                strcpy(buf, "hello, i am server~~");
   111	                send(new_sockfd, buf, strlen(buf), 0);
   112	            }
   113	        }
   114	    }
   115	
   116	    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, listen_sockfd, NULL);
   117	    close(listen_sockfd);
   118	    return 0;
   119	}

8.epoll的ET模式的tcp代码

【Linux从青铜到王者】第二十二篇:Linux高级IO_第56张图片

 1	/*================================================================
     2	================================================================*/
     3	
     4	#include <stdio.h>
     5	#include <unistd.h>
     6	#include <string.h>
     7	#include <fcntl.h>
     8	#include <sys/epoll.h>
     9	#include <sys/socket.h>
    10	#include <arpa/inet.h>
    11	#include <netinet/in.h>
    12	#include <string>
    13	
    14	using namespace std;
    15	
    16	/*
    17	 * 1.实现单线程的tcp服务端
    18	 *
    19	 *
    20	 * 2.在单线程tcp服务端基础上, 添加epoll代码(让epoll监控两种文件描述符 侦听套接字 & 新连接套接字)
    21	 *   epoll监控的文件描述符 = 1个侦听套接字 + 若干个新连接的套接字
    22	 *        侦听套接字(读事件): 一旦读事件就绪, 表示当前有新连接三次握手建立连接了, 调用accept函数处理读事件
    23	 *        新连接的套接字(读事件) : 一旦读事件就绪了,表示当前客户端给服务端发送消息了, 调用recv函数处理读事件
    24	 * */
    25	
    26	int main()
    27	{
    28	    int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    29	    if(listen_sockfd < 0){
    30	        perror("socket:");
    31	        return 0;
    32	    }
    33	
    34	    struct sockaddr_in addr;
    35	    addr.sin_family = AF_INET;
    36	    addr.sin_port = htons(45678);
    37	    addr.sin_addr.s_addr = inet_addr("0.0.0.0");
    38	
    39	    int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
    40	    if(ret < 0){
    41	        perror("bind:");
    42	        return 0;
    43	    }
    44	
    45	    ret = listen(listen_sockfd, 5);
    46	    if(ret < 0){
    47	        perror("listen: ");
    48	        return 0;
    49	    }
    50	
    51	    /*
    52	     * 1.创建epoll操作句柄
    53	     * 2.添加侦听套接字, 让epoll进行监控
    54	     * 3.监控到侦听套接字的读事件之后, 调用accept函数处理读事件(接收新连接)
    55	     *  区分到底是新连接套接字还是侦听套接字, 分别处理
    56	     *    3.1 将新连接的套接字添加到epoll当种进行监控
    57	     *
    58	     *
    59	     *  如果说是新连接套接字的读事件产生, 则接收数据
    60	     *
    61	     * */
    62	
    63	
    64	    int epoll_fd = epoll_create(5);
    65	    if(epoll_fd < 0){
    66	        perror("epoll_create");
    67	        return 0;
    68	    }
    69	
    70	    struct epoll_event ee;
    71	    ee.events = EPOLLIN;
    72	    ee.data.fd = listen_sockfd;
    73	    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sockfd, &ee);
    74	
    75	    while(1){
    76	        struct epoll_event arr[10];
    77	        int ret = epoll_wait(epoll_fd, arr, 10, -1);
    78	        if(ret < 0){
    79	            perror("epoll_wait:");
    80	            continue;
    81	        }else if(ret == 0){
    82	            continue;
    83	        }
    84	        //一定是由epoll监控的文件描述符, 有对应的就绪事件产生了
    85	        for(int i = 0; i < ret; i++){
    86	            if(arr[i].data.fd == listen_sockfd){
    87	                //accept
    88	                int new_sockfd = accept(listen_sockfd, NULL, NULL);
    89	                if(new_sockfd < 0){
    90	                    perror("accept:");
    91	                    continue;
    92	                }
    93	
    94	                int flag = fcntl(new_sockfd, F_GETFL);
    95	                fcntl(new_sockfd, F_SETFL, flag | O_NONBLOCK);
    96	
    97	                struct epoll_event new_ee;
    98	                new_ee.events = EPOLLIN | EPOLLET;
    99	                new_ee.data.fd = new_sockfd;
   100	                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_sockfd, &new_ee);
   101	            }else{
   102	                int new_sockfd = arr[i].data.fd;
   103	                string recv_buf;
   104	                int flag = true;
   105	
   106	                while(1){
   107	                    char buf[2] = {0};
   108	                    ssize_t recv_size = recv(new_sockfd, buf, sizeof(buf) - 1, 0);
   109	                    if(recv_size < 0){
   110	                        if(errno == EAGAIN || errno == EWOULDBLOCK){
   111	                            flag = true;
   112	                            break;
   113	                        }
   114	                        perror("recv:");
   115	                        flag = false;
   116	                        break;
   117	                    }else if(recv_size == 0){
   118	                        printf("peershutdown connect\n");
   119	                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, new_sockfd, NULL);
   120	                        close(new_sockfd);
   121	                        flag = false;
   122	                        break;
   123	                    }
   124	                    //printf("%s\n", buf);
   125	                    recv_buf.append(buf);
   126	                }
   127	
   128	                if(flag == true){
   129	                    printf("recv_buf: %s\n", recv_buf.c_str());
   130	                    char buf[1024] = {0};
   131	                    memset(buf, '\0', sizeof(buf));
   132	                    strncpy(buf, "hello, i am server~~", sizeof(buf));
   133	                    send(new_sockfd, buf, strlen(buf), 0);
   134	               }
   135	            }
   136	        }
   137	    }
   138	
   139	    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, listen_sockfd, NULL);
   140	    close(listen_sockfd);
   141	    return 0;
   142	}

总结

以上就是今天要讲的内容,本文详细介绍了Linux高级IO中的5中ION模型的原理及用法,高级IO提供了大量的方法供我们使用,非常的便捷,我们务必掌握。希望大家多多支持!另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。加油啊!

【Linux从青铜到王者】第二十二篇:Linux高级IO_第57张图片

你可能感兴趣的:(Linux,操作系统)