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

基于 Docker 容器技术构建可移植嵌入式 Linux 交叉编译环境的实践报告

一、项目背景与技术挑战

在嵌入式 Linux 系统的开发流程中,构建一个一致且可复现的编译环境,是保障项目成功的关键前置条件。在特定的团队实践中,显现出若干具体的技术挑战:

  • 工具链版本冲突: 不同的硬件项目(例如 RK3588 与 MIPS/wifi)依赖于特定版本乃至特定供应商的交叉编译工具链。例如,RK3588 的 U-Boot 编译需要 gcc-linaro-7.4.1,而 Kernel 和 Rootfs 则依赖 gcc-arm-11.2。在单一主机上管理此种并存的 PATH 环境变量,极易引发配置错误。

  • 宿主机环境依赖: 特定的 SDK(Yocto, Buildroot)对宿主操作系统的版本及库文件(如 libssl-dev, libncurses5-dev 等)具有严格的依赖关系。此种依赖性导致新成员的环境配置周期过长,且难以在不同宿主机(如 Ubuntu 18.04, 20.04, 22.04)间保证环境的一致性。

  • 构建复现性难题: 所谓“本地构建成功,远端构建失败”的现象,是团队协作中的一个主要障碍,此种差异通常归因于前述的环境不一致性。

为实现编译流程的标准化并消除环境异构性所带来的挑战,本文决定采用 Docker 容器技术对交叉编译工具链进行封装与隔离。

二、核心架构设计:编译环境与项目源码的分离

在制定 Docker 实施方案时,存在两种主要的技术策略:

  1. “一体化”镜像: 将工具链、依赖库及项目源代码(Kernel, U-Boot, Buildroot)完整地 COPY 至单一 Docker 镜像中。

  2. “工具箱”镜像: 该镜像包含编译所需的“工具”(操作系统、依赖库、交叉编译工具链),而“原材料”(项目源代码)则保留于主机。

方案一虽在初步评估中被纳入考量,但深入分析后暴露出其固有的严重缺陷:

  • 版本控制机制的失效: 源代码被打包为静态快照,致使开发者无法利用 git pull/push 等标准工具与团队进行代码同步。

  • 开发效率的降低: 开发者必须通过 docker exec 指令进入容器内部,在终端环境中使用 vim 等文本编辑器修改代码,无法利用主机端的集成开发环境(IDE,如 VSCode)。

  • 镜像的过度冗余: 任何微小的代码变更都将迫使维护者重新构建一个体积庞大(通常达数十 GB)的镜像,并进行重新分发。

鉴于上述缺陷,本文采用了方案二(“工具箱”策略)。此策略据信是当前嵌入式开发领域的行业标准实践。

其核心工作流程可解构如下:

  1. 镜像 (Image): 作为一个自包含的编译环境实体。其内部通过 DockerfileFROM 指令(例如 ubuntu:22.04)定义了一个纯净的根文件系统。RUN 指令负责安装所有必需的 apt 依赖包(如 build-essential)。COPY 指令则负责将所有交叉编译工具链(toolchains)从构建上下文精确复制到镜像内的指定路径(如 /opt/toolchains)。

  2. 主机 (Host): 作为开发者的工作平台(例如 Ubuntu 18.04 宿主机)。开发者在此利用 git 进行源代码的版本管理(clone, pull, commit),并使用 VSCode, Sublime Text 等图形化 IDE 执行代码的阅读、编辑和调试。所有源代码文件均作为标准文件系统的一部分,持久化存储于主机硬盘。

  3. 连接机制 (docker run -v): 此为实现环境与代码分离的核心。在执行编译时,docker run 命令的 -v $(pwd):/project 参数(即“绑定挂载”,Bind Mount)发挥了关键作用。此参数指示 Docker 守护进程将主机端的当前工作目录($(pwd),即源代码根目录)实时映射到容器内部的 /project 目录(即 Dockerfile 中定义的 WORKDIR)。此映射是双向的:容器内对 /project 的任何I/O操作(读/写/创建/删除)均被直接作用于主机上的源代码目录。

  4. 执行: 容器启动后,docker run 命令指定的 [COMMAND](例如 env-rk3588-kernel.sh make)开始执行。

    • env-rk3588-kernel.sh 脚本(位于镜像内 /usr/local/bin)首先被调用,其内部的 export PATH="..." 指令将镜像内的交叉编译工具链(如 /opt/toolchains/.../bin)添加到环境变量中。

    • 随后,该脚本通过 exec "$@" 执行 make 命令。

    • make 命令在容器的 /project 目录(即主机的源代码目录)中运行,它读取 Makefile,并调用 PATH 中已设定的交叉编译器(例如 aarch64-linux-gnu-gcc)对挂载进来的源代码(.c 文件)进行编译。

    • 编译产物(如 zImage, *.bin, *.ko)被写入 /project 目录,并因此即时出现在主机端的源代码目录中。容器在 make 执行完毕后自动销毁(--rm),不留任何痕迹。

此项设计实现了“编译环境”与“项目代码”的彻底解耦。

三、Docker 引擎的安装与配置(宿主机环境)

在实现上述架构之前,所有团队成员(客户端宿主机,例如 Ubuntu 18.04)必须首先正确安装和配置 Docker 引擎。此过程是执行 docker loaddocker run 的前提。

1. 添加 Docker 官方 APT 仓库

Docker Community Edition (docker-ce) 未包含在 Ubuntu 的默认软件源中。必须手动添加 Docker 官方仓库。

# 1. 安装基础依赖包
sudo apt-get update
sudo apt-get install -y \apt-transport-https \ca-certificates \curl \gnupg \lsb-release# 2. 添加 Docker 官方 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL [https://download.docker.com/linux/ubuntu/gpg](https://download.docker.com/linux/ubuntu/gpg) | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg# 3. 设置稳定版仓库
echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] [https://download.docker.com/linux/ubuntu](https://download.docker.com/linux/ubuntu) \$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null# 4. 刷新 APT 索引
sudo apt-get update

2. 安装 Docker 引擎

sudo apt-get install -y docker-ce docker-ce-cli containerd.io

3. 安装过程中的常见故障排查

在执行安装时,遇到了若干与网络环境和 GPG 密钥相关的典型错误:

  • E: 无法定位软件包 docker-ce

    • 归因: 在执行 apt-get install 之前,未能成功执行 apt-get update,或者 apt 源列表(第 1 步中的 docker.list)配置不正确。

    • 解决: 严格执行第 1 步和第 2 步,确保 sudo apt-get update 成功刷新了 Docker 仓库索引。

  • NO_PUBKEY 7EA0A9C3F273FCD8 GPG 错误

    • 归因: 在执行第 1 步的 curl ... | sudo gpg ... 命令时,因网络瞬断(SSL_ERROR_SYSCALL)导致 curl 失败,gpg 未能接收到有效数据。这导致 /etc/apt/keyrings/docker.gpg 密钥文件损坏或为空。

    • 解决:

      1. sudo rm /etc/apt/keyrings/docker.gpg 删除无效密钥文件。

      2. 重新运行 curl -fsSL ... | sudo gpg ... 命令,确保其无错误执行。

      3. 再次运行 sudo apt-get update,GPG 错误应消失。

  • Could not handshake / SSL_ERROR_SYSCALL (在 install 阶段)

    • 归因: 宿主机访问 download.docker.com 官方服务器(位于境外)的网络连接不稳定,导致下载软件包(.deb 文件)时 SSL 握手失败。

    • 解决(推荐): 更换为高可用的国内 apt 镜像源。

      1. 编辑 /etc/apt/sources.list.d/docker.list 文件。

      2. https://download.docker.com 替换为 https://mirrors.aliyun.com/docker-ce (阿里云镜像源)。

      3. 重新运行 sudo apt-get updatesudo apt-get install ...

4. (关键)配置非 Root 用户权限

默认情况下,docker 命令需要 sudo 权限。为方便日常使用,需将当前用户添加到 docker 组。

sudo usermod -aG docker $USER

注意: 此命令执行后,必须完全退出登录重新登录(或重启虚拟机),以使用户组权限变更生效。

四、第一部分:Dockerfile 的实现细节

依据上述架构设计,Dockerfile 的编写工作得以展开,其核心职责被严格限定于安装“工具”。

# 1. 基础镜像:选择一个稳定且兼容的基线
FROM ubuntu:22.04# 2. 设置为非交互式,防止 apt 在构建时卡住
ENV DEBIAN_FRONTEND=noninteractive# 3. 安装所有编译依赖包
# (注意:每一行末尾的 \ 换行符至关重要!)
RUN apt-get update && apt-get install -y \build-essential \git \make \python3 \libssl-dev \libncurses5-dev \cpio \unzip \rsync \bc \wget \python3-dev \python3-pip \libxslt1-dev \zlib1g-dev \libglib2.0-dev \libsm6 \libgl1-mesa-glx \libprotobuf-dev \gcc \g++ \g++-aarch64-linux-gnu \gnupg \flex \bison \gperf \zip \curl \ccache \libgl1-mesa-dev \libxml2-utils \libncurses-dev \file \texinfo \gawk \libtool \libtool-bin \pkg-config \lzop \sudo \xz-utils \device-tree-compiler \&& rm -rf /var/lib/apt/lists/*# 4. (核心) 只复制“工具”,不复制“源码”
# 将 RK3588 的两个工具链复制到镜像的 /opt/toolchains/ 目录
COPY ./workspace/rk3588S/orangepi-build/toolchains /opt/toolchains/
# 将 MIPS/wifi 的工具链(staging_dir)复制到镜像的 /opt/staging_dir_mips/
COPY ./workspace/wifi/build/buildroot-2009.08/build_mips/staging_dir /opt/staging_dir_mips/# 5. (核心) 创建三个独立的环境"激活"脚本
# (注意:RUN 命令同样用 \ 换行)# 脚本 1: 用于编译 内核(Kernel) 和 Rootfs (gcc-11.2)
RUN echo '#!/bin/bash' > /usr/local/bin/env-rk3588-kernel && \echo 'echo "--- Activating RK3588 Kernel/Rootfs Environment (gcc-11.2) ---"' >> /usr/local/bin/env-rk3588-kernel && \echo 'export PATH="/opt/toolchains/gcc-arm-11.2-2022.02-x86_copy_64-aarch64-none-linux-gnu/bin:${PATH}"' >> /usr/local/bin/env-rk3588-kernel && \echo 'exec "$@"' >> /usr/local/bin/env-rk3588-kernel# 脚本 2: 用于编译 U-Boot (gcc-linaro-7.4.1)
RUN echo '#!/bin/bash' > /usr/local/bin/env-rk3588-uboot && \echo 'echo "--- Activating RK3588 U-Boot Environment (gcc-linaro-7.4.1) ---"' >> /usr/local/bin/env-rk3588-uboot && \echo 'export PATH="/opt/toolchains/gcc-linaro-7.4.1-2019.02-x86_64_aarch64-linux-gnu/bin:${PATH}"' >> /usr/local/bin/env-rk3588-uboot && \echo 'exec "$@"' >> /usr/local/bin/env-rk3588-uboot# 脚本 3: 用于编译 MIPS/wifi
RUN echo '#!/bin/bash' > /usr/local/bin/env-mips && \echo 'echo "--- Activating MIPS/wifi Environment ---"' >> /usr/local/bin/env-mips && \echo 'export PATH="/opt/staging_dir_mips/usr/bin:${PATH}"' >> /usr/local/bin/env-mips && \echo 'exec "$@"' >> /usr/local/bin/env-mips# 6. 给这三个脚本添加可执行权限
RUN chmod +x /usr/local/bin/env-rk3588-kernel && \chmod +x /usr/local/bin/env-rk3588-uboot && \chmod +x /usr/local/bin/env-mips# 7. 设置默认工作目录(主机源码的挂载点)
WORKDIR /project

设计实现详述:

  • 第 3 步 RUN apt-get ...:此指令负责安装项目编译时所需的主机库。\ 换行符系 Dockerfile 的标准语法,用于将冗长的单行命令分解为可读性更佳的多行结构。

  • 第 4 步 COPY ...:此为本方案的核心实现。该指令复制包含编译器的 toolchainsstaging_dir 目录,并将其统一部署于镜像内部的 /opt/ 路径下,以便于管理和区分。

  • 第 5 步 RUN echo ...:此为解决工具链版本冲突的关键机制。系统并未设置全局 PATH 变量,而是创建了三个独立的激活脚本。exec "$@" 是一种标准的 bash 语法,其功能在于使用后续命令(例如 make)替换当前的 shell 进程,此举可确保命令能正确接收所有传入参数,并透明地传递最终的退出状态码。

  • 第 7 步 WORKDIR /project:此指令设置容器的默认工作目录为 /project,该路径亦是 docker run -v $(pwd):/project 命令的标准挂载目标。

五、第二部分:构建、优化与分发

1. 构建优化:.dockerignore 文件的应用

docker build 命令在执行初始阶段,会将 Dockerfile 所在目录(定义为“构建上下文”)进行打包,并将其发送至 Docker 守护进程。若 workspace 目录中包含了 git 版本控制历史(.git 目录)或编译产物(*.o, build/),构建上下文将变得异常庞大。

通过在 Dockerfile 同级目录创建 .dockerignore 文件,可指示 Docker 守护进程在打包上下文时忽略指定的文件与目录。此项优化显著加速了 COPY 指令的执行效率,并减小了镜像中间层的体积。

# 忽略所有 git 历史记录
**/.git# 忽略所有编译输出目录
**/build/
**/output/
**/out/# 忽略所有编译产物
**/*.o
**/*.a
**/*.ko
**/*.bin
**/*.img

2. 构建命令的执行

Dockerfile.dockerignore 文件所在的根目录执行:

docker build -t my-sdk-toolbox:1.0 .

3. 故障排查:unknown instruction: echo

在编写 Dockerfile 过程中,遇到了此解析错误。 归因分析: RUN 指令跨越多行,但未能严格遵守 Dockerfile 语法,在除最后一行外的每一行末尾添加 \ 换行符。 解决措施: 仔细审查 RUN apt-getRUN echoRUN chmod 等多行指令块,确保 \ 语法的正确应用。

4. 镜像的打包与分发

构建完成的镜像(纵使经过优化)其体积依旧可观(约 10-20GB)。

  • 打包: docker images 命令可显示镜像元数据,但 find / 无法定位其实体文件,因其作为“层”存储于 Docker 的内部目录(通常为 /var/lib/docker/overlay2)。故需使用 docker save 命令将其打包为可分发的 TAR 归档文件:

    docker save -o my-sdk-toolbox-1.0.tar my-sdk-toolbox:1.0
  • 分发:.tar 归档文件可通过物理介质(U 盘)或网络共享进行分发。本文采用了后者:

    1. 在 Windows 主机(IP: 191.168.31.1)上,将 D:\wifi-and-rk3588-project 目录配置为网络共享(SMB/CIFS)。

    2. (关键) 在“共享”及“安全”权限选项卡中,必须确保目标用户(或 Everyone 组)被授予**“读取”(Read)或“完全控制”**(Full Control)权限。

六、第三部分:客户端(团队成员)的工作流与故障排查

团队其他成员(分别于 Ubuntu 18.04 及 20.04 宿主机环境)执行了下述标准化流程。

1. (一次性)Docker 引擎的安装与配置

此步骤的详细技术实现已在 “三、Docker 引擎的安装与配置” 章节中详述。

2. (一次性)数据分发与共享挂载

为使团队成员(客户端)能获取 .tar 归档文件,本文实践了两种共享方案:VMware 共享文件夹 (HGFS) 和 Windows 网络共享 (SMB/CIFS)。

方案 A:VMware 共享文件夹 (HGFS) 挂载排错(注:此方案后被方案 B 替代

此方案尝试使用 VMware 的原生共享文件夹功能在宿主机(Windows)与客户机(Ubuntu)间传递文件。

  • 初始状态: 在 VMware 设置中启用共享,并安装 open-vm-toolsopen-vm-tools-desktop

  • 问题 1:vmware-hgfsclient 有输出,但 /mnt/hgfs 目录为空。

    • 归因: HGFS 驱动已加载并识别到共享,但 vmhgfs-fuse 服务未能自动挂载。

  • 问题 2:手动挂载失败 mount: /mnt/hgfs: unknown filesystem type 'vmhgfs-fuse'

    • 归因: mount 命令无法识别 vmhgfs-fuse 类型。经排查,fuse3 包配置不完整(例如 fuse 用户组未创建)。

    • 解决:

      1. 执行 sudo apt-get --reinstall install fuse3 open-vm-tools-desktop 强制重新运行安装后配置脚本。

      2. 重启虚拟机。

  • 问题 3:mount -t vmhgfs-fuse 仍失败,但 vmhgfs-fuse .host:/ /mnt/hgfs 提示 permission denied

    • 归因: mount 的快捷方式(mount.vmhgfs-fuse)可能配置不当,但 vmhgfs-fuse 程序本身已可运行。普通用户 (zzq) 无权写入 root 拥有的 /mnt/hgfs 挂载点。

    • 解决: 使用 sudo 直接调用程序,并使用 -o allow_other 允许非 root 用户访问:

      sudo /usr/bin/vmhgfs-fuse .host:/ /mnt/hgfs -o allow_other
      
  • 问题 4:开机卡住,提示 [FAILED] Failed to mount /mnt/hgfs

    • 归因: 将上述命令写入 /etc/fstab 后,系统启动时 vmhgfs-fuse 驱动加载时序晚于 fstab 的解析,导致挂载失败并中断系统启动流程。

    • 解决:fstab 条目中添加 nofail 选项,允许系统在挂载失败时也能继续启动:

      .host:/   /mnt/hgfs   vmhgfs-fuse   defaults,allow_other,nofail   0   0
      
  • 结论: HGFS 方案配置复杂且存在诸多不稳定性,最终被 SMB/CIFS 方案替代。

方案 B:Windows 网络共享 (SMB/CIFS) 挂载(最终采用方案)

此方案将 Windows 主机的文件夹作为网络共享(SMB)提供给局域网内的所有 Ubuntu 客户机。

  • E: 无法定位软件包 cifs-utils

    • 归因: apt 源配置异常。经排查,发现客户端 apt 配置中包含了非原生的 arm64 架构(可能由历史交叉编译尝试残留),导致包索引混乱。

    • 解决: 执行 sudo dpkg --remove-architecture arm64 永久移除 arm64 架构;随后执行 sudo apt-get update;最后执行 sudo apt-get install cifs-utils。此软件包提供了 mount 命令执行 cifs 类型挂载所必需的 /sbin/mount.cifs 辅助程序。

  • mount error(13): Permission denied (关键排错)

    • 现象: 使用 sudo mount -t cifs ... -o guest 命令尝试匿名挂载时,返回“权限被拒绝”。

    • 归因分析:

      1. Windows Win+R 测试的误导性: 在 Windows 主机上使用 Win+R 访问 \\191.168.31.1\share 可以成功,是因为 Windows 自动使用了当前登录用户(administrator)的凭据进行认证。

      2. guest 挂载的实质: -o guest 选项是尝试以一个匿名的“来宾”账户身份从 Ubuntu 访问 Windows。

      3. 根本原因: Windows 主机的默认安全策略正确地拒绝了此匿名“来宾”账户的网络访问请求,因此返回 Permission denied

    • 解决: 挂载时必须停止使用 guest 选项,转而显式提供一个有权访问该共享的 Windows 账户凭据。

    • 凭据获取: 在 Windows 主机上运行 whoami,得到 zhengzhiqian\administrator,确认用户名为 administrator

    • 最终命令:

      sudo mount -t cifs //191.168.31.1/wifi-and-rk3588-project /mnt/docker -o user=administrator,pass=对应的Windows账户密码
      

3. (一次性)加载镜像

客户端从已挂载的网络共享路径加载镜像实体至本地 Docker 守护进程:

docker load -i /mnt/docker/my-sdk-toolbox-1.0.tar

(通过 docker images 确认加载成功)

4. (最终日常工作流)

此为团队成员的日常开发编译循环:

  1. (一次性) 克隆项目源代码(“原材料”)至本地主机目录。

    cd ~
    git clone [https://gitserver.com/rk3588-kernel.git](https://gitserver.com/rk3588-kernel.git)
    git clone [https://gitserver.com/wifi-project.git](https://gitserver.com/wifi-project.git)
    
  2. (日常) cd 至目标源代码目录,并使用主机 IDE(如 VSCode)进行代码编辑。

    cd ~/rk3588-kernel
    # ... (执行代码编辑) ...
    
  3. (日常) 调用容器化“工具箱”执行编译。

    # 编译内核
    docker run --rm -v $(pwd):/project my-sdk-toolbox:1.0 \env-rk3588-kernel.sh \make# 编译 U-Boot
    docker run --rm -v $(pwd):/project my-sdk-toolbox:1.0 \env-rk3588-uboot.sh \make
    
  • docker run:实例化一个新容器。

  • --rm:指定容器在执行完毕后自动移除,防止文件系统残留。

  • -v $(pwd):/project:核心的绑定挂载 (Bind Mount)。将主机当前目录(由 $(pwd) 解析)映射至容器的 /project 目录(即 Dockerfile 中定义的 WORKDIR)。

  • my-sdk-toolbox:1.0:指定作为执行环境的“工具箱”镜像。

  • env-rk3588-kernel.sh make:作为容器的 [COMMAND] 参数。容器启动后,首先执行 /usr/local/bin/env-rk3588-kernel.sh 脚本,该脚本设定 PATH 环境变量,随后 exec 执行 make 命令。

  • 编译产物(zImage, *.ko)被直接生成于主机的 ~/rk3588-kernel 目录中。

七、结论与总结

经由上述实践过程,本团队在嵌入式开发中所面临的核心技术难题得到了有效解决。一个可移植、可复现、且版本一致的编译环境得以实现。

技术收益总结:

  1. 环境标准化: 全体团队成员(无论其宿主机环境为 Ubuntu 18.04 或 20.04),均可利用完全相同的 Ubuntu 22.04 编译环境(包含一致的依赖库与工具链版本)。

  2. 工具与源码的解耦: 开发者得以在功能丰富的主机 IDE 中利用 Git 进行版本控制与代码开发,同时利用 Docker 容器的强隔离性执行编译任务。

  3. 可维护性的提升: 当工具链或依赖库需要升级时,维护者仅需更新 Dockerfile,构建一个新版本的镜像(例如 my-sdk-toolbox:1.1),并分发 .tar 归档文件。团队其他成员仅需执行 docker load 即可完成无缝升级。

  4. CI/CD 集成可行性: 此工作流可被无缝迁移至 Jenkins 或 GitLab CI/CD 等自动化流水线中,从而实现自动化的构建与验证。

通过本次技术实践,团队的编译流程实现了显著的简化与稳定性提升,由环境依赖性所导致的“本地构建成功”问题得到了根本性的解决。

http://www.dtcms.com/a/570697.html

相关文章:

  • 建网站如何备案廉江市住房和城乡建设局网站
  • 哈尔滨网站建设唯辛ls15227南京百度网站推广
  • 网站优化的主要任务泰兴企业网站建设
  • 基于AI大模型智能硬件--小智AI项目RK3588开发板部署
  • 万江东莞网站建设住建部和城乡建设官网
  • 专业的营销网站建设公司wordpress收费播放插件
  • 在线推广网站的方法win8风格网站 源码
  • dede 网站源码百科网wordpress
  • C++类成员变量的存储逻辑与析构函数的资源管理
  • 中国摄影在线网站seo销售话术开场白
  • 网站建设及数据分析公众号做电影网站赚钱
  • 阿里巴巴外贸网站论坛cms建站方案
  • 浙江建设职业技术学院尔雅网站易捷网站内容管理系统漏洞
  • PAI Physical AI Notebook详解1:基于Isaac仿真的操作动作数据扩增与模仿学习
  • 网站信息邯郸做网站推广
  • 网站开发区书籍上海浦东哪里有做网站的公司
  • 保定市建设局质监站网站dede网站备份
  • vue Template 1.3.1在代理时拿到的process.env.BASE_API不存在
  • 基于Vue的儿童手工创意店管理系统as8celp7(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • el-tooltip加背景图
  • 江苏营销型网站推广商城微网站建设
  • 最小二乘问题详解8:Levenberg-Marquardt方法
  • 徐州网站建设与推广河南推广网站的公司
  • asp做招聘网站流程品牌对于企业的重要性
  • Python中生成13位时间戳方法
  • Mybatis入门
  • SpringBoot之动态代理
  • java每日精进 11.04【关于线程的思考】
  • 广州 餐饮 网站建设微网站策划方案
  • 公司网站首页怎么制作网站建设邯郸