文章目录

  • Gos中线程的使用
    • PCB结构
    • 线程基本信息初始化
    • 线程栈信息初始化
  • 线程调度
    • 获取当前线程信息
    • 线程切换
  • Gos中进程
    • 进程的虚拟地址空间
    • 处理器进入3特权级
  • 进程实现
    • 进程切换
  • 参考文献

写在前面:自制操作系统Gos 第三章第二篇:主要内容线程实现与管理
有关线程或者进程基础知识见以下博客:

  • Linux——进程
  • C++高级——多线程编程

Gos完整代码:Github

Gos中线程的使用

平时大家应该有使用Linux下的pthread:

 pthread_t new_pthread;pthread_create(&new_pthread,NULL,function,NULL);

这样就创建了一个线程,线程的本质其实就是一段执行序列,负责执行我们传入的函数。而在Gos中,我也实现了线程,不过我们使用一个函数就可以了:

//函数原型:
/** @brief 创建一个优先级为priority并且名称为name的线程,并指定其执行函数和函数参数* @param name 线程名称* @param priority 线程优先级* @param function 待执行函数地址* @param func_arg 函数的参数*/
//struct task_struct *thread_start(char *name, int priority, thread_func function, void *func_arg)
thread_start("test_thread", 10, function, NULL);

而这个函数所做的主要工作如下:

  1. 分配存放线程PCB实体的内存空间
    //申请一页的内核空间struct task_struct *thread = get_kernel_pages(1);
  1. 调用函数init_thread初始化线程基本信息
    init_thread(thread, name, priority);
  1. 调用thread_start初始化线程栈信息
    thread_create(thread, function, func_arg);
  1. 之后将这个线程添加到就绪队列和全部线程队列
    list_append(&thread_ready_list, &thread->general_tag);list_append(&thread_all_list, &thread->all_list_tag);

现在来解释一下这两个队列。为了线程能够调度,我们肯定要有一个指针指向要被调度的下一个线程。而许多个这样的指针便构成了一个队列,当然我们也可以使用其他的容器,但是为了方便实现,我这里选择了队列来实现。这样,在Gos中便形成了以下两个概念队列:

PCB结构

对于一个线程来说,以下元素是必须的:

  • pid:这个进程和线程都有的
  • name:这个进程和线程都有的
  • 线程优先级信息:这个元素在Gos中代表了其能够占有时间片的时间
  • 页表及位图信息:这个是进程所有的,线程只是共享状态,由于设计线程和进程公用task_struct结构,所以现在先列出来
  • 文件描述符数组:这个是进程所有的,线程只是共享状态,由于设计线程和进程公用task_struct结构,所以现在先列出来
  • 线程的内核栈信息
//进程或线程的pcb
struct task_struct
{uint32_t *self_kstack; //各个内核线程都有自己的内核栈pid_t pid;enum task_status task_status; //线程状态char name[TASK_NAME_LEN];     //线程名称uint8_t priority;       //线程优先级uint8_t ticks;          //每一次线程占用CPU的时间数uint32_t elapsed_ticks; //线程从诞生起总共执行的CPU数struct list_elem general_tag;  //表示线程在一般队列中的节点身份struct list_elem all_list_tag; //作用于线程队列thread_all_list中的节点uint32_t *pgdir;                                  //进程页表的虚拟地址struct virtual_addr userprog_vaddr;               //用户进程的虚拟地址struct mem_block_desc u_block_desc[MEM_DESC_CNT]; //进程的内存管理模块int32_t fd_table[MAX_FILES_OPEN_PER_PROC]; //文件描述符数组uint32_t cwd_inode_no; //进程所在的工作目录的inode编号uint16_t parent_pid;   //父进程的piduint32_t stack_magic;  //栈的边界标记,用于检测栈溢出
};

可以看到这里并没有表示待执行函数即其参数的变量,而这些东西都在线程栈之中。线程栈中保存了线程当前的工作环境信息,这个下面会介绍。

线程基本信息初始化

而线程基本信息初始化,其实也就是根据我们的输入对这些参数进行赋值:

/** @brief 初始化线程基本信息* @param pthread 待初始化的线程地址* @param name 线程名称* @param pthread_priority 线程的优先级*/
void init_thread(struct task_struct *pthread, char *name, int pthread_priority)
{memset(pthread, 0, sizeof(*pthread));pthread->pid = allocate_pid();strcpy(pthread->name, name);//增加一个判断,如果是main函数,则其运行状态一直是runningif (pthread == main_thread){pthread->task_status = TASK_RUNNING;}else{pthread->task_status = TASK_READY;}pthread->priority = pthread_priority;pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);pthread->ticks = pthread_priority; //设置线程运行时间为线程的优先级,无疑优先级越高,运行时间越高pthread->elapsed_ticks = 0;pthread->pgdir = NULL;//初始化文件描述符信息pthread->fd_table[0] = 0; //标准输入pthread->fd_table[1] = 1; //标准输出pthread->fd_table[2] = 2; //标准错误//其余全部为-1uint8_t fd_idx = 3;while (fd_idx < MAX_FILES_OPEN_PER_PROC){pthread->fd_table[fd_idx] = -1;fd_idx++;}pthread->cwd_inode_no = 0; //以根目录为默认工作路径pthread->parent_pid = -1;pthread->stack_magic = 0x20000314; //自定义的魔数,这里是我的生日
}

这样在我们分配的这一页物理空间之中,这个线程的实体就会被进行赋值:

可以看到这里有部分元素被初始化过,而有一些则是没有,而没有用到的元素其实不属于线程基本信息,其更多的是辅助信息,我们会调用其他函数去初始化它。

线程栈信息初始化

要理解线程栈信息初始化的过程,我们就必须先了解线程栈中有哪些元素:首先就是几个寄存器变量,这几个是遵循ABI被调函数约定,用于线程切换的时候保存现场用的;其次其实也是非常重要的元素eip,这个变量第一次调用之后会指向待执行的函数。但是当线程切换之后,这个变量就会保存任务切换后新任务的返回地址;最后便是咱之前提到的用于指向待执行函数和函数参数的指针。

/** @brief 线程栈* @note 用于存储线程中待执行的函数* @note 此结构在线程自己的内核栈中位置不固定* @note 记录在switch_to时保存线程环境*/
struct thread_stack
{uint32_t ebp; //被调函数中用于保存主调函数中这几个寄存器的值,主要是怕破坏现场uint32_t ebx;uint32_t edi;uint32_t esi;// * @brief 当线程第一次执行的时候,eip指向待调用函数;其他时候,eip指向switch_to的返回地址void (*eip)(thread_func *func, void *func_arg);//以下仅供第一次被调度上cpu时使用void (*unused_retaddr); //返回地址,按道理说线程一般执行结束之后是没有返回地址这一说的,所以这个仅仅占位用啦thread_func *function; //保存所调用的函数地址void *func_arg;        //保存所调用函数的所需参数
};

接下来便是初始化线程栈信息了:

  1. 预留中断栈和线程栈的空间
  2. 让eip指向函数kernel_thread,这个函数做的工作很简单,其实就是通过kernel_thread函数调用我们传递进去的function
  3. 初始化四个寄存器的信息
/** @brief 初始化线程栈信息* @param pthread 待初始化的线程的地址* @param function 线程待执行函数信息* @param func_arg 函数所需参数*/
void thread_create(struct task_struct *pthread, thread_func function, void *func_arg)
{//预留中断使用栈的空间pthread->self_kstack -= sizeof(struct intr_stack);//预留线程栈空间pthread->self_kstack -= sizeof(struct thread_stack);//初始化线程栈信息struct thread_stack *kthread_stack = (struct thread_stack *)pthread->self_kstack;kthread_stack->eip = kernel_thread;kthread_stack->function = function;kthread_stack->func_arg = func_arg;kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}

至此,我们就知道线程栈在内存中的信息了,如下图所示:

线程调度

在完成线程初始化之后,线程的实体其实就存在于我们所创建的就绪线程队列全部线程队列之中了,而我们下一步要完成的就是实现线程调度,毕竟不能让kernel的主线程一直占用CPU资源。这个实现主要在 thread.cschedule函数,而调用这个函数主要就是由时钟中断来触发,每次时钟中断都会检查当前线程的时间片是否到期了,如果到期就会执行schedule函数。

/** @brief 时钟的中断处理函数*/
static void intr_timer_handler(void)
{struct task_struct *current_thread = running_thread();//检查是否栈溢出,0x20000314是个魔数,我生日,随便设置其他数也可以ASSERT(current_thread->stack_magic == 0x20000314);current_thread->elapsed_ticks++; //记录此线程占用的cpu时间数ticks++;                         //内核时间++if (current_thread->ticks == 0){schedule();}else{current_thread->ticks--;}
}

为了实现线程调度,我们肯定要知道当前运行的线程信息,因为我们需要保存当前的执行现场,便于以后切换回来:

    //得到当前线程的地址struct task_struct *current = running_thread();

之后,我们需要判断当前线程是什么原因需要被换下CPU,如果是时间片到了,那么我们的处理就是:

  1. 当前线程进入线程就绪队列
    if (current->task_status == TASK_RUNNING){//这种情况属于时间片到了,该轮转了//将其加入到就绪队列队尾就好啦ASSERT(!elem_find(&thread_ready_list, &current->general_tag));list_append(&thread_ready_list, &current->general_tag);current->ticks = current->priority; //重新赋予时间片current->task_status = TASK_READY;}
  1. 判断就绪队列中是否有待运行线程,如果没有默认运行线程idle_thread,这个线程什么也不会做
    if (list_empty(&thread_ready_list)){//如果没有待运行的线程就唤醒idle_thread线程,让它执行thread_unblock(idle_thread);}
  1. 从线程就绪队列中得到下一个待运行线程的信息
    thread_tag = list_pop(&thread_ready_list);struct task_struct *next = elem2entry(struct task_struct, general_tag, thread_tag);next->task_status = TASK_RUNNING;
  1. 当然,如果是进行进程间切换我们还需要更换页表,毕竟运行的内存空间变了
    process_activate(next);
  1. 调用switch_to进行线程运行现场的切换
    switch_to(current, next);

获取当前线程信息

我们都知道esp是程序的栈顶指针,那么我们如果获取了esp当前的值,之后对齐和0xffff f000进行与运算,就可以或者这个线程所在程序页的起始地址了,起始也就是这个线程在内存中的地址。

/** @brief 获取当前线程的PCB指针* @return 返回当前线程的地址* @note 每个线程占一页的空间大小,即4096字节,所以这里进行esp & 0xfffff000,便能得到起始地址*/
struct task_struct *running_thread()
{uint32_t esp;asm("mov %%esp,%0": "=g"(esp));return (struct task_struct *)(esp & 0xfffff000);
}

线程切换

刚刚也说了,线程切换的本质其实就是切换现场的运行现场,那么其实就是保存当前线程线程,切换下个线程的寄存器信息。函数switch_to接受两个参数,第一个参数时当前线程current,第二个函数时下一个待运行的线程next,其会保存current线程的寄存器映像,之后将下个线程的映像装载到处理器。

switch_to:;栈中此处为返回地址push esipush edipush ebxpush ebpmov eax,[esp+20]    ;压入两个参数,第一个是current,第二个是next。由于有4个寄存器+返回地址;所以current的地址是esp+20mov [eax],esp       ;保存栈顶指针,也就是task_struct的self_kstack字段地址,即起始地址;其实也就是备份当前线程环境;切换线程mov eax,[esp+24]    ;得到nextmov esp,[eax]       ;切换到self_kstack;以下这些寄存器都是next线程的寄存器,非current。其是next被换下处理器保存的pop ebppop ebxpop edipop esiret

而此时的栈情况如下图,其中每个矩形代表四字节,我们首先会将当前的寄存器信息压栈,然后得到current的线程信息,current线程中的第一个元素就是self_kstack字段,将currentesp栈顶指针进行保存,之后加载nextself_kstack的栈顶指针就可以了,这个时候出栈的四个ABI约定寄存器也就是next的中保存的了。

Gos中进程

现在,我们在线程的基础上实现进程。刚刚我们也讲了线程的实现步骤,在thread_start函数中:

  1. 先分配一页物理内存做线程的PCB
  2. 初始化线程PCB中的信息
  3. 创建线程运行时的栈
  4. kernel_thread中通过调用 function使函数得到执行

所以我们要实现进程,不仅要新增虚拟地址空间,还要把function替换成创建进程的函数。

进程的虚拟地址空间

进程和线程的最大区别就是进程拥有独立的地址空间,不同的地址空间就是不同的页表,所以我们创建虚拟地址空间就是创建进程的页表(页表也需要空间存储)。而虚拟地址空间的实现主要使依靠task_struct结构体中的两个变量,其被用来跟踪用户空间虚拟地址的分配情况:

    uint32_t *pgdir;                                  //进程页表的虚拟地址struct virtual_addr userprog_vaddr;               //用户进程的虚拟地址

处理器进入3特权级

除此之外,进程还往往工作在特权级3,所以我们还要为进程创建在3 特权级运行的栈。而切换至3特权级这个过程就需要我们使用中断的手段去实现。

当我们的内核从中断返回的时候会用到iretd指令,这个指令会用到栈中的数据作为返回地址,还会加载栈中存储的eflags信息到eflags寄存器。如果此时栈中的cs.rpl是更低的特权级,那么在特权级检查之后,CPU还会将栈中从cs载入CS寄存器栈中的ss载入SS寄存器

当我们进入中断的时候,我们通过kernel.S文件中的intr%1entry函数去实现,而退出中断的时候则是执行函数intr_exit函数,这个是intr%1entry函数的逆过程,将intr%1entry函数保存的寄存器pop出来以恢复上下文运行环境。

intr_exit:add esp,4               ;回收栈空间popadpop gspop fspop espop dsadd esp,4               ;跳过error_codeiretd

而这个时候就需要用到我们定义的结构中断栈intr_stack。其中保存了任务的上下文信息,我们借用一系列pop出栈的机会,将用户进程的上下文信息载入CPU的寄存器,为用户进程的运行准备好环境:

/** @brief 中断栈* @note 此结构用于中断发生时保护程序的上下文环境* @note 当被外部打断的时候,会将此结构压入上下文寄存器* @note 此栈再线程自己的内核栈中位置固定,所在页的最顶端*/
struct intr_stack
{uint32_t vec_no; //kernel.S中宏vector中push压入的中断号uint32_t edi;    //以下用于通用寄存器现场的保存uint32_t esi;uint32_t ebp;uint32_t esp_dummy; //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; //error会被压入在eip之后void (*eip)(void);uint32_t cs; //段基址会变,所以这个也要保存uint32_t eflags;void *esp;uint32_t ss;
};

而进入到3特权级的关键就在这个过程,CPU通过CS寄存器的RPL位来表示寄存器,当执行iretd时,就会把在栈中保存的CS信息加载到CS寄存器之中。最后,总结一下这个过程的几个要点:

  1. eflags寄存器的IF位必须为1,这样才能打开中断
  2. eflags寄存器的IOPL位必须为0,因为用户进程处于3特权级,不允许访问硬件
  3. 栈中段寄存器选择子必须指向DPL为3的内存段
  4. 需要从中断中返回
  5. 需要提前准备用户进程所用的栈结构,在里面装填好用户进程的上下文信息
  6. 在栈中需要存储CS信息,其RPL必须为3

进程实现

在Gos中进程的实现也就是在线程的基础上实现刚刚提到的那几点:

  1. 首先就是先创建线程并对线程进行初始化
  2. 之后我们要做的就是创建进程的位图信息,用于管理内存。这一步主要做的就是申请位图所用的物理内存空间,之后初始化这个位图。
  3. 然后我们将进程作为线程的执行函数,线程会执行这个叫start_process的函数,这个函数实际上是初始化中断栈,并将其作为进程的特权级3下的栈,做的就是我们上面提到的六点事情,这个时候CPU已经工作在特权级3了。
  4. 再然后就创建页表信息了,在这里我们先申请物理空间存放页表。由于是用户进程,要共享内核,所以需要复制复制内核页目录表的镜像到用户进程页目录表768~1024的位置。
/** @brief 创建用户进程* @param filename 程序文件名称* @param name 进程的名字*/
void create_process(void *filename, char *name)
{//分配内存进程实体struct task_struct *thread = get_kernel_pages(1);init_thread(thread, name, default_prio);//创建进程位图信息create_user_vaddr_bitmap(thread);//进程作为线程的执行函数thread_create(thread, start_process, filename);thread->pgdir = create_page_dir();//block_desc_init(thread->u_block_desc);//加入线程就绪队列enum intr_status old_status = intr_disable();ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag);ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag);intr_set_status(old_status);
}

进程切换

进程切换的原理和线程是一样的,不过要在切换的时候激活新进程的页表:

/** @brief 线程调度器,调度下一个就绪线程执行* @note 必须处于关中断状态,不能被其他线程打断,原子操作*/
void schedule()
{...// next->task_status = TASK_RUNNING;process_activate(next);//switch_to(current, next);
}

而激活页表的主要操作就是重新加载cr3寄存器,而这个寄存器是需要实际物理地址的,所以我们需要先将虚拟地址转换为物理地址,这个实际上是一个差页表的逆过程。

        pagedir_phyaddr = addr_v2p((uint32_t)pthread->pgdir);

之后,重新加载cr3寄存器便可以了:

    //重新激活页表asm volatile("movl %0,%%cr3" ::"r"(pagedir_phyaddr): "memory");

参考文献

[1] 操作系统真相还原

Gos —— 实现线程和进程相关推荐

  1. iOS开发 - 线程与进程的认识与理解

    进程: 进程是指在系统中正在运行的一个应用程序,比如同时打开微信和Xcode,系统会分别启动2个进程; 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内; 线程: 一个进程要想执行任务 ...

  2. Java多线程001——一图读懂线程与进程

    本博客 猫叔的博客,转载请申明出处 前言 本系列将由浅入深,学习Java并发多线程. 一图读懂线程与进程 1.一个进程可以包含一个或多个线程.(其实你经常听到"多线程",没有听过& ...

  3. 为什么校招面试中总被问“线程与进程的区别”?我该如何回答?

    作者 | 宇宙之一粟 责编 | 徐威龙 出品 | AI 科技大本营(rgznai100) 进程与线程?(Process vs. Thread?) 面试官(正襟危坐中):给我说说"线程&quo ...

  4. 进程、线程、进程池、进程三态、同步、异步、并发、并行、串行

    点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 来源:cnblogs.com/songhaixing/p/1 ...

  5. 写给小白看的线程和进程,高手勿入

    计算机的核心是CPU,它承担了计算机的所有计算任务,CPU就像一个工厂,时刻在运行着,而操作系统管理着计算机,负责任务的调度.资源的分配和管理. 图片来源于网络 进程和线程都是计算机操作系统中的基本概 ...

  6. php 进程 线程,php进程还是线程

    php编程常见的进程和线程 一.什么是进程 (推荐学习:PHP视频教程) 进程是程序执行是的一个实例,进程能够分配给cpu和内存等资源.进程一般包括指令集和系统资源,其中指令集就是你的代码,系统资源就 ...

  7. 线程VS进程,多线程VS多进程,并行VS并发,单核cpuVS多核cpu

    目录 概论 进程VS线程 并发VS并行 多线程VS多进程 总结 概论 程序是为完成特定任务.用某种语言编写的组指令的集合.即指一段静态的代码,静态对象. 进程是程序的次执行过程, 或是正在运行的 一个 ...

  8. 编程思想之多线程与多进程——以操作系统的角度述说线程与进程

    原文:http://blog.csdn.net/luoweifu/article/details/46595285  作者:luoweifu  转载请标名出处 什么是线程 什么是线程?线程与进程与有什 ...

  9. iOS进阶之底层原理-线程与进程、gcd

    线程与进程 线程的定义 线程是进程的基本单位,一个进程的所有任务都在线程中执行 进程要想执行任务,必须的有线程,进程至少要有一条线程 程序启动默认会开启一条线程,也就是我们的主线程 进程的定义 进程是 ...

  10. python 协程、进程、线程_Python进程、线程、协程之间的关系

    一.从操作系统角度 操作系统处理任务, 调度单位是 进程 和 线程 . 1.进程: 表示一个程序的执行活动 (打开程序.读写程序数据.关闭程序) 2.线程: 执行某个程序时, 该进程调度的最小执行单位 ...

最新文章

  1. prefixspan是挖掘频繁子序列,子序列不一定是连续的,当心!!!
  2. iPhone SDK发布
  3. matlab 安装glpk,mac上安装GLPK
  4. Python爬虫入门四urllib库的高级用法
  5. 【快讯】JeecgBoot低代码平台,成功入选2021科创中国·开源创新榜
  6. Android超链接
  7. Scala对象 转Json字符串
  8. python爬取豆丁网文章_百度文库爬取分析 - osc_tgjycqas的个人空间 - OSCHINA - 中文开源技术交流社区...
  9. 信息安全管理体系ISO27001IT服务管理体系ISO20000(转)
  10. 应聘客户端主程需做哪些准备
  11. SuperMap根据栅格数据制作专题图
  12. Google账号登录后直接跳转百度首页,登陆不上
  13. 细谈永恒之蓝,实现复现
  14. 【吴刚】电商网站详情页设计初级入门标准视频教程-吴刚-专题视频课程
  15. 8个有用的表单构建工具,你一定要使用并收藏好
  16. 我工作上常用的--测试用例文档模板
  17. 【考研总结】考研失败后的反省
  18. ESD/EMI防护设计
  19. Shell语言基本操作一(Xshell5)
  20. vscode终端运行vue报错:无法加载文件 C:\Users\14353\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本

热门文章

  1. JS鼠标移入移出事件:onmouseover事件和onmouseout事件实例
  2. Spring @scheduled注解周期性执行超时任务对任务调度的影响分析
  3. 我的世界服务器如何做无限箱子,《我的世界》无限存储箱子制作方法 制作流程介绍...
  4. OPENWRT-LUCI开发总结-LUCI开发过程中的小技巧
  5. 【Codewars】Bouncing Balls
  6. ES集群health为yellow解决办法
  7. 单机 elasticsearch 7.12 索引状态yellow问题解决
  8. 笔记本电脑的计算机名称在哪里看,如何查看笔记本电脑的IP地址
  9. 计算机在水产养殖学中的应用,计算机技术在生物学中的应用
  10. 【论文阅读笔记】用于真实图像超分辨率的一种局部判别学习方法(LDL)