首页 > 其他分享 >《操作系统真象还原》第九章 线程(一) 在内核中实现线程

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

时间:2025-01-21 18:43:04浏览次数:1  
标签:thread uint32 线程 内核 func pcb stack 真象

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

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

我们在本节的任务:

  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);           // 初始化线程的pcb
    thread_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初始化为0
    pthread->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;   
  
    //下面的寄存器用不到, 先置为0
    kthread_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); 
}

​​

​​

标签:thread,uint32,线程,内核,func,pcb,stack,真象
From: https://www.cnblogs.com/fdxsec/p/18684183/chapter-9-thread-1-realize-threads-in-the-kernel

相关文章

  • Java多线程循环list集合
    1.Java多线程基本概念在开始之前,先简单了解一下Java的多线程。如果一个应用程序在执行多个任务时,每个任务都是独立的,那么我们就可以把这些任务放在多个线程中并发执行。Java通过Thread类和Runnable接口提供了创建和管理线程的技术。1.1创建线程创建线程最常见的方法有两......
  • [Qt]系统相关-多线程、线程安全问题以及线程的同步机制
    目录一、Qt多线程编程1.介绍2.多线程的操作线程的创建QThread的常用API使用案例3.Qt线程的使用场景二、线程安全问题1.互斥锁介绍使用案例2.读写锁三、线程的同步1.条件变量2.信号量一、Qt多线程编程1.介绍    Qt中的多线程的底层原理和注意事项等......
  • golang 多线程 备份文件夹到兄弟层级 wail group
    golang多线程备份文件夹到兄弟层级wailgroupD:\GolangTools\src\config\config.gopackageconfigtypeConfigHandlerstruct{ includeDirNames[]string includeFileNames[]string excludeDirNames[]string excludeFileNames[]string}funcNewConfigHandler......
  • 面试必会(嵌入式)操作系统面试高频(三)线程与进程
    目录1.请你说说CPU工作原理⭐⭐2.死锁的原因、条件?以及如何预防⭐⭐⭐3.死锁与活锁⭐⭐死锁:活锁:解决活锁问题的一般策略包括:4.说说sleep和wait的区别?⭐⭐⭐sleep和wait的区别:5.简述epoll和select的区别,epoll为什么高效?⭐⭐⭐⭐epoll:Select:epoll为什么高效?拷贝开......
  • 面试必会(嵌入式)操作系统面试高频(一)线程与进程
    目录1.什么是线程?进程,线程,彼此有什么区别?⭐⭐⭐进程线程线程和进程区别:2.什么时候用进程,什么时候用线程?⭐⭐使用进程的情况:使用线程的情况:3.一个线程占多大内存?⭐⭐⭐4.说说什么是信号量,有什么作用?⭐⭐5.多进程内存共享可能存在什么问题?如何处理?⭐⭐⭐⭐⭐多进程内......
  • webWorker 开启javascript另外的线程
    javascript是一个单线程语音,因此所有执行代码放在一个线程里面因此javascriot是从上到小执行代码的,但是遇到大量切繁重的任务例如图形计算请求,轮询等需要耗时的任务虽然可以使用异步来避免造成页面渲染的阻塞,但是异步任务完成后还要对数据进行处理因此也会导致页面的卡顿,因此......
  • Java线程相关知识及线程池学习二
    阻塞队列定义在Java中,阻塞队列(BlockingQueue)是一种线程安全的队列。阻塞队列是Java并发包(java.util.concurrent)中的一个重要组件,常用于生产者-消费者模式中,一个线程产生数据放入队列,另外一个从队列取出数据进行消费。主要有两种情况在尝试添加元素到队列中时,如果队列已......
  • IO进程----线程
    什么是线程概念线程是一个轻量级的进程,为了提高系统的性能引入线程。线程和进程是参与统一的调度。在同一个进程中可以创建的多个线程,共享进程资源。(Linux里同样用task_struct来描述一个线程)进程和线程的区别相同点:都为系统提供了并发执行的能力不同点:调度和资源:......
  • ARM Context M0芯片内核介绍
    概述:Cortex-M0处理器具有非常小的硅面积、低功耗和最小的代码占用,使开发人员能够以8位的价格实现32位的性能,绕过了16位设备的步骤。该处理器的超低门数使其能够部署在模拟和混合信号设备中。Cortex-M0处理器是一种极低门数、高能效的处理器,适用于需要区域优化处理器的微控......
  • 实例1--C#上位机+后台C应用线程
    实例1C#上位机+后台C应用线程目录实例1C#上位机+后台C应用线程1. 需求1.1 使用场景1.2 关联工具1.3 实例需求概述1.4 实现步骤设计2. C/C++项目2.1 创建项目2.2C/C++代码2.3生成DLL3. C#项目3.1 新建项目3.2窗体UI控件设计3.3C#代码3.4配......