• 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)
    
    • 1

    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的处理队列
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    为了容易区分,我们把需要创建新线程的叫做申请者,具体负责创建新进程的叫做执行者,这边执行者就是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);            //唤醒新建线程,这样新线程处于就绪态
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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);
    ........................................
     
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    下面看一下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;
    }
    
    • 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

    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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到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(是进程的退出码,是子进程返回给父进程的值)
    }
    
    • 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

    3.5进程终结

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

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

  • 相关阅读:
    C++中显示构造和隐式构造
    C++11 - 右值引用
    Redis7--基础篇1(概述,安装、卸载及配置)
    HTML基础学习第一课
    Lua04——基本语法
    【LeetCode滑动窗口专题#2】无重复字符的最长子串
    k8s的service自动发现服务:实战版
    Transformer:开源机器学习项目,上千种预训练模型 | 开源日报 No.66
    内链外链抓取生成sitemap和主动推送的技巧
    [网络工程师]-应用层协议-SNMP
  • 原文地址:https://blog.csdn.net/weixin_55255438/article/details/126656749