linux下IO模型

今天看了一篇博客园大佬写的关于linux下IO模型博客, 写的真是通俗易懂。 看完之后意犹未尽,意犹未尽, 真的是太香了! 对于想要学习linuxIO模型有很多帮助!
大家可以去看一下, 力推!! Matrix海子大佬2014年的写的
Matrix海子大佬 Java NIO:浅谈IO模型
还有一篇大佬写的, 也是不错的。
高级IO----select
epoll解析, junren大佬2014年写的
Linux下的I/O复用与epoll详解

我就做一些知识点总结

一、IO操作大体可以分为两步
      1、IO数据是否就绪
      2、对于数据的拷贝

二、同步通信(同步IO)、异步通信(异步IO) 不同于 线程安全那块的同步
      1、同步通信是指: 用户请求一个IO操作, 当IO操作的数据就绪时,也就是IO操作的第一步完成, 对于第二步数据拷贝是由用户线程操作的。 异步通信则是对于第二部数据拷贝是内核操作的。
      2、线程那块的同步:由于多线程是访问临界资源是随机的, 因此对于临界资源的操作会存在安全问题(也就是线程安全的起源), 因此同步就是解决这个问题。 对于一个临界资源的访问, 保证多线程是有序的、排队的访问临界资源就是同步操作。

三、五种IO模型
      分别是1、阻塞IO; 2、非阻塞IO; 3、信号驱动IO; 4、多路复用IO; 5、异步IO这五种。前4种都属于同步IO类型,数据拷贝用户线程搞的。 最后一个异步IO一条龙服务。
1、阻塞IO: 在没有IO数据就绪之前,用户进程一直阻塞状态。数据就绪后,数据拷贝程序员做。
2、非阻塞IO: 访问IO资源, 如果资源不可用, IO请求不会阻塞,返回当前资源不可用, 如果资源可用,就数据拷贝, 数据拷贝程序员做。
3、信号驱动IO: 自定一个IO信号处理函数, 当IO资源就绪时, 内核会给用户线程发送一个信号, 用户线程接收到信号执行处理函数(处理函数包括数据拷贝操作),数据拷贝程序员做。
4、多路复用IO:完成对大量IO(大量文件描述符)监控, 监控事件有:可读事件;可写事件;异常事件。 当监控的IO资源有某个就绪的, 会通知用户线程, 进行处理。其他的IO资源接着监控。 数据拷贝程序员做。
5、异步IO: 自定一个IO信号处理函数,和信号驱动IO一样。 但是区别在于数据拷贝已经由内核做好了, 当通知用户(发送信号)时, 已经做好了(请慢用)。
综上所述: 前4步为同步IO, 只有最后一步为异步IO (可见区别在于数据拷贝)。

多路转接IO

一、select
优点:
      1、由于select遵循posix标准, 所以平台移植性好。
      2、select超时可以精确到毫秒。
缺点:
      1、内核采用轮询遍历监控,随着监控的文件描述符增多,效率会下降
      2、监控的文件描述符是由上限的, 取决于宏_FD_SETSIZE,内核中定义最大一般为1024(0~1023)(可以修改哦!不过修改之后需要重新编译内核)
      3、select监控文件描述符时,会将集合拷贝到内核当中, 返回时就将集合返回。
      4、select不会告诉用户哪一个文件描述符就绪了, 需要用户自己去判断。
      5、select返回就绪的文件描述符时, 会将set集合中未就绪的文件描述符移除。(这就导致如果想要再次监控,需要重新添加)

select分析:
      select可以监控多个多个文件描述符, 当这些文件描述符有我们关心的事件发生时, select会返回我们对应的文件描述符, 我们就可以对这个文件描述符进行相应的操作!

举个例子: 在实现cs(客户端-服务器)模型时, 很多人都喜欢使用recv、recvfrom、send、sendto等阻塞接口实现通信, 同时为了实现并发, 会创建一个个线程去专门处理一个用户。 如果用户连接请求过多时,我们的机器资源就可能不够。 并且当用户处理线程处于阻塞时,什么都不干,也是一种线程资源浪费。

学习select,我们就可以有效的解决这个问题。以tcp服务器通信为例, 我们把监控socket交给select监控。 当监控socket文件描述符就绪时, 代表着有一个用户连接请求。 我们会创建一个new socket套接字(用于于客户端交互)并把它交给select监控。 当下一次select监控的文件描述符有就绪时, 我们就可以判断是哪一个文件描述符就绪, 如果时监听socket文件描述符就绪,则代表有新的连接请求。
如果是new socket文件描述符就绪,则代表一个用户发送数据请求, 我们需要处理应答。

下面是我模拟实现的select监控tcp通信。
selectser.hpp

#pragma once                                                                                                                                    
    
/*                               
 *   封装select接口功能 - 为了更加方便使用tcp编程    
 */                                 
                          
#include"tcpser.hpp"                       
                                 
#include<stdio.h>    
#include<unistd.h>    
#include<sys/select.h>    
#include<vector>                                 
                    
class SelectSvr{    
    public:                            
                                  
        //构造函数                  
        SelectSvr(){                         
            //清空readfds集合    
            FD_ZERO(&readfds_);    
            //初始化maxfd_    
            maxfd_ = -1;    
        }    
                
        //添加管理文件描述符到可读事件集合    
        void AddFdInSet(int fd){    

            FD_SET(fd, &readfds_); 
            //如果插入的文件大, 需要更新maxfd_
            if(fd > maxfd_){
                maxfd_ = fd;
            }
        }
        
        //从集合删除fd
        void DeleteFdForSet(int fd){
            
            if(FD_ISSET(fd, &readfds_)){
                //fd在集合当中
                FD_CLR(fd, &readfds_);
                if(maxfd_ == fd){
                    //删除的最后一个fd, 需要更新maxfd_
                    maxfd_--;
                }
            }
        }
                                                                                                                                                
        int SelectWait(std::vector<TcpSer>& output){
            
            //定义超时时间
            struct timeval tv;
            //秒
            tv.tv_sec = 0;
            //毫秒 3000ns = 3s
            tv.tv_usec = 300000;

            fd_set tmp = readfds_;
            //监听 - 超时时间
            int fdnum = select(maxfd_ + 1, &tmp, NULL, NULL, &tv);
            if(fdnum < 0){
                //监控出错
                perror("select error!\n");
                return false;
            }
            else if(0 == fdnum){
                //超时
             //   perror("timeout!\n");
                return false;
            }

            //正常情况
            for(int i = 0; i <= maxfd_; i++){
                                                                                                                                                
                if(FD_ISSET(i, &tmp)){
                    //i号fd发生事件
                    TcpSer ts;
                    ts.Setfd(i);
                    output.push_back(ts);
                }
            }
            
            return true;
        }
    private:
        //最大文件描述符数指
        int maxfd_;
        //定义管理数组 - 可读
        fd_set readfds_;
};                                                

tcpser.hpp

/*                                                                                                                                              
 *  实现简单的tcp server类    
 *  1、socket    
 *  2、bind    
 *  3、connect                 
 *  4、listen                           
 *  5、accept            
 *  6、sned & recv                        
 *  7、close                                                            
 */    
                                  
#pragma once                     
    
#include<stdio.h>                
#include<iostream>    
#include<sys/socket.h>                  
#include<string>                    
#include<string.h>                                       
#include<netinet/in.h>           
#include<unistd.h>    
#include<arpa/inet.h>    
#include<stdlib.h>                         
                            
class TcpSer{    
    public:    
        //重定义struct sockaddr & struct sockaddr     
        typedef struct sockaddr_in sockaddr_in;
        typedef struct sockaddr sockaddr;
        
        //构造函数
        TcpSer()
            :sockfd_(-1)
        {

        }   
        
        //析构函数
        ~TcpSer(){
        }
        
        //socket
        bool Socket(){
            sockfd_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            if(sockfd_ < 0){
                //socket error
                perror("socket!\n");
                return false;                                                                                                                   
            }
            return true;
        }
        
        //bind
        bool Bind(const std::string& IP, uint16_t PORT){
            
            //定义地址信息 - 并填充
            sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(PORT);
            addr.sin_addr.s_addr = inet_addr(IP.c_str());
            
            int ret = bind(sockfd_, (sockaddr*)&addr, sizeof(addr));
            if(ret < 0){
                //bind error
                perror("bind!");
                return false;
            }
            return true;
        }
        
        //connect - 为了客户端提供的
        bool Connect(const char* IP, uint16_t PORT){
         
            //定义服务器地址信息 - 并填充                                                                                                       
           sockaddr_in peer;
           peer.sin_family = AF_INET;
           peer.sin_port = htons(PORT);
           peer.sin_addr.s_addr = inet_addr(IP);
           int ret = connect(sockfd_, (sockaddr*)&peer, sizeof(peer));
           if(ret < 0){
               perror("connect!");
               return false;
           } 

           return true;
        }
        
        //监听三次握手成功接口 - 为了服务器
        bool Listen(const int backlog = 5){
            
            int ret = listen(sockfd_, backlog);
            if(ret < 0){
                //listen error!
                perror("listen!");
                return false;
            }
            return true;
        }                                                                                                                                       
        
        //accept - peeraddr出参 - 接收客户端地址信息
        bool Accept(TcpSer& ts, sockaddr_in& peeraddr){
            //出参 - 接收客户端的地址信息长度
            socklen_t addrlen = sizeof(peeraddr);
            
            int ret = accept(sockfd_, (sockaddr*)&peeraddr, &addrlen);
            if(ret < 0){
                perror("accept!");
                return false;
            }
            
            //newsocketfd = ret; 采用类封装
            ts.sockfd_ = ret;
            return true;
        }
        
        //send 发送消息接口
        bool Send(const std::string& Buf){
            
            int write_size = send(sockfd_, Buf.c_str(), Buf.size(), 0);
            
            if(write_size < 0){
                //send error!
                perror("send!");
                return false;                                                                                                                   
            }
            return true;
        }
        
        //recv 接收消息接口
        bool Recv(std::string& Buf){
            //定义buf
            char buf[1024 * 10] = {0};
            int read_size = recv(sockfd_, buf , sizeof(buf) - 1, 0);
         
            if(read_size < 0){
                //read error!
                perror("read_size!");
                return false;
            }
            else if(0 == read_size){
                //客户端断开连接
                printf("peer close this connect!\n");
                return false;
            }
            //拷贝
            Buf.assign(buf, read_size);
            return true;
        }
     
        //关闭socket文件描述符                                                                                                                  
        void Close(){
            close(sockfd_);
            sockfd_ = -1;
        }
        
        //修改sockfd_  - 该接口为new socket提供的
        void Setfd(int fd){
            sockfd_ = fd;
        }
        
        //显示sockfd_ - 该接口为new socket提供的
        int Getfd(){
            return sockfd_;
        }
    private:
        //定义socket fd
        int sockfd_;
};                                                    

main.cpp

#include"selectser.hpp"                                                                                                                         
    
int main(){                                    
                                                                     
    //创建侦听套接字       
    TcpSer listen_fd;                                                                                   
    if(!listen_fd.Socket()) { return 0; }            
    if(!listen_fd.Bind("192.168.48.129", 17777)) { return 0; }    
    if(!listen_fd.Listen(5)) { return 0; }                        
                                              
    //创建select集合                         
    SelectSvr  ss;                                                
    //添加侦听套接字fd                               
    ss.AddFdInSet(listen_fd.Getfd());    
                                             
    while(1){         
                          
        //监控                                                 
        //监控失败,超时 - continue    
                                       
        //定义事件准备就绪管理数组    
        std::vector<TcpSer> output;    
        if(!ss.SelectWait(output)) { continue; }    
                                                    
        //轮次遍历就绪事件数组 - 并处理    
        for(size_t i = 0; i < output.size(); i++){    
            
            //1、侦听套接字事件就绪
            if(listen_fd.Getfd() == output[i].Getfd()){
                //即有新的连接请求, 需要使用accept函数建立一个新的连接
                
                //创建TcpSer对象管理新的socketfd & 用户的地址信息
                TcpSer tmp;
                struct sockaddr_in cliaddr;
                if(!listen_fd.Accept(tmp, cliaddr)) { continue; }   
                
                printf("Ser has a new connect IP:[%s] --> PORT:[%d]\n", inet_ntoa(cliaddr.sin_addr),
                        ntohs(cliaddr.sin_port));
                //添加new套接字fd到set中
                ss.AddFdInSet(tmp.Getfd());
            }
            else{  
                //2、非侦听套接字、创建连接的套接字
                //即:客户端发送来数据, 需要处理
                std::string buf;
                if(!output[i].Recv(buf)){
                    continue;                                                                                                                   
                }
                printf("client says: [%s]\n", buf.c_str());

                //处理 - 暂未实现
            }
        }
    }
    return 0;
}                                    

测试结果:
在这里插入图片描述
这是mian进程创建文件描述符情况
在这里插入图片描述
没有创建线程实现一个简单的并发的tcp通信, 只用一个主线程做到的。

二、poll
poll也是多路转发的一种, 它也可以监控多个文件描述符。 既然有相同功能,就要和select作对比。
1、poll内核也是采用轮询遍历, 因此事件效率没有比select提高多少。
2、poll跨平台不如select。
3、select里面是构建关心事件集合(每多一个关心事件就需要多一个事件集合), poll是构建事件结构数组(是结构体, 对于关心的fd, 事件可以在里面填充)这个poll比较方便。
4、poll监控的文件描述符无上限,poll二次监控的时候不需要重新添加文件描述符。
5、返回事件并不会告诉是哪个文件描述符就绪, 需要我们去判断。

poll简单的写一个测试例子, 就不用它做tcp通信

  #include<stdio.h>                                                                                                                             
  #include<unistd.h>
  #include<poll.h>         
      
  int main(){      
      
      //1、定义事件数组并初始化
      struct pollfd fd_arr[10];                      
      //填充事件结构      
      fd_arr[0].fd = 0;                 
      //关心0号文件描述符的可读时间
      fd_arr[0].events = POLLIN;
                                
      //2、监视的有效文件描述符个数                
      int vaildnum = 1;    
               
      while(1){
                                                
          //3000毫秒                            
          int ret = poll(fd_arr, vaildnum, 3000);
          if(ret < 0){                     
              perror("poll errer!");                           
              return 0;                                 
          }        
          else if(0 == ret){
              printf("超时了!\n");
              continue;
          }
  
          for(int i = 0; i < vaildnum; i++){
		      if(fd_arr[i].revents = POLLIN){
                  //产生事件
                  char buf[1024] = {0};
                  read(fd_arr[i].fd, buf, sizeof(buf) - 1);
                  printf("read content: [%s]", buf);
              }
          }
      }
  
      return 0;
  }                                         

三、epoll
epoll是公认性能最高的IO多路转接模型。 这是我学习它的时候听到的第一句话。

        在很早以前, 是使用select和poll来时监控文件描述符的, select由于需要将set集合拷贝到内核中,遍历拷贝,时间效率o(n)。 就绪返回时不知道是哪个文件描述符就绪,需要遍历判断,时间效率o(n), 内核监控时轮次遍历监控效率低等因素不行, poll也没有好到哪里去,内核还是轮次监控。 效率也没有提供多少。
因此这两个都不行, 当epoll出现时, 可以说是这两个的升级版本。支持一次性大量添加监控文件描述符, 最后再阻塞监控。 内核当中采用红黑树来监控监控, 双向循环链表存储就绪的事件等(链表任意插、任意删效率不错)等因素名利前茅!
在这里插入图片描述
先从它的接口来分析:
1、epoll_create
创建epoll操作句柄(文件描述符), 并且在内核当中初始化eventpoll结构体。 这个结构体是包含双向循环链表、红黑树。
在这里插入图片描述
2、epoll_ctl
前言:当我们想要监控一个文件描述符, 我们会封装一个事件结构体(这个结构体包含我们监控的文件描述符、关心事件等一些信息)

当添加一个事件结构体, 就会在内核中struct eventpoll结构体中红黑树添加这个结点,要知道红黑树的增删查改是logn。 这个红黑树中每个结点就是epoll监控的事件结构体。

3、epoll_wait
当红黑树监控的事件结构体有某些就绪时, 就会把这个就绪的事件结构体拷贝到双向循环链表中, 所以这个链表就是存储就绪的事件结构体。 然后把这个链表中的所有信息以一个事件结构体数组的形势返回给程序员,程序员可以通过返回的数组来处理就绪文件描述符。 清空双向循环链表。这就是一次监控返回的操作。 红黑树中监控信息仍然存在,不需要重新添加监控文件描述符。

使用epoll简单实现的tcp通信cs模型测试代码

最后再介绍一篇顶级大佬罗培羽的epoll详解
该作者出版过 <<网络游戏同步算法>>
如果这篇文章说不清epoll的本质,那就过来掐死我吧!

感想:
之前写过一个多线程实现的TCP通信, 当然就是采用阻塞IO的函数, 使用多线程去实现并发。 当时觉得这样还不错,很好。 现在学习IO模型, 我才发现之前写的并不好, 让每一个线程管理一个用户, 是很浪费线程资源的, 并且有限。学了IO模型中的多路复用IO, 竟可以发现用一个线程可以管理多个用户。 真的是很神奇。

本篇博客没有什么, 主要引荐一些大佬们的IO模型博客。对于理解IO模型有很大作用。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值