I/O多路转接之select和非阻塞IO

Posted 昨天;明天。今天。

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了I/O多路转接之select和非阻塞IO相关的知识,希望对你有一定的参考价值。

文章目录

1. 非阻塞IO

1-1 fcntl

  • 一个文件描述符, 默认都是阻塞IO.

  • 函数原型如下:

     #include <unistd.h>
     #include <fcntl.h>
     
     fcntl - manipulate file descriptor
    
  • 传入的cmd的值不同, 后面追加的参数也不相同(可变参数列表)

fcntl函数有5种功能:

参数功能
cmd=F_DUPFD复制一个现有的描述符
cmd=F_GETFD或F_SETFD获得/设置文件描述符标记
cmd=F_GETFL或F_SETFL获得/设置文件状态标记
cmd=F_GETOWN或F_SETOWN获得/设置异步I/O所有权
cmd=F_GETLK,F_SETLK或F_SETLKW获得/设置记录锁
  • 我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞。

1-2 实现函数SetNoBlock

void SetNoBlock(int fd)

    int f1 = fcntl(fd, F_GETFL);    //  获取状态标记位 --- 位图
    if(f1<0)
    
        perror("fcntl");
        return;
    
    fcntl(fd, F_SETFL, f1 | O_NONBLOCK);    //  设置状态标记位

  • 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
  • 使用F_SETFL将文件描述符设置回去, 设置回去的同时, 加上一个O_NONBLOCK参数。

2. I/O多路转接之select

1-1 初识select

系统提供select函数来实现多路复用输入/输出模型

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

1-2 select函数原型

#include <sys/select.h>

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

参数解释:

  • 参数nfds是需要监视的最大的文件描述符值+1;
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合、可写文件描述符的集合、异常文件描述符的集合;
    • 这些参数都是输入输出型参数,也就是说;
    • 例如:readfds 读集合我们设置成功了,知道有一个或多个文件就绪它就返回;不一定我们设置的所有文件全部就绪完成,所有我们需要不断的重新设置。
  • 参数timeout为结构timeval,用来设置select()的等待时间.

参数timeout取值:

  • NULL:则表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

关于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


函数返回值:

  • 执行成功则返回文件描述词状态已改变的个数;
  • 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测。

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值。
  • ENOMEM 核心内存不足

常见的程序片段如下:

fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset))……

1-3 理解select执行过程

  • 理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set); 后set变0001,0000(第5位置为1)
(3)若再加入fd= 2, fd=1,则set变为0001,0011
(4)执行 select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011。
注意:没有事件发生的fd=5被清空。

1-4 socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件
    描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;
    写就绪
  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大 于 等 于 低 水 位 标 记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE
    信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

1-5 select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)= 128,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是128*8=1024。
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
  • 一是用于再select 返回后, array作为源数据和fd_set进行FD_ISSET判断。
  • 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

1-6 select缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

1-7 代码链接

我这份代码只写了读端;数组我用vector实现; 数组poll的使用数组

https://gitee.com/ding-xushengyun/linux__cpp/tree/master/23NO3_27/1_selectServer

Linux 高级IO

文章目录

五种IO模型

  1. 阻塞IO

在内核将数据准备好之前, 系统调用会一直等待。 所有的套接字, 默认都是阻塞方式。

阻塞IO是最常见的IO模型

  1. 非阻塞IO

如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。

  1. 信号驱动IO

内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。

  1. IO多路转接

虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态

  1. 异步IO

由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

小结: 任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少。

高级IO重要概念

什么叫做高效的IO?

IO=等待+数据拷贝,而高效的IO就是在整个周期内,等的比重特别少,一直在做拷贝。提高IO效率就是减少IO过程等待的比重。

同步通信 vs 异步通信

同步和异步关注的是消息通信机制

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果。
  • 异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念

  • 进程/线程同步也是进程/线程之间直接的制约关系。
  • 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候。

同学们以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步。

阻塞 vs 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

其他高级IO

非阻塞IO,纪录锁,系统V流机制, I/O多路转接(也叫I/O多路复用) ,readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。

非阻塞IO

fcntl

一个文件描述符, 默认都是阻塞IO

函数原型:

int fcntl(int fd,int cmd,.../* arg */ );

传入的cmd的值不同, 后面追加的参数也不相同

fcntl函数有5种功能

  1. 复制一个现有的描述符(cmd=F_DUPFD)
  2. 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)
  3. 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
  4. 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
  5. 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞

实现函数SetNoBlock

基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞

void SetNonBlock(int fd)

    int f1=fcntl(fd,F_GETFL);
    if(f1<0)
        std::cerr<<"获取文件标记位失败..."<<std::endl;
        return;
    

    fcntl(fd,F_SETFL,f1|O_NONBLOCK);

  • 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)
  • 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数

轮询方式读取标准输入

实现代码

#include<iostream>
#include<string>
#include<unistd.h> 
#include<fcntl.h>
#include<errno.h>

void SetNonBlock(int fd)

    int f1=fcntl(fd,F_GETFL);
    if(f1<0)
        std::cerr<<"获取文件标记位失败..."<<std::endl;
        return;
    

    fcntl(fd,F_SETFL,f1|O_NONBLOCK);



int main()

    char buffer[1024];
    SetNonBlock(0);
    while(true)
        ssize_t s=read(0,buffer,sizeof(buffer)-1);
        if(s>0)
            buffer[s]=0;
            std::cout<<"buffer: "<<buffer<<std::endl;
        
        else
            if(errno==EAGAIN || errno == EWOULDBLOCK)
            
                sleep(2);
                std::cout << "当前没有出错,仅仅底层数据没有就绪罢了..." << std::endl;
                continue;

            
            if(errno == EINTR)
                std::cout << "读取被信号中断" << std::endl;
                continue;
            

            std::cout<<"read error"<<std::endl;
            break;
        
    
    return 0;

运行结果

I/O多路转接之select

初识select

系统提供select函数来实现多路复用输入/输出模型

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数

#include <sys/select.h>

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

参数解释

  • 参数nfds是需要监视的最大的文件描述符值+1
  • rdset 对应于需要检测的可读文件描述符的集合
  • wrset 对应于需要检测的可写文件描述符的集合
  • exset 对应于需要检测的异常文件描述符的集合
  • 参数 timeout 为结构 timeval ,用来设置select()的等待时间

关于timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

参数timeout取值

  • NULL:则表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生
  • 特定的时间值:如果在指定的时间段里没有事件发生, select将超时返回

fd_set结构


fd_set是操作系统提供的文件描述符集,其这个结构就是一个整型数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符。

fd_set是一个输入输出型函数,它的功能如下

  • 所有关心读事件的文件描述符,都应该添加在这个集合中。
  • 输入:用户告诉内核,OS你要帮我检测一下在这个集合中的fd的读事件。
  • 输出:内核告诉用户,你关心的fd,有那些文件描述符已经就绪了,可以读取了!

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的全部位

函数返回值

  • 执行成功则返回文件描述符状态已就绪的个数
  • 如果返回0代表在描述词状态改变前已超过timeout时间
  • 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n为负值
  • ENOMEM 核心内存不足

常见的程序片段如下

fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset))......

理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd

  1. 执行 fd_set set; FD_ZERO(&set) ;则set用位表示是0000,0000。
  2. 若 fd=5 ,执行 FD_SET(fd,&set) ;后set变为0001,0000(第5位置为1)
  3. 若再加入fd=2,fd=1,则set变为0001,0011.
  4. 执行 select(6,&set,0,0,0) 阻塞等待
  5. 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生时fd=5被清空

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0。
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0
  • 监听的socket上有新的连接请求
  • socket上有未处理的错误

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号
  • socket使用非阻塞connect连接成功或失败之后
  • socket上有未读取的错误

select的特点

  • 可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096
  • 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。一是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断;二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数

select缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说非常不便
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

select使用示例

套接字封装 sock.hpp

#pragma once

#include<iostream>
#include<string>
#include<cstdlib>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<strings.h>

namespace ns_sock

    enum
        SOCKET_ERR=2,
        BIND_ERR,
        LISTEN_ERR
    ;

    const int g_backlog=5;

    class Sock  
    
    public:
        static int Socket()
        
            int sock=socket(AF_INET,SOCK_STREAM,0);
            if(sock<0)
                std::cerr<<"socket error!"<<std::endl;
                exit(SOCKET_ERR);
            
            int opt = 1;
            setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 设置套接口选项值
            return sock;
        

        static void Bind(const int &sock,const u_int16_t &port)
        
            struct sockaddr_in local;
            bzero(&local,sizeof(local));
            local.sin_family=AF_INET;
            local.sin_port=htons(port);
            local.sin_addr.s_addr=INADDR_ANY;

            if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
            
                std::cerr<<"bind error!"<<std::endl;
                exit(BIND_ERR);
            
        
        
        static void Listen(const int &sock)
        
            if(listen(sock,g_backlog)<0)
            
                std::cerr<<"bind error!"<<std::endl;
                exit(LISTEN_ERR);               
            
        
    ;

select 服务端头文件 select_server.hpp

#pragma once

#include"sock.hpp"
#include<sys/select.h>
#include<sys/types.h>
#include<sys/socket.h>

namespace ns_select

    using namespace ns_sock;

#define NUM (sizeof(fd_set)*8)

    const int g_default = 8080;

    class SelectServer
    
    private:
        u_int16_t port_;
        int listen_sock_;

        int fd_arrar_[NUM];

        // EndPoint fd_arrar_[NUM];
    public:
        SelectServer(int port=g_default):port_(port),listen_sock_(-1)
        
            for(int i=0;i<NUM;i++)
            
                fd_arrar_[i]=-1;
            
        

        void InitSelectServer()
        
            listen_sock_=Sock::Socket();
            Sock::Bind(listen_sock_,port_);
            Sock::Listen(listen_sock_);
            fd_arrar_[0]=listen_sock_;
        

	    std::ostream& PrintFd()
        
            for(int i=0;i<NUM;i++)
            
                if(fd_arrar_[i]!=-1) std::cout<<fd_arrar_[i]<<' ';
            

            return std::cout;
        

        void HandlerEvent(fd_set &rfds)
        
            //判断我的有效sock,是否在rfds中
            for(int i=0;i<NUM;i++)
            
                if(-1==fd_arrar_[i])
                    continue;
                
                //区分新链接和数据
                if(FD_ISSET(fd_arrar_[i],&rfds))// 测试描述词组set中相关fd的位是否为真
                
                    if(fd_arrar_[i]==listen_sock_)
                    
                        //新链接
                        struct sockaddr_in peer;
                        socklen_t len=sizeof(peer);
                        int sock=accept(listen_sock_,(struct sockaddr*)&peer,&len);
                        if(sock<0)
                        
                            std::cout<<"accept error"<<std::endl;
                        
                        else
                        
                            //将新的sock添加到文件描述符中
                            int j=0;
                            for(;j<NUM;j++)
                            
                                if(fd_arrar_[j]==-1)
                                
                                    break;
                                
                            

                            if(j==NUM)
                            
                                std::cout<<"fd_arrar 已经满了"<<std::endl;
                                close(sock);
                            
                            else
                                fd_arrar_[j]=sock;
                                std::cout<<"获取链接成功,sock: "<<sock<<" 已经添加到数组中了,当前:"<<std::endl;
                                PrintFd() << " [当前]" << std::endl;
                            
                        
                    
                    else
                    
                        //数据
                        // 1.网络通信,定制协议,和业务场景有关
                        // 2.是不是每一个sock,都必须有自己独立的buffer
                        char buffer[1024];
                        ssize_t s=recv(fd_arrar_[i],buffer,sizeof(buffer),0);
                        if(s>0)
                        
                            buffer[s]='\\0';
                            std::cout<<"clint say#"<<buffer<<std::endl;
                        
                        else if(s==0)
                        
                            std::cout<<"client quit------sock: "<<fd_arrar_[i]<<std::endl;
                            
                            
                            // 对端链接关闭
                            close(fd_arrar_[i]);

                            // 从rfds中,去掉sock
                            fd_arrar_[i]=-1;
                            PrintFd()<<"[当前]"<<std::endl;
                        
                        else
                            // 读取异常
                            std::cerr<<"recv error"<<std::endl;
                        
                    
                
            
        

        void Loop()
        
            //在服务器最开始的时候,我们只有一个sock,listen_sock, 有读事件就绪,读文件描述符看待的!
            fd_set rfds;           
            
            //FD_SET(listen_sock_,&rfds);
            while(true)
            
                //struct timeval timeout=3,0;
                // 对位图结构进行清空
                FD_ZERO(&rfds); 
                int max_fd=-1;

                for(int i=0;i<NUM;i++)
                
                    if(-1==fd_arrar_[i]) continue;
                    FD_SET(fd_arrar_[i],&rfds);
                    if(max_fd<fd_arrar_[i]) max_fd=fd_arrar_[i];
                

                int n=select(max_fd+1,&rfds,nullptr,nullptr,nullptr);
                switch(n)
                
                    case 0:
                        std::cout<<"timeout ... "<<std::endl;
                        break;
                    case -1:
                        std::cout<<"select error"<<std::endl;
                        break;
                    default:
                        // select成功,至少有一个fd是就绪的
                        HandlerEvent(rfds);
                        //select成功,至少有一个fd是就绪的
                        //std::cout<<"有事件发生"<<std::endl;
                        break;
                
            
        

        ~SelectServer()
        
            if(listen_sock_>=0)
                close(listen_sock_);
        
    ;

select 服务端文件

#Linux 高级IO

Linux 高级IO

I/O多路转接复用机制---select,poll,epoll

典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)

select函数与I/O多路转接

I/O多路转接select/poll/epoll