《操作系统真象还原》第九章 线程(一) 在内核中实现线程

第九章 线程(一) 在内核中实现线程

本文是对《操作系统真象还原》第九章(一)学习的笔记,欢迎大家一起交流。

我们在本节的任务:

  1. 创建并初始化PCB
  2. 模拟pthread_create函数创建线程并执行线程函数

首先我们要明确内核级线程的优势,内核级线程是cpu的一个调度单位,当一个进程中的线程越多,享受cpu服务的时间也就越多。所谓线程,其实也就是去执行一个函数,和在进程的没有本质区别,但是借助书中的一个例子,我们喜欢吃黄瓜,宫保鸡丁里面有黄瓜,但是我们也可以点一个拍黄瓜让厨师专门做黄瓜,线程所执行的函数也是这样的,执行整个进程时可以顺便执行这个函数,也可以新起一个线程专门执行这个函数。

准备的数据结构

进程/线程状态

/* 进程或线程的状态 */
enum task_status {TASK_RUNNING,TASK_READY,TASK_BLOCKED,TASK_WAITING,TASK_HANGING,TASK_DIED
};

线程栈

定义线程栈,存储线程执行时的运行信息

/***********  线程栈thread_stack  ************ 线程自己的栈,用于存储线程中待执行的函数* 此结构在线程自己的内核栈中位置不固定,* 用在switch_to时保存线程环境。* 实际位置取决于实际运行情况。******************************************/
struct thread_stack {uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;//这个位置会放一个名叫eip,返回void的函数指针(*epi的*决定了这是个指针),//该函数传入的参数是一个thread_func类型的函数指针与函数的参数地址void (*eip) (thread_func* func, void* func_arg);//以下三条是模仿call进入thread_start执行的栈内布局构建的,call进入就会压入参数与返回地址,因为我们是ret进入kernel_thread执行的//要想让kernel_thread正常执行,就必须人为给它造返回地址,参数void (*unused_retaddr);thread_func* function;           // Kernel_thread运行所需要的函数地址void* func_arg;                  // Kernel_thread运行所需要的参数地址
};

PCB

PCB以后还会进行补充,本节用到的东西如下:

/* 进程或线程的pcb,程序控制块, 此结构体用于存储线程的管理信息*/
struct task_struct {uint32_t* self_kstack;	        // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息enum task_status status;uint8_t priority;		        // 线程优先级char name[16];                   //用于存储自己的线程的名字uint32_t stack_magic;	       //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};

第一个结构self_kstack​即指向thread_stack

一个pcb占一个自然页,即4kb,低地址开始是pcb相关信息,高地址是线程的栈,向低地址扩展,所以在最后一项定义一个魔数,每次对该数值进行校验即可判断有没有溢出。

中断栈

本节中不会用到,但是要为它预留空间,所以先定义

/***********   中断栈intr_stack   ************ 此结构用于中断发生时保护程序(线程或进程)的上下文环境:* 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文* 寄存器,  intr_exit中的出栈操作是此结构的逆操作* 此栈在线程自己的内核栈中位置固定,所在页的最顶端
********************************************/
struct intr_stack
{uint32_t vec_no;	        // kernel.S 宏VECTOR中push %1压入的中断号uint32_t edi;uint32_t esi;uint32_t ebp;uint32_t esp_dummy;	        // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略uint32_t ebx;uint32_t edx;uint32_t ecx;uint32_t eax;uint32_t gs;uint32_t fs;uint32_t es;uint32_t ds;/* 以下由cpu从低特权级进入高特权级时压入 */uint32_t err_code;		    // err_code会被压入在eip之后void (*eip) (void);uint32_t cs;uint32_t eflags;void* esp;uint32_t ss;
};

代码部分

代码逻辑如下:

  1. 向内存申请一页空间,分配给要创建的线程
  2. 初始化该线程的PCB
  3. 通过PCB中的栈顶指针进一步初始化线程栈的运行信息
  4. 正式运行线程执行函数

thread_start

thread_start函数即对应了上面说的代码逻辑,对应第四步的汇编我们后面再说

/* 创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg) */
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_arg)
{/* pcb都位于内核空间,包括用户进程的pcb也是在内核空间 */struct task_struct *thread = get_kernel_pages(1); // 为线程的pcb申请4K空间的起始地址init_thread(thread, name, prio);           // 初始化线程的pcbthread_create(thread, function, func_arg); // 初始化线程的线程栈// 我们task_struct->self_kstack指向thread_stack的起始位置,然后pop升栈,// 到了通过线程启动器来的地址,ret进入去运行真正的实际函数// 通过ret指令进入,原因:1、函数地址与参数可以放入栈中统一管理;2、ret指令可以直接从栈顶取地址跳入执行asm volatile("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g"(thread->self_kstack) : "memory");return thread;
}

初始化pcb

即对task_struct结构体进行初始化

/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct *pthread, char *name, int prio)
{memset(pthread, 0, sizeof(*pthread));   // 把pcb初始化为0pthread->status = TASK_RUNNING;          //这个函数是创建线程的一部分,自然线程的状态就是运行态strcpy(pthread->name, name);pthread->priority = prio;/* self_kstack是线程自己在内核态下使用的栈顶地址 */pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE); // 本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间pthread->stack_magic = 0x19870916; // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}

我们前面说过了一个pcb占一个自然页,即4kb,低地址开始是pcb相关信息,高地址是线程的栈,向低地址扩展,故有

pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);

目前的pcb布局如下(中断栈和线程栈现在还没有,下一步就有了):

image

初始化线程栈

/*用于根据传入的线程的pcb地址、要运行的函数地址、函数的参数地址来初始化线程栈中的运行信息,核心就是填入要运行的函数地址与参数 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {/* 先预留中断使用栈的空间,可见thread.h中定义的结构 *///pthread->self_kstack -= sizeof(struct intr_stack);  //-=结果是sizeof(struct intr_stack)的4倍//self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数pthread->self_kstack = (uint32_t*)((int)(pthread->self_kstack) - sizeof(struct intr_stack));//再预留thread_stack的位置pthread->self_kstack = (uint32_t*)((int)pthread->self_kstack) - sizeof(struct thread_stack);//我们已经留出了线程栈的空间,现在将栈顶变成一个线程栈结构体//指针,方便我们提前布置数据达到我们想要的目的struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;   kthread_stack->function = function;kthread_stack->func_arg = func_arg;//我们将线程的栈顶指向这里,并ret,就能直接跳入线程启动器开始执行。//为什么这里我不能直接填传入进来的func,这也是函数地址啊,为什么还非要经过一个启动器呢?其实是可以不经过线程启动器的kthread_stack->eip = kernel_thread;   //下面的寄存器用不到, 先置为0kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;//因为用不着,所以不用初始化这个返回地址kthread_stack->unused_retaddr}

首先先预留中断时使用的栈,然后再预留线程栈的空间,预留完之后对线程栈进行初始化。

kernel_thread是通用的线程启动器,里面核心是执行function(func_arg)​,我们将eip初始化为该值,后面就可以直接去这个函数执行,后面再详说。

目前的pcb布局如下:

image

thread_start中的关键汇编

    /*4.上述准备好线程运行时的栈信息后,即可运行执行函数了*/asm volatile("movl %0,%%esp;    \pop %%ebp;          \pop %%ebx;          \pop %%edi;          \pop %%esi;          \ret":: "g"(thread->self_kstack): "memory");

当来到这里时,首先将esp赋为thread->self_kstack,也就是pcb线程栈的最下端,然后不断pop,pop完四个寄存器,然后ret,此时正好对应线程启动器的指针,如下图:

image

然后执行ret,eip就会来到线程启动器,其中esp自动+4,布局如下:

image

然后再往下一步很多人讲错了,我们去执行线程启动器,反汇编如下:

image

会再push ebp,此时内存中布局如下,正好符合取参规范

image

可以验证以下下面的数据和上面的数据。

image

于是接下来,根据c语言的函数调用约定,kernel_thread​会取出占位的返回地址上边的两个参数,也就是执行函数的地址与执行函数的参数,然后调用执行函数运行

kernel_thread​如下:

// 线程启动器
/* 由kernel_thread去执行function(func_arg) , 这个函数就是线程中去开启我们要运行的函数*/
static void kernel_thread(thread_func* function, void* func_arg) {function(func_arg); 
}

​​

​​

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/872927.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1.21 javaweb学习

今天学习了html中onsubmit的使用 onsubmit事件处理器是专门用于表单(form)的提交事件,所以要注意div标签是不能直接使用onsubmit的 今天在作业项目中出现了这样的问题,将onsubmit放在了div标签中,导致数据无法被正常处理,上传数据有误,修改至form后问题解决 修改前数据提…

思通数科舆情系统的分析报告主要内容及其市场价值探析

思通数科舆情系统的分析报告广泛应用于以下几个领域: (1) 企业品牌管理与危机预警:系统能够自动发出警报,为企业的公关部门提供应对策略和决策依据,帮助企业迅速做出反应,避免危机的进一步扩展。 (2) 政府舆情监管与社会治理:政府部门能够利用该系统的热点事件排名、舆情…

北汇信息致客户的一封感谢信

北汇信息致客户的一封感谢信尊敬的客户:感谢您选择北汇信息!2024年是不平凡的一年,中国汽车产量再创新高,出海与内卷挑战不断。北汇信息作为汽车电子测试领域的服务商,秉承“价值创造、共享成功”的理念,一直致力于为国内外汽车客户提供优质的产品和服务,共同面对这些挑…

001 修改博客园侧边栏的顺序

打开配置页:https://i.cnblogs.com/settings在“博客侧边栏公告”添加代码<script> $(document).ready(function(){//returnvar list=[sidebar_recentcomments,//最新评论sidebar_categories,//随笔分类、随笔档案sidebar_toptags,//我的标签sidebar_shortcut,//常用链接…

销售进阶:三步提问法,掌握客户心理

在销售行业,时间就是金钱,我们必须争分夺秒地搞定客户。但也不能盲目行动,而要稳扎稳打。关键在于快速抓住客户需求,而客户往往不会主动透露他们的需求,甚至自己都不清楚自己想要什么。这就需要我们通过巧妙的提问来破局,否则忙活半天也只是白费力气。 最让人头疼的是,跟…

寒假集训笔记 | | 第一课

C++STL --第一课 C标准库常用函数<cstring>memset() 暴力清空 char str[10]; memset(str,0,sizeof(str));<cmath>三角函数、指数函数、浮点取整函数<cstdlib>qsort() C语言快排 rand() 随机数 malloc() free() C语言动态内存分配<cctype>isdigit()…

Svelte 最新中文文档翻译(1)—— 概述与入门指南

前言 Svelte,一个非常“有趣”、用起来“很爽”的前端框架。从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1:Svelte 以其独特的编译时优化机制著称,具有轻量级、高性能、易上手等特性,非常适合构…

平面二连杆机构的动力学方程

动力学研究物体的运动和作用力之间的关系。机器人动力学问题有两类:一是已知机器人各关节的驱动力或力矩,求解机器人各关节的位置、速度和加速度,这是动力学正问题;二是已知各关节的位置、速度和加速度,求各关节所需的驱动力或力矩,这是动力学逆问题。机器人的动力学正问…

【红队】C2框架:Covenant

一、项目介绍 Covenant是一个.NET开发的C2(command and control)框架,旨在突出.NET的攻击面,并充当红队成员的协作命令和控制平台,该工具不仅支持Linux,MacOS和Windows,还支持docker容器,最特别的地方是支持动态编译,能够将输入的C#代码上传至C2 Server,获得编译后的文…

3. 使用sql查询csv/json文件内容,还能关联查询?

1. 简介 我们在前面的文章提到了calcite可以支持文件系统的数据源适配, 其实官方已经提供了相应的能力, 其支持csv和json的查询适配, 废话不多说, 直接展示. 2. Maven <!-- calcite文件系统支持 --> <dependency><groupId>org.apache.calcite</groupId>…

一文搞懂 APP 算法备案

今天来给大家好好科普一下超重要的 APP 算法备案,这可是和我们日常使用 APP 以及 APP 运营都息息相关的知识点哦! 什么是算法备案 简单来讲,算法备案就相当于 APP 运营者要把自家 APP 里使用的算法详情,向有关部门进行申报登记。这就如同给算法这个 “幕后大脑” 办一张正式…

IDEA 在远程 Tomcat 上运行项目(转载补充版)

转载:IDEA 在远程 Tomcat 上运行项目(亲身避坑版) 我在操作的时候,遇到个问题,提示Unable to open debugger port (192.168.252.232:54578): java. net. ConnectException "Connection refused: connect" 原因是,开启JVM调试,需要在2.1小节设置环境变量的时候…