认识Namespace

namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。linux 内核提供的 namespace 技术为 docker 等容器技术的出现和发展提供了基础条件。

Linux 现有的namespace 有7种:

namespace 隔离内容
Cgroup CLONE_NEWCGROUP Cgroup root directory
IPC CLONE_NEWIPC System V IPC, POSIX消息队列
Network CLONE_NEWNET 网络设备、栈、端口等
Mount CLONE_NEWNS 挂载点
PID CLONE_NEWPID 进程ID
User CLONE_NEWUSER 用户和组ID
UTS CLONE_NEWUTS 主机名和NIS域名

其中Cgroup namespace在4.6的内核中才实现。本文只介绍pid namespace。文章写的比较繁琐,还请大家耐心看到后面( ^_^ ) ~~~~

Namespace操作函数

和namespace相关的函数只有三个,如下所示:

一、clone: 创建一个新的进程并把他放到新的namespace中。

int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg); 

其中:flags用于指定一个或者多个上面的CLONE_NEW*宏定义(当然也可以包含跟namespace无关的flags,多个flags 用|进行分隔),这样就会创建一个或多个新的不同类型的namespace,并把新创建的子进程加入新创建的这些namespace中。

二、setns: 将当前进程加入到已有的namespace中

int setns(int fd, int nstype); 

其中:

  • fd:指向/proc/[pid]/ns/目录里相应namespace对应的文件,表示要加入哪个namespace
  • nstype:指定namespace的类型(上面的任意一个CLONE_NEW*),具体分为两种情况:1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建,并通过UNIX domain socket传给当前进程,那么就需要通过nstype来指定fd指向的namespace的类型。2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的,那么nstype设置为0即可。

三、unshare: 使当前进程退出指定类型的namespace,并加入到新创建的namespace(相当于创建并加入新的namespace)。

int unshare(int flags); 

其中:flags用于指定一个或者多个上面的CLONE_NEW*宏定义(当然也可以包含跟namespace无关的flags,多个flags 用|进行分隔),这样就会创建一个或多个新的不同类型的namespace,并把新创建的子进程加入新创建的这些namespace中。 clone和unshare的区别

clone和unshare的功能都是创建并加入新的namespace, 他们的区别是:

  • unshare是使当前进程加入新的namespace。
  • clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变。

pid namespace有什么用?

PID Namespace对进程PID重新标号,即不同的Namespace下的进程可以有同一个PID。

内核为所有的PID Namespace维护了一个树状结构,最顶层的是系统初始化创建的,被称为Root Namespace,由它创建的新的PID Namespace成为它的Child namespace,原先的PID Namespace成为新创建的Parent Namespace,这种情况下不同的PID Namespace形成一个等级体系:父节点可以看到子节点中的进程,可以通过信号对子节点的进程产生影响,反过来子节点无法看到父节点PID Namespace里面的进程。下面用一个图描述容器、进程pid、pid namespace关系:

PID namesapce 对容器类应用特别重要, 可以实现容器内进程的暂停/恢复等功能,还可以支持容器在跨主机的迁移前后保持内部进程的 PID 不发生变化。

pid namespace 特性

1、进程所属的 PID namespace 在它创建的时候就确定了,不能更改,所以调用 unshare 和 nsenter 等命令后,原进程还是属于老的 PID namespace,新 fork 出来的进程才属于新的 PID namespace;

2、PID namespace 可以嵌套;

3、PID namespace 中的 init 进程。当一个进程的父进程退出后,该进程就变成了孤儿进程。孤儿进程会被当前 PID namespace 中 PID 为 1 的进程接管,而不是被最外层的系统级别的 init 进程接管。


下面从kernel源码中了解下pid namespace的原理和用法。所有kernel源码均来自linux 4.18.0。

核心数据结构

1、内核使用struct pid_namespace 结构体描述进程号命名空间:

//include/linux/pid_namespace.h
struct pid_namespace {
struct kref kref;
struct idr idr;
struct rcu_head rcu;
unsigned int pid_allocated;
struct task_struct *child_reaper;
struct kmem_cache *pid_cachep;
unsigned int level;
struct pid_namespace *parent;
#ifdef CONFIG_PROC_FS
struct vfsmount *proc_mnt;
struct dentry *proc_self;
struct dentry *proc_thread_self;
#endif
#ifdef CONFIG_BSD_PROCESS_ACCT
struct fs_pin *bacct;
#endif
struct user_namespace *user_ns;
struct ucounts *ucounts;
struct work_struct proc_work;
kgid_t pid_gid;
int hide_pid;
int reboot; /* group exit code if this pidns was rebooted */
struct ns_common ns;
} __randomize_layout;

2、内核将所有的namespace封装成struct nsproxy ,struct pid_namespace就在该结构体中:

//include/linux/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_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
};

3、命名空间是进程资源隔离技术,自然是要放在进程描述符中,下面截取struct task_struct部分:

struct task_struct {
... ...
struct fs_struct *fs;
struct files_struct *files;
struct nsproxy *nsproxy;
... ...
};

至此内核中 pid namespace组织形式有了初步认识,下面介绍下pid namespace的初始化和相关内核API。

调用过程分析

【1】系统 init进程的pid namespace

每一个进程都有自己的namespace(struct nsproxy),可以看做是这个进程自己的“地盘”。进程默认都是共享init进程的namespace,即系统“默认”的根命名空间(目录树、pid等)。pid_namespace按层次组织成一棵树,init进程pid namespace是树的根,对应全局变量 init_pid_ns:

注意,创建进程时,从进程所属的pid_namespace到init_pid_ns 都会分配进程号!

init进程的pid_namespace是在内核初始化阶段创建的。在x86体系结构上,kernel将init进程namespace数据放在了“.data”段上,下面是截取的部分源码:

  • 上面提到的 init_pid_ns 统一放在init进程全局变量init_nsproxy中:
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, //init进程的pid_namespace
#ifdef CONFIG_NET
.net_ns = &init_net,
#endif
#ifdef CONFIG_CGROUPS
.cgroup_ns = &init_cgroup_ns,
#endif
};

init进程的进程描述符是静态定义的,init_nsproxy就放在其中:

struct task_struct
… …
.fs = &init_fs,
.files = &init_files,
.signal = &init_signals,
.sighand = &init_sighand,
.nsproxy = &init_nsproxy, //命名空间代理
… …
}

通过链接脚本,可以看到init进程静态定义的进程描述符放在内核数据段:

//include/asm-generic/vmlinux.lds.h
#define INIT_TASK_DATA(align) \
. = ALIGN(align); \
__start_init_task = .; \
init_thread_union = .; \
init_stack = .; \
KEEP(*(.data..init_task)) \
KEEP(*(.data..init_thread_info)) \
. = __start_init_task + THREAD_SIZE; \
__end_init_task = .; //arch/x86/kernel/vmlinux.lds.S
SECTIONS
{
… …
/* init_task */
INIT_TASK_DATA(THREAD_SIZE)
… …
}

【2】子进程的pid_namespace

进程创建时函数调用栈如下(包含pid namespace创建):

sys_fork / sys_vfork / sys_clone / kernel_thread
└→ _do_fork
└→ copy_process
└→ copy_namespaces
└→ create_new_namespaces
└→ copy_pid_ns

在内核中进程创建核心函数是copy_process(),新建pid namespace的核心函数是copy_pid_ns()。下面依次分析。

  • 首先在进程创建时,会判断是否需要新建pid namespace。copy_process()函数调用copy_namespaces() 函数处理子进程namespace相关,下面截取该函数部分:
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{
struct nsproxy *old_ns = tsk->nsproxy;
struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
struct nsproxy *new_ns; if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
CLONE_NEWPID | CLONE_NEWNET |
CLONE_NEWCGROUP)))) {
get_nsproxy(old_ns); //如果flags中没有CONE_NEW相关新建namespace标记,则继承父进程的namespace
return 0;
}
... ...
new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs); //否则创建新的namespace
... ...
}

这里“get_nsproxy(old_ns) ”比较简单,增加父进程namespace引用计数,大部分子进程都是继承init进程的namespace。下面重点看下如何创建新的namespace。create_new_namespaces() 函数中调用copy_pid_ns() 来新建pid namespace,他的核心是create_pid_namespace() ,源码节选:

static struct pid_namespace *create_pid_namespace(struct user_namespace *user_ns,
struct pid_namespace *parent_pid_ns)
{
struct pid_namespace *ns;
unsigned int level = parent_pid_ns->level + 1; //新创建的pid_namespace在树中新一层
… …
ns = kmem_cache_zalloc(pid_ns_cachep, GFP_KERNEL);
if (ns == NULL)
goto out_dec;
… …
ns->pid_cachep = create_pid_cachep(level); //struct pid结构体高速缓存
ns->ns.ops = &pidns_operations; kref_init(&ns->kref);
ns->level = level;
ns->parent = get_pid_ns(parent_pid_ns); //父pid_namespace
ns->user_ns = get_user_ns(user_ns);
ns->ucounts = ucounts;
ns->pid_allocated = PIDNS_ADDING; //初始化新pid_namespace pid计数
INIT_WORK(&ns->proc_work, proc_cleanup_work);
… …
}

【3】创建子进程pid

如何用标识一个进程呢?对于进程id,虽然用户空间使用一个正整数来表示各种IDs,但是对于内核,我们需要使用“pid namespace,ID number”这样的二元组来表示。因为同样的进程在不同的 PID namespace 中拥有不同的 PID。linux内核使用struct pid结构体来标识进程:

struct pid
{
atomic_t count;
unsigned int level; //该进程所属的pid_ns的level,也就表示了这个pid对象在多少个pid namespace中可见。
/* lists of tasks that use this pid */
struct hlist_head tasks[PIDTYPE_MAX]; //使用该pid结构体的进程描述符集合
struct rcu_head rcu;
struct upid numbers[1]; //存储每层的pid信息的变成数组,长度就是上面的level
};
struct upid {
int nr; //该层pid ns 的PID值
struct pid_namespace *ns; //该层pid ns结构体地址
};

关于标识进程的各个ID详解(pid/tid/tgid/sid等),感兴趣的可以参考《Linux系统如何标识进程?》一文。

在进程创建核心函数copy_process中,通过alloc_pid 函数为子进程申请一个“struct pid”:

//copy_process()
if (pid != &init_struct_pid) { //pid指针是函数入参,普通进程创建时调用的__do_fork中该参数为NULL
pid = alloc_pid(p->nsproxy->pid_ns_for_children);
if (IS_ERR(pid)) {
retval = PTR_ERR(pid);
goto bad_fork_cleanup_thread;
}
}

pid指针是函数入参,普通进程创建时调用的__do_fork函数中,pid参数为NULL,因此一般进程都会调用“alloc_pid”创建新的struct pid:

struct pid *alloc_pid(struct pid_namespace *ns)  //参数是新进程pid ns,返回值是申请的pid结构体
{
struct pid *pid;
enum pid_type type;
int i, nr;
struct pid_namespace *tmp;
struct upid *upid;
int retval = -ENOMEM; pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); //pid_ns 初始化时赋值了pid缓存
if (!pid)
return ERR_PTR(retval); tmp = ns;
pid->level = ns->level; //pid成员level记录新进程pid_ns的level,即进程ns的编号 for (i = ns->level; i >= 0; i--) { //遍历所有父pid_ns,pid结构体内需要记录每层pid_ns分配的pid值
int pid_min = 1; //如果进程的pid_ns是新创建的,则pid值从1开始
… …
nr = idr_alloc_cyclic(&tmp->idr, NULL, pid_min,
pid_max, GFP_ATOMIC);
spin_unlock_irq(&pidmap_lock);
idr_preload_end();
… …
pid->numbers[i].nr = nr; //在pid结构体中记录每层pid_ns分配的pid值
pid->numbers[i].ns = tmp; //记录每层pid_ns分配同时,记录pid值对应进程的pid_ns地址
tmp = tmp->parent;
}
… … upid = pid->numbers + ns->level; //更新每层pid_ns的pid已使用值:pid_allocated
… …
for ( ; upid >= pid->numbers; --upid) {
/* Make the PID visible to find_pid_ns. */
idr_replace(&upid->ns->idr, pid, upid->nr);
upid->ns->pid_allocated++; //更新这层pid_ns的pid已使用值,即直接加一即可
}
spin_unlock_irq(&pidmap_lock); return pid;
… …
}

【4】保存进程ID信息

在解析完子进程pid创建后,下面看看如何保存到进程描述符(task_struct)中。再回到进程创建copy_process函数中,有两处和保存进程ID信息相关:

//in copy_process func:
p->pid = pid_nr(pid); //获取global pid_ns中的pid,即第0层
… …
init_task_pid(p, PIDTYPE_PID, pid); //将struct pid 结构地址保存到进程描述符中
  • 首先task_struct->pid中存的是global pid namespace中的PID value,因为对于内核来说,它只需要看全局的pid namespace即可(init进程),里面包含系统全局进程的global PID。
  • 第二个函数init_task_pid 和相关结构体如下:
static inline void
init_task_pid(struct task_struct *task, enum pid_type type, struct pid *pid)
{
task->pids[type].pid = pid;
} struct task_struct
{
… …
struct pid_link pids[PIDTYPE_MAX];
… …
}
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
/* only valid to __task_pid_nr_ns() */
__PIDTYPE_TGID
};
struct pid_link
{
struct hlist_node node;
struct pid *pid;
};

我们知道在linux中并不区分进程和线程,都是用task_struct来抽象,只不过支持多线程的进程是由一组task_struct来抽象。struct pid 结构可能被多个进程共享(比如表示pgid时),为了既能方便从task struct快速找到对应的struct pid,又能方便从struct pid能够遍历所有使用该pid的task,内核设计了 struct pid_link 来保存各个ID对应的struct pid 结构地址。

【5】获取进程PID value

linux内核提供三个标准API,用于获取进程PID value:

  1. pid_nr(): 获取全局pid_ns pid value,即第0 level,来自init namespace
static inline pid_t pid_nr(struct pid *pid)
{
pid_t nr = 0;
if (pid)
nr = pid->numbers[0].nr;
return nr;
}

2. pid_vnr() :获取当前pid_ns pid value,即进程当前pid namespace

pid_t pid_vnr(struct pid *pid)
{
return pid_nr_ns(pid, task_active_pid_ns(current));
}
EXPORT_SYMBOL_GPL(pid_vnr);

3. pid_nr_ns() :获取指定ns 中的pid value

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
struct upid *upid;
pid_t nr = 0; if (pid && ns->level <= pid->level) {
upid = &pid->numbers[ns->level];
if (upid->ns == ns)
nr = upid->nr;
}
return nr;
}
EXPORT_SYMBOL_GPL(pid_nr_ns);
  • 内核空间系统管理只需要关注“默认”的根命名空间中的PID value即可,因此调用pid_nr在task_struct->pid中缓存的PID value ,称为global PID
  • 用户空间运用namespace进程资源隔离,因此用户空间获取进程PID 的系统调用getpid需要关注pid namespace。相关系统调用源码如下:
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
static inline pid_t task_tgid_vnr(struct task_struct *tsk)
{
return __task_pid_nr_ns(tsk, __PIDTYPE_TGID, NULL);
}
pid_t __task_pid_nr_ns(struct task_struct *task, enum pid_type type,
struct pid_namespace *ns)
{
if (!ns)
ns = task_active_pid_ns(current);
… …
nr = pid_nr_ns(rcu_dereference(task->pids[type].pid), ns);
}
rcu_read_unlock(); return nr;
}

因此用户空间获取的是进程当前pid namespace里的pid value,称为virtual PID

【6】根据pid获取pid_namespace

函数ns_of_pid 用于根据pid获取pid_namespace:

static inline struct pid_namespace *ns_of_pid(struct pid *pid)
{
struct pid_namespace *ns = NULL;
if (pid)
ns = pid->numbers[pid->level].ns;
return ns;
}

docker namespace原理:https://blog.csdn.net/zhonglinzhang/article/details/64441263

namaspace之pid namespace的更多相关文章

  1. 使用独立PID namespace防止误杀进程

    一段错误的代码 首先看一段错误的代码: #!/bin/bash SLICE=100; slppid=1; pidfile=/var/run/vpnrulematch.pid # 停止之前的sleep ...

  2. 子PID namespace中获取父namespace中pid的方法

    在那篇< 使用独立PID namespace防止误杀进程>中的最后,我碰到了一个难题,那就是父PID namespace中的进程无法使用进入子PID namespace中通过echo $$ ...

  3. Linux内核中namespace之PID namespace

    前面看了LInux PCI设备初始化,看得有点晕,就转手整理下之前写的笔记,同时休息一下!!~(@^_^@)~ 这片文章是之前写的,其中参考了某些大牛们的博客!! PID框架的设计 一个框架的设计会考 ...

  4. The Linux Process Principle,NameSpace, PID、TID、PGID、PPID、SID、TID、TTY

    目录 . 引言 . Linux进程 . Linux命名空间 . Linux进程的相关标识 . 进程标识编程示例 . 进程标志在Linux内核中的存储和表现形式 . 后记 0. 引言 在进行Linux主 ...

  5. Linux Namespace : PID

    PID namespace 用来隔离进程的 PID 空间,使得不同 PID namespace 里的进程 PID 可以重复且互不影响.PID namesapce 对容器类应用特别重要, 可以实现容器内 ...

  6. 理解Docker(3):Docker 使用 Linux namespace 隔离容器的运行环境

    本系列文章将介绍Docker的有关知识: (1)Docker 安装及基本用法 (2)Docker 镜像 (3)Docker 容器的隔离性 - 使用 Linux namespace 隔离容器的运行环境 ...

  7. Docker之Linux Namespace

    Linux Namespace 介绍 我们经常听到说Docker 是一个使用了Linux Namespace 和 Cgroups 的虚拟化工具,但是什么是Linux Namespace 它在Docke ...

  8. linux namespace note

    --------------------------------- from http://oldwiki.linux-vserver.org/Namespaces //开源不只是代码,还有思想 Na ...

  9. Docker基础技术:Linux Namespace(下)

    在 Docker基础技术:Linux Namespace(上篇)中我们了解了,UTD.IPC.PID.Mount 四个namespace,我们模仿Docker做了一个相当相当山寨的镜像.在这一篇中,主 ...

随机推荐

  1. requests接口自动化-assert断言

    断言,自动判断接口返回的结果与预期结果是否一致 from common.get_mysql import * def test_assert(): a=0 b=1 # assert a,'断言失败打印 ...

  2. 鸿蒙内核源码分析(重定位篇) | 与国际接轨的对外部发言人 | 百篇博客分析OpenHarmony源码 | v55.01

    百篇博客系列篇.本篇为: v55.xx 鸿蒙内核源码分析(重定位篇) | 与国际接轨的对外部发言人 | 51.c.h.o 加载运行相关篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应用程 ...

  3. [源码解析] PyTorch 流水线并行实现 (4)--前向计算

    [源码解析] PyTorch 流水线并行实现 (4)--前向计算 目录 [源码解析] PyTorch 流水线并行实现 (4)--前向计算 0x00 摘要 0x01 论文 1.1 引论 1.1.1 数据 ...

  4. Angular 的性能优化

    目录 序言 变更检查机制 性能优化原理 性能优化方案 小结 参考 序言 本文将谈一谈 Angular 的性能优化,并且主要介绍与运行时相关的优化.在谈如何优化之前,首先我们需要明确什么样的页面是存在性 ...

  5. .Net Core 实现 自定义Http的Range输出实现断点续传或者分段下载

    一.Http的Range请求头,结合相应头Accept-Ranges.Content-Range 可以实现如下功能: 1.断点续传.用于下载文件被中断后,继续下载. 2.大文件指定区块下载,如视频.音 ...

  6. virtualbox nat 模式下连接虚拟机redis

    主要是使用端口转发的方法 如果你能使用xshell等工具连接这个方法基本一样  接着修改redis.conf文件的69 行(我使用的是5.0)将这里的地址修改为虚拟机的 ip 地址,这里我使用的是备份 ...

  7. 在Unity中渲染一个黑洞

    在Unity中渲染一个黑洞 前言 N年前观看<星际穿越>时,被其中的"卡冈图雅"黑洞所震撼.制作团队表示这是一个最贴近实际的黑洞效果,因为它是通过各种科学理论实现的.当 ...

  8. java 从零开始手写 RPC (01) 基于 websocket 实现

    RPC 解决的问题 RPC 主要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑. 这一节我们来学习下如何基于 we ...

  9. Parcel Fabric Tools(宗地结构工具)

    宗地结构工具 1.图层和表视图 # Process: 创建宗地结构图层 arcpy.MakeParcelFabricLayer_fabric("", 输出图层) # Process ...

  10. 洛谷3703 SDOI2017树点涂色(LCT+线段树+dfs序)

    又一道好题啊qwqqqq 一开始看这个题,还以为是一个树剖的什么毒瘤题目 (不过的确貌似可以用树剖啊) qwq这真是一道\(LCT\)维护颜色的好题 首先,我们来一个一个操作的考虑. 对于操作\(1\ ...