操作系统真象还原 --- 9. 线程

还是先来问自己几个问题

  1. 什么是线程?
  2. 什么是进程?
  3. 用什么管理线程/进程?
  4. 如何进程线程调度(切换)?这个问题是我最感兴趣的

简单的问题就简单回答

什么是线程? 线程是一套机制,此机制可以为一般的代码创造它所依赖的上下文环境,从而让代码块具有独立性,能够单独获得处理器资源。
什么是进程? 进程是线程+资源
用什么标识进程? PCB:Process Control Block,程序控制块,用来记录此进程的相关信息,比如进程状态、PID、优先级等

1.线程处理相关数据结构

中断栈:中断发生时用来保存程序的上下文环境,中断退出时恢复程序的上下文环境。(结合kernel.S的中断代码学习)

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.线程创建时设置eip为kernel_thread。2.在switch_to调用时根据abi的要求保存线程的上下文环境(我觉得也不能叫线程上下文环境,应该是函数调用规范)

struct thread_stack {
    uint32_t ebp;
    uint32_t ebx;
    uint32_t edi;
    uint32_t esi;

    /* 线程第一次执行时,eip指向待调用的函数kernel_thread 
    其它时候,eip是指向switch_to的返回地址*/
    void (*eip) (thread_func* func, void* func_arg);

    /*****   以下仅供第一次被调度上cpu时使用   ****/

    /* 参数unused_ret只为占位置充数为返回地址 */
    void (*unused_retaddr);
    thread_func* function;   // 由Kernel_thread所调用的函数名
    void* func_arg;    // 由Kernel_thread所调用的函数所需的参数
};

task_struct:pcb,保存进程或者叫线程(因为这里一个进程只有一个线程)的属性。

struct task_struct {
    uint32_t* self_kstack;	 // 各内核线程都用自己的内核栈
    enum task_status status;
    char name[16];
    uint8_t priority;		 // 线程优先级
    uint8_t ticks;// 每次在处理器上执行的时间嘀嗒数
    uint32_t elapsed_ticks;//执行cpu时钟
    struct list_elem general_tag;//用于线程在一般的队列中的节点 ready
    struct list_elem all_list_tag;//用于线程在thread_all_list中的节点
    uint32_t* pgdir;//进程自己页表的虚拟地址
    uint32_t stack_magic;	 // 用这串数字做栈的边界标记,用于检测栈的溢出
};

2.线程环境初始化流程

在使用线程前需要准备一些必要的环境:创建两个链表保存就绪进程和全部的进程,并将当前程序流初始化为主线程

  1. 创建thread_ready_list(就绪队列)
  2. 创建thread_all_list(所有线程队列)
  3. make_main_thread:将当前运行流当作主线程(预留的esp=0xc009f000所在页为PCB)初始化线程并加入ready队列

3.线程初始化流程

thread_start函数创建线程时做了哪些工作?

  1. 获得一个内核页作为PCB
  2. init_thread:初始化线程结构体task_struct(PCB)
  3. thread_create:初始化线程栈thread_stack
  4. 将初始化的线程加入到thread_ready_list中,等待被调度上cpu执行
  5. 将初始化的线程加入到thread_all_list中

4.多线程调度/切换流程

线程调度才是这一章的核心并且和前面的中断联系紧密。多线程调度的基础是时钟中断。只有中断的时候调度程序才能获得对系统的控制权。

  1. 中断处理函数
    • 获取当前线程的PCB
    • 判断是否溢出(stack_magic被破坏)
    • 线程占用时间(elapsed_ticks)+1
    • 判断线程剩余时间片(ticks)是否大于0
    • 剩余时间片小于0,进行线程调度
  2. 调度器schedule
    • 获取当前线程的pcb
    • 如果线程是因为时间片用完才替换的,需要重新设置ticks,并置status为TASK_READY,加入thread_ready_list队尾
    • 从thread_ready_list中弹出下一个需要上cpu的线程next
    • switch_to(cur,next)进行线程切换
  3. 任务切换函数switch_to
    • 就干了一件事情,切换esp为next

所以到现在都没看出来什么时候换了线程?其实这里分为了三个部分进行,进入中断中断处理退出中断

进入中断(kernel.S):保存了中断发生时的上下文环境
中断处理(timer.c和interrupt.c和thread.c):调用中断处理函数intr_timer_handler(intr_timer_handler->schedule->switch_to),完成了esp的切换
中断退出(kernel.S):此时的esp为next线程的,退出中断会进行上下文环境的恢复,最后iret就能到next中执行,实现进程间的切换。

总结

线程切换和前面的中断密切相关,需要借助中断才能实现线程切换。
需要将kernel.S、interrupt.c、timer.c、thread.c结合起来才比较好理解。
本来想画图的,然后偷懒了。如果有好用的画数据结构和函数调用的工具,可以推荐一下。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值