Linux进程管理之pid


前言

一、struct task_struct

列出struct task_struct结构体中与pid有关的成员:

struct task_struct {
    
    
	......
	pid_t pid;
	pid_t tgid;
	
	/* PID/PID hash table linkage. */
	struct pid_link pids[PIDTYPE_MAX];
	struct list_head thread_group;

	/* namespaces */
	struct nsproxy *nsproxy;
}
/*
 * A structure to contain pointers to all per-process
 * namespaces - fs (mount), uts, network, sysvipc, etc.
 *
 * The pid namespace is an exception -- it's accessed using
 * task_active_pid_ns.  The pid namespace here is the
 * namespace that children will use.
 *
 * '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;
	struct cgroup_namespace *cgroup_ns;
};

从上面我们可以看到 struct pid_namespace 的命名和其他的有点不一样。注释说明:pid namespace是个例外——使用task_active_pid_ns来访问pid namespace。这里的 pid namespace是children将使用的namespace。

task_active_pid_ns通过struct task_struct来获取struct pid_namespace:

struct pid_namespace *task_active_pid_ns(struct task_struct *tsk)
{
    
    
	return ns_of_pid(task_pid(tsk));
}
EXPORT_SYMBOL_GPL(task_active_pid_ns);

通过struct task_struct获取struct pid:

static inline struct pid *task_pid(struct task_struct *task)
{
    
    
	return task->pids[PIDTYPE_PID].pid;
}
struct pid_link
{
    
    
	struct hlist_node node;
	struct pid *pid;
};
struct task_struct {
    
    
	......
	/* PID/PID hash table linkage. */
	struct pid_link pids[PIDTYPE_MAX];
	......
}

通过struct pid获取struct pid_namespace,ns_of_pid()返回分配了指定pid的pid namespace。

/*
 * ns_of_pid() returns the pid namespace in which the specified pid was
 * allocated.
 *
 * NOTE:
 * 	ns_of_pid() is expected to be called for a process (task) that has
 * 	an attached 'struct pid' (see attach_pid(), detach_pid()) i.e @pid
 * 	is expected to be non-NULL. If @pid is NULL, caller should handle
 * 	the resulting NULL pid-ns.
 */
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;
}
struct pid_namespace {
    
    
	struct kref kref;
	struct pidmap pidmap[PIDMAP_ENTRIES];
	struct rcu_head rcu;
	int last_pid;
	unsigned int nr_hashed;
	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;
};

kref:是一个引用计数器,代表此命名空间在多少进程中被使用。
该结构体只有一个成员,为什么只有一个成员也要封装成结构呢?
这是为了防止原子变量被不小心直接赋值,这样封装后就不能直接操作该值,让开发者通过对应的函数接口来操作该原子变量。

struct kref {
    
    
	atomic_t refcount;
};

比如,必须通过调用kref_init函数来初始化该原子变量:

/**
 * kref_init - initialize object.
 * @kref: object in question.
 */
static inline void kref_init(struct kref *kref)
{
    
    
	atomic_set(&kref->refcount, 1);
}

pidmap[]:记录当前系统的PID使用情况。
last_pid:记录上一次分配给进程的PID值。
child_reaper:每个PID命名空间都有一个类似于全局的init进程,其作用和init进程一样,一个局部的init进程。比如:init进程对孤儿进程调用wait,命名空间的局部init进程也要完成该工作。child_reaper保存了指向该局部init进程的task_struct的指针。
pid_cachep:指向struct pid高速缓存的指针,sla(u)b分配器指针,slab对象为 struct pid 。

struct pid_namespace *ns;
struct pid *pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);

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

二、struct pid简介

2.1 struct pid

// linux-4.10.1/include/linux\pid.h

struct pid
{
    
    
	atomic_t count;
	unsigned int level;
	/* lists of tasks that use this pid */
	struct hlist_head tasks[PIDTYPE_MAX];
	struct rcu_head rcu;
	struct upid numbers[1];
};

struct pid是内核对PID的内部表示:

count:代表当前使用此struct pid的任务数量。
level:表示可以看到该进程的命名空间数目,也就是进程所属的进程号命名空间的层次。level = 0,代表全局命名空间。
tasks:是当前使用此struct pid的任务列表。tasks是一个数组,每个数组项都是一个哈希表头,对应于一个进程ID的类型。因为每一个进程ID可能属于几个进程,所有共享同一给定进程ID的task_struct实例都通过该哈希表连接起来。
numbers是一个struct upid实例的数组,每个数组项对应一个命名空间。这里这个数组项只有1个,只有1个时代表这个进程只属于全局的命名空间。但是这个数组位于结构体末端,这样就可以分配更多的内存空间,即可以向这个numbers数组增加更多成员,这样这个进程就可以属于多个局部命名空间了。

其中PIDTYPE_MAX表示PID类型:

enum pid_type
{
    
    
	PIDTYPE_PID,	//进程的进程号
	PIDTYPE_PGID,	//进程组领头进程的进程号
	PIDTYPE_SID,	//会话领头进程的进程号
	PIDTYPE_MAX		//表示进程号类型的数目 == 3
};

一个进程可能在多个命名空间中可见。而在各个命名空间的局部ID也都不相同

备注:可以使用 int nr(局部进程PID的数值) 和 struct pid_namespace *ns 通过 find_pid_ns() 找到 struct pid。

2.2 struct upid

struct upid {
    
    
	/* Try to keep pid_chain in the same cacheline as nr for find_vpid */
	int nr;
	struct pid_namespace *ns;
	struct hlist_node pid_chain;
};

struct upid则表示特定的命名空间的可见信息,用于获取struct pid的数值id,在特定的名称空间中被看到。
nr:进程ID的数值,保存进程的局部PID值。
ns:指向该进程ID所属的命名空间的指针。
pid_chain:哈希表节点,哈希表溢出链表。

所有struct upid实例都保存在一个哈希表中

注意:struct pid的tasks是struct hlist_head是哈希表表头,struct upid中的struct hlist_node是哈希表节点。

2.3 pid_t pid/tgid

struct pid是进程ID的内核表现形式,pid_t pid是用户空间可见的数值PID。内核提供了两者相互转换的辅助函数。

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

struct task_struct {
    
    
	......
	pid_t pid;
	pid_t tgid;
	......
}

task_struct结构体中的pid和tgid都是全局ID。

(2)
应用层调用getpid函数(返回当前进程的PID),实际上获取的是struct task_struct 结构体的pid_t tgid。
如果一个进程没有使用线程(即该进程只有一个主线程),则该进程的pid和tgid一样。如果一个进程有多个线程,这多个线程的pid是不一样的,但tgid是一样的。
内核中每个任务的 pid_t pid 都是不一样。

/**
 * sys_getpid - return the thread group id of the current process
 *
 * Note, despite the name, this returns the tgid not the pid.  The tgid and
 * the pid are identical unless CLONE_THREAD was specified on clone() in
 * which case the tgid is the same in all threads of the same group.
 *
 * This is SMP safe as current->tgid does not change.
 */
SYSCALL_DEFINE0(getpid)
{
    
    
	return task_tgid_vnr(current);
}
static inline pid_t task_tgid_vnr(struct task_struct *tsk)
{
    
    
	return pid_vnr(task_tgid(tsk));
}
static inline struct pid *task_tgid(struct task_struct *task)
{
    
    
	return task->group_leader->pids[PIDTYPE_PID].pid;
}

从源码中可以看到是获取线程组组长的pid,即tgid。

(3)
应用层调用gettid函数,获取线程标识,也就是获取线程的thread ID (TID),实际上是获取struct task_struct 结构体的pid_t pid。

/* Thread ID - the internal kernel "pid" */
SYSCALL_DEFINE0(gettid)
{
    
    
	return task_pid_vnr(current);
}
static inline pid_t task_pid_vnr(struct task_struct *tsk)
{
    
    
	return __task_pid_nr_ns(tsk, PIDTYPE_PID, NULL);
}
pid_t __task_pid_nr_ns(struct task_struct *task, enum pid_type type,
			struct pid_namespace *ns)
{
    
    
	pid_t nr = 0;

	rcu_read_lock();
	if (!ns)
		ns = task_active_pid_ns(current);
	if (likely(pid_alive(task))) {
    
    
		if (type != PIDTYPE_PID)
			task = task->group_leader;
		nr = pid_nr_ns(rcu_dereference(task->pids[type].pid), ns);
	}
	rcu_read_unlock();

	return nr;
}
EXPORT_SYMBOL(__task_pid_nr_ns);
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);

(4)

对用应用层来说,在多线程进程中,所有线程都有相同的 PID,但每个线程都有唯一的 TID。这是因为应用层调用getpid函数返回的是pid_t tgid,应用层调用gettid函数返回的是pid_t pid。而在内核中,同一个进程的多线程,pid_t tgid都是一样的,但是pid_t pid都各不相同。
在这里插入图片描述

(5)
int nr是局部空间的进程ID号。局部ID属于某个特定的命名空间,不具备全局性,对于每个局部ID类型,在所属的命名空间内部有效,但类型相同、值也相同的ID可能出现在不同的命名空间。

如果struct pid中的成员 level = 0,表示没有局部命名空间,struct pid的struct upid numbers[1],也就只有一个结构体,那么struct upid中局部 int nr 就等于该任务(struct task_struct)的 pid_t pid 。

注意:在内核中,全局的进程号ID一般都用 pid_t 类型描述。局部的进程号ID都用 int 类型 描述。
pid_t 类型其实也是int 类型。

typedef int					__kernel_pid_t;
typedef __kernel_pid_t		pid_t;

三、PID的生成

3.1 _do_fork

(1) fork():该函数是一个系统调用,可以复制一个现有的进程来创建一个全新的进程,产生一个 task_struct。
(2) pthread_create():该函数是Glibc中的函数,然后调用clone()系统调用创建一个线程(又叫轻量级进程),产生一个 task_struct。
(3)kthread_create():创建一个新的内核线程,产生一个 task_struct。

其实这三个API最后都会调用 _do_fork(),不同之处是传入给 _do_fork() 的参数不同(clone_flags)。

_do_fork()中会调用alloc_pid()生成struct pid,同时也会生成数值pid (pid_t pid),作为_do_fork()函数的返回值。

_do_fork()
	-->copy_process()
		-->alloc_pid()  

	-->get_task_pid()
	-->pid_vnr()
	-->put_pid()

生成struct pid:

struct task_struct *p;
struct pid *pid = alloc_pid(p->nsproxy->pid_ns_for_children)

生成pid_t pid:

struct task_struct *p = copy_process();
struct pid * pid = get_task_pid(p, PIDTYPE_PID);
long nr = pid_vnr(pid);
put_pid(pid);
return nr;
pid_t pid_vnr(struct pid *pid)
{
    
    
	return pid_nr_ns(pid, task_active_pid_ns(current));
}
EXPORT_SYMBOL_GPL(pid_vnr);
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);

3.2 alloc_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;
	int retval = -ENOMEM;

	(1)
	pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
	if (!pid)
		return ERR_PTR(retval);

	(2)
	tmp = ns;
	pid->level = ns->level;
	for (i = ns->level; i >= 0; i--) {
    
    
		nr = alloc_pidmap(tmp);
		if (nr < 0) {
    
    
			retval = nr;
			goto out_free;
		}

		pid->numbers[i].nr = nr;
		pid->numbers[i].ns = tmp;
		tmp = tmp->parent;
	}

	......

	(3)
	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;
	......
}

(1)
使用sla(u)b高速缓存分配器分配一个struct pid结构体实例。

(2)
创建一个新进程时,进程可能在多个命名空间是可见的。对于每个局部命名空间,都要生成一个局部PID,即struct upid,pid->numbers[i]就表示一个局部PID(struct upid),其中pid->numbers[i].nr = nr,nr是局部数值PID,使用 alloc_pidmap 分配。内核使用一个位图进行分配局部数值PID,其中每一个数值PID有一个bit标识,分配一个空闲的数值PID,将位图中的某一bit由0置1,释放一个数值PID,将位图中对应的bit由1置0。

(3)
将struct pid中每一个struct upid(pid_chain)添加到其哈希表中。所有的struct upid都在步骤(2)中更新了数据。

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

四、PID有关API

4.1 find_pid_ns

通过进程的局部数值PID和局部数值PID相关联的pid_namespace来获取struct pid实例。
主要用了container_of宏的特性,struct upid是struct pid的结构体成员。

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
    
    
	struct upid *pnr;

	hlist_for_each_entry_rcu(pnr,
			&pid_hash[pid_hashfn(nr, ns)], pid_chain)
		if (pnr->nr == nr && pnr->ns == ns)
			return container_of(pnr, struct pid,
					numbers[ns->level]);

	return NULL;
}
EXPORT_SYMBOL_GPL(find_pid_ns);

4.2 pid_task

通过struct pid找到struct task_struct,获取pid->tasks[type]哈希表中的第一个struct task_struct实例。此struct task_struct实例在struct pid的使用链表中,并且搜索的链表的起始元素的下标为参数type的值。

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;
}
EXPORT_SYMBOL(pid_task);
struct pid_link
{
    
    
	struct hlist_node node;
	struct pid *pid;
}

struct task_struct {
    
    
	......
	/* PID/PID hash table linkage. */
	struct pid_link pids[PIDTYPE_MAX];
	......
}

也是用了container_of宏的特性。

#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

container_of:通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({
      
      			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

总结

每一个进程的struct pid只有一个,但struct upid可以有多个,因为一个进程可以在多个局部命名空间。

参考资料

Linux内核 4.10.0
深入Linux内核架构

猜你喜欢

转载自blog.csdn.net/weixin_45030965/article/details/126394485
今日推荐