Docker 底层关键技术-namespace && cgroup

Docker 底层关键技术-namespace && cgroup

八月 23, 2025 次阅读

namespace

Linux NamespaceLinux 内核提供的一种内核级别环境隔离机制。它的主要目的是将全局的系统资源包装在一个抽象的隔离空间中,使得从每个 Namespace 内部看来,它都拥有自己独立的全局资源实例。

你可以把它想象成一种“障眼法”:它对进程“撒谎”,让一组进程“看到”一套独立的系统资源,而另一组进程“看到”另一套完全不同的资源。这些被隔离的资源包括进程 ID、主机名、用户 ID、网络接口、文件系统挂载点等。

核心目标:实现轻量级虚拟化(容器化),让一个进程或一组进程在一个隔离的运行环境中运行,而无需启动完整的虚拟机。这正是 DockerLXCKubernetes 等容器技术的基石。

dd 命令

dd 是一个命令行工具,用于在 Unix 和类 Unix 操作系统(如 Linux)上转换和复制文件。它的名字源于 IBM 的 Job Control Language (JCL) 语句 “Data Definition”,但其功能更像是“磁盘转储”(Disk Dump)或“数据复制器”。

它的核心功能是按块从输入文件(if)读取数据,进行可能的转换,然后写入输出文件(of)。由于其底层操作的特性和灵活性,它成为了系统管理员、开发者和高级用户进行各种低级磁盘操作的瑞士军刀。


命令语法

基本语法非常简单:

dd if=<输入文件> of=<输出文件> [选项]
  • if=: input file,输入文件。默认为标准输入(stdin)。
  • of=: output file,输出文件。默认为标准输出(stdout)。

核心常用选项

选项 说明 示例
bs= block size: 同时设置输入和输出的块大小。这是最重要的选项之一,极大影响性能。 bs=4M
count= 要复制的块的数量 count=1
skip= 从输入文件开头跳过的块数。 skip=1
seek= 在输出文件开头跳过的块数。 seek=1
status= 显示进度信息的级别。progress 显示周期性进度,none 不显示。 status=progress
conv= 转换参数,用逗号分隔多个。最常用的是 fsync(确保数据物理写入)和 nocreat(不创建输出文件)。 conv=fsync

status 选项的作用

经典实用示例

1. 制作启动盘(最经典的用途)

将 ISO 镜像文件写入 U 盘 (/dev/sdb)。警告:操作前务必确认目标设备,否则可能导致数据丢失!

sudo dd if=ubuntu-22.04.iso of=/dev/sdb bs=4M status=progress oflag=sync
  • if=ubuntu-22.04.iso: 输入是 ISO 镜像文件。
  • of=/dev/sdb: 输出是整个 U 盘设备(不是分区,如 /dev/sdb1)。
  • bs=4M: 使用 4MiB 的大块进行读写,提高速度。
  • status=progress: 显示复制进度和速度。
  • oflag=sync: 确保每次写入都同步到物理介质,保证数据完整性。
2. 磁盘/分区备份与还原

备份整个磁盘(例如 /dev/sda)到一个镜像文件:

sudo dd if=/dev/sda of=/path/to/backup.img bs=4M status=progress

从镜像文件还原到磁盘:

sudo dd if=/path/to/backup.img of=/dev/sda bs=4M status=progress

注意: 对于包含大量空块的磁盘(如虚拟机磁盘),使用 gzip 压缩会更高效:

# 备份并压缩
sudo dd if=/dev/sda bs=4M | gzip > /path/to/backup.img.gz

# 解压并还原
gzip -dc /path/to/backup.img.gz | sudo dd of=/dev/sda bs=4M status=progress
3. 测试磁盘读写速度

测试写入速度:
创建一个 1GB 的文件,测试写入性能。

dd if=/dev/zero of=./testfile bs=1G count=1 oflag=direct
  • if=/dev/zero: 提供无限的空字符(0x00)。
  • of=./testfile: 输出到当前目录的 testfile。
  • bs=1G count=1: 写一个 1GiB 的块。
  • oflag=direct: 使用直接 I/O,绕过系统缓存,得到更真实的磁盘速度。

测试读取速度:
读取刚才创建的文件。

dd if=./testfile of=/dev/null bs=1G count=1 iflag=direct
  • of=/dev/null: 相当于一个数据黑洞,丢弃所有写入的数据。
  • iflag=direct: 使用直接 I/O 读取。
4. 安全擦除磁盘数据

用随机数据覆盖整个磁盘,使其难以恢复。

sudo dd if=/dev/urandom of=/dev/sdX bs=4M status=progress
  • if=/dev/urandom: 提供高质量的随机数据。
  • of=/dev/sdX: 目标设备。

(对于快速擦除,用零覆盖一次通常就足够了):

sudo dd if=/dev/zero of=/dev/sdX bs=4M status=progress
5. 转换文件内容

将文件转换为大写:

dd if=input.txt of=output.txt conv=ucase

将 EBCDIC 编码的文件转换为 ASCII:

dd if=ebcdic_file.txt of=ascii_file.txt conv=ascii
6. 仅复制磁盘的 MBR(主引导记录)

MBR 通常位于磁盘的第一个扇区(512 字节)。

# 备份 MBR
sudo dd if=/dev/sda of=mbr_backup.bak bs=512 count=1

# 还原 MBR
sudo dd if=mbr_backup.bak of=/dev/sda bs=512 count=1
7. 创建特定大小的空文件

快速创建一个充满零的 100MB 文件,用于测试或占位。

dd if=/dev/zero of=test.file bs=1M count=100

注意事项和警告

  1. 极具破坏性dd 命令不会询问确认。如果你将 of=(输出目标)指定错了(比如是你的系统盘),它就会毫不犹豫地覆盖并销毁所有数据。“Double-check before you press Enter!”(按下回车前务必双重检查!)
  2. 理解设备名: 在操作 /dev/sdX 这样的块设备时,务必使用 lsblkfdisk -l 命令确认设备标识符。
  3. 性能优化bs(块大小)的设置对性能影响巨大。太小的值(如 512Bytes)会导致大量 I/O 操作,降低速度。太大的值可能会超出系统缓存。通常 1M8M 是一个很好的起点。使用 status=progress 来观察速度。
  4. 数据完整性: 在写入重要数据(如制作启动盘)后,使用 oflag=syncconv=fsync 来确保所有数据都从内存缓冲区刷新到了物理设备上。

mkfs 命令

mkfsMaKe FileSystem 的缩写。它的作用就是在块设备(如硬盘分区、U 盘、软盘)或镜像文件创建(格式化)一个文件系统

您可以把它想象成:在一块已经划分好的“土地”(分区)上,按照特定的“规划和布局”(文件系统类型,如 ext4, FAT32)修建一座“城市”的基础设施(文件系统结构)。在这之后,您才能在上面“存放人员和物资”(存储文件)。


命令格式与工作原理

mkfs 其实是一个前端工具,它本身并不直接完成所有工作,而是会去调用相应的特定文件系统创建工具。

基本语法:

mkfs [选项] [-t 文件系统类型] [设备名] [块数]
  • -t 文件系统类型: 指定要创建的文件系统,如 ext4, xfs, vfat 等。
  • 设备名: 要在哪个设备上创建文件系统,例如 /dev/sdb1
  • 块数: 很少手动指定,通常使用整个设备。

例如,当你执行:

sudo mkfs -t ext4 /dev/sdb1

mkfs 会实际上调用更专门的程序 mkfs.ext4 来完成工作。这就是为什么你也会经常看到直接调用 mkfs.<类型> 的命令格式。


常用文件系统类型

类型 命令 描述 适用场景
ext4 mkfs.ext4 Linux 最主流、最稳定的日志文件系统。 Linux 系统根分区、家用、通用服务器。
XFS mkfs.xfs 高性能日志文件系统,擅长处理大文件。 大型服务器、数据库、视频编辑。
Btrfs mkfs.btrfs 先进的功能性文件系统,支持写时复制、快照、压缩等。 需要高级功能的桌面用户或服务器。
vFAT mkfs.vfat 兼容性最好的文件系统,无权限设置。 U 盘、SD 卡,需要在 Windows/Linux/Mac 间交叉使用。
NTFS mkfs.ntfs Windows 现代文件系统,支持大文件和 ACL。 主要用于 Windows 系统分区,Linux 也可读写。
exFAT mkfs.exfat 微软为闪存设备设计的文件系统,比 FAT32 支持更大文件。 大容量 U 盘、移动硬盘,用于跨平台交换大文件。

常用选项

这些选项通常用于直接调用 mkfs.<类型> 命令时。

选项 示例 描述
-L -L MYDATA 为文件系统设置一个卷标,便于用标签挂载而非设备名。
-c -c 在创建文件系统前检查设备是否有坏块
-q -q 安静模式,减少输出信息。
-f -f 强制创建,即使参数可能有问题也会尝试。
-N -N 10000 显式指定 inode 的数量(文件数量的上限)。

重要警告与前提

`mkfs 命令会永久销毁目标设备上的所有现有数据!
格式化之前,必须

  1. 确认设备名: 使用 lsblkfdisk -l 等命令双重、三重确认你要格式化的设备是否正确。
  2. 卸载设备: 确保该设备没有被系统挂载 (umount /dev/sdb1)。对已挂载的设备进行格式化会导致严重错误。

实用示例

示例 1:将 U 盘格式化为 FAT32(最大兼容性)

假设 U 盘被识别为 /dev/sdb1请务必确认!

# 先卸载
sudo umount /dev/sdb1

# 再格式化为 FAT32,并设置卷标为“MYUSB”
sudo mkfs -t vfat -n MYUSB /dev/sdb1

# 或者直接使用 mkfs.vfat
sudo mkfs.vfat -n MYUSB /dev/sdb1
示例 2:将分区格式化为 Linux 常用的 ext4

假设要格式化的分区是 /dev/sdc1

sudo umount /dev/sdc1
sudo mkfs -t ext4 -L BackupDrive /dev/sdc1

# 更常见的直接用法
sudo mkfs.ext4 -L BackupDrive /dev/sdc1
示例 3:创建 XFS 文件系统

XFS 是很多企业级应用的首选。

sudo umount /dev/sdd1
sudo mkfs.xfs -f -L Database /dev/sdd1
# `-f` 强制创建,如果设备上已有文件系统则需要此选项
示例 4:检查坏块后再格式化

这是一个好习惯,尤其对于新旧不明的硬盘。

sudo mkfs.ext4 -c /dev/sde1
# 这个过程会很久,因为它会先扫描整个设备的坏道
示例 5:格式化一个镜像文件

你不仅可以格式化设备,也可以格式化一个文件(通常需要先关联到 loop 设备)。

# 创建一个 1G 的空文件
dd if=/dev/zero of=my-disk.img bs=1M count=1000

# 将这个文件格式化为 ext4
mkfs.ext4 my-disk.img

# 挂载这个镜像文件来使用它
sudo mount -o loop my-disk.img /mnt

mkfs 执行成功后你会看到什么?

运行 sudo mkfs.ext4 /dev/sdb1 后,你会看到类似如下输出:

mke2fs 1.46.5 (30-Dec-2021)
Creating filesystem with 2621440 4k blocks and 655360 inodes
Filesystem UUID: 6e5cff33-5f3d-4a86-b965-85c821a9e6b7
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632

Allocating group tables: done
Writing inode tables: done
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done

这些信息告诉你文件系统的基本参数:块数、inode 数、UUID、超级块备份位置等。


  • mkfs 是用于创建文件系统的命令,也就是高级格式化。
  • 它是一个前端,实际调用的是 mkfs.ext4mkfs.xfs 等具体工具。
  • 使用前务必确认设备未挂载且设备名正确,否则会导致数据丢失。
  • 根据使用场景(Linux 专用、跨平台、大文件性能)选择合适的文件系统类型。
  • 常用的选项包括设置卷标 -L 和检查坏块 -c

df 命令

dfDisk Filesystem 的缩写。它的主要功能是报告文件系统的磁盘空间使用情况。简单来说,它告诉你:

  1. 系统上有哪些磁盘/分区(文件系统)已经被挂载了。
  2. 每个磁盘/分区总共有多少空间
  3. 已经使用了多少空间
  4. 还剩下多少可用空间
  5. 它们被挂载到了哪个目录(挂载点)。

它是监控磁盘空间、防止磁盘被写满导致系统问题的首选工具。


命令语法

df [选项] [文件或目录]
  • 如果不加任何参数,df 会显示所有已挂载的文件系统的使用情况。
  • 如果指定了文件或目录df 会报告该文件或目录所在的文件系统的空间情况。

核心常用选项

选项 描述
-h 人类可读模式。以易读的单位(K, M, G, T)显示大小,而不是单纯的块数。这是最常用的选项。
-T 显示文件系统类型。会在输出中增加一列 Type,显示是 ext4, xfs, tmpfs 还是其他类型。
-i 显示 inode 信息而不是块使用情况。可以查看文件数量是否达到上限。
-x [类型] 排除指定类型的文件系统。例如 -x tmpfs 不显示临时文件系统。
--total 在输出结果的末尾添加一行总计信息。
-a 显示所有文件系统,包括那些大小为 0 的伪文件系统(如 /proc, /sys)。

输出列解读

当你运行 df -h,你会看到类似这样的输出:

文件系统        容量  已用  可用 已用% 挂载点
udev            3.9G     0  3.9G    0% /dev
tmpfs           787M  2.1M  785M    1% /run
/dev/nvme0n1p2  234G   65G  157G   30% /
tmpfs           3.9G   87M  3.8G    3% /dev/shm
tmpfs           5.0M  4.0K  5.0M    1% /run/lock
/dev/nvme0n1p1  511M  6.1M  505M    2% /boot/efi
tmpfs           787M   88K  787M    1% /run/user/1000

各列的含义如下:

  • 文件系统: 磁盘设备的名称(如 /dev/nvme0n1p2)或特殊文件系统(如 tmpfs, udev)。
  • 容量: 该文件系统的总大小
  • 已用: 已经使用了多少空间。
  • 可用剩余可用的空间。这是最需要关注的列,防止它变为 0。
  • 已用%: 已用空间占总空间的百分比。一眼就能看出哪个盘快满了。
  • 挂载点: 该文件系统被挂载到了哪个目录。例如,根分区 / 是系统最重要的挂载点。

实用示例

示例 1:最基本用法(人类可读格式)

这是最常用的命令,没有之一。

df -h

输出易于阅读,快速查看所有磁盘的使用情况。

示例 2:查看特定目录所在分区的空间

比如你想知道 /home 目录还有多少空间。

df -h /home

输出示例:

文件系统        容量  已用  可用 已用% 挂载点
/dev/sda1       200G   50G  150G   25% /home
示例 3:显示文件系统类型

想知道各个分区都是什么格式(ext4? NTFS?)。

df -hT

输出示例:

文件系统       类型      容量  已用  可用 已用% 挂载点
/dev/sda1      ext4      234G   65G  157G   30% /
/dev/sdb1      vfat      30G    5G   25G    17% /media/myusb
示例 4:检查 inode 使用情况

有时候磁盘空间明明还有剩余,但系统却报错“No space left on device”。这通常是 inode 用尽了(创建了太多小文件)。这个命令可以诊断这个问题。

df -i

输出示例:

文件系统       Inode 已用(I) 可用(I) 已用%(I) 挂载点
/dev/sda1      5.8M    1.1M    4.7M      19% /

如果 可用(I) 接近 0 或 已用%(I) 是 100%,说明 inode 耗尽,需要清理文件。

示例 5:排除特定类型的文件系统

例如,不想看那些临时的 tmpfs 文件系统,只关心真实的硬盘。

df -h -x tmpfs
示例 6:查看所有文件系统(包括伪文件系统)

显示包括 /proc, /sys 在内的所有信息。

df -a

(这个输出通常会很长,包含很多内核虚拟的文件系统。)


需要注意的要点

  1. “可用”空间: 通常,根文件系统 (/) 会保留约 5% 的磁盘空间(可由 tune2fs -m 调整)仅供 root 用户使用,以防止系统完全崩溃。所以普通用户看到的“可用”空间可能比实际计算的少一点点。
  2. 伪文件系统: 像 tmpfs, udev, proc 这些并不是真正的物理磁盘,它们是内核在内存中创建的虚拟文件系统,用于管理设备、进程信息等。它们的空间使用是动态变化的。
  3. 网络文件系统df 也会显示通过网络挂载的文件系统,如 NFS 或 Samba 共享。
  4. 挂载点: 理解挂载点的概念至关重要。/home 目录可能是一个独立的大分区,而 /boot 通常是一个较小的独立分区。

总结

  • df 是查看磁盘空间总体使用情况的命令。
  • du 是查看某个目录或文件具体占用多少空间的命令。它们两个通常配合使用:先用 df 发现哪个盘空间紧张,再用 du 去那个盘下找是哪个目录或文件占用了大量空间。
  • df -h 是你最应该记住的命令。
  • 如果遇到磁盘空间满的错误,但 df -h 显示还有空间,记得用 df -i 检查是否是 inode 用尽了。

掌握 df 命令是每个 Linux 用户和管理员进行系统监控和存储管理的基本功。

mount 命令

mount 命令用于将存储设备(如硬盘分区、U 盘、光盘、ISO 镜像)上的文件系统附加到 Linux 目录树的某个挂载点上,从而使该设备中的文件和目录可以被访问。

反之,umount 命令则用于将其分离,断开访问通道。

核心思想:它是在物理存储设备和用户可见的目录路径之间搭建一座桥梁


命令语法

mount [-选项] [-o 挂载选项] <设备名> <挂载点>
umount <挂载点或设备名>

基本用法示例

1. 查看所有已挂载的文件系统

不加任何参数直接运行 mount,会显示系统中所有已挂载的文件系统列表,信息非常详细。

mount

输出会包括设备、挂载点、文件系统类型、挂载选项等,类似于 df -h 但更详细。

2. 挂载一个 U 盘(手动挂载)

假设系统将 U 盘识别为 /dev/sdb1,我们要将它挂载到 /mnt/usb

# 第一步:创建挂载点目录 (如果不存在)
sudo mkdir -p /mnt/usb

# 第二步:执行挂载
sudo mount /dev/sdb1 /mnt/usb

# 第三步:访问U盘内容
ls /mnt/usb
3. 卸载设备

操作完成后,必须卸载才能安全移除设备。

# 通过挂载点卸载
sudo umount /mnt/usb

# 或者通过设备名卸载
sudo umount /dev/sdb1

重要提示umount 而不是 unmount


常用选项和高级用法

-t:指定文件系统类型

如果系统无法自动识别设备类型,或者你想明确指定,可以使用 -t 选项。

# 明确指定文件系统类型为 vfat (FAT32)
sudo mount -t vfat /dev/sdb1 /mnt/usb

# 挂载光盘,类型通常是 iso9660
sudo mount -t iso9660 /dev/sr0 /mnt/cdrom

# 挂载 exFAT 格式的U盘 (可能需要先安装 exfat-fuse)
sudo mount -t exfat /dev/sdc1 /mnt/usb
-o:指定挂载选项(非常强大)

这是 mount 命令最灵活的部分,允许你精细控制挂载行为。多个选项用逗号分隔。

常用挂载选项:

选项 描述 示例
ro / rw 只读 / 读写 挂载。挂载光盘或只读镜像时用 ro -o ro
remount 重新挂载一个已挂载的文件系统。常用于将只读挂载改为读写。 -o remount,rw /
loop 挂载镜像文件。将普通文件(如 .iso)当作块设备来挂载。 -o loop ubuntu.iso /mnt/iso
defaults 使用默认选项:rw, suid, dev, exec, auto, nouser, async -o defaults
noexec 禁止执行该文件系统上的二进制程序。增加安全性。 -o noexec
nosuid 忽略 suidsgid 权限位。增加安全性。 -o nosuid
user 允许普通用户挂载设备。 -o user
uid, gid 设定挂载后文件所有者的用户 ID 和组 ID,解决权限问题。 -o uid=1000,gid=1000
username, password 挂载 Windows 共享 (CIFS/SMB) 时使用的认证信息。 -o username=john,password=secret

示例:

# 以只读方式挂载光盘
sudo mount -t iso9660 -o ro /dev/sr0 /mnt/cdrom

# 挂载ISO镜像文件
sudo mount -o loop ubuntu-22.04.iso /mnt/iso

# 挂载NTFS分区并赋予当前用户读写权限 (需安装ntfs-3g)
sudo mount -t ntfs-3g -o uid=1000,gid=1000 /dev/sdd1 /mnt/ntfs

# 重新挂载根目录为读写模式 (在救援模式中常用)
sudo mount -o remount,rw /

注意事项

  1. 卸载失败: 如果 umount 失败并提示 device is busy,表示有进程正在使用该挂载点下的文件。你可以:
    • 使用 lsof /mnt/usb 查看是哪个进程在占用。
    • 切换到其他目录,再尝试卸载。
    • 使用 umount -l(lazy unmount)延迟卸载,等设备不再繁忙时再断开。
  2. 权限问题: 挂载的设备可能因为权限问题导致当前用户无法读写。通常用 -o uid=...,gid=... 选项解决。
  3. 安全卸载永远在拔出 U 盘或移动硬盘前执行 umount,否则可能导致数据损坏。

总结

命令 作用
mount 挂载文件系统到目录树
umount 卸载已挂载的文件系统
mount -a 挂载 /etc/fstab 中定义的所有文件系统
mount -o loop 挂载镜像文件
mount -o remount 重新挂载,改变挂载参数

mount 命令是 Linux 系统管理的瑞士军刀,它将物理存储、网络资源与系统的逻辑目录树无缝连接起来,体现了 Linux “一切皆文件” 的设计哲学。

unshare 命令

unshare 命令允许一个已经存在的进程从其父进程“脱离”一个或多个 Namespace,从而进入一个新的、隔离的上下文中运行。

简单来说: 它能让你的当前 shell 或命令“脱离”原来的网络、进程树、挂载点等视图,获得一个全新的、隔离的环境,而不需要启动一个新的容器或虚拟机


clone() 系统调用的区别

要理解 unshare,最好先知道它的“兄弟”——clone()

  • clone(): 在创建新进程的同时就将其放入新的 Namespace。docker runpodman run 等容器命令在底层最终调用的是 clone()(出生即隔离)
  • unshare(): 让一个已经运行着的进程脱离它当前的 Namespace。(后天改造隔离)

unshare 命令就是这个系统调用的命令行包装。


命令语法

unshare [选项] [<程序> [<参数>...]]
  • 选项: 指定要脱离哪种类型的 Namespace。
  • 程序: 可选参数。如果指定了,则在新创建的 Namespace 中直接运行这个程序。如果没指定,则默认会在新 Namespace 中启动一个新的 shell。

核心选项(指定要脱离的 Namespace)

选项 对应的 Namespace 效果
--mount or -m Mount (CLONE_NEWNS) 拥有独立的文件系统挂载点视图。
--uts or -u UTS (CLONE_NEWUTS) 可以设置独立的主机名和域名。
--ipc or -i IPC (CLONE_NEWIPC) 拥有独立的 System V IPC 和 POSIX 消息队列。
--net or -n Network (CLONE_NEWNET) 拥有独立的网络栈(接口、路由、防火墙等)。
--pid or -p PID (CLONE_NEWPID) 拥有独立的进程 ID 空间。需要与 --fork 一起使用。
--user or -U User (CLONE_NEWUSER) 拥有独立的用户和用户组 ID 映射。
--cgroup or -C Cgroup (CLONE_NEWCGROUP) 拥有独立的 cgroup 根目录。
--time or -T Time (CLONE_NEWTIME) 允许独立设置系统时钟偏移量。

常用辅助选项:

选项 描述
--fork or -f 在启动指定程序前先 fork 一个子进程。--pid 必须一起使用,否则新 Namespace 中第一个进程的 PID 会是 1,它退出后整个 Namespace 就销毁了。
--mount-proc /proc 文件系统重新挂载到新的 Mount Namespace 中。--pid 一起使用时是必须的,否则 ps, top 等命令无法正常工作(它们仍然会看到宿主机的所有进程)。
--map-root-user 在新的 User Namespace 中,将当前用户映射为 root (uid 0)。这允许你在没有外部 root 权限的情况下,在内部拥有 root 权限。
--root=<目录> 将指定的目录设置为新的根目录(类似 chroot)。

实用示例

示例 1:创建一个新的 Network Namespace

创建一个拥有独立网络环境的 shell。在这个 shell 里,你看不到宿主机的网络接口(除了 loopback)。

sudo unshare --net /bin/bash
# 进入新的bash后,查看网络,只有lo接口
ip a
示例 2:创建一个新的 PID 和 Mount Namespace(最像迷你容器)

这是最经典的用法,创建一个进程隔离的环境。

sudo unshare --pid --fork --mount-proc /bin/bash
  • --pid: 创建新的 PID Namespace。
  • --fork: 让 unshare 先 fork,再执行 /bin/bash,这样 bash 的 PID 就是 1。
  • --mount-proc: 重新挂载 /proc,使得 ps aux 等命令只显示这个新 Namespace 内的进程(实际上只有几个)。
    现在,你运行 ps aux,会发现看不到宿主机上的其他进程,仿佛进入了一个崭新的系统。
示例 3:在新的 Namespace 中改变主机名(UTS)
# 先查看当前主机名
hostname

# 创建新的 UTS Namespace 并运行 bash
sudo unshare --uts /bin/bash

# 在新环境中修改主机名
hostname my-new-container

# 再次查看,主机名已变
hostname

# 打开另一个终端,查看原主机名,发现并未改变。证明隔离成功。
exit # 退出后,主机名恢复
示例 4:普通用户创建“伪 root”环境(User Namespace)

这是非常强大的一点:普通用户可以不需 root 权限就创建新的 User Namespace,并在其中拥有 root 权限。

# 不需要 sudo!
unshare --user --map-root-user --pid --fork --mount-proc /bin/bash
whoami # 输出:root
id # 输出:uid=0(root) gid=0(root) groups=0(root)
  • --user--map-root-user: 让你在内部成为 root。
  • 注意:这个内部的 root 权限仅限于这个新的 Namespace 之内,例如你可以挂载文件系统、设置能力(capabilities),但对宿主机的影响非常有限,安全性很高。
示例 5:直接运行一个程序

不启动交互式 shell,而是直接在新 Namespace 中运行一个一次性命令。

sudo unshare --net --pid --fork --mount-proc sleep 300
# 这个 `sleep` 进程在一个隔离的 PID 和 Network Namespace 中运行300秒

unshareDocker/容器技术的底层基础之一

  • 当你运行 docker run -it ubuntu bash 时,Docker 引擎在底层做的事情,就类似于一个超级加强版的 unshare 命令:它一次性调用了几乎所有 --mount --uts --ipc --net --pid --user --cgroup 选项,并在此基础上增加了 Cgroups(资源限制)、镜像根文件系统等更多功能。
  • unshare 让你能够手动地、一步一步地体验和理解容器是如何实现隔离的。

mount 隔离

在 Linux 容器技术中,Mount Namespace 是实现文件系统隔离的核心机制,它允许每个容器拥有自己独立的文件系统挂载点视图,仿佛运行在一个完全独立的系统之上。这意味着在一个容器内部进行的挂载或卸载操作,不会影响到宿主机器或其他容器,反之亦然。

核心原理:挂载传播与命名空间

Mount Namespace 的实现并非简单的“屏蔽”或“过滤”,而是依赖于 Linux 内核一项更为精巧的设计:挂载传播(Mount Propagation)

每个挂载点都有一个传播类型属性,它决定了挂载事件如何在不同的命名空间之间通知和共享。当一个新 Mount Namespace 被创建时(例如通过 unshare --mountclone() 系统调用),它会复制当前命名空间的挂载点列表。然而,这些复制过来的挂载点的默认传播类型被设置为 MS_PRIVATE(私有)。

MS_PRIVATE 的含义是:在该挂载点下发生的任何挂载或卸载事件,都被视为当前 Mount Namespace 的“私事”。内核不会将此事件通知给任何其他命名空间,其他命名空间也不会将它们的挂载变更传播过来。这就像在两者之间设置了一道双向不可见的单向玻璃,完美实现了隔离。

实践演示:私有挂载实验

通过一个简单的命令,我们可以清晰地观察到 Mount Namespace 的隔离效果:

  1. 创建隔离环境:首先,我们使用 unshare 命令创建一个新的 Mount Namespace(--mount)并启动一个 shell。--mount-proc 选项不仅会挂载一个新的 /proc,其内部也会调用 unshare(CLONE_NEWNS) 来创建新的 Mount Namespace。

    sudo unshare --mount --fork --mount-proc /bin/bash
  2. 准备存储介质:在新创建的隔离 shell 中,我们创建一个空的镜像文件并将其格式化为 ext4 文件系统,模拟一块虚拟磁盘。

    dd if=/dev/zero of=./my-disk.img bs=1M count=100
    mkfs.ext4 ./my-disk.img
  3. 执行挂载操作:将格式化好的镜像文件挂载到一个目标目录(如 ./mnt)。

    mkdir ./mnt
    mount ./my-disk.img ./mnt

    此时,在隔离环境内使用 df -hmount 命令,可以清晰地看到该挂载点。

  4. 验证隔离性这是最关键的一步。 此时,在宿主系统或其他任何终端窗口中使用 mountdf -h 命令查看,都无法发现 my-disk.img 被挂载到了 ./mnt。这个挂载操作完全被限制在了新创建的 Mount Namespace 内部,对外部系统不可见。

cgroup

cgroup(Control Groups)是 Linux 内核提供的一种机制,用于对一组进程进行统一的资源限制、隔离和统计。它是容器化技术(如 Docker 和 Kubernetes)最核心的基石之一。

与早期的 cgroup v1 相比,cgroup v2 带来了更清晰统一的管理模型、更一致的组织结构以及更强大的功能(如原子化资源分配)。目前主流的 Linux 发行版均已默认采用 cgroup v2。

其核心思想可以通过下图来理解:

flowchart TD
subgraph Host[宿主机系统]
    subgraph CgroupRoot[统一层级结构 /sys/fs/cgroup/]
        direction TB
        Root[/根控制组
控制所有系统资源/] Root --> A[控制组 A
e.g. 数据库] Root --> B[控制组 B
e.g. Web 服务] Root --> C[控制组 C
e.g. 备份任务] A --> A1[控制组 A1
e.g. 数据库子任务] end end A -.->|施加 CPU、内存等限制| LA[进程列表 A] B -.->|施加 CPU、内存等限制| LB[进程列表 B] C -.->|施加 CPU、内存等限制| LC[进程列表 C] A1 -.->|继承父组限制并可叠加| LA1[进程列表 A1]

cgroup v2 通过一个统一的层级结构(通常挂载在 /sys/fs/cgroup/)来管理所有资源控制器(cpu、memory、io 等)。每个子目录都是一个控制组,可以为其配置资源限制,并将进程纳入其中进行管理。

管理基础:挂载与目录操作

cgroup v2 的管理接口通过虚拟文件系统(通常为 cgroup2fs)暴露。首先需要挂载,但现代系统通常已自动完成:

# 检查 cgroup2 挂载点
mount | grep cgroup2
# 输出示例:cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

管理 cgroup 的本质就是操作 /sys/fs/cgroup/ 下的目录和文件:

  • 创建控制组:使用 mkdir 创建一个子目录。
  • 配置限制:向该目录下的特定文件(如 cpu.max)写入配置值。
  • 加入进程:将进程的 PID 写入该目录下的 cgroup.procs 文件。
  • 删除控制组:使用 rmdir 删除空目录。

实战:使用 cgroup v2 限制 CPU 使用率

本示例将演示如何创建一个 cgroup,并将其内所有进程的 CPU 使用率限制为 单个核心的 20%

第 1 步:创建控制组

我们创建一个名为 cpu_demo 的控制组。创建目录即创建了 cgroup。

# 切换到 cgroup 根目录
cd /sys/fs/cgroup/
# 创建新的控制组
sudo mkdir cpu_demo

创建后,系统会自动在该目录下生成所有控制文件(cpu.max, cgroup.procs 等)。

第 2 步:配置 CPU 限制

cgroup v2 通过 cpu.max 文件来设置 CPU 配额,其格式为:$MAX $PERIOD

  • $PERIOD: 一个周期的时间长度(微秒)。通常固定为 100000(即 100 毫秒)。
  • $MAX: 在上述一个周期内,该 cgroup 中所有进程最多能使用的 CPU 时间(微秒)。计算公式为:MAX = PERIOD * desired_cpu_percentage

我们的目标是 20%,所以计算:100000 * 0.2 = 20000

# 设置 CPU 配额:100ms 周期内最多使用 20ms 的 CPU 时间
echo "20000 100000" | sudo tee /sys/fs/cgroup/cpu_demo/cpu.max

这表示该 cgroup 中的进程在任何 100ms 的时间窗口内,最多只能运行 20ms,即 20% 的 CPU 时间。

第 3 步:验证限制效果

为了验证限制,我们需要一个能消耗大量 CPU 的程序。这里使用 stress 工具。

  1. 将当前 Shell 加入控制组
    当前 Shell 中启动的进程会自动继承其 cgroup。我们将当前 Shell 加入 cpu_demo

    # 将当前 shell 的 PID 写入 cgroup.procs
    echo $$ | sudo tee /sys/fs/cgroup/cpu_demo/cgroup.procs
  2. 启动压力测试
    在新终端中,使用 pidstat 监控 CPU。在原终端中启动 stress,它会尝试占满一个核心。

    # 启动一个消耗 CPU 的 worker
    stress -c 1 &
  3. 监控 CPU 使用率
    在另一个终端,使用 pidstat 监控 stress 进程。

    pidstat -p $(pgrep -o stress) -u 1
    # 输出示例:
    # 06:14:48 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
    # 06:14:49 PM     0      1234    20.0      0.0      0.0     80.0     20.0     1  stress

    结果分析:可以看到,%CPU 列被稳定地限制在了 ~20%。同时,%wait 值很高,表明进程大部分时间在等待 CPU 时间片,这正是 cgroup 通过内核调度器进行“节流”限制的直观表现。

第 4 步:清理

实验完成后,务必进行清理:

# 1. 终止测试进程
pkill stress

# 2. 将当前 shell 移回根 cgroup
echo $$ | sudo tee /sys/fs/cgroup/cgroup.procs

# 3. 删除创建的 cgroup (必须确保组内无进程)
sudo rmdir /sys/fs/cgroup/cpu_demo

核心特性与优势

通过上述演示,我们可以看到 cgroup v2 的一些核心优势:

  • 统一层级结构:所有控制器(CPU、内存、IO 等)在同一目录下管理,结构清晰。
  • 接口简单直观:通过读写文件即可完成所有配置,易于理解和自动化。
  • 限制精确有效:在内核层面进行资源调度和隔离,效果稳定可靠。
  • 继承性:子 cgroup 继承父 cgroup 的限制,并可以进一步收紧,提供了灵活的资源配置能力。

cgroup v2 是理解和实现现代 Linux 系统资源管理的基础,从简单的进程限制到复杂的容器编排,都离不开它的强大功能。