Linux 学习笔记——第二章 进程管理和调度(2)

Linux 学习笔记——第二章 进程管理和调度(2)

《深入 Linux 内核架构》阅读笔记。书籍参考的内核版本较老,文章参考的 Linux 内核版本为 5.4.103,并根据新版内核调整了一些代码片段

进程类型

典型的 UNIX 进程包括:由二进制代码组成的应用程序、单线程、分配给应用程序的一组资源(如内存、文件等)。新进程是使用 forkexec 系统调用产生的。

  • 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 以递增次序分配。

虽然子容器不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程。

新的命名空间可以用下面两种方法创建。

  • 在用 forkclone 系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。
  • 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 属性不会反映到父进程,而父进程的修改也不会传播到子进程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值