IO复用 select函数

news/2025/1/23 1:00:03/文章来源:https://www.cnblogs.com/qiuliw/p/18687007

I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。

通常,网络程序在下列情况下需要使用I/O复用技术:

客户端

  • 客户端程序要同时处理多个socket。比如非阻塞connect技术。
  • 客户端程序要同时处理用户输入和网络连接。比如聊天室程序。

服务器

  • TCP服务器要同时处理监听socket和连接socket。这是I/O复用使最多的场合。
  • 服务器要同时处理TCP请求和UDP请求。
  • 服务器要同时监听多个端口,或者处理多种服务。

阻塞性:需要指出的是,I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。

Linux下实现I/O复用的系统调用主要有select、poll和epoll。

select系统调用

select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。(由内核执行)

select API

#include<sys/select.h>
int select(int nfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,struct timeval*timeout);
  1. nfds:指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的。

  2. readfds、writefds和exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这3个参数是fd_set结构指针类型。

fd_set结构体的定义如下:

#include<typesizes.h>
#define__FD_SETSIZE 1024
#include<sys/select.h>
#define FD_SETSIZE__FD_SETSIZE
typedef long int__fd_mask;
#undef__NFDBITS
#define__NFDBITS(8*(int)sizeof(__fd_mask))
typedef struct
{
#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结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
由于位操作过于烦琐,我们应该使用下面的一系列宏来访问fd_set结构体中的位:

#include<sys/select.h>
FD_ZERO(fd_set*fdset);/*清除fdset的所有位*/
FD_SET(int fd,fd_set*fdset);/*设置fdset的位fd*/
FD_CLR(int fd,fd_set*fdset);/*清除fdset的位fd*/
int FD_ISSET(int fd,fd_set*fdset);/*测试fdset的位fd是否被设置*/
  1. timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout值是不确定的。

timeval结构体的定义如下:

struct timeval
{
long tv_sec;/*秒数*/
long tv_usec;/*微秒数*/
};

由以上定义可见,select 给我们提供了一个微秒级的定时方式:

  • 如果给 timeout 变量的 tv_sec 成员和 tv_usec 成员都传递 0,则 select 将立即返回。
  • 如果给 timeout 传递 NULL,则 select 将一直阻塞,直到某个文件描述符就绪。

select 的返回值

  • 成功时,返回就绪(可读、可写和异常)文件描述符的总数。
  • 如果在超时时间内没有任何文件描述符就绪,select 将返回 0
  • 失败时,返回 -1 并设置 errno

如果在 select 等待期间,程序接收到信号,则 select 立即返回 -1,并设置 errnoEINTR

文件描述符就绪条件

哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于 select 的使用非常关键。

在网络编程中,下列情况下 socket 可读:

  • socket 内核接收缓存区中的字节数大于或等于其低水位标记 SO_RCVLOWAT。此时我们可以无阻塞地读该 socket,并且读操作返回的字节数大于 0。
  • socket 通信的对方关闭连接。此时对该 socket 的读操作将返回 0。
  • 监听 socket 上有新的连接请求。
  • socket 上有未处理的错误。此时我们可以使用 getsockopt 来读取和清除该错误。

下列情况下 socket 可写:

  • socket 内核发送缓存区中的可用字节数大于或等于其低水位标记 SO_SNDLOWAT。此时我们可以无阻塞地写该 socket,并且写操作返回的字节数大于 0。
  • socket 的写操作被关闭。对写操作被关闭的 socket 执行写操作将触发一个 SIGPIPE 信号。
  • socket 使用非阻塞 connect 连接成功或者失败(超时)之后。
  • socket 上有未处理的错误。此时我们可以使用 getsockopt 来读取和清除该错误。

网络程序中,select 能处理的异常情况只有一种:socket 上接收到带外数据。

主旨思想

  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
  2. 调用一个系统函数(select),监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回
    • 这个函数是阻塞
    • 函数对文件描述符的检测的操作是由内核完成的
  3. 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作

工作过程分析

  1. 初始设定

  1. 设置监听文件描述符,将fd_set集合相应位置为1

  1. 调用select委托内核检测

  1. 内核检测完毕后,返回给用户态结果

代码实现

注意事项

  • select中需要的监听集合需要两个
    • 一个是用户态真正需要监听的集合rSet
    • 一个是内核态返回给用户态的修改集合tmpSet
  • 需要先判断监听文件描述符是否发生改变
    • 如果改变了,说明有客户端连接,此时需要将新的连接文件描述符加入到rSet,并更新最大文件描述符
    • 如果没有改变,说明没有客户端连接
  • 由于select无法确切知道哪些文件描述符发生了改变,所以需要执行遍历操作,使用FD_ISSET判断是否发生了改变
  • 如果客户端断开了连接,需要从rSet中清除需要监听的文件描述符
  • 程序存在的问题:中间的一些断开连接后,最大文件描述符怎么更新?=>估计不更新,每次都会遍历到之前的最大值处,解决方案见高并发优化思考

服务端

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>#define SERVERIP "127.0.0.1"
#define PORT 6789int main()
{// 1. 创建socket(用于监听的套接字)int listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd == -1) {perror("socket");exit(-1);}// 2. 绑定struct sockaddr_in server_addr;server_addr.sin_family = PF_INET;// 点分十进制转换为网络字节序inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr.s_addr);// 服务端也可以绑定0.0.0.0即任意地址// server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);int ret = bind(listenfd, (struct sockaddr*)&server_addr, sizeof(server_addr));if (ret == -1) {perror("bind");exit(-1);}// 3. 监听(TCP连接队列的上限)ret = listen(listenfd, 8);if (ret == -1) {perror("listen");exit(-1);}// 创建读检测集合// rSet用于记录正在的监听集合,tmpSet用于记录在轮训过程中由内核态返回到用户态的集合fd_set rSet, tmpSet;// 清空FD_ZERO(&rSet);// 将监听文件描述符加入FD_SET(listenfd, &rSet);// 此时最大的文件描述符为监听描述符int maxfd = listenfd;// 不断循环等待客户端连接while (1) {tmpSet = rSet;// 使用select,设置为永久阻塞,有文件描述符变化才返回int num = select(maxfd + 1, &tmpSet, NULL, NULL, NULL);if (num == -1) {perror("select");exit(-1);} else if (num == 0) {// 当前无文件描述符有变化,执行下一次遍历// 在本次设置中无效(因为select被设置为永久阻塞)continue;} else {// 首先判断监听文件描述符是否发生改变(即是否有客户端连接)if (FD_ISSET(listenfd, &tmpSet)) {// 4. 接收客户端连接struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_addr_len);if (connfd == -1) {perror("accept");exit(-1);}// 输出客户端信息,IP组成至少16个字符(包含结束符)char client_ip[16] = {0};inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip));unsigned short client_port = ntohs(client_addr.sin_port);printf("ip:%s, port:%d\n", client_ip, client_port);FD_SET(connfd, &rSet);// 更新最大文件符maxfd = maxfd > connfd ? maxfd : connfd;}// 遍历集合判断是否有变动,如果有变动,那么通信char recv_buf[1024] = {0};for (int i = listenfd + 1; i <= maxfd; i++) {if (FD_ISSET(i, &tmpSet)) {ret = read(i, recv_buf, sizeof(recv_buf));if (ret == -1) {perror("read");exit(-1);} else if (ret > 0) {printf("recv server data : %s\n", recv_buf);write(i, recv_buf, strlen(recv_buf));} else {// 表示客户端断开连接printf("client closed...\n");close(i);FD_CLR(i, &rSet);break;}}}}}close(listenfd);return 0;
}

客户端

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>#define SERVERIP "127.0.0.1"
#define PORT 6789int main()
{// 1. 创建socket(用于通信的套接字)int connfd = socket(AF_INET, SOCK_STREAM, 0);if (connfd == -1) {perror("socket");exit(-1);}// 2. 连接服务器端struct sockaddr_in server_addr;server_addr.sin_family = PF_INET;inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr.s_addr);server_addr.sin_port = htons(PORT);int ret = connect(connfd, (struct sockaddr*)&server_addr, sizeof(server_addr));if (ret == -1) {perror("connect");exit(-1);}// 3. 通信char recv_buf[1024] = {0};while (1) {// 发送数据char *send_buf = "client message";write(connfd, send_buf, strlen(send_buf));// 接收数据ret = read(connfd, recv_buf, sizeof(recv_buf));if (ret == -1) {perror("read");exit(-1);} else if (ret > 0) {printf("recv server data : %s\n", recv_buf);} else {// 表示客户端断开连接printf("client closed...\n");}// 休眠的目的是为了更好的观察,放在此处可以解决read: Connection reset by peer问题sleep(1);}// 关闭连接close(connfd);return 0;
}

高并发优化思考

问题

  • 每次都需要利用FD_ISSET轮训[0, maxfd]之间的连接状态,如果位于中间的某一个客户端断开了连接,此时不应该再去利用FD_ISSET轮训,造成资源浪费
  • 如果在处理客户端数据时,某一次read没有对数据读完,那么造成重新进行下一次时select,获取上一次未处理完的文件描述符,从0开始遍历到maxfd,对上一次的进行再一次操作,效率十分低下

解决

  • 考虑到select只有1024个最大可监听数量,可以申请等量客户端数组
    • 初始置为-1,当有状态改变时,置为相应文件描述符
    • 此时再用FD_ISSET轮训时,跳过标记为-1的客户端,加快遍历速度
  • 对于问题二:对读缓存区循环读,直到返回EAGAIN再处理数据

存在问题(缺点)

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小了,默认是1024,如果链接客户端过多,select 采用的是轮询模型,会大大降低服务器响应效率,不应在 select 上投入更多精力。
  • fds集合不能重用,每次都需要重置

参考

《Linux高性能服务器编程》
《牛客C++实战 webserver笔记》
connect及bind、listen、accept背后的三次握手
select源码剖析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/873515.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

25.1.22小记

今天终于涉及到了面向对象中的类与对象的内容,在这里进行简单的记录 封装 : 把数据和对于数据的操作放在一起 对象 : 属性(数据) + 服务(操作) 一般情况,用户只可进行操作,而数据则被保护 自己定义的class可以作为数据类型定义 对象变量是对象的管理者 this : 成员函…

Vue2_引入及基本功能

介绍了 Vue 核心功能,或者说最基本的功能,包括声明式渲染、条件与循环、处理用户输入、组件化应用构建等,声明式渲染包括文本插值和指令两种方法;条件与循环主要是 v-if 和 v-for 这两个指令;处理用户输入涉及 v-on 和 v-model;组件化应用中指明一个组件本质上是一个拥有…

单纯形法原理

单纯形法的原理介绍及python实现代码单纯形法参考连接:单纯形法单纯形法是针对求解线性规划问题的一个算法,这个名称里的 “单纯形” 是代数拓扑里的一个概念,可以简单将“单纯形”理解为一个凸集,标准的线性规划问题(线性规划标准型)可以表示为: \[max\,(or\,min)\quad…

Tomcat 高并发之道原理拆解与性能调优

上帝视角拆解 Tomcat 架构设计,在了解整个组件设计思路之后。我们需要下凡深入了解每个组件的细节实现。从远到近,架构给人以宏观思维,细节展现饱满的美。 上回👉详情点我【Tomcat】Tomcat 架构原理解析到架构设计借鉴 站在上帝视角给大家拆解了 Tomcat 架构设计,分析 To…

Vue3 —— 安装及配置环境

Vue3的安装、配置(✿◕‿◕✿)Vue官网:https://vuejs.org/配置环境终端:Linux和Mac上可以用自带的终端。Windows上推荐用powershell或者cmd。Git Bash有些指令不兼容。安装Node.js:安装地址:https://nodejs.org/en/安装@vue/cli:执行:npm i -g @vue/cli如果执行后面的操作…

二. Redis 超详细的安装教程((七步)一步一步指导,步步附有截屏操作步骤)

二. Redis 超详细的安装教程((七步)一步一步指导,步步附有截屏操作步骤) @目录二. Redis 超详细的安装教程((七步)一步一步指导,步步附有截屏操作步骤)1. Redis 详细安装教程2. Redis 后台基本启动 & 详细的基本使用3. Redis 服务器的关闭和启动的注意事项4. 如何修改 Re…

数据分库分表和迁移方案

在我们业务快速发展的过程中,数据量必然也会迎来突飞猛涨。那么当我们的数据量百倍、千倍、万倍、亿倍增长后,原有的单表性能就不能满足我们日常的查询和写入了,此时数据架构就不得不进行拆分,比如单表拆分成10张表、100张表、单个月分多张表等等。下面我们针对具体案例分析…

Power BI 连接GaussDB提取数据方法

Power BI本身没有直接的链接器来获取GaussDB,目前连接GaussDB的方法有2个: ODBC, JDBC,这两种方式在云端都要通过设置网关,pbi云端通过网关链接到虚拟机或者某台电脑上,电脑安装个人网关(组织网关没有成功,不知道为什么,知道原因的希望能留言),下面说下两种连接方式: …

2025-1-20-盒子模型-弹性盒子模型

重新学一下巩固,之前发的看不了,本来还想着直接看呢 盒子模型 width,height是宽高,padding是内边距,如果里边有文本的话一般是贴着左上方,但是有内边距就不会,类似下边的演示图;border是内外之间边框,就是给宽高之外加一层;margin是外边距,可以理解为是你构造的边框…

【Ubuntu】安装OpenSSH启用远程连接

【Ubuntu】安装OpenSSH启用远程连接 零、安装软件 使用如下代码安装OpenSSH服务端: sudo apt install openssh-server壹、启动服务 使用如下代码启动OpenSSH服务端: sudo systemctl start ssh贰、配置SSH(可跳过) 配置文件 OpenSSH的配置文件所在位置:/etc/ssh/sshd_confi…

CTF-web第二步!

菜狗杯web的传说之下。打开F12,发现有个Game=new Underophidian(gameCanvas)表明有个Game变量存储着数据。在控制台输入Game获取,根据题意,修改分数,然后玩一下就可以得到flag了。

【CodeForces训练记录】Codeforces Round 1000 (Div. 2)

训练情况赛后反思 C题猜了个假结论WA4,每次选择度最多的删掉,在连续三个度都是最大的情况下,删中间的会寄 A题 有点前缀和的感觉,\([1,l]\) 互质个数为 \(l\),\([1,r]\) 互质个数为 \(r\),所以区间 \([l,r]\) 的个数就是 \(r-l\),特判一下 \(l=1,r=1\) 的情况答案是 \(1\…