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.04 或 FROM 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 内核,不能依赖其提供的特定隔离特性,包括 Cgroups和 Namespaces,也不能共享其内核。因此它们不能直接运行 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. 容器引擎的守护进程: 运行在 Linux VM 内部,负责接收来自 macOS 宿主机的指令,并管理容器的整个生命周期。Docker 是有守护进程的(Daemon-based),而 Podman 是无守护进程的(Daemonless)
  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 宿主机和容器上工作的。对提高深度学习应用的构建效率、运行性能都非常重要。