在Linux世界中,clone()
系统调用通过复制调用进程创建一个新进程。新进程称为子进程,原始进程称为父进程。clone()
系统调用有几个选项,允许我们控制父进程和子进程之间资源的共享。其中一个重要的选项是Cloneflags
。
Cloneflags
是一个用于指定创建 Linux 命名空间的标志位, 是 Linux 内核中用于进程创建的参数之一,它用于控制新进程如何与父进程共享资源。
具体来说,Cloneflags的作用是通过设置位标志(flag)来改变新进程的行为。这些标志可以用于控制新进程的命名空间、信号处理、文件描述符和虚拟内存等方面。Cloneflags可以控制新进程是否共享内存、文件描述符、信号处理、CPU时间限制、内存映射等行为。
Cloneflags
参数是 clone()
系统调用的一部分,它通过一个位掩码来指定新进程应该继承哪些资源以及如何共享这些资源。这个位掩码可以通过按位或(OR)操作来设置多个标志,下面是一些常见的 Cloneflags
标志:
CLONE_NEWNS:使新进程拥有一个新的、独立的挂载命名空间,可以隔离文件系统。
CLONE_NEWUTS:使新进程拥有一个新的、独立的 UTS 命名空间,可以隔离主机名和域名。
CLONE_NEWIPC:使新进程拥有一个新的、独立的 IPC 命名空间,可以隔离 System V IPC 和 POSIX 消息队列。
CLONE_NEWNET:使新进程拥有一个新的、独立的网络命名空间,可以隔离网络设备、协议栈和端口。
CLONE_NEWPID:使新进程拥有一个新的、独立的 PID 命名空间,可以隔离进程 ID。
CLONE_NEWUSER:使新进程拥有一个新的、独立的用户命名空间,可以隔离用户和组 ID。
CLONE_FILES:使新进程共享打开的文件描述符表,但不共享文件描述符的状态(例如文件偏移量)。
CLONE_FS:使新进程共享文件系统信息(例如当前工作目录和根目录)。
CLONE_VM:使新进程共享虚拟内存空间,即在进程之间共享代码和数据段。
CLONE_SIGHAND:使新进程共享信号处理程序。
CLONE_THREAD:使新进程成为调用进程的线程,与父进程共享进程 ID 和资源,但拥有独立的栈。
我们可以通过一些例子来熟悉下
CLONE_VM
CLONE_VM
标志将使新进程共享与父进程相同的内存空间,这意味着它们将具有相同的虚拟地址空间。这意味着父进程和子进程将共享相同的全局变量、静态变量和堆内存,但它们的栈将是不同的。这是创建线程的一种方法。
我们使用CLONE_VM
标志创建了一个新进程。SIGCHLD
标志告诉父进程,在子进程退出时接收一个SIGCHLD
信号
package main
import (
"fmt"
"syscall"
"sync"
"runtime"
)
const stackSize = 1024 * 1024
var sharedVariable = 10
func childProcess(wg *sync.WaitGroup) uintptr {
defer wg.Done()
// 修改共享变量的值
sharedVariable = 20
// 输出子进程的进程 ID 和共享变量的值
fmt.Printf("Child process: PID=%d, sharedVariable=%dn", syscall.Getpid(), sharedVariable)
// 子进程退出
return 0
}
func main() {
// 增加逻辑处理器的数量和每个处理器的堆栈空间大小
runtime.GOMAXPROCS(2)
runtime.GOMAXPROCS(1<<16 - 1)
// 使用等待组来等待 goroutine 完成
var wg sync.WaitGroup
wg.Add(1)
go childProcess(&wg)
// 在子进程中输出共享变量的值
fmt.Printf("Parent process: PID=%d, sharedVariable=%dn", syscall.Getpid(), sharedVariable)
// 等待子进程退出
wg.Wait()
}
CLONE_FS
CLONE_FS
标志将使新进程共享与父进程相同的文件系统信息,包括当前工作目录、根目录和挂载点。这使得子进程能继承父进程的文件系统环境,可以从相同的目录访问相同的文件。
CLONE_FILES
CLONE_FILES
标志将使新进程共享与父进程相同的文件描述符表,这意味着它们将共享打开的文件。子进程可以在没有另一个打开文件描述符的情况下访问它们。
docker的实现原理中,一些基础的功能也是用这些标志位来实现,下面是一些用 Go 语言实现 Cloneflags
标志位的示例代码:
package main
import (
"fmt"
"syscall"
)
func main() {
// 创建新的文件系统命名空间
if err := syscall.Unshare(syscall.CLONE_NEWNS); err != nil {
fmt.Println("Failed to create new mount namespace:", err)
return
}
// 创建新的网络命名空间
if err := syscall.Unshare(syscall.CLONE_NEWNET); err != nil {
fmt.Println("Failed to create new network namespace:", err)
return
}
// 创建新的进程间通信(IPC)命名空间
if err := syscall.Unshare(syscall.CLONE_NEWIPC); err != nil {
fmt.Println("Failed to create new IPC namespace:", err)
return
}
// 创建新的主机名和域名命名空间
if err := syscall.Unshare(syscall.CLONE_NEWUTS); err != nil {
fmt.Println("Failed to create new UTS namespace:", err)
return
}
// 创建新的用户命名空间
if err := syscall.Unshare(syscall.CLONE_NEWUSER); err != nil {
fmt.Println("Failed to create new user namespace:", err)
return
}
// 创建新的进程 ID 命名空间
if err := syscall.Unshare(syscall.CLONE_NEWPID); err != nil {
fmt.Println("Failed to create new PID namespace:", err)
return
}
fmt.Println("All namespaces created successfully.")
}
上面创建了六个不同类型的命名空间,分别对应文件系统、网络、IPC、UTS、用户和进程 ID。在每个 Unshare
调用中,我们传入对应的 Cloneflags
标志位,以创建对应的命名空间。如果命名空间创建成功,我们就可以在新的命名空间中运行进程,并且该进程将只能访问新的命名空间中的资源,而不能访问主机上的资源。
后面我们利用这些原理来深入的理解docker 的底层实现原理与编写对应功能的demo。