当前位置:   article > 正文

Linux操作系统运维-Docker的基础知识梳理总结

Linux操作系统运维-Docker的基础知识梳理总结

Linux操作系统运维-Docker的基础知识梳理总结

  • docker用来解决不同开发人员软件调试时环境不统一的问题,保证了程序调试时运行环境的一致性。
  • docker的设计理念便是一处镜像,处处运行,即通过产生用户软件,运行环境及其运行配置的统一镜像来解决不一致的开发环境部署。
  • docker是基于go语言实现的云开源项目,它是linux容器技术的进一步发展而来。

docker与传统虚拟机的差异

  • linux容器(容器虚拟化技术)

Linux容器是一种轻量级的虚拟化技术,它允许在同一主机上运行多个隔离的用户空间实例,每个实例都有自己的文件系统、进程空间和网络资源。容器是基于操作系统级虚拟化实现的,与传统的虚拟机相比,它们更加轻量级、启动更快,并且占用更少的系统资源。

Linux容器的核心技术是Linux内核的命名空间和控制组(cgroup)。命名空间提供了隔离的环境,使得每个容器都具有自己独立的进程、网络、文件系统等资源。控制组用于限制和管理容器的资源使用,例如CPU、内存、磁盘IO等。

容器可以在同一主机上并行运行,互相之间相互隔离,不会相互影响。每个容器都可以运行自己的应用程序和服务,就像在独立的操作系统中一样。容器还可以快速部署、复制和迁移,使得应用程序的部署和管理更加灵活和高效。

常见的Linux容器技术包括Docker、LXC(Linux Containers)、rkt等。它们提供了易于使用的工具和接口,简化了容器的创建、管理和部署过程。

所以docker与传统虚拟机的差异体现在:docker是在操作系统层面实现虚拟化,而传统虚拟机是在硬件层面实现虚拟化

docker快速的底层原因

  • docker拥有更少的抽象层,不需要硬件资源虚拟化,直接使用物理机的硬件资源
  • docker利用的是宿主机的内核,无需加载操作系统内核,避免了引导加载系统内核等消耗大量时间的过程

Docker帮助启动类命令

  • docker概要信息
docker info
  • 1
  • docker帮助文档
docker --hlep
  • 1
  • docker对应指令的文档
docker [command] --help
  • 1

例如:docker run --help

Docker常见镜像操作命令

  • 查看存在镜像列表
docker images
  • 1
选项描述
-a列出本地所有镜像,含历史镜像列表
-q只显示镜像ID
  • 在仓库检索镜像
docker search [镜像名]
  • 1

例如:docker search hello-world

--limit选项:用于列出指定数目的镜像(默认列出25个,即不指定数目),因为不同组织或个人提交的镜像是比较多的

例如:docker search --limit 10 redis

  • 下载镜像
docker pull [镜像名]
  • 1

例如:docker pull redis 就是默认下载官方最新版的redis

在此命令后还可以以冒号衔接一个TAG,TAG是镜像的标签或版本号,例如 docker pull [镜像名]:6将下载一个TAG为6的版本

  • 查看镜像,容器,数据卷所占空间
docker system df
  • 1
[root@localhost ~]# docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          4         1         954.8MB   954.8MB (100%)
Containers      2         1         106.9MB   15B (0%)
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 删除镜像
docker rmi [镜像名/镜像ID]
  • 1

删除镜像的两个阶段:

  • Untagged(取消标签): 如果指定的镜像标签是一个具体的标签(例如ubuntu:latest),那么这个命令会取消这个标签与镜像的关联。

  • Deleted(删除): 如果删除操作成功,Docker会删除与指定镜像ID相关联的所有层。

Untagged的操作意味着这个镜像不再与特定的标签相关联,但实际的镜像层可能仍然存在,因为可能还有其他标签或镜像依赖于这些层。Deleted的操作则彻底删除了指定的镜像层,释放相关的存储空间。如下正在执行删除Ubuntu操作的docker

[root@localhost ~]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
mysql        latest    56b21e040954   8 days ago    632MB
ubuntu       latest    e34e831650c1   2 weeks ago   77.9MB
redis        latest    bdff4838c172   2 weeks ago   138MB
[root@localhost ~]# docker rmi e34e831650c1
Untagged: ubuntu:latest
Untagged: ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74
Deleted: sha256:e34e831650c1bb0be9b6f61c6755749cb8ea2053ba91c6cda27fded9e089811f
Deleted: sha256:8e87ff28f1b5ff2d5131999ccfa1e674cb252631c50683f5ee43fad59cbea8e1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意

  • 被使用过的镜像有可能删除不了,可以使用 -f选项进行强制删除,例如:docker rmi -f hello-world

  • 也可以使用镜像id进行删除,例如:docker rmi -f xxxxxx,值得注意的是镜像的id实际上是sha256加密字符串的前几位

  • 删除多个镜像 例如:docker rmi -f hello-world ubuntu

  • 删除镜像时在镜像后也可以接TAG,进行指定删除

  • 全部删除:docker rmi -f $(docker images -qa),实际上是将全部镜像作为参数传递给删除命令

docker的虚悬镜像:仓库名与标签均为<none>的镜像被称为虚悬镜像

Docker常见容器操作命令

以镜像为模板,以容器为实例

命令模板docker run 指令 镜像名 命令 其他参数

指令描述
--name="容器新名称"为运行的容器指定一个名称
-d后台运行一个容器,并返回容器ID
-it-i与-t常一起使用,前者开启交互式运行,后者用于启动伪终端
-P进行随机端口映射
-p指定对应端口进行映射,参数为 [主机端口]:[容器端口]
-e允许你在运行容器时向其中传递环境变量。对于配置应用程序、传递配置信息以及控制应用程序的行为非常有用。

注意:在-t指令后是可以开启指定终端的,例如:docker run -it ubuntu /bin/bash

  • docker run命令的执行过程

在这里插入图片描述

图源:尚硅谷

  • 查看正在运行的容器
命令描述
docker ps查看正在运行的容器
docker ps -a查看所有容器,包括已停止的
docker ps -l显示最近创建的容器
docker ps -n显示最近创建的 n 个容器
docker ps -q静默模式,只显示容器编号

注意:在未指定的情况下,我们每run一个镜像都会产生一个对应的容器,并且在伪终端界面输入exit将会退出容器,并且将容器关闭,使用快捷键ctrl+q+p将会退出容器,但是容器仍然在后台存活

  • 容器的管理与监控
命令描述
docker start [容器id或者名称]启动已经停止的容器
docker restart [容器id或者名称]重启容器
docker stop [容器id或者名称]停止容器
docker kill [容器id或者名称]强制停止容器
docker rm [容器ID]删除已经停止的容器
docker rm -f [容器ID/容器名称]强制删除一个容器
docker rm -f $(docker ps -a -q)强制删除多个停止的容器
docker run -d [容器id或者名称]在后台运行服务
docker logs [容器id或者名称]查看对应容器的日志

需要注意的是,使用后台运行指令时,在容器的前台必须要存在一个前台进程,否则容器将会自动退出

命令描述
docker inspect [容器ID]用于检查Docker容器的详细信息
docker exec -it [容器ID] [shell程序]在容器中打开新的终端进程,使用exit命令退出不会导致容器停止
docker attach [容器ID]不会在容器中打开新的终端进程,使用exit命令退出将导致容器停止
docker cp [容器ID]:[容器内路径] [目标主机路径]将容器中的文件复制到主机磁盘
docker export [容器ID] > [文件名.tar]导出整个容器作为tar归档文件,即备份容器
`cat [文件名.tar]docker import - [镜像用户]/[镜像名]:[镜像版本号]`
[root@localhost ~]# docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS         PORTS     NAMES
f0e00969856e   ubuntu    "/bin/bash"   2 minutes ago   Up 2 minutes             focused_napier
[root@localhost ~]# docker inspect f0e00969856e
[
    {
        "Id": "f0e00969856e0a81bfb3d625d799be247734be9fae09339a11c40bba6f4232c2",
        "Created": "2024-01-29T02:57:21.589434796Z",
        "Path": "/bin/bash",
        "Args": [],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

容器启动时的行为

已知如下命令默认不会在容器中启动前台进程,所以会在容器创建完成后直接退出:

docker run ubuntu
  • 1

那么我们考虑如下行为:

docker run -it ubuntu
docker stop [容器ID]
docker start [容器ID]
docker exec -it [容器ID] [shell程序]
  • 1
  • 2
  • 3
  • 4

我们进行逐步解析:

  • docker run -it ubuntu启动容器时将会默认开启bash作为前台的主进程,保证了容器不会关闭,而是等待用户的输入

  • 当通过 docker stop [容器ID] 命令停止一个正常运行的容器时,Docker 会尝试关闭容器内的应用程序,并发送一个 SIGTERM 信号给主进程。如果容器内的应用程序在收到 SIGTERM 信号后能够正确处理并正常退出,那么容器会在一定时间内停止。如果容器内的应用程序没有正确响应 SIGTERM 信号,Docker 将发送一个 SIGKILL 信号,强制终止容器。

  • docker start [容器ID]启动容器时,docker start 不会重新运行 docker run 中指定的启动命令,而是尝试重新运行容器最初启动时的主进程。主进程即docker run -it ubuntu启动的交互式shell。如果容器内的应用程序在启动时没有遇到问题,它将继续在后台运行。即处于挂起状态

  • docker exec -it [容器ID] [shell程序]将会在容器内新建一个可交互shell进程,我们通过在容器内运行ps -aux即可发现以下内容

    root@fc68f54b4133:/# ps aux
    USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root          1  0.0  0.2   4612  2088 pts/0    Ss+  09:37   0:00 /bin/bash
    root          9  0.0  0.2   4612  2324 pts/1    Ss   09:38   0:00 /bin/bash
    root        229  0.0  0.1   7048  1444 pts/1    R+   09:47   0:00 ps aux
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们会发现有两个bash进程,根据STAT与PID可知第一个bash是会话的领导者,并且处于挂起状态,并且还是一个前台进程组成员

程序状态表:

状态描述
R进程正在运行或可运行(runnable)。这表示进程目前正在执行或等待执行。
S进程处于休眠状态。通常是因为在等待某个事件而挂起。
D不可中断的休眠状态。进程正在等待硬件操作的完成,无法被中断。
Z僵尸进程。进程已经终止,但其父进程还没有等待回收它的状态信息。
T停止状态。进程被停止或暂停执行。
  • Ss 表示进程是会话领导者,同时进程处于休眠状态。
  • S< 表示进程是会话领导者,同时进程处于非休眠状态。

Docker镜像分层与加载原理

镜像的分层概念与特性

Docker镜像是Docker容器的基础构建块,它是一个轻量级、独立、可执行的软件包,其中包含运行一个应用所需的所有内容,包括代码、运行时、库、环境变量和配置文件等。

Docker的镜像分层是指镜像是由多个只读层(layers)组成的结构。这些层是对文件系统的一系列叠加操作,每一层都包含了一组文件和配置,与前一层相比有所修改。这种分层结构的设计使得镜像更加轻量、高效、可维护,同时具备一些重要的优势:

  • 缓存和重用: 因为每一层都是只读的,且具有唯一标识,Docker可以缓存这些层,从而在构建和推送镜像时更加高效。如果多个镜像共享相同的层,这些层只需存储一次,节省了存储空间。

  • 分布式构建: 当构建一个镜像时,Docker可以将各个层分布式地下载和构建,从多个源获取。这使得构建过程更加快速,尤其是当镜像的各个层已经存在于本地或远程的镜像仓库时。

  • 可继承性: 镜像的分层结构允许基于现有镜像构建新的镜像,只需添加或修改几个层,而不必重新构建整个镜像。这提高了镜像的可继承性和可重用性。

  • 更容易管理和更新: 当一个容器运行时,Docker在其上添加一个可写层(即容器层,用于存储容器的运行时状态和文件修改。这个可写层是容器特有的,而镜像的只读层是共享的。这使得容器可以轻松地更新、删除和重新创建,而不影响镜像的原始只读层。

联合文件系统概念与镜像构建

联合文件系统(Union File System)是一种文件系统技术,它将多个文件系统层联合在一起,形成一个单一的文件系统视图。Union文件系统(UnionES)是一种分层、轻量级并目高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。它的使用使得docker具有以下特性:

  • 分层文件系统: Docker镜像是由多个只读层(layers)组成的,每一层都包含文件和配置。这些只读层采用分层结构,每一层相对于前一层表示一系列文件的添加、修改或删除。

  • 联合挂载: Docker使用联合挂载技术将这些只读层联合在一起,形成一个单一的文件系统视图。这样,当容器运行时,这个文件系统视图包含了所有必要的文件和配置,提供了容器的运行环境。

  • 可写层: 在容器运行时,Docker在只读层之上添加一个可写层,用于存储容器的运行时状态和文件修改。这个可写层是容器特有的,允许容器在运行时进行文件修改而不影响镜像的原始只读层。

  • 镜像的不可变性: 由于只读层的不可变性,Docker镜像具有不可变性的特性。一旦镜像被构建,它的只读层就不再改变,这样可以确保镜像的一致性和可重复性。

如下正在执行分层拉取MySQL镜像的docker

[root@localhost ~]# docker pull mysql
Using default tag: latest
latest: Pulling from library/mysql
558b7d69a2e5: Downloading [==========>                                        ]     11MB/51.32MB
2cb5a921059e: Download complete 
b85878fb9bb2: Download complete 
d16f3fd26a82: Downloading [======>                                            ]  593.1kB/4.591MB
afd51b5329cb: Waiting 
374d2f7f3267: Waiting 
4ea1bb2c9574: Waiting 
1c9054053605: Waiting 
d79cd2da03be: Waiting 
e3a1aa788d17: Waiting 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Docker的镜像加载原理

docker加载镜像的过程与Linux/Unix系统启动是类似的,它也有bootfs用来引导镜像,不过docker中的bootfs是Docker引擎用于引导容器的一部分

bootfs(boot file system)主要包含bootloaderkernelbootloader主要是引导加载kernel,Linux刚启动时会加载bootfs文件系统,在Docker镜像的最底层是引导文件系统bootfs。这与我们典型的Linux/Unix系统是一样的,包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfsrootfs (root file system)在bootfs之上。包含的就是典型 Linux 系统中的/dev/proc/bin/etc 等标准目录和文件。rootfs就是各科不同的操作系统发行版,比如UbuntuCentos等等。

Docker的镜像为何如此轻量

首先需要明白镜像的linux是精简的,并没有许多不必要的基础组件。对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了,因为底层直接用Hostkernel,自己只需要提供 rootfs 就行了。由此可见对于不同的linux发行版,bootfs基本是一致的,rootfs会有差别,因此不同的发行版可以公用bootfs

当我们将一个镜像进行实例化开启容器后,一个新的可写层将会加载到镜像顶部变为我们的容器,也被称为容器层。其下的镜像层均是不可写的

提交容器副本构建新镜像

docker commit -m="容器描述信息" -a="作者信息" [容器ID] 新的镜像名:标签名
  • 1
[root@localhost ~]# docker commit -m="installed the vim" -a="2333" f0e00969856e vim/ubuntu:2333333
sha256:9926edd5141d5adf6104f0ac1ea14b2881423f6fb10e816aff905e137b835e52
[root@localhost ~]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
vim/ubuntu   2333333   9926edd5141d   12 seconds ago   185MB
mysql        latest    56b21e040954   10 days ago      632MB
ubuntu       latest    e34e831650c1   2 weeks ago      77.9MB
redis        latest    bdff4838c172   2 weeks ago      138MB
[root@localhost ~]# 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

修改自定义镜像的tag

docker tag [原有镜像名]:[原有tag] [新镜像名]:[新的tag]
  • 1

正在运行的镜像也是可以进行构建新镜像的,在上述命令的过程中,我们扩展了现有的镜像,类似于Java的继承,新的镜像是从base镜像一层一层叠加生成的,每提交一个镜像就在现有的镜像上增加一层,则我们上述的内容可以得出一个大致的模型:

Docker镜像构建模型

在这里插入图片描述

Docker容器数据卷概念与使用

Docker容器数据卷是在Docker容器之间共享和持久化数据的一种方式。它们允许在容器之间共享文件、目录或者数据,并且可以在容器的生命周期内保留数据,即使容器停止或被删除(类似于虚拟机上的共享文件夹)。它具有以下特性:

  1. 容器与主机之间的数据传输: 数据卷不仅仅可以在容器之间共享,还可以在容器与主机之间传输数据。这使得容器中的数据可以持久化保存在主机上。

  2. 数据卷的生命周期: 数据卷的生命周期独立于容器。即使容器被删除,数据卷中的数据仍然存在,除非显式删除数据卷。

  3. 数据卷的备份和恢复: Docker提供了一些工具,如docker cp命令,允许将数据卷中的数据复制到主机上,从而实现备份和恢复操作。

  4. 容器之间的解耦: 使用数据卷可以实现容器之间的解耦,允许更新或替换一个容器而不影响其它容器的数据。

为什么会谈到容器停止时会保留数据?

默认情况下,Docker容器停止后,容器内的数据是不会丢失的。Docker 使用一种称为写时复制(Copy-On-Write)的技术,使得容器在运行时可以修改其文件系统,但在容器停止后,对文件系统的修改将被保存在容器的镜像中。

然而,需要注意的是,虽然容器内的数据在停止时不会丢失,但容器本身可能会被删除。如果你使用了临时性的容器(例如通过 docker run --rm 命令启动的容器),容器停止后会被自动删除,但数据卷仍然存在。临时性容器是一种短暂存在的、用于执行特定任务的 Docker 容器。它们与长期运行的常规容器不同,因为它们在任务完成后会自动删除。临时性容器通常用于执行一次性的、短暂的操作,例如调试、测试、数据备份或手动操作。--rm 标志表示容器在停止后应该被自动删除。

  • 进行容器卷挂载
docker run -it -v [宿主机的绝对路径]:[容器内的路径]:[读写模式] [镜像名]
  • 1

需要注意的是,在宿主机或容器内的路径不存在的情况下将会自动创建,并且可以使用多个v来挂载多个容器卷,读写模式默认是rw,只读模式ro,不过读写模式只限制容器,该选项默认挂载的主机路径为/var/lib/docker/volumes

使用–privileged=true来进行挂载权限不足的问题

在Docker中,--privileged=true是一个非常强大且潜在危险的选项,它赋予了容器超级用户的权限,使得容器内的进程具有对主机上所有设备的访问权限。这包括了对主机上的特权操作系统功能的完全访问,这可能会导致安全风险。

使用--privileged=true的主要目的是为了让容器内的进程能够执行一些需要特殊权限的操作,比如在容器内运行一些需要访问主机设备或进行底层系统操作的应用。然而,这样的权限可能会被滥用,因此建议仅在确切了解和信任容器中运行的应用程序的情况下使用该选项。

  • 下方命令将创建一个数据卷,它的默认路径为/var/lib/docker/volumes,通常需要我们自己指定
docker volume create [数据卷名]
  • 1
  • 第二种挂载数据卷的方式
docker run -d --name [新容器的名称] --mount source=[数据卷路径],target=[容器内路径] [新容器镜像名]
  • 1
  • 继承容器卷
docker run -d --name [新容器的名称] --volumes-from [已存在容器名称] [新容器镜像名]
  • 1

新的容器将继承选定的已存在容器上的容器数据卷

配置阿里云镜像加速服务

可以在登录阿里云后在容器服务->容器工具里找到镜像加速服务及使用文档

在这里插入图片描述

点击进入控制台,有以下界面:

在这里插入图片描述

根据操作指南,里面会有你自己的加速链接(我这里使用的是CentOS):

sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://xxxxxxxc.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

本地镜像发布到阿里云

在上述界面中找到以下内容,创建个人实例:

在这里插入图片描述

然后创建自己的命名空间,在自定义的命名空间中创建仓库后,根据页面提示的命令操作即可,和GitHub是类似的

私人镜像仓库的部署

抓取官方的镜像

docker pull registry
  • 1

创建私有仓库运行实例

docker run -d -p [主机端口]:5000 registry
  • 1

例如:

docker run -d -p 8080:5000 registry
  • 1

查看仓库中的镜像有哪些

curl -X GET http://仓库的地址/v2/_catalog
  • 1

也可以将默认镜像储存目录作为容器数据卷进行挂载,例如以下命令

docker run -d -p 8080:5000 -v ~/tmp_docker/:/var/lib/registry --name docker_test registry
  • 1

例如在宿主机的仓库映射端口为8080时

[root@localhost ~]# curl -X GET http://127.0.0.1:8080/v2/_catalog
{"repositories":[]}
  • 1
  • 2

Docker Registry 提供了一个 API 端点,可以使用 curl 或其他 HTTP 客户端工具进行访问。通过调用 /v2/_catalog 端点,可以列出所有仓库。

创建自定义的镜像

docker commit -m="容器描述信息" -a="作者信息" [容器ID] 新的镜像名:标签名
  • 1

修改镜像tag为规范格式

docker tag [原有镜像名]:[原有tag] [仓库地址/新镜像名]:[新的tag]
  • 1

以看HTTP方式推送自定义镜像

  • 配置信任HTTP推送
[root@localhost ~]# vim /etc/docker/daemon.json
[root@localhost ~]# cat /etc/docker/daemon.json 
{
        "insecure-registries":["127.0.0.1:5000"]
}
  • 1
  • 2
  • 3
  • 4
  • 5

进行推送

[root@localhost ~]# docker push 127.0.0.1:8080/vim_ubuntu:233
The push refers to repository [127.0.0.1:8080/vim_ubuntu]
14501abaf824: Pushed 
8e87ff28f1b5: Pushed 
233: digest: sha256:b1a9da2a1fa274bea7c2ad57bd6d7775156ba288d62261124f646fe95edc393f size: 741
[root@localhost ~]# curl -X GET http://127.0.0.1:8080/v2/_catalog
{"repositories":["vim_ubuntu"]}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

拉取该镜像

[root@localhost ~]# docker pull 127.0.0.1:8080/vim_ubuntu:233
233: Pulling from vim_ubuntu
df2fac849a45: Already exists 
314a19a38b7c: Pull complete 
Digest: sha256:b1a9da2a1fa274bea7c2ad57bd6d7775156ba288d62261124f646fe95edc393f
Status: Downloaded newer image for 127.0.0.1:8080/vim_ubuntu:233
127.0.0.1:8080/vim_ubuntu:233
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/80071
推荐阅读
相关标签
  

闽ICP备14008679号