彻底搞懂容器技术的基石:namespace(下)

容器技术和虚拟化技术都能实现资源层面的隔离和限制。容器技术依赖于 linux 内核的 cgroup 和 namespace 技术来实现这一功能。

首先,我们简要概述一下这两项技术的作用:

cgroup 的主要作用是管理资源的分配和限制;namespace 的主要作用是封装抽象、限制和隔离,使命名空间内的进程看起来拥有它们自己的全局资源。这是一个系列文章,对此系列感兴趣的朋友可以查看:

彻底搞懂容器技术的基石:cgroup彻底搞懂容器技术的基石:namespace(上)

本文将继续讨论 namespace。

我们先来总览一下 namespace 的类型,上一篇文章中已经介绍了 Cgroup、IPC、Network 和 Mount 等四种类型的 namespace。我们继续讨论剩余的部分。

namespace名称 使用的标识 – Flag 控制内容

CgroupCLONE_NEWCGROUPcgroup 根目录IPCCLONE_NEWIPC系统 V IPC, POSIX 消息队列NetworkCLONE_NEWNET网络设备、协议栈、端口等MountCLONE_NEWNS挂载点PIDCLONE_NEWPID进程号TimeCLONE_NEWTIME启动和单调时钟UserCLONE_NEWUSER用户和用户组UTSCLONE_NEWUTS主机名与 NIS 域名

PID namespaces 在 Linux 系统中,每个进程都有自己的独立 PID,而 PID namespace 主要用于隔离进程号。即,在不同的 PID namespace 中可以包含相同的进程号。

每个 PID namespace 中的进程号都是从 1 开始的,在此 PID namespace 中可以通过调用 fork(2)vfork(2)clone(2) 等系统调用来创建其他拥有独立 PID 的进程。

要使用 PID namespace 需要内核支持 CONFIG_PID_NS 选项。如下:

(MoeLove) ➜ grep CONFIG_PID_NS /boot/config-$(uname -r)CONFIG_PID_NS=y

init 进程 在 Linux 系统中有一个进程比较特殊,所谓的 init 进程,也就是 PID 为 1 的进程。

前面我们已经说了每个 PID namespace 中进程号都是从 1 开始的,那么它有什么特点呢?

首先,PID namespace 中的 1 号进程是所有孤立进程的父进程。

其次,如果这个进程被终止,内核将调用 SIGKILL 发出终止此 namespace 中的所有进程的信号。这部分内容与 Kubernetes 中应用的优雅关闭/平滑升级等都有一定的联系。(对此部分感兴趣的朋友可以留言交流,如果对这些内容感兴趣的话,我可以专门写一篇展开来聊)

最后,从 Linux v3.4 内核版本开始,如果在一个 PID namespace 中发生 reboot() 的系统调用,则 PID namespace 中的 init 进程会立即退出。这算是一个比较特殊的技巧,可用于处理高负载机器上容器退出的问题。

PID namespace 的层次结构 PID namespace 支持嵌套,除了初始的 PID namespace 外,其余的 PID namespace 都拥有其父节点的 PID namespace。

也就是说 PID namespace 也是树形结构的,此结构内的所有 PID namespace 我们都可以追踪到祖先 PID namespace。当然,这个深度也不是无限的,从 Linux v3.7 内核版本开始,树的最大深度被限制成 32。

如果达到此最大深度,将会抛出 No space left on device 的错误。(我之前尝试嵌套容器的时候遇到过)

在同一个(且同级) PID namespace 中,进程间彼此可见。

但如果某个进程位于子 PID namespace 的话,那么该进程是看不到上一层(即,父 PID namespace)中的进程的。

进程间是否可见,决定了进程间能否存在一定的关联和调用关系,朋友们对这个应该比较熟悉,这里我就不赘述了。

那么,进程是否可以调度到不同层级的 PID namespace 呢?

我们先来说结论,进程在 PID namespace 中的调度只能是单向调度(从高 -> 低)。即:

进程只能从父 PID namespace 调度到子 PID namespace 中;进程不能从子 PID namespace 调度到父 PID namespace 中;

彻底搞懂容器技术的基石:namespace(下)

图 1,通过 setns(2) 调度进程说明

PID namespace 的层级关系其实是由 ioctl_ns(2) 系统调用进行发现和维护的(NS_GET_PARENT),这里先不展开。那么,上述内容中的调度是如何实现的呢?

要解答这个问题,就必须先意识到在 PID namespace 创建之初,哪些进程具备该 namespace 的权限就已经确定了。至于调度,我们可以简单地将其理解成关系映射或者符号链接。

线程必须在同一个 PID namespace 中,以便保证进程中的线程间可以彼此互传信号。这就导致了 CLONE_NEWPID 不能与 CLONE_THREAD 同时使用。但如果分布在不同 PID namespace 的多个进程互相有信号传递的需求要怎么办呢?用共享的信号队列即可解决。

此外,我们常接触到的 /proc 目录下有很多 /proc/${PID} 的目录,在其中可看到 PID namespace 中的进程情况。同时此目录也是可直接通过挂载方式进行操作的。比如:

(MoeLove) ➜ mount |grep procproc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

有没有办法知道当前最大的 PID 数呢?

这也是可以的,自从 Linux v3.3 版本的内核开始新增了一个 /proc/sys/kernel/ns_last_pid 的文件,用于记录最后一个进程的 ID。

当需要分配下一个进程 ID 的时候,内核会去搜索最大的未使用 ID 进行分配,随后会更新此文件中 PID 的信息。

Time namespaces 在聊 time namespace 之前,我们需要先聊下单调时间。首先,我们通常提到的系统时间,指的是 clock realtime,即,机器对当前时间的展示。它可以向前或者向后调整(结合 NTP 服务来理解)。而 clock monotonic 表示在某一时刻之后的时间记录,它是单向向后的绝对时间,不受系统时间的变化所影响。

使用 time namespace 需要内核支持 CONFIG_TIME_NS 选项。如:

萤石开放平台 萤石开放平台

萤石开放平台:为企业客户提供全球化、一站式硬件智能方案。

萤石开放平台 106 查看详情 萤石开放平台

(MoeLove) ➜ grep CONFIG_TIME_NS /boot/config-$(uname -r)CONFIG_TIME_NS=y

time namespace 不会虚拟化 CLOCK_REALTIME 时钟。你可能会好奇,为什么内核支持 time namespace 呢?主要是为了一些特殊的场景。

time namespace 中的所有进程共享由 time namespace 提供的以下两个参数:

CLOCK_MONOTONIC – 单调时间,一个不可设置的时钟;CLOCK_BOOTTIME(可参考 CLOCK_BOOTTIME_ALARM 内核参数) – 不可设置的时钟,包括系统暂停的时间。time namespace 目前只能使用 CLONE_NEWTIME 标识,通过调用 unshare(2) 系统调用进行创建。创建 time namespace 的进程是独立于新建的 time namespace 之外的,而该进程后续的子进程将会被放置到新建的 time namespace 之内。同一个 time namespace 中的进程们会共享 CLOCK_MONOTONIC 和 CLOCK_BOOTTIME。

当父进程创建子进程时,子进程的 time namespace 归属将在文件 /proc/[pid]/ns/time_for_children 中显示。

(MoeLove) ➜ ls -al /proc/self/ns/time_for_childrenlrwxrwxrwx. 1 tao tao 0 12月 14 02:06 /proc/self/ns/time_for_children -> 'time:[4026531834]'

文件 /proc/PID/timens_offsets 定义了初始 time namespace 的单调时钟和启动时钟,并记录了偏移量。(如果一个新的 time namespace 还没有进程入驻时,是可以进行修改的。这里暂不展开,感兴趣的朋友可讨论区留言交流讨论。)

需要注意的是:在初始的 time namespace 中,/proc/self/timens_offsets 显示的偏移量都为 0。

(MoeLove) ➜ cat /proc/self/timens_offsetsmonotonic           0         0boottime            0         0

其中第二列和第三列的含义如下:

可以为负值,单位:秒(s)是个无符号值,单位:纳秒(ns)以下的时钟接口都与此 namespace 有所关联:

clock_gettime(2)clock_nanosleep(2)nanosleep(2)timer_settime(2)timerfd_settime(2)整体而言,time namespace 在一些特殊场景下还是很有用的。

User namespaces User namespaces 顾名思义是隔离了用户 id、组 id 等。

使用 user namespaces 需要内核支持 CONFIG_USER_NS 选项。如:

➜  local_time grep CONFIG_USER_NS /boot/config-$(uname -r)CONFIG_USER_NS=y

进程的用户 id 和组 id 在一个 user namespace 内和外有可能是不同的。

比如,一个进程在 user namespace 中的用户和组可以是特权用户(root),但在该 user namespace 之外,可能只是一个普通的非特权用户。这就涉及到用户、组映射(uid_map、gid_map)等相关的内容了。

自 Linux v3.5 版本的内核开始,在 /proc/[pid]/uid_map/proc/[pid]/gid_map 文件中,我们可以查看到映射内容。

(MoeLove) ➜ cat /proc/self/uid_map          0          0 4294967295(MoeLove) ➜ cat /proc/self/gid_map          0          0 4294967295

user namespace 也支持嵌套,使用 CLONE_NEWUSER 标识,使用 unshare(2) 或者 clone(2) 等系统调用来创建,最大的嵌套层级深度也是 32。

如果是通过 fork(2) 或者 clone(2) 创建的子进程没带有 CLONE_NEWUSER 标识,也是一样的,子进程跟父进程同在一个 user namespace 中。树状的关联关系同样通过 ioctl(2) 系统调用接口维护。

一个单线程进程可以通过 setns(2) 系统调用来调整其归属的 user namespace。

此外,user namespace 还有个很重要的规则,那就是关于 Linux capability 的继承关系。关于 Linux capability 我就不展开了,这里简单记录一下:

当进程所在的 user namespace 拥有 effective capability set 中的 capability 时,该进程具有该 capability。当进程在该 user namespace 中拥有 capability 时,该进程在此 user namespace 的所有子 user namespace 中都拥有该 capability。创建该 user namespace 的用户会被内核记录为 owner,即,拥有该 user namespace 中的全部 capabilities。对于 Docker 而言,它可以原生的支持此能力,进而达到对容器环境的一种保护。

UTS namespaces UTS namespaces 隔离了主机名和 NIS 域名。

使用 UTS namespaces 需要内核支持 CONFIG_UTS_NS 选项。如:

(MoeLove) ➜ grep CONFIG_UTS_NS /boot/config-$(uname -r)CONFIG_UTS_NS=y

在同一个 UTS namespace 中,通过 sethostname(2) 和 and setdomainname(2) 系统调用进行的设置和修改是所有进程共享查看的,但是对于不同 UTS namespaces 而言,则彼此隔离不可见。

Namespaces 主要的 API 前面内容中提到了很多的系统调用,这里我们来挑几个重要的介绍一下。

clone(2) 系统调用 clone(2) 创建一个新的进程,它会根据参数中的 CLONE_NEW* 设置,逐个实现对应的配置功能。当然这个系统调用也实现了一些与 namespace 无关的功能。对低于 Linux 3.8 版本内核的系统而言,大多数情况下,需要具备 CAP_SYS_ADMIN 的 capability。

unshare(2) 系统调用 unshare(2) 将进程分配至新的 namespace,同样,它也会根据参数中的 CLONE_NEW* 设置来调整实现对应的配置功能。对低于 Linux 3.8 的系统而言,大多数情况,需要具备 CAP_SYS_ADMIN 的 capability。

setns(2) 系统调用 setns(2) 将进程移动到某一已存在的 namespace,这会导致 /proc/[pid]/ns 对应的目录中内容的变更。进程创建的子进程可以通过调用 unshare(2) 和 setns(2) 来调整所属的 namespace。这通常是需要具备 CAP_SYS_ADMIN 的 capability 的。

一些关键目录说明 /proc/[pid]/ns/ 目录 每个进程都有一个 /proc/[pid]/ns/ 子目录,目录中的内容会受到 setns(2) 系统调用的影响。只要目录中的文件被打开,对应的 namespace 就不能被销毁。系统可以通过调用 setns(2) 来变更这些文件内容。

Linux 3.7 及更早期的版本 – 文件是以硬链接方式存在的;Linux 3.8 开始 – 文件以软连接的方式存在;

(MoeLove) ➜ ls -l --time-style='+' /proc/$$/ns总用量 0lrwxrwxrwx. 1 tao tao 0  cgroup -> 'cgroup:[4026531835]'lrwxrwxrwx. 1 tao tao 0  ipc -> 'ipc:[4026531839]'lrwxrwxrwx. 1 tao tao 0  mnt -> 'mnt:[4026531840]'lrwxrwxrwx. 1 tao tao 0  net -> 'net:[4026532008]'lrwxrwxrwx. 1 tao tao 0  pid -> 'pid:[4026531836]'lrwxrwxrwx. 1 tao tao 0  pid_for_children -> 'pid:[4026531836]'lrwxrwxrwx. 1 tao tao 0  time -> 'time:[4026531834]'lrwxrwxrwx. 1 tao tao 0  time_for_children -> 'time:[4026531834]'lrwxrwxrwx. 1 tao tao 0  user -> 'user:[4026531837]'lrwxrwxrwx. 1 tao tao 0  uts -> 'uts:[4026531838]'

如果两个进程的 namespace 相同,那么它们这个目录内的内容应该是一样的。

以下是该目录下文件的详细说明:

文件名称 起始版本 描述

/proc/[pid]/ns/cgroupLinux 4.6进程的 cgroup namespace/proc/[pid]/ns/ipcLinux 3.0进程的 IPC namespace/proc/[pid]/ns/mntLinux 3.8进程的 mount namespace/proc/[pid]/ns/netLinux 3.0进程的 network namespace/proc/[pid]/ns/pidLinux 3.8进程的 PID namespace 在进程的整个生命周期里是不变的/proc/[pid]/ns/pid_for_childrenLinux 4.12进程创建子进程的 PID namespace 这个文件与 /proc/[pid]/ns/pid 不一定一致。/proc/[pid]/ns/timeLinux 5.6进程的 time namespace/proc/[pid]/ns/time_for_childrenLinux 5.6进程创建子进程的 time namespace/proc/[pid]/ns/userLinux 3.8进程的 user namespace/proc/[pid]/ns/utsLinux 3.0进程的 UTS namespace

/proc/sys/user 目录 /proc/sys/user 目录下的文件记录了各 namespace 的相关限制。当达到限制,相关调用会报错 error ENOSPC。

文件名称 限制内容说明

max_cgroup_namespaces在 user namespace 中的每个用户可以创建的最大 cgroup namespaces 数max_ipc_namespaces在 user namespace 中的每个用户可以创建的最大 ipc namespaces 数max_mnt_namespaces在 user namespace 中的每个用户可以创建的最大 mount namespaces 数max_net_namespaces在 user namespace 中的每个用户可以创建的最大 network namespaces 数max_pid_namespaces在 user namespace 中的每个用户可以创建的最大 PID namespaces 数max_time_namespacesLinux 5.7 在 user namespace 中的每个用户可以创建的最大 time namespaces 数max_user_namespaces在 user namespace 中的每个用户可以创建的最大 user namespaces 数max_uts_namespaces在 user namespace 中的每个用户可以创建的最大 uts namespaces 数

Namespace 的生命周期 正常的 namespace 的生命周期与最后一个进程的终止和离开相关。

但有一些情况,即使最后一个进程已经退出了,namespace 仍不能被销毁。这里来稍微聊下这些特殊的情况:

/proc/[pid]/ns/* 中的文件被打开或者 mount,即使最后一个进程退出,也不能被销毁;namespace 存在分层,子 namespace 仍存在,即使最后一个进程退出,也不能被销毁;一个 user namespace 拥有一些非 user namespace(比如拥有 PID namespace 等其他的 namespace 存在),即使最后一个进程退出,也不能被销毁;对于 PID namespace 而言,如果与 /proc/[pid]/ns/pid_for_children 存在关联关系时,即使最后一个进程退出,也不能被销毁;当然除此之外还有一些其他的情况,基本都是存在被占用或未被释放。

总结 通过之前的一篇,和本篇,主要为大家介绍了 Linux namespace 的发展历程,基本类型,主要 API 以及一些使用场景和用途。

namespace 对于容器技术而言,是非常核心的部分。后续本系列中还将继续为大家分享关于容器和 Kubernetes 等技术的内容,敬请期待。

以上就是彻底搞懂容器技术的基石:namespace(下)的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/478039.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Win11任务栏不显示图标 Win11任务栏图标消失解决办法
上一篇 2025年11月8日 10:12:36
Win10系统切换输入法就提示出错的解决办法
下一篇 2025年11月8日 10:12:44

相关推荐

  • Golang JSON序列化:控制敏感字段暴露的最佳实践

    本教程探讨golang中如何高效控制结构体字段在json序列化时的可见性。当需要将包含敏感信息的结构体数组转换为json响应时,通过利用`encoding/json`包提供的结构体标签,特别是`json:”-“`,可以轻松实现对特定字段的忽略,从而避免敏感数据泄露,确保api…

    2026年5月10日
    000
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • c++中的SFINAE技术是什么_c++模板编程中的SFINAE原理与应用

    SFINAE 是“替换失败不是错误”的原则,指模板实例化时若参数替换导致错误,只要存在其他合法候选,编译器不报错而是继续重载决议。它用于条件启用模板、类型检测等场景,如通过 decltype 或 enable_if 控制函数重载,实现类型特征判断。尽管 C++20 引入 Concepts 简化了部分…

    2026年5月10日
    000
  • Go语言mgo查询构建:深入理解bson.M与日期范围查询的正确实践

    本文旨在解决go语言mgo库中构建复杂查询时,特别是涉及嵌套`bson.m`和日期范围筛选的常见错误。我们将深入剖析`bson.m`的类型特性,解释为何直接索引`interface{}`会导致“invalid operation”错误,并提供一种推荐的、结构清晰的代码重构方案,以确保查询条件能够正确…

    2026年5月10日
    100
  • 理解编程指令:当结果正确,但实现方式不符要求时

    本文探讨了在编程实践中,即使程序输出了正确的结果,但若其实现方式未能严格遵循既定指令,仍可能被视为“不正确”的问题。我们将通过具体示例,对比直接求和与累加求和两种实现策略,强调理解和遵守编程规范的重要性,以确保代码的健壮性、可维护性及符合项目要求。 在软件开发过程中,我们经常会遇到这样的情况:编写的…

    2026年5月10日
    000
  • Golang goroutine与channel调试技巧

    使用go run -race检测数据竞争,结合runtime.NumGoroutine监控协程数量,通过pprof分析阻塞调用栈,利用select超时避免永久阻塞,有效排查goroutine泄漏、死锁和数据竞争问题。 Go语言的goroutine和channel是并发编程的核心,但它们也带来了调试上…

    2026年5月10日
    000
  • 使用 Jupyter Notebook 进行探索性数据分析

    Jupyter Notebook通过单元格实现代码与Markdown结合,支持数据导入(pandas)、清洗(fillna)、探索(matplotlib/seaborn可视化)、统计分析(describe/corr)和特征工程,便于记录与分享分析过程。 Jupyter Notebook 是进行探索性…

    2026年5月10日
    000
  • 《魔兽世界》将于6月11日开启国服回归技术测试

    《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试

    《%ign%ignore_a_1%re_a_1%》官方宣布,将于6月11日开启国服回归技术测试,时间为7天,并称可以在6月内正式开服,玩家们可以访问官网下载战网客户端并预下载“巫妖王之怒”客户端,技术测试详情见下图。 WordAi WordAI是一个AI驱动的内容重写平台 53 查看详情 以上就是《…

    2026年5月10日 用户投稿
    200
  • 如何在HTML中插入表单元素_HTML表单控件与输入类型使用指南

    HTML表单通过标签构建,包含action和method属性定义数据提交目标与方式,常用input类型如text、password、email等适配不同输入需求,配合label、required、placeholder提升可用性,结合textarea、select、button等控件实现完整交互,是…

    2026年5月10日
    100
  • 创建指定大小并填充特定数据的Golang文件教程

    本文将介绍如何使用Golang创建一个指定大小的文件,并用特定数据填充它。我们将使用 `os` 包提供的函数来创建和截断文件,从而实现快速生成大文件的目的。示例代码展示了如何创建一个10MB的文件,并将其填充为全零数据。掌握这些方法,可以方便地在例如日志系统或磁盘队列等场景中,预先创建测试文件或初始…

    2026年5月10日
    000
  • Python命令怎样使用profile分析脚本性能 Python命令性能分析的基础教程

    使用Python的cProfile模块分析脚本性能最直接的方式是通过命令行执行python -m cProfile your_script.py,它会输出每个函数的调用次数、总耗时、累积耗时等关键指标,帮助定位性能瓶颈;为进一步分析,可将结果保存为文件python -m cProfile -o ou…

    2026年5月10日
    000
  • 如何插入查询结果数据_SQL插入Select查询结果方法

    如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法

    使用INSERT INTO…SELECT语句可高效插入数据,通过NOT EXISTS、LEFT JOIN、MERGE语句或唯一约束避免重复;表结构不一致时可通过别名、类型转换、默认值或计算字段处理;结合存储过程可提升可维护性,支持参数化与动态SQL。 将查询结果数据插入到另一个表中,可以…

    2026年5月10日 用户投稿
    000
  • 使用 WebCodecs VideoDecoder 实现精确逐帧回退

    本文档旨在解决在使用 WebCodecs VideoDecoder 进行视频解码时,实现精确逐帧回退的问题。通过比较帧的时间戳与目标帧的时间戳,可以避免渲染中间帧,从而提高用户体验。本文将提供详细的解决方案和示例代码,帮助开发者实现精确的视频帧控制。 在使用 WebCodecs VideoDecod…

    2026年5月10日
    000
  • Debian Copilot的社区活跃度如何

    debian copilot是codeberg社区维护的ai助手,旨在为debian用户提供服务。尽管搜索结果中没有直接提供关于debian copilot社区支持活跃度的具体数据,但我们可以通过debian社区的整体活跃度和特点来推断其活跃性。 Debian社区的一般情况: Debian拥有详尽的…

    2026年5月10日
    000
  • Discord.py 交互按钮超时与持久化解决方案

    本教程旨在解决Discord.py中交互按钮在一段时间后出现“This Interaction Failed”错误的问题。我们将深入探讨视图(View)的超时机制,并提供通过正确设置timeout参数以及利用bot.add_view()方法实现按钮持久化的具体方案,确保您的机器人交互功能稳定可靠,即…

    2026年5月10日
    000
  • JavaScript 动态菜单点击高亮效果实现教程

    本教程详细介绍了如何使用 JavaScript 实现动态菜单的点击高亮功能。通过事件委托和状态管理,当用户点击菜单项时,被点击项会高亮显示(绿色),同时其他菜单项恢复默认样式(白色)。这种方法避免了不必要的DOM操作,提高了性能和代码可维护性,确保了无论点击方向如何,功能都能稳定运行。 动态菜单高亮…

    2026年5月10日
    200
  • c++如何实现UDP通信_c++基于UDP的网络通信示例

    UDP通信基于套接字实现,适用于实时性要求高的场景。1. 流程包括创建套接字、绑定地址(接收方)、发送(sendto)与接收(recvfrom)数据、关闭套接字;2. 服务端监听指定端口,接收客户端消息并回传;3. 客户端发送消息至服务端并接收响应;4. 跨平台需处理Winsock初始化与库链接,编…

    2026年5月10日
    100
  • 谷歌浏览器如何截图 谷歌浏览器页面截图技巧

    谷歌浏览器如何截图 谷歌浏览器页面截图技巧谷歌浏览器如何截图 谷歌浏览器页面截图技巧谷歌浏览器如何截图 谷歌浏览器页面截图技巧谷歌浏览器如何截图 谷歌浏览器页面截图技巧

    使用谷歌浏览器的开发者工具截图步骤:1. 按ctrl+shift+i(windows/linux)或cmd+option+i(mac)打开开发者工具。2. 点击右上角三个点,选择”更多工具”,再选择”截图”。3. 选择截取整个页面。推荐的谷歌浏览器扩展…

    2026年5月10日 用户投稿
    100
  • JavaScript函数中插入加载动画(Spinner)的正确方法

    本文旨在解决在JavaScript函数中插入加载动画(Spinner)时遇到的异步问题。通过引入async/await和Promise.all,确保在数据处理完成前后正确显示和隐藏加载动画,提升用户体验。我们将提供两种实现方案,并详细解释其原理和优势。 在Web开发中,当执行耗时操作时,显示加载动画…

    2026年5月10日
    100
  • JS如何实现迭代器?迭代器协议

    JavaScript中实现迭代器需遵循可迭代协议和迭代器协议,通过定义[Symbol.iterator]方法返回具备next()方法的迭代器对象,从而支持for…of和展开运算符;该机制统一了数据结构的遍历接口,实现惰性求值,适用于自定义对象、树、图及无限序列等复杂场景,提升代码通用性与…

    2026年5月10日
    100

发表回复

登录后才能评论
关注微信