Jason Pan

容器运行共享内核原理与GPU支持深度解析

潘忠显 / 2025-12-09


前段时间,在做模型服务的开发,该场景会在 Docker 中使用 GPU 的能力。当时对构建这样的基础镜像不是很理解,后来梳理学习理解透彻。

不过在介绍 Docker 使用 GPU 之前,会先讨论容器服务在原生 Linux 环境和非原生环境(如 macOS 和 Windows)上的工作机制,以及内核共享的原理。由浅入深,让你真正理解容器运行技术。

一、Linux 宿主机上容器的运行机制

提起容器技术,很多人都对下边比较容器和虚拟机的图烂熟于心。

左图里边的 Docker 层负责管理复杂的生命周期、镜像、网络和存储高级管理工具,它将这些高级操作最终转换为对底层 Linux 内核 Namespaces 和 Cgroups 的低级系统调用,从而实现容器的隔离和运行。

图片来自官网:https://www.docker.com/resources/what-container/

comparison-between-containers-and-vms

上图其实并不完全正确——缺少了一点说明——这是在「在原生 Linux 环境中」。

容器化技术起源于 Linux,在原生 Linux 宿主机上运行时最为高效和直接,因为它不需要任何虚拟化层。我们这节就先讨论一下原生 Linux 环境下,容器是如何运行的

容器共享的是什么

Linux 容器并非完整的操作系统,它本质上是一组被隔离的宿主机进程。所有容器共享宿主机的 唯一一个 Linux 内核

容器的轻量级和高性能正是源于这种直接的内核共享模式。

内核是什么

Linux 内核是操作系统的核心,它提供了一个抽象层,将底层硬件的复杂性封装起来,并通过一套标准接口(系统调用)为用户空间(即 Linux 发行版、Shell、应用程序和库)提供所有基础服务,包括进程、内存、设备、文件系统的四大管理服务,并利用 Namespaces 和 Cgroups 实现了现代计算环境所需的隔离和资源控制。

linux-kernel-system-call-interface-and-glibc

上边说的其实非常抽象,听上去看不见摸不着。内核既不是进程,也不是文件,他具体的存在形式是:常驻于内存、运行在特权模式下的核心代码和数据结构,它通过系统调用和中断机制被动激活,以管理硬件和系统资源,并为上层的所有用户进程(包括容器进程)提供服务。

正因为它仅仅是内存中的一块代码和数据,所以它具备以下两个核心特性,从而实现了容器级的共享

我们可以使用 uname -r 来查看内核版本,下图展示了在不同容器以及宿主机的内核版本是相同的

same-kernel-version

内核不是什么

当你真正理解了什么是内核之后,就很容易理解哪些东西不属于内核:

但这三样又跟内核有密切的联系:

容器与内核的关系

如果我们在 Linux 机器上通过 Docker 启动一个 Nginx 容器,那么这个容器包括什么内容呢?

最重要的一点,容器不包含自己的 Linux 内核。它与所有运行在该 Linux 上的其他容器共享同一个内核

内核的兼容性

写Dockerfile的时候,会有 FROM Ubuntu 22.04FROM Ubuntu 24.04 来指定基础镜像。前面介绍来,这只是用户空间的工具包和皮肤的不同。但是,毕竟不同发行版使用的 Glibc 的版本也不一样,他们是如何在同一内核上运行呢?

尽管 Glibc 版本不同,但是它们调用的底层系统调用接口(Syscall Number)是保持一致的Linux 内核通过确保系统调用号的向后兼容性,来保证即使上层的 glibc 版本不同,也能稳定地请求内核服务。

向后兼容性具体点的例子:在 5.15 编译的用户程序(如 Nginx、libc 等)通常可以在更新的 6.1 内核上完美运行。

我刚入职的时候,为了在开发机上用更新版本的 GCC,从别的机器复制了一个 glibc 库过去,结果因为内核版本过低而无法使用——其实这就是向前兼容性限制 (Forward Compatibility Limitation)

上述絮叨一堆,主要为了说明:为了能尽可能稳定地兼容各种容器,宿主机内核的版本应该保持相对较新(如主流发行版最新的 LTS 版本),以确保所有容器都能找到它们需要的系统调用,并能够利用最新的隔离和性能增强功能。

通过 ldd --version 指令可以查看 glibc 的版本,下图展示了在不同容器以及宿主机的glibc版本是不同的

diff-glibc-version

容器隔离的是什么

容器在原生 Linux 上运行依赖于两个核心的内核特性:

这些具体的介绍可以看之前的文章:《揭秘容器(一):内核空间 chroot & namespace

容器启动运行流程

当在原生 Linux 上运行容器时(例如使用 Docker Engine 或 containerd):

  1. 容器运行时(Runtime,如 runc)接收到创建容器的指令。
  2. 运行时利用 Namespaces 创建一个隔离的视图环境。
  3. 运行时利用 Cgroups 为容器分配和限制资源。
  4. 容器内的进程直接在宿主机的 Linux 内核上启动并运行。

这些具体介绍可以看之前的文章:《揭秘容器(二):容器运行时

二、 非Linux环境上容器的运行机制

由于 macOS 和 Windows 宿主机运行的不是 Linux 内核,不能依赖其提供的特定隔离特性,包括 CgroupsNamespaces,也不能共享其内核。因此它们不能直接运行 Linux 容器,必须引入虚拟化层作为桥梁。

macOS 上的容器运行

我这里日常用的就是一个 MacBook,因为现在公司合规要求不让使用 Docker Desktop,所以下边的介绍都是以 Podman 为例进行的。如果你用 Docker Desktop 应该是类似的。

在 macOS 上的容器运行架构中,主要有四个层次协作:

  1. 宿主操作系统 (macOS): 位于最底层,负责提供硬件资源和运行环境,并使用 macOS Hypervisor 框架或 HyperKit 等技术来支持虚拟化。
  2. 虚拟化层 (轻量级 Linux 虚拟机 - Podman Machine): 这是一个完整的 Linux 虚拟机 (VM),它作为容器的真正宿主机,运行着 Linux 内核。它依赖 HyperKit 或 Virtualization.framework 来实现。
  3. 容器引擎 (Podman 守护进程): 运行在 Linux VM 内部,负责接收来自 macOS 宿主机的指令,并管理容器的整个生命周期。
  4. Linux 容器: 运行在 Linux VM 内部,容器本身共享该 VM 的 Linux 内核,并利用 Namespaces 和 Cgroups 等核心技术实现其视图隔离和资源限制。

这里我们可以通过 podman 的相关指令,查看一些信息。通过 podman machine list 我们可以列出启动的 Linux VM(applehv 字段也表明使用 macOS 原生的 Hypervisor 框架进行虚拟化):

podman-machine-list

通过 podman machine ssh 可以登入到 VM 内:

podman-machine-ssh

在 VM 内部,就可以找到启动容器对应的相关进程(比如我这里启动了一个redis-server,这里common是容器监控器,在另外命名空间启动了redis-server 进程,直接 ps 是看不到这个进程的,因为 ps 只展示宿主机命名空间的进程):

podman-machine-pstree

简而言之,macOS 上的容器并非直接在 macOS 内核上运行,而是运行在一个 Podman Desktop 启动的轻量级 Linux 虚拟机内部(Podman Machine),共享该 VM 的 Linux 内核。

很明显,这里因为引入了虚拟化技术,中间会带来性能开销。这对于我们日常的开发使用影响不大,但是如果像做压测或其他一些追求极致性能的任务,特别是针对 I/O 和计算密集型应用,那么直接在原生 Linux 服务器才是效率最高的选择。

Windows 上的容器运行

我手头不太用 Windows 环境,这里仅仅简单说明下 Windows 环境运行 Linux 容器的原理和特点。

在 Windows 上,容器运行的现代主流架构是基于 WSL 2 (Windows Subsystem for Linux 2)。整个架构分为以下几个主要层次:

Windows 借助 WSL 2 提供了对原生 Linux 内核的深度集成,在文件系统共享、网络等性能方面非常原生 Linux 环境,比 macOS环境里容器运行有明显提升。

wsl2

小结

本文详细介绍了容器化技术在 Linux 和非 Linux 宿主机(macOS 和 Windows)上的运行原理。读完之后,你应该深刻地理解了:什么是内核,容器共享的是什么,容器隔离的是什么,容器镜像里到底包含什么,以及非 Linux 宿主机上容器运行所依赖的虚拟化层

接下我会介绍使用GPU的情况下,NVIDIA 驱动、CUDA Toolkit等是如何在 Linux 宿主机和容器上工作的。对提高深度学习应用的构建效率、运行性能都非常重要。