彻底理解零拷贝技术,zero-copy
计算机处理的任务大体可以分为两类:CPU密集型与IO密集型。当前流行的互联网应用更多的属于IO密集型,传统的IO标准接口都是基于数据拷贝的,这篇文章我们主要关注该怎样从数据拷贝的角度来优化IO性能,让你的程序在IO性能方面赶超P8。
为什么IO接口要基于数据拷贝?
操作系统本质上就是一个管家,目的就是更加公平合理的给各个进程分配硬件资源,在操作系统出现之前,程序员需要直面各类硬件,就像这样:
在这一时期程序员真可谓掌控全局,掌控全局带来的后果就是你需要掌控所有细节,这显然不利于生产力的释放。
操作系统应用而生。
计算机系统就变成这样了:
现在应用程序不需要和硬件直接交互了,仅从IO的角度上看,操作系统变成了一个类似路由器的角色,把应用程序递交过来的数据分发到具体的硬件上去,或者从硬件接收数据并分发给相应的进程。
数据传递是通过什么呢?就是我们常说的buffer,所谓buffer就是一块可用的内存空间,用来暂存数据。
操作系统这一中间商导致的问题就是:你需要首先把东西交给操作系统,操作系统再转手交给硬件,这就必然涉及到数据拷贝。
这就是为什么传统的IO操作必然需要进行数据拷贝的原因所在。关于操作系统系统完整的阐述请参见博主的《深入理解操作系统》。然而数据拷贝是有性能损耗的,接下来我们用一个实例来让大家对该问题有一个更直观的认知。
网络服务器
浏览器打开一个网页需要很多数据,包括看到的图片、html文件、css文件、js文件等等,当浏览器请求这类文件时服务器端的工作其实是非常简单的:服务器只需要从磁盘中抓出该文件然后丢给网络发送出去。
代码基本上类似这样:
read(fileDesc, buf, len);
write(socket, buf, len);
这两段代码非常简单,第一行代码从文件中读取数据存放在buf中,然后将buf中的数据通过网络发送出去。
注意观察buf,服务器全程没有对buf中的数据进行任何修改,buf里的数据在用户态逛了一圈后挥一挥衣袖没有带走半点云彩就回到了内核态。
这两行看似简单的代码实际上在底层发生了什么呢?
答案是这样的:
在程序看来简单的两行代码在底层是比较复杂的,看到这张图你应该真心感激操作系统,操作系统就像一个无比称职的管家,替你把所有脏活累活都承担下来,好让你悠闲的在用户态指点江山。
这简单的两行代码涉及:四次数据拷贝以及四次上下文切换:
- read函数会涉及一次用户态到内核态的切换,操作系统会向磁盘发起一次IO请求,当数据准备好后通过DMA技术把数据拷贝到内核的buffer中,注意本次数据拷贝无需CPU参与。
- 此后操作系统开始把这块数据从内核拷贝到用户态的buffer中,此时read()函数返回,并从内核态切换回用户态,到这时read(fileDesc,buf, len);这行代码就返回了,buf中装好了新鲜出炉的数据。
- 接下来send函数再次导致用户态与内核态的切换,此时数据需要从用户态buf拷贝到网络协议子系统的buf中,具体点该buf属于在代码中使用的这个socket。
- 此后send函数返回,再次由内核态返回到用户态;此时在程序员看来数据已经成功发出去了,但实际上数据可能依然停留在内核中,此后第四次数据copy开始,利用DMA技术把数据从socket
buf拷贝给网卡,然后真正的发送出去。
这就是看似简单的这两行代码在底层的完整过程。你觉得这个过程有什么问题吗?
发现问题
有的同学肯定已经注意到了,既然在用户态没有对数据进行任何修改,那为什么要这么麻烦的让数据在用户态来个一日游呢?直接在内核态从磁盘给到网卡不就可以了吗?
恭喜你,答对了!
这种优化思路就是所谓的零拷贝技术,Zero Copy。
总体上来看,优化数据拷贝会有以下三个方向:
- 用户态不需要真正的去访问数据,就像上面这个示例,用户态根本不需要知道buf里面装的是什么。在这种情况下无需把数据从内核态拷贝到用户态然后再把数据从用户态拷贝回内核态。数据无需用户态感知,数据拷贝完全发生在内核态。
- 内核态不要真正的去访问数据,用户态程序可以绕过内核直接和硬件交互,这样就避免了内核的参与,从而减少数据拷贝的可能。内核无需感知数据。
- 如果内核态和用户态不得不进行数据交互,则优化用户态与内核态数据的交互方式。知道了解决问题的思路,我们来看下为了实现零拷贝,计算机系统中都有哪些巧妙的设计。
mmap
是的,就是mmap。对于本文提到的网络服务器我们可以这样修改代码:
buf = mmap(file, len);
write(socket, buf, len);
你可能会想仅仅将read替换为mmap会有什么优化吗?
如果你真的理解了mmap就会知道,mmap仅仅将文件内容映射到了进程地址空间中,并没有真正的拷贝到进程地址空间,这节省了一次从内核态到用户态的数据拷贝。
同样的,当调用write时数据直接从内核buf拷贝给了socket buf,而不是像read/write方法中把用户态数据拷贝给socket buf。
我们可以看到,利用mmap我们节省了一次数据拷贝,上下文切换依然是四次。
尽管mmap可以节省数据拷贝,但维护文件与地址空间的映射关系也是有代价的,除非CPU拷贝数据的时间超过维系映射关系的代价,否则基于mmap的程序性能可能不及传统的read/write。
此外,如果映射的文件被其它进程截断,在Linux系统下你的进程将立即接收到SIGBUS信号,因此这种异常情况也需要正确处理。
除了mmap之外,还有其它办法也可以实现零拷贝。
sendfile你没有看错,在Linux系统下为了解决数据拷贝问题专门设计了这一系统调用:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
Windows下也有一个作用类似的API:TransmitFile。
这一系统调用的目的是在两个文件描述之间拷贝数据,但值得注意的是,数据拷贝的过程完全是在内核态完成,因此在网络服务器的这个例子中我们将把那两行代码简化为一行,也就是调用这里的sendfile。
使用sendfile将节省两次数据拷贝,因为数据无需传输到用户态:
调用sendfile后,首先DMA机制会把数据从磁盘拷贝到内核buf中,接下来把数据从内核buf拷贝到相应的socket buf中,最后利用DMA机制将数据从socket buf拷贝到网卡中。
我们可以看到,同使用传统的read/write相比少了一次数据拷贝,而且内核态和用户态的切换只有两次。
有的同学可能已经看出了,这好像不是零拷贝吧,在内核中这不是还有一次从内核态buf到socket buf的数据拷贝吗?这次拷贝看上去也是没有必要的。
的确如此,为解决这一问题,单纯的软件机制已经不够用了,我们需要硬件来帮一点忙,这就是DMA Gather Copy。
sendfile 与DMA Gather Copy
传统的DMA机制必须从一段连续的空间中传输数据,就像这样:
很显然,你需要在源头上把所有需要的数据都拷贝到一段连续的空间中:
现在肯定有同学会问,为什么不直接让DMA可以从多个源头收集数据呢?
这就是所谓的DMA Gather Copy。
有了这一特性,无需再将内核文件buf中的数据拷贝到socket buf,而是网卡利用DMA Gather Copy机制将消息头以及需要传输的数据等直接组装在一起发送出去。
在这一机制的加持下,CPU甚至完全不需要接触到需要传输的数据,而且程序利用sendfile编写的代码也无需任何改动,这进一步提升了程序性能。
当前流行的消息中间件kafka就基于sendfile来高效传输文件。
其实你应该已经看出来了,高效IO的秘诀其实很简单:尽量少让CPU参与进来。
实际上sendfile的使用场景是比较受限的,大前提是用户态无需看到操作的数据,并且只能从文件描述符往socket中传输数据,而且DMA Gather Copy也需要硬件支持,那么有没有一种不依赖硬件特性同时又能在任意两个文件描述符之间以零拷贝方式高效传递数据的方法呢?答案是肯定的!这就要说到Linux下的另一个系统调用了:splice。
Splice
这里还要再次强调一下不管是sendfile还是这里的splice系统调用,使用的大前提都是无需在用户态看到要传递的数据。
让我们再来看一下传统的read/write方法。在这一方法下必须将数据从内核态拷贝的用户态,然后在从用户态拷贝回内核态,既然用户态无需对该数据有任何操作,那么为什么不让数据传输直接在内核态中进行呢?
现在目标有了,实现方法呢?
答案是借助Linux世界中用于进程间通信的管道,pipe。
还是以网络服务器为例,DMA把数据从磁盘拷贝到文件buf,然后将数据写入管道,当在再次调用splice后将数据从管道读入socket buf中,然后通过DMA发送出去,值得注意的是向管道写数据以及从管道读数据并没有真正的拷贝数据,而仅仅传递的是该数据相关的必要信息。
你会看到,splice和sendfile是很像的,实际上后来sendfile系统调用经过改造后就是基于splice实现的,既然有splice那么为什么还要保留sendfile呢?答案很简单,如果直接去掉sendfile,那么之前依赖该系统调用的所有程序将无法正常运行。
原文链接:https://mp.weixin.qq.com/s/Xty9zxiGUhrkccrLl3myVg
相关文章:
彻底理解零拷贝技术,zero-copy
计算机处理的任务大体可以分为两类:CPU密集型与IO密集型。当前流行的互联网应用更多的属于IO密集型,传统的IO标准接口都是基于数据拷贝的,这篇文章我们主要关注该怎样从数据拷贝的角度来优化IO性能,让你的程序在IO性能方面赶超P8。…...
使用 MySQL 从 JSON 字符串提取数据
使用 MySQL 从 JSON 字符串提取数据 在现代数据库管理中,JSON 格式因其灵活性而广泛使用。然而,当数据存储在 JSON 中时,我们经常需要将其转换为更易于处理的格式。本篇文章将通过一个具体的 SQL 查询示例,展示如何从存储在 MySQ…...
矛盾(WEB)
##解题思路 打开靶场就是一段自相矛盾的代码,既要num是数字类型,又要判断为1 这种情况我们会想到弱类型的编程语言,插件查看过后,php就是弱类型的语言,此处并非是严格相等,只是 因此可以根据弱类型编程语言…...
优先队列(典型算法思想)—— OJ例题算法解析思路
目录 一、1046. 最后一块石头的重量 - 力扣(LeetCode) 算法代码: 代码思路 使用优先队列(大根堆) 将所有石头放入堆中 模拟碰撞过程 返回最后的重量 代码解析 时间复杂度 示例 输入 输出 二、703. 数据流…...
IGBT的损耗性分析
铝合金外壳设计器地址:嘉立创铝合金外壳/壳体/盒型-让您的产品更出彩 IGBT和MOS的损耗谁大: 电子元器件常见失效模式: 电子元器件的失效模式多种多样,以下是一些常见的失效模式及其原因: 开路: 原因:内部连接断开、焊点断裂、导线断裂等。 影响:电流无法通过,电路中断。…...
TypeScript跟js,es6这些的区别
TypeScript 一、TypeScript 是什么 想象 JavaScript 是一个自由奔放的艺术家,它在创作(编写代码)时不受太多约束,非常灵活,但有时也容易犯错且难以调试。而 TypeScript 就像是给这位艺术家配备了一套精确的工具和规范…...
单例模式代码示例
饿汉式:在类加载时就创建单例实例,线程安全。代码如下: public class Singleton {// 私有静态实例private static final Singleton instance new Singleton();// 私有构造函数private Singleton() {}// 公共访问方法public static Singleto…...
掌握 ElasticSearch的 _source 过滤
掌握 ElasticSearch的 _source 过滤 1. 引言2. _source 元数据基础2.1 什么是 _source 字段?2.2 _source 的基本用法 3. 禁用 _source3.1 如何禁用 _source 字段3.2 禁用 _source 的利弊3.3 最佳实践建议 4. _source 数据源过滤4.1 为什么需要数据源过滤?…...
车载音频配置(二)
目录 OEM 自定义的车载音频上下文 动态音频区配置 向前兼容性 Android 14 车载音频配置 在 Android 14 中,AAOS 引入了 OEM 插件服务,使你可以更主动地管理由车载音频服务监督的音频行为。 随着新的插件服务的引入,车载音频配置文件中添加了以下更改: • OEM 自定义的车…...
开目3DCAPP系列:三维制造成本分析与估算软件3DDFC
开目3DDFC 是一款基于 MBD 模型和工艺知识库的专业三维制造成本分析与估算软件,在不依赖详细工艺路线的情况下,根据零件几何信息和精度信息一键式完成零件成本的分析与估算,为面向成本的设计优化提供参考指引,也为企业对外产品报价…...
化学品安全数据表(MSDS)的全面解析与实用指南
SDS(安全数据表),也称为MSDS(材料安全数据表),是用于详细说明化学品的理化特性(如pH值、闪点、易燃性、反应活性等)及其对使用者健康(如致癌、致畸等)潜在危害…...
赛前启航 | Azure 应用开发实战指南:开启创意的无限可能
在 AI 时代,如何高效构建、优化和部署你的应用?如何充分利用微软 Azure 的强大能力,让开发更敏捷,性能更卓越?2 月 21 日 14:00-16:00,微软 AI 开发者挑战赛赛前指导第二场直播,带你全方位掌握 …...
Visual Studio Code的下载安装与汉化
1.下载安装 Visual Studio Code的下载安装十分简单,在本电脑的应用商店直接下载安装----注意这是社区版-----一般社区版就足够用了---另外注意更改安装地址 2.下载插件 重启后就是中文版本了...
home assistant ddns动态域名解析插件
home assistant ddns动态域名解析插件 使用方法 在HACS中搜索 ddns安装(hacs目前还没有合并我的提交,目前不可用),或者 clone https://github.com/weiangongsi/ddns.git, 将 custom_components/ddns目录拷贝至 Home Assistant 配置目录的 custom_compon…...
金融交易算法单介绍
0.背景 股票交易时,常见的订单类型有基础订单和条件订单。 基础订单 市价单限价单碎股单等等 条件订单 止损市价单止损限价单触及市价单(止盈)触及限价单(止盈)跟踪止损市价单跟踪止损限价单等等 除了基础订单和…...
LabVIEW利用CANopen的Batch SDO写入
本示例展示了如何通过CANopen协议向设备写入Batch SDO(批量服务数据对象)。Batch SDO允许用户在一次操作中配置多个参数,适用于设备的批量配置和参数设置。此方法能够简化多个参数的写入过程,提高设备管理效率。 主要步骤…...
正式页面开发-登录注册页面
整体路由设计: 登录和注册的切换是切换组件或者是切换内容(v-if和 v-else),因为点击两个之间路径是没有变化的。也就是登录和注册共用同一个路由。登录是独立的一级路由。登录之后进到首页,有三个大模块:文章分类&…...
vLLM专题(二):安装-CPU
vLLM 是一个 Python 库,支持以下 CPU 变体。选择您的 CPU 类型以查看供应商特定的说明: Intel/AMD x86 vLLM 最初支持在 x86 CPU 平台上进行基本模型推理和服务,支持的数据类型包括 FP32、FP16 和 BF16。 注意 此设备没有预构建的 wheel 包或镜像,因此您必须从源代码构建 v…...
【CSS进阶】常见的页面自适应的方法
在前端开发中,自适应布局(Responsive Design)是一种让网页能够适应不同屏幕尺寸、设备和分辨率的技术。常见的自适应布局方法包括 流式布局、弹性布局(Flexbox)、栅格布局(Grid)、媒体查询&…...
Java编程语言:从基础到高级应用的全面探索
在当今的软件开发领域中,Java无疑是一种极为流行且强大的编程语言。自1995年由Sun Microsystems推出以来,Java凭借其跨平台性、面向对象特性和丰富的API库,迅速成为企业级应用开发的首选语言。本文将带您从Java的基础语法入手,逐步…...
计算机视觉:神经网络实战之手势识别(附代码)
第一章:计算机视觉中图像的基础认知 第二章:计算机视觉:卷积神经网络(CNN)基本概念(一) 第三章:计算机视觉:卷积神经网络(CNN)基本概念(二) 第四章:搭建一个经典的LeNet5神经网络(附代码) 第五章࿱…...
linux 面试题
1. 文件与目录操作 ls 功能:列出目录内容 常用参数: -l:长格式显示(权限、大小、时间等)-a:显示隐藏文件(以.开头的文件)-h:以易读格式显示文件大小(如KB/…...
利用websocket检测网络连接稳定性
浏览器中打开F12,控制台中输入以下内容 > 回车 > 等待结果 连接关闭 表示断网 let reconnectDelay 1000; // 初始重连间隔 let pingInterval null; let socketManuallyClosed false; // 标志是否手动关闭function createWebSocket() {if (socketManuallyCl…...
Go入门之数组与切片
var arr1 [...]int{1, 2, 3}fmt.Println(len(arr1)) 数组长度不能扩展 var arr2 [...]int{0: 100, 5: 101}fmt.Println(len(arr2)) } 指定索引初始化 可以通过for和range遍历 值类型:基本数据类型和数组都是值类型,改变副本的值不会改变本身的值 切片为引用数…...
《Nuxt.js 实战:从放弃到入门》六、打造个性化文字转图片工具
在当今短视频的时代,将文字转化为图片是一个常见且实用的需求,无论是用于社交媒体分享、设计宣传材料,还是制作个性化的视觉内容。今天,我们就来深入剖析一个使用 Vue 3 和 ElementPlus 构建的文字转图片工具的代码…...
软硬链接?
目录 1. 硬链接(Hard Link) 2. 软链接(Symbolic Link,符号链接) 总结: 1. 硬链接(Hard Link) 定义: 硬链接是直接指向文件数据块(inode)的链接。…...
轻松搭建本地大语言模型(二)Open-WebUI安装与使用
文章目录 前置条件目标一、安装 Open-WebUI使用 Docker 部署 二、使用 Open-WebUI(一)访问Open-WebUI(二)注册账号(三)模型选择(四)交互 四、常见问题(一)容器…...
windows Redis Insight 如何查看宝塔docker里的redis数据
1、ping 命令用于测试网络连通性,它只需要目标 IP 地址作为参数,不需要端口号。正确的命令如下: ping 公网地址2、使用 Telnet 测试端口连通性 telnet 公网地址 端口 telnet 47.108.67.228 6379如果连接成功,窗口会变为空白&am…...
Python高级语法之urllib
目录: 1、爬虫的介绍2、urllib的使用2.1、urllib的异常捕获2.2、urllib的实现微博cookie登陆2.3、urllib的handler处理器2.4、urllib的代理服务器2.5、urllib的代理服务池 1、爬虫的介绍 2、urllib的使用 2.1、urllib的异常捕获 2.2、urllib的实现微博cookie登陆 2…...
word$deepseep
1、进入官网地址。 DeepSeek 2、进入DeepSeek的API文档 3、点击DeepSeek开放平台左侧的“API Keys”, 再点击“创建API Key” 4、在弹出的对话框中,输入自己的API Key名称,点击创建。 sk-0385cad5e19346a0a4ac8b7f0d7be428 5、打开Word文档。 6、Word找…...
【koa】05-koa+mysql实现数据库集成:连接和增删改查
前言 前面我们已经介绍了第二阶段的第1-4点内容,本篇介绍第5点内容:数据库集成(koamysql) 也是第二阶段内容的完结。 一、学习目标 在koa项目中正常连接数据库,对数据表进行增删改查的操作。 二、操作步骤 本篇文章…...
【深度学习】分布偏移纠正
分布偏移纠正 正如我们所讨论的,在许多情况下训练和测试分布 P ( x , y ) P(\mathbf{x}, y) P(x,y)是不同的。 在一些情况下,我们很幸运,不管协变量、标签或概念如何发生偏移,模型都能正常工作。 在另一些情况下,我们…...
数据结构_前言
本次我们将进入一个新的阶段啦~ 要注意哦: 在学数据结构之前,我们要先掌握c语言中所学的指针、结构体、内存的存储这几部分,如果还没太掌握的话,那记得去复习回顾一下噢。 下面我们就一起进入数据结构的学习吧! 知识…...
spark任务运行
运行环境 在这里插入代码片 [roothadoop000 conf]# java -version java version "1.8.0_144" Java(TM) SE Runtime Environment (build 1.8.0_144-b01)[roothadoop000 conf]# echo $JAVA_HOME /home/hadoop/app/jdk1.8.0_144[roothadoop000 conf]# vi spark-env.sh …...
由because it is a JDK dynamic proxy that implements温习Spring的代理
由because it is a JDK dynamic proxy that implements温习Spring的代理 项目场景原因分析1、报错位置2、错误原因3、业务需求 解决方案1、注入CGlib代理2、取出原生对象 项目场景 昨日在启动一个SpringBoot项目时,发现启动失败,并在日志中出现了这样的…...
mac相关命令
显示和隐藏usr等隐藏文件文件 terminal输入: defaults write com.apple.Finder AppleShowAllFiles YESdefaults write com.apple.Finder AppleShowAllFiles NO让.bashrc每次启动shell自动生效 编辑vim ~/.bash_profile 文件, 加上 if [ -f ~/.bashrc ]; then. ~/.bashrc fi注…...
Banana Pi OpenWRT One 官方路由器的第一印象
OpenWRT One是OpenWRT开源社区推出的首款官方开发板,与Banana Pi社区共同设计,由Banana Pi制造和发行。路由器采用蓝色铝合金外壳,质感极佳,视觉效果远超宣传图。整体设计简洁,呈长方形,虽然不是特别时尚&a…...
基于SpringBoot的“高考志愿智能推荐系统”的设计与实现(源码+数据库+文档+PPT)
基于SpringBoot的“高考志愿智能推荐系统”的设计与实现(源码数据库文档PPT) 开发语言:Java 数据库:MySQL 技术:SpringBoot 工具:IDEA/Ecilpse、Navicat、Maven 系统展示 系统总体结构图 系统首页界面 系统注册页…...
探索Hugging Face:开源AI社区的核心工具与应用实践
引言:AI民主化的先锋 在自然语言处理(NLP)领域,Hugging Face已成为开源社区的代名词。这个成立于2016年的平台,通过提供易用的工具和丰富的预训练模型库,彻底改变了开发者使用和部署AI模型的方式。截至202…...
SVM对偶问题
1、对偶问题数学基础 对偶问题:在线性规划中,每一个线性规划问题(称为原问题)都有一个与之对应的对偶问题。从数学形式上看,如果原问题是求解一个线性目标函数的最大值(或最小值),在满足一系列线性不等式&…...
萃取的实现(三)
探测成员 基于SFINAE,判断一个给定类型T,是否含有名为x的成员。 探测类型成员 判断一个给定类型T,是否含有类型成员size_type,源码如下: #include <type_traits> #include <iostream> #include <vect…...
nodejs版本管理,使用 nvm 删除node版本,要删除 Node.js 的某个版本详细操作
要删除 Node.js 的某个版本并保持 Node Version Manager (nvm) 的管理整洁,可以按以下步骤操作: 步骤 1:查看已安装的 Node.js 版本 nvm ls这会列出你通过 nvm 安装的所有 Node.js 版本。输出类似于: -> v18.17.1v16.20…...
openCV中如何实现滤波
图像滤波用于去除噪声和图像平滑,OpenCV 提供了多种滤波器: 1.1. 均值滤波: import cv2# 读取图像 image cv2.imread("example.jpg")# 均值滤波 blurred_image cv2.blur(image, (5, 5)) # (5, 5) 是滤波核的大小 滤波核大小的…...
vscode通过ssh连接服务器实现免密登录+删除
文章目录 参考: 1、 vscode通过ssh连接服务器实现免密登录删除(吐血总结)...
智能硬件新时代,EasyRTC开启物联音视频新纪元
在万物互联的时代浪潮中,智能硬件正以前所未有的速度融入我们的生活,从智能家居的便捷控制,到智能穿戴设备的健康监测,再到工业物联网的高效管理,智能硬件的应用场景不断拓展。而在这个智能硬件蓬勃发展的背后…...
《机器学习数学基础》补充资料:求解线性方程组的克拉默法则
《机器学习数学基础》中并没有将解线性方程组作为重点,只是在第2章2.4.2节做了比较完整的概述。这是因为,如果用程序求解线性方程组,相对于高等数学教材中强调的手工求解,要简单得多了。 本文是关于线性方程组的拓展,供…...
mysql的rpm包安装
(如果之前下载过mariadb,使用yum remove mariadb卸载,因为mariadb与rpm包安装的mysql有很多相似的组件和文件,会发生冲突,而源码包安装的mysql不会,所以不用删除源码包安装myqsl,只删除mariadb就可以&#…...
TailwindCss的vue3安装使用
按照官网的安装教程,属性最后无法生效,这是自我改良版,添加了额外步骤,但是每次引入新的tailwindcss属性 需要重新跑一次项目 npm下载tailwindcss npm install -D tailwindcss npx初始化tailwind配置 npx tailwindcss init 此时根…...
foobar2000设置DSP使用教程及软件推荐
foobar2000安卓中文版:一款高品质手机音频播放器 foobar2000安卓中文版是一款备受好评的高品质手机音频播放器。 几乎支持所有的音频格式,包括 MP3、MP4、AAC、CD 音频等。不论是经典老歌还是最新的流行音乐,foobar2000都能完美播放。除此之…...
开源多商户商城源码最新版_适配微信小程序+H5+APP+PC多端
在数字化时代,电子商务已经成为各行业不可或缺的一部分,开源多商户商城源码为中小企业和个人开发者提供了快速搭建和定制电商平台的利器。分享一款最新版的开源多商户商城源码,它能够适配微信小程序、H5、APP和PC等多个端口,满足商…...