深入理解网络 I/O 多路复用:SELECT、POLL

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:网络 I/O
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • I/O 复用模型
  • 图解分析
    • SELECT 函数
    • POLL 函数
    • 两者函数区别
  • 源码实践
    • 服务端代码
    • 流程说明
    • 测试挥手问题
  • NIO、I/O 多路复用
  • 总结

前言

Unix/Linux 下可用的 I/O 模型有以下五种:

  1. 阻塞式 I/O
  2. 非阻塞式 I/O
  3. I/O 复用(select、poll)
  4. 信号驱动式 I/O(SIGIO)
  5. 异步 I/O

在 Linux 中操作内核时,所有的无非三种操作,分别是输入、输出、报错输出

0-输入
1-输出
2-报错输出

一个输入操作通常包括两个不同的阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字(Socket)的输入操作,第一步通常涉及等待数据从网络中;当所等待分组到达时,它被复制到内核中的某个缓冲区,第二步就是把数据从内核缓冲区复制到应用进程缓冲区

I/O 复用模型

I/O 复用(I/O multiplexing):SELECT、POLL,阻塞在这两个系统「用户态、内核态」调用中的某一个之上,而不是阻塞在真正的 I/O 系统调用上

在这里插入图片描述

将阻塞于 select 调用,等待数据报套接字变为可读,当 select 返回的套接字可读这一条件时,再调用 recvfrom 把所读的数据报复制到应用进程缓冲区

使用 select 的优势在于我们可以等待多个描述符就绪

与 I/O 复用密切相关的另外一种模型就是在多线程的场景下使用阻塞时 I/O 模型 BIO,两者极其相似,但它没有使用 select 阻塞在多个文件描述符上,而是使用多个线程「每个文件描述符分配一个线程的方式」这样每个线程都可以自由地调用诸如:recvfrom 之类的阻塞式 I/O 系统调用了.

图解分析

SELECT 属于 synchronous I/O multiplexing 同步 I/O 多路复用

它允许程序监视多个文件描述符,等待一个或多个文件描述符尾某种类型(read、write)的 I/O 操作

在这里插入图片描述

SELECT 仅用一次系统调用,recv | recvfrom 接收数据都只是会接收具体可操作的文件描述符数量,例如:图中带有 data 的 IO 操作

SELECT 函数

该函数允许进程指示内核等待多个事件「文件描述符」中的任何一个发生,并且只有在一个或多个时间发生或经历一段指定的时间后才能唤醒它

以上图中的 IO 块作为例子延迟,当调用 select 函数时,告知内核仅在以下情况发生时才进行返回

  1. IO 集合:1、7 中任何描述符准备好读
  2. IO 集合:4、5 中任何描述符准备好写
  3. IO 集合:2、3、6 中任何描述符有异常条件待处理
  4. 这个过程经历了 Xxx 秒

当调用 select 时要告知内核对哪些描述符(read、write、exception)感兴趣以及需等待多长时间

通过 Linux 内核帮助文档来学习 SELECT

man 2 select
在已经有的 Linux 内核中已经不支持使用 SELECT 函数了,查看文档时会出现:none - deprecated system calls,例如内核版本:Linux version 5.11.12-300.el7.aarch64

当然在 x86_64 内核版本仍然可以查看该帮助文档,例如内核版本:Linux version 3.10.0-1160.90.1.el7.x86_64

可通过 cat /proc/version 命令来查看

在它的文档中可以看到以下这些函数都是同步 I/O 多路复用使用的.

select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

观察 select 函数源码,如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

nfds:代表文件描述符的数量
*readfds:代表读取的文件描述符
*writefds:代表写入的文件描述符
*exceptfds:代表异常条件的文件描述符
*timeout:代表等待多长的时间

它的 timeval 结构用于指定这段时间的秒数、微妙数,结构源码如下:

struct timeval {long    tv_sec;         /* seconds */long    tv_usec;        /* microseconds */
};

其中 select > timeout 参数有以下三种可能性:

  1. 永远等待下去:仅在有一个描述符准备好 I/O 时才返回,为此,可以将该参数设置为空
  2. 等待一段固定的时间:在有一个描述符准备好 I/O 时才返回,但是不会超过该参数所设置的秒数、微妙数
  3. 根本不等待:检查描述符完成后立即返回,称之为 轮询(polling),为此,该参数中的秒数、微妙数必须设置为 0

前面两种可能性的等待通常会被进程在等待期间捕获的信号中断,并从信号处理函数中返回

其中 select > readfds、writefds、exceptfds 三个参数指向的是一组描述符结果集

最大描述符数
SELECT 最大描述符数,大多数应用程序不会用到许多描述符,臂如说很少能够找到一个同时使用几百个描述符的应用程序,然而使用那么多描述符的应用程序确实村再,它们往往使用 select 来复选描述符。最初设计 select 时,操作系统通常对每个进程可用的最大描述符数设置了上限

取自于 4.4 BSD <sys/types.h> 头文件中,最大的描述符在内核被定义为一个常量:FD_SETSIZE 值为 1024,此时可以使用 poll 代表 select,这样可以避免描述符有限的问题.

POLL 函数

执行与 SELECT 类似的任务,它等待一组文件描述符中的一个准备好执行 I/O,不过在处理流设备时,它能够提供额外的信息

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

第一个参数是一个指向一个结构数组第一个元素的指针,每个数组元素都是一个 pollfd 结构,用于指定测试某个给定描述符 fd 的条件.

struct pollfd {int   fd;         /* file descriptor */short events;     /* requested events */short revents;    /* returned events */
};

要测试的条件由 events 成员指定,函数在相应的 revents 成员中返回该描述符的状态(每个描述符都有两个变量,一个为调用值,另外一个为返回结果,从而避免了「值-结果」参数)这两个成员的每一个都由指定某个特定条件的一位或多位构成

值-结果参数:代表传入的值和输出的值不是同一个参数,对比 select 来说,它的中间三个参数都是「值-结果」参数

字段 events:代表一个输入参数,一个指定应用程序对文件描述符 fd 感兴趣的事件的位 bit 掩码;若将此字段指定为零,则忽略 fd 中所有的事件,并且 revents 返回 0

字段 revents:代表一个输出参数,由内核填充实际发生的事件,事件返回的 bit 位可以包括事件中指定的任何位,也可以包括:POLLERR、POLLHUP 或 POLLNVAL 值之一

POLLERR、POLLHUP 或 POLLNVAL 这三个值在 events 字段中设置是没有意义的,当相应的条件为真时,将在 revents 字段中设置.

用于指定 events 标志以及 revents 标志的一些常量值,如下表格:

常量值作为 events 输入作为 events 结果说明
POLLIN☑️☑️普通或优先级带数据可读
POLLRDNORM
(Read Normal)
☑️☑️普通数据可读
POLLRDBAND
(Read Band)
☑️☑️优先级带数据可读
POLLPRI
(Priority)
☑️☑️高优先级数据可读
POLLOUT☑️☑️普通数据可写
POLLWRNORM
(Write Normal)
☑️☑️普通数据可写
POLLWRBAND
(Write Band)
☑️☑️优先级带数据可写
POLLERR
(Error)
☑️发生错误条件(仅输出)
POLLHUP
(Hang up)
☑️发生挂起(仅输出)
POLLNVAL☑️无效请求:fd未打开(仅输出)
描述符不是一个打开的文件

该表格分为三个部分:第一部分是处理输入的四个常量值,第二部分是处理输出的三个常量值,第三部分是处理错误的三个常量值,其中第三部分的常量值不能在 events 中设置,但是当相应条件(第三部分常量值)存在时就在 revents 中返回.

POLL 识别三类数据:普通(Normal)、优先级带(Priority Band)、高优先级(Hign Priority)

POLLIN 可被定义为 POLLRDNORM、POLLRDBAND 的逻辑或

第二个参数 nfds:调用者应该在 nfds 中指定 fds 数组中的项数

第三个参数 timeout:timeout 参数指定 poll() 将阻塞的最小毫秒数(这个时间间隔将四舍五入到系统时钟粒度,内核调度延迟意味着阻塞时间间隔可能会超出一小部分)在 timeout 中指定负值意味着无限超时。指定超时为零将导致 poll() 立即返回,即使没有文件描述符准备好

负值:无限超时
0:立即返回
正数:阻塞的最小毫秒数

两者函数区别

在 SELECT 函数中提及到了 FD_SETSIZE,每个描述符集最大的描述符数量,当有了 POLL 就无须考虑这个问题,因为分配了一个 pollfd 结构的数组并把数组中元素的数目通知内核成了调用者的责任,内核不再需要知道 fd_set(SELECT 中的参数类型) 固定大小的数据类型

POSIX 规范对 select、poll 都有需要,然而从当今的可移植性来考虑,支持 select 系统比支持 poll 系统要多,另外除了 select 函数还定义了 pselect 函数,它能够处理信号阻塞并提供了更高时间分辨率的 select 的增强版本,而在 POSIX 规范中没有为 poll 定义类似的东西.

源码实践

以下代码是服务端侧的所有代码,处理三种类型事件:注册事件、写事件、读事件,客户端代码可以沿用之前 BIO、NIO 中的源码,或者说可以在命令窗口通过 nc 命令进行连接!!!

服务端代码

package org.vnjohn.select;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;/*** @author vnjohn* @since 2023/12/7*/
public class SelectMultiplexingSocketThread extends Thread {private Selector selector = null;/*** 初始化 socket 服务端实例,进行 accept*/public void initServer() {try {ServerSocketChannel server = ServerSocketChannel.open();server.configureBlocking(false);server.bind(new InetSocketAddress(8090));//  select、poll、*epoll 都是使用同样的方式打开selector = Selector.open();server.register(selector, SelectionKey.OP_ACCEPT);} catch (IOException e) {e.printStackTrace();}}@Overridepublic void start() {initServer();System.out.println("Socket Server start...");try {while (true) {// select()// select(long timeout):毫秒级别的等待while (selector.select() > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();if (key.isAcceptable()) {acceptHandler(key);} else if (key.isReadable()) {// 只处理了{read}并注册一个对这个 key 感兴趣的 write 事件readHandler(key);} else if (key.isWritable()) {// 写事件->当 Send-Queue 为空时,就一定会给你返回可以写的事件,就会回调我们的写方法// 多路复用器能不能写是参考:Send-Queue 有没有空间// 1、你准备好要写什么了,这是第一步// 2、第二步你才关心 Send-Queue 是否有空间// 3、so,读 read 一开始就要注册,但是 write 依赖以上关系,什么时候用什么时候注册// 4、若一开始就注册了 write 事件,进入死循环,一直调起!!!writeHandler(key);}}}}} catch (IOException e) {e.printStackTrace();}}/*** 该方法用于接收新的客户端连接进来,在此处就会向 SELECT 结果集注册一个 read 事件,用于接收客户端发送过来的数据** @param key*/public void acceptHandler(SelectionKey key) {try {ServerSocketChannel ssc = (ServerSocketChannel) key.channel();SocketChannel client = ssc.accept();// NIO->NON_BLOCKING!!!client.configureBlocking(false);// 分配给 8M 写入的空间ByteBuffer buffer = ByteBuffer.allocate(8192);client.register(selector, SelectionKey.OP_READ, buffer);System.out.println("-------------------------------------------");System.out.println("new SocketClient:" + client.getRemoteAddress());System.out.println("-------------------------------------------");} catch (IOException e) {e.printStackTrace();}}/**** @param key*/private void writeHandler(SelectionKey key) {System.out.println("write handler...");SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();buffer.flip();// 判断服务端与客户端之间所在的缓冲区是否有数据存在,有则进行写入操作,将数据发送给客户端while (buffer.hasRemaining()) {try {client.write(buffer);} catch (IOException e) {e.printStackTrace();}}// 模拟一下业务延迟,将注册进当前客户端的写事件注销掉,下一次 select 时该事件不会被获取到// 当下一次客户端有数据要读取时,写入该客户端的事件将会再次被注册try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}buffer.clear();key.cancel();try {client.close();} catch (IOException e) {e.printStackTrace();}}/*** 写事件,针对当前选中的键做读取操作,当读取到的数据不为空时,在我们这里模拟写的操作,此时 Send-Queue 肯定是有数据存在的* 所以在这里会模拟>注册上一个 write 写事件,把数据写给对应的客户端** @param key 当前选中的键 > 文件描述符*/public void readHandler(SelectionKey key) {System.out.println("readHandler...");SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();buffer.clear();int read;try {while (true) {read = client.read(buffer);if (read > 0) {// 关心 OP_WRITE 其实就是关心 Send-Queue 是不是有空间client.register(key.selector(), SelectionKey.OP_WRITE, buffer);} else if (read == 0) {break;} else {client.close();break;}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {SelectMultiplexingSocketThread service = new SelectMultiplexingSocketThread();service.start();}
}

通过模拟一个线程,死循环的方式从 Selector 中获取所有的事件 Key,通过 Key 类型的不同来执行不同的处理逻辑

注册 > accept:acceptHandler
写 > write:writeHandler
读 > read:readHandler

Selector 是 SELECT/POLL、Epoll 在 Java 应用程序中的抽象,通过 Selector 优先选择的是 Epoll,但是可以通过 -D 参数来修正,主要就是更改所依赖的 Selector 实现类

select/poll-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider
epoll-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider

  1. Selector#open:在 select/poll 模型下,它和内核之间是没有交互的,至于 Epoll 我们在下篇文章详细讲解
  2. SelectableChannel#register(Selector, SelectionKey.OP_ACCEPT):相当于在内核中是一个 listen 函数的调用

register:在 select/poll 模型中,JVM 里面会为其开辟一个数组将监听的文件描述符放进去,期间是无系统调用的

  1. Selector#select:相当于就是内核中的 select(fd)、poll(fd) 函数

select 有两种方式支持调用,select()、select(long timeout)
当设置了时间,等待对应参数值的毫秒级别数返回
当未设置时间,值为 0

也就是在介绍两个函数时所描述的一样,0 代表立即返回,设置了时间那么就等待一定时间以后才返回调用的结果

  1. Selector#selectedKeys:返回的是哪些有状态的 fds 文件描述符集合 > SelectionKey

无论是什么样的多路复用器:select、poll、epoll、kqueue,只需要返回给我有状态的 fd 文件描述符即可

在 NIO 处理时,是服务端这一侧对着每一个 fd 触发系统调用,这样肯定是会浪费很多资源的,而这里调用一次 select 就可以知道那么 fd 可以进行 R/W

在这个处理期间仍然会有两种类型的 Socket:服务端 Socket 用于 accept 接收客户端连接的、客户端连接后的 Socket(用于读写数据使用的)

  1. SelectionKey#isAcceptable:准备好去接收新的 Socket 连接

在语义上,accept 接收连接且返回新连接的 FD,那么在 select/poll 中 FD 是如何存储的呢?

因为在它们内核是没有空间的,所在会在 JVM 中保存这些 FD 信息,与第二点所描述的 SelectableChannel#register(Selector, SelectionKey.OP_ACCEPT) 存放在一起

在源码中的 acceptHandler 方法中,出现 client.register(selector, SelectionKey.OP_READ, buffer) 将其装入 JVM 数组

  1. SelectionKey#isReadable:准备去接收读取的 Socket 连接,在这里会将 R/W 同时都处理

在当前线程,这个方法可能会被阻塞住(处理过多的数据与逻辑)

所以,后面提出了 IO Threads 概念(拿到数据以后就不管后续的事情了,丢给后面的 worker 线程去进行处理)

Redis 就是运用 epoll 模型,同时它里面也有 IO Threads 概念,Redis 工作线程虽然是单线程的,但是它的网络 I/O、数据处理流都是采用多线程的

在源码中的 readHandler 方法中,拿到 FD buffer 中的数据以后,进行 read 读取,有以下几种情况:
1、若返回 > 0,就会注册上一个 write 写事件,把数据写给对应的客户端
2、若返回为 0,就退出当前循环
3、若返回为 -1,说明客户端已经断开连接,同时将当前读取的 socket 连接关闭

流程说明

以上对源码以及相关的方法进行了说明,接下来我们来测试 select/poll 模型与 TCP/IP 挥手的问题

通过 select/poll I/O 多路复用运行,并 strace 追踪如上代码:

1、先将首行 package 通过 vi 命令移除
2、编译源文件为 class 文件:javac SelectMultiplexingSocketThread.java
3、以 select/poll 模型追踪运行 class 文件:strace -ff -o poll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider SelectMultiplexingSocketThread

运行服务端以后,观察内核所输出的系统调用函数,如下:

在这里插入图片描述

ppoll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}], 2, NULL, NULL, 0
fd:4、5,请求事件:POLLIN,无返回的事件类型

开启另外一个命令窗口,通过 nc localhost 8090 连接服务端,此时在服务端会打印出具体的客户端连接信息,但观察内核文件中时它不会有任何的具体输出内容,由此可见,它并未与内核发生调用关系,而是存储在了自身的 JVM 数组中

在这里插入图片描述

此时,再观察 netstat -natp TCP/IP 连接信息,会出现以下两条条目:

在这里插入图片描述

此时在客户端写入一定内容,观察服务端的执行过程,它会先读取客户端写入的内容,然后通过写事件将内容回放给客户端的窗口中,如下图所示:

客户端:

在这里插入图片描述

服务端:

在这里插入图片描述

在内核侧做出的具体处理,如下图:

在这里插入图片描述

正常状态下,将 nc localhost 8090 开启的客户端,正常退出,再观察 TCP/IP 网络条目,可以发现只有服务端侧的一条条目信息存在,也就是进行了四次挥手的正常流程!!!

在这里插入图片描述

测试挥手问题

在流程说明的最后,客户端与服务端之间的挥手流程是正常走完的,也就是说不会在服务端中存在非正常状态的客户端 sockfd 存在

若我们将源码中 client.close(); 代码进行注释,然后再测试客户端关闭的一个过程

1、注释 client.close(); 客户端关闭的代码,然后重新编译执行服务端代码

2、通过 nc localhost 8090 模拟客户端再次连接到服务端上.

3、关闭客户端的连接以后,此时再观察 TCP/IP 条目,如下所示:

在这里插入图片描述

tcp6       0      0 ::1:50306               ::1:8090                FIN_WAIT2   -
tcp6       0      0 ::1:8090                ::1:50306               CLOSE_WAIT  16838/java

TCP 需要经过四次挥手,虽然在客户端这边 FIN 标记成功了,但是在服务端这边还没有关闭,因为服务端的 FIN 标记在客户端那一侧未作出 ACK 反应,所以在 TCP/IP 条目中就会出现不完整的状态 FIN_WAIT2 | CLOSE_WAIT 信息

由此可见,TCP 四次挥手的过程,是两端都需要经过 FIN+ACK 处理以后,整个信息才能完全保证安全的释放

以上就会造成一个问题:服务端一直持有客户端的连接信息,但是这条信息是已经是没必要的,它会浪费在服务端这一侧的一条「四元组:源 IP:端口>目标 IP:端口」信息,内核中的 socketfd 就一直会被占用,相同的对端服务端也就一直不能使用这个资源建立新的连接,浪费的资源和名额!!!

NIO、I/O 多路复用

在这里插入图片描述

在 NIO 中,每次都需要遍历获取 I/O 状态,每次都需要经过用户态、内核态之间的转换才能实现,然后再交由给应用程序去进行 R/W

在 I/O 多路复用中,通过一个系统调用,可以获取到所有 I/O 状态,由应用程序对有状态的 IO 进行 R/W

总结

该篇博文主要介绍的是 I/O 模型中的多路复用:SELECT、POLL,简要分析了 I/O 多路复用的模型,通过图解分析的方式告知多路复用所带来的好处「介绍了 SELECT、POLL 函数以及它们的区别」,通过实践代码的方式来分析 I/O 多路复用在系统调用中所涉及到的内核流程代码,同时也当代码编写不当时会给 TCP 挥手带来的问题,最后介绍了上篇 NIO 博文与 I/O 多路复用之间的区别,希望能够得到你的支持,感谢三连

四元组唯一:源 IP、源端口、目标 IP、目标端口

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

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

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

相关文章

工业4G路由器助力轨道交通城市地铁实现数字化转型

随着城市的科技不断发展&#xff0c;地铁系统的智能化程度也在不断提高。地铁闸机的网络部署已经成为地铁建设中必不可少环节。而4G路由器作为地铁闸机的网络通讯设备&#xff0c;助力轨道交通地铁闸机实现数字化转型。 工业4G路由器在地铁系统光纤宽带网络遇到故障或其他问题…

CGAL的推进前表面重建

从非结构化点云进行的曲面重建相当于生成一个合理的曲面&#xff0c;该曲面很好地近似于输入点。由于可能生成许多曲面&#xff0c;因此此问题不适合解决。已经提出了一系列广泛的方法来解决这个问题。其中包括变分方法、张量表决、隐式曲面和Delaunay三角剖分。 对于基于Delau…

gRPC三种流和消息格式

文章目录 消息格式RPC流长度前缀的消息分帧请求消息响应信息 三种流一元RPC服务流RPC客户流RPC双工流RPC 代码仓库 消息格式 RPC流 服务端实现protocol buffer定义的方法&#xff0c;客户端保留一个存根&#xff0c;提供服务端方法的抽象&#xff0c;客户端只需要调用存根中的…

【dig命令查询方法】

dig&#xff08;Domain Information Groper&#xff09;是一个用于查询DNS&#xff08;域名系统&#xff09;的命令行工具&#xff0c;它可以帮助您获取关于域名的各种信息&#xff0c;如IP地址、MX记录、NS记录等。下面是dig的详细使用教程。 基本语法&#xff1a; dig [ser…

文件被删除了怎么恢复?3个宝藏方法,快来get!

“我是一个学生党&#xff0c;期末的一些资料保存在电脑上&#xff0c;但是不知道是不是被我误删了&#xff0c;导致很多文件都找不到了。文件被删除了怎么恢复呢&#xff1f;大家帮我出出主意吧&#xff01;” 对于经常在电脑上保存各种文件的用户来说&#xff0c;文件误删除是…

IPC之五:使用 System V 信号量集解决经典的‘哲学家就餐问题‘

IPC 是 Linux 编程中一个重要的概念&#xff0c;IPC 有多种方式&#xff0c;本文主要介绍信号量集(Semaphore Sets)&#xff0c;尽管信号量集被认为是 IPC 的一种方式&#xff0c;但实际上通常把信号量集用于进程间同步或者资源访问互斥&#xff0c;信号量集和共享内存(Shared …

系列学习前端之第 2 章:一文精通 HTML

全套学习 HTMLCSSJavaScript 代码和笔记请下载网盘的资料&#xff1a; 链接: https://pan.baidu.com/s/1-vY2anBdrsBSwDZfALZ6FQ 提取码: 6666 HTML 全称&#xff1a;HyperText Markup Language&#xff08;超文本标记语言&#xff09; 1、 HTML 标签 1. 标签又称元素&#…

PR剪辑视频素材|真实胶片特效转场PR项目模板文件

Premiere转场模板&#xff0c;8个真实胶片特效转场PR项目模板视频素材。可以编辑和自定义媒体占位符。在视频的开头、中间和结尾使用。包含视频教程。 适用软件&#xff1a;PR2023及更新版本 | 分辨率&#xff1a;38402160 (4K) | 文件大小&#xff1a;16.43MB | 无需插件 来自…

AI并行计算:CUDA和ROCm

1 介绍 1.1 CUDA CUDA&#xff08;Compute Unified Device Architecture&#xff09;是Nvidia于2006年推出的一套通用并行计算架构&#xff0c;旨在解决在GPU上的并行计算问题。其易用性和便捷性能够方便开发者方便的进行GPU编程&#xff0c;充分利用GPU的并行能力&#xff0…

如何使用ArcGIS Pro制作类似CAD的尺寸注记

经常使用CAD制图的朋友应该比较熟悉CAD内的尺寸标注&#xff0c;这样的标注看起来直观且简洁&#xff0c;那么在ArcGIS Pro内能不能制作这样尺寸注记呢&#xff0c;答案是肯定的&#xff0c;这里为大家介绍一下制作的方法&#xff0c;希望能对你有所帮助。 数据来源 本教程所…

OpenGL ES 帧缓冲对象介绍和使用示例

一、介绍 1. 帧缓冲对象 默认情况下&#xff0c;OpenGL渲染的目标是屏幕&#xff0c;但如果你不想直接渲染到屏幕上&#xff0c;还需要对渲染结果做某些后期处理、渲染到纹理、阴影映射等操作&#xff0c;便可以使用帧缓冲对象&#xff0c;实现离屏渲染。 帧缓冲对象&#x…

JSON字符串转泛型对象

JSON字符串转泛型对象 以下问题只仅限于博主自身遇到&#xff0c;不代表绝对出现问题 相关类展示&#xff1a; 参数基类 public class BaseParams { }基类 public abstract class AbstractPush<Params extends BaseParams> {protected abstract void execute(Params…

怎么选护眼灯?考公必备护眼灯推荐

随着现在的近视问题受到越来越广泛的关注&#xff0c;各种护眼产品的需求量也一直在快速增加&#xff0c;其中最受广大家长、学生党欢迎的还是护眼台灯。因为专业的护眼台灯不仅可以补充我们夜晚工作、学习时不足的光亮&#xff0c;避免眼睛长时间处于昏暗的环境导致疲劳。 而…

018 OpenCV 人脸检测

目录 一、环境 二、分类器原理 2.1、概述 2.2、工作原理 三、人脸检测代码 一、环境 本文使用环境为&#xff1a; Windows10Python 3.9.17opencv-python 4.8.0.74 二、分类器原理 CascadeClassifier是OpenCV&#xff08;开源计算机视觉库&#xff09;中的一个强大的类…

day01十五模拟第二期

2 2——608#include <stdio.h> #include <math.h>int main(int argc, const char * argv[]) {int res1;for(int i1;i<2023;i){res(res*2)%1000;}printf("%d",res );return 0; }3 4-----4169 #include <stdio.h> #include<string.h> #inc…

C语言——指针(五)

&#x1f4dd;前言&#xff1a; 上篇文章C语言——指针&#xff08;四&#xff09;更加深入的介绍了不同类型指针的特点&#xff0c;这篇文章主要想记录一下函数与指针的结合运用以及const和assert关于指针的用法&#xff1a; 1&#xff0c;函数与指针 2&#xff0c;const 3&am…

3.PyTorch——常用神经网络层

import numpy as np import pandas as pd import torch as t from PIL import Image from torchvision.transforms import ToTensor, ToPILImaget.__version__2.1.13.1 图像相关层 图像相关层主要包括卷积层&#xff08;Conv&#xff09;、池化层&#xff08;Pool&#xff09;…

node.js和浏览器之间的区别

node.js是什么 Node.js是一种基于Chrome V8引擎的JavaScript运行环境&#xff0c;可以在服务器端运行JavaScript代码 Node.js 在浏览器之外运行 V8 JavaScript 引擎。 这使得 Node.js 非常高效。 浏览器如何运行js代码 nodejs运行环境 在浏览器中&#xff0c;大部分时间你所…

Linux系统调试课:USB 常用调试方法

文章目录 一、USB调试工具有哪些二、USB相关节点2.1、USB枚举成功标志2.2、USB speed查询2.3、USB 查询PID、VID沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要 介绍 USB 常用调试方法。 一、USB调试工具有哪些

Sanic:一个极速Python Web框架

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;我是彭涛&#xff0c;今天为大家分享 Sanic&#xff1a;一个极速Python Web框架&#xff0c;全文3500字&#xff0c;阅读大约12分钟。 随着 Web 应用的日益复杂&#xff0c;选择一个高性能的 Web 框架变得尤为…