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)
}
进程的状态
-
运行 :该进程此刻正在执行。
-
等待 :进程能够运行,但没有得到许可,因为CPU分配给另一个进程。调度器可以在下一次任务切换时选择该进程。
-
睡眠 :进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。
进程ID
UNIX进程总是会分配一个号码用于在其命名空间中唯一地标识它们。该号码被称作进程ID号,简称PID。用fork 或clone 产生的每个进程都由内核自动地分配了一个新的唯一的PID值。
但每个进程除了PID这个特征值之外,还有其他的ID。有下列几种可能的类型。
- 处于某个线程组(在一个进程中,以标志CLONE_THREAD来调用clone建立的该进程的不同的执行上下文)中的所有进程都有统一的线程组ID (TGID)。如果进程没有使用线程,则其PID和TGID相同。线程组中的主进程被称作组长 (group leader)。通过clone创建的所有线程的
task_struct
的group_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数据结构
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 等特性
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个进程复制的系统调用。
fork
建立父进程的一个完整副本,然后作为子进程执行。为减少与该调用相关的工作量,Linux使用了写时复制 (copy-on-write)技术。vfork
类似于fork ,但并不创建父进程数据的副本。相反,父子进程之间共享数据。这节省了大量CPU时间(如果一个进程操纵共享数据,则另一个会自动注意到)。vfork 设计用于子进程形成后立即执行execve 系统调用加载新程序的情形。在子进程退出或开始新程序之前,内核保证父进程处于堵塞状态。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)
内核线程
内核线程
是直接由内核本身启动的进程,其实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行,有两种类型的内核线程:
- 线程启动后一直等待,直至内核请求线程执行某一特定操作。
- 线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制值时采取行动。内核使用这类线程用于连续监测任务。
内核线程与其他线程的不同:
- 它们在CPU的管态(supervisor mode)执行,而不是用户状态。
- 它们只可以访问虚拟地址空间的内核部分(高于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而没有进程再使用对应的结构,那么将相应的内存区域返还给内存管理模块。