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

重新认识 Java 中的内存映射(mmap)

mmap 基础概念
mmap 是一种内存映射文件的方法,即将一个文件映射到进程的地址空间,实现文件磁盘地址和一段进程虚拟地址的映射。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read,write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
在这里插入图片描述

mmap工作原理

操作系统提供了这么一系列 mmap 的配套函数

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap( void * addr, size_t len);
int msync( void *addr, size_t len, int flags);

Java 中的 mmap
Java 中原生读写方式大概可以被分为三种:普通 IO,FileChannel(文件通道),mmap(内存映射)。区分他们也很简单,例如 FileWriter,FileReader 存在于 java.io 包中,他们属于普通 IO;FileChannel 存在于 java.nio 包中,也是 Java 最常用的文件操作类;而今天的主角 mmap,则是由 FileChannel 调用 map 方法衍生出来的一种特殊读写文件的方式,被称之为内存映射。

mmap 的使用方式:

FileChannel fileChannel = new RandomAccessFile(new File("db.data"), "rw").getChannel();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, filechannel.size();

MappedByteBuffer 便是 Java 中的 mmap 操作类。

// 写
byte[] data = new byte[4];
int position = 8;
// 从当前 mmap 指针的位置写入 4b 的数据
mappedByteBuffer.put(data);
// 指定 position 写入 4b 的数据
MappedByteBuffer subBuffer = mappedByteBuffer.slice();
subBuffer.position(position);
subBuffer.put(data);// 读
byte[] data = new byte[4];
int position = 8;
// 从当前 mmap 指针的位置读取 4b 的数据
mappedByteBuffer.get(data);
// 指定 position 读取 4b 的数据
MappedByteBuffer subBuffer = mappedByteBuffer.slice();
subBuffer.position(position);
subBuffer.get(data);

mmap 不是银弹
FileChannel 与 mmap 同时存在,大概率说明两者都有其合适的使用场景,而事实也的确如此。在看待二者时,可以将其看待成实现文件 IO 的两种工具,工具本身没有好坏,主要还是看使用场景。

mmap vs FileChannel
这一节,详细介绍一下 FileChannel 和 mmap 在进行文件 IO 的一些异同点。

pageCache
FileChannel 和 mmap 的读写都经过 pageCache,或者更准确的说法是通过 vmstat 观测到的 cache 这一部分内存,而非用户空间的内存。

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st3  0      0 4622324  40736 351384    0    0     0     0 2503  200 50  1 50  0  0

至于说 mmap 映射的这部分内存能不能称之为 pageCache,我并没有去调研过,不过在操作系统看来,他们并没有太多的区别,这部分 cache 都是内核在控制。后面本文也统一称 mmap 出来的内存为 pageCache。

缺页中断
对 Linux 文件 IO 有基础认识的读者,可能对缺页中断这个概念也不会太陌生。mmap 和 FileChannel 都以缺页中断的方式,进行文件读写。

以 mmap 读取 1G 文件为例, fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, _GB); 进行映射是一个消耗极少的操作,此时并不意味着 1G 的文件被读进了 pageCache。只有通过以下方式,才能够确保文件被读进 pageCache。

FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, _GB);
for (int i = 0; i < _GB; i += _4kb) {temp += map.get(i);
}

关于内存对齐的细节在这里就不拓展了,可以详见 java.nio.MappedByteBuffer#load 方法,load 方法也是通过按页访问的方式触发中断

如下是 pageCache 逐渐增长的过程,共计约增长了 1.034G,说明文件内容此刻已全部 load。

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st2  0      0 4824640   1056 207912    0    0     0     0 2374  195 50  0 50  0  02  1      0 4605300   2676 411892    0    0 205256     0 3481 1759 52  2 34 12  02  1      0 4432560   2676 584308    0    0 172032     0 2655  346 50  1 25 24  02  1      0 4255080   2684 761104    0    0 176400     0 2754  380 50  1 19 29  02  3      0 4086528   2688 929420    0    0 167940    40 2699  327 50  1 25 24  02  2      0 3909232   2692 1106300    0    0 176520     4 2810  377 50  1 23 26  02  2      0 3736432   2692 1278856    0    0 172172     0 2980  361 50  1 17 31  03  0      0 3722064   2840 1292776    0    0 14036     0 2757  392 50  1 29 21  02  0      0 3721784   2840 1292892    0    0   116     0 2621  283 50  1 50  0  02  0      0 3721996   2840 1292892    0    0     0     0 2478  237 50  0 50  0  0

两个细节:
mmap 映射的过程可以理解为一个懒加载, 只有 get() 时才会触发缺页中断
预读大小是有操作系统算法决定的,可以默认当作 4kb,即如果希望懒加载变成实时加载,需要按照 step=4kb 进行一次遍历
而 FileChannel 缺页中断的原理也与之相同,都需要借助 PageCache 做一层跳板,完成文件的读写。

内存拷贝次数
很多言论认为 mmap 相比 FileChannel 少一次复制,我个人觉得还是需要区分场景。

例如需求是从文件首地址读取一个 int,两者所经过的链路其实是一致的:SSD -> pageCache -> 应用内存,mmap 并不会少拷贝一次。

但如果需求是维护一个 100M 的复用 buffer,且涉及到文件 IO,mmap 直接就可以当做是 100M 的 buffer 来用,而不用在进程的内存(用户空间)中再维护一个 100M 的缓冲。

用户态与内核态
在这里插入图片描述

用户态和内核态
操作系统出于安全考虑,将一些底层的能力进行了封装,提供了系统调用(system call)给用户使用。这里就涉及到“用户态”和“内核态”的切换问题,私认为这里也是很多人概念理解模糊的重灾区,我在此梳理下个人的认知,如有错误也欢迎指正。

先看 FileChannel,下面两段代码,你认为谁更快?

// 方法一: 4kb 刷盘
FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_4kb);
for (int i = 0; i < _4kb; i++) {byteBuffer.put((byte)0);
}
for (int i = 0; i < _GB; i += _4kb) {byteBuffer.position(0);byteBuffer.limit(_4kb);fileChannel.write(byteBuffer);
}// 方法二: 单字节刷盘
FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1);
byteBuffer.put((byte)0);
for (int i = 0; i < _GB; i ++) {byteBuffer.position(0);byteBuffer.limit(1);fileChannel.write(byteBuffer);
}

使用方法一:4kb 缓冲刷盘(常规操作),在我的测试机器上只需要 1.2s 就写完了 1G。而不使用任何缓冲的方法二,几乎是直接卡死,文件增长速度非常缓慢,在等待了 5 分钟还没写完后,中断了测试。

使用写入缓冲区是一个非常经典的优化技巧,用户只需要设置 4kb 整数倍的写入缓冲区,聚合小数据的写入,就可以使得数据从 pageCache 刷盘时,尽可能是 4kb 的整数倍,避免写入放大问题。

但这不是这一节的重点,大家有没有想过,pageCache 其实本身也是一层缓冲,实际写入 1byte 并不是同步刷盘的,相当于写入了内存,pageCache 刷盘由操作系统自己决策。那为什么方法二这么慢呢?主要就在于 filechannel 的 read/write 底层相关联的系统调用,是需要切换内核态和用户态的,注意,这里跟内存拷贝没有任何关系,导致态切换的根本原因是 read/write 关联的系统调用本身。方法二比方法一多切换了 4096 倍,态的切换成为了瓶颈,导致耗时严重。

阶段总结一下重点,在 DRAM 中设置用户写入缓冲区这一行为有两个意义:

方便做 4kb 对齐,ssd 刷盘友好
减少用户态和内核态的切换次数,cpu 友好
但 mmap 不同,其底层提供的映射能力不涉及到切换内核态和用户态,注意,这里跟内存拷贝还是没有任何关系,导致态不发生切换的根本原因是 mmap 关联的系统调用本身。验证这一点,也非常容易,我们使用 mmap 实现方法二来看看速度如何:

FileChannel fileChannel = new RandomAccessFile(file, "rw").getChannel();
MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, _GB);
for (int i = 0; i < _GB; i++) {map.put((byte)0);
}

在我的测试机器上,花费了 3s,它比 FileChannel + 4kb 缓冲写要慢,但远比 FileChannel 写单字节快。

这里也解释了我之前文章《文件 IO 操作的一些最佳实践》中一个疑问:"一次写入很小量数据的场景使用 mmap 会比 fileChannel 快的多“,其背后的原理就和上述例子一样,在小数据量下,瓶颈不在于 IO,而在于用户态和内核态的切换。

mmap 细节补充
copy on write 模式
我们注意到 public abstract MappedByteBuffer map(MapMode mode,long position, long size) 的第一个参数,MapMode 其实有三个值,在网络冲浪的时候,也几乎没有找到讲解 MapMode 的文章。MapMode 有三个枚举值 READ_WRITE、READ_ONLY、PRIVATE,大多数时候使用的可能是 READ_WRITE,而 READ_ONLY 不过是限制了 WRITE 而已,很容易理解,但这个 PRIVATE 身上似乎有一层神秘的面纱。

实际上 PRIVATE 模式正是 mmap 的 copy on write 模式,当使用 MapMode.PRIVATE 去映射文件时,你会获得以下的特性:

其他任何方式对文件的修改,会直接反映在当前 mmap 映射中。
private mmap 之后自身的 put 行为,会触发复制,形成自己的副本,任何修改不会会刷到文件中,也不再感知该文件该页的改动。
俗称:copy on write。

这有什么用呢?重点就在于任何修改都不会回刷文件。其一,你可以获得一个文件副本,如果你正好有这个需求,直接可以使用 PRIVATE 模式去进行映射,其二,令人有点小激动的场景,你获得了一块真正的 PageCache,不用担心它会被操作系统刷盘造成 overhead。假设你的机器配置如下:机器内存 9G,JVM 参数设置为 6G,堆外限制为 2G,那剩下的 1G 只能被内核态使用,如果想被用户态的程序利用起来,就可以使用 mmap 的 copy on write 模式,这不会占用你的堆内内存或者堆外内存。

回收 mmap 内存
更正之前博文关于 mmap 内存回收的一个错误说法,回收 mmap 很简单

((DirectBuffer) mmap).cleaner().clean();

mmap 的生命中简单可以分为:map(映射),get/load (缺页中断),clean(回收)。一个实用的技巧是动态分配的内存映射区域,在读取过后,可以异步回收掉。

mmap 使用场景
1 使用 mmap 处理小数据的频繁读写
如果 IO 非常频繁,数据却非常小,推荐使用 mmap,以避免 FileChannel 导致的切态问题。例如索引文件的追加写。

2 mmap 缓存
当使用 FileChannel 进行文件读写时,往往需要一块写入缓存以达到聚合的目的,最常使用的是堆内/堆外内存,但他们都有一个问题,即当进程挂掉后,堆内/堆外内存会立刻丢失,这一部分没有落盘的数据也就丢了。而使用 mmap 作为缓存,会直接存储在 pageCache 中,不会导致数据丢失,尽管这只能规避进程被 kill 这种情况,无法规避掉电。

3 小文件的读写
恰恰和网传的很多言论相反,mmap 由于其不切态的特性,特别适合顺序读写,但由于 sun.nio.ch.FileChannelImpl#map(MapMode mode, long position, long size) 中 size 的限制,只能传递一个 int 值,所以,单次 map 单个文件的长度不能超过 2G,如果将 2G 作为文件大 or 小的阈值,那么小于 2G 的文件使用 mmap 来读写一般来说是有优势的。在 RocketMQ 中也利用了这一点,为了能够方便的使用 mmap,将 commitLog 的大小按照 1G 来进行切分。对的,忘记说了,RocketMQ 等消息队列一直在使用 mmap。

4 cpu 紧俏下的读写
在大多数场景下,FileChannel 和读写缓冲的组合相比 mmap 要占据优势,或者说不分伯仲,但在 cpu 紧俏下的读写,使用 mmap 进行读写往往能起到优化的效果,它的根据是 mmap 不会出现用户态和内核态的切换,导致 cpu 的不堪重负(但这样承担起动态映射与异步回收内存的开销)。

5 特殊软硬件因素
例如持久化内存 Pmem、不同代数的 SSD、不同主频的 CPU、不同核数的 CPU、不同的文件系统、文件系统的挂载方式…等等因素都会影响 mmap 和 filechannel read/write 的快慢,因为他们对应的系统调用是不同的。只有 benchmark 过后,方知快慢。

转载自:https://cloud.tencent.com/developer/article/1902272

相关文章:

重新认识 Java 中的内存映射(mmap)

mmap 基础概念 mmap 是一种内存映射文件的方法&#xff0c;即将一个文件映射到进程的地址空间&#xff0c;实现文件磁盘地址和一段进程虚拟地址的映射。实现这样的映射关系后&#xff0c;进程就可以采用指针的方式读写操作这一段内存&#xff0c;而系统会自动回写脏页到对应的文…...

224. 基本计算器

224. 基本计算器给你一个字符串表达式 s &#xff0c;请你实现一个基本计算器来计算并返回它的值。注意:不允许使用任何将字符串作为数学表达式计算的内置函数&#xff0c;比如 eval() 。 示例 1&#xff1a;输入&#xff1a;s "1 1"输出&#xff1a;2示例 2&#…...

微信小程序通过 node 连接 mysql——方法,简要原理,及一些常见问题

前言 博主自己在22年夏天根据课程要求做了一个小程序连接阿里云服务器的案例&#xff0c;在最近又碰到了相应的需求。 原参考文章&#xff1a;微信小程序 Node连接本地MYSQL_微信小程序nodejs连接数据库_JJJenny0607的博客-CSDN博客 ,还请多多支持原作者&#xff01; 第二次…...

uni-app项目搭建和代码托管

文章目录一、项目搭建步骤一、HBuilder X 创建uniapp项目步骤二、开启微信小程序服务端口步骤三、把项目运行到微信小程序步骤四、解决警告二、使用Git管理项目2-1、本地管理2-2、托管到码云一、项目搭建 步骤一、HBuilder X 创建uniapp项目 步骤二、开启微信小程序服务端口 步…...

win10+python3.6+cuda9+pytorch1.1.0安装

为了让torch可以使用显卡GPU加速&#xff0c;需要安装对应版本的cudatoolkit和pytorch。这里我的nvidia显卡驱动是9.1版本&#xff0c;只能安装cudatoolkit9。 一般支持gpu加速的显卡大部分都是英伟达nvidia系列&#xff0c;都自带了nvidia驱动&#xff0c;所以不需要安装nvidi…...

【2023】某python语言程序设计跟学第二周内容

本文说明&#xff1a; 案例内容为北理工python语言程序设计课程&#xff0c;如有不妥请联系&#xff01; 目录蟒蛇绘制案例&#xff1a;执行结果&#xff1a;代码分析&#xff1a;举一反三&#xff1a;绘制一个五角星图案执行结果&#xff1a;turtle库根据案例简单说明&#xf…...

spring源码篇——BeanDefinition的注册

spring-framework 版本&#xff1a;v5.3.19 文章目录注解方式&#xff08;AnnotationConfigApplicationContext&#xff09;AnnotationConfigApplicationContext#registerAnnotatedBeanDefinitionReader#doRegisterBeanBeanDefinitionRegistry#registerBeanDefinitionAnnotatio…...

virtualbox7虚拟机中安装苹果macOS big sur系统详细教程

第1步&#xff0c;在 Windows 10/11 PC 上启用虚拟化。 现在的电脑一般都默认开启虚拟化技术了。 如果你遇到一些报错&#xff0c;比如收到错误消息“无法在虚拟机上打开会话”&#xff0c;可以查看 如果没有遇到问题&#xff0c;可以直接进入到第二步。 第2步&#xff0c;在…...

用spectralayers 简单去一下人声做个伴奏

最近有个同事说有个工作要一个歌的伴奏不会下载问我能不能给下一个。问题是我五音不全&#xff0c;也不咋关注伴奏这方面的事儿&#xff0c;然后巧了&#xff0c;当天晚上就有个网上的大哥在群里聊天的时候说有个去人声比较给力的软件&#xff0c;我马上给要来了。 软件叫啥sp…...

高峰对话|深度探讨「多云与边缘」

2022 年 12 月&#xff0c;分析师 Zeus Kerravala 与 VMware 通信运营商和边缘事业部高级副总裁兼总经理 Sanjay Uppal 进行非常有启发性的谈话&#xff0c;分享了科技行业领导者的见解。 二位主要围绕以下主题进行探讨&#xff1a; &#x1f4cd; 如何定义多云&#xff0c;以…...

开发手册——一、编程规约_2.常量定义

这篇文章主要梳理了在java的实际开发过程中的编程规范问题。本篇文章主要借鉴于《阿里巴巴java开发手册终极版》 下面我们一起来看一下吧。 1. 【强制】不允许任何魔法值&#xff08;即未经定义的常量&#xff09;直接出现在代码中。 反例&#xff1a;String key "Id#…...

Sandstorm 建设者亮点——2023 年 2 月

隆重推出 Sandstorm 建设者亮点——2023 年 2 月版&#xff0c;这是由最厉害的 Sandstorm 社区制作的独一无二的 NFT 系列。 从突破性的兔子机器人到神奇的蒸汽朋克海盗船&#xff0c;Sandstorm 建设者亮点 NFT 系列展示了一系列独一无二的创作。 19 项新资产将添加至 Sandstor…...

MyBatis快速入门

创建表&#xff08;自行完成&#xff09;创建模块&#xff0c;引入坐标&#xff08;1&#xff09;.进入mybatis官网&#xff1a;MyBatis中文网按步骤进行添加坐标先添加mybatis依赖然后手动添加mysql驱动junit单元测试坐标&#xff1a;logback坐标&#xff1a;用的时候直接复制…...

Mysql的一些提权方式(mysql提权、UDF)

目录 bash命令提权 必要条件 实验 UDF提权 什么是UDF 必要条件 实验 手动测试...

【2023】DevOps、SRE、运维开发面试宝典之Docker相关面试题

文章目录 1、docker的工作原理是什么2、docker的组成包含哪几大部分3、讲一下镜像的分层结构以及为什么要使用镜像的分层结构?4、简单描述一下Dockerfile的整个构建镜像过程?5、Docker的四种网络类型?6、Docker跨宿主机通讯的方式1、docker的工作原理是什么 docker是一个Cl…...

圣杯布局的实现方式

1.什么是圣杯布局&#xff1f; 左右盒子固定&#xff0c;中间盒子自适应 2.实现方式 &#xff08;1&#xff09;flex布局 思路&#xff1a;左右盒子给固定的宽高&#xff0c;中间盒子flex:1 <!DOCTYPE html> <html lang"en"> <head> <met…...

RecastDemo用法

这里写自定义目录标题recastnavigation介绍recastnavigation的内容RecastDemo安装RecastDemo介绍可配置参数合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个…...

IIC总线式驱动开发(mpu6050)(二)

目录 六、I2C总线二级外设驱动开发方法 七、I2C总线二级外设驱动开发之名称匹配 1. i2c_register_board_info 2. i2c_new_device&#xff1a;明确二级外设地址的情况下可用 3. i2c_new_probed_device 八、I2C总线二级外设驱动开发之设备树匹配 六、I2C总线二级外设驱动开…...

盘点一下那些远程办公的神仙公司

其实远程办公已经有50多年的历史了&#xff0c;这几年&#xff0c;这种工作方式越来越受到大家的喜欢&#xff0c;对于员工来说&#xff0c;工作效率可以大幅提高&#xff0c;节省下来的通勤时间和成本&#xff0c;有更多的时间花在工作上。可以更好的平衡工作与生活。对于公司…...

Spring Cloud Alibaba全家桶(四)——微服务调用组件Feign

前言 本文小新为大家带来 微服务调用组件Feign 的相关知识&#xff0c;具体内容包含什么是Feign&#xff0c;Spring Cloud Alibaba快速整合OpenFeign&#xff0c;Spring Cloud Feign的自定义配置及使用&#xff08;包括&#xff1a;日志配置、契约配置、自定义拦截器实现认证逻…...

安装pytorch

一、在anaconda中创建虚拟环境 打开Anaconda Prompt创建一个虚拟环境。比如要创建一个名字为pytorch的虚拟环境&#xff0c;可以如下输入。其中python3.7指定该虚拟环境的python版本号。 conda create -n pytorch python3.7 二、进入新创建的虚拟环境。 创建好虚拟环境后&a…...

自动化测试 Appium之Python运行环境搭建 Part2

环境部署 1、安装Android SDK 安装好后&#xff0c;配置ANDROID_HOME环境变量&#xff0c;设置为Android SDK安装路径(例中&#xff1a;D:\Program Files (x86)\Android\android-sdk) 2、安装其它SDK相关软件包 安装好Android SDK后&#xff0c;选择打开Android SDK Manager…...

LeetCode 2 - 两数相加

给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这两个数都不会以 0 开…...

用Python实现九九乘法表的几种方式,最简单只需一行代码

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 我们在学习Python的过程中需要不断的积累和练习&#xff0c;这样才能够走的更远&#xff0c; 今天一起来学习怎么用Python写九九乘法表~ 更多教程源码资料电子书: 点击此处跳转文末名片获取 第一种方法、for-for 代码&…...

入职外包三个月,我提桶跑路了

有一种打工人的羡慕&#xff0c;叫做“大厂”。 真是年少不知大厂香&#xff0c;错把青春插稻秧。 但是&#xff0c;在深圳有一群比大厂员工更庞大的群体&#xff0c;他们顶着大厂的“名”&#xff0c;做着大厂的工作&#xff0c;还可以享受大厂的伙食&#xff0c;却没有大厂…...

企业邮箱对企业有哪些好处以及便捷性

企业邮箱拥有更专业的办公功能&#xff0c;更适合职场使用。同时&#xff0c;使用企业邮箱还可以帮助企业“公私分明”。一方面保护了公司信息&#xff0c;另一方面也提高了工作效率。加上公司统一邮箱也有助于提升公司形象。使用企业邮箱除了收发邮件方便外&#xff0c;还可以…...

ConvNeXt V2实战:使用ConvNeXt V2实现图像分类任务(一)

文章目录摘要安装包安装timm安装 grad-cam数据增强Cutout和MixupEMA项目结构计算mean和std生成数据集关于不上分的问题摘要 论文&#xff1a;https://arxiv.org/pdf/2301.00808.pdf 论文翻译&#xff1a;https://wanghao.blog.csdn.net/article/details/128541957 官方源码&am…...

3.2 报错整理

报错1&#xff1a; 报错&#xff1a;RuntimeError: DataLoader worker (pid 93789) is killed by signal: Killed.原因&#xff1a;显存不够报错2&#xff1a; 报错&#xff1a;TqdmWarning: IProgress not found. Please update jupyter and ipywidgets.解决&#xff1a;pip i…...

从0开始学python -46

Python CGI编程 什么是CGI CGI 目前由NCSA维护&#xff0c;NCSA定义CGI如下&#xff1a; CGI(Common Gateway Interface),通用网关接口,它是一段程序,运行在服务器上如&#xff1a;HTTP服务器&#xff0c;提供同客户端HTML页面的接口。 网页浏览 为了更好的了解CGI是如何工作…...

JavaScript事件委托机制详解

一、什么是事件委托机制 事件委托机制就是&#xff1a;我们给元素添加click事件时不在该元素上添加&#xff0c;而是委托给某个公共的祖辈元素&#xff0c;告诉祖辈元素如果接收到了click事件&#xff0c;并且这个click事件是由该元素触发的&#xff0c;就执行祖辈元素上委托绑…...