当前位置: 首页 > news >正文

Linux内核设计与实现 第三章 进程管理

3.1进程

实际上,进程就是正在执行的程序代码的实时结果。
进程是出于执行期的程序以及相关的资源的总称。
进程的另一个名字是任务。
进程不仅仅局限于一段可执行程序代码通常进程还要包含其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,若干具有内存映射的内存地址空间,若干执行线程,存放全局变量的数据段等

内核调度对象是线程,而不是进程(看看李志军老师的核心级线程与用户级线程)
对Linux而言,不特别区分线程与进程,线程只不过是一种特殊的进程

Linux系统中,这调用函数fork(),创建进程,该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。
exec()
clone()
exit()
wait4()
wait()
waitpid()

3.2进程描述符及任务结构

1)内核把进程的列表存放在叫做任务列表的双向循环链表中。进程描述符中包含一个具体进程的所有信息
在这里插入图片描述
2)分配进程描述符
1)铺垫知识
在进程创建时,内核会为进程创建一系列数据结构,其中最重要的就是上章学习的task_struct结构,它就是进程描述符,表明进程在生命周期内的所有特征。同时,内核为进程创建两个栈,一个是用户栈,一个是内核栈,分别处于用户态和内核态使用的栈

实际上在linux kernel中,task_struct、thread_info都用来保存进程相关信息,即进程PCB信息。然而不同的体系结构里,进程需要存储的信息不尽相同,linux使用task_struct存储通用的信息,将体系结构相关的部分存储在thread_info中。
在内核态,32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 pt_regs 结构上

在内核态,32 位和 64 位的内核栈和 task_struct 的关联关系不同。
x86中32 位主要靠 thread_info,64 位主要靠 Per-CPU 变量,而ARM平台不论是32位还是64位,都是使用thread_info,其原理基本类似。

Linux 给每个 task 都分配了内核栈。在 32 位系统上 arch/x86/include/asm/page_32_types.h,是这样定义的:一个 PAGE_SIZE 是 4K,左移一位就是乘以 2,也就是 8K。但是内核栈在 64 位系统上arch/x86/include/asm/page_64_types.h,是这样定义的:在 PAGE_SIZE 的基础上左移两位,也即 16K,并且要求起始地址必须是 8192 的整数倍。
Linux
2)总结《Linux内核设计与实现》的本节
Linux通过slab分配器分配task_struck结构,这样达到对象复用和缓存着色。(详细见12章)用slab分配器动态生成task_struck时,只需要在向下增长的栈的栈底,在向上增长的栈的栈顶创建结构struct thread_info,thread_info中有一个指向进程描述符的指针(该指针根据slab分配器赋值)
在这里插入图片描述
3)进程描述符的存放
内核通过一个唯一的进程标识值或PID来标识每个进程。PID的最大值默认设置是32768,即short int 的最大值。
内核大部分的处理进程的代码都是直接通过task_struct进行的

通过current宏找到当前正在运行进程的进程描述符的速度是非常重要的。硬件体系结构复杂,硬件资源丰富的专门拿个寄存器来存放指向当前进程的teask_struct的指针。x86这样资源拮据的就只能在内核栈的尾巴创建thread_info结构,通过计算偏移间接地查找task_struct结构。

通过current宏找到当前正在运行进程的进程描述符的时间
=从专门的寄存器读取进程描述符
=计算hread_info的偏移(即用current找到hread_info的位置)+读取hread_info中的进程描述符指针+根据进程描述符指针读取进程描述符

4)进程状态
在这里插入图片描述
在这里插入图片描述

5)设置当前进程状态

set_task_state(task,state)

6)进程上下文
进程上下文:当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。

7)进程家族树
Unix和Linux系统都存在明显的继承关系,所有进程都是PID为1后代。
内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。
拥有同一个父进程的所有进程被称为兄弟。进程描述符中有指向父进程和所有子进程的指针
由于任务列表是双向循环链表,因此遍历系统中的所有进程很容易

3.3进程创建

fork()拷贝当前进程创建子进程。
exec()读取可执行文件并将其载入地址空间开始运行。
1)写时拷贝
fork()系统调用直接把所有的资源复制给新建的进程,效率低下。因此fork()使用写时拷贝页实现,不写时父子进程共享数据,需要写时才另存共享的数据
写时拷贝是一种可以推迟甚至免除拷贝数据的技术。

2)fork()
在这里插入图片描述
在这里插入图片描述

3)vfork()

3.4线程在Linux中的实现

在这里插入图片描述

假如我们有一个包含四个线程的进程,
window系统通常会有一个包含指向四个不同线程的指针的进程描述符,该描述符负责描述像地址空间、打开的文件这样的共享资源。线程本身再去描述它独占的资源。
在这里插入图片描述

Linux系统仅仅创建四个进程并分配四个普通的task_struct结构,建立这四个进程时指定它们共享某些资源
1)创建线程
在这里插入图片描述
在这里插入图片描述

2)内核线程
linux在初始化的时候,除了静态的idle线程,还会创建kernel_init线程和kthreadd线程。kthreadd线程为2号线程,该线程专门用来负责为kernel创建其他线程。下面看一下如何利用kthreadd创建一个内核线程。

//kernel thread create information内核线程创建信息
struct kthread_create_info
{
	/* Information passed to kthread() from kthreadd. */
	int (*threadfn)(void *data); //要创建的线程的执行函数
	void *data;
	int node;
 
	/* Result passed back to kthread_create() from kthreadd. */
	struct task_struct *result; //用来向线程申请者返回task_struct
	struct completion done;//向申请者通知创建完成
 
	struct list_head list;//挂载进kthreadd的处理队列
};

为了容易区分,我们把需要创建新线程的叫做申请者,具体负责创建新进程的叫做执行者,这边执行者就是kthreadd线程。kthread_create_info数据结构用来在申请者和执行者之间传递对象。

a)新线程创建的申请

struct kthread_create_info create;
struct task_struct *task;
create.threadfn = threadfn;   //新建线程的执行函数
create.data = data;
create.node = node;
init_completion(&create.done);//初始化完成量
 
spin_lock(&kthread_create_lock);
list_add_tail(&create.list, &kthread_create_list);//添加到kthreadd执行队列
spin_unlock(&kthread_create_lock);
 
wake_up_process(kthreadd_task);   //唤醒kthreadd线程
wait_for_completion(&create.done);//等待kthreadd线程完成线程创建
task=create.result;               //返回新建线程的描述符
wake_up_process(task);            //唤醒新建线程,这样新线程处于就绪态

b) 新线程创建
kthreadd_task是kthreadd线程的进程描述符,在系统初始化的时候创建:

static noinline void __init_refok rest_init(void)
{
........................................
	pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
........................................
 
}

下面看一下kthreadd线程如何管理调度其它的内核线程:
可以看到,在kthread_create_list链表中获取到申请者传过来的kthread_create_info结构,利用该信息调用create_kthread来创建线程。

int kthreadd(void *unused)
{
	struct task_struct *tsk = current;
 
	/* Setup a clean context for our children to inherit. */
	set_task_comm(tsk, "kthreadd");
	ignore_signals(tsk);
	set_cpus_allowed_ptr(tsk, cpu_all_mask);
	set_mems_allowed(node_states[N_MEMORY]);
	current->flags |= PF_NOFREEZE;
	for (;;) {
		set_current_state(TASK_INTERRUPTIBLE);
		if (list_empty(&kthread_create_list))//如果队列空,睡眠
			schedule();
		__set_current_state(TASK_RUNNING);
		spin_lock(&kthread_create_lock);
		while (!list_empty(&kthread_create_list)) {//队列不为空,则对该队列进行循环,创建线程
			struct kthread_create_info *create;
 
			create = list_entry(kthread_create_list.next,
					    struct kthread_create_info, list);//这个就是申请者传过来的结构
			list_del_init(&create->list);//先从队列上删除该create 
			spin_unlock(&kthread_create_lock);
 
			create_kthread(create);//为申请者创建线程
 
			spin_lock(&kthread_create_lock);
		}
		spin_unlock(&kthread_create_lock);
	}
	return 0;
}

create_kthread()调用kernel_thread()创建kthread线程,参数为create,看一下kernel_thread是如何执行的:

static void create_kthread(struct kthread_create_info *create)
{
	int pid;
 
#ifdef CONFIG_NUMA
	current->pref_node_fork = create->node;
#endif
	/* 我们需要自己的信号处理程序(默认情况下不接受信号)。 */
	pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);//create_kthread()调用kernel_thread()创建kthread线程
	if (pid < 0) {
		create->result = ERR_PTR(pid);
		complete(&create->done);
	}
}

可以看到kthread()是kthreadd()函数创建的线程的入口地址,该函数最终执行到申请者提供的的threadfn函数,至此创建者完成了自己的使命,申请者开始有了自己的新线程,并执行threadfn任务

static int kthread(void *_create)
{
	/* Copy data: it's on kthread's stack复制数据:它在kthread的堆栈上 */
	struct kthread_create_info *create = _create;//内核线程创建信息结构体kthread_create_info
	int (*threadfn)(void *data) = create->threadfn;
	void *data = create->data;
	struct kthread self;
	int ret;
 
	self.flags = 0;
	self.data = data;
	init_completion(&self.exited);
	init_completion(&self.parked);
	current->vfork_done = &self.exited;
 
	/* OK, tell user we're spawned, wait for stop or wakeup 好,告诉用户我们已经生成,等待停止或唤醒*/
	__set_current_state(TASK_UNINTERRUPTIBLE);
	create->result = current;//向申请者返回当前线程的描述符
	complete(&create->done);//告诉申请者,线程创建完成
	schedule(); // 进入休眠状态后,调度去执行申请者的wake_up_process(task); //唤醒新建线程,这样新线程处于就绪态,马上就会执行threadfn新建线程的执行函数
                //kthread会将其所在进程的状态设为TASK_UNINTERRUPTIBLE,然后调用schedule函数。所以,kthread将会使其所在的进程进入休眠状态,直到被别的进程唤醒。如果被唤醒,将会调用create->threadfn(create->data);
	ret = -EINTR;
 
	if (!test_bit(KTHREAD_SHOULD_STOP, &self.flags)) {
		__kthread_parkme(&self);
		ret = threadfn(data);//申请者提供的线程执行函数
	}
	/* we can't just return, we must preserve "self" on stack我们不能只是返回,我们必须在堆栈上保留“self” */
	do_exit(ret);//do_exit(是进程的退出码,是子进程返回给父进程的值)
}

3.5进程终结

1)删除进程的描述符
释放与进程相关联的所有可以释放的资源,进程进入终止状态,它仅占用的内核栈、thread_info结构、tast_struct结构会在其父进程检索后,由其父进程通知内核可以释放它仅占用的内核栈、thread_info结构、tast_struct结构

2)孤儿进程造成的进退维谷
如果夫进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白浪费内存。
解决方法是给子进程在当前线程组内找一个线程作为父亲,如果当前线程组内没有其他线程,就让init做它们的父亲。init会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程

相关文章:

  • Exception in thread “main“ java.lang.NoClassDefFoundError: org/apache/flink/
  • springboot项目整理(持续更新)
  • Linux Shell重定向 管道命令 awk编程 sed文件操作高阶函数
  • jQuery表单选择器:快速选择input标签
  • 6、Java——三种方式循环出水仙花数
  • 前端核心二:VUE
  • Javassist基本用法
  • 电子学会2022年6月青少年软件编程(图形化)等级考试试卷(一级)
  • 【Vue 基础知识】keep-alive是什么?怎么用?
  • 数据结构(四) -- 递归
  • C++的STL--->map和set容器的使用
  • 【Docker系列】Docker生产常用命令01
  • 【编程题】【Scratch四级】2021.06 从小到大排序
  • Framwork入门のPiex 6P源码(下载/编译/刷机)
  • 极简idea下git操作(二)
  • (十五)java多线程之并发集合ArrayBlockingQueue
  • es6要点
  • Gradle 5.0 正式版发布
  • JDK9: 集成 Jshell 和 Maven 项目.
  • JS笔记四:作用域、变量(函数)提升
  • JS函数式编程 数组部分风格 ES6版
  • Leetcode 27 Remove Element
  • Linux gpio口使用方法
  • spark本地环境的搭建到运行第一个spark程序
  • SwizzleMethod 黑魔法
  • 阿里云前端周刊 - 第 26 期
  • 每天10道Java面试题,跟我走,offer有!
  • 少走弯路,给Java 1~5 年程序员的建议
  • 手机端车牌号码键盘的vue组件
  • 我从编程教室毕业
  • 一天一个设计模式之JS实现——适配器模式
  • 智能合约开发环境搭建及Hello World合约
  • 400多位云计算专家和开发者,加入了同一个组织 ...
  • Mac 上flink的安装与启动
  • Semaphore
  • ​iOS实时查看App运行日志
  • ​Python 3 新特性:类型注解
  • #Linux(权限管理)
  • #pragam once 和 #ifndef 预编译头
  • (1)(1.8) MSP(MultiWii 串行协议)(4.1 版)
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (2)STM32单片机上位机
  • (5)STL算法之复制
  • (Java)【深基9.例1】选举学生会
  • (SpringBoot)第七章:SpringBoot日志文件
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (二)什么是Vite——Vite 和 Webpack 区别(冷启动)
  • (分享)自己整理的一些简单awk实用语句
  • (附源码)ssm高校运动会管理系统 毕业设计 020419
  • (每日持续更新)jdk api之FileReader基础、应用、实战
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (十一)手动添加用户和文件的特殊权限
  • (转)项目管理杂谈-我所期望的新人
  • ******IT公司面试题汇总+优秀技术博客汇总