高级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

目录
相关文章
|
3天前
|
网络协议 安全 测试技术
手撕测试tcp服务器效率工具——以epoll和io_uring对比为例
手撕测试tcp服务器效率工具——以epoll和io_uring对比为例
21 2
|
1月前
|
存储 Linux 调度
io复用之epoll核心源码剖析
epoll底层实现中有两个关键的数据结构,一个是eventpoll另一个是epitem,其中eventpoll中有两个成员变量分别是rbr和rdlist,前者指向一颗红黑树的根,后者指向双向链表的头。而epitem则是红黑树节点和双向链表节点的综合体,也就是说epitem即可作为树的节点,又可以作为链表的节点,并且epitem中包含着用户注册的事件。当用户调用epoll_create()时,会创建eventpoll对象(包含一个红黑树和一个双链表);
51 0
io复用之epoll核心源码剖析
|
1月前
|
消息中间件 架构师 Java
性能媲美epoll的io_uring
性能媲美epoll的io_uring
19 0
|
1月前
|
网络协议 架构师 Linux
一文说透IO多路复用select/poll/epoll
一文说透IO多路复用select/poll/epoll
108 0
|
1月前
|
网络协议 Linux
2.1.1网络io与io多路复用select/poll/epoll
2.1.1网络io与io多路复用select/poll/epoll
|
1月前
|
存储 网络协议
TCP服务器 IO多路复用的实现:select、poll、epoll
TCP服务器 IO多路复用的实现:select、poll、epoll
27 0
|
1月前
|
网络协议 Linux C++
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
32 0
|
19天前
|
缓存 分布式计算 Java
Java基础深化和提高-------IO流
Java基础深化和提高-------IO流
97 0
|
27天前
|
存储 算法 Java
从零开始学习 Java:简单易懂的入门指南之IO序列化、打印流、压缩流(三十三)
从零开始学习 Java:简单易懂的入门指南之IO序列化、打印流、压缩流(三十三)
|
27天前
|
存储 自然语言处理 Java
从零开始学习 Java:简单易懂的入门指南之IO缓冲流、转换流(三十二)
从零开始学习 Java:简单易懂的入门指南之IO缓冲流、转换流(三十二)

热门文章

最新文章

相关产品

  • 云迁移中心