当前位置: 首页 > news >正文

Linux编程:基于 Unix Domain Socket 的进程/线程间通信实时性优化

文章目录

    • 0. 引言
    • 1. 使用 `epoll` 边缘触发模式
      • 非不要不选择阻塞模式
      • 边缘触发(ET)模式
      • 优点
      • 示例
    • 2. 使用实时调度策略
    • 3. CPU 绑定
    • 4. 使用无锁缓冲区
    • 5. 优化消息传递的大小和频率
    • 6. 使用 `SO_RCVTIMEO` 和 `SO_SNDTIMEO`
    • 7. 示例代码
    • 其他阅读

0. 引言

前几天被问到“如何优化Linux中Domain Socket的线程间通信实时性?”当时的回答感觉不够好,经过仔细思考后,我整理出以下优化策略,考虑的是高并发和低延迟场景中的应用优化。

1. 使用 epoll 边缘触发模式

非不要不选择阻塞模式

阻塞式 read() 在单客户端的情况下,能够立即响应数据的到达,但其局限性在于:

  • 无法同时处理多个 I/O 操作。如果同时需要接收和发送数据,阻塞式 read() 会在读取数据时阻塞当前线程,直到数据可用,这使得线程无法在等待数据时执行其他任务(例如发送数据)。 也就是处理双向通信不够高效。
  • 阻塞导致线程空闲。即使线程处于阻塞状态,系统仍需要为其调度,但线程无法做任何实际工作。这样会浪费 CPU 时间,降低系统的响应性和资源利用率。

边缘触发(ET)模式

epoll边缘触发 模式(ET)在文件描述符的状态发生变化时仅触发一次事件。当状态从“不可读”变为“可读”时,epoll 只会通知一次,后续不会触发事件直到状态再次变化。这减少了重复触发事件的系统调用,降低了上下文切换的频率。

优点

  • 减少系统调用和上下文切换:边缘触发模式比水平触发模式(LT)减少了不必要的系统调用。
  • 更低延迟:每个事件只触发一次,避免了多次触发导致的等待时间。
  • 更高效率:配合非阻塞 I/O 使用,避免了重复的事件通知。

示例

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // 设置为边缘触发模式
ev.data.fd = sockfd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {perror("epoll_ctl");exit(EXIT_FAILURE);
}

2. 使用实时调度策略

Linux 提供了 SCHED_FIFOSCHED_RR 等实时调度策略,可以降低调度延迟。通过 sched_setscheduler() 函数设置线程调度策略,有助于提升线程的响应速度。

struct sched_param param;
param.sched_priority = 99;  // 设置较高的优先级
sched_setscheduler(pid, SCHED_FIFO, &param);  // 设置实时调度策略

3. CPU 绑定

将线程绑定到特定的 CPU 核,减少跨核调度和缓存失效,降低延迟。

cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);  // 将线程绑定到指定的 CPU 核
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);

4. 使用无锁缓冲区

使用无锁缓冲区可以减少CPU时间片切换次数:

  • 无锁队列:使用原子操作管理数据结构,避免传统锁机制的性能瓶颈,减少线程同步的开销。

实现请见:C++生产者-消费者无锁缓冲区的简单实现

5. 优化消息传递的大小和频率

每次发送或接收的数据大小直接影响通信延迟。频繁的小数据传输会增加 I/O 操作次数,导致延迟增加。优化措施包括:

  • 批量传输:将多个小消息合并为一个大消息,减少系统调用次数和上下文切换频率。
  • 调整缓冲区大小:根据应用需求调整套接字的发送和接收缓冲区大小,以避免缓冲区过小导致频繁的上下文切换。
int bufsize = 8192;  // 请根据实际设置合适的缓冲区大小
setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
setsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));

6. 使用 SO_RCVTIMEOSO_SNDTIMEO

SO_RCVTIMEOSO_SNDTIMEO 是用来防止套接字在接收或发送数据时无限期阻塞的选项。当设置了这些超时选项后,套接字在等待数据时会在超时后返回错误(如 EAGAINEWOULDBLOCK),从而提高应用程序的响应性。然而,这些选项不能直接解决由于 CPU 调度延迟引起的实时性问题。它们的作用仅仅是在指定时间内没有完成操作时返回错误,而不是保证操作在一定时间内完成。

// 设置接收超时时间
struct timeval recv_timeout = { 1, 0 }; // 1 seconds
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &recv_timeout, sizeof(recv_timeout)) == -1) {perror("setsockopt SO_RCVTIMEO");close(sock);exit(EXIT_FAILURE);
}// 设置发送超时时间
struct timeval send_timeout = { 1, 0 }; // 1 seconds
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &send_timeout, sizeof(send_timeout)) == -1) {perror("setsockopt SO_SNDTIMEO");close(sock);exit(EXIT_FAILURE);
}

7. 示例代码

// g++ -o uds_server uds_server.cpp -pthread
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/un.h>
#include <cstring>
#include <cerrno>
#include <atomic>
#include <pthread.h>
#include <sched.h>#define SOCKET_PATH "/tmp/uds_socket"
#define MAX_EVENTS 10
#define BUF_SIZE 1024
#define SOCKET_BACKLOG 5// 无锁环形缓冲区
class LockFreeBytesBuffer {
public:static const std::size_t kBufferSize = 10240U;  // 缓冲区大小LockFreeBytesBuffer() noexcept : readerIndex_(0U), writerIndex_(0U) {std::memset(buffer_, 0, kBufferSize);}bool append(const char* data, std::size_t length) noexcept;std::size_t beginRead(const char** target) noexcept;void endRead(std::size_t length) noexcept;private:char buffer_[kBufferSize];std::atomic<std::size_t> readerIndex_;std::atomic<std::size_t> writerIndex_;
};bool LockFreeBytesBuffer::append(const char* data, std::size_t length) noexcept {const std::size_t currentWriteIndex = writerIndex_.load(std::memory_order_relaxed);const std::size_t currentReadIndex = readerIndex_.load(std::memory_order_acquire);const std::size_t freeSpace = (currentReadIndex + kBufferSize - currentWriteIndex - 1U) % kBufferSize;if (length > freeSpace) {return false;  // 缓冲区满}const std::size_t pos = currentWriteIndex % kBufferSize;const std::size_t firstPart = std::min(length, kBufferSize - pos);std::memcpy(&buffer_[pos], data, firstPart);std::memcpy(&buffer_[0], data + firstPart, length - firstPart);writerIndex_.store(currentWriteIndex + length, std::memory_order_release);return true;
}std::size_t LockFreeBytesBuffer::beginRead(const char** target) noexcept {const std::size_t currentReadIndex = readerIndex_.load(std::memory_order_relaxed);const std::size_t currentWriteIndex = writerIndex_.load(std::memory_order_acquire);const std::size_t availableData = (currentWriteIndex - currentReadIndex) % kBufferSize;if (availableData == 0U) {return 0U;  // 缓冲区空}const std::size_t pos = currentReadIndex % kBufferSize;*target = &buffer_[pos];return std::min(availableData, kBufferSize - pos);
}void LockFreeBytesBuffer::endRead(std::size_t length) noexcept {const std::size_t currentReadIndex = readerIndex_.load(std::memory_order_relaxed);readerIndex_.store(currentReadIndex + length, std::memory_order_release);
}// 设置套接字为非阻塞
int setSocketNonBlocking(int sockfd) {int flags = fcntl(sockfd, F_GETFL, 0);if (flags == -1) {fprintf(stderr, "Error getting socket flags: %s\n", strerror(errno));return -1;}if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {fprintf(stderr, "Error setting socket to non-blocking: %s\n", strerror(errno));return -1;}return 0;
}// 设置实时调度策略
void setRealTimeScheduling() {struct sched_param param;param.sched_priority = 99;  // 设置较高的优先级if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {fprintf(stderr, "Error setting real-time scheduler: %s\n", strerror(errno));exit(EXIT_FAILURE);}
}// 绑定线程到指定 CPU
void setThreadAffinity(int cpuId) {cpu_set_t cpuset;CPU_ZERO(&cpuset);CPU_SET(cpuId, &cpuset);if (pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset) != 0) {fprintf(stderr, "Error setting thread affinity: %s\n", strerror(errno));exit(EXIT_FAILURE);}
}// 处理新连接
void handleNewConnection(int epollFd, int sockfd) {struct epoll_event ev;int connfd = accept(sockfd, nullptr, nullptr);if (connfd == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {return;}fprintf(stderr, "Error accepting connection: %s\n", strerror(errno));return;}if (setSocketNonBlocking(connfd) == -1) {close(connfd);return;}ev.events = EPOLLIN | EPOLLET;  // 设置为边缘触发模式ev.data.fd = connfd;if (epoll_ctl(epollFd, EPOLL_CTL_ADD, connfd, &ev) == -1) {fprintf(stderr, "Error adding connection to epoll: %s\n", strerror(errno));close(connfd);}
}// 处理读取数据
void handleRead(int epollFd, struct epoll_event& event, LockFreeBytesBuffer& buffer) {char buf[BUF_SIZE];ssize_t nread = read(event.data.fd, buf, sizeof(buf));if (nread == -1) {if (errno != EAGAIN) {fprintf(stderr, "Error reading data: %s\n", strerror(errno));epoll_ctl(epollFd, EPOLL_CTL_DEL, event.data.fd, nullptr);close(event.data.fd);}} else if (nread == 0) {epoll_ctl(epollFd, EPOLL_CTL_DEL, event.data.fd, nullptr);close(event.data.fd);  // 连接关闭} else {fprintf(stdout, "Received data: %.*s\n", static_cast<int>(nread), buf);if (!buffer.append(buf, nread)) {fprintf(stderr, "Error appending to buffer: Buffer overflow!\n");}}
}// 处理写操作
void handleWrite(int epollFd, struct epoll_event& event, LockFreeBytesBuffer& buffer) {const char* data;std::size_t len = buffer.beginRead(&data);if (len > 0) {ssize_t nwrite = write(event.data.fd, data, len);if (nwrite == -1) {if (errno != EAGAIN) {fprintf(stderr, "Error writing data: %s\n", strerror(errno));epoll_ctl(epollFd, EPOLL_CTL_DEL, event.data.fd, nullptr);close(event.data.fd);}} else {buffer.endRead(nwrite);}}
}// 主函数
int main() {int sockfd, epollFd;struct sockaddr_un addr;struct epoll_event ev, events[MAX_EVENTS];// 设置实时调度setRealTimeScheduling();// 设置线程亲和性setThreadAffinity(0);  // 绑定到 CPU 0// 创建 Unix Domain Socketsockfd = socket(AF_UNIX, SOCK_STREAM, 0);if (sockfd == -1) {fprintf(stderr, "Error creating socket: %s\n", strerror(errno));return EXIT_FAILURE;}// 设置套接字为非阻塞if (setSocketNonBlocking(sockfd) == -1) {close(sockfd);return EXIT_FAILURE;}// 绑定套接字到文件路径std::memset(&addr, 0, sizeof(struct sockaddr_un));addr.sun_family = AF_UNIX;std::strcpy(addr.sun_path, SOCKET_PATH);unlink(SOCKET_PATH);if (bind(sockfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) == -1) {fprintf(stderr, "Error binding socket: %s\n", strerror(errno));close(sockfd);return EXIT_FAILURE;}// 监听连接请求if (listen(sockfd, SOCKET_BACKLOG) == -1) {fprintf(stderr, "Error listening on socket: %s\n", strerror(errno));close(sockfd);return EXIT_FAILURE;}// 创建 epoll 实例epollFd = epoll_create1(0);if (epollFd == -1) {fprintf(stderr, "Error creating epoll instance: %s\n", strerror(errno));close(sockfd);return EXIT_FAILURE;}// 将服务器套接字加入 epollev.events = EPOLLIN | EPOLLET;  // 边缘触发模式ev.data.fd = sockfd;if (epoll_ctl(epollFd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {fprintf(stderr, "Error adding socket to epoll: %s\n", strerror(errno));close(sockfd);close(epollFd);return EXIT_FAILURE;}LockFreeBytesBuffer buffer;// 主循环,等待并处理事件while (true) {int n = epoll_wait(epollFd, events, MAX_EVENTS, -1);if (n == -1) {fprintf(stderr, "Error in epoll_wait: %s\n", strerror(errno));break;}for (int i = 0; i < n; i++) {if (events[i].data.fd == sockfd) {// 处理新连接handleNewConnection(epollFd, sockfd);} else if (events[i].events & EPOLLIN) {// 处理读取数据handleRead(epollFd, events[i], buffer);} else if (events[i].events & EPOLLOUT) {// 处理写操作handleWrite(epollFd, events[i], buffer);}}}close(epollFd);close(sockfd);return EXIT_SUCCESS;
}

这个程序监听 Unix 域套接字 /tmp/uds_socket,能够处理多个客户端的连接,并异步地读取和写入数据:

  • 监听和接受连接:服务器首先通过 bindlisten 绑定套接字,然后通过 accept 等待来自客户端的连接。
  • 异步 I/O 事件处理:使用 epoll 来监听并处理事件(如接收数据、发送数据、错误等)。
  • epoll边缘触发:通过设置非阻塞 I/O 和边缘触发模式,程序能够高效地处理大量并发连接。
  • 缓冲区管理:使用环形缓冲区管理接收的数据。

其他阅读

  • 非Domain Socket的优化请参考:Linux编程:嵌入式ARM平台Linux网络实时性能优化
  • Linux 编程:高实时性场景下的内核线程调度与网络包发送优化
  • Linux I/O编程:I/O多路复用与异步 I/O对比

相关文章:

Linux编程:基于 Unix Domain Socket 的进程/线程间通信实时性优化

文章目录 0. 引言1. 使用 epoll 边缘触发模式非不要不选择阻塞模式边缘触发&#xff08;ET&#xff09;模式优点示例 2. 使用实时调度策略3. CPU 绑定4. 使用无锁缓冲区5. 优化消息传递的大小和频率6. 使用 SO_RCVTIMEO 和 SO_SNDTIMEO7. 示例代码其他阅读 0. 引言 前几天被问…...

PET-文件包含-FINISHED

include发生错误报warning&#xff0c;继续执行。require发生错误直接error&#xff0c;不继续执行 无视扩展名&#xff0c;只要能解析&#xff0c;就能当可执行文件执行&#xff0c;哪怕文件后缀或没后缀 1 条件竞争 pass17 只需要知道tmp的路径。把xieshell.jpg上传&…...

《WebGL编程指南》书籍分享

在这个数字化时代&#xff0c;WebGL作为一门前沿的图形渲染技术&#xff0c;为网页带来了前所未有的交互体验。今天&#xff0c;我很荣幸向大家分享一本关于学习WebGL的书籍——《Webgl编程指南》 电子版下载链接: https://pan.baidu.com/s/1eTX2Y5ynYH0pUQRf0Jcbow?...

go T 泛型

目录 1、类型约束 2、泛型函数 3、泛型结构体 4、泛型接口 5、以接口作为类型约束 关键词&#xff1a;泛型、类型参数、类型约束 Go 语言在 1.18 版本引入了泛型&#xff08;Generics&#xff09;特性&#xff0c;可以编写更通用、可复用的代码&#xff0c;泛型可以用于&a…...

React的基础API介绍(二)

目录 useStateuseState 的基本原理1. 状态在函数组件中的引入2. useState 的工作机制3. Hook 状态与组件渲染 useState 的使用方法1. 基本用法2. 多个状态变量3. 更新状态 注意事项与最佳实践1. 状态更新可能是异步的2. 不要直接修改状态3. 更新对象或数组状态4. 避免闭包陷阱 …...

远程开发测试必看:如何在群晖NAS上运行网页版Ubuntu

文章目录 前言1. 下载Docker-Webtop镜像2. 运行Docker-Webtop镜像3. 本地访问网页版Linux系统4. 群晖NAS安装Cpolar工具5. 配置异地访问Linux系统6. 异地远程访问Linux系统7. 固定异地访问的公网地址 前言 本文将详细讲解如何在群晖NAS上部署docker-webtop&#xff0c;并利用c…...

JAVA题目笔记(十五)经典算法题

一、按要求排序 要求&#xff1a;定义数组并存储一些女朋友对象&#xff0c;利用Arrays中的sort方法进行排序 属性包括&#xff1a;姓名&#xff0c;年龄&#xff0c;身高 按照年龄大小进行排序&#xff0c;年龄一样按照身高排序&#xff0c;身高一样按照姓名字母进行排序。…...

「Mac玩转仓颉内测版8」入门篇8 - Cangjie函数与方法

本篇介绍Cangjie编程语言中的函数与方法&#xff0c;帮助理解如何通过函数封装重复操作&#xff0c;提升代码的复用性和可维护性。 关键词 Cangjie函数方法定义参数传递返回值模块化与复用性 一、什么是函数&#xff1f; 函数是一个代码块&#xff0c;用于接收参数、执行操作…...

2024最新版JavaScript逆向爬虫教程-------基础篇之Proxy与Reflect详解

目录 一、监听对象的操作二、Proxy基本使用2.1 创建空代理2.2 定义捕获器2.2.1 Proxy的set和get捕获器2.2.2 Proxy(handler)的13个捕获器 三、Reflect的作用3.1 Reflect的使用3.2 Reflect其余方法(9个)3.3 Proxy与Reflect中的receiver参数3.4 Reflect中的construct方法 ECMAScr…...

代码修改材质参数

1、 如何得到对象使用的材质 获取到对象的渲染器Renderer Mesh Renderer和Skinned Mesh Renderer都继承Renderer&#xff0c;可以用里式替换原则父类获取、装载子类对象 通过渲染器获取到对应材质 可以利用渲染器中的material或者sharedMaterial来获取物体的材质&#xff0…...

[C++11] 包装器 : function 与 bind 的原理及使用

文章目录 functionstd::function 的基本语法使用 std::function 包装不同的可调用对象function包装普通成员函数为什么要传入 this 指针参数&#xff1f;传入对象指针与传入对象实例的区别 例题 &#xff1a;150. 逆波兰表达式求值 - ⼒扣&#xff08;LeetCode&#xff09; bin…...

java项目-jenkins任务的创建和执行

参考内容: jenkins的安装部署以及全局配置 1.编译任务的general 2.源码管理 3.构建里编译打包然后copy复制jar包到运行服务器的路径 clean install -DskipTests -Pdev 中的-Pdev这个参数用于激活 Maven 项目中的特定构建配置&#xff08;Profile&#xff09; 在 pom.xml 文件…...

单片机中的BootLoader(重要的概念讲解)

文章目录 一、链接地址和执行地址1. 链接地址(Load Address)2. 执行地址(Execution Address)链接地址与执行地址的关系实际工作流程总结二、相对跳转和绝对跳转1. 相对跳转(Relative Jump)2. 绝对跳转(Absolute Jump)3. `BX` 和 `BL` 指令总结三、散列文件1. 散列文件的…...

【数据分享】中国食品工业年鉴(1984-2023) PDF

数据介绍 一、《中国食品工业年鉴》(以下简称《年鉴》)是一部全面反映上一年度全国食品工业发展情况纪年性、资料性、权威大型年刊。《年鉴(2023)》系统收录了全国食品行业各专业和 31个省(自治区、直辖市)2022年食品工业经济运行情况的综述&#xff0c;《年鉴》是由中国食品工…...

优选算法 - 1 ( 双指针 移动窗口 8000 字详解 )

一&#xff1a;双指针 1.1 移动零 题目链接&#xff1a;283.移动零 class Solution {public void moveZeroes(int[] nums) {for(int cur 0, dest -1 ; cur < nums.length ; cur){if(nums[cur] 0){}else{dest; // dest 先向后移动⼀位int tmp nums[cur];nums[cur] num…...

FairyGUI和Unity联动(入门篇)

一、FairyGUI编辑器中 1.新建按钮、新建组件 编辑器中界面简易设计如下 2.文件-发布设置-发布路径&#xff1a;自己unity项目Resources所在的路径 二、Unity 使用代码展示UI using FairyGUI; using System.Collections; using System.Collections.Generic; using UnityEngi…...

Go:文件输入输出以及json解析

文章目录 读取用户的输入文件读写读文件写文件 文件拷贝io包中接口的概念JSON 数据格式编码解码任意的数据&#xff1a; 读取用户的输入 从键盘和标准输入 os.Stdin 读取输入&#xff0c;最简单的办法是使用 fmt 包提供的 Scan… 和 Sscan… 开头的函数 看如下的程序 func t…...

编写红绿起爆线指标(附带源码下载)

编写需求&#xff1a; 想问问有没有能标注行情起爆点的指标。 效果展示&#xff1a; 红线上&#xff0c;出现绿柱转红柱做多。 蓝线下&#xff0c;出现红柱转绿柱做空。 源码展示&#xff08;部分源码&#xff0c;完整源码需下载源码文件&#xff09;&#xff1a; IsMainIn…...

设计模式(四)装饰器模式与命令模式

一、装饰器模式 1、意图 动态增加功能&#xff0c;相比于继承更加灵活 2、类图 Component(VisualComponent)&#xff1a;定义一个对象接口&#xff0c;可以给这些对象动态地添加职责。ConcreteComponent(TextView)&#xff1a;定义一个对象&#xff0c;可以给这个对象添加一…...

Android11 修改系统语言

1.定义一个view <RelativeLayoutandroid:id"id/rlChooseLanguage"style"style/SettingAboutItem"><TextViewstyle"style/SettingAboutItemTextView"android:text"string/choose_language" /><ImageView style"st…...

在软件开发中正确使用MySQL日期时间类型的深度解析

在日常软件开发场景中&#xff0c;时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志&#xff0c;到供应链系统的物流节点时间戳&#xff0c;时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库&#xff0c;其日期时间类型的…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案

问题描述&#xff1a;iview使用table 中type: "index",分页之后 &#xff0c;索引还是从1开始&#xff0c;试过绑定后台返回数据的id, 这种方法可行&#xff0c;就是后台返回数据的每个页面id都不完全是按照从1开始的升序&#xff0c;因此百度了下&#xff0c;找到了…...

【算法训练营Day07】字符串part1

文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接&#xff1a;344. 反转字符串 双指针法&#xff0c;两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…...

12.找到字符串中所有字母异位词

&#x1f9e0; 题目解析 题目描述&#xff1a; 给定两个字符串 s 和 p&#xff0c;找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义&#xff1a; 若两个字符串包含的字符种类和出现次数完全相同&#xff0c;顺序无所谓&#xff0c;则互为…...

汇编常见指令

汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX&#xff08;不访问内存&#xff09;XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

uniapp中使用aixos 报错

问题&#xff1a; 在uniapp中使用aixos&#xff0c;运行后报如下错误&#xff1a; AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...

Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成

一个面向 Java 开发者的 Sring-Ai 示例工程项目&#xff0c;该项目是一个 Spring AI 快速入门的样例工程项目&#xff0c;旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计&#xff0c;每个模块都专注于特定的功能领域&#xff0c;便于学习和…...

Chrome 浏览器前端与客户端双向通信实战

Chrome 前端&#xff08;即页面 JS / Web UI&#xff09;与客户端&#xff08;C 后端&#xff09;的交互机制&#xff0c;是 Chromium 架构中非常核心的一环。下面我将按常见场景&#xff0c;从通道、流程、技术栈几个角度做一套完整的分析&#xff0c;特别适合你这种在分析和改…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...