文章

linux内核笔记(二)进程的表示和相关系统调用

linux内核笔记(二)进程的表示和相关系统调用

进程的表示和相关系统调用

进程的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct task_struct {
  volatile long state; /* 进程状态 */
  void *stack; /* 线程栈地址指针 void* 万能指针*/
  ...
  pid_t pid;
  pid_t tgid; /* 线程组id */
  ...
  struct task_struct *group_leader; /* 线程组长 */
  struct nsproxy *nsproxy;  /* 命名空间 */
  struct pid_link pids[PIDTYPE_MAX];  /* 与pid的关联 */
  struct list_head children;     /* 子进程链表 */
  struct list_head sibling;      /* 连接到父进程的子进程链表 */ 
  ...
  struct thread_struct thread;   /* 特定架构cpu的状态信息,包含寄存器等数据*/
  ...
  /* 通过此宏将stack万能指针 指向thread_info中的栈 */
  #define task_thread_info(task)((struct thread_info *)(task)->stack)
}

进程的状态

Thread Status 进程状态流转

  • 运行 :该进程此刻正在执行。

  • 等待 :进程能够运行,但没有得到许可,因为CPU分配给另一个进程。调度器可以在下一次任务切换时选择该进程。

  • 睡眠 :进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。

进程ID

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

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

  • 处于某个线程组(在一个进程中,以标志CLONE_THREAD来调用clone建立的该进程的不同的执行上下文)中的所有进程都有统一的线程组ID (TGID)。如果进程没有使用线程,则其PID和TGID相同。线程组中的主进程被称作组长 (group leader)。通过clone创建的所有线程的task_structgroup_leader成员,会指向组长的task_struct实例
  • 独立进程可以合并成进程组 (使用setpgrp 系统调用)。进程组成员的task_struct 的pgrp 属性值都是相同的,即进程组组长的PID。

进程与命名空间的关联

Linux 命名空间是实现容器技术(如 Docker、LXC)和沙箱隔离的关键基础。它提供了一种机制,使得进程可以拥有自己独立的系统资源视图,而这些视图与其他进程的视图是隔离的。

命名空间层次关联 命名空间层次关联

进程与命名空间的关联 进程与命名空间的关联

命名空间与进程关联的数据结构-nsproxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct nsproxy {
  refcount_t count;   // 引用计数
  // Unix Time-sharing System 控制主机名和域名的隔离。
  // 通过 UTS 命名空间,不同的容器或进程可以感知自己的主机名和域名,从而实现隔离和独立运行。
  struct uts_namespace *uts_ns;
  struct ipc_namespace *ipc_ns;   // 进程间通信(IPC) ns视图。
  struct mnt_namespace *mnt_ns;   // 文件系统的ns视图
  struct pid_namespace *pid_ns;   // 进程ID的ns视图
  struct net *net_ns;             // 网络命名空间视图
  struct time_namespace *time_ns; // 时钟命名空间视图
	struct time_namespace *time_ns_for_children;  //  子进程的时钟命名空间视图
	struct cgroup_namespace *cgroup_ns; // cgroup命名空间视图
}; 

内核命名空间通用结构体

1
2
3
4
5
6
struct ns_common {
	struct dentry *stashed; // 扩展的dentry信息
	const struct proc_ns_operations *ops; // 命名空间操作函数指针
	unsigned int inum;      // 命名空间的inode编号
	refcount_t count;       // 引用计数
};

uts命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct uts_namespace {
	struct new_utsname name;        // 存储主机名、域名等UTS信息
	struct user_namespace *user_ns; // 关联的用户命名空间(用于权限控制)
	struct ucounts *ucounts;        // 用户资源计数(用于容器限制)
	struct ns_common ns;            // 内核命名空间通用结构体 包含了引用计数
}
#define __NEW_UTS_LEN 64
struct new_utsname {
  char sysname[__NEW_UTS_LEN + 1];   // 操作系统名称(如 "Linux")
  char nodename[__NEW_UTS_LEN + 1];  // 主机名(通过 `hostname` 命令查看)
  char release[__NEW_UTS_LEN + 1];   // 内核版本
  char version[__NEW_UTS_LEN + 1];   // 内核编译版本信息
  char machine[__NEW_UTS_LEN + 1];   // 硬件架构(如 "x86_64")
  char domainname[__NEW_UTS_LEN + 1];// 域名(NIS域名)
};

用户命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct user_namespace {
  struct uid_gid_map	uid_map;    // UID 映射表
	struct uid_gid_map	gid_map;    // GID 映射表
	struct uid_gid_map	projid_map; // Project ID 映射表
	struct user_namespace	*parent;  // 父用户命名空间的引用
	int			level;                  // 命名空间嵌套级别
	kuid_t			owner;              // 命名空间的创建者 UID
	kgid_t			group;              // 命名空间的创建者 GID
	struct ns_common	ns;           // 内核命名空间通用结构体
	unsigned long		flags;          // 命名空间标
  struct ucounts		*ucounts;     // 用户计数器
}; 

// union 结合体 uid_gid_map的数据较少时,使用数组方便管理;数量较多时使用链表管理;
struct uid_gid_map { /* 64 bytes -- 1 cache line */
	union {
		struct {
			struct uid_gid_extent extent[UID_GID_MAP_MAX_BASE_EXTENTS];
			u32 nr_extents;
		};
		struct {
			struct uid_gid_extent *forward;
			struct uid_gid_extent *reverse;
		};
	};
};

struct uid_gid_extent {
	u32 first;        // 命名空间内的起始 ID
	u32 lower_first;  // 父命名空间或宿主系统中的起始 ID
	u32 count;        // 映射的 ID 数量
};
// e.g. {first=0, lower_first=100000, count=1000},则命名空间内的 UID 0 映射到宿主系统的 UID 100000,UID 1 映射到 100001,以此类推

进程ID的命名空间数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct pid_namespace {
	struct idr idr;                 //(ID Allocator)数据结构,用于管理 PID 分配
	struct rcu_head rcu;            // RCU(Read-Copy-Update)头,用于延迟释放命名空间
	unsigned int pid_allocated;     // 已分配的 PID 数量
	struct task_struct *child_reaper;// 负责处理子进程的信号(如 SIGCHLD)和清理僵尸进程。
	struct kmem_cache *pid_cachep;  // 指向分配 struct pid 的 slab 缓存。
	unsigned int level;             // 表示 PID 命名空间的嵌套深度
	struct pid_namespace *parent;   // 父命名空间
	struct user_namespace *user_ns; // 关联的用户命名空间
	struct ucounts *ucounts;        // 用户计数器
  // 当命名空间内的 child_reaper(PID 1)退出时,可能触发命名空间“重启”,影响子进程的行为。
	int reboot;	                    
	struct ns_common ns;
} 

pid与task_struct的互相关联 pid与task_struct的互相关联

pid数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//表示特定命名空间的进程PID
struct upid {
  int nr;  /* PID的数值 */
  struct pid_namespace *ns; /* 指向该id所属命名空间的指针 */
};

struct pid
{
  refcount_t count;       // 引用计数
	unsigned int level;     // PID 命名空间嵌套级别
	spinlock_t lock;        // 自旋锁,保护 PID 数据
	struct dentry *stashed; // 扩展的dentry信息
	struct hlist_head tasks[PIDTYPE_MAX]; // 关联的进程列表
  ...
	struct rcu_head rcu;    // RCU 延迟释放头
	struct upid numbers[];  // 灵活数组,存储多级 PID 信息
};

<linux/pid_types.h> ->
enum pid_type 
{ 
  PIDTYPE_PID, 
  PIDTYPE_TGID,  // 线程组id
  PIDTYPE_PGID,  // 进程组id 
  PIDTYPE_SID,   // 会话id 
  PIDTYPE_MAX 
}; 

pid新实例 attach到task_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct pid **task_pid_ptr(struct task_struct *task, enum pid_type type)
{
	return (type == PIDTYPE_PID) ?
		&task->thread_pid :
		&task->signal->pids[type];
}
//将pid实例与task_struct绑定 必须持有 tasklist_lock 的写锁
void attach_pid(struct task_struct *task, enum pid_type type)
{
	struct pid *pid = *task_pid_ptr(task, type);
	hlist_add_head_rcu(&task->pid_links[type], &pid->tasks[type]);
}

为进程分配pid实例

idr分配器

IDR 分配器是一个基于xarray 包装的整数 ID 管理机制,设计目标是高效分配和回收整数 ID,同时支持动态范围的 ID 分配。

1
2
3
4
5
6
7
8
9
struct idr {
	struct radix_tree_root	idr_rt;
	unsigned int		idr_base;       // ID 分配的起始基数(通常为 0 或 1)
	unsigned int		idr_next;       // 下一个分配的 ID
};
/*
 * include/linux/radix-tree.h
 * #define radix_tree_root		xarray 
 */

Xarray 数据结构

Xarray 的核心是一个灵活的数组,通过 64 位无符号长整型 index 索引 64 位 entry (默认为 NULL)。它维护着 Index 到 entry 的映射关系,类似于一个 Radix Tree。

Xarray 中的 entry 存储单位称为 Slot,可存放特定范围的整数值、字节对齐的指针或 NULL。Xarray 会利用 entry 值的末尾比特位存储类型信息,以便区分是数值、指针还是空值,并支持 Tag 等特性

xarray xarray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct xarray {
	spinlock_t	xa_lock;  // 自旋锁,保护并发访问
	gfp_t		xa_flags;     // 内存分配标志
	void __rcu *	xa_head;// 指向根节点的 RCU 指针
};

struct xa_node {
	unsigned char	shift;		// 节点层级的位移(决定索引范围)
	unsigned char	offset;		// 在父节点中的偏移量
	unsigned char	count;		// 非空槽位数
	unsigned char	nr_values;	// 值(非指针)槽位数
	struct xa_node __rcu *parent;	// 父节点
	struct xarray	*array;		// 所属 XArray
	void __rcu	*slots[XA_CHUNK_SIZE]; // 槽位数组,存储子节点或数据 64个
  ...
};

struct xa_state {
	struct xarray *xa;      // 目标 XArray
	unsigned long xa_index; // 当前操作的索引
	unsigned char xa_shift; // 当前节点的位移
	unsigned char xa_offset;// 当前槽位偏移
	unsigned char xa_pad;		// 编译器内存对齐填充
	struct xa_node *xa_node;// 当前节点
  ...
};

void *xa_load(struct xarray *, unsigned long index);
void *xa_store(struct xarray *, unsigned long index, void *entry, gfp_t);
void *xa_erase(struct xarray *, unsigned long index);
void *xa_store_range(struct xarray *, unsigned long first, unsigned long last,
			void *entry, gfp_t);

为进程分配pid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/* 
 * 核心函数: 为task_struct绑定pid实例  ns: 命名空间 set_tid: 指定的pid数组  
 * set_tid_size: pid数组的长度
 */
struct pid *alloc_pid(struct pid_namespace *ns, pid_t *set_tid,
		      size_t set_tid_size)
{
	struct pid *pid;
	enum pid_type type;
	int i, nr;
	struct pid_namespace *tmp;
	struct upid *upid;
	int retval = -ENOMEM;

	if (set_tid_size > ns->level + 1)
		return ERR_PTR(-EINVAL);
  // 申请pid实例内存
	pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);
	if (!pid)
		return ERR_PTR(retval);

	tmp = ns;
	pid->level = ns->level; //继承ns的level

  // 从当前层级ns开始,向上层级遍历 分配各层ns下的pid
	for (i = ns->level; i >= 0; i--) { 
		int tid = 0;

		if (set_tid_size) { // 如果指定了pid数组
			tid = set_tid[ns->level - i];

			retval = -EINVAL;
			if (tid < 1 || tid >= pid_max)
				goto out_free;
			// 如果tid不是1 而且没有命名空间的僵尸进程清理器 也报错
			if (tid != 1 && !tmp->child_reaper)
				goto out_free;
			retval = -EPERM;
			if (!checkpoint_restore_ns_capable(tmp->user_ns))
				goto out_free;
			set_tid_size--;
		}

		idr_preload(GFP_KERNEL);
		spin_lock_irq(&pidmap_lock);  // pid位图加锁

		if (tid) {
      // 使用idr分配器 分配指定的pid
			nr = idr_alloc(&tmp->idr, NULL, tid,
				       tid + 1, GFP_ATOMIC);
			if (nr == -ENOSPC)
				nr = -EEXIST;
		} else {
			int pid_min = 1;
			if (idr_get_cursor(&tmp->idr) > RESERVED_PIDS)
				pid_min = RESERVED_PIDS;

			// 使用idr分配器 自动分配pid
			nr = idr_alloc_cyclic(&tmp->idr, NULL, pid_min,
					      pid_max, GFP_ATOMIC);
		}
		spin_unlock_irq(&pidmap_lock); // pid位图解锁
		idr_preload_end();

		if (nr < 0) {
			retval = (nr == -ENOSPC) ? -EAGAIN : nr;
			goto out_free;
		}

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

	retval = -ENOMEM;

	get_pid_ns(ns);
	refcount_set(&pid->count, 1);
	spin_lock_init(&pid->lock);
	for (type = 0; type < PIDTYPE_MAX; ++type)
		INIT_HLIST_HEAD(&pid->tasks[type]);

	init_waitqueue_head(&pid->wait_pidfd);
	INIT_HLIST_HEAD(&pid->inodes);
  // 从灵活数组中 定位当前命名空间(ns)中对应的 upid 结构 
	upid = pid->numbers + ns->level;
	spin_lock_irq(&pidmap_lock);
	if (!(ns->pid_allocated & PIDNS_ADDING))
		goto out_unlock;
	pid->stashed = NULL;
	pid->ino = ++pidfs_ino;
	for ( ; upid >= pid->numbers; --upid) {
		/* 
     * 通过 idr_replace() 将 PID 插入到对应命名空间的 IDR 树中
     * 增加命名空间的 PID 分配计数
     */
		idr_replace(&upid->ns->idr, pid, upid->nr);
		upid->ns->pid_allocated++;
	}
	spin_unlock_irq(&pidmap_lock);

	return pid;

out_unlock:
	spin_unlock_irq(&pidmap_lock);
	put_pid_ns(ns);

out_free: // 释放被分配的内存、pid 报错
	spin_lock_irq(&pidmap_lock);
	while (++i <= ns->level) {
		upid = pid->numbers + i;
		idr_remove(&upid->ns->idr, upid->nr);
	}

	/* On failure to allocate the first pid, reset the state */
	if (ns->pid_allocated == PIDNS_ADDING)
		idr_set_cursor(&ns->idr, 0);

	spin_unlock_irq(&pidmap_lock);

	kmem_cache_free(ns->pid_cachep, pid);
	return ERR_PTR(retval);
}

进程复制

Linux实现了3个进程复制的系统调用。

  1. fork 建立父进程的一个完整副本,然后作为子进程执行。为减少与该调用相关的工作量,Linux使用了写时复制 (copy-on-write)技术。
  2. vfork 类似于fork ,但并不创建父进程数据的副本。相反,父子进程之间共享数据。这节省了大量CPU时间(如果一个进程操纵共享数据,则另一个会自动注意到)。vfork 设计用于子进程形成后立即执行execve 系统调用加载新程序的情形。在子进程退出或开始新程序之前,内核保证父进程处于堵塞状态。
  3. clone 产生线程,可以对父子进程之间的共享、复制进行精确控制。

fork 、vfork 和clone 系统调用的入口点分别是sys_fork 、sys_vfork 和sys_clone 函数。其定义依赖于具体的体系结构,因为在用户空间和内核空间之间传递参数的方法因体系结构而异。上述函数的任务是从处理器寄存器中提取由用户空间提供的信息,调用体系结构无关的do_fork 函数,后者负责进程复制。

进程复制的实现 进程复制的实现

do_fork 函数签名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
long do_fork(
  unsigned long clone_flags,   /*指定控制复制过程的一些属性。最低字节指定了在子进程终止时被发给父进程的信号号码。其余的高位字节保存了各种常数。不同的fork变体,主要是通过标志集合区分*/
  unsigned long stack_start,   /*用户状态下栈的起始地址*/
  struct pt_regs *regs,        /*指向寄存器集合的指针,其中以原始形式保存了调用参数*/
  unsigned long stack_size,    /*用户状态下栈的大小*/
  int __user *parent_tidptr,   /*指向用户空间中父子进程的TID指针*/
  int __user *child_tidptr)


static struct task_struct *copy_process(
  unsigned long clone_flags,
  unsigned long stack_start,
  struct pt_regs *regs,
  unsigned long stack_size,
  int __user *child_tidptr,
  struct pid *pid)

内核线程

内核线程是直接由内核本身启动的进程,其实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行,有两种类型的内核线程:

  1. 线程启动后一直等待,直至内核请求线程执行某一特定操作。
  2. 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制值时采取行动。内核使用这类线程用于连续监测任务。

内核线程与其他线程的不同:

  1. 它们在CPU的管态(supervisor mode)执行,而不是用户状态。
  2. 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE 的所有地址),但不能访问用户空间。

运行新程序

Linux提供的execve系统调用通过用新代码替换现存程序,启动新程序,类似于fork(系统调用)->sys_fork(依托于架构体系的处理)->do_fork(无关架构体系的函数),execve(系统调用)->sys_execve(依托于架构体系的处理)->do_execve函数。

运行新程序的实现 运行新程序的实现

do_execve 函数签名

1
2
3
4
int do_execve(const char * filename,     /* 可执行文件名称 */
  const char __user *const __user *argv, /* 程序的参数 */
  const char __user *const __user *envp, /* 环境的指针 */
  struct pt_regs * regs /* 寄存器*/)

进程退出

Linux进程必须用exit系统调用终止进程使内核将进程使用的资源释放回系统。该调用的入口点是sys_exit 函数,需要一个错误码作为其参数,以便退出进程,其将工作大部分委托给了do_exit函数。该函数的实现就是将各个引用计数器减1,如果引用计数器归0而没有进程再使用对应的结构,那么将相应的内存区域返还给内存管理模块。

本文由作者按照 CC BY 4.0 进行授权