六、x86cpu手工实现简单的多进程并发

写在开头:由于真正的并发需要操作系统的调度算法进行优化,本文仅在x86cpu的保护模式下实现进程间的切换,而且是c语言和汇编直接对寄存器操作,比较简陋,不涉及任何系统调度算法,仅供参考

这里先简单介绍两个概念

1、x86cpu中断 

中断是指cpu在运行某一任务时收到系统的中断信号,然后停止正在运行的任务转而去执行中断栈中所对应的任务,在执行完由中断设置的任务之后(或者在执行这个任务的过程中又收到中断信号去执行别的任务)本文例子仅举例两个进程之间的切换,所以会继续执行原来完成一部分的任务。

五、x86处理器手动创建进程中断栈并切换特权级-CSDN博客

2、tss结构

TSS(Task State Segment)即任务状态段。在 x86 系统结构中,每个任务(可以理解为进程或线程)都可以对应一个独立的 TSS,它是内存中的一个结构体,里面包含了几乎所有的 CPU 寄存器的映像,例如通用寄存器的值、段寄存器的值、栈寄存器的值、EFLAGS 寄存器等。
TSS 的作用是方便操作系统管理进程和进行任务切换。当进行任务切换时,CPU 可以将当前任务的相关寄存器信息保存到当前 TSS 中,然后从目标任务的 TSS 中加载寄存器映像,从而实现执行现场的切换。

在 32 位的 x86 CPU 中,TSS 段结构(struct tss32)通常包含以下成员:
backlink:可能用于链接到前一个任务的 TSS 或其他相关信息。
esp0、ss0:用于存储特权级 0(内核态)的栈指针和栈段选择子。
esp1、ss1:可能用于其他特权级的栈指针和栈段选择子。
esp2、ss2:类似地,可用于其他特权级或特定情况下的栈指针和栈段选择子。
cr3:页目录基址寄存器,用于分页机制。
eip:指令指针,指向下一条要执行的指令地址。
eflags:标志寄存器,包含各种状态标志和控制标志。
eax、ecx、edx、ebx、esp、ebp、esi、edi:通用寄存器。
es、cs、ss、ds、fs、gs:段寄存器。
ldtr:局部描述符表寄存器的段选择子。
iomap:I/O 许可位图。
如下图

那么如何在代码中配置tss结构呢

在本文中我们最后实现的是两个进程之间的切换,所以这里先初始化两个进程,task0和task1

void task_0 (void) {
    for (;;) {
    }
}


void task_1 (void) {
    for (;;) {
    }
}

这里随便简单初始化一下,因为重点不在进程的内容。 

然后在gdt表中初始化,下图是tss表的描述符

#define TASK0_TSS_SEL           (5 * 8)   //gdt表第五位
#define TASK1_TSS_SEL           (6 * 8)   //gdt表第六位


//tss结构初始化
[TASK0_TSS_SEL/ 8] = {0x0068, 0, 0xe900, 0x0},   //0x0068代表申请的tss大小为104个字节,起始地址暂时不能设置,看后面申请的栈地址
[TASK1_TSS_SEL/ 8] = {0x0068, 0, 0xe900, 0x0}, //0xe900:特权级为3,存在位为1,段类型默认0

设置tss表起始地址

gdt_table[TASK0_TSS_SEL / 8].base_l = (uint16_t)(uint32_t)task0_tss;  //bass_l,16位地址,因为整个task0,task1栈大小不会超过64k
gdt_table[TASK1_TSS_SEL / 8].base_l = (uint16_t)(uint32_t)task1_tss;

通过TR寄存器判断目前运行的是task0还是task1

mov $TASK0_TSS_SEL, %ax		// 加载任务0对应的状态段,因为系统从启动之后先运行的进程是task0
ltr %ax

call task_sched //在中断中调用task_sched调试函数,这个调试函数优先级是0

task0和task1的tss结构配置,进程的上下文切换,保存上文到tss结构中,在任务切换回时,tss结构中的值会恢复到相应的寄存器中去,设置eip寄存器,将eip寄存器值设置为要恢复的函数的地址

uint32_t task0_tss[] = {
   //如果在task0运行中突然发生中断,则会切换到特权级0,给task0单独分配一个相应栈空间,去执行中断代码初,始值为内核数据段

    0,  (uint32_t)task0_dpl0_stack + 4*1024, KERNEL_DATA_SEG , 0x0, 0x0, 0x0, 0x0,
   //进程在切换时页表可以发生变化,每个进程可以有自己对应的映射关系,这里延用之前配置好的恒等映射,eip指向task1

    (uint32_t)pg_dir,  (uint32_t)task_0/*入口地址*/, 0x202, 0xa, 0xc, 0xd, 0xb, (uint32_t)task0_dpl3_stack + 4*1024/* 栈 */, 0x1, 0x2, 0x3,

    //因为工作中特权级三的模式下,所以全部设成应用数据段

    APP_DATA_SEG, APP_CODE_SEG, APP_DATA_SEG, APP_DATA_SEG, APP_DATA_SEG, APP_DATA_SEG, 0x0, 0x0,
};

//对照intel白皮书中的tss结构内容看

uint32_t task1_tss[] = {

    0/*prelink*/,  (uint32_t)task1_dpl0_stack + 4*1024/*esp0*/, KERNEL_DATA_SEG/*ss0*/ ,  0x0/*esp1*/, 0x0/*ss1*/, 0x0/*esp2*/, 0x0,/*ss2*/

    //cr3, eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi

    (uint32_t)pg_dir,  (uint32_t)task_1, 0x202, 0xa, 0xc, 0xd, 0xb, (uint32_t)task1_dpl3_stack + 4*1024, 0x1, 0x2, 0x3,

    // es, cs, ss, ds, fs, gs, ldt, iomap

    APP_DATA_SEG, APP_CODE_SEG, APP_DATA_SEG, APP_DATA_SEG, APP_DATA_SEG, APP_DATA_SEG, 0x0, 0x0,
};

 task运行的时候还要再额外再分配一个栈空间,因为task里有局部变量、函数调用,在特权级3的模式下运行

uint32_t task0_dpl0_stack[1024], task0_dpl3_stack[1024], task1_dpl0_stack[1024], task1_dpl3_stack[1024];

 调试函数,在函数中判断

void task_sched (void) {
    static int task_tss = TASK0_TSS_SEL;

    task_tss = (task_tss == TASK0_TSS_SEL) ? TASK1_TSS_SEL : TASK0_TSS_SEL;
//判断当前执行的是不是task0任务,是的话就切换到task1,不是的话就从task1切换回task0
    uint32_t addr[] = {0, task_tss };  //为了下文远跳转指令,要加上代码段选择子和偏移,预先初始化一个数组
    __asm__ __volatile__("ljmpl *(%[a])"::[a]"r"(addr));
//ljmpl 指令用于长跳转,通过指定 addr 数组的地址来获取跳转的目标地址。
}

进程切换的流程

CPU在执行任务时,会根据TSS结构保存和恢复寄存器值,实现任务切换。切换过程中,会保存内核代码段和数据段的值,并在中断处理后恢复。任务切换对进程函数透明,实现进程间来回切换,由于这个切换是我们人为规定时间,时间可以设置的很短,在切换结束后cpu会继续执行原来的进程,这就是现代操作系统并发的雏形。
 

  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值