多进程并行执行

简介: 多进程并行执行

引言

  • 我们的目标是仅仅就是创建一个任务,然后运行这个任务吗?
  • 显然不是,我们的目标是要多个任务同时运行
  • 问题:创建两个任务:TASK A 和 TASK B,执行 TASK A,那么,什么时候执行 TASK B 呢,又由谁来进行任务切换执行呢?

任务切换过程

  • TASK A 执行过程中,如果不被打断,那么 TASK B 就永远不会执行,需要在 TASK A 运行期间无条件的将其打断,如何打断呢?利用时钟中断打断任务的执行,而这个时钟中断我们在上一个章节中已经实现了
  • 打断了之后又要做什么工作吗?很显然,打断之后还必须保存打断前 TASK A 的运行状态,我们将其称之为保存上下文
  • 接下来就是要切换到 TASK B 执行,如何做到呢?答案就是恢复 TASK B 的上下文

  • 继续看一下更加详细的过程

目标

  • 使用时钟中断打断任务(每个任务执行固定的时间片)
  • 中断发生后立即进入中断服务程序
  • 在中断服务程序中完成上下文的保存并切换任务
  • 接下来就来分步实现代码

任务与时钟中断同时实现

  • 前面我们已经单独实现了 TASK A 与时钟中断,接下来就让它们两个同时实现在同一个程序中
  • 就在上一章节的代码基础上改动吧,上一章节已经实现了时钟中断,我们再加上任务 TASK A 的相关代码就可以了,见 main.c
  • 看起来一切都好,然而,当我们运行起来后,发现只有 TASK A 任务在执行,中断貌似并没有执行,什么原因呢?
  • 哈哈哈,深入查找原因,发现在创建任务 TaskCreat 函数中,eflags 被赋值为 0x3002(IPOL=3), 其中 bit9 IF 位并未被置 1, IF 为 0 表示 CPU 不使能外部中断,把 IF 位置 1

task->reg.eflags=0x3202;  // IOPL=3: 允许任务(特权级 3)进行 /O 操作; IF=1: 允许外部中断

  • 编译运行,感觉应该没问题了,然而实际上问题好像更严重了,这次不光中断没有执行,就连 TASK A 任务好像也没有执行,懵逼树上懵逼果,怪事练练
  • Ctrl + C 退出程序,使用 “reg” 命令,发现 esp 寄存器的值为 0x1b9357e4, 莫名其妙的一个值,并不是任务栈,也不是内核栈,这说明是栈出问题
  • 继续思考,当中断发生时,程序由特权级 3 跳转到特权级 0,CPU 使用栈由使用任务栈转到使用内核栈,而内核栈的是由 TSS 决定的,于是我们查看 loader.asm 中的 TSS,发现 esp0 的值为 0,栈顶怎么能是 0 呢,我们将内核栈顶设置为 0x7c00(BOOT_START_ADDR)
TSS_SEGMENT:
    dd    0
    dd    BOOT_START_ADDR     ; esp0
    dd    DATA32_FLAT_SELECTOR; ss0
    dd    0                   ; esp1
    dd    0                   ; ss1
    dd    0                   ; esp2
    dd    0                   ; ss2
    times 4 * 18 dd 0
    dw    0
    dw    $ - TSS_SEGMENT + 2
    db    0xFF   
TSS_LEN    equ    $ - TSS_SEGMENT
  • 再次编译运行,哈哈,这次终于一切 OK,看一下效果

深入思考

  • 上面的中断服务程序和任务看起来都成功执行了,似乎并没什么错误,那么,整个过程就一定是正确的吗?
  • 中断服务程序仅完成了逻辑功能,但在中断发生时并没有保存上下文,在中断发生后也没有恢复上下文

中断服务程序的重新设计

  • 中断发生时,立即保存上下文(寄存器)
  • 逻辑功能实现
  • 中断返回时恢复上下文

保存上下文

  • 任务切换的整个流程我们也已经有了一定的了解,各部分代码也基本上都实现过了,就剩下最后一个功能,保存上下文
  • 进程的初步实现 中,我们已经学习过了如何恢复上下文,那么保存上下文就是跟恢复上下文反着操作而已

  • 首先我们要做的工作就是在中断发生时,让栈顶指针 esp 指针指向 reg 数据结构的末尾,不然中断发生时,① 中的 5 个寄存器会被入栈到内核栈中,这显然不是我们想要的
  • 如何才能实现中断发生时,栈顶指针 esp 指向 reg 数据结构的末尾呢?
  • 方法:把 reg 数据结构的末尾地址值放到 TSS 中 esp0 处即可
  • 问题又来了, TSS 在 loader 中实现,现在在内核中,找不到 TSS 位置
  • 解决方案:共享内存,在 “loader.asm” 把 TSS 位置信息放入共享内存中,在内核 “share.h” 宏定义其位置就可以了
; loader.asm 中
PutDataToShare:
    ...
    ; 将 TSS 基地址放到共享内存 TSS_ENTRY_ADDR 中
    mov dword [TSS_ENTRY_ADDR], TSS_SEGMENT
    ; 将 TSS 大小放到共享内存 TSS_SIZE_ADDR 中
    mov dword [TSS_SIZE_ADDR], TSS_LEN
    ...
    ret
// share.h 中
#define TSS_ENTRY_ADDR          (SHARE_START_ADDR + 24)
#define TSS_SIZE_ADDR           (SHARE_START_ADDR + 28)
  • TSS 数据结构我们还没有定义,在 “desc.h” 中定义一下吧
typedef struct TSS
{
    U32         previous;               // 上一个任务链接(TSS 选择符)
    U32         esp0;                   // 内核栈顶
    U32         ss0;                    // 内核栈基址
    U32         unused[22];             // 不使用
    U16         reserved;               // 保留
    U16         iomb;                   // I/O 位图基地址
} TSS;
  • 利用指针很容易就实现对 TSS 中 esp0 进行修改
TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR);                                   // 找到 TSS
tss->esp0 = (U32)(&taskA.reg) + sizeof(taskA.reg); // 修改 TSS.esp0 = reg 的末尾

增加保存与恢复上下文代码实现

  • 任务切换的所有部分知识都已经了解了,接下来就来真正的实现代码吧
  • 主要相关代码:interrupt.asmmain.cdesc.h
  • 在任务切换 0x20 号中断服务程序如下:
Int0x20_Entry:
    ; 保存上下文, ss esp eflags cs eip 这 5 个寄存器已被 CPU 自动入栈保存
    pushad                      ; 保存通用寄存器
    push ds
    push es
    push fs 
    push gs
    mov esp, KERNEL_STACK       ; 重新指定栈顶 esp 到内核栈,以供接下来的逻辑功能代码部分使用 
    call Int0x20Handle          ; 中断逻辑功能
    mov esp, [gRegAddr]         ; 使栈顶 esp 指向上下文数据结构 reg 的起始位置
    ; 恢复上下文 
    pop gs
    pop fs
    pop es
    pop ds   
    popad                       ; 恢复通用寄存器
    iret
  • 由于中断前我们就已经将 TSS.esp0 修改位任务 TASK A 的上下文 reg 的末尾,CPU 进入中断时会自动将 ss esp eflags cs eip 这 5 个寄存器入栈,即上面图中 ①
  • 接下来的 ② ③ 保存上下文工作就好理解了
  • 再往后是 “mov esp, KERNEL_STACK” 执行语句,它是放在 “call Int0x20Handle” 之前的,因为此时 esp 经过上面的步骤后已经指向了 reg 数据结构的起始位置,接下来再进行入栈操作的话就会破坏 reg 上面的内存区数据,所以必须重新设置一下 esp 的值(KERNEL_STACK 值为 0x7c00)
  • “call Int0x20Handle” 这条语句没什么说的,这是中断逻辑功能部分
  • 再往下就是要恢复中断上下文,想要恢复上下文,关于恢复上下文我们在 进程的初步实现 中已经做过详细介绍了
  • 想要恢复上下文,首先就是找到 reg 的起始位置,然而目前情况是好像并不能找到 reg 的起始位置
  • 于是就利用全局变量来记住 reg 的起始位置

gRegAddr=(U32)&taskA.reg;

  • 在有了上面的赋值操作后, “mov esp, [gRegAddr]” 这个语句就很好理解了吧,就是使得 esp 指向 reg 的起始位置
  • 再往下的语句就完全是我们前面实现过的恢复上下文了
  • 没图没真相,必须看一下运行效果图,虽然效果跟没有保存上下文和恢复上下文的一样

再加一个任务 TASK B

  • 多任务并行执行,最少也得实现两个任务吧,代码见:main.c
  • 参照 TASK A 的代码,复制一个 TASK B,为了区分 TASK A,我们让 TASK B 循环打印 26 个字母
TASK taskB = {0};     // 任务对象
U08 taskB_stack[512];   // 任务私有栈
void TaskBFunc(void)    // 任务执行函数
{
    static U32 count = 0;
    while(1)
    {
        count++;
        if(count % 1000 == 0)
        {
            static U32 j = 0;
            SetCursorPos(0, 6);
            printk("TASK B: %c\n", (j++%26)+'A');   
        }  
    }
}
  • 调用任务创建函数进行 TASK B 的初始化
TaskCreat(&taskB, TaskBFunc, taskB_stack, 512, "Task B");   // 创建任务 TASK B
  • 任务切换是借助时钟中断实现的,所以任务切换代码肯定要写在时钟中断服务程序的逻辑函数
• Int0x20Handle 中
volatile TASK* gTask = NULL;
void Int0x20Handle(void)
{
    static U32 count = 0;
    if(count++ % 5 == 4)
    {
        gTask = (gTask == &taskA) ? &taskB : &taskA;
        TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR);               // 找到 TSS
        tss->esp0 = (U32)(&gTask->reg) + sizeof(gTask->reg);        // TSS.esp0 指向任务上下文数据结构 reg 的末尾
        gRegAddr = (U32)(&gTask->reg);                              // gRegAddr 指向任务上下文数据结构 reg 的起始位置
    }
    write_m_EOI();  
}
  • 下面这条语句的作用是让 TSS.esp0 指向将要跳转的任务的上下文末尾,其作用是在下一次中断服务程序 Int0x20_Entry 进入时保存上下文
tss->esp0 = (U32)(&gTask->reg) + sizeof(gTask->reg);
  • 下面这条语句的作用是使 gRegAddr 指向任务上下文数据结构 reg 的起始位置,当 Int0x20Handle 函数执行结束后,程序将执行 Int0x20_Entry 的下半部分,即恢复任务上下文,而恢复上下文的前提条件就是 esp 指向任务上下文数据结构 reg 的起始位置
gRegAddr = (U32)(&gTask->reg);
  • 上图

代码优化

  • main 函数里面有点乱,简单优化一下
  • 重新在 “task.c” 文件中定义变量,替代 gTask 和 gRegAddr,并在 “task.h” 文件中使用 extern 声明这两个变量

volatileTASK*current_task=NULL;       // 当前任务指针,永远指向当前任务; current_task 代替 gTask

volatileU32current_reg;                   // 当前任务的上下文起始位置; current_reg 代替 gRegAddr

  • 把 main 函数里面的杂乱语句整理放到一个函数中,该函数接口设计如下
  • 函数名称: E_RET TaskStart(TASK* task0)
  • 输入参数: TASK* task0 --任务指针
  • 输出参数: 无
  • 函数返回: E_OK:成功; E_ERR:失败
  • 其它说明:想要启动所有任务,只要启动第一个任务就可以了,其它任务将由任务调度启动
  • 函数具体实现:
E_RET TaskStart(TASK* task0)
{
    // 检查参数合法性
    if(NULL == task0)
        return E_ERR;
    current_task = task0;                                   // 当前任务指针指向 task0
    TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR);           // 找到 TSS
    tss->esp0 = (U32)(&task0->reg) + sizeof(task0->reg);    // TSS.esp0 指向 task0 的上下文数据结构 reg 的末尾   
    current_reg = (U32)&task0->reg;                         // current_reg 指向任务上下文数据结构 reg 的起始位置
    asm volatile("sti");                                    // 开中断
    SWITCH_TO(task0);                                       // 切换到 TASK A 执行
    return E_OK;
}
  • 因为变量名更改,所以 Int0x20Handle 函数和 Int0x20_Entry 中都要记得修改。
  • 改动后的 main.c

解决打印异常

  • 程序长时间运行后,出现了下面异常现象,打印出现了异常

  • 本以为是进程切换代码有问题,查了好久,最终确定进程切换代码并没有问题。打印是需要硬件支持的,一个进程正在打印时被切换到了另一进程,此时另一个进程也需要打印,两个打印同时进行,硬件并不支持这种的情况
  • 优化代码,在打印相关需要硬件参与的代码前加上关中断操作,在打印结束后开中断,保证硬件执行过程不被中断打断
  • 在相关硬件操作前后加上下面代码,用了 eflags_c 是为了记住硬件操作前的状态,执行完毕后要恢复到之前的状态
// 获取 eflags 寄存器的值放到 eflags_c 变量中
asm volatile("pushf;popl %%eax":"=a"(eflags_c));
// 关闭外部中断
asm volatile("cli");
// 硬件操作,省略...
// 若 bit9(IF 位) 为 1,则开启外部中断
if(EFLAGS_IF(eflags_c))
    asm volatile("sti");
  • 打印函数只优化了 printk,其它打印相关函数并没有优化,主要是因为懒,那么此时 “print.h” 中相关打印函数声明就去掉了,只保留 printk 这一个接口函数以供使用,统一了也挺好,省的乱七八糟的
  • 另外,在设置光标位置 SetCursorPos 中端口操作后加了一定的延时,给硬件一定的处理时间
  • 稍不注意还有遗漏,在 “main.c” 中, SetCursorPos 和 printk 同时使用,也有极低概率出现 SetCursorPos 刚设置完光标位置,又被切换到其它任务的 printk 处执行,此时也会出现打印异常情况,最好是在整个设置光标和打印期间都关闭外部中断
asm volatile("cli");
SetCursorPos(0, 4);
printk("change: %d\n", count);
asm volatile("sti");
  • 跟硬件有关的问题查起来都很难,没啥道理可言,这个调好了换一个硬件又有其它的问题,就不深入纠结了,就此 over
  • 优化后的代码:print.cprint.hmain.c
  • 还有更好的优化方式,那就是给一个较大的打印数据循环缓冲区 + 硬件 DMA,只不过这种方式太麻烦了,我懒得搞,现在将就用吧,别频繁打印,慎用打印
目录
相关文章
|
机器学习/深度学习 缓存 Java
Python 线程,进程,多线程,多进程以及并行执行for循环笔记
Python 线程,进程,多线程,多进程以及并行执行for循环笔记
713 0
Python 线程,进程,多线程,多进程以及并行执行for循环笔记
|
并行计算 安全 Java
深入理解Java并发编程:并行与并发、进程与线程、优先级、休眠与让步
深入理解Java并发编程:并行与并发、进程与线程、优先级、休眠与让步
330 0
|
5月前
|
Python
解锁Python并发新世界:线程与进程的并行艺术,让你的应用性能翻倍!
【7月更文挑战第9天】并发编程**是同时执行多个任务的技术,提升程序效率。Python的**threading**模块支持多线程,适合IO密集型任务,但受GIL限制。**multiprocessing**模块允许多进程并行,绕过GIL,适用于CPU密集型任务。例如,计算平方和,多线程版本使用`threading`分割工作并同步结果;多进程版本利用`multiprocessing.Pool`分块计算再合并。正确选择能优化应用性能。
41 1
|
4月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
5月前
|
小程序 Linux
【编程小实验】利用Linux fork()与文件I/O:父进程与子进程协同实现高效cp命令(前半文件与后半文件并行复制)
这个小程序是在文件IO的基础上去结合父子进程的一个使用,利用父子进程相互独立的特点实现对数据不同的操作
128 2
|
6月前
|
开发框架 并行计算 安全
Python的GIL限制了CPython在多核下的并行计算,但通过替代解释器(如Jython, IronPython, PyPy)和多进程、异步IO可规避
【6月更文挑战第26天】Python的GIL限制了CPython在多核下的并行计算,但通过替代解释器(如Jython, IronPython, PyPy)和多进程、异步IO可规避。Numba、Cython等工具编译优化代码,未来社区可能探索更高级的并发解决方案。尽管GIL仍存在,现有策略已能有效提升并发性能。
78 3
|
6月前
|
安全 Java Python
GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。
【6月更文挑战第20天】GIL是Python解释器的锁,确保单个进程中字节码执行的串行化,以保护内存管理,但限制了多线程并行性。线程池通过预创建线程池来管理资源,减少线程创建销毁开销,提高效率。示例展示了如何使用Python实现一个简单的线程池,用于执行多个耗时任务。
50 6
|
6月前
|
Java 程序员
Java多线程编程是指在一个进程中创建并运行多个线程,每个线程执行不同的任务,并行地工作,以达到提高效率的目的
【6月更文挑战第18天】Java多线程提升效率,通过synchronized关键字、Lock接口和原子变量实现同步互斥。synchronized控制共享资源访问,基于对象内置锁。Lock接口提供更灵活的锁管理,需手动解锁。原子变量类(如AtomicInteger)支持无锁的原子操作,减少性能影响。
49 3
|
5月前
|
Java 调度 Windows
Java面试之程序、进程、线程、管程和并发、并行的概念
Java面试之程序、进程、线程、管程和并发、并行的概念
33 0