用汇编在8088单板机上创建一个进程
在8088单板机上用汇编语言创建进程本质上是实现一个极简的多任务切换机制。由于8088工作在实模式、无MMU所有进程共享同一物理地址空间进程切换的核心是保护/恢复CPU寄存器现场即上下文切换。一、整体思路协作式多任务模型在资源有限的8088单板机上典型配置RAM约32KB、ROM 64KB、无MMU常见的实现方式是协作式、单地址空间、中断驱动的微型OS。其核心特点单地址空间所有任务共享同一内存地址空间任务间独立依赖编程约定没有硬件保护协作式调度任务必须主动调用调度函数yield让出CPU非抢占式切换核心保存当前任务的寄存器到其私有栈恢复下一个任务保存的寄存器通过IRET返回执行可选定时器可用8253定时器产生周期中断如每20ms在中断中调用调度器本文采用协作式实现无定时器抢占这样代码最简洁适合教学理解。你也可以在此基础上加入中断调用。二、核心数据结构任务控制块TCB每个任务需要一个任务控制块TCB来记录其运行状态。在8088实模式下一个极简TCB只需保存任务的下一个TCB指针用于循环调度任务的栈指针备份SS:SP任务的程序入口CS:IP为实现简单我们直接让每个任务拥有独立栈区域TCB只保存栈指针备份。text; TCB结构定义4字节 TCB_Next dw 0 ; 下一任务TCB指针 TCB_SP dw 0 ; 任务栈指针备份SS:SP实际实现中还可以添加一个字节作为任务状态标志就绪/运行/等待但协作式调度下最简单的是循环就绪队列。三、实现步骤详解步骤1定义TCB数组和任务栈; 定义最大任务数和TCB区域 MAX_TASKS EQU 3 TCB_TABLE DW MAX_TASKS DUP(0, 0) ; 每任务占2个字Next指针 SP备份 CurrentTask DW 0 ; 当前运行的任务索引 (0~MAX_TASKS-1) ; 定义各任务的私有栈区域每任务256字节 Task0Stack DB 256 DUP(0) Task1Stack DB 256 DUP(0) Task2Stack DB 256 DUP(0)步骤2初始化TCB与任务栈在系统启动时为每个任务设置栈指针和任务入口。InitTasks: mov cx, MAX_TASKS ; 任务个数 mov si, offset Task0Stack ; 指向任务0的栈底部 mov di, offset TCB_TABLE ; 指向TCB表 xor bx, bx ; 任务索引从0开始 InitTask: ; 计算任务栈顶(栈底256-2因为栈向下增长) mov ax, ss ; 内核栈段 add si, 256 - 2 mov [di2], si ; 保存栈指针备份到TCB_SP ; 链接TCB: 下一任务指针 mov ax, bx inc ax cmp ax, MAX_TASKS jb SetNext xor ax, ax ; 最后一个任务指向任务0 SetNext: shl ax, 1 ; 每个TCB占4字节Next SP索引*2得偏移 add ax, offset TCB_TABLE mov [di], ax ; 保存Next指针 ; 更新到下一个任务 add di, 4 inc bx add si, 256 - 2 - 256 ; si恢复到下一个任务的栈底 loop InitTask ret更常用的做法在每个任务的私有栈顶预先压入任务入口CS:IP和FLAGS这样第一次切换时通过IRET即可直接进入任务执行。下面展示如何在任务栈中构造初始上下文; 初始化任务0栈构造返回上下文 mov ax, SEG Task0_Entry mov bx, OFFSET Task0_Entry push ax ; 压入CS push bx ; 压入IP pushf ; 压入FLAGS模拟IRET弹出的状态将Task0Stack作为栈底栈指针指向上述压栈后的位置存到TCB_SP。切换时用IRET直接弹出FLAGS、IP、CS进入任务。步骤3编写任务切换函数核心任务切换的核心是保存当前任务的寄存器AX,BX,CX,DX,SI,DI,BP,DS,ES等到其栈将当前SP保存到当前TCB_SP从当前TCB.Next获取下一个TCB的SP并加载到SS:SP从新任务的栈恢复寄存器执行IRET返回新任务继续执行实际代码实现如下TaskSwitch: ; 保存当前任务现场到栈中 push ax push bx push cx push dx push si push di push bp push ds push es ; 保存当前任务的栈指针到TCB_SP mov bx, [CurrentTask] ; 当前任务索引 shl bx, 1 ; 索引*4得TCB偏移(每个TCB4字节) shl bx, 1 add bx, offset TCB_TABLE 2 ; TCB_SP的偏移 mov [bx], sp ; 保存当前SP到TCB_SP ; 切换到下一个任务 sub bx, 2 ; bx指向TCB.Next mov bx, [bx] ; 取下一任务的TCB指针 mov ax, [bx] ; 取下一任务的TCB_SP mov sp, ax ; 切换栈 ; 恢复新任务现场 pop es pop ds pop bp pop di pop si pop dx pop cx pop bx pop ax iret步骤4创建新任务的入口每个任务是一个独立的程序段任务结束后必须调用调度器或无限循环避免CPU失控Task0_Entry: ; 任务0代码 call Task1_Start jmp $ ; 或调用调度器循环为方便演示可让每个任务在控制台输出字符串后主动让出CPUTask0_Entry: mov dx, offset Msg0 call PrintString call Yield ; 主动让出CPU jmp Task0_Entry ; 无限循环 Task1_Entry: mov dx, offset Msg1 call PrintString call Yield jmp Task1_Entry步骤5让出CPU函数YieldYield函数调用TaskSwitch实现切换Yield: call TaskSwitch ret或使用软件中断实现系统调用; 设置INT 21h系统调用 SysCall: cmp ah, 1 je Yield_Service iret Yield_Service: call TaskSwitch iret四、完整代码示例下面给出一个完整的协作式多任务切换汇编程序在8088/DOS下可直接汇编运行。; ************************************************** ; 8088 单板机多任务切换演示协作式调度 ; 编译: MASM mytask.asm ; 链接: LINK mytask.obj ; 运行: mytask.exe ; ************************************************** CODE SEGMENT ASSUME CS:CODE, DS:DATA, SS:STACK START: ; 初始化DS mov ax, DATA mov ds, ax ; 初始化TCB表 call InitTasks ; 设置当前任务为任务0 mov [CurrentTask], 0 ; 设置初始栈从任务0开始 mov bx, offset TCB_TABLE 2 ; TCB_SP of task0 mov sp, [bx] ; 模拟IRET 进入任务0 pop ax pop bx pop cx pop dx pop si pop di pop bp pop ds pop es iret ; 初始化任务栈和TCB InitTasks: mov cx, MAX_TASKS mov si, offset Task0Stack mov di, offset TCB_TABLE xor bx, bx InitLoop: ; 为任务栈顶构造初始返回上下文 mov ax, SEG Task0_Entry push ax mov ax, OFFSET Task0_Entry push ax pushf ; 保存栈指针到TCB mov sp, si add sp, 254 mov [di2], sp ; 创建TCB链表 mov ax, bx inc ax cmp ax, MAX_TASKS jb SetNext xor ax, ax SetNext: shl ax, 1 add ax, offset TCB_TABLE mov [di], ax add di, 4 inc bx add si, 256 loop InitLoop ret ; 任务切换函数 TaskSwitch: push ax push bx push cx push dx push si push di push bp push ds push es mov bx, [CurrentTask] shl bx, 1 shl bx, 1 add bx, offset TCB_TABLE 2 mov [bx], sp sub bx, 2 mov bx, [bx] mov sp, [bx 2] pop es pop ds pop bp pop di pop si pop dx pop cx pop bx pop ax iret ; Yield系统调用 Yield: call TaskSwitch ret ; 任务0入口 Task0_Entry: mov dx, offset Msg0 call PrintString call Yield jmp Task0_Entry ; 任务1入口 Task1_Entry: mov dx, offset Msg1 call PrintString call Yield jmp Task1_Entry ; 简单字符打印调用DOS中断 PrintString: mov ah, 9 int 21h ret CODE ENDS DATA SEGMENT Msg0 db Task 0 running..., 0Dh, 0Ah, $ Msg1 db Task 1 running..., 0Dh, 0Ah, $ MAX_TASKS equ 2 TCB_TABLE dw MAX_TASKS DUP(0, 0) CurrentTask dw 0 DATA ENDS STACK SEGMENT Task0Stack db 256 DUP(?) Task1Stack db 256 DUP(?) STACK ENDS END START五、在此基础上扩展如果你想在此基础上加入定时器抢占时间片轮转可以在代码中加入8253/8259中断处理初始化8253定时器设置每20ms产生一次中断编写中断服务例程在中断中保存当前寄存器、调用TaskSwitch切换任务、恢复新任务在中断向量表中注册将中断向量指向定时器ISR清除中断屏蔽寄存器允许中断这样就能实现简单的基于时间片的抢占式多进程了。六、注意事项堆栈溢出风险每个任务栈只分配了256字节实际使用要注意避免递归或大局部变量导致栈溢出共享资源冲突多任务共享打印等资源时需要自己加锁或确保互斥协作式下可依靠主动让出规避无内存保护一个任务的错误可能破坏其他任务数据需要开发者严格遵守编程约定单板机运行如果是真正的8088单板机无DOS环境需要自己实现字符输出的硬件级代码通过8255并口或串口驱动显示希望这份代码能帮助你在8088上跑起自己的微型多进程系统