首页 > 系统相关 >【Linux网络编程】I/O 多路复用技术

【Linux网络编程】I/O 多路复用技术

时间:2024-08-28 16:47:25浏览次数:9  
标签:多路复用 epoll 编程 描述符 fd 事件 Linux 线程 select

【Linux网络编程】I/O 多路复用技术

什么是 I/O 多路复用?为什么需要 I/O 多路复用

最简单的 socket 网络模型,就是单线程模型,一个同时进行监听、处理,然而,单线程模型同时只能服务一个客户端,当线程发生阻塞的时候,其他客户端只能排队等待,甚至连接失败。

为了能够同时服务更多的客户端,可以使用多进程模型,多进程模型中,主进程负责监听 socket 连接,当有客户端连接后,创建新的进程负责专门处理该客户端的请求,但多进程往往占用资源较多。

多线程模型与多进程模型类似,但是线程的资源占用远远小于进程。然而,在实际生产环境中,大多数网络请求往往处理很快,相对客户端请求处理而言,线程的创建和销毁占用了更多的资源。

为了减少线程的创建和销毁,可以采用线程池模型,线程池模型预先创建多个线程,使用“线程池”对这些线程进行管理,当有客户端连接后,主线程将请求放入待处理请求队列,而线程池从待处理请求队列中获取请求,分配给空闲的线程,线程处理完后并不直接销毁,而是阻塞等待新的请求产生。同时,线程池可以根据当前请求产生速度自适应改变线程数量,当请求少的时候,缩减线程池规模,减少资源占用,当请求多的时候,线程池扩容,防止请求大量堆积。

然而,以上的这些模型,一个线程都只能处理一个客户端的请求,使用“I/O 多路复用”,可以使单个线程处理多个请求,I/O 多路复用将等待连接、读、写Socket等事件发生交给内核,因此线程不会因为这些事件阻塞。I/O 多路复用模型通过API将SOCKET交给内核,等待事件发生,当一个或多个事件,API函数将事件从内核态返回,因此线程可以一次处理一个或多个事件。

Linux操作系统提供了三种 I/O 多路复用的 API,即 select/poll/epoll。

select

select 系统调用的原型如下:

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

nfds 参数指定被监听的文件描述符的总数。它被设置为 select 监听所有文件描述符中的最大值加 1,因为文件描述符是从 0 开始计数的。readfds、writefds 和 exceptfds 参数分别指向可读、可写与异常等事件对应的文件描述符集合。

如下为 select 的工作模式:

select 工作模式

当调用 select,fd 集合需要从用户态拷贝至内核态,然后由内核遍历传递进来的 fd 集合,并改变发送数据的 fd 的状态,再由内核态拷贝至用户态,用户再遍历已改变状态的 fd 集合,并读取数据。

缺点:

每次调用 select,都需要将 fd 集合由用户态拷贝至内核态,当 fd 很多时开销很大。
每次调用 select,都需要在内核态与用户态遍历一遍 fd 集合,当 fd 很多时仍很大。
select 支持的文件描述符数量有限,默认是 1024(fd_set 类型决定的)。
fd 集合不能重用,每次都需要重置。

poll

poll 系统调用原型如下:

#include <poll>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

struct pollfd {
  int fd;         // 文件描述符
  short events;   // 注册的事件
  short revents;  // 实际发生的事,由内核填充
}

其中 events 告诉 poll 监听 fd 上的哪些事件,由一系列事件的按位或,revents 则有内核修改,通知应用程序 fd 实际发生了哪些事件。poll 支持的事件类型可参考:pool 事件类型

poll 与 select 相似,相比于 select 优点则是没有文件描述符数量上的限制与 fd 集合也无需每次调用 poll 就要重置。

epoll

内核事件表

epoll 在实现和使用上与 select、poll 有很大的差异。首先 epoll 是使用一组函数来完成任务,而不是单个函数,其次 epoll 把用户关心的文件描述符上的事件放在内核的一个事件表中,从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符集或事件集,但 epoll 需要一个额外的文件描述符,来唯一标识内核中的这个事件表。

使用 epoll_create() 来创建这个文件描述符:

#include <sys/epoll.h>
int epoll_create(int size);

该函数返回的文件描述符将用作其它所有 epoll 系统调用的第一个参数,以指定要访问的内核事件表。

使用 epoll_ctl() 来操作 epoll 的内核事件表:

#include <sys/epoll.h>
int epoll_ctl(int efd, int op, int fd, struct epoll_event* event);

fd 是要操作的文件描述符,op 则指定操作类型,操作类型有如下三种:

  • EPOLL_CTL_ADD:往事件表中注册 fd 上的事件。
  • EPOLL_CTL_MOD:修改 fd 上的注册事件。
  • EPOLL_CTL_DEL:删除 fd 上的注册事件。

event 指定事件,它是 epoll_event 结构指针类型。epoll_event 的定义如下:

struct epoll_event {
  __uint32_t events;  // epoll 事件
  epoll_data_t data;  // 用户数据
};

events 为成员描述事件类型,epoll 支持的事件类型与 poll 基本相同。data 成员中有一个 fd ,以指定事件所属目标的文件描述符。

epoll_ctl() 成功返回 0,失败返回 -1 并设置 errno。

epoll_wait 函数

epoll 系列系统调用的主要接口是 epoll_wait(),它在一段超时时间内等待一组文件描述符上的事件,其原型如下:

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

关于函数的参数,我们从后往前讨论。timeout 指定 epoll 的超时值,当 timeout 为 -1 时,epoll_wait() 将永远阻塞直至某个事件发生,当 timeout 为 0 时,epoll_wait() 立即返回,当 timeout 大于 0 时,表示其阻塞的时长。maxevents 指定最多监听多少个事件,必须大于 0。

当 epoll_wait() 检测到事件,会将所有的就绪事件从内核事件表(epfd 参数指定)复制到第二个参数 events 所指向的数组中。而这个数组只用于 epoll_wait() 检测到的就绪事件,以极大提高应用程序索引就绪文件描述符的效率。

LT 触发与 ET 触发

LT(Level Trigger,水平触发):LT 是默认的工作模式,当 epoll_wait() 检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件,因为应用程序下一次调用 epoll_wait() 后,epoll_wait() 还会再次向应用程序告知此事件,直至事件被处理。

ET(Edge Trigger,边沿触发):在文件描述符上注册 EPOLLET 事件后,当 epoll_wait() 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait() 不会再向应用程序通知这一事件。可见 ET 模式是是一种高效的工作模式,因为它很大程度上减少了同一个 epoll 事件被触发的次数,同时 ET 模式是需要与非阻塞一起使用,因为需要循环的处理读写事件直至完全。

标签:多路复用,epoll,编程,描述符,fd,事件,Linux,线程,select
From: https://www.cnblogs.com/yangxuanzhi/p/18385094

相关文章

  • Linux基础命令
    Linux的目录结构/,根目录是最顶级的目录了Linux只有一个顶级目录:/路径描述的层次关系同样适用/来表示/home/itheima/a.txt,表示根目录下的home文件夹内有itheima文件夹,内有a.txtls命令功能:列出文件夹信息语法:ls[-l-h-a][参数]参数:被查看的文件夹,不提供参数,表示查看......
  • 【Linux网络编程】字节序
    【Linux网络编程】字节序字节序字节序就是字节在内存中存储的顺序,如32位整数0x01234567,在内存中存储时,有如下两种顺序:大端序将数值的高位存储在低位地址中,小端序则相反。网络字节序网络中传输数据均采用大端序。Linux字节序转换函数在#include<netinet/in.h>中提供了4......
  • 【Linux网络编程】Socket Api函数
    【Linux网络编程】SocketApi函数TCP/IP协议族TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用的socket地址结构体,它们分别用于IPv4和IPv6,在此只将IPv4,如下为structsockaddr_in:structsockaddr_in{sa_family_tsin_family;//地址族:AF_INETu_int......
  • Linux 软件目录迁移
    背景:很多软件默认安装到了/root盘下的/var/lib/目录下,导致/root盘很快被占满,如docker。为了释放/root盘,让系统能正常工作,就需要把这些特别大磁盘占用量的目录迁移到挂载了其他磁盘的目录,比如:/home,下面是具体步骤:1. 停止Docker服务systemctlstopdocker 2. 复制 /var/l......
  • 【MSF系列】使用meterpreter从linux系统下载文件到kali
    成功拿到了shell,在/var/www/html/cacti底下有个sql文件,将其下载下来命令download/path/to/file.txt/opt/downloads/file.txt下载后导入本地,可以获得登录密码+----+----------+----------------------------------+-------+---------------+---------------+--------------......
  • Linux APP查询驱动的方式归类总
    LinuxAPP查询驱动的方式归类总前言一、具体方式是什么?1、休眠与唤醒2、阻塞与非阻塞3、POLL机制4、异步通知tips:等待队列用于进程等待条件,工作队列用于异步任务处理。二、使用步骤休眠与唤醒tips:在中断处理函数中,不能休眠,也就不能调用会导致休眠的函数。上半......
  • linux总线设备驱动模型
    linux总线设备驱动模型platform平台驱动模型linux自带I2C、SPI、USB等总线。但是在SOC中有些外设是没有总线这个概念的,但是又要使用总线、驱动和设备模型该怎么办呢?为了解决此问题,Linux提出了platform这个虚拟总线,相应的就有platform_driver和platform_devi......
  • linux编程——认识GCC编译器
    目录一、引言二、GCC编译器简介三、GCC的主要特点四、GCC的使用方法五、GCC的高级功能六、总结一、引言  在Linux编程的广阔世界中,GCC(GNUCompilerCollection)编译器无疑是一个至关重要的工具。对于广大的开发者来说,熟练掌握GCC编译器的使用,能够极大地......
  • 【Linux入门】shell基础篇——if、case、与for循环
    文章目录if的条件分支基本`if`语句包含`else`的`if`语句包含`elseif`(或`elif`)的`if`语句注意示例if的嵌套使用case`case`语句的基本语法:示例使用if语句结合casefor循环for循环的基本格式1.基于列表的`for`循环2.C语言风格的`for`循环注意其他循环基于文件的for循环......
  • 【Linux入门】shell基础篇——while循环与until循环
    文章目录while循环与until循环while循环while循环的基本格式示例:打印0到5的数字死循环的几种实现方式注意退出循环until循环、与while循环的区别until循环示例while循环方式注意有趣的实例批量建立用户批量删除用户猜价格游戏密码输入验证说明while循环与until......