Linux 学习笔记——第二章 进程管理和调度(2)
《深入 Linux 内核架构》阅读笔记。书籍参考的内核版本较老,文章参考的 Linux 内核版本为 5.4.103,并根据新版内核调整了一些代码片段
进程类型
典型的 UNIX 进程包括:由二进制代码组成的应用程序、单线程、分配给应用程序的一组资源(如内存、文件等)。新进程是使用 fork
和 exec
系统调用产生的。
fork
生成当前进程的一个相同副本,该副本称之为子进程。原进程的所有资源都以适当的方式复制到子进程,因此该系统调用之后,原来的进程就有了两个独立的实例。exec
从一个可执行的二进制文件加载另一个应用程序,来代替当前运行的进程。换句话说,加载了一个新程序。因为exec
并不创建新进程,所以必须首先使用fork
复制一个旧的程序,然后调用exec
在系统上创建另一个应用程序。
除此之外 Linux 还提供了 clone
系统调用。clone
的工作原理基本上与 fork
相同,但新进程不是独立于父进程的,而可以与其共享某些资源。可以指定需要共享和复制的资源种类,例如,父进程的内存数据、打开文件或安装的信号处理程序。clone
用于实现线程,但仅仅该系统调用不足以做到这一点,还需要用户空间库才能提供完整的实现。
命名空间
概念
传统上,在 Linux 以及其他衍生的 UNIX 变体中,系统中的所有进程按照惯例是通过 PID 标识的,这意味着内核必须管理一个全局的 PID 列表。全局 ID 使得内核可以有选择地允许或拒绝某些特权。例如 UID 为 n 的用户,不允许杀死属于用户 m 的进程(m ≠ n)。但这不能防止用户看到彼此,即用户 n 可以看到另一个用户 m 也在计算机上活动。但有些情况下,这种效果可能是不想要的。使用 KVM 或 VMWare 提供的虚拟化环境是一种解决问题的方法,但资源分配做得不是非常好。计算机的各个用户都需要一个独立的内核,以及一份完全安装好的配套的用户层应用。
命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性。命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。
本质上,命名空间建立了系统的不同视图。此前的每一项全局资源都必须包装到容器数据结构中,只有资源和包含资源的命名空间构成的二元组仍然是全局唯一的。每个容器必须看起来像是单独的一台 Linux 计算机。因此其中每一个都有自身的 init 进程,PID 为 0,其他进程的 PID 以递增次序分配。
虽然子容器不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程。
新的命名空间可以用下面两种方法创建。
- 在用
fork
或clone
系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。 unshare
系统调用将进程的某些部分从父进程分离,其中也包括命名空间。
在进程已经使用上述的两种机制之一从父进程命名空间分离后,从该进程的角度来看,改变全局属性不会传播到父进程命名空间,而父进程的修改也不会传播到子进程,至少对于简单的量是这样。而对于文件系统来说,情况就比较复杂。
实现
命名空间的实现需要两个部分:
- 每个子系统的命名空间结构,将此前所有的全局组件包装到命名空间中。
- 将给定进程关联到所属各个命名空间的机制。
每个进程都关联到自身的命名空间视图:
// include/linux/sched.h
struct task_struct {
// ...
/* Namespaces: */
struct nsproxy *nsproxy;
// ...
}
因为使用了指针,多个进程可以共享一组子命名空间。这样,修改给定的命名空间,对所有属于该命名空间的进程都是可见的。子系统此前的全局属性现在封装到命名空间中,每个进程关联到一个选定的命名空间。struct nsproxy
用于汇集指向特定于子系统的命名空间包装器的指针:
// include/linux/nsproxy.h
struct nsproxy {
atomic_t count; // 计数器,持有该 nsproxy 的 task_struct 的数目
struct uts_namespace *uts_ns; // 包含运行内核的名称、版本、底层体系结构类型等信息
struct ipc_namespace *ipc_ns; // 所有与进程间通信(IPC)有关的信息
struct mnt_namespace *mnt_ns; // 已经装载的文件系统的视图
struct pid_namespace *pid_ns_for_children; // 供 children 使用的进程 ID 信息
struct net *net_ns; // 网络相关的命名空间参数
struct cgroup_namespace *cgroup_ns; // 虚拟化进程的 cgroup 视图
};
extern struct nsproxy init_nsproxy;
由于在创建新进程时可使用 fork
建立一个新的命名空间,因此必须提供控制该行为的适当的标志。每个命名空间都有一个对应的标志:
// /usr/include/linux/sched.h
#define CLONE_NEWCGROUP 0x02000000 /* New cgroup namespace */
#define CLONE_NEWUTS 0x04000000 /* New utsname namespace */
#define CLONE_NEWIPC 0x08000000 /* New ipc namespace */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */
对命名空间的支持必须在编译时启用,而且必须逐一指定需要支持的命名空间。
init_nsproxy
定义了初始的全局命名空间,其中维护了指向各子系统初始的命名空间对象的指针:
// kernel/nsproxy.c
struct nsproxy init_nsproxy = {
.count = ATOMIC_INIT(1),
.uts_ns = &init_uts_ns,
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
.ipc_ns = &init_ipc_ns,
#endif
.mnt_ns = NULL,
.pid_ns_for_children = &init_pid_ns,
#ifdef CONFIG_NET
.net_ns = &init_net,
#endif
#ifdef CONFIG_CGROUPS
.cgroup_ns = &init_cgroup_ns,
#endif
};
UTS 命名空间
UTS (UNIX Timesharing System) 命名空间几乎不需要特别的处理,因为它只需要简单量,没有层次组织。
// include/linux/utsname.h
struct uts_namespace {
struct kref kref; // 计数器,跟踪内核中有多少地方使用了 uts_namespace 的实例
struct new_utsname name; // 属性信息
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct ns_common ns;
} __randomize_layout;
extern struct uts_namespace init_uts_ns;
// include/uapi/linux/utsname.h
struct new_utsname {
char sysname[__NEW_UTS_LEN + 1];
char nodename[__NEW_UTS_LEN + 1];
char release[__NEW_UTS_LEN + 1];
char version[__NEW_UTS_LEN + 1];
char machine[__NEW_UTS_LEN + 1];
char domainname[__NEW_UTS_LEN + 1];
};
可以在 Shell 里使用 uname 工具获取这些属性的当前值
~$ uname -a
Linux ubuntu 5.4.0-66-generic #74~18.04.2-Ubuntu SMP Fri Feb 5 11:17:31 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
初始设置保存在 init_uts_ns
中:
// init/version.c
struct uts_namespace init_uts_ns = {
.kref = KREF_INIT(2),
.name = {
.sysname = UTS_SYSNAME,
.nodename = UTS_NODENAME,
.release = UTS_RELEASE,
.version = UTS_VERSION,
.machine = UTS_MACHINE,
.domainname = UTS_DOMAINNAME,
},
.user_ns = &init_user_ns,
.ns.inum = PROC_UTS_INIT_INO,
#ifdef CONFIG_UTS_NS
.ns.ops = &utsns_operations,
#endif
};
EXPORT_SYMBOL_GPL(init_uts_ns);
copy_utsname
函数负责创建一个新的 UTS 命名空间。在某个进程调用 fork
并通过CLONE_NEWUTS
标志指定创建新的 UTS 命名空间时,则调用该函数。在这种情况下,会生成先前的uts_namespace
实例的一份副本,当前进程的 nsproxy
实例内部的指针会指向新的副本。在当前进程修改 UTS 属性不会反映到父进程,而父进程的修改也不会传播到子进程。