IO多路复用及其机制(1) select()

一、IO多路复用简介

I/O多路复用(I/O Multiplexing)是一种高效的并发编程技术,旨在通过单个线程或进程同时管理多个I/O操作,从而提高系统的并发处理能力和资源利用率。

核心概念

I/O多路复用通过一种机制(如select、poll、epoll等)同时监听多个I/O事件(如可读、可写、异常等),并在事件就绪时通知应用程序进行处理。这种方式避免了为每个I/O操作创建独立的线程或进程,从而减少了系统开销和资源消耗。

简单来说就是一线程,多路IO,且收发互不影响。

 多路:指多个I/O操作(如多个TCP连接或文件描述符)。

 复用:指复用一个或少量线程来处理这些I/O操作。


二、IO多路复用的优势

 减少线程/进程开销:通过复用单个线程或进程处理多个I/O操作,避免了频繁的线程创建、销毁和上下文切换。

 提高并发性能:特别适合处理大量并发连接或I/O操作的场景,如网络服务器。

 资源利用率高:减少了系统资源的浪费,提升了单机的性能极限。


三、IO多路复用的应用场景

 网络服务器:需要同时处理大量客户端连接,如Web服务器、聊天服务器等。

 高性能I/O操作:如文件读写、数据库连接等需要高效处理的场景。

 多协议支持:同时处理多种网络协议的套接字。


四、IO多路复用的实现机制

在Linux系统中,常见的I/O多路复用机制包括:

 select:最早的多路复用机制,使用位图表示文件描述符集合,但有文件描述符数量限制和性能问题。

 poll:改进版select,使用数组表示文件描述符集合,无数量限制,但性能仍不如epoll。

 epoll:Linux特有的高性能多路复用机制,使用事件驱动模型,支持边缘触发和水平触发模式,适合高并发场景。

在Linux发展过程中,select、poll 和 epoll 是三种用于处理并发网络连接的 I/O 复用机制。它们的出现和演变与 Linux 系统对于高并发、低延迟处理的需求密切相关。

select(最早的I/O复用机制)

1、历史

select()首次出现在1970年代末和1980年代初期,最早出现在POSIX标准中,并且在Unix系统中广泛使用。随着POSIX标准的普及(POSIX.1标准发布于1988年),它成为了跨平台的标准I/O复用方法。

select()最初是为了提供一种方法来监控多个文件描述符,尤其是网络套接字(socket)。它的目标是让应用程序可以同时等待多个连接事件(例如,某个socket是否准备好进行读取或写入)。

2、性能瓶颈

① 轮询机制:select()在内部使用轮询的方式检查每个文件描述符的状态。这意味着在大量文件描述符时,性能会显著下降。每次调用select()时,都会遍历整个文件描述符集合,效率较低。

② 文件描述符数量限制:select()的一个显著缺点是它限制了可监视的文件描述符数量。通常这个上限是1024(虽然可以通过调整系统参数来增加,但并非无限制)。

3、意义

select()的出现是Unix系统中多路复用I/O的第一次尝试,它提供了基于事件的非阻塞I/O操作,使得程序可以在等待多个事件发生时避免阻塞。

4、select的组成

 如图,select的组成很简单!

1个函数:select()

1个类型:fd_set

4个宏定义:FD_ZERO, FD_SET, FD_CLR, FD_ISSET

(1)select()

select() 可以同时监控多个文件描述符的状态。具体来说,它可以监听一个文件描述符集合,判断每个文件描述符是否准备好进行读操作、写操作,或者是否发生异常。

select()函数中,有五个参数,分别对应:

最大fd+1(fd的范围)、可读集合、可写集合、出错集合、阻塞时长。

以下是声明文件中的定义。

看着很复杂?但实际上在代码中并没有这么夸张。

int maxfd;
fd_set rset, wset;    //fd_set是一个结构体,因此可以这样声明
int nready = select(maxfd+1, &rset, &wset, NULL, NULL);

这样就成功调用了select函数了!

代码中的rset可以理解为一个用于存储信息的结构体,而timeout = NULL意味着无限阻塞,阻塞点为select有无信息返回。select() 在没有事件发生时会阻塞,直到文件描述符集合中的某些文件描述符变得“就绪”。

至于为何是maxfd+1,涉及到fd的分配机制:fd是一个个int型非负整数,从0开始分配(且0,1,2被系统自动分配给stdin, stdout, stderr)。因此从0开始的情况下,如果不+1,就会少遍历一个。

接着,让我们假设:

rset和wset集合中包含的fd(文件描述符)为:3,4,5,6,7,8。
其中,4和5可读,6和7可写。

那么,select会返回什么给nready呢?
select() 会返回就绪的文件描述符的数量,即在调用时满足条件的文件描述符数量。对于每种文件描述符集合,它会分别检查:

rset:select() 会检查哪些文件描述符是可读的。4 和 5 是可读的,所以有 2 个文件描述符满足条件。

wset:select() 会检查哪些文件描述符是可写的。6 和 7 是可写的,所以有 2 个文件描述符满足条件。
因此,nready 的值是满足条件的文件描述符的总数,即:
nready = 2 (可读的) + 2 (可写的) = 4

(2)fd_set

fd_set 是用于表示文件描述符集合的数据类型,通常定义在头文件 <sys/select.h> 中。

由头文件定义可知,fd_set 是一个用于存储文件描述符集合的结构体。它通过 fds_bits 或 __fds_bits 数组来表示文件描述符的状态,每个文件描述符对应一个二进制位;__fd_mask 是用于存储和操作这些位的类型(通常是 unsigned long);fds_bits[__FD_SETSIZE / __NFDBITS]是数组的大小,确保可以容纳足够的位来表示所有的文件描述符;__FDS_BITS(set) 宏提供了访问位数组的方式。

总的来说,fd_set是一个比特位集合。大小默认为1024。

我们通常使用四个宏来操作fd_set,实现添加、删除或检查文件描述符在集合中的状态。

 (3)FD_ZERO, FD_SET, FD_CLR, FD_ISSET

select() 为了操作和管理文件描述符集合,提供了四个宏:FD_ZERO、FD_SET、FD_CLR 和 FD_ISSET

头文件声明如下:

FD_ZERO(fd_set *set):初始化文件描述符集合,将集合中的所有文件描述符状态清零。

  FD_ZERO(&set);

 FD_SET(int fd, fd_set *set):将指定的文件描述符 fd 添加到文件描述符集合 set 中。

  FD_SET(fd, &set);

FD_CLR(int fd, fd_set *set):从文件描述符集合 set 中移除指定的文件描述符 fd。

  FD_CLR(fd, &set);

FD_ISSET(int fd, fd_set *set):检查文件描述符集合 set 中是否包含指定的文件描述符 fd。

  if (FD_ISSET(fd, &set)) {
      // 文件描述符 fd 在集合 set 中
  }

5、案例

通过select()实现一个多客户端支持的 TCP 服务器,能够同时处理多个客户端的连接和数据传输。

完整代码

#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>


int main(){
  // 创建 socket
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  // 设置服务器地址
  struct sockaddr_in servaddr;
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
  servaddr.sin_port = htons(2000);  //0-1023
  // 绑定 socket
  if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
  {
    printf("bind failed:%s\n", strerror(errno));
    close(sockfd);
    return 1;
  }
  // 监听连接
  listen(sockfd, 10);
  printf("listen finished\n");

  fd_set rfds, rset;       //fd集合+可读集合
  FD_ZERO(&rfds);          //清空
  FD_SET(sockfd,&rfds);    //设置集合内fd

  struct sockaddr_in clientaddr;
  socklen_t len = sizeof(clientaddr);
  int maxfd = sockfd;

  while(1){
    rset = rfds;
    int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);    //返回集合总数
    if (FD_ISSET(sockfd, &rset)){
      int clientfd = accept(sockfd, (struct sockaddr*) & clientaddr, &len);
      FD_SET(clientfd, &rfds);
      if(clientfd > maxfd){    //考虑fd复用的情况,client连接断开后重连可能复用小fd
        maxfd = clientfd;
      }
      printf("New client connected: %d\n", clientfd);
    }

    int i = 0;
    for(i = sockfd+1; i <= maxfd+1; i++){
      if(FD_ISSET(i, &rset)){
        char buffer[1024] = {0};
        int count = recv(i, buffer, 1024, 0);
        if(count == 0){
          printf("client disconnect: %d\n", i);
          close(i);
          FD_CLR(i, &rfds);
          continue;
        }
        printf("RECV:%s\n", buffer);
        count = send(i, buffer, count, 0);
        printf("SEND:%d\n", count);
      }
    }
  }
printf("listen finished\n"); 
getchar();

printf("exit\n");

return 0;
}

五、总结

Select是早期 Unix/Linux 系统中实现 I/O 多路复用的基础机制,它通过一个位图管理多个文件描述符,允许程序同时监听多个 I/O 事件(如可读、可写、异常等)。它的历史意义在于首次引入了 I/O 多路复用的概念,为后续更高效的机制(如 Poll 和 Epoll)奠定了基础。

然而,Select 存在明显的缺陷:一是文件描述符数量有限(通常为1024),无法满足高并发需求;二是每次调用都需要将文件描述符集合从用户空间拷贝到内核空间,且返回时需要遍历所有描述符,性能较低。

这些缺陷促使了 Poll 的诞生,后者在文件描述符数量和灵活性上进行了改进,为 I/O 多路复用技术的进一步发展铺平了道路。

六、资料参考

https://github.com/0voice

后文 IO多路复用及其机制(2) poll()-优快云博客 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值