《一个操作系统的实现》笔记(6)--进程


我们可以把一个单独的任务所用到的所有东西封装在一个LDT中,这种思想是多任务处理的雏形。 多任务所用的段类型如下图,使用LDT来隔离每个应用程序任务的方法,正是关键保护需求之一:

进程示意:

我们需要一个数据结构记录一个进程的状态,在进程要被挂起的时候,进程信息就被写入这个数据结构,等到进程重新启动的时候,这个信息重新被读出来。


最简单的进程

进程切换的过程: - 1.进程A运行中 - 2.时钟中断发生,ring1->ring0,时钟中断处理器启动 - 3.进程调度,下一个应运行的进程B被指定 - 4.进程B恢复,ring0->ring1 - 5.进程B运行中

进程表

保存进程信息的数据结构称为进程表,或叫进程控制块,即PCB。

进程栈和内核栈

esp的位置出现在3个不同的区域: - 进程栈–进程运行时自身的堆栈 - 进程表–存储进程状态信息的数据结构 - 内核栈–进程调度模块运行时使用的堆栈


第1步–ring0->ring1

开始第一个进程,我们使用iretd指令来实现由ring0到ring1的转移,转移成功后,就可以认为A进程在运行了。

进程表数据结构

typedef struct s_stackframe {   /* proc_ptr points here             ↑ Low           */
    u32 gs;     /* ┓                        │           */
    u32 fs;     /* ┃                        │           */
    u32 es;     /* ┃                        │           */
    u32 ds;     /* ┃                        │           */
    u32 edi;        /* ┃                        │           */
    u32 esi;        /* ┣ pushed by save()               │           */
    u32 ebp;        /* ┃                        │           */
    u32 kernel_esp; /* <- 'popad' will ignore it            │           */
    u32 ebx;        /* ┃                        ↑栈从高地址往低地址增长*/      
    u32 edx;        /* ┃                        │           */
    u32 ecx;        /* ┃                        │           */
    u32 eax;        /* ┛                        │           */
    u32 retaddr;    /* return address for assembly code save()  │           */
    u32 eip;        /*  ┓                       │           */
    u32 cs;     /*  ┃                       │           */
    u32 eflags;     /*  ┣ these are pushed by CPU during interrupt  │           */
    u32 esp;        /*  ┃                       │           */
    u32 ss;     /*  ┛                       ┷High           */
}STACK_FRAME;


typedef struct s_proc {
    STACK_FRAME regs;          /* process registers saved in stack frame */

    u16 ldt_sel;               /* gdt selector giving ldt base and limit */
    DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */

    int ticks;                 /* remained ticks */
    int priority;

    u32 pid;                   /* process id passed in from MM */
    char p_name[16];           /* name of the process */
}PROCESS;

当要恢复一个进程时,便将esp指向这个结构体的开始处,然后运行一系列的pop命令将寄存器值弹出。

实现ring0->ring1 而堆栈的信息也不外乎ss和esp两个寄存器。 由于要为下一次ring1->ring0做准备,所以用iretd返回之前要保证tss.esp0是正确的。

restart:
    mov esp, [p_proc_ready]
    lldt    [esp + P_LDT_SEL]
    lea eax, [esp + P_STACKTOP]
    mov dword [tss + TSS3_S_SP0], eax
restart_reenter:
    dec dword [k_reenter]
    pop gs
    pop fs
    pop es
    pop ds
    popad
    add esp, 4
    iretd

进程表及相关数据结构对应关系:

第一个进程的启动过程:


第2步–丰富中断处理程序

赋值tss.esp0

由ring0到ring1时,推展的切换直接在指令iretd被执行时就完成了,目标代码的cs、eip、ss、esp等都是从堆栈中得到的,这很简单。但ring1到ring0切换时就免不了用到TSS了。 而堆栈的信息也不外乎ss和esp两个寄存器。 由于要为下一次ring1->ring0做准备,所以用iretd返回之前要保证tss.esp0是正确的。

现在的中断例程: 在中断发生的开始,esp的值是刚刚从TSS里面渠道的进程表A中的regs的最高地址,然后个寄存器的值被压栈入进程表,然后esp指向regs的最低地址处,然后设置tss.esp0的值,准备下一次进程被中断时使用。

内核栈

    mov esp, StackTop       ; 切到内核栈
    ;...
    mov esp, [p_proc_ready] ; 离开内核栈

中断重入

中断程序是被动的。 为了避免这种嵌套现象的发生,我们必须想一个办法让中断程序知道自己是不是在嵌套执行。 只要设置一个全局变量就可以了。 目前我们的处理,如果发现当前是嵌套的,则直接跳到最后,结束中断处理程序的执行。


多进程

从进程A切换到进程B之前,如何保留和恢复现场(即各寄存器的值)? 后面会提到。

一个进程只要有一个进程体和堆栈就可以运行了。

typedef void    (*task_f)   ();
typedef struct s_task {
    task_f  initial_eip;
    int stacksize;
    char    name[32];
}TASK;

TASK    task_table[NR_TASKS] = 
{{TestA, STACK_SIZE_TESTA, "TestA"},
{TestB, STACK_SIZE_TESTB, "TestB"}};

void TestA()
{
    //...
}
void TestB()
{
    //...
}

初始化到proc_table中,从TASK结构中读取不同任务入口地址、堆栈栈顶和进程名,然后赋值给相应的进程表项。

    for(i=0;i<NR_TASKS;i++){
        strcpy(p_proc->p_name, p_task->name);   // name of the process
        p_proc->pid = i;            // pid
        p_proc->ldt_sel = selector_ldt;

        memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS >> 3],
               sizeof(DESCRIPTOR));
        p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5;
        memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS >> 3],
               sizeof(DESCRIPTOR));
        p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5;
        p_proc->regs.cs = ((8 * 0) & SA_RPL_MASK & SA_TI_MASK)
            | SA_TIL | RPL_TASK;
        //...
        p_proc->regs.eip = (u32)p_task->initial_eip;
        p_proc->regs.esp = (u32)p_task_stack;

        p_task_stack -= p_task->stacksize;
        p_proc++;
        p_task++;
        selector_ldt += 1 << 3;
    }

LDT

填充 GDT 中进程的 LDT 的描述符。

    int i;
    PROCESS* p_proc = proc_table;
    u16 selector_ldt = INDEX_LDT_FIRST << 3;
    for(i=0;i<NR_TASKS;i++){
        init_descriptor(&gdt[selector_ldt>>3],
                vir2phys(seg2phys(SELECTOR_KERNEL_DS),
                    proc_table[i].ldts),
                LDT_SIZE * sizeof(DESCRIPTOR) - 1,
                DA_LDT);
        p_proc++;
        selector_ldt += 1 << 3;
    }

每个进程都会在GDT中对应一个LDT描述符。 每个进程都有自己的LDT.所以当进程切换时需要重新加载ldtr

多进程的实现–交替执行A和B进程

一个进程如何由“睡眠”态变成“运行”态?

无非是将esp指向进程表项的开始处,然后在执行lldt之后经历一系列pop指令恢复各个寄存器的值。一切的信息都包含在进程表中。所以,要想恢复不同的进程,只需要将esp指向不同的进程表就可以了。 在离开内核栈时给esp赋值。

    ;...
    mov esp, StackTop       ; 切到内核栈
    ;...
    call    clock_handler
    ;...    
    mov esp, [p_proc_ready] ; 离开内核栈
    lldt    [esp + P_LDT_SEL]
    lea eax, [esp + P_STACKTOP]
    mov dword [tss + TSS3_S_SP0], eax
    ;...

一个进程如何由“运行”态变成“睡眠”态?

当CPU不再执行该进程的代码指令时,就可以认为这个进程已经睡眠了。 那么这个寄存器的值是怎么保存的呢?肯定是保存在该进程的进程表里的,因为由“睡眠”态变成“运行”态就是从这里获取的信息。 保护的时机就在进程调度切换之前。 我们在时钟中断切换进程时这样写:

hwint00:        ; Interrupt routine for irq 0 (the clock).
    sub esp, 4
    pushad      ; `.
    push    ds  ;  |
    push    es  ;  | 保存原寄存器值
    push    fs  ;  |
    push    gs  ; /
    mov dx, ss
    mov ds, dx
    mov es, dx

    inc byte [gs:0]     ; 改变屏幕第 0 行, 第 0 列的字符
    ;...
    mov esp, StackTop       ; 切到内核栈
    ;...
    call    clock_handler
    ;...

    mov esp, [p_proc_ready] ; 离开内核栈
    lldt    [esp + P_LDT_SEL]
    lea eax, [esp + P_STACKTOP]
    mov dword [tss + TSS3_S_SP0], eax
    ;...
    pop gs  ; `.
    pop fs  ;  |
    pop es  ;  | 恢复原寄存器值
    pop ds  ;  |
    popad       ; /
    add esp, 4

    iretd

简单来说,在调用clock_handler之前, 我们保存的是进程A的寄存器到esp所指向的堆栈,也就是进程表A(从ring1跳到ring0,esp的值变成TSS中夜色少的ring0下的esp值)。 之后esp被切换成进程B的堆栈,所以pop出来的就是保存在进程表B里的寄存器值了。


系统调用

用户进程因为特权级的关系,无法访问某些权限更高的内存区域, 只能通过系统调用来实现,它是应用程序和操作系统之间的桥梁。 用中断可以方便地实现系统调用。

实现一个简单的系统调用

操作系统给应用程序提供一个get_ticks()的系统调用,用来获得当前总共发生了多少次时钟中断。 系统调用的过程: - 1、“问”,告诉操作系统自己要什么; - 2、操作系统“找”,即处理; - 3、“回答”,也就是把结果返回给进程。

;syscall.asm
_NR_get_ticks       equ 0 ; 要跟 global.c 中 sys_call_table 的定义相对应!
INT_VECTOR_SYS_CALL equ 0x90

global  get_ticks ; 导出符号

bits 32
[section .text]

get_ticks:
    mov eax, _NR_get_ticks
    int INT_VECTOR_SYS_CALL
    ret

sys_call_table是一个函数指针数组,每一个成员都指向一个函数,用以处理相应的系统调用。注意:sys_call是内核调用的,如果要把返回值告诉应用进程的话,需要把函数的返回值放在进程表eax的位置,以便进程P被恢复执行时eax中装的是正确的返回值。

;kernel.asm
sys_call:
        call    save
        sti
        call    [sys_call_table + eax * 4]
        mov     [esi + EAXREG - P_STACKBASE], eax ; 把函数的返回值放在进程表eax的位置,以便进程P被恢复执行时eax中装的是正确的返回值。
        cli
        ret

进程调度

进程优先级调度

在中断发生时,我们要优先级选择下一个要执行的进程时。

PUBLIC void schedule()
{
    PROCESS* p;
    int  greatest_ticks = 0;

    while (!greatest_ticks) {
        for (p = proc_table; p < proc_table+NR_TASKS; p++) {
            if (p->ticks > greatest_ticks) {
                greatest_ticks = p->ticks;
                p_proc_ready = p;
            }
        }

        if (!greatest_ticks) {
            for (p = proc_table; p < proc_table+NR_TASKS; p++) {
                p->ticks = p->priority;
            }
        }
    }
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏「3306 Pai」社区

关于MySQL 8.0的几个重点,都在这里

在MySQL8.0中重新设计了redo log,主要改进fsync,使得效率更高,减少锁,优化flush机制,不会频繁flush。同时,支持更高用户并发请求。

680
来自专栏linux驱动个人学习

Linux下进程的创建过程分析(_do_fork do_fork详解)--Linux进程的管理与调度(八)

Unix标准的复制进程的系统调用时fork(即分叉),但是Linux,BSD等操作系统并不止实现这一个,确切的说linux实现了三个,fork,vfork,cl...

642
来自专栏xingoo, 一个梦想做发明家的程序员

HBase官方文档 之 Region的相关知识

一般来说对于每个Region Server,官方推荐最好是控制Region的数量在20-200个、大小在5-20Gb左右。

830
来自专栏cs

研究生的一份试题的几道题节选

首先祝朋友考研成功,勇往直前,我是不考研的,所以完全以提高能力,使用为主,不在意细节。小伙伴让我帮忙看了一下试卷,故截取了几道题目。 c我是真的应了那句话,从入...

3378
来自专栏北京马哥教育

awk学习笔记

awk是一种模式扫描和处理工具,相对于grep的查找,sed的编辑,它在对数据进行分析生成报表时显得尤为强大。awk通过逐行遍历一个或多个 文件的方式,查找模...

2656
来自专栏Linux驱动

第3阶段——内核启动分析之start_kernel初始化函数(5)

内核启动分析之start_kernel初始化函数(init/main.c) stext函数启动内核后,就开始进入start_kernel初始化各个函数, 下面只...

26510
来自专栏刘望舒

Android深入四大组件(四)广播的注册、发送和接收过程

前言 我们接着来学习Android四大组件中的BroadcastReceiver,广播主要就是分为注册、接收和发送过程。建议阅读此文前请先阅读Android深入...

1936
来自专栏开发与安全

《linux c 编程一站式学习》课后部分习题解答

1、假设变量x和n是两个正整数,我们知道x/n这个表达式的结果要取Floor,例如x是17,n是4,则结果是4。如果希望结果取Ceiling应该怎么写表达式呢?...

2756
来自专栏Kubernetes

docker stats命令源码分析结果

本文是基于docker 1.10.3版本的源码,对docker stats命令进行源码分析,看看docker stats命令输出的数据是从cgroups fs中...

3168
来自专栏linux驱动个人学习

Linux进程描述符task_struct结构体详解--Linux进程的管理与调度(一)【转】

Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在include/linux/sc...

642

扫码关注云+社区