• 第九章-线程


    初始时,CPU的执行流为进程;当产生了线程概念后,CPU执行流变为了线程,大大增大了一个周期以内进程的执行速度。

    线程产生的作用就是为了提速,利用线程提速,原理就是实现多个执行流的伪并行,让处理器多执行自己进程中的代码。若进程只占用处理器的一个时间片,那将进程细分为线程后,一个进程中的多个线程可同时占用处理器。类比拼车与不可拼车两种模式,CPU相当于一辆车,拼车表示开启线程,允许拼车后,用户的出行成本就大大降低了。

    Ⅰ.程序、进程、线程关系

    程序是指静态的、存储在文件系统上、尚未运行的指令代码,它是实际运行时程序的映像。

    进程是指正在运行的程序,即进行中的程序,程序必须在获得运行所需要的各类资源后才能成为进程,资源包括进程所使用的栈,使用的寄存器等。

    进程=线程+资源,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

    1.进程线程区别
    1. 进程是资源分配的基本单位,线程是处理器调度的基本单位。
    2. 进程拥有自己独立的地址空间,每启动一个进程,系统为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段;线程没有自己的地址空间,需要借助进程的资源“生存”。
    3. CPU切换线程的开销比进程小。
    4. 创建线程的资源开销比进程小。
    5. 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行。
    6. 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间);多线程程序更不易维护,一个线程死掉,可能整个进程就死掉了(因为共享地址空间)。
    7. 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换。
    2.线程进程状态

    初始态、就绪态、运行态、阻塞态、终止态

    在这里插入图片描述

    3.进程的身份证-PCB

    针对多任务处理系统中,任务切换执行存在的如下疑问:

    (1)要加载一个任务上处理器运行,任务由哪来?也就是说,调度器从哪里才能找到该任务?进程表
    (2)即使找到了任务,任务要在系统中运行,其所需要的资源从哪里获得?PCB中的寄存器和栈等资源
    (3)即使任务已经变成进程运行了,此进程应该运行多久呢?总不能让其独占处理器吧。时间片
    (4)即使知道何时将其换下处理器,那当前进程所使用的这一套资源(寄存器内容)应该存在哪里?PCB最顶层的寄存器映像
    (5)进程被换下的原因是什么?下次调度器还能把它换上处理器运行吗?取决于PCB的状态
    (6)前面都说过了,进程独享地址空间,它的地址空间在哪里? PCB-页表

    ……

    为解决以上问题,操作系统为每个进程提供了一个 PCB,Process Control Block,即程序控制块,它就是进
    程的身份证,用它来记录与此进程相关的信息,比如进程状态、PID、优先级等。

    每个进程都有自己的 PCB,所有 PCB 放到一张表格中维护,这就是进程表,调度器可以根据这张表选择上处理器运行的进程 。

    在这里插入图片描述

    PCB的栈为进程所使用的 0 特权级下内核栈,寄存器映像存储的也是内核态的寄存器,栈指针对应的是内核态下的寄存器映像地址,即内核态栈地址。

    3.实现线程的两种方式一一内核或用户进程

    线程仅仅是个执行流,在用户空间,还是在内核空间实现它,最大的区别就是线程表在哪里,由谁来调度它上处理器 。 如果线程在用户空间中实现,线程表就在用户进程中,用户进程就要专门写个线程用作线程调度器,由它来调度进程内部的其他线程。如果线程在内核空间中实现,线程表就在内核中,该线程就会由操作系统的调度器统一调度,无论该线程属于内核,还是用户进程。

    (1)内核中实现线程

    线程机制由内核完成。

    好处:

    • 相比在用户空间中实现线程,内核提供的线程相当于让进程多占了处理器资源
    • 当进程中的某一线程阻塞后 , 由于线程是由内核空间实现的,操作系统认识线程,所以就只会阻塞这一个线程,此线程所在进程内的其他线程将不受影响

    缺点:

    • 用户进程需要通过系统调用陷入内核,这多少增加了 一些现场保护的栈操作,这还是会消耗一些处理器时间
    (2)用户程序中实现线程

    **线程机制由用户程序通过标准库完成。**处理器依旧按照进程作为执行流执行程序。

    好处:

    • 可移植性强,在不支持线程的操作系统上也可以写出完美支持线程的用户程序。
    • 线程的调度算法是由用户程序自己实现的,可以根据实现应用情况为某些线程加权调度。
    • 将线程的寄存器映像装载到 CPU 时,可以在用户空间完成,即不用陷入到内核态,这样就免去了进入内核时的入栈和出栈操作

    缺点:

    • 进程中的某个线程若出现了阻塞(通常是由于系统调用造成的),操作系统不知道进程中存在线程,它以为此进程是传统型进程(单线程进程),因此会将整个进程挂起,即进程中的全部线程都无法运行
    • 如果在用户空间中实现线程,但凡进程中的某个线程开始在处理器上执行后,只要该线程不主动让出处理器,此进程中的其他线程都没机会运行。 只能凭借开发人员“人为”地在线程中调用类似 pthread_yield 或 pthread_exit 之类的方法使线程让出处理器使用权,此类方法通过回调方式触发进程内的线程调度器,让调度器有机会选择进程内的其他线程上处理器运行。
    • 进程在一个时间片内既要完成资源分配,又要处理线程调度的工作,导致提速效果较差

    如果在用户空间中实现线程,用户线程就要肩负起调度器的责任,因此除了要实现进程内的线程调度器外,还要自己在进程内维护线程表 ,导致开销很大。

    在这里插入图片描述

    Ⅱ.在内核空间实现线程

    包括主线程和新建立的线程两种方式。

    在这里插入图片描述

    call 指令属于“有去有回”的指令,它在“去”之前先在栈中(进入被调函数时的栈顶处〉留下返回地址,它的“回”则需要在 ret 指令的配合下才能完成, ret 将楼顶的值当作 call 留下的返回地址,在保证栈顶值正确的情况下, ret 能把处理器重新带回到主调函数中。

    1.线程执行过程
    1.构建就绪线程队列和所有线程队列
    struct task_struct* main_thread;    // 主线程 PCB
    struct list* thread_ready_list;     // 就绪队列
    struct list* thread_all_list;       // 所有线程
    static struct list_elem* thread_tag;    // 用于保存队列中的线程结点
    
    • 1
    • 2
    • 3
    • 4

    队列中存储的是PCB标志位,general_tag和all_list_tag,分别表示就绪线程队列标志和所有线程队列标志。

    在这里插入图片描述

    /*  根据结构体成员找到结构体地址    */
    // 计算偏移量,建立一个起始地址为0的虚拟结构体,对成员取地址,就是偏移量
    #define offset(struct_type, member) (int)(&((struct_type*)0)->member)
    // 结构体入口地址=当前成员变量地址-当前成员变量的偏移地址
    #define elem2entry(struct_type, struct_member_name, elem_ptr) \
               (struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    2.申请一页大小的PCB(内核空间)
    struct task_struct* thread= get_kernel_pages(l);  
    
    • 1
    3.初始化线程PCB
    (1)定义PCB结构
    /*   进程控制块     */
    struct task_struct{
        uint32_t *self_kstack;   // 线程的内核栈
        enum THREAD_STATUS status;        // 线程状态
        uint32_t priority;          // 线程优先级
        char name[16];
        
        // 此任务自上 cpu 运行后至今占用了多少 cpu 嘀嗒数,也就是此任务执行了多久
        uint32_t elapsed_ticks;
        
        // general_tag 的作用是用于线程在一般的队列中的结点
        struct list_elem general_tag;
    
        // all_list_tag 的作用是用于线程队列 thread_all_ list 中的结点
        struct list_elem all_list_tag;
    
        uint32_t* pgdir; // 进程自己页表的虚拟地址
        uint32_t stack_magic;       // 线程栈的边界标记,用于标记栈是否溢出
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 内核栈。存储线程执行的函数、传参、寄存器、内存地址等信息

    • 任务的时间片。每次时钟中断都会将当前任务的 ticks 减1 ,当减到 0时就被换下处理器。

    • 优先级。priority 表示任务的优先级,咱们这里优先级体现在任务执行的时间片上,即优先级越高,每次任务被调度上处理器后执行的时间片就越长。

    • general_tag。线程的标签, 当线程被加入到就绪队列也thread_ready_list 或其他等待队列中时,就把该线程 PCB 中 general_tag 的地址加入队列。

    • all_list_tag。在所设计的系统中, 为管理所有线程,还存在一个全部线程队列thread_all_list,因此线程还需要另外一个标签,即 all_list_tag。专用于线程被加入全部线程队列时使用

      这两个标签仅仅是加入队列时用的,将来从队列中把它们取出来时,还需要再通过 offset 宏与 elem2entry宏的“反操作“实现从&general_tag 到&thread 的地址转换,将它们还原成线程的 PCB 地址后才能使用。

    • pgdir。任务自己的页表 。如果该任务为线程, pgdir 则为 NULL,否则 pgdir会被赋予页表的虚拟地址,注意此处是虚拟地址,页表加载时还是要被转换成物理地址的 。

    如何实现就绪队列的标签到PCB转换的过程呢?通过上面的offset函数和elem2entry函数

    (2)初始化PCB

    /*  初始线程PCB   */
    void init_thread(struct task_struct* pthread, char* name, uint32_t proi){
        memset(pthread,0, sizeof(*pthread));
        strcpy(pthread->name, name);
        if (pthread == main_thread){
            pthread->status = TASK_RUNNING;
        }
        else{
            pthread->status = TASK_READY;
        }
        
        // 初始化在线程PCB的最顶端,栈向下生长
        // pthread在分配了一页内存后指向PCB的最底端,加上PG_SIZE即为PCB最顶端地址
        pthread->self_kstack =(uint32_t*) ((uint32_t)pthread + PG_SIZE);
        
        pthread->priority = proi;
        pthread->ticks = proi;
        pthread->elapsed_ticks = 0;
        pthread->pgdir = NULL;
        pthread->stack_magic = 0x19870916;  // 自定义魔数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    分配栈空间

    初始化PCB中优先级、时间片、状态

    3.初始化线程栈
    (1)定义线程栈

    线程栈结构,由于线程中断后再次执行需要恢复执行的函数、参数,以及对应的寄存器信息,因此建立栈维护线程现场

    /*  建立线程自己的栈 
    	* 线程自己的栈,用于存储线程中待执行的函数
    	* 此结构在线程自己的内核梭中位置不固定,
    	* 仅用在 switch_to 时保存线程环境。
    	* 实际位置取决于实际运行情况。
    */
    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 时使用 ****/
        void (*unused_retaddr);		// 参数 unused_retaddr 只为占位置充数为返回地址
        thread_func* funciton;		// 自 kernel_thread 所调用的函数名
        void* func_arg;				// 由 kernel_thread 所调用的函数所需的参数
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    (2)初始化线程栈
    /*  初始化线程栈    */
    void thread_creat(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->funciton = function;
        kthread_stack->func_arg = func_arg;
        kthread_stack->eip = kernel_thread;
        kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0;    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    4.将创建的线程添加到就绪线程队列和所有线程队列中
        /*  确保之前不在就绪队列中  */
        ASSERT(!(elem_find(&thread_ready_list, &thread->general_tag)));
        list_append(&thread_ready_list, &thread- >general_tag);
    
        /*  确保之前不在所有队列中  */
        ASSERT(!(elem_find(&thread_ready_list, &thread->all_list_tag)));
        list_append(&thread_ready_list, &thread- >all_list_tag);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    5.设置kernel中main函数为主线程

    main函数在启动之后会自动建立线程栈,我们在kernel.S中为其分配了PCB空间,并未初始化PCB,因此需要对其进行初始化 。

    (1)获取当前esp指针赋值给main_thread的PCB
    /*  获取当前线程 pcb 指针   */
    struct task_struct* running_thread(){
        uint32_t esp;
        asm("mov %%esp, %0":"=g"(esp));
        /* 取 esp 整数部分,即 pcb 起始地址*/
        return (struct task_struct*) (esp & Oxfffff000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    (2)完善main_threadPCB信息
    static void make_main_thread(void){
        // 定义main_thread的PCB地址
        main_thread = running_thread();
        // 初始化名字和优先级
        init_thread(main_thread, "main", 31);
        
        // 由于main程序已经在运行了,因此只需要放入all_list即可
        ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
        list_append(&thread_all_list, &main_thread->all_list_tag);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Ⅲ.任务调度器和任务切换

    1.任务调度器工作过程

    线程调度器主要任务就是读写就绪队列,增删里面的结点,结点是线程 PCB 中的 general_tag,“相当于”线程的 PCB,从队列中将其取出时一定要还原成 PCB 才行 。

    • 当前任务执行的状态,什么时候结束?ticks===0
    • 结束了之后,如何完成保护现场操作?
    • 结束了如何找到下一个线程?thread_ready_list
    • 如何恢复下一个线程的现场?switch_to

    (1)PCB的ticks决定了任务执行的时间,系统设定:当ticks减为0时,当前任务被换下处理器,根据线程的运行状态决定线程是否加入就绪队列:

    • TASK-RUNNING。将当前队列加入thread_ready_list队尾,并将ticks设置为prio。
    • others。不对当前线程进行任何操作。

    故需要设定时钟中断系统。

    (2)调度器按照队列先进先出的顺序,把就绪队列中的第 1 个结点作为下一个要运行的新线程,将该线程的状态置为 TASK_RUNNING,之后通过函数 switch_to 将新线程的寄存器环境恢复,新线程便开始执行 。

    (1)完整调度过程的3个步骤
    1. 时钟中断处理函数
    2. 调度器schedule
    3. 任务切换函数switch_to
    (2)PART1-注册时钟中断处理函数

    Intel处理器支持256个中断,在前面的kernel.S中,通过中断向量号调用中断处理程序数组 idt_table 中的 C 版本的处理程序,就是文件 kemel.S 中代码 call [idt_table + %1 *4]的作用。由于idt_table存储的就是中断处理程序,因此,为设备注册中断处理程序的工作变得很简单,我们不用去修改中断描述符,直接把中断向量作为数组下标,去修改 idt_table[中断向量]数组元素即可。

       for (i = 0; i < IDT_DESC_CNT; i++) {
    	/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
    	 * 见kernel/kernel.S的call [idt_table + %1*4] */
          idt_table[i] = general_intr_handler;		    // 默认为general_intr_handler。
    							    // 以后会由register_handler来注册具体处理函数。
          intr_name[i] = "unknown";				    // 先统一赋值为unknown 
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    之前的时钟中断处理函数还是用通用的函数来处理的, 即 general_intr_handler,此函数作为默认的中断处理函数,即某个中断源没有中断处理程序时才用它来代替。

    (2.1)改进的通用中断处理函数general_intr_handler()
    • 由于需要打印输出中断信息,为防止由于光标错误值引发异常,加入了set_cursor()光标位置设置函数

    • 为保证中断调用出现的缺页异常能及时被发现和处理,设置缺页中断号14判定函数,标定缺页异常

      加了 Pagefault 的处理。 Pagefault 就是通常所说的缺页异常,它表示虚拟地址对应的物理地址不存在,也就是虚拟地址尚未在页表中分配物理页,这样会导致 Pagefault 异常。导致 Pagefault 的虚拟地址会被存放到控制寄存器 CR2 中,我们加入的内联汇编代码就是让 Pagefault 发生时,将寄存器 cr2 中的值转储到整型变量 page_fault_vaddr 中,并通过 put_str函数打印出来。因此,如果程序运行过程中出现异常 Pagefault 时,将会打印出导致 Pagefault 出现的虚拟地址。

    /* 通用的中断处理函数,一般用在异常出现时的处理 */
    static void general_intr_handler(uint8_t vec_nr) {
       if (vec_nr == 0x27 || vec_nr == 0x2f) {	// 0x2f是从片8259A上的最后一个irq引脚,保留
          return;		//IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
       }
      /* 将光标置为0,从屏幕左上角清出一片打印异常信息的区域,方便阅读 */
       set_cursor(0);
       int cursor_pos = 0;
       while(cursor_pos < 320) {
          put_char(' ');
          cursor_pos++;
       }
    
       set_cursor(0);	 // 重置光标为屏幕左上角
       put_str("!!!!!!!      excetion message begin  !!!!!!!!\n");
       set_cursor(88);	// 从第2行第8个字符开始打印
       put_str(intr_name[vec_nr]);
       if (vec_nr == 14) {	  // 若为Pagefault,将缺失的地址打印出来并悬停
          int page_fault_vaddr = 0; 
          asm ("movl %%cr2, %0" : "=r" (page_fault_vaddr));	  // cr2是存放造成page_fault的地址
          put_str("\npage fault addr is ");
          put_int(page_fault_vaddr); 
       }
       put_str("\n!!!!!!!      excetion message end    !!!!!!!!\n");
      // 能进入中断处理程序就表示已经处在关中断情况下,
      // 不会出现调度进程的情况。故下面的死循环不会再被中断。
       while(1);
    }
    
    • 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
    (2.2)注册时钟中断处理函数
    ……
        uint32_t ticks;
    ……
    static void intr_timer_handler(void){
        struct task_struct* cur_thread = running_thread();
        ASSERT(cur_thread->stack_magic == 0x19870916);
    
        cur_thread->elapsed_ticks++;        // 记录占用 CPU的时间
        ticks++;                            // 从内核开始处理第一次中断后开始至今的滴答数
    
        if(cur_thread->ticks == 0){
            schedule();
        }
        else{
            cur_thread->ticks--;
        }
    }
    
    void timer_init(){
        put_str("timer init");
        // 设置 8253 的定时周期,也就是发中断的周期 
        frequency_set( CNTRERO_PORT, \
        COUNTERO_NO, \
        READ_WRITE_LATCH, \
        COUNTER MODE, \
        COUNTERO_VALUE);
        // 注册时钟中断函数
        register_handler(0x20, intr_timer_handler);
        put_str("timer_init done\n") ;
    }
    
    • 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
    (2.3)中断注册函数
    /*  中断处理程序数组第 vector_no 个元素中
    注册安装中断处理程序 function   */
    void register_handler(uint32_t vec_no, intr_handler* function){
        /*  idt_table 数组中的函数是在进入中断后根据中断向量号调用的
        * 见 kernel/kernel.S 的 call [idt_table+%1*4]  */
        idt_table[vec_no] = function;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    (3)PART2-调度器schedule
    /*  实现任务调度    */
    void schedule(){
        // 系统关中断下进行
        ASSERT(intr_get_status == INTR_OFF);
    
        struct task_struct *cur_thread = running_thread();
        
        // 若此线程只是 cpu 时间片到了,将其加入到就绪队列尾
        if(cur_thread->status == TASK_RUNNING){
            ASSERT(&thread_ready_list, &cur_thread->general_tag);
            list_append(&thread_ready_list, &cur_thread->general_tag);
            // 重新将当前线程的 ticks 再重置为其 priority
            cur_thread->ticks = cur_thread->priority;
            cur_thread->status = TASK_READY;
        }
        else{
            /*  若此线程需要某事件发生后才能继续上 cpu 运行,
                不需要将其加入队列,因为当前线程不在就绪队列中  */
        }
        ASSERT(list_empty(&thread_ready_list));
        /* 从就绪队列取出下一个就绪线程*/
        // 清空线程节点
        thread_tag = NULL;
        thread_tag = list_pop(&thread_ready_list);
        struct task_struct *next_thread = elem2entry(struct task_struct, general_tag, thread_tag);
        next_thread->status = TASK_RUNNING;
        // 载入寄存器
        switch_to(cur_thread, next_thread);
    }
    
    • 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
    1. 判断当前线程是否需要继续执行,决定是否重新加入就绪队列
    2. 从就绪队列pop出下一个线程
    3. switch_to载入新线程寄存器组

    要求在关中断下进行

    (4)PART3-任务切换函数switch_to

    任务切换的过程包括:保存当前任务上下文;执行中断处理程序,保存当前中断处理程序上下文;执行下一个任务,因此

    需要保存任务的上下文,既需要保存中断发生时任务的寄存器、栈状态,同时也要保存内核中任务的还未执行的环境。具体包括两部分:

    在这里插入图片描述

    (1)上下文保护的第一部分负责保存任务进入中断前的全部寄存器,目的是能让任务恢复到中断前 。

    通过kernel.S完成中断前的保存工作,以及中断处理程序跳转执行工作

    extern idt_table		 ;idt_table是C中注册的中断处理程序数组
    
    %macro VECTOR 2
    section .text
    intr%1entry:		 ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
    
       %2				 ; 中断若有错误码会压在eip后面 
    ; 以下是保存上下文环境
       push ds
       push es
       push fs
       push gs
       pushad			 ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
    
       ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
       mov al,0x20                   ; 中断结束命令EOI
       out 0xa0,al                   ; 向从片发送
       out 0x20,al                   ; 向主片发送
    
       push %1			 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
       call [idt_table + %1*4]       ; 调用idt_table中的C版本中断处理函数
       jmp intr_exit
    
    section .data
       dd    intr%1entry	 ; 存储各个中断入口程序的地址,形成intr_entry_table数组
    %endmacro
    
    section .text
    global intr_exit
    intr_exit:	     
    ; 以下是恢复上下文环境
       add esp, 4			   ; 跳过中断号
       popad
       pop gs
       pop fs
       pop es
       pop ds
       add esp, 4			   ; 跳过error_code
       iretd
    
    • 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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    (2)上下文保护的第二部分负责保存这 4 个寄存器 esi 、 edi 、 ebx 和 ebp ,目的是让任务恢复执行在任务切换发生时剩下尚未执行的内核代码,保证顺利走到退出中断的出口,利用第一部分保护的寄存器环境彻底恢复任务。

    [bits 32]
    section .text
    global switch_to
    switch_to:
       ;栈中此处是返回地址	       
       push esi
       push edi
       push ebx
       push ebp
    
       mov eax, [esp + 20]		 ; 得到栈中的参数cur, cur = [esp+20]
       mov [eax], esp                ; 保存栈顶指针esp. task_struct的self_kstack字段,
    				 ; self_kstack在task_struct中的偏移为0,
    				 ; 所以直接往thread开头处存4字节便可。
    ;------------------  以上是备份当前线程的环境,下面是恢复下一个线程的环境  ----------------
       mov eax, [esp + 24]		 ; 得到栈中的参数next, next = [esp+24]
       mov esp, [eax]		 ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,
    				 ; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针
       pop ebp
       pop ebx
       pop edi
       pop esi
       ret				 ; 返回到上面switch_to下面的那句注释的返回地址,
    				 ; 未由中断进入,第一次执行时会返回到kernel_thread
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    (5)PART4-启用线程调度

    系统初始化函数中加入thread_init()函数,在main函数中加入thread_start()函数。

    开启执行……

    总结:

    pushad 
    
    • 1

    本指令将EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI 这8个32位通用寄存器依次压入堆栈,其中SP的值是在此条件指令未执行之前的值,压入堆栈之后,ESP-32–>ESP。

    popad
    
    • 1

    本指令依次弹出堆栈中的32位字到 EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX中,弹出堆栈之后,ESP+32–>ESP。

  • 相关阅读:
    Vue 3响应式对象: ref和reactive
    Java核心知识:重写(Override)与重载(Overload)
    基于FPGA的IIR滤波器的实现(1)— MATLAB函数实现
    java基于springboot校园餐厅订餐管理系统附源码
    常见网络协议总结(五层&自顶向下)
    笔记(上):mysql-DuplicateUpdate和java的threadpool的“死锁“
    C++ 内存泄漏检测与实现
    计算机毕业设计Java校园摄影爱好者交流网站(源码+系统+mysql数据库+Lw文档)
    DOE认证是什么
    【每日一题】 和为 K 的子数组
  • 原文地址:https://blog.csdn.net/zhengmmm1999/article/details/133768755