最近看《深入理解linux內核架構》中pid名字空間一塊,略有感悟。這裏開門見山,直接來講pid名字空間是如何實現的,以及如何使用。先看一幅圖,圖中描述了所使用的數據結構和他們的關係。
首先強調圖中的幾個比較容易搞混的字段。看task_struct中的struct pid_link *pids數組中,每個元素裏有一個struct hlist_node node字段。也就是說,可以利用這個字段把task_struct加入到一個哈希表中。再看右邊的struct pid結構中的struct hlist_head tasks[]數組,明顯這個hlist_head,也就是表頭,這個地方的每個數組元素都可以是一個鏈表。由於這裏數據結構比較多,所以一定要注意是一個內結點,還是一個鏈表的頭結點。
圖中綠線(無箭頭)表示頭結點把中間結點鏈接成了一個鏈。而藍色單箭頭線表示,是隻有一個指向目標結點的指針。藍色雙箭頭線表示是雙向鏈表。
爲什麼要搞這麼複雜的結構呢?因爲滿足一些需求。
搞pid名字空間就是爲了把一些進程隔離開來,讓他們以爲他們獨佔了整個系統,在虛擬化等中有應用。
還有,內核中經常需要由PID的數字值得到進程task_struct。(struct hlist_head *pid_hash的作用)。
需要由PID數字值得到進程組ID,會話ID。
當一個會話結束了,需要終止屬於這個會話的所有進程。也就是要由會話ID找到所有屬於這個會話的進程task_struct。(看看struct pid中有個SID字段,這裏鏈接着屬於這個PID的所有會話進程)。
linux用輕量級進程模擬線程,所以需要由線程組長ID找到所有屬於這個進程的線程ID。
新創建一個進程是怎麼影響名字空間的?
新建進程時的調用棧如下圖。可以看出這是在創建內核線程。創建用戶線程是調用fork()系統調用,這fork裏也調用了do_fork(),也就是圖中的do_fork(). do_fork中調用了copy_process函數,在這個函數中子進程複製了父進程的task_struct,其中就複製了task_struct中的struct pid_link pids[]數組。也就是說這時子進程與父進程有一樣的PID,SID,GID。不過不用擔心,這時子進程沒有運行,在copy_process後邊會重新給pids[PID].pid賦值。
在kernel/fork.c:copy_process函數中,依次調用了下列函數:
p = dup_task_struct(current);複製了父進程的PID,GID,SID。
if (pid != &init_struct_pid) {
retval = -ENOMEM;
pid = alloc_pid(p->nsproxy->pid_ns);//分配新的PID,包括在每個名字空間中看到的不同PID。
if (!pid)
goto bad_fork_cleanup_io;
if (clone_flags & CLONE_NEWPID) {
retval = pid_ns_prepare_proc(p->nsproxy->pid_ns);
if (retval < 0)
goto bad_fork_free_pid;
}
}
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid;
//自己成了新的進程組長,但是在真正獲取進程組ID時:task->group_leader->pids[PIDTYPE_PGID];
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
if (likely(p->pid)) {
list_add_tail(&p->sibling, &p->real_parent->children);
tracehook_finish_clone(p, clone_flags, trace);
if (thread_group_leader(p)) {
if (clone_flags & CLONE_NEWPID)
p->nsproxy->pid_ns->child_reaper = p;
p->signal->leader_pid = pid;
tty_kref_put(p->signal->tty);
p->signal->tty =tty_kref_get(current->signal->tty);
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
**//加入會話PID的task[PIDTYPE_SID]鏈表。**
attach_pid(p, PIDTYPE_SID, task_session(current));
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++;
}
**//將自己加入struct pid的tasks[PIDTYPE_PID]鏈表。**
attach_pid(p, PIDTYPE_PID, pid);
nr_threads++;
}
每個進程有一個stuct pid結構,在這個結構中,tasks[]數組中有三個鏈表,第一個是PIDTYPE_PID鏈表,這個鏈表中只有一個進程x,也就是說每個進程有唯一一個struct PID.
第二個是PIDTYPE_PGID,這個鏈表中可能多於一個進程,多個進程可能屬於同一個進程組,由此可見,組長進程退出後,這個struct結點不應該釋放,直到進程組中所有進程退出。第三個是PIDTYPE_SID,這個與PGID一樣,屬於同一個會話的進程都在這個鏈表中。有了這些知識,上面的問題都可以迎刃而解了。
分配struct pid
下面代碼在kernel/pid.c中,在分配struct 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;
//從slab中獲得struct pid結構
pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
if (!pid)
goto out;
tmp = ns;
for (i = ns->level; i >= 0; i--) {
nr = alloc_pidmap(tmp);//從pid名字空間的位圖中分配一個數字PID號。
if (nr < 0)
goto out_free;
pid->numbers[i].nr = nr;
pid->numbers[i].ns = tmp;
tmp = tmp->parent;
}
get_pid_ns(ns);
pid->level = ns->level;
atomic_set(&pid->count, 1); //引用計數。計數爲0就釋放struct pid結構。
for (type = 0; type < PIDTYPE_MAX; ++type)
INIT_HLIST_HEAD(&pid->tasks[type]);
spin_lock_irq(&pidmap_lock);
for (i = ns->level; i >= 0; i--) {
upid = &pid->numbers[i];
hlist_add_head_rcu(&upid->pid_chain,
&pid_hash[pid_hashfn(upid->nr, upid->ns)]);
}
spin_unlock_irq(&pidmap_lock);
out:
return pid;
out_free:
while (++i <= ns->level)
free_pidmap(pid->numbers + i);
kmem_cache_free(ns->pid_cachep, pid);
pid = NULL;
goto out;
}
*
* attach_pid() must be called with the tasklist_lock write-held.
*/
void attach_pid(struct task_struct *task, enum pid_type type,
struct pid *pid)
{
struct pid_link *link;
link = &task->pids[type];
link->pid = pid;
hlist_add_head_rcu(&link->node, &pid->tasks[type]);
}
最初的struct pid是如何建立起來的?
一開始,內核用靜態變量初始化了一個struct pid結構給swap(0號進程)使用。還靜態初始化了一個pid名字空間。
這是在kernel/pid.ck上定義的。
struct pid_namespace init_pid_ns = {
.kref = {
.refcount = ATOMIC_INIT(2),
},
.pidmap = {
[ 0 ... PIDMAP_ENTRIES-1] = { ATOMIC_INIT(BITS_PER_PAGE), NULL }
},
.last_pid = 0,
.level = 0,
.child_reaper = &init_task, //每個pid名字空間有一個init進程,用來收養孤兒進程
};
//全局pid_hash中。鏈接的是struct upid結構,哈希值是由nr(pid)和ns(pid所在名字空間)算出來的.由pid和ns就能得到struct upid結構,再用container_of得到struct pid結構,再從struct pid結構中tasks[]獲取task_struct。
static struct hlist_head *pid_hash;
static unsigned int pidhash_shift = 4;
struct pid init_struct_pid = INIT_STRUCT_PID;//一個全局的初始pid結構。也是系統中最初始的PID。其他PID都是由alloc_pid函數從slab中分配的。
下面代碼在include/linux/init_task.h中:
extern struct nsproxy init_nsproxy;//名字空間根結點,靜態定義
#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) \
}
#define INIT_STRUCT_PID { \
.count = ATOMIC_INIT(1), \
.tasks = { \
下邊三個字段就是初始PID的初始化。可以看到0號進程是自己的組長,會話組長。
**{ .first = &init_task.pids[PIDTYPE_PID].node }, \
{ .first = &init_task.pids[PIDTYPE_PGID].node }, \
{ .first = &init_task.pids[PIDTYPE_SID].node }, \**
}, \
.rcu = RCU_HEAD_INIT, \
.level = 0, \
.numbers = { { \
.nr = 0, \
**.ns = &init_pid_ns,** \
.pid_chain = { .next = NULL, .pprev = NULL }, \
}, } \
}
#define INIT_PID_LINK(type) \
{ \
.node = { \
.next = NULL, \
.pprev = &init_struct_pid.tasks[type].first, \
}, \
.pid = &init_struct_pid, \
}
/*
* INIT_TASK is used to set up the first task table, touch at
* your own risk!. Base=0, limit=0x1fffff (=2MB)
*/
#define INIT_TASK(tsk) \初始化第一個進程
{ \
.state = 0, \
.stack = &init_thread_info, \
.usage = ATOMIC_INIT(2), \
.flags = PF_KTHREAD, \
.lock_depth = -1, \
.prio = MAX_PRIO-20, \
.static_prio = MAX_PRIO-20, \
.normal_prio = MAX_PRIO-20, \
.policy = SCHED_NORMAL, \
.cpus_allowed = CPU_MASK_ALL, \
.mm = NULL, \
.active_mm = &init_mm, \
.se = { \
.group_node = LIST_HEAD_INIT(tsk.se.group_node), \
}, \
.rt = { \
.run_list = LIST_HEAD_INIT(tsk.rt.run_list), \
.time_slice = HZ, \
.nr_cpus_allowed = NR_CPUS, \
}, \
.tasks = LIST_HEAD_INIT(tsk.tasks), \
.pushable_tasks = PLIST_NODE_INIT(tsk.pushable_tasks, MAX_PRIO), \
.ptraced = LIST_HEAD_INIT(tsk.ptraced), \
.ptrace_entry = LIST_HEAD_INIT(tsk.ptrace_entry), \
.real_parent = &tsk, \
.parent = &tsk, \
.children = LIST_HEAD_INIT(tsk.children), \
.sibling = LIST_HEAD_INIT(tsk.sibling), \
**.group_leader = &tsk,** \
.real_cred = &init_cred, \
.cred = &init_cred, \
.cred_guard_mutex = \
__MUTEX_INITIALIZER(tsk.cred_guard_mutex), \
.comm = "swapper", \
.thread = INIT_THREAD, \
.fs = &init_fs, \
.files = &init_files, \
.signal = &init_signals, \
.sighand = &init_sighand, \
.nsproxy = &init_nsproxy, \
.pending = { \
.list = LIST_HEAD_INIT(tsk.pending.list), \
.signal = {{0}}}, \
.blocked = {{0}}, \
.alloc_lock = __SPIN_LOCK_UNLOCKED(tsk.alloc_lock), \
.journal_info = NULL, \
.cpu_timers = INIT_CPU_TIMERS(tsk.cpu_timers), \
.fs_excl = ATOMIC_INIT(0), \
.pi_lock = __SPIN_LOCK_UNLOCKED(tsk.pi_lock), \
.timer_slack_ns = 50000, /* 50 usec default slack */ \
**將自己加入struct pid的tasks字段中。**
.pids = { \
[PIDTYPE_PID] = INIT_PID_LINK(PIDTYPE_PID), \
[PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID), \
[PIDTYPE_SID] = INIT_PID_LINK(PIDTYPE_SID), \
}, \
.dirties = INIT_PROP_LOCAL_SINGLE(dirties), \
INIT_IDS \
INIT_PERF_EVENTS(tsk) \
INIT_TRACE_IRQFLAGS \
INIT_LOCKDEP \
INIT_FTRACE_GRAPH \
INIT_TRACE_RECURSION \
INIT_TASK_RCU_PREEMPT(tsk) \
}
下面代碼在arch/x86/kernel/init_task.c中:
/*
* Initial task structure.
*
* All other task structs will be allocated on slabs in fork.c
*/
struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);