高级IO以及IO多路复用(select、poll、epoll网络编程)1

简介: 高级IO以及IO多路复用(select、poll、epoll网络编程)

一、高级IO相关

1.1 同步通信和异步通信

同步通信和异步通信是两种不同的通信方式,二者的概念如下:

  • 同步通信是指通信双方需要在某时刻达成一致,才进行数据交换。在同步通信中,发送方会在发送数据时等待接收方的响应,直到接收到响应后才会继续执行后续任务。同步通信可以保证数据传输的可靠性和一致性,但是可能造成系统的阻塞和资源浪费。
  • 异步通信是指通信双方可以独立的进行数据交换,不需要在某一时刻达成一致。在异步通信中,发送方会在发送数据后立即返回,而接收方会在接收到数据后立即进行处理。异步通信可以提高系统的并发性和效率,但是也可能会带来一些数据的不一致问题。


总的来说,同步通信适用于对数据传输的可靠性和一致性要求较高的场景,例如数据库的读写操作。而异步通信适用于对系统的并发性和效率要求较高的场景,如网络通信。具体选择哪种通信方式需要根据实际情况来考虑。

1.2 阻塞与非阻塞

阻塞和非阻塞是两种不同的操作方式,二者的概念如下:

  • 阻塞是指在进行某种操作时,如果当前操作无法完成,那么程序就会一直等待,直到操作完成或者出现错误时才返回结果。阻塞操作会阻塞当前线程或进程的执行,直到操作完成,因此会占用CPU资源,并且可能造成系统的阻塞。
  • 非阻塞是指在进行某种操作时,如果当前操作无法完成,则程序会立马返回,并且告诉调用者当前操作无法完成。非阻塞操作不会阻塞当前线程或进程的执行,因此不会占用CPU资源,并且可以让程序执行其他任务。

总的来说,阻塞和非阻塞是对于操作的执行方式的描述,阻塞操作会等待操作的完成,而非阻塞操作会立即返回。选择使用阻塞或非阻塞操作取决于应用程序的需求和实际情况。如果需要快速响应和处理多个并发请求,通常会使用非阻塞操作。而如果需要保证数据传输的可靠性和一致性,则可能需要使用阻塞操作。

1.3 fcntl 函数

fcntl是一个Unix/Linux系统编程中的函数,用于控制文件描述符的一些属性和操作。其定义如下:

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fildes, int cmd, ...);

参数fd是被参数cmd操作(如下面的描述)的文件描述符,传入的cmd的值不同,fcntl后面追加的参数也不相同。


fcntl函数有5种功能:


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

此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞,如下面的例子:

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

bool SetNoBlock(int sock)
{
    int flag = fcntl(sock, F_GETFL);
    if(flag == -1)
        return false;
    int n = fcntl(sock, F_SETFL, flag | O_NONBLOCK);
    if(n == -1)
        return false;
    return true;
}

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

下面以轮询的方式读取标准输入:

#include <iostream>
#include <vector>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
using func_t = std::function<void()>;
void func1()
{
    cout << "func1 " << endl;   
}
void func2()
{
    cout << "func2 " << endl;   
}
void func3()
{
    cout << "func3 " << endl;   
}
bool SetNoBlock(int sock)
{
    int flag = fcntl(sock, F_GETFL);
    if(flag == -1)
        return false;
    int n = fcntl(sock, F_SETFL, flag | O_NONBLOCK);
    if(n == -1)
        return false;
    return true;
}
int main()
{
    std::vector<func_t> funcs;
    funcs.push_back(func1);
    funcs.push_back(func2);
    funcs.push_back(func3);
    SetNoBlock(0);
    char buff[1024];
    while(true)
    {
        memset(buff, 0, sizeof buff);
        ssize_t read_size = read(0, buff, sizeof(buff) - 1);
        if(read_size < 0)
        {
            cerr << "errno:" << errno << "desc: " << strerror(errno) << endl;
            for(const auto& f : funcs)
            {
                f();
            }
        }
        else 
        {
            cout << "buff: " << buff << "read_size: " << read_size << endl;
        }
        sleep(1);
    }
    return 0;
}

可以发现此时不输入时就不会阻塞而直接处理另外一个逻辑。

如果此时取消调用SetNoBlock函数:

就按照阻塞的方式进行读取了。

二、五种IO模型

2.1 阻塞式IO模型

在阻塞式IO模型中,当应用程序发起一个IO请求时。程序会一直阻塞等待,直到数据传输完成才继续执行其他任务。这种模型的缺点是会造成CPU资源的浪费,降低系统的响应速度。但是阻塞式IO是最常见的IO模型,所有的套接字默认都是阻塞方式。

2.2 非阻塞式IO模型

在非阻塞式IO模型中,当应用程序发起一个IO请求时,即使数据没有准备好也会立即返回,并且通过轮询的方式不断地查询IO操作的状态,直到数据准备完成。这种模型可以减少CPU资源的浪费,但也会增加系统的负担,降低IO操作的效率。

2.3 多路复用IO模型

在多路复用IO模型中,应用程序可以将多个IO操作绑定到同一个事件轮询器中,然后等待IO操作完成的通知。这种模型可以有效地提高系统的并发性能,但是实现较为复杂。

2.4 信号驱动式IO模型

在信号驱动IO模型中,应用程序向操作系统注册一个信号,当IO操作完成时,操作系统会向应用程序发送信号通知。这种模型可以减少轮询带来的系统负担,提高系统的效率。

2.5 异步IO模型

在异步IO模型中,应用程序可以在发起IO请求后立即返回,并在IO操作完成后由操作系统通知应用程序。这种模型可以提高系统的并发性能和效率,但是实现较为复杂。

总之,不论是何种IO模型,都包含了两个步骤,第一是等待数据,第二是拷贝数据。而且在实际应用场景中,等待消耗的时间往往都远高于拷贝数据的时间。让IO变得更高效,最核心的办法就是尽量减少等待的时间。

三、认识IO多路复用

IO多路复用是一种高效的I/O处理机制,它允许在单个线程中同时监视和处理多个I/O操作,以提高程序的性能和可扩展性。


在传统的阻塞I/O模型中,每个I/O操作都会阻塞整个进程,直到该操作完成。这就意味着如果应用程序需要处理多个并发I/O操作,就需要建立多个线程或者进程来处理它们,这样就会导致系统的开销过高,并且导致可扩展性下降。


使用IO多路复用,应用程序可以将多个I/O操作注册到一个事件的循环中,然后使用一个线程来监视这些操作的状态。当其中任何一个操作就绪时,事件循环就会通知相应的应用程序,来执行相应的操作。这种方式允许应用程序同时处理多个I/O操作,并且无需创建多个线程或者进程,因此提高了系统的性能和可扩展性。


常见的IO多路复用技术包括select、poll、epoll等,其中epoll是最常用的技术之一,因为它可以更好地处理大量的并发连接。

四、select

4.1 认识select函数

select()函数是一种在 Unix/Linux 系统中实现多路复用 IO 的一种机制,它可以等待多个文件描述符(socket、文件等)中任何一个变为”就绪“状态(可读、可写、异常),然后立即进行处理,而不是阻塞在一个文件描述符上等待数据到来。

4.2 select函数原型

select函数原型:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数说明:


  • nfds:需要监听的最大文件描述符的值加 1,即文件描述符集合中最大的文件描述符值加 1;
  • readfds:指向可读文件描述符集合的指针;
  • writefds:指向可写文件描述符集合的指针;
  • exceptfds:指向异常文件描述符集合的指针;
  • timeout:超时时间,若设置为NULL则表示永远等待,直到有文件描述符就绪。函数返回值:

函数返回值:

  • 若有文件描述符就绪,则返回就绪文件描述符的个数;
  • 若超时或被信号中断,则返回 0;
  • 若出现错误,则返回 -1,并且设置errno变量。

fd_set 结构体:

fd_set是一个用于表示文件描述符的结构体,它包含了一组标志位,每个标志位的值表示了一个文件描述符是否在集合中。fd_set结构体的定义如下:

/* The fd_set member is required to be an array of longs.  */
typedef long int __fd_mask;
/* fd_set for select and pselect.  */
typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

其实这个结构就是一个整数数组,更严格的说是一个"位图",使用位图中对应的位来表示要监视的文件描述符。


为了使得操作fd_set变得更加方便,操作系统还专门提供了一组接口:


  • FD_ZERO(fd_set *set):将集合中的所有位清零;
  • FD_SET(int fd, fd_set *set):将指定文件描述符加入集合中;
  • FD_CLR(int fd, fd_set *set):将指定文件描述符从集合中删除;
  • FD_ISSET(int fd, fd_set *set):判断指定的文件描述符是否在集合中。

timeval 结构体:timeval结构体用于描述一段时间长度,如果在这段时间内,需要监听的文件描述符没有就绪则函数返回,返回值为 0。timeval结构体的定义如下:

/* A time value that is accurate to the nearest
   microsecond but also has a range of years.  */
struct timeval
  {
    __time_t tv_sec;    /* Seconds.  */
    __suseconds_t tv_usec;  /* Microseconds.  */
  };

理解socket就绪状态:


读就绪:


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

写就绪:


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

异常就绪:

  • socket上收到带外数据关于带外数据,和TCP紧急模式相关。

高级IO以及IO多路复用(select、poll、epoll网络编程)2:https://developer.aliyun.com/article/1384008

目录
相关文章
|
26天前
|
Linux C++
Linux C/C++之IO多路复用(poll,epoll)
这篇文章详细介绍了Linux下C/C++编程中IO多路复用的两种机制:poll和epoll,包括它们的比较、编程模型、函数原型以及如何使用这些机制实现服务器端和客户端之间的多个连接。
20 0
Linux C/C++之IO多路复用(poll,epoll)
|
4月前
|
安全 Java Linux
(七)Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
IO(Input/Output)方面的基本知识,相信大家都不陌生,毕竟这也是在学习编程基础时就已经接触过的内容,但最初的IO教学大多数是停留在最基本的BIO,而并未对于NIO、AIO、多路复用等的高级内容进行详细讲述,但这些却是大部分高性能技术的底层核心,因此本文则准备围绕着IO知识进行展开。
151 1
|
3月前
|
监控
【网络编程】select函数
【网络编程】select函数
56 0
|
4月前
|
存储 Java Unix
(八)Java网络编程之IO模型篇-内核Select、Poll、Epoll多路复用函数源码深度历险!
select/poll、epoll这些词汇相信诸位都不陌生,因为在Redis/Nginx/Netty等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容。
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
4月前
|
Java 大数据
解析Java中的NIO与传统IO的区别与应用
解析Java中的NIO与传统IO的区别与应用
|
2月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
3月前
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
41 2
|
3月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
|
3月前
|
缓存 Java
【IO面试题 一】、介绍一下Java中的IO流
Java中的IO流是对数据输入输出操作的抽象,分为输入流和输出流,字节流和字符流,节点流和处理流,提供了多种类支持不同数据源和操作,如文件流、数组流、管道流、字符串流、缓冲流、转换流、对象流、打印流、推回输入流和数据流等。
【IO面试题 一】、介绍一下Java中的IO流