文章

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。

进程与命名空间的关联

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct nsproxy {
  atomic_t count;
  //Unix Time-sharing System 控制主机名和域名的隔离。通过 UTS 命名空间,不同的容器或进程可以感知自己的主机名和域名,从而实现隔离和独立运行。
  struct uts_namespace *uts_ns;
  //所有与进程间通信(IPC)有关的信息。
  struct ipc_namespace *ipc_ns;
  //已经装载的文件系统的视图
  struct mnt_namespace *mnt_ns;
  //有关进程ID的信息,请看下文pid_namespace.h。
  struct pid_namespace *pid_ns;
  //用于隔离用户和用户组,和用于限制用户资源的信息
  struct user_namespace *user_ns;
  //网络相关的命名空间参数
  struct net *net_ns;
}; 

uts命名空间

1
2
3
4
5
6
7
8
9
10
11
12
struct uts_namespace {
  struct kref kref; /* 引用计数器 */
  struct new_utsname name;
}; 
struct new_utsname {
  char sysname[65]; /* 系统名称 */
  char nodename[65];
  char release[65]; /* 内核发布版本 */
  char version[65];
  char machine[65]; /* 机器名*/
  char domainname[65];
};

用户命名空间

1
2
3
4
5
6
7
struct user_namespace {
  struct kref kref;
  //user_struct的实例散列表
  struct hlist_head uidhash_table[UIDHASH_SZ];
  //该结构维护了一些统计数据(如进程和打开文件的数目)
  struct user_struct *root_user;
}; 

进程id命名空间数据结构

1
2
3
4
5
6
7
8
9
10
struct pid_namespace { 
  ...
  //每个PID命名空间都具有一个进程,其发挥的作用相当于全局的init进程。init的一个目的是对孤儿进程调用wait4,命名空间局部的init变体也必须完成该工作。child_reaper保存了指向该进程的task_struct的指针。
  struct task_struct *child_reaper;
  ...
  //表示当前命名空间在命名空间层次结构中的深度。level的计算比较重要,因为level较高的命名空间中的ID,对level较低的命名空间来说是可见的。从给定的level设置,内核即可推断进程会关联到多少个ID。
  int level;
  //指向父命名空间的指针
  struct pid_namespace *parent;
}

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
//表示特定命名空间的进程PID
struct upid {
  int nr;  /* PID的数值 */
  struct pid_namespace *ns; /* 指向该id所属命名空间的指针 */
  struct hlist_node pid_chain;
};

struct pid
{
  atomic_t count; /* 引用计数 */
  unsigned int level;
  struct hlist_head tasks[PIDTYPE_MAX]; /*使用这一id的进程数组*/
  struct rcu_head rcu;
  struct upid numbers[1]; /* 命名空间的数组 */
};
enum pid_type 
{ 
  PIDTYPE_PID, 
  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
14
15
16
17
18
19
//将pid实例与task_struct绑定
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]);
}

//使用位图分配pid,分配空闲的pid 将位图位置置为1,释放则置为0
static int alloc_pidmap(struct pid_namespace *pid_ns){
  ...
}

//为新进程在本层命名空间与父层命名空间生成PID,并更新pid->numbers[i] upid结构
struct pid *alloc_pid(struct pid_namespace *ns) {
  ...
}

进程复制

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 进行授权