在Linux系统中,`fork`和`clone`都是用于创建新进程的系统调用,但它们有不同的用途和行为:
1. `fork()`
- 作用: `fork()` 是一个经典的Unix系统调用,用于创建一个新进程。新进程是调用`fork()`的进程(父进程)的副本,通常被称为子进程。
- 子进程的特点:
- 子进程拥有与父进程相同的内存内容、文件描述符、环境变量等。
- 子进程的进程ID(PID)不同于父进程,并且子进程的`PPID`(父进程ID)是父进程的PID。
- 子进程从`fork()`调用之后的代码开始执行。
- `fork()` 调用在子进程中返回0,而在父进程中返回子进程的PID。
- 用途: `fork()` 是进程创建的基础,在许多Unix-like系统中广泛使用。它通常与`exec()`系列函数一起使用,以在子进程中执行新程序。
pid_t pid = fork();
if (pid == 0) {
// 子进程代码
} else if (pid > 0) {
// 父进程代码
} else {
// fork() 失败
}
2. `clone()`
- 作用: `clone()` 是Linux特有的系统调用,比`fork()`更加灵活。它允许调用者指定在新创建的子进程中哪些资源是共享的,哪些是独立的。
- 子进程的特点:
- `clone()` 允许指定一系列标志来控制子进程与父进程共享或不共享的资源,例如:内存空间、文件描述符、信号处理、用户和组ID等。
- 它可以创建线程(通过共享内存空间)或进程(不共享内存空间),因此`clone()` 是`fork()`和线程创建机制的基础。
- 标志: 常见的标志包括:
- `CLONE_VM`: 共享内存空间。
- `CLONE_FS`: 共享文件系统信息。
- `CLONE_FILES`: 共享文件描述符。
- `CLONE_SIGHAND`: 共享信号处理。
- `CLONE_THREAD`: 使新进程成为与调用进程属于同一个线程组的线程。
- 用途: `clone()` 主要用于实现线程库(如pthread)和容器(如Docker)的底层机制。它提供了比`fork()`更细粒度的控制。
#include <sched.h>
int child_func(void *arg) {
// 子进程(或线程)的代码
return 0;
}
pid_t pid = clone(child_func, stack, CLONE_VM | CLONE_FS | CLONE_FILES, NULL);
if (pid == -1) {
// clone() 失败
}
3. 主要区别
- 灵活性: `fork()` 创建的子进程是父进程的完整副本,通常用于创建独立的进程,而`clone()`允许创建具有共享资源的子进程或线程,提供了更大的灵活性。
- 用例: `fork()`主要用于传统的进程创建,而`clone()`可以用于创建线程、轻量级进程或实现容器化技术。
总结
- 使用`fork()`时,新进程与父进程几乎完全独立;而使用`clone()`时,可以选择性地共享资源,适用于更复杂的进程或线程管理场景。
在Linux系统中,每个用户能够创建的最大进程数(即可以`fork`的最大数)由以下几个因素限制:
1. 系统级限制:
- `kernel.pid_max`:这是系统中进程ID的最大值,默认通常是 `32768`,但可以通过 `/proc/sys/kernel/pid_max` 来查看和修改。例如:
cat /proc/sys/kernel/pid_max
要修改它,可以执行:
echo 4194304 > /proc/sys/kernel/pid_max
这个值可以设定为最大 `2^22`,即 `4194304`。
2. 用户级限制:
- `max user processes` (`ulimit -u`):这是限制每个用户可以创建的最大进程数。你可以用以下命令查看:
ulimit -u
也可以修改它:
ulimit -u 4096
如果想要永久修改,可以将对应的设置添加到 `/etc/security/limits.conf` 文件中,例如:
username soft nproc 4096
username hard nproc 8192
或者使用通配符 `*` 对所有用户生效。
3. 其他限制:
- 系统的可用内存:每个进程都会消耗一定的内存,因此系统的物理内存和交换分区大小也会限制可以创建的进程数量。
- 其他资源限制:例如文件描述符(`ulimit -n`)的限制也可能影响进程的创建。
在Linux中,`namespace` 和 `cgroup` 是两种关键技术,通常用于进程隔离和资源控制,特别是在容器化技术(如Docker)中。它们各自负责不同的功能,但经常结合使用来提供完整的隔离和资源管理解决方案。
1. Namespace(命名空间)
作用: `namespace` 是Linux内核提供的一种机制,用于隔离系统的全局资源,让不同的进程看到不同的系统视图。通过使用`namespace`,可以将不同的进程放入不同的命名空间,从而实现资源的隔离。
常见的命名空间类型:
- `PID namespace`: 隔离进程ID(PID)空间。每个命名空间中的进程都有自己独立的PID编号。父命名空间中的进程可以看到子命名空间的进程,而反之则不行。这在容器中非常有用,使得每个容器可以有自己从1开始的进程ID编号。
- `NET namespace`: 隔离网络栈,包括网络接口、路由表、IP 地址、防火墙规则等。每个命名空间可以有自己的独立网络配置。
- `MNT namespace`: 隔离挂载点视图。不同的命名空间可以有不同的文件系统挂载点。
- `UTS namespace`: 隔离主机名和域名。每个命名空间可以有自己的主机名和域名。
- `IPC namespace`: 隔离进程间通信资源,如信号量、消息队列和共享内存段。
- `USER namespace`: 隔离用户和组ID。使得在容器内部,用户可以以非特权用户身份运行,但在容器内具有root权限。
- `CGROUP namespace`: 隔离控制组(cgroup)的视图,使得不同的命名空间可以有不同的cgroup视图。
用途:
- `namespace` 允许创建多个独立的环境,例如多个容器,它们相互隔离,拥有各自独立的网络、文件系统和进程ID空间。
- 在容器化环境中,`namespace` 用于确保每个容器的进程、网络和文件系统等资源与其他容器隔离。
2. Cgroup(Control Group,控制组)
作用: `cgroup` 是Linux内核的另一种机制,用于控制和限制进程的资源使用。它可以对一组进程的资源使用进行分组和管理,例如CPU、内存、网络带宽等。
常见的cgroup子系统:
- `cpu`: 限制和监控CPU的使用。
- `memory`: 限制和监控内存的使用。包括物理内存和交换分区。
- `blkio`: 限制和监控块设备I/O(如磁盘)的使用。
- `net_cls`/`net_prio`: 限制和监控网络带宽的使用,设置网络优先级。
- `freezer`: 暂停和恢复cgroup中的所有进程。
- `devices`: 控制cgroup中的进程可以访问哪些设备。
- `pids`: 限制一个cgroup中可以创建的最大进程数。
用途:
- `cgroup` 主要用于确保进程或容器不会超出分配给它们的资源限制。它使得系统管理员能够分配资源给不同的进程组,并确保资源分配符合系统策略。
- 通过使用`cgroup`,可以防止一个进程或容器过度消耗系统资源,导致其他进程或容器受到影响。
3. Namespace与Cgroup的结合使用
在容器化技术中,`namespace` 和 `cgroup` 常常结合使用:
- `namespace` 提供进程间的隔离:通过将每个容器放置在不同的`namespace`中,可以确保容器之间的网络、进程、挂载点等系统资源是独立的,互不干扰。
- `cgroup` 提供资源管理和限制:通过将容器中的进程放置在不同的cgroup中,可以控制每个容器的CPU、内存、I/O等资源的使用,从而确保资源的公平分配和系统的稳定性。
- `namespace` 用于实现进程的隔离,确保进程之间的系统资源视图是独立的。
- `cgroup` 用于控制和管理进程的资源使用,确保系统资源的合理分配和使用。
- 在容器化技术中,二者的结合可以实现进程隔离和资源控制,提供类似于虚拟机的隔离性,同时具有较低的资源开销。
要深入了解 `namespace` 和 `cgroup` 的实现原理,需要查看它们在 Linux 内核中的源码。下面是对两者的实现进行简要介绍,包括涉及的核心数据结构和主要函数调用。
1. Namespace(命名空间)的源码分析
命名空间的实现主要集中在 Linux 内核的 `kernel/`, `include/linux/`, 和 `fs/` 目录下。命名空间的核心结构是 `struct nsproxy` 和对应的具体命名空间类型。
核心数据结构
- `struct nsproxy`:这是每个进程都关联的结构体,保存了进程所属的各个命名空间实例的指针。
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
struct time_namespace *time_ns;
};
每个字段对应不同的命名空间类型,如 `mnt_ns` 对应挂载命名空间,`pid_ns_for_children` 对应 PID 命名空间。
- `struct pid_namespace`: 例如,这是 `PID` 命名空间的结构,保存了该命名空间的进程树、命名空间级别、父命名空间等信息。
c
struct pid_namespace {
struct kref kref;
struct pidmap pidmap[PIDMAP_ENTRIES];
int last_pid;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct work_struct proc_work;
struct completion proc_done;
struct ctl_table_set set;
};
主要函数
- `clone()`:
- 在用户空间调用 `clone()` 时,会调用内核中的 `do_fork()` 函数。
- `do_fork()` 根据传入的标志位创建相应类型的命名空间。例如,`CLONE_NEWNS` 创建新的挂载命名空间,`CLONE_NEWPID` 创建新的 PID 命名空间。
- `copy_namespaces()` 函数负责将父进程的命名空间信息复制到子进程,或者根据需要创建新的命名空间实例。
- `unshare()`:
- `unshare()` 系统调用使调用进程脱离当前共享的命名空间,并创建新的命名空间。
- 内核函数 `do_unshare()` 通过 `unshare_nsproxy_namespaces()` 实现这一点,根据标志位决定哪些命名空间需要新的实例。
- `setns()`:
- `setns()` 系统调用用于让当前进程加入指定的命名空间。
- 内核实现主要在 `do_setns()` 中,通过 `ns_get_path()` 解析传入的文件描述符,然后调用相应的命名空间切换函数。
代码位置
- `kernel/nsproxy.c`: 包含了 `nsproxy` 的创建、引用计数管理等代码。
- `kernel/pid.c`: 包含了 `PID namespace` 的实现。
- `fs/mount.c`: 包含了挂载命名空间的实现。
2. Cgroup(控制组)的源码分析
`cgroup` 的实现主要集中在 `kernel/cgroup/` 目录下。`cgroup` 通过一组子系统来管理和限制进程的资源使用,每个子系统负责一种资源的管理。
核心数据结构
- `struct cgroup`: 描述一个 cgroup 控制组。
c
struct cgroup {
struct kernfs_node *kn;
struct cgroup_root *root;
struct cgroup *parent;
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
struct cgroup_file events_file;
struct cgroup_file notify_on_release;
struct cgroup_file procs_file;
struct list_head csets;
struct list_head css_sets;
struct list_head e_csets;
struct list_head self;
struct list_head sibling;
struct list_head children;
struct list_head release_list;
atomic_t nr_descendants;
atomic_t nr_dying_descendants;
struct percpu_ref refcnt;
struct percpu_ref dying_refcnt;
struct list_head pidlists;
struct list_head release_agent_work;
struct work_struct release_agent_work;
struct list_head pressure_events;
struct cgroup_file notify_pressure_events;
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
unsigned long flags;
};
- `struct cgroup_subsys`: 描述每个子系统,例如 `cpu`, `memory`, `blkio` 等。
c
struct cgroup_subsys {
struct list_head sibling;
struct idr css_idr;
int id;
const char *name;
struct list_head all_cgroups;
unsigned int subsys_id;
struct kernfs_root *root;
struct cgroup_root *root_subsys;
struct list_head all_css;
struct list_head all_css_sets;
};
主要函数
- `cgroup_create()`:
- 用于创建一个新的 cgroup。在内核中,调用 `cgroup_create()` 函数,通过路径找到相应的父 cgroup,分配并初始化新的 `struct cgroup` 结构。
- `cgroup_attach_task()`:
- 将进程或任务(task)附加到指定的 cgroup 中。它涉及将进程的 `cgroup_subsys_state` 设置为新的 cgroup。
- 在内核中,这由 `cgroup_attach_task()` 函数实现,通过更新 `task_struct` 中的 `cgroups` 字段。
- `cgroup_mkdir()` 和 `cgroup_rmdir()`:
- 通过创建和删除 cgroup 文件夹来管理 cgroup 的生命周期。`cgroup_mkdir()` 创建新 cgroup,而 `cgroup_rmdir()` 则删除指定的 cgroup。
代码位置
- `kernel/cgroup/cgroup.c`: 包含了主要的 cgroup 管理功能,包括创建、删除、附加任务等操作。
- `kernel/cgroup/*`: 包含了各种 cgroup 子系统的实现代码,例如 `cpu.c`, `memory.c` 等。
3. Namespace 和 Cgroup 的结合
在 Linux 容器化技术中,`namespace` 和 `cgroup` 结合使用,实现进程隔离与资源控制。
- 创建容器时,通过调用 `clone()` 或 `unshare()` 来创建新的命名空间;通过 `cgroup_create()` 和 `cgroup_attach_task()` 将进程放入特定的控制组,从而限制其资源使用。
- 这种结合使得容器能够独立运行,并确保它们只能使用分配给它们的资源,而不影响其他容器或系统的整体性能。
Docker利用Linux内核中的`namespace`和`cgroup`机制,实现了容器之间的进程隔离和资源控制,使得每个容器在运行时都具有独立的系统视图和资源分配。Docker通过这些机制实现进程间的隔离与互不可见。
1. Namespace在Docker中的应用
`namespace` 是实现容器隔离的核心。Docker通过创建不同类型的命名空间,确保每个容器有独立的进程空间、网络栈、文件系统等。这使得一个容器中的进程无法看到或影响其他容器的进程。
使用的命名空间类型:
- PID namespace: 隔离进程ID。每个容器有自己的进程ID空间,容器内的进程ID从1开始,这使得容器内的进程看不到宿主机或其他容器的进程。
- NET namespace: 隔离网络栈。每个容器有自己独立的网络接口、IP地址、路由表等网络资源,从而确保容器之间的网络隔离。
- MNT namespace: 隔离文件系统挂载点。Docker通过`MNT namespace`确保每个容器有独立的文件系统视图,容器内部只能访问其分配的文件系统,而无法访问宿主机或其他容器的文件系统。
- UTS namespace: 隔离主机名和域名。每个容器可以有独立的主机名和域名,容器内部对`hostname`的修改不会影响其他容器或宿主机。
- IPC namespace: 隔离进程间通信资源。确保容器间的共享内存、信号量等IPC资源互不影响。
- USER namespace: 允许在容器内使用非特权用户运行进程,但在容器内看起来是以root权限运行。
实现过程:
- 当Docker启动一个新容器时,它通过调用`clone()`系统调用,传递相应的命名空间标志(如`CLONE_NEWPID`,`CLONE_NEWNET`等),创建新命名空间。
- Docker通过这些命名空间实现容器之间的隔离,使得容器内的进程无法感知或影响其他容器或宿主机的进程。
示例代码:
pid_t pid = clone(child_func, stack, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER, NULL);
if (pid == -1) {
// 处理错误
}
2. Cgroup在Docker中的应用
`cgroup` 负责容器的资源管理和限制,确保每个容器只能使用分配给它的CPU、内存、I/O等系统资源,防止一个容器过度消耗资源影响其他容器和宿主机的性能。
使用的cgroup子系统:
- CPU cgroup: 限制和监控容器的CPU使用,确保每个容器得到公平的CPU资源分配。
- Memory cgroup: 限制容器的内存使用,防止某个容器耗尽系统内存。
- Blkio cgroup: 限制容器的块设备I/O操作,确保磁盘I/O资源的公平分配。
- PIDs cgroup: 限制每个容器可以创建的最大进程数,防止fork炸弹攻击。
实现过程:
- Docker通过创建cgroup控制组,将启动的容器进程加入到特定的cgroup中。
- 这些cgroup会根据预先配置的限制(如CPU限制、内存限制等)监控和控制容器的资源使用。
- Docker在容器启动时,通过libcontainer或runc接口与cgroup交互,设置相应的资源限制。
示例代码:
struct cgroup *cgrp;
cgrp = cgroup_create("docker/cgroup_name");
cgroup_add_task(cgrp, pid);
cgroup_set_limit(cgrp, "memory", "512M");
cgroup_set_limit(cgrp, "cpu", "2");
3. Docker如何结合Namespace和Cgroup实现隔离
当Docker启动一个新容器时,它会:
1. 创建命名空间:调用`clone()`或`unshare()`系统调用,创建新的命名空间并启动容器进程。通过创建独立的PID、NET、MNT等命名空间,确保容器之间的进程、网络、文件系统等互不可见。
2. 设置资源限制:创建相应的cgroup控制组,并将容器进程加入到cgroup中。然后,Docker会配置cgroup的资源限制,如内存、CPU等。
3. 启动容器进程:容器进程在新的命名空间和cgroup中运行,享有独立的系统视图和资源限制。
通过这种方式,Docker实现了对容器的进程隔离和资源控制,确保容器之间的互不可见性和资源使用的公平性。
- Namespace 在Docker中用于实现进程、网络、文件系统等资源的隔离,使得每个容器运行在独立的系统视图中。
- Cgroup 在Docker中用于控制和限制容器的资源使用,确保资源分配的公平性和系统的稳定性。
- Docker结合`namespace`和`cgroup`,通过创建独立的命名空间和分配独立的资源限制,实现了容器的隔离和资源控制,从而保障了容器的安全性和稳定性。
Docker 在源码中通过多层次的抽象和库函数调用实现了对 `namespace` 和 `cgroup` 的使用,从而实现容器的进程隔离和资源管理。下面是对 Docker 源码中如何处理 `namespace` 和 `cgroup` 的解释。
1. Docker 的架构概览
Docker 主要由以下几个核心组件组成:
Docker CLI:用户与 Docker 交互的命令行工具。
Docker Daemon (`dockerd`):Docker 的守护进程,处理所有容器管理任务。
Libcontainer:Docker 的底层库,负责容器的创建和管理,包括 `namespace` 和 `cgroup` 的操作。
Containerd:管理容器的生命周期。
Runc:用于运行容器的 CLI 工具,负责调用 `namespace` 和 `cgroup`。
2. Namespace 在 Docker 源码中的实现
关键代码位置
Libcontainer 是 Docker 使用的一个 语言库,专门用于容器的底层管理。`namespace` 的相关代码主要在 `libcontainer` 库中。
Namespace 创建过程
当 Docker 启动一个容器时,它会调用 `libcontainer` 中的代码来设置命名空间:
调用流程:
1. CLI > Daemon: 用户通过 CLI 启动一个容器,例如 `docker run`。CLI 将请求发送给 `dockerd`。
2. Daemon > Libcontainer: `dockerd` 处理请求后,通过 `libcontainer` 创建一个新的容器进程。具体实现是在 `libcontainer` 的 `container_linux.` 文件中。
3. Namespace 设置:
`libcontainer` 使用 `namespaces` 包(例如 `libcontainer/nsenter`)来设置和进入新的命名空间。
`namespace` 设置的代码在 `config.json` 中定义,然后由 `runc` 通过 `libcontainer` 的 `exec` 系列函数(例如 `init_linux.` 中的 `initProcess` 函数)进行执行。
// container_linux.
func (c *linuxContainer) Run(config *execConfig) error {
// 初始化命名空间
if err := c.initProcess(config); err != nil {
return err
}
// 其他启动步骤
}
Namespace 类型:
在 Docker 启动容器时,`runc` 创建并设置不同类型的命名空间,如 PID, NET, MNT 等。相关设置可以在 `libcontainer/configs/namespaces.` 中找到。
// namespaces.
type Namespace struct {
Type NamespaceType `json:"type"`
Path string `json:"path,omitempty"`
}
type Namespaces []Namespace
Namespace 进入和隔离
`nsenter`: `nsenter` 工具被用来进入现有的命名空间。在 Docker 进程启动后,使用 `nsenter` 进入新的命名空间,使得新启动的进程只能看到和操作属于该命名空间内的资源。
3. Cgroup 在 Docker 源码中的实现
关键代码位置
`cgroup` 的管理也由 `libcontainer` 负责,具体代码分布在 `libcontainer/cgroups` 目录中。
Cgroup 的创建和管理
当 Docker 启动一个容器时,它会通过 `libcontainer` 创建和管理 cgroup。
Cgroup 配置:
Docker 容器的 cgroup 设置通常在 `config.json` 文件中定义,包含 CPU、内存、blkio 等资源限制。
// cgroups.
type Cgroup struct {
Name string `json:"name,omitempty"`
Path string `json:"path,omitempty"`
CPU *CgroupCPU `json:"cpu,omitempty"`
Memory *CgroupMemory `json:"memory,omitempty"`
Blkio *CgroupBlkio `json:"blkio,omitempty"`
}
Cgroup 创建过程:
1. 调用流程:
Docker Daemon 在启动容器时调用 `libcontainer` 的 `New()` 方法创建一个新的 cgroup 控制组。
`cgroup` 配置的加载和创建过程由 `applyCgroupConfig()` 函数完成。
2. 设置限制:
在容器启动时,Docker 使用 `libcontainer/cgroups` 包中的函数来设置 CPU、内存等资源的限制。具体的资源限制操作在 `cgroups/apply.` 中定义。
func applyCgroupConfig(pid int, config *configs.Cgroup) error {
// CPU 限制
if err := setCpuLimit(pid, config); err != nil {
return err
}
// 内存限制
if err := setMemoryLimit(pid, config); err != nil {
return err
}
// 其他资源限制
}
Cgroup 附加到进程:
在容器进程启动后,`libcontainer` 将进程的 `pid` 通过 `cgroups.Manager` 接口附加到 cgroup 控制组,从而应用资源限制。
4. 结合 Namespace 和 Cgroup 实现隔离
在 Docker 的源码中,`namespace` 和 `cgroup` 的使用贯穿了容器的整个生命周期:
1. 创建和配置阶段:
通过 `libcontainer` 库设置容器的 `namespace` 和 `cgroup`,定义了容器的隔离级别和资源限制。
2. 运行阶段:
在 `libcontainer` 中,Docker 使用 `initProcess` 函数来初始化进程,包括进入新的命名空间和应用 cgroup 限制。这个过程确保容器在隔离的环境中运行,且资源使用受到严格控制。
3. 管理和销毁阶段:
Docker 通过 `libcontainer` 监控容器进程的运行状态,并在容器终止时清理相应的 `namespace` 和 `cgroup` 设置,确保系统资源得到释放。