1.  Linux内核namespace机制

Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于某个特定的Namespace。每个namespace下的资源对于其他namespace下的资源都是透明,不可见的。因此在操作系统层面上看,就会出现多个相同pid的进程。系统中可以同时存在两个进程号为0,1,2的进程,由于属于不同的namespace,所以它们之间并不冲突。而在用户层面上只能看到属于用户自己namespace下的资源,例如使用ps命令只能列出自己namespace下的进程。这样每个namespace看上去就像一个单独的Linux系统。

2 .  Linux内核中namespace结构体

在Linux内核中提供了多个namespace,其中包括fs (mount), uts, network, sysvipc, 等。一个进程可以属于多个namesapce,既然namespace和进程相关,那么在task_struct结构体中就会包含和namespace相关联的变量。在task_struct 结构中有一个指向namespace结构体的指针nsproxy。

struct task_struct {

……..

/* namespaces */

struct nsproxy *nsproxy;

…….

}

再看一下nsproxy是如何定义的,在include/linux/nsproxy.h文件中,这里一共定义了5个各自的命名空间结构体,在该结构体中定义了5个指向各个类型namespace的指针,由于多个进程可以使用同一个namespace,所以nsproxy可以共享使用,count字段是该结构的引用计数。

/* 'count' is the number of tasks holding a reference.

* The count for each namespace, then, will be the number

* of nsproxies pointing to it, not the number of tasks.

* The nsproxy is shared by tasks which share all namespaces.

* As soon as a single namespace is cloned or unshared, the

* nsproxy is copied

*/

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;

};

(1)     UTS命名空间包含了运行内核的名称、版本、底层体系结构类型等信息。UTS是UNIX Timesharing System的简称。

(2)     保存在struct ipc_namespace中的所有与进程间通信(IPC)有关的信息。

(3)     已经装载的文件系统的视图,在struct mnt_namespace中给出。

(4)     有关进程ID的信息,由struct pid_namespace提供。

(5)     struct net_ns包含所有网络相关的命名空间参数。

系统中有一个默认的nsproxy,init_nsproxy,该结构在task初始化是也会被初始化。#define INIT_TASK(tsk)  \

{

.nsproxy   = &init_nsproxy,

}

其中init_nsproxy的定义为:

static struct kmem_cache *nsproxy_cachep;

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

};

对于         .mnt_ns   没有进行初始化,其余的namespace都进行了系统默认初始。

3. 使用clone创建自己的Namespace

如果要创建自己的命名空间,可以使用系统调用clone(),它在用户空间的原型为

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg)

这里fn是函数指针,这个就是指向函数的指针,, child_stack是为子进程分配系统堆栈空间,flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数也就是fn指向的函数参数。下面是flags可以取的值。这里只关心和namespace相关的参数。

CLONE_FS          子进程与父进程共享相同的文件系统,包括root、当前目录、umask

CLONE_NEWNS     当clone需要自己的命名空间时设置这个标志,不能同时设置CLONE_NEWS和CLONE_FS。

Clone()函数是在libc库中定义的一个封装函数,它负责建立新轻量级进程的堆栈并且调用对编程者隐藏了clone系统条用。实现clone()系统调用的sys_clone()服务例程并没有fn和arg参数。封装函数把fn指针存放在子进程堆栈的每个位置处,该位置就是该封装函数本身返回地址存放的位置。Arg指针正好存放在子进程堆栈中的fn的下面。当封装函数结束时,CPU从堆栈中取出返回地址,然后执行fn(arg)函数。

#include       int clone(int (*fn)(void *), void *child_stack,                 int flags, void *arg, ...                 /* pid_t *ptid, struct user_desc *tls, pid_t *ctid

long clone(unsigned long flags, void *child_stack,                 void *ptid, void *ctid,                 struct pt_regs *regs);<span color:#181818;"="" style="word-wrap: break-word; font-size: 12pt;">

我们在Linux内核中看到的实现函数,是经过libc库进行封装过的,在Linux内核中的fork.c文件中,有下面的定义,最终调用的都是do_fork()函数。

#ifdef __ARCH_WANT_SYS_CLONE

#ifdef CONFIG_CLONE_BACKWARDS

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,

int __user *, parent_tidptr,

int, tls_val,

int __user *, child_tidptr)

#elif defined(CONFIG_CLONE_BACKWARDS2)

SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,

int __user *, parent_tidptr,

int __user *, child_tidptr,

int, tls_val)

#elif defined(CONFIG_CLONE_BACKWARDS3)

SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,

int, stack_size,

int __user *, parent_tidptr,

int __user *, child_tidptr,

int, tls_val)

#else

SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,

int __user *, parent_tidptr,

int __user *, child_tidptr,

int, tls_val)

#endif

{

return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);

}

#endif

3.1  do_fork函数

在clone()函数中调用do_fork函数进行真正的处理,在do_fork函数中调用copy_process进程处理。

long do_fork(unsigned long clone_flags,

unsigned long stack_start,

unsigned long stack_size,

int __user *parent_tidptr,

int __user *child_tidptr)

{

struct task_struct *p;

int trace = 0;

long nr;

/*

* Determine whether and which event to report to ptracer.  When

* called from kernel_thread or CLONE_UNTRACED is explicitly

* requested, no event is reported; otherwise, report if the event

* for the type of forking is enabled.

*/

if (!(clone_flags & CLONE_UNTRACED)) {

if (clone_flags & CLONE_VFORK)

trace = PTRACE_EVENT_VFORK;

else if ((clone_flags & CSIGNAL) != SIGCHLD)

trace = PTRACE_EVENT_CLONE;

else

trace = PTRACE_EVENT_FORK;

if (likely(!ptrace_event_enabled(current, trace)))

trace = 0;

}

p = copy_process(clone_flags, stack_start, stack_size,

child_tidptr, NULL, trace);

/*

* Do this prior waking up the new thread - the thread pointer

* might get invalid after that point, if the thread exits quickly.

*/

if (!IS_ERR(p)) {

struct completion vfork;

struct pid *pid;

trace_sched_process_fork(current, p);

pid = get_task_pid(p, PIDTYPE_PID);

nr = pid_vnr(pid);

if (clone_flags & CLONE_PARENT_SETTID)

put_user(nr, parent_tidptr);

if (clone_flags & CLONE_VFORK) {

p->vfork_done = &vfork;

init_completion(&vfork);

get_task_struct(p);

}

wake_up_new_task(p);

/* forking complete and child started to run, tell ptracer */

if (unlikely(trace))

ptrace_event_pid(trace, pid);

if (clone_flags & CLONE_VFORK) {

if (!wait_for_vfork_done(p, &vfork))

ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);

}

put_pid(pid);

} else {

nr = PTR_ERR(p);

}

return nr;

}

3.2  copy_process函数

在copy_process函数中调用copy_namespaces函数。

static struct task_struct *copy_process(unsigned long clone_flags,

unsigned long stack_start,

unsigned long stack_size,

int __user *child_tidptr,

struct pid *pid,

int trace)

{

int retval;

struct task_struct *p;

/*下面的代码是对clone_flag标志进行检查,有部分表示是互斥的,例如CLONE_NEWNS和CLONENEW_FS*/

if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))

return ERR_PTR(-EINVAL);

if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))

return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))

return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))

return ERR_PTR(-EINVAL);

if ((clone_flags & CLONE_PARENT) &&

current->signal->flags & SIGNAL_UNKILLABLE)

return ERR_PTR(-EINVAL);

……

retval = copy_namespaces(clone_flags, p);

if (retval)

goto bad_fork_cleanup_mm;

retval = copy_io(clone_flags, p);

if (retval)

goto bad_fork_cleanup_namespaces;

retval = copy_thread(clone_flags, stack_start, stack_size, p);

if (retval)

goto bad_fork_cleanup_io;

/*do_fork中调用copy_process函数,该函数中pid参数为NULL,所以这里的if判断是成立的。为进程所在的namespace分配pid,在3.0的内核之前还有一个关键函数,就是namespace创建后和cgroup的关系,

if (current->nsproxy != p->nsproxy) {

retval = ns_cgroup_clone(p, pid);

if (retval)

goto bad_fork_free_pid;

但在3.0内核以后给删掉了,具体请参考remove the ns_cgroup*/

if (pid != &init_struct_pid) {

retval = -ENOMEM;

pid = alloc_pid(p->nsproxy->pid_ns_for_children);

if (!pid)

goto bad_fork_cleanup_io;

}…..

}

3.3  copy_namespaces 函数

在kernel/nsproxy.c文件中定义了copy_namespaces函数。

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;

/*首先检查flag,如果flag标志不是下面的五种之一,就会调用get_nsproxy对old_ns递减引用计数,然后直接返回0*/

if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |

CLONE_NEWPID | CLONE_NEWNET)))) {

get_nsproxy(old_ns);

return 0;

}

/*当前进程是否有超级用户的权限*/

if (!ns_capable(user_ns, CAP_SYS_ADMIN))

return -EPERM;

/*

* CLONE_NEWIPC must detach from the undolist: after switching

* to a new ipc namespace, the semaphore arrays from the old

* namespace are unreachable.  In clone parlance, CLONE_SYSVSEM

* means share undolist with parent, so we must forbid using

* it along with CLONE_NEWIPC.

对CLONE_NEWIPC进行特殊的判断,*/

if ((flags & (CLONE_NEWIPC | CLONE_SYSVSEM)) ==

(CLONE_NEWIPC | CLONE_SYSVSEM))

return -EINVAL;

/*为进程创建新的namespace*/

new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs);

if (IS_ERR(new_ns))

return  PTR_ERR(new_ns);

tsk->nsproxy = new_ns;

return 0;

}

3.4  create_new_namespaces函数

create_new_namespaces创建新的namespace

static struct nsproxy *create_new_namespaces(unsigned long flags,

struct task_struct *tsk, struct user_namespace *user_ns,

struct fs_struct *new_fs)

{

struct nsproxy *new_nsp;

int err;

/*为新的nsproxy分配内存空间,并对其引用计数设置为初始1*/

new_nsp = create_nsproxy();

if (!new_nsp)

return ERR_PTR(-ENOMEM);

/*如果Namespace中的各个标志位进行了设置,则会调用相应的namespace进行创建*/

new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);

if (IS_ERR(new_nsp->mnt_ns)) {

err = PTR_ERR(new_nsp->mnt_ns);

goto out_ns;

}

new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);

if (IS_ERR(new_nsp->uts_ns)) {

err = PTR_ERR(new_nsp->uts_ns);

goto out_uts;

}

new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);

if (IS_ERR(new_nsp->ipc_ns)) {

err = PTR_ERR(new_nsp->ipc_ns);

goto out_ipc;

}

new_nsp->pid_ns_for_children =

copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);

if (IS_ERR(new_nsp->pid_ns_for_children)) {

err = PTR_ERR(new_nsp->pid_ns_for_children);

goto out_pid;

}

new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns);

if (IS_ERR(new_nsp->net_ns)) {

err = PTR_ERR(new_nsp->net_ns);

goto out_net;

}

return new_nsp;

out_net:

if (new_nsp->pid_ns_for_children)

put_pid_ns(new_nsp->pid_ns_for_children);

out_pid:

if (new_nsp->ipc_ns)

put_ipc_ns(new_nsp->ipc_ns);

out_ipc:

if (new_nsp->uts_ns)

put_uts_ns(new_nsp->uts_ns);

out_uts:

if (new_nsp->mnt_ns)

put_mnt_ns(new_nsp->mnt_ns);

out_ns:

kmem_cache_free(nsproxy_cachep, new_nsp);

return ERR_PTR(err);

}

3.4.1 create_nsproxy函数

static inline struct nsproxy *create_nsproxy(void)

{

struct nsproxy *nsproxy;

nsproxy = kmem_cache_alloc(nsproxy_cachep, GFP_KERNEL);

if (nsproxy)

atomic_set(&nsproxy->count, 1);

return nsproxy;

}

例子1:namespace pid的例子

#include

#include

#include

#include

#include

#include

#include

static int fork_child(void *arg)

{

int a = (int)arg;

int i;

pid_t pid;

char *cmd  = "ps -el;

printf("In the container, my pid is: %d\n", getpid());

/*ps命令是解析procfs的内容得到结果的,而procfs根目录的进程pid目录是基于mount当时的pid namespace的,这个在procfs的get_sb回调中体现的。因此只需要重新mount一下proc, mount -t proc proc /proc*/

mount("proc", "/proc", "proc", 0, "");

for (i = 0; i<a; i++)="" {<="" span="" style="word-wrap: break-word;">

pid = fork();

if (pid <0)

return pid;

else if (pid)

printf("pid of my child is %d\n", pid);

else if (pid == 0) {

sleep(30);

exit(0);

}

}

execl("/bin/bash", "/bin/bash","-c",cmd, NULL);

return 0;

}

int main(int argc, char *argv[])

{

int cpid;

void *childstack, *stack;

int flags;

int ret = 0;

int stacksize = getpagesize() * 4;

if (argc != 2) {

fprintf(stderr, "Wrong usage.\n");

return -1;

}

stack = malloc(stacksize);

if(stack == NULL)

{

return -1;

}

printf("Out of the container, my pid is: %d\n", getpid());

childstack = stack + stacksize;

flags = CLONE_NEWPID | CLONE_NEWNS;

cpid = clone(fork_child, childstack, flags, (void *)atoi(argv[1]));

printf("cpid: %d\n", cpid);

if (cpid <0) {

perror("clone");

ret = -1;

goto out;

}

fprintf(stderr, "Parent sleeping 20 seconds\n");

sleep(20);

ret = 0;

out:

free(stack);

return ret;

}

}运行结果:

root@ubuntu:~/c_program# ./namespace 7

Out of the container, my pid is: 8684

cpid: 8685

Parent sleeping 20 seconds

In the container, my pid is: 1

pid of my child is 2

pid of my child is 3

pid of my child is 4

pid of my child is 5

pid of my child is 6

pid of my child is 7

pid of my child is 8

F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD

4 R     0     1     0  0  80   0 -  1085 -      pts/0    00:00:00 ps

1 S     0     2     1  0  80   0 -   458 hrtime pts/0    00:00:00 namespace

1 S     0     3     1  0  80   0 -   458 hrtime pts/0    00:00:00 namespace

1 S     0     4     1  0  80   0 -   458 hrtime pts/0    00:00:00 namespace

1 S     0     5     1  0  80   0 -   458 hrtime pts/0    00:00:00 namespace

1 S     0     6     1  0  80   0 -   458 hrtime pts/0    00:00:00 namespace

1 S     0     7     1  0  80   0 -   458 hrtime pts/0    00:00:00 namespace

1 S     0     8     1  0  80   0 -   458 hrtime pts/0    00:00:00 namespace

例子2:UTS的例子

#define _GNU_SOURCE

#include

#include

#include

#include

#include

#include

#include

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \

} while (0)

static int              /* Start function for cloned child */

childFunc(void *arg)

{

struct utsname uts;

/* Change hostname in UTS namespace of child */

if (sethostname(arg, strlen(arg)) == -1)

errExit("sethostname");

/* Retrieve and display hostname */

if (uname(&uts) == -1)

errExit("uname");

printf("uts.nodename in child:  %s\n", uts.nodename);

/* Keep the namespace open for a while, by sleeping.

*               This allows some experimentation--for example, another

*                             process might join the namespace. */

sleep(200);

return 0;           /* Child terminates now */

}

#define STACK_SIZE (1024 * 1024)    /* Stack size for cloned child */

int

main(int argc, char *argv[])

{

char *stack;                    /* Start of stack buffer */

char *stackTop;                 /* End of stack buffer */

pid_t pid;

struct utsname uts;

if (argc < 2) {

fprintf(stderr, "Usage: %s \n", argv[0]);

exit(EXIT_SUCCESS);

}

/* Allocate stack for child */

stack = malloc(STACK_SIZE);

if (stack == NULL)

errExit("malloc");

stackTop = stack + STACK_SIZE;  /* Assume stack grows downward */

/* Create child that has its own UTS namespace;

*               child commences execution in childFunc() */

pid = clone(childFunc, stackTop, CLONE_NEWUTS | SIGCHLD, argv[1]);

if (pid == -1)

errExit("clone");

printf("clone() returned %ld\n", (long) pid);

/* Parent falls through to here */

sleep(1);           /* Give child time to change its hostname */

/* Display hostname in parent's UTS namespace. This will be

*               different from hostname in child's UTS namespace. */

if (uname(&uts) == -1)

errExit("uname");

printf("uts.nodename in parent: %s\n", uts.nodename);

if (waitpid(pid, NULL, 0) == -1)    /* Wait for child */

errExit("waitpid");

printf("child has terminated\n");

exit(EXIT_SUCCESS);

}

root@ubuntu:~/c_program# ./namespace_1 test

clone() returned 4101

uts.nodename in child:  test

uts.nodename in parent: ubuntu

对于网络命名空间可以参考:

http://www.opencloudblog.com/?p=42

http://wenx05124561.blog.163.com/blog/static/124000805201311250241189/

http://man7.org/linux/man-pages/man2/clone.2.html

Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于特定的Namespace。每个Namespace里面的资源对其他Namespace都是透明的。要创建新的Namespace,只需要在调用clone时指定相应的flag。Linux Namespaces机制为实现基于容器的虚拟化技术提供了很好的基础,LXC(Linux containers)就是利用这一特性实现了资源的隔离。不同container内的进程属于不同的Namespace,彼此透明,互不干扰。下面我们就从clone系统调用的flag出发,来介绍各个Namespace。

命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性。该机制类似于Solaris中的zone或 FreeBSD中的jail。对该概念做一般概述之后,我将讨论命名空间框架所提供的基础设施。

1. 概念

传统上,在Linux以及其他衍生的UNIX变体中,许多资源是全局管理的。例如,系统中的所有进程按照惯例是通过PID标识的,这意味着内核必须管理一个全局的PID列表。而且,所有调用者通过uname系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。用户ID的管理方式类似,即各个用户是通过一个全局唯一的UID号标识。

全局ID使得内核可以有选择地允许或拒绝某些特权。虽然UID为0的root用户基本上允许做任何事,但其他用户ID则会受到限制。例如UID为n 的用户,不允许杀死属于用户m的进程(m≠ n)。但这不能防止用户看到彼此,即用户n可以看到另一个用户m也在计算机上活动。只要用户只能操纵他们自己的进程,这就没什么问题,因为没有理由不允许用户看到其他用户的进程。

但有些情况下,这种效果可能是不想要的。如果提供Web主机的供应商打算向用户提供Linux计算机的全部访问权限,包括root权限在内。传统上,这需要为每个用户准备一台计算机,代价太高。使用KVM或VMWare提供的虚拟化环境是一种解决问题的方法,但资源分配做得不是非常好。计算机的各个用户都需要一个独立的内核,以及一份完全安装好的配套的用户层应用。

命名空间提供了一种不同的解决方案,所需资源较少。在虚拟化的系统中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。例如,容器可以设置为使用自身的PID集合,但仍然与其他容器共享部分文件系统。

本质上,命名空间建立了系统的不同视图。此前的每一项全局资源都必须包装到容器数据结构中,只有资源和包含资源的命名空间构成的二元组仍然是全局唯一的。虽然在给定容器内部资源是自足的,但无法提供在容器外部具有唯一性的ID。

考虑系统上有3个不同命名空间的情况。命名空间可以组织为层次,我会在这里讨论这种情况。一个命名空间是父命名空间,衍生了两个子命名空间。假定容器用于虚拟主机配置中,其中的每个容器必须看起来像是单独的一台Linux计算机。因此其中每一个都有自身的init进程,PID为0,其他进程的PID 以递增次序分配。两个子命名空间都有PID为0的init进程,以及PID分别为2和3的两个进程。由于相同的PID在系统中出现多次,PID号不是全局唯一的。

虽然子容器不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程。图中子容器的进程映射到父容器中,PID为4到 9。尽管系统上有9个进程,但却需要15个PID来表示,因为一个进程可以关联到多个PID。至于哪个PID是"正确"的,则依赖于具体的上下文。

如果命名空间包含的是比较简单的量,也可以是非层次的,例如下文讨论的UTS命名空间。在这种情况下,父子命名空间之间没有联系。

请注意,Linux系统对简单形式的命名空间的支持已经有很长一段时间了,主要是chroot系统调用。该方法可以将进程限制到文件系统的某一部分,因而是一种简单的命名空间机制。但真正的命名空间能够控制的功能远远超过文件系统视图。

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

(1) 在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。

(2) unshare系统调用将进程的某些部分从父进程分离,其中也包括命名空间。更多信息请参见手册页unshare(2)。

在进程已经使用上述的两种机制之一从父进程命名空间分离后,从该进程的角度来看,改变全局属性不会传播到父进程命名空间,而父进程的修改也不会传播到子进 程,至少对于简单的量是这样。而对于文件系统来说,情况就比较复杂,其中的共享机制非常强大,带来了大量的可能性。在标准内核中命名空间当前仍然标记为试 验性的,为使内核的所有部分都能够感知到命名空间,相关开发仍然在进行中。但就内核版本2.6.24而言,基本的框架已经建立就绪。 当前的实现仍然存在一些问题,相关的信息可以参见Documentation/namespaces/compatibility-list.txt文件。

2. 实现

命名空间的实现需要两个部分:每个子系统的命名空间结构,将此前所有的全局组件包装到命名空间中;将给定进程关联到所属各个命名空间的机制。图 2说明了具体情形。

<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;
};

PID Namespace

当调用clone时,设定了CLONE_NEWPID,就会创建一个新的PID Namespace,clone出来的新进程将成为Namespace里的第一个进程。一个PID Namespace为进程提供了一个独立的PID环境,PID Namespace内的PID将从1开始,在Namespace内调用fork,vfork或clone都将产生一个在该Namespace内独立的PID。新创建的Namespace里的第一个进程在该Namespace内的PID将为1,就像一个独立的系统里的init进程一样。该Namespace内的孤儿进程都将以该进程为父进程,当该进程被结束时,该Namespace内所有的进程都会被结束。PID Namespace是层次性,新创建的Namespace将会是创建该Namespace的进程属于的Namespace的子Namespace。子Namespace中的进程对于父Namespace是可见的,一个进程将拥有不止一个PID,而是在所在的Namespace以及所有直系祖先Namespace中都将有一个PID。系统启动时,内核将创建一个默认的PID Namespace,该Namespace是所有以后创建的Namespace的祖先,因此系统所有的进程在该Namespace都是可见的。

IPC Namespace

当调用clone时,设定了CLONE_NEWIPC,就会创建一个新的IPC Namespace,clone出来的进程将成为Namespace里的第一个进程。一个IPC Namespace有一组System V IPC objects 标识符构成,这标识符有IPC相关的系统调用创建。在一个IPC Namespace里面创建的IPC object对该Namespace内的所有进程可见,但是对其他Namespace不可见,这样就使得不同Namespace之间的进程不能直接通信,就像是在不同的系统里一样。当一个IPC Namespace被销毁,该Namespace内的所有IPC object会被内核自动销毁。

PID Namespace和IPC Namespace可以组合起来一起使用,只需在调用clone时,同时指定CLONE_NEWPID和CLONE_NEWIPC,这样新创建的Namespace既是一个独立的PID空间又是一个独立的IPC空间。不同Namespace的进程彼此不可见,也不能互相通信,这样就实现了进程间的隔离。

mount Namespace

当调用clone时,设定了CLONE_NEWNS,就会创建一个新的mount Namespace。每个进程都存在于一个mount Namespace里面,mount Namespace为进程提供了一个文件层次视图。如果不设定这个flag,子进程和父进程将共享一个mount Namespace,其后子进程调用mount或umount将会影响到所有该Namespace内的进程。如果子进程在一个独立的mount Namespace里面,就可以调用mount或umount建立一份新的文件层次视图。该flag配合pivot_root系统调用,可以为进程创建一个独立的目录空间。

Network Namespace

当调用clone时,设定了CLONE_NEWNET,就会创建一个新的Network Namespace。一个Network Namespace为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口,IPv4和IPv6协议栈,IP路由表,防火墙规则,sockets等等。一个Network Namespace提供了一份独立的网络环境,就跟一个独立的系统一样。一个物理设备只能存在于一个Network Namespace中,可以从一个Namespace移动另一个Namespace中。虚拟网络设备(virtual network device)提供了一种类似管道的抽象,可以在不同的Namespace之间建立隧道。利用虚拟化网络设备,可以建立到其他Namespace中的物理设备的桥接。当一个Network Namespace被销毁时,物理设备会被自动移回init Network Namespace,即系统最开始的Namespace。

UTS Namespace

当调用clone时,设定了CLONE_NEWUTS,就会创建一个新的UTS Namespace。一个UTS Namespace就是一组被uname返回的标识符。新的UTS Namespace中的标识符通过复制调用进程所属的Namespace的标识符来初始化。Clone出来的进程可以通过相关系统调用改变这些标识符,比如调用sethostname来改变该Namespace的hostname。这一改变对该Namespace内的所有进程可见。CLONE_NEWUTS和CLONE_NEWNET一起使用,可以虚拟出一个有独立主机名和网络空间的环境,就跟网络上一台独立的主机一样。

以上所有clone flag都可以一起使用,为进程提供了一个独立的运行环境。LXC正是通过clone时设定这些flag,为进程创建一个有独立PID,IPC,FS,Network,UTS空间的container。一个container就是一个虚拟的运行环境,对container里的进程是透明的,它会以为自己是直接在一个系统上运行的。一个container就像传统虚拟化技术里面的一台安装了OS的虚拟机,但是开销更小,部署更为便捷。

Linux Namespaces机制本身就是为了实现 container based virtualizaiton开发的。它提供了一套轻量级、高效率的系统资源隔离方案,远比传统的虚拟化技术开销小,不过它也不是完美的,它为内核的开发带来了更多的复杂性,它在隔离性和容错性上跟传统的虚拟化技术比也还有差距。

2.3.2 命名空间(1)

命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性。该机制类似于Solaris中的zone或 FreeBSD中的jail。对该概念做一般概述之后,我将讨论命名空间框架所提供的基础设施。

1. 概念

传统上,在Linux以及其他衍生的UNIX变体中,许多资源是全局管理的。例如,系统中的所有进程按照惯例是通过PID标识的,这意味着内核必须管理一个全局的PID列表。而且,所有调用者通过uname系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。用户ID的管理方式类似,即各个用户是通过一个全局唯一的UID号标识。

全局ID使得内核可以有选择地允许或拒绝某些特权。虽然UID为0的root用户基本上允许做任何事,但其他用户ID则会受到限制。例如UID为n 的用户,不允许杀死属于用户m的进程(m≠ n)。但这不能防止用户看到彼此,即用户n可以看到另一个用户m也在计算机上活动。只要用户只能操纵他们自己的进程,这就没什么问题,因为没有理由不允许用户看到其他用户的进程。

但有些情况下,这种效果可能是不想要的。如果提供Web主机的供应商打算向用户提供Linux计算机的全部访问权限,包括root权限在内。传统上,这需要为每个用户准备一台计算机,代价太高。使用KVM或VMWare提供的虚拟化环境是一种解决问题的方法,但资源分配做得不是非常好。计算机的各个用户都需要一个独立的内核,以及一份完全安装好的配套的用户层应用。

命名空间提供了一种不同的解决方案,所需资源较少。在虚拟化的系统中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。例如,容器可以设置为使用自身的PID集合,但仍然与其他容器共享部分文件系统。

本质上,命名空间建立了系统的不同视图。此前的每一项全局资源都必须包装到容器数据结构中,只有资源和包含资源的命名空间构成的二元组仍然是全局唯一的。虽然在给定容器内部资源是自足的,但无法提供在容器外部具有唯一性的ID。图2-3给出了此情况的一个概述。

 
图2-3 命名空间可以按层次关联起来。 每个命名空间都发
源于一个父命名空间,一个父命名空间可以有多个子命名空间

考虑系统上有3个不同命名空间的情况。命名空间可以组织为层次,我会在这里讨论这种情况。一个命名空间是父命名空间,衍生了两个子命名空间。假定容器用于虚拟主机配置中,其中的每个容器必须看起来像是单独的一台Linux计算机。因此其中每一个都有自身的init进程,PID为0,其他进程的PID 以递增次序分配。两个子命名空间都有PID为0的init进程,以及PID分别为2和3的两个进程。由于相同的PID在系统中出现多次,PID号不是全局唯一的。

虽然子容器不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程。图中子容器的进程映射到父容器中,PID为4到 9。尽管系统上有9个进程,但却需要15个PID来表示,因为一个进程可以关联到多个PID。至于哪个PID是"正确"的,则依赖于具体的上下文。

如果命名空间包含的是比较简单的量,也可以是非层次的,例如下文讨论的UTS命名空间。在这种情况下,父子命名空间之间没有联系。

请注意,Linux系统对简单形式的命名空间的支持已经有很长一段时间了,主要是chroot系统调用。该方法可以将进程限制到文件系统的某一部分,因而是一种简单的命名空间机制。但真正的命名空间能够控制的功能远远超过文件系统视图。

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

(1) 在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。

(2) unshare系统调用将进程的某些部分从父进程分离,其中也包括命名空间。更多信息请参见手册页unshare(2)。

在进程已经使用上述的两种机制之一从父进程命名空间分离后,从该进程的角度来看,改变全局属性不会传播到父进程命名空间,而父进程的修改也不会传播到子进程,至少对于简单的量是这样。而对于文件系统来说,情况就比较复杂,其中的共享机制非常强大,带来了大量的可能性,具体的情况会在第8章讨论。

在标准内核中命名空间当前仍然标记为试验性的,为使内核的所有部分都能够感知到命名空间,相关开发仍然在进行中。但就内核版本2.6.24而言,基本的框架已经建立就绪。 当前的实现仍然存在一些问题,相关的信息可以参见Documentation/namespaces/compatibility-list.txt文件。

2. 实现

命名空间的实现需要两个部分:每个子系统的命名空间结构,将此前所有的全局组件包装到命名空间中;将给定进程关联到所属各个命名空间的机制。图 2-4说明了具体情形。

 
(点击查看大图)图2-4 进程和命名空间之间的联系

2.3.2 命名空间(2)

子系统此前的全局属性现在封装到命名空间中,每个进程关联到一个选定的命名空间。每个可以感知命名空间的内核子系统都必须提供一个数据结构,将所有通过命名空间形式提供的对象集中起来。struct nsproxy用于汇集指向特定于子系统的命名空间包装器的指针:

  1. <nsproxy.h>
  2. struct nsproxy {
  3. atomic_t count;
  4. struct uts_namespace *uts_ns;
  5. struct ipc_namespace *ipc_ns;
  6. struct mnt_namespace *mnt_ns;
  7. struct pid_namespace *pid_ns;
  8. struct user_namespace *user_ns;
  9. struct net *net_ns;
  10. };

当前内核的以下范围可以感知到命名空间。

UTS命名空间包含了运行内核的名称、版本、底层体系结构类型等信息。UTS是UNIX Timesharing System的简称。

保存在struct ipc_namespace中的所有与进程间通信(IPC)有关的信息。

已经装载的文件系统的视图,在struct mnt_namespace中给出。

有关进程ID的信息,由struct pid_namespace提供。

struct user_namespace保存的用于限制每个用户资源使用的信息。

struct net_ns包含所有网络相关的命名空间参数。读者在第12章中会看到,为使网络相关的内核代码能够完全感知命名空间,还有许多工作需要完成。

当我讨论相应的子系统时,会介绍各个命名空间容器的内容。在本章中,我们主要讲解UTS和用户命名空间。由于在创建新进程时可使用fork建立一个新的命名空间,因此必须提供控制该行为的适当的标志。每个命名空间都有一个对应的标志:

  1. <sched.h>
  2. #define CLONE_NEWUTS    0x04000000      /* 创建新的utsname组 */
  3. #define CLONE_NEWIPC    0x08000000      /* 创建新的IPC命名空间  */
  4. #define CLONE_NEWUSER   0x10000000      /* 创建新的用户命名空间   */
  5. #define CLONE_NEWPID    0x20000000      /* 创建新的PID命名空间  */
  6. #define CLONE_NEWNET    0x40000000      /* 创建新的网络命名空间   */

每个进程都关联到自身的命名空间视图:

  1. <sched.h>
  2. struct task_struct {
  3. ...
  4. /* 命名空间 */
  5. struct nsproxy *nsproxy;
  6. ...
  7. }

因为使用了指针,多个进程可以共享一组子命名空间。这样,修改给定的命名空间,对所有属于该命名空间的进程都是可见的。

请注意,对命名空间的支持必须在编译时启用,而且必须逐一指定需要支持的命名空间。但对命名空间的一般性支持总是会编译到内核中。 这使得内核不管有无命名空间,都不必使用不同的代码。除非指定不同的选项,否则每个进程都会关联到一个默认命名空间,这样可感知命名空间的代码总是可以使用。但如果内核编译时没有指定对具体命名空间的支持,默认命名空间的作用则类似于不启用命名空间,所有的属性都相当于全局的。

init_nsproxy定义了初始的全局命名空间,其中维护了指向各子系统初始的命名空间对象的指针:

  1. <kernel/nsproxy.c>
  2. struct nsproxy init_nsproxy = INIT_NSPROXY(init_nsproxy);
  3. <init_task.h>
  4. #define INIT_NSPROXY(nsproxy) { \
  5. .pid_ns = &init_pid_ns, \
  6. .count = ATOMIC_INIT(1), \
  7. .uts_ns = &init_uts_ns, \
  8. .mnt_ns = NULL, \
  9. INIT_NET_NS(net_ns) \
  10. INIT_IPC_NS(ipc_ns) \
  11. .user_ns = &init_user_ns, \
  12. }

UTS命名空间

UTS命名空间几乎不需要特别的处理,因为它只需要简单量,没有层次组织。所有相关信息都汇集到下列结构的一个实例中:

  1. <utsname.h>
  2. struct uts_namespace {
  3. struct kref kref;
  4. struct new_utsname name;
  5. };

kref是一个嵌入的引用计数器,可用于跟踪内核中有多少地方使用了struct uts_namespace的实例(回想第1章,其中讲述了更多有关处理引用计数的一般框架信息)。uts_namespace所提供的属性信息本身包含在struct new_utsname中:

  1. <utsname.h>
  2. struct new_utsname {
  3. char sysname[65];
  4. char nodename[65];
  5. char release[65];
  6. char version[65];
  7. char machine[65];
  8. char domainname[65];
  9. };

各个字符串分别存储了系统的名称(Linux...)、内核发布版本、机器名,等等。使用uname工具可以取得这些属性的当前值,也可以在 /proc/sys/kernel/中看到:

  1. wolfgang@meitner> cat /proc/sys/kernel/ostype
  2. Linux
  3. wolfgang@meitner> cat /proc/sys/kernel/osrelease
  4. 2.6.24

初始设置保存在init_uts_ns中:

  1. init/version.c
  2. struct uts_namespace init_uts_ns = {
  3. ...
  4. .name = {
  5. .sysname = UTS_SYSNAME,
  6. .nodename = UTS_NODENAME,
  7. .release = UTS_RELEASE,
  8. .version = UTS_VERSION,
  9. .machine = UTS_MACHINE,
  10. .domainname = UTS_DOMAINNAME,
  11. },
  12. };

相关的预处理器常数在内核中各处定义。例如,UTS_RELEASE在<utsrelease.h>中定义,该文件是连编时通过顶层 Makefile动态生成的。

请注意,UTS结构的某些部分不能修改。例如,把sysname换成Linux以外的其他值是没有意义的,但改变机器名是可以的。

内核如何创建一个新的UTS命名空间呢?这属于copy_utsname函数的职责。在某个进程调用fork并通过CLONE_NEWUTS标志指定创建新的UTS命名空间时,则调用该函数。在这种情况下,会生成先前的uts_namespace实例的一份副本,当前进程的nsproxy实例内部的指针会指向新的副本。如此而已!由于在读取或设置UTS属性值时,内核会保证总是操作特定于当前进程的uts_namespace实例,在当前进程修改 UTS属性不会反映到父进程,而父进程的修改也不会传播到子进程。

用户命名空间

用户命名空间在数据结构管理方面类似于UTS:在要求创建新的用户命名空间时,则生成当前用户命名空间的一份副本,并关联到当前进程的 nsproxy实例。但用户命名空间自身的表示要稍微复杂一些:

  1. <user_namespace.h>
  2. struct user_namespace {
  3. struct kref kref;
  4. struct hlist_head uidhash_table[UIDHASH_SZ];
  5. struct user_struct *root_user;
  6. };

如前所述,kref是一个引用计数器,用于跟踪多少地方需要使用user_namespace实例。对命名空间中的每个用户,都有一个struct user_struct的实例负责记录其资源消耗,各个实例可通过散列表uidhash_table访问。

对我们来说user_struct的精确定义是无关紧要的。只要知道该结构维护了一些统计数据(如进程和打开文件的数目)就足够了。我们更感兴趣的问题是:每个用户命名空间对其用户资源使用的统计,与其他命名空间完全无关,对root用户的统计也是如此。这是因为在克隆一个用户命名空间时,为当前用户和root都创建了新的user_struct实例:

  1. kernel/user_namespace.c
  2. static struct user_namespace *clone_user_ns(struct 
    user_namespace *old_ns)
  3. {
  4. struct user_namespace *ns;
  5. struct user_struct *new_user;
  6. ...
  7. ns = kmalloc(sizeof(struct user_namespace), GFP_KERNEL);
  8. ...
  9. ns->root_user = alloc_uid(ns, 0);
  10. /* 将current->user替换为新的 */
  11. new_user = alloc_uid(ns, current->uid);
  12. switch_uid(new_user);
  13. return ns;
  14. }

alloc_uid是一个辅助函数,对当前命名空间中给定UID的一个用户,如果该用户没有对应的user_struct实例,则分配一个新的实例。在为root和当前用户分别设置了user_struct实例后,switch_uid确保从现在开始将新的user_struct实例用于资源统计。实质上就是将struct task_struct的user成员指向新的user_struct实例。

请注意,如果内核编译时未指定支持用户命名空间,那么复制用户命名空间实际上是空操作,即总是会使用默认的命名空间。

2.3.3 进程ID号(1)

UNIX进程总是会分配一个号码用于在其命名空间中唯一地标识它们。该号码被称作进程ID号,简称PID。用fork或clone产生的每个进程都由内核自动地分配了一个新的唯一的PID值。

1. 进程ID

但每个进程除了PID这个特征值之外,还有其他的ID。有下列几种可能的类型。

处于某个线程组(在一个进程中,以标志CLONE_THREAD来调用clone建立的该进程的不同的执行上下文,我们在后文会看到)中的所有进程都有统一的线程组ID(TGID)。如果进程没有使用线程,则其PID和TGID相同。

线程组中的主进程被称作组长(group leader)。通过clone创建的所有线程的task_struct的group_leader成员,会指向组长的task_struct实例。

另外,独立进程可以合并成进程组(使用setpgrp系统调用)。进程组成员的task_struct的pgrp属性值都是相同的,即进程组组长的 PID。进程组简化了向组的所有成员发送信号的操作,这对于各种系统程序设计应用(参见系统程序设计方面的文献,例如[SR05])是有用的。请注意,用管道连接的进程包含在同一个进程组中。

几个进程组可以合并成一个会话。会话中的所有进程都有同样的会话ID,保存在task_struct的session成员中。SID可以使用 setsid系统调用设置。它可以用于终端程序设计,但和我们这里的讨论不相干。

命名空间增加了PID管理的复杂性。回想一下,PID命名空间按层次组织。在建立一个新的命名空间时,该命名空间中的所有PID对父命名空间都是可见的,但子命名空间无法看到父命名空间的PID。但这意味着某些进程具有多个PID,凡可以看到该进程的命名空间,都会为其分配一个PID。 这必须反映在数据结构中。我们必须区分局部ID和全局ID。

全局ID是在内核本身和初始命名空间中的唯一ID号,在系统启动期间开始的init进程即属于初始命名空间。对每个ID类型,都有一个给定的全局 ID,保证在整个系统中是唯一的。

局部ID属于某个特定的命名空间,不具备全局有效性。对每个ID类型,它们在所属的命名空间内部有效,但类型相同、值也相同的ID可能出现在不同的命名空间中。

全局PID和TGID直接保存在task_struct中,分别是task_struct的pid和tgid成员:

  1. <sched.h>
  2. struct task_struct {
  3. ...
  4. pid_t pid;
  5. pid_t tgid;
  6. ...
  7. }

这两项都是pid_t类型,该类型定义为__kernel_pid_t,后者由各个体系结构分别定义。通常定义为int,即可以同时使用232个不同的ID。

会话和进程组ID不是直接包含在task_struct本身中,但保存在用于信号处理的结构中。task_ struct->signal->__session表示全局SID,而全局PGID则保存在 task_struct->signal->__pgrp。辅助函数set_task_session和set_task_pgrp可用于修改这些值。

2. 管理PID

除了这两个字段之外,内核还需要找一个办法来管理所有命名空间内部的局部量,以及其他ID(如TID和SID)。这需要几个相互连接的数据结构,以及许多辅助函数,并将在下文讨论。

数据结构

下文我将使用ID指代提到的任何进程ID。在必要的情况下,我会明确地说明ID类型(例如,TGID,即线程组ID)。

一个小型的子系统称之为PID分配器(pid allocator)用于加速新ID的分配。此外,内核需要提供辅助函数,以实现通过ID及其类型查找进程的task_struct的功能,以及将ID的内核表示形式和用户空间可见的数值进行转换的功能。

在介绍表示ID本身所需的数据结构之前,我需要讨论PID命名空间的表示方式。我们所需查看的代码如下所示:

  1. <pid_namespace.h>
  2. struct pid_namespace {
  3. ...
  4. struct task_struct *child_reaper;
  5. ...
  6. int level;
  7. struct pid_namespace *parent;
  8. };

实际上PID分配器也需要依靠该结构的某些部分来连续生成唯一ID,但我们目前对此无需关注。我们上述代码中给出的下列成员更感兴趣。

每个PID命名空间都具有一个进程,其发挥的作用相当于全局的init进程。init的一个目的是对孤儿进程调用wait4,命名空间局部的 init变体也必须完成该工作。child_reaper保存了指向该进程的task_struct的指针。

parent是指向父命名空间的指针,层次表示当前命名空间在命名空间层次结构中的深度。初始命名空间的level为0,该命名空间的子空间 level为1,下一层的子空间level为2,依次递推。level的计算比较重要,因为level较高的命名空间中的ID,对level较低的命名空间来说是可见的。从给定的level设置,内核即可推断进程会关联到多少个ID。

回想图2-3的内容,命名空间是按层次关联的。这有助于理解上述的定义。

PID的管理围绕两个数据结构展开:struct pid是内核对PID的内部表示,而struct upid则表示特定的命名空间中可见的信息。两个结构的定义如下:

  1. <pid.h>
  2. struct upid {
  3. int nr;
  4. struct pid_namespace *ns;
  5. struct hlist_node pid_chain;
  6. };
  7. struct pid
  8. {
  9. atomic_t count;
  10. /* 使用该pid的进程的列表 */
  11. struct hlist_head tasks[PIDTYPE_MAX];
  12. int level;
  13. struct upid numbers[1];
  14. };

由于这两个结构与其他一些数据结构存在广泛的联系,在分别讨论相关结构之前,图2-5对此进行了概述。

对于struct upid,nr表示ID的数值,ns是指向该ID所属的命名空间的指针。所有的upid实例都保存在一个散列表中,稍后我们会看到该结构。 pid_chain用内核的标准方法实现了散列溢出链表。

struct pid的定义首先是一个引用计数器count。tasks是一个数组,每个数组项都是一个散列表头,对应于一个ID类型。这样做是必要的,因为一个ID可能用于几个进程。所有共享同一给定ID的task_struct实例,都通过该列表连接起来。PIDTYPE_MAX表示ID类型的数目:

  1. <pid.h>
  2. enum pid_type
  3. {
  4. PIDTYPE_PID,
  5. PIDTYPE_PGID,
  6. PIDTYPE_SID,
  7. PIDTYPE_MAX
  8. };
 
(点击查看大图)图2-5 实现可感知命名空间的 ID表示所用的数据结构

2.3.3 进程ID号(2)

请注意,枚举类型中定义的ID类型不包括线程组ID!这是因为线程组ID无非是线程组组长的PID而已,因此再单独定义一项是不必要的。

一个进程可能在多个命名空间中可见,而其在各个命名空间中的局部ID各不相同。level表示可以看到该进程的命名空间的数目(换言之,即包含该进程的命名空间在命名空间层次结构中的深度),而numbers是一个upid实例的数组,每个数组项都对应于一个命名空间。注意该数组形式上只有一个数组项,如果一个进程只包含在全局命名空间中,那么确实如此。由于该数组位于结构的末尾,因此只要分配更多的内存空间,即可向数组添加附加的项。

由于所有共享同一ID的task_struct实例都按进程存储在一个散列表中,因此需要在struct task_struct中增加一个散列表元素:

  1. <sched.h>
  2. struct task_struct {
  3. ...
  4. /* PID与PID散列表的联系。 */
  5. struct pid_link pids[PIDTYPE_MAX];
  6. ...
  7. };

辅助数据结构pid_link可以将task_struct连接到表头在struct pid中的散列表上:

  1. <pid.h>
  2. struct pid_link
  3. {
  4. struct hlist_node node;
  5. struct pid *pid;
  6. };

pid指向进程所属的pid结构实例,node用作散列表元素。

为在给定的命名空间中查找对应于指定PID数值的pid结构实例,使用了一个散列表:

  1. kernel/pid.c
  2. static struct hlist_head *pid_hash;

hlist_head是一个内核的标准数据结构,用于建立双链散列表(附录C描述了该散列表的结构,并介绍了用于处理该数据结构的几个辅助函数)。

pid_hash用作一个hlist_head数组。数组的元素数目取决于计算机的内存配置,大约在24=16和212=4096之间。 pidhash_init用于计算恰当的容量并分配所需的内存。

假如已经分配了struct pid的一个新实例,并设置用于给定的ID类型。它会如下附加到task_struct:

  1. kernel/pid.c
  2. int fastcall attach_pid(struct task_struct *task, enum pid_type type,
  3. struct pid *pid)
  4. {
  5. struct pid_link *link;
  6. link = &task->pids[type];
  7. link->pidpid = pid;
  8. hlist_add_head_rcu(&link->node, &pid->tasks[type]);
  9. return 0;
  10. }

这里建立了双向连接:task_struct可以通过task_struct->pids[type]->pid访问pid实例。而从 pid实例开始,可以遍历tasks[type]散列表找到task_struct。hlist_add_head_rcu是遍历散列表的标准函数,此外还确保了遵守RCU机制(参见第5章)。因为,在其他内核组件并发地操作散列表时,可防止竞态条件(race condition)出现。

函数

内核提供了若干辅助函数,用于操作和扫描上面描述的数据结构。本质上内核必须完成下面两个不同的任务。

(1) 给出局部数字ID和对应的命名空间,查找此二元组描述的task_struct。

(2) 给出task_struct、ID类型、命名空间,取得命名空间局部的数字ID。

我们首先专注于如何将task_struct实例变为数字ID。这个过程包含下面两个步骤。

(1) 获得与task_struct关联的pid实例。辅助函数task_pid、task_tgid、task_pgrp和task_session分别用于取得不同类型的ID。获取PID的实现很简单:

  1. <sched.h>
  2. static inline struct pid *task_pid(struct task_struct *task)
  3. {
  4. return task->pids[PIDTYPE_PID].pid;
  5. }

获取TGID的做法类似,因为TGID不过是线程组组长的PID而已。只要将上述实现替换为task-> group_leader->pids[PIDTYPE_PID].pid即可。

找出进程组ID则需要使用PIDTYPE_PGID作为数组索引,但该ID仍然需要从线程组组长的task_ struct实例获取:

  1. <sched.h>
  2. static inline struct pid *task_pgrp(struct task_struct *task)
  3. {
  4. return task->group_leader->pids[PIDTYPE_PGID].pid;
  5. }

(2) 在获得pid实例之后,从struct pid的numbers数组中的uid信息,即可获得数字ID:

  1. kernel/pid.c
  2. pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
  3. {
  4. struct upid *upid;
  5. pid_t nr = 0;
  6. if (pid && ns->level <= pid->level) {
  7. upid = &pid->numbers[ns->level];
  8. if (upid->ns == ns)
  9. nr = upid->nr;
  10. }
  11. return nr;
  12. }

因为父命名空间可以看到子命名空间中的PID,反过来却不行,内核必须确保当前命名空间的level小于或等于产生局部PID的命名空间的 level。

同样重要的是要注意到,内核只需要关注产生全局PID。因为全局命名空间中所有其他ID类型都会映射到PID,因此不必生成诸如全局TGID或 SID。

除了在第2步使用的pid_nr_ns之外,内核还可以使用下列辅助函数:

pid_vnr返回该ID所属的命名空间所看到的局部PID;

pid_nr则获取从init进程看到的全局PID。

这两个函数都依赖于pid_nr_ns,并自动选择适当的level:0用于获取全局PID,而pid->level则用于获取局部PID。

内核提供了几个辅助函数,合并了前述步骤:

  1. kernel/pid.c
  2. pid_t task_pid_nr_ns(struct task_struct 
    *tsk, struct pid_namespace *ns)
  3. pid_t task_tgid_nr_ns(struct task_struct 
    *tsk, struct pid_namespace *ns)
  4. pid_t task_pgrp_nr_ns(struct task_struct 
    *tsk, struct pid_namespace *ns)
  5. pid_t task_session_nr_ns(struct task_struct 
    *tsk, struct pid_namespace *ns)

从函数名可以明显推断其语义,因此我们不再赘述。

2.3.3 进程ID号(3)

现在我们把注意力转向内核如何将数字PID和命名空间转换为pid实例。同样需要下面两个步骤。

(1) 给出进程的局部数字PID和关联的命名空间(这是PID的用户空间表示),为确定pid实例(这是PID的内核表示),内核必须采用标准的散列方案。首先,根据PID和命名空间指针计算在pid_hash数组中的索引, 然后遍历散列表直至找到所要的元素。这是通过辅助函数find_pid_ns处理的:

  1. kernel/pid.c
  2. struct pid * fastcall find_pid_ns(int nr,
    struct pid_namespace *ns)

struct upid的实例保存在散列表中,由于这些实例直接包含在struct pid中,内核可以使用container_of机制(参见附录C)推断出所要的信息。

(2) pid_task取出pid->tasks[type]散列表中的第一个task_struct实例。

这两个步骤可以通过辅助函数find_task_by_pid_type_ns完成:

  1. kernel/pid.c
  2. struct task_struct *find_task_by_pid_type_ns(int type, int nr,
  3. struct pid_namespace *ns)
  4. {
  5. return pid_task(find_pid_ns(nr, ns), type);
  6. }

一些简单一点的辅助函数基于最一般性的find_task_by_pid_type_ns:

find_task_by_pid_ns(pid_t nr, struct pid_namespace * ns)根据给出的数字PID和进程的命名空间来查找task_struct实例。

find_task_by_vpid(pid_t vnr)通过局部数字PID查找进程。

find_task_by_pid(pid_t nr)通过全局数字PID查找进程。

内核源代码中许多地方都需要find_task_by_pid,因为很多特定于进程的操作(例如,使用kill发送一个信号)都通过PID标识目标进程。

3. 生成唯一的PID

除了管理PID之外,内核还负责提供机制来生成唯一的PID(尚未分配)。在这种情况下,可以忽略各种不同类型的PID之间的差别,因为按一般的 UNIX观念,只需要为PID生成唯一的数值即可。所有其他的ID都可以派生自PID,在下文讨论fork和clone时会看到这一点。在随后的几节中,名词PID还是指一般的UNIX进程ID(PIDTYPE_PID)。

为跟踪已经分配和仍然可用的PID,内核使用一个大的位图,其中每个PID由一个比特标识。PID的值可通过对应比特在位图中的位置计算而来。

因此,分配一个空闲的PID,本质上就等同于寻找位图中第一个值为0的比特,接下来将该比特设置为1。反之,释放一个PID可通过将对应的比特从1 切换为0来实现。这些操作使用下述两个函数实现:

  1. kernel/pid.c
  2. static int alloc_pidmap(struct pid_namespace *pid_ns)

用于分配一个PID,而

  1. kernel/pid.c
  2. static fastcall void free_pidmap(struct 
    pid_namespace *pid_ns, int pid)

用于释放一个PID。我们这里不关注具体的实现方式,但它们必须能够在命名空间下工作。

在建立一个新进程时,进程可能在多个命名空间中是可见的。对每个这样的命名空间,都需要生成一个局部PID。这是在alloc_pid中处理的:

  1. kernel/pid.c
  2. struct pid *alloc_pid(struct pid_namespace *ns)
  3. {
  4. struct pid *pid;
  5. enum pid_type type;
  6. int i, nr;
  7. struct pid_namespace *tmp;
  8. struct upid *upid;
  9. ...
  10. tmp = ns;
  11. for (i = ns->level; i >= 0; i--) {
  12. nr = alloc_pidmap(tmp);
  13. ...
  14. pid->numbers[i].nr = nr;
  15. pid->numbers[i].ns = tmp;
  16. tmptmp = tmp->parent;
  17. }
  18. pid->level = ns->level;
  19. ...

起始于建立进程的命名空间,一直到初始的全局命名空间,内核会为此间的每个命名空间分别创建一个局部PID。包含在struct pid中的所有upid都用重新生成的PID更新其数据。每个upid实例都必须置于PID散列表中:

  1. kernel/pid.c
  2. for (i = ns->level; i >= 0; i--) {
  3. upid = &pid->numbers[i];
  4. hlist_add_head_rcu(&upid->pid_chain,
  5. &pid_hash[pid_
    hashfn(upid->nr, upid->ns)]);
  6. }
  7. ...
  8. return pid;
  9. }

2.3.4 进程关系

除了源于ID连接的关系之外,内核还负责管理建立在UNIX进程创建模型之上"家族关系"。相关讨论一般使用下列术语。

如果进程A分支形成进程B,进程A称之为父进程而进程B则是子进程。

如果进程B再次分支建立另一个进程C,进程A和进程C之间有时称之为祖孙关系。

如果进程A分支若干次形成几个子进程B1,B2,…,Bn,各个Bi进程之间的关系称之为兄弟关系。

图2-6说明了可能的进程家族关系。

 
(点击查看大图)图2-6 进程之间的家族关系

task_struct 数据结构提供了两个链表表头,用于实现这些关系:

  1. <sched.h>
  2. struct task_struct {
  3. ...
  4. struct list_head children;  /* 子进程链表 */
  5. struct list_head sibling;   /* 连接到父进程的子进程链表 */
  6. ...
  7. }

children是链表表头,该链表中保存有进程的所有子进程。

sibling用于将兄弟进程彼此连接起来。

新的子进程置于sibling链表的起始位置,这意味着可以重建进程分支的时间顺序。

Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。该数据结构在内核文件 include/linux/sched.h 中定义,在Linux 3.8 的内核中,该数据结构足足有 380 行之多,在这里我不可能逐项去描述其表示的含义,本篇文章只关注该数据结构如何来组织和管理进程ID的。

进程ID类型

要想了解内核如何来组织和管理进程ID,先要知道进程ID的类型:

  • PID:这是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程ID号,简称PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。
  • TGID:在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程,它们处于一个线程组,该线程组的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID;线程组组长的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。
  • PGID:另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作,例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。
  • SID:几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID。

PID 命名空间

命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。命名空间有的还有层次关系,如PID命名空间,图1 为命名空间的层次关系图。

图1 命名空间的层次关系

在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以PID命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的PID 号5~10。

命名空间增大了 PID 管理的复杂性,对于某些进程可能有多个PID——在其自身命名空间的PID以及其父命名空间的PID,凡能看到该进程的命名空间都会为其分配一个PID。因此就有:

  • 全局ID:在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一。
  • 局部ID:对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中。

进程ID管理数据结构

Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素:

  1. 如何快速地根据进程的 task_struct、ID类型、命名空间找到局部ID
  2. 如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct
  3. 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID

如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。

一个PID对应一个task_struct

如果先不考虑进程之间的关系,不考虑命名空间,仅仅是一个PID号对应一个task_struct,那么我们可以设计这样的数据结构:

struct task_struct {//...struct pid_link pids;//...
};struct pid_link {struct hlist_node node;  struct pid *pid;
};struct pid {struct hlist_head tasks;        //指回 pid_link 的 nodeint nr;                       //PIDstruct hlist_node pid_chain;    //pid hash 散列表结点
};

每个进程的 task_struct 结构体中有一个指向 pid 结构体的指针,pid 结构体包含了 PID 号。结构示意图如图2。

图2 一个task_struct对应一个PID

图中还有两个结构上面未提及:

  • pid_hash[]: 这是一个hash表的结构,根据 pid 的 nr 值哈希到其某个表项,若有多个 pid 结构对应到同一个表项,这里解决冲突使用的是散列表法。这样,就能解决开始提出的第2个问题了,根据PID值怎样快速地找到task_struct结构体:

    • 首先通过 PID 计算 pid 挂接到哈希表 pid_hash[] 的表项
    • 遍历该表项,找到 pid 结构体中 nr 值与 PID 值相同的那个 pid
    • 再通过该 pid 结构体的 tasks 指针找到 node
    • 最后根据内核的 container_of 机制就能找到 task_struct 结构体
  • pid_map:这是一个位图,用来唯一分配PID值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将pid_map 中该值设为已分配标志。这也就解决了上面的第3个问题——如何快速地分配一个全局的PID。

至于上面的第1个问题就更加简单,已知 task_struct 结构体,根据其 pid_link 的 pid 指针找到 pid 结构体,取出其 nr 即为 PID 号。

进程ID有类型之分

如果考虑进程之间有复杂的关系,如线程组、进程组、会话组,这些组均有组ID,分别为 TGID、PGID、SID,所以原来的 task_struct 中pid_link 指向一个 pid 结构体需要增加几项,用来指向到其组长的 pid 结构体,相应的 struct pid 原本只需要指回其 PID 所属进程的task_struct,现在要增加几项,用来链接那些以该 pid 为组长的所有进程组内进程。数据结构如下:

enum pid_type
{PIDTYPE_PID,PIDTYPE_PGID,PIDTYPE_SID,PIDTYPE_MAX
};struct task_struct {//...pid_t pid;     //PIDpid_t tgid;    //thread group idstruct task_struct *group_leader;   // threadgroup leaderstruct pid_link pids[PIDTYPE_MAX];//...
};struct pid_link {struct hlist_node node;  struct pid *pid;
};struct pid {struct hlist_head tasks[PIDTYPE_MAX];int nr;                         //PIDstruct hlist_node pid_chain;    // pid hash 散列表结点
};

上面 ID 的类型 PIDTYPE_MAX 表示 ID 类型数目。之所以不包括线程组ID,是因为内核中已经有指向到线程组的 task_struct 指针 group_leader,线程组 ID 无非就是 group_leader 的PID。

假如现在有三个进程A、B、C为同一个进程组,进程组长为A,这样的结构示意图如图3。

图3 增加ID类型的结构

关于上图有几点需要说明:

  • 图中省去了 pid_hash 以及 pid_map 结构,因为第一种情况类似;
  • 进程B和C的进程组组长为A,那么 pids[PIDTYPE_PGID] 的 pid 指针指向进程A的 pid 结构体;
  • 进程A是进程B和C的组长,进程A的 pid 结构体的 tasks[PIDTYPE_PGID] 是一个散列表的头,它将所有以该pid 为组长的进程链接起来。

再次回顾本节的三个基本问题,在此结构上也很好去实现。

增加进程PID命名空间

若在第二种情形下再增加PID命名空间,一个进程就可能有多个PID值了,因为在每一个可见的命名空间内都会分配一个PID,这样就需要改变 pid 的结构了,如下:

struct pid
{unsigned int level;  /* lists of tasks that use this pid */struct hlist_head tasks[PIDTYPE_MAX];struct upid numbers[1];
};struct upid {int nr;struct pid_namespace *ns;struct hlist_node pid_chain;
};

在 pid 结构体中增加了一个表示该进程所处的命名空间的层次level,以及一个可扩展的 upid 结构体。对于struct upid,表示在该命名空间所分配的进程的ID,ns指向是该ID所属的命名空间,pid_chain 表示在该命名空间的散列表。

举例来说,在level 2 的某个命名空间上新建了一个进程,分配给它的 pid 为45,映射到 level 1 的命名空间,分配给它的 pid 为 134;再映射到 level 0 的命名空间,分配给它的 pid 为289,对于这样的例子,如图4所示为其表示:

图4 增加PID命名空间之后的结构图

图中关于如果分配唯一的 PID 没有画出,但也是比较简单,与前面两种情形不同的是,这里分配唯一的 PID 是有命名空间的容器的,在PID命名空间内必须唯一,但各个命名空间之间不需要唯一。

至此,已经与 Linux 内核中数据结构相差不多了。

进程ID管理函数

有了上面的复杂的数据结构,再加上散列表等数据结构的操作,就可以写出我们前面所提到的三个问题的函数了:

获得局部ID

根据进程的 task_struct、ID类型、命名空间,可以很容易获得其在命名空间内的局部ID:

  1. 获得与task_struct 关联的pid结构体。辅助函数有 task_pid、task_tgid、task_pgrp和task_session,分别用来获取不同类型的ID的pid 实例,如获取 PID 的实例:

    static inline struct pid *task_pid(struct task_struct *task)
    {return task->pids[PIDTYPE_PID].pid;
    }
    

    获取线程组的ID,前面也说过,TGID不过是线程组组长的PID而已,所以:

    static inline struct pid *task_tgid(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_PID].pid;
    }
    

    而获得PGID和SID,首先需要找到该线程组组长的task_struct,再获得其相应的 pid:

    static inline struct pid *task_pgrp(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_PGID].pid;
    }static inline struct pid *task_session(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_SID].pid;
    }
    

  2. 获得 pid 实例之后,再根据 pid 中的numbers 数组中 uid 信息,获得局部PID。

    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;
    }
    

    这里值得注意的是,由于PID命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能,因此,函数中需要确保当前命名空间的level 小于等于产生局部PID的命名空间的level。
    除了这个函数之外,内核还封装了其他函数用来从 pid 实例获得 PID 值,如 pid_nr、pid_vnr 等。在此不介绍了。

结合这两步,内核提供了更进一步的封装,提供以下函数:

pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

从函数名上就能推断函数的功能,其实不外于封装了上面的两步。

查找进程task_struct

根据局部ID、以及命名空间,怎样获得进程的task_struct结构体呢?也是分两步:

  1. 获得 pid 实体。根据局部PID以及命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。代码如下:

    struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
    {struct hlist_node *elem;struct upid *pnr;//遍历散列表hlist_for_each_entry_rcu(pnr, elem,&pid_hash[pid_hashfn(nr, ns)], pid_chain)     //pid_hashfn() 获得hash的索引if (pnr->nr == nr && pnr->ns == ns)     //比较 nr 与 ns 是否都相同return container_of(pnr, struct pid,     //根据container_of机制取得pid 实体numbers[ns->level]);return NULL;
    }
    

  2. 根据ID类型取得task_struct 结构体。

    struct task_struct *pid_task(struct pid *pid, enum pid_type type)
    {struct task_struct *result = NULL;if (pid) {struct hlist_node *first;first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),lockdep_tasklist_lock_is_held());if (first)result = hlist_entry(first, struct task_struct, pids[(type)].node);}return result;
    }
    

内核还提供其它函数用来实现上面两步:

struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns);
struct task_struct *find_task_by_vpid(pid_t vnr);
struct task_struct *find_task_by_pid(pid_t vnr);

具体函数实现的功能也比较简单。

生成唯一的PID

内核中使用下面两个函数来实现分配和回收PID的:

static int alloc_pidmap(struct pid_namespace *pid_ns);
static void free_pidmap(struct upid *upid);

在这里我们不关注这两个函数的实现,反而应该关注分配的 PID 如何在多个命名空间中可见,这样需要在每个命名空间生成一个局部ID,函数 alloc_pid 为新建的进程分配PID,简化版如下:

struct pid *alloc_pid(struct pid_namespace *ns)
{struct pid *pid;enum pid_type type;int i, nr;struct pid_namespace *tmp;struct upid *upid;tmp = ns;pid->level = ns->level;// 初始化 pid->numbers[] 结构体for (i = ns->level; i >= 0; i--) {nr = alloc_pidmap(tmp);            //分配一个局部IDpid->numbers[i].nr = nr;pid->numbers[i].ns = tmp;tmp = tmp->parent;}// 初始化 pid->task[] 结构体for (type = 0; type < PIDTYPE_MAX; ++type)INIT_HLIST_HEAD(&pid->tasks[type]);// 将每个命名空间经过哈希之后加入到散列表中upid = pid->numbers + ns->level;for ( ; upid >= pid->numbers; --upid) {hlist_add_head_rcu(&upid->pid_chain,&pid_hash[pid_hashfn(upid->nr, upid->ns)]);upid->ns->nr_hashed++;}return pid;
}


参考资料

  • 深入Linux 内核架构(以前不觉得这本书写得多好,现在倒发现还不错,本文很多都是照抄上面的)
  • 周徐达师弟的PPT(让我受益匪浅的一次讨论,周由浅入深告诉我们该数据结构是如何设计出来的,本文主思路就是按照该PPT,在此 特别感谢!)

Linux内核的namespace机制分析相关推荐

  1. Linux内核抢占实现机制分析【转】

    Linux内核抢占实现机制分析 转自:http://blog.chinaunix.net/uid-24227137-id-3050754.html [摘要]本文详解了Linux内核抢占实现机制.首先介 ...

  2. 【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】Linux内核抢占实现机制分析

    Linux内核抢占实现机制分析 Sailor_forever  sailing_9806@163.com 转载请注明 http://blog.csdn.net/sailor_8318/archive/ ...

  3. Linux内核态抢占机制分析

    http://blog.sina.com.cn/s/blog_502c8cc401012pxj.html [摘要]本文首先介绍非抢占式内核(Non-Preemptive Kernel)和可抢占式内核( ...

  4. linux禁止内核抢占,Linux内核态抢占机制分析

    [51CTO晃荡]8.26 带你深度懂得清华大年夜学.搜狗基于算法的IT运维实践与摸索 本文起首介绍非抢占式内核(Non-Preemptive Kernel)和可抢占式内核(Preemptive Ke ...

  5. Linux内核IP Queue机制的分析(一)

    将会通过包括本文在内的三篇文章,对IP Queue机制从用户态的应用到内核态的模块程序设计进行分析.三篇文章的题目分别是: Linux内核IP Queue机制的分析(一)--用户态接收数据包 Linu ...

  6. linux收发包内核进程名称,Linux内核IP Queue机制的分析(一)——用户态接收数据包...

    序 笔者将会通过包括本文在内的三篇文章,对IP Queue机制从用户态的应用到内核态的模块程序设计进行分析.三篇文章的题目分别是: Linux内核IP Queue机制的分析(一)­--用户态接收数据包 ...

  7. Linux内核中锁机制之完成量、互斥量

    在上一篇博文中笔者分析了关于信号量.读写信号量的使用及源码实现,接下来本篇博文将讨论有关完成量和互斥量的使用和一些经典问题. 八.完成量 下面讨论完成量的内容,首先需明确完成量表示为一个执行单元需要等 ...

  8. linux 信号量锁 内核,Linux内核中锁机制之信号量、读写信号量

    在上一篇博文中笔者分析了关于内存屏障.读写自旋锁以及顺序锁的相关内容,本篇博文将着重讨论有关信号量.读写信号量的内容. 六.信号量 关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程 ...

  9. 大话Linux内核中锁机制之原子操作、自旋锁【转】

    转自:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实 ...

最新文章

  1. AliOS Things v1.2.0新特性
  2. 在线引入bootstrap包
  3. 文本编辑器创建工具栏
  4. json支持的最大长度_Swifter.Json 可能是 .Net 平台迄今为止性能最佳的 Json 序列化库【开源】...
  5. 大数据驱动业务决策,CDN实时日志重磅上线
  6. VSCode自定义代码片段13——Vue的状态大管家
  7. 浏览器中的事件循环机制
  8. 【转】js中forEach回调同异步问题
  9. java .entryset_Java中map的entrySet()方法返回的是什么内容啊?有点晕
  10. excel快捷键大全常用分享
  11. 【96】太空射击游戏_笔记
  12. 费马小定理 欧拉定理 逆元
  13. 解决Chrome账户无法同步
  14. qt打开xls文件_Qt操作Excel
  15. 拥有微软Windows CE的实时系统
  16. 2021-08-16 WPF控件专题 WrapPanel 控件详解
  17. Linux中设置开机启动执行命令和普通用户配置环境变量开机启动生效
  18. 程序实现蒙特卡洛算法计算PI值和积分
  19. 【小白爬POJ2431】3.6 探险车加油问 Expedition
  20. LINUX-配置环境变量

热门文章

  1. 【转载】FckEditor 2.6.3 for Java 2.4 配置
  2. iOS 9: UIStackView入门
  3. windows快捷启动命令
  4. zz职位是有负面作用的
  5. Reids实战(7)数据类型五sorted sets
  6. php 连接mysql 错误排查一例
  7. Windows之建立C++开发环境
  8. 检测数(二进制形式)中1的个数
  9. 解决小米手机无法收到开机广播的问题
  10. ORACLE ORA-01653: unable to extend table 的错误