在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 [di+2], 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 $ ; 或调用调度器循环为方便演示,可让每个任务在控制台输出字符串后主动让出CPU:
Task0_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函数(Yield)
Yield函数调用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 [di+2], 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上跑起自己的微型"多进程"系统!