用Linux5.0的视角看《Linux内核设计与实现》——(一)进程管理

本文回顾《LinuxKernelDevelopment》一书,探讨了进程管理在Linux内核5.13版本中的更新,包括进程与线程概念、task_struct结构变化、进程创建与终结机制,以及现代内核中的技术演进。
摘要由CSDN通过智能技术生成

趁新买的硬件还在路上,给自己再挖一个新坑。之前一直很喜欢《Linux Kernel Development》这本书,这本书写得简洁明了,而又章章经典,中译本仅300多页,把整个Linux Kernel的基本设计框架讲解得明明白白。虽说经典永远都是经典,但毕竟当时的内核只发行到2.6版本,而经过广大开源开发者的贡献,内核现在已经发展到5.13版了。书中的内容有哪些已经被改得天翻地覆,又有哪些经典永存呢?想到这里,突然觉得重温这本书和对照现在的代码会是一件十分有趣的事情。这就是大概为什么我会开始这个新坑。所有内容建议和书对应章节一起阅读。

2021/6/28

第三章 进程管理

本章引入进程的概念,进程是Unix操作系统抽象概念中最基本的一种。其中涉及进程的定义以及相关的概念,比如线程;然后讨论Linux内核如何管理每个进程:他们在内核中如何被列举,如何创建,最终如何消亡。我们拥有操作系统就是为了运行用户程序,因此,进程管理就是所有操作系统的心脏所在,Linux也不例外。

3.1 进程

进程(Task)就是处于执行期的程序(目标代码存放在某种存储介质上),通常还包含其他资源,如打开的文件,挂起的信号,内核内部数据,处理器状态,内存地址空间以及一个或多个线程。
线程(Thread),是进程中的活动对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器,是内核调度的对象。

3.2 进程描述符及任务结构

内核把进程的列表存放在任务队列(task list),它是一个双向链表,链表中的每一项都是类型为task_struct,被称为进程描述描述符(process descriptor)的结构,该结构定义在<linux/sched.h>文件中。进程描述符包含一个具体进程的所有信息,包括打开的文件,进程的地址空间,挂起的信号,进程的状态等。在书中提到,当时结构体的大小已经有1.7KB,而我尝试在Ubuntu16.04看了一下当前task_struct的大小,现在足足有9KB!不得不说硬件算力的提升给软件提供了更大更广阔的发展空间,那么接下来我们再仔细看看task_struct是在Linux kernel中如何存放和使用的吧。

3.2.1-3.2.2进程描述符的存放

在书中提到,每个进程的task_struct通过slab进行分配,并记录在thread_info中,如下图,task_struct保存在thread_info的其实地址。
image.png

而令我惊讶的是,在Linux5.13的thread_info中,已经不存在task_struct的指针了。
其实,在32bit或以下的架构仍然保留着task_struct在thread_info中。而在64bit系统中,内核提供了一个get_current函数,以arm64架构举例,在arch/arm64/include/asm/current.h中,我们能找到这样一段代码。

static __always_inline struct task_struct *get_current(void)
{
	unsigned long sp_el0;
	asm ("mrs %0, sp_el0" : "=r" (sp_el0));
	return (struct task_struct *)sp_el0;
}

可以暂时理解为task_struct的地址会被保存在sp_el0中,我们直接读取保存在sp_el0的地址便可以找到task_struct,至于怎么放在sp_el0的,sp_el0有什么作用,可以以后慢慢了解。同时在task_struct中保存了thread_info,所以可以理解为现在是调转过来了,真是风水轮流转,同时也说明了task_struct和thread_info是紧密联系的。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	struct thread_info		thread_info;
#endif
……
}

而由于当前主流64bit架构CPU都已经提供单独的寄存器存放task_struct,再也不需要通过复杂的运算得到该结构体了。

3.2.2-3.2.6 task_struct中的重要成员

  • PID
    内核通过一个唯一的进程标识值(process identification value)或者PID来标识每个进程

  • 进程状态
    在task_struct中,task->state会记录进程的状态,系统主要用到的5种状态,依然延续到了当前内核版本,但同时又多了几种延伸的状态,可参考当前的状态定义,至于用途可在状态切换章节深挖。

/* Used in tsk->state: */
#define TASK_RUNNING			0x0000
#define TASK_INTERRUPTIBLE		0x0001
#define TASK_UNINTERRUPTIBLE		0x0002
#define __TASK_STOPPED			0x0004
#define __TASK_TRACED			0x0008
/* Used in tsk->exit_state: */
#define EXIT_DEAD			0x0010
#define EXIT_ZOMBIE			0x0020
#define EXIT_TRACE			(EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED			0x0040
#define TASK_DEAD			0x0080
#define TASK_WAKEKILL			0x0100
#define TASK_WAKING			0x0200
#define TASK_NOLOAD			0x0400
#define TASK_NEW			0x0800
#define TASK_STATE_MAX			0x1000

但主体依然遵从这个经典的图,且状态切换的代码实现也没有改变(通过内存屏障保证CPU cache一致)。
image.png

  • 进程上下文
    可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,我们称之为陷入内核空间。此时,可以称为内核“代表进程执行”并处于进程上下文。

  • 进程家族树
    在Linux中,所有的进程都是PID为1——init进程的后代。系统中的每个进程都必有一个父进程,相应也可以拥有多个子进程。拥有同个父进程的所有进程被称为兄弟进程,以下为描述进程关系树的结构体成员。

	/*
	 * Pointers to the (original) parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p->father can be replaced with
	 * p->real_parent->pid)
	 */

	/* Real parent process: */
	struct task_struct __rcu	*real_parent;

	/* Recipient of SIGCHLD, wait4() reports: */
	struct task_struct __rcu	*parent;

	/*
	 * Children/sibling form the list of natural children:
	 */
	struct list_head		children;
	struct list_head		sibling;
	struct task_struct		*group_leader;

3.3 进程创建

进程的创建经过,fork()——通过拷贝进程创建一个子进程,和exec()——负责读取可执行文件并将其载入地址空间开始运行。

在Linux5.13中fork的实现:
普通程序调用fork()–>库函数fork()–>系统调用(fork功能号)–>由功能号在 sys_call_table[]中寻到sys_fork()函数地址–>调用kernel_clone(),这就完成拉从用户态到内核态的变化过程。而sys_fork主要动作都在kernel_clone中实现。

在实现中虽然函数名从do_fork改成kernel_clone,但大致实现是一致的,主要在copy_process中实现主要功能。

在copy_process中,如书中所讲,总体上现在的代码流程基本上没变:

  1. dup_task_struct(),获取一个新的task_struct并将父进程的task_struct拷贝到新的task_struct,同时获取并设置thread stack,获取vm区域等。
  2. 区别父子的task_struct,包括设置父子、兄弟进程间的关系,清零一些标志位、关键信息等。
  3. sched_fork,和调度器相关的初始化,同时把任务状态设置成TASK_NEW(2.6中是TASK_UNINTERRUPTBLE),保证任务不会被调度。
  4. 获取PID,并根据新建的是否为线程,区别设置任务资源。

3.3.3 vfork

vfork 和创建thread其实就是kernel_clone时带入的clone_flag不一样。
从调用kernel_clone的函数中能找到,

SYSCALL_DEFINE0(vfork)
{
	struct kernel_clone_args args = {
		.flags		= CLONE_VFORK | CLONE_VM,
		.exit_signal	= SIGCHLD,
	};

	return kernel_clone(&args);
}

但是vfork在当前适合使用的场景越来越少,所以重点来看看thread的创建。

3.4 thread

线程的创建也可以看作是子进程创建,是通过不同的标志位组合实现的。其原理上就是通过CLONE_VM、CLONE_FS、CLONE_FILES、CLONE_SIGHAND等标志位实现与父进程的资源共享,不同的标志位表示了父子进程之间共享不同的资源。

3.5 进程终结

进程终结简单的来讲就是在不影响其他进程的前提下,释放所有进程所占用的资源,依然是在kernel/exit.c中do_exit中实现。流程就不赘述了,基本没有变化:

  1. 判断执行exit的合法性。
  2. 把进程状态设置成PF_EXITING。
  3. 清理资源,包括内存,状态量,计时器,文件句柄,线程等。
  4. 在exit_notify维护进程间的父子关系。
  5. 调用schedule切换到新进程。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值