常用IO模型及IO多路复用原理

目录

一、IO的概念

二、IO过程

三、linux下五大IO模型

1、阻塞IO

     2、非阻塞IO

      3、IO多路复用

        4、信号驱动IO

        5、异步IO模型

 四、IO多路复用原理

        1、select系统调用

        2、epoll


一、IO的概念

IO是input(输入)和output(输出)的缩写。计算机作为信息处理工具,首先要通过输入设备接收外界的输入,然后经过cpu的计算,最后通过输出设备把结果输出。因此,io设备是计算机体系结构中不可或缺的组成部分。常用的输入设备有:键盘、鼠标、扫描仪、光笔、摄像头、磁盘等;常用的输出设备有:显示屏、打印机、磁盘等。

二、IO过程

在linux系统下,应用程序通过系统调用 完成io。系统调用是内核提供给用户程序的接口,用户程序通过系统调用完成特定的工作,而不必关系硬件的细节。常用的系统调用如fork()创建进程,open()打开文件,read()、write()读写文件。系统调用的主要作用有:

⑴为用户空间提供了一种硬件的抽象接口

(2)保证了系统的安全和稳定

(3)为进程的运行提供虚拟化的环境

应用程序调用标准io库函数printf() 的完整流程

三、linux下五大IO模型

  • 阻塞式io模型(blocking I/O)
  • 非阻塞I/O模型

  • IO复用模型

  • 信号驱动式I/O模型
  • 异步I/O模型

    1、阻塞IO

       应用进程通过系统调用读取(或写入)数据,当数据还未准备好时,进程会进入睡眠,即阻塞在系统调用,直到数据准备好,系统调用返回。进程拿到数据后进行后续的处理

     2、非阻塞IO

        进程调用系统调用时通知内核,当请求的I/O操作需要把进程投入睡眠等待时,不把进程投入睡眠,而是返回一个错误。这样进程可以选择继续调用系统调用进行轮询(polling),或者转而处理其他事情。

      3、IO多路复用

io复用可以监听、等待多个描述符

        4、信号驱动IO

        进程通过sigaction系统调用安装一个信号处理函数,该系统调用立即返回,进程继续工作。当数据报准备好时,内核就为进程产生一个SIGIO信号。我们随后就可以在信号处理函数中调用recvfrom读取数据报。这种模型的优势在于,等待数据报到达期间进程不被阻塞。

        5、异步IO模型

        异步IO由POSIX规范定义。这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。

其中第1到第4种IO模型属于同步IO,因为调用read从内核复制数据到用户空间时,进程时阻塞的;只有第5种IO模型属于异步IO

 四、IO多路复用原理

内核提供的IO多路复用机制主要有select、poll、epoll。

        1、select系统调用

            函数原型:

        int  select(int  maxfdp1,  fd_set  *readset,  fd_set  *writeset,  fd_set  *excepset,  const  struct   timeval  *timeout);

        其中:maxfdp1参数(通常不能超过1024,要设置比1024更大的值需要修改内核代码然后重新编译内核)指定待测试的描述符个数,它的值是待测试的最大描述符加1。

fd_set 是文件描述符集,readset是值-结果参数,传递关心的描述符,函数返回时包含关心的描述符IO事件。指明关系的文件描述符。

timeout参数指明等待时长。

        函数返回跨所有描述符集的已就绪的总位数,如果是定时器到时,返回0,返回-1表示出错。

        内核提供了三个辅助宏: 

void  FD_ZERO(fd_set  *fdset);  //比特位置为0
void  FD_SET(int  fd,   fd_set  *fdset); //往描述符集添加文件描述符
void FD_CLR(int fd, fd_set  *fdset); //清除描述符
void FD_ISSET(int fd, fd_set  *fdset); //判断某个文件描述符的位是否打开

        select工作原理:for循环遍历所有关心的描述符,检查每个描述符上是否有IO事件。因此当描述符很多时且事件很少时,需要遍历才能返回,效率直线下降。

        2、epoll

epoll包含三个函数:

1、创建epoll句柄

int epoll_create(int size);
成功返回一个epoll句柄,参数size告诉内核监听的数目一共多大。(size参数在内核版本大于2.6.8之后已被弃用)

2、设置事件
int epoll_ctl(int epfd,     int op,   int fd,    struct epoll_event *event);
    ➊op参数的值有以下几种:
    EPOLL_CTL_ADD 在epfd中注册指定的fd文件描述符并能把event和fd关联起来。
    EPOLL_CTL_MOD 改变 fd和event之间的联系。
    EPOLL_CTL_DEL 从指定的epfd中删除fd文件描述符。在这种模式中event是被忽略的,    并且为可以等于NULL。
    ➋fd参数使我们要监听的描述符
    ➌event这个参数是用于关联制定的fd文件描述符的。它的定义如下:

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 */
};

在结构体epoll_event中:

events这个字段是一个字节的掩码构成的。下面是可以用的事件:

EPOLLIN  -  当关联的文件可以执行 read ()操作时。
EPOLLOUT  -  当关联的文件可以执行 write ()操作时。
EPOLLRDHUP - (从 linux 2.6.17 开始)当socket关闭的时候,或者半关闭写段    的(当使用边缘触发的时候,这个标识在写一些测试代码去检测关    闭的时候特别好用)
EPOLLPRI - 当 read ()能够读取紧急数据的时候。
EPOLLERR - 当关联的文件发生错误的时候,epoll_wait() 总是会等待这个事    件,并不是需要必须设置的标识。
EPOLLHUP - 当指定的文件描述符被挂起的时候。epoll_wait() 总是会等待这    个事件,并不是需要必须设置的标识。当socket从某一个地方读取    数据的时候(管道或者socket),这个事件只是标识出这个已经读取到    最后了(EOF)。所有的有效数据已经被读取完毕了,之后任何的读    取都会返回0(EOF)。
EPOLLET - 设置指定的文件描述符模式为边缘触发,默认的模式是水平触发。
EPOLLONESHOT - (从 linux 2.6.17 开始)设置指定文件描述符为单次模式。这    意味着,在设置后只会有一次从epoll_wait() 中捕获到事件,之后    你必须要重新调用 epoll_ctl() 重新设置。

3、等待事件

int epoll_wait(int epfd,     struct epoll_event * events,
      int maxevents,    int timeout);

①epoll_wait 这个系统调用是用来等待epfd中的事件。
②events指向调用者可以使用的事件的内存区域。
③maxevents告知内核有多少个events,必须要大于0.
④timeout这个参数是用来制定epoll_wait 会阻塞多少毫秒。当timeout等于-1的时候这个函数会无限期的阻塞下去,当timeout等于0的时候,就算没有任何事件,也会立刻返回。

返回值:有多少个IO事件已经准备就绪。如果返回0说明没有IO事件就绪,而是timeout超时。遇到错误的时候,会返回-1,并设置 errno。

示例代码:

            #define MAX_EVENTS 10
           struct epoll_event ev, events[MAX_EVENTS];
           int listen_sock, conn_sock, nfds, epollfd;

           /* Code to set up listening socket, 'listen_sock',
              (socket(), bind(), listen()) omitted */

           epollfd = epoll_create1(0);
           if (epollfd == -1) {
               perror("epoll_create1");
               exit(EXIT_FAILURE);
           }

           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }
            for (;;) {
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_wait");
                   exit(EXIT_FAILURE);
               }
               for (n = 0; n < nfds; ++n) {
                   if (events[n].data.fd == listen_sock) {
                       conn_sock = accept(listen_sock,  (struct sockaddr *) &addr,              &addrlen);
                       if (conn_sock == -1) {perror("accept"); exit(EXIT_FAILURE);}
                       setnonblocking(conn_sock);
                       ev.events = EPOLLIN | EPOLLET;
                       ev.data.fd = conn_sock;
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,&ev) == -1) {
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       do_use_fd(events[n].data.fd);
                   }
               }
           }

epoll原理:

1、红黑树管理文件描述符。实现快速的查找、删除、更新等操作;

2、双向链表存放就绪的描述符。epoll_waite调用查看链表是否为空即可知道是否有事件产生。

3、基于事件。发生IO事件,回调函数会把就绪的描述符添加到就绪链表,并且唤醒阻塞在epoll_waite系统调用的进程,因此,即使同时监听几万个描述符,epoll的效率也不会下降。

4、slab高速缓存。特定结构体的内存缓存,减少内存频繁分配、回收的开销。


参考资料

《unix环境高级编程》
《unix网络编程》
《Linux 内核设计与实现》

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值