认识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. javascript Date 日期格式化 formatDate, require.js 模块 支持全局js引入 / amd方式加载

    * 引入AMD加载方式: require.js CDN https://cdn.bootcss.com/require.js/2.3.5/require.js *  创建模块文件./js/util/d ...

  2. jmeter设置为中文

    我的jmeter安装路径是在D:\Jmeter\apache-jmeter-5.1.1\bin. 设置中文有2种方法: 1.第一种方法:点击jmeter.bat进入jmeter界面,点击[option ...

  3. 让selenium规避网站的检测

    在使用selenium对某些网站模拟访问的时候会被检测出来,检测出来之后就有可能拿不到我们想要的数据,那么我们怎么可以规避掉呢? 在使用谷歌浏览器的时候我们右键-检查-console-输入window ...

  4. Gitee自动化部署python脚本

    一.前期准备 1.1 安装环境 1.安装python3 2.打开命令行安装selenium pip install selenium 二.python代码 2.1 源码 #!/usr/bin/pyth ...

  5. P6091-[模板]原根

    正题 题目链接:https://www.luogu.com.cn/problem/P6091 题目大意 给出一个数\(p\),求出它的所有在\([0,p]\)的原根. 解题思路 原根的定义,\(\de ...

  6. 深入理解netty---从偶现宕机看netty流量控制

    一.业务背景 目前移动端的使用场景中会用到大量的消息推送,push消息可以帮助运营人员更高效地实现运营目标(比如给用户推送营销活动或者提醒APP新功能). 对于推送系统来说需要具备以下两个特性: 消息 ...

  7. Ubuntu开发相关环境搭建

    一.Ubuntu系统语言环境切换修改 安装时,选择的中文版,但实际使用起来,很不爽,果断切换为英文 1.1 打开终端: vim /etc/default/locale 1.2 修改配置 LANG=&q ...

  8. mybatis: No enum constant org.apache.ibatis.type.JdbcType."VARCHAR"

    mybatis 插入的时候一句sql报错如下. org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ib ...

  9. AtCoder Beginner Contest 221 A~E题解

    目录 A - Seismic magnitude scales B - typo C - Select Mul D - Online games E - LEQ 发挥比较好的一场,就来搓篇题解. F ...

  10. 【深度学习】线性回归(Linear Regression)——原理、均方损失、小批量随机梯度下降

    1. 线性回归 回归(regression)问题指一类为一个或多个自变量与因变量之间关系建模的方法,通常用来表示输入和输出之间的关系. 机器学习领域中多数问题都与预测相关,当我们想预测一个数值时,就会 ...