参考书目:《Linux-UNIX系统编程手册》,《深入Linux内核架构》,《Linux内核完全注释》,国内各chatgpt等。如有不正确或争议之处还请大佬们多多指正。后续可能会补充差异于体系结构的内容。
1. 内核中的进程表示
1.1 task_struct
Linux中每个进程都有一个数据结构task_struct来供内核来管理,实际上就是PCB(Process Control Block)进程控制块,其中包含众多成员,将进程与各个子系统之间连接。为方便起见,后文各处介绍到相关内容的时候,再详细说明结构其中的各类字段以及含义。
#include <sched.h>
struct task_struct {
...
}
1.2 命名空间
1.2.1 命名空间的解释
【概念】
为了实现进程和资源的隔离,内核采取命名空间的方式,通过为不同的进程或进程组提供独立的系统资源视图,来实现这些进程或进程组之间的隔离。
命名空间是Linux容器技术(如Docker)的基础,容器技术允许在单个物理机上运行多个相互隔离的系统实例。
【为什么需要命名空间】
如果提供Web主机的供应商打算向用户提供Linux计算机的全部访问权限,包括root权限在内。需要为每个用户准备一台计算机(或是虚拟机),代价太大。而且计算机的各个用户都需要一个独立的内核,以及一份完全安装好的配套的用户层应用。
在虚拟机中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间只使用一个内核在一台物理计算机上运作,将各种全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器内部的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。例如,容器可以自定义隔离或者共享的内容,容器可以设置为使用自身的PID集合,这样仍然与其他容器共享部分文件系统。
命名空间建立了系统的不同视图。每一项全局资源都必须包装到容器数据结构中,只有资源和包含资源的命名空间构成的二元组是全局唯一的。
图片中显示的实际上是只有9个进程,但是有9+6=15个pid来标识,具体哪个是进程真正的pid,需要依赖上下文。
1.2.2 创建命名空间的方式
- fork()、clone():创建新进程的同时建立一个新的命名空间。clone_flags参数可以选择是创建怎样的命名空间。例如,CLONE_NEWUTS等。有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。
- unshare():将进程的某些部分从父进程分离,其中也包括命名空间。其flags参数有一些常用的标志位。
将进程与父进程的命名空间分离后,任何一方的全局属性的修改均不会影响。但是文件系统的共享机制会更加复杂一些,存在多种可能性。
1.2.3 命名空间的具体实现
命名空间的实现有两部分组成:一是将进程关联到各个命名空间的指针,即task_struct的成员nsproxy。二是每个子系统的命名空间结构实体,将子系统的全局属性封装到命名空间中,即下图最右侧。
task_struct指向nsproxy结构的指针即成员nsproxy,是指针所以不同的进程可以共享一个命名空间,修改命名空间对所有在它之下的进程都可见。
<sched.h>
struct task_struct {
...
struct nsproxy *nsproxy; //命名空间
...
}
nsproxy结构则包含了各子系统命名空间的指针。
<nsproxy.h>
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;
struct user_namespace *user_ns;
struct net *net_ns;
};
- uts_ns:UTS(UNIX Timesharing System)命名空间,包含了内核名称、版本、底层体系结构等信息。
- ipc_ns:IPC命名空间,包含了与进程间通信有关的所有信息,如信号量、消息队列等。
- mnt_ns:MNT命名空间,包含了已经加载的文件系统。
- pid_ns:PID命名空间,包含了有关pid的信息。
- user_ns:用户命名空间,包含了用于限制每个用户资源使用的信息。
- net_ns:网络命名空间,包含了所有网络相关的命名空间参数。
1.2.3.1 命名空间相关的clone_flags
clone_flags是clone()系统调用的一个参数,用于设置创建怎样的子进程。是各种位掩码的“或”的组合。以下是一些clone_flags的定义。
<sched.h>
#define CLONE_NEWUTS 0x04000000 //创建新的utsname组
#define CLONE_NEWIPC 0x08000000 //创建新的IPC命名空间
#define CLONE_NEWUSER 0x10000000 //创建新的用户命名空间
#define CLONE_NEWPID 0x20000000 //创建新的PID命名空间
#define CLONE_NEWNET 0x40000000 //创建新的网络命名空间
init_nsproxy定义了初始的全局命名空间,其中定义了指向各子系统初始的命名空间对象的指针。
<kernel/nsproxy.c>
struct nsproxy init_nsproxy = INIT_NSPROXY(init_nsproxy);
<init_task.h>
#define INIT_NSPROXY(nsproxy) {
\
.pid_ns = &init_pid_ns, \
.count = ATOMIC_INIT(1), \
.uts_ns = &init_uts_ns, \
.mnt_ns = NULL, \
INIT_NET_NS(net_ns) \
INIT_IPC_NS(ipc_ns) \
.user_ns = &init_user_ns, \
}
1.2.3.2 UTS命名空间
UTS(UNIX Timesharing System),只有简单量,没有层次组织。
<utsname.h>
struct uts_namespace {
struct kref kref;
struct new_utsname name;
};
- kref:Kernel Reference,引用计数器,跟踪内核中有多少地方使用了struct uts_namespace的实例。
<kref.h>
struct kref {
atomic_t refcount; //引用计数本身,使用原子类型来确保多线程安全
...
// 在实际的内核实现中,kref 可能不会直接包含这些函数指针,
// 而是通过其他机制(如回调函数)来处理引用计数为零时的情况。
// 但为了说明目的,我们可以想象有这样的函数指针。
void (*release)(struct kref *kref); // 当引用计数为零时调用的释放函数
};
- name:包含了uts_namespace所提供的属性信息。uname命令或者/proc/sys/kernel/下包含了这些信息。
<utsname.h>
struct new_utsname {
char sysname[65]; //系统名称,如Linux
char nodename[65]; //hostname
char release[65]; //内核版本号
char version[65]; //内核版本日期
char machine[65]; //体系结构,如x86_64
char domainname[65]; //域名
};
new_utsname的初始值被保存在init_uts_ns中。
init/version.c
struct uts_namespace init_uts_ns = {
...
.name = {
.sysname = UTS_SYSNAME,
.nodename = UTS_NODENAME,
.release = UTS_RELEASE,
.version = UTS_VERSION,
.machine = UTS_MACHINE,
.domainname = UTS_DOMAINNAME,
},
};
预处理器常数常数在内核中各处定义,例如,UTS_RELEASE在<utsrelease.h>中定义,这个文件是编译时通过顶层makefile动态生成的。
UTS结构的某些部分不能修改。例如,把sysname换成Linux以外的其他值,但可以改变machine。
- 创建UTS命名空间
copy_utsname():在进程调用fork(),使用CLONE_NEWUTS标志,指定创建新的UTS命名空间,则调用该函数。
在这种情况下,会生成父进程的uts_namespace实例的一份副本,创建的子进程的nsproxy实例内部的指针会指向这个新的副本。
由于在读取或设置UTS属性值时,内核保证操作特定于当前进程的uts_namespace实例,在当前子进程修改UTS属性不会传递到父进程,父进程的修改也不会传递到子进程。
1.2.3.3 用户命名空间
<user_namespace.h>
struct user_namespace {
struct kref kref;
struct hlist_head uidhash_table[UIDHASH_SZ];
struct user_struct *root_user;
};
-
kref:引用计数器,用于跟踪多少地方需要使用user_namespace实例。
-
uidhash_table:命名空间中的每个用户,都有一个struct user_struct的实例负责记录其资源消耗,各个实例可通过散列表uidhash_table访问。用户命名空间对其用户资源使用的统计,与其他命名空间完全无关,对root用户的统计也是如此。因为在clone_user_ns()克隆一个用户命名空间时,为当前用户和root都创建了新的user_struct实例。
-
root_user:root用户的user_struct。user_struct维护了一些统计数据(如进程和打开文件的数目)。
-
克隆用户命名空间 clone_user_ns():
kernel/user_namespace