内核与应用分离

简介: 内核与应用分离

引言

  • 思考:在当前设计中,内核与应用间的界限是否清晰?
  • 很显然,一点也不清晰,内核程序与应用程序统一被编译在一起,应用程序也被当做内核中的一部分
  • 所以本章节我们的重点工作就是将应用程序与内核拆分开来

拆分应用程序

  • 我们来重新设计一下架构,把应用程序从内核中拆分出去

  • 回顾之前的系统启动流程

  • 拆分应用程序后的流程如下

拆分应用程序之代码重构

  • 代码见:app.capp.htask.ctask.hschedule.cmain.c
  • 前面的实验中,我们把任务测试代码都写在了 “main.c” 文件中,这显然是不合理的,现在,我们就来把它从 “main.c” 中移出去
  • 创建 “app” 文件夹,其中新增 “app.c” 文件,创建对应的 “app.h” 文件并放到 “include” 文件夹下,那么最外层 “BUILD.json” 配置文件需要在 “dir” 中增加 “app” 文件夹名,同时在 “app” 文件夹下也要新增一个 “BUILD.json” 配置文件负责管理其下的工程源文件
  • 首先,我们把原本在 “main.c” 中的任务相关代码移到 “app.c” 中
U08 taskA_stack[512];   // 任务私有栈
void TaskAFunc(void)    // 任务执行函数
{
    static U32 count = 0;
    while(1)
    {
        if(count++ % 10000 == 0)
        {
            static U32 j = 0;
            asm volatile("cli");
            SetCursorPos(0, 6);
            printk("TASK A: %d\n", j++); 
            asm volatile("sti");
        } 
    }
}
... 省略任务 B C D
  • 把 app 和 task 分层,即 app 中不直接调用 task 内容,而是把数据传递到 task,task 负责处理这些数据,我们用 APP_INFO 这个数据结构来传递数据,
typedef struct APP_INFO
{
    void        (*pfunc)(void);
    U08*        stackAddr;
    U16         stackSize;
    U08*        name;
    E_APP_PRI   priority;
} APP_INFO;
  • 传递过来的数据放到 TASK_OOP taskOop[MAX_TASK_NUM] 数组中,TASK_OOP 类型定义如下:
typedef struct TASK_OOP
{
    QUEUE_NODE      QueueNode;
    U08             active;
    TASK            task;
} TASK_OOP;
  • 在 app 模块中,我们将应用程序相关数据写到 appInfo 这个数组中
APP_INFO appInfo[MAX_APP_NUM] = {0};
U16 appNum = 0;
E_RET AppRegister(APP_FUNC pFunc, U08* stackAddr, U16 stackSize, U08* name, E_APP_PRI priority)
{
    if(appNum >= MAX_APP_NUM)
        return E_ERR;
    appInfo[appNum].pfunc = pFunc;
    appInfo[appNum].stackAddr = stackAddr;
    appInfo[appNum].stackSize = stackSize;
    appInfo[appNum].name = name;
    appInfo[appNum].priority = priority;
    appNum++;
    return E_OK;
}
void AppInit(void)
{
    AppRegister(TaskAFunc, taskA_stack, 512, "TASK A", E_APP_PRI5);
    AppRegister(TaskBFunc, taskB_stack, 512, "TASK B", E_APP_PRI7);
    AppRegister(TaskCFunc, taskC_stack, 512, "TASK C", E_APP_PRI9);
    AppRegister(TaskDFunc, taskD_stack, 512, "TASK D", E_APP_PRI11);
}
  • 在 task 模块中,我们将 appInfo 中的应用数据转移到 task 自己的任务数据区 TASK_OOP taskOop[MAX_TASK_NUM] 数组中,并根据这些数据创建任务并加入就绪任务队列
#include <app.h>
extern APP_INFO appInfo[MAX_APP_NUM];
extern U16 appNum;
void TaskInit(void)
{
    U16 index = 0;
    U16 taskNum = 0;
    QueueInit(&TASK_READY_QUEUE);                  // 就绪任务队列初始化
    QueueInit(&TASK_WAIT_QUEUE);                   // 等待任务队列初始化
    // 创建第一个任务(空闲任务)
    TaskCreat(&taskIdle.task, TaskIdleFunc, taskIdle_stack, IDLE_STACK_SIZE, "Idle", E_TASK_PRI15);
    // 将空闲任务节点添加到就绪任务队列中
    QueueAdd(&TASK_READY_QUEUE, (QUEUE_NODE *)&taskIdle);
    for(index = 0; index < MAX_TASK_NUM && taskNum < appNum; index++)
    {
        if(0 == taskOop[index].active)
        {
            taskOop[index].active = 1;
            taskOop[index].task.name = appInfo[taskNum].name;
            taskOop[index].task.stack_addr = appInfo[taskNum].stackAddr;
            taskOop[index].task.stack_size = appInfo[taskNum].stackSize;
            taskOop[index].task.task_entry = appInfo[taskNum].pfunc;
            taskOop[index].task.priority = appInfo[taskNum].priority;
            TaskCreat(&taskOop[index].task, taskOop[index].task.task_entry, taskOop[index].task.stack_addr, taskOop[index].task.stack_size, taskOop[index].task.name, taskOop[index].task.priority);
            QueueAdd(&TASK_READY_QUEUE, (QUEUE_NODE *)&taskOop[index]);
            taskNum++; 
        }
    }
}
  • 注意:由于任务节点改变, “schedule.c” 中 schedule 函数中也要稍微修改一下
void schedule(void)
{
    ...
    // current_task = (volatile TASK *)((TASK_QUEUE_NODE *)QUEUE_NODE(node, TASK_QUEUE_NODE, QueueNode)->task);
    current_task = (volatile TASK *)&(((TASK_OOP *)QUEUE_NODE(node, TASK_OOP, QueueNode))->task);
    ...
}
  • 注意别忘了任务销毁也要改动
E_RET TaskDestory(void)
{ 
    ...
    ((TASK_OOP *)QUEUE_NODE(node, TASK_OOP, QueueNode))->active = 0;
    ...
}

拆分应用程序之单独编译 app

  • 上面哗啦哗啦写了一大堆,然而实际上应用程序和内核还是编译在一起,现在我们把它们分开,单独编译出应用程序
  • 我们可以单独实现一个编译脚本放到 “app” 文件夹下,专门用于对应用程序部分代码进行编译管理,目前 “app” 文件下目录结构如下:
app
 |--- AppBuild.py
 |--- BUILD.json
 |--- aentry.asm
 |--- app.c
  • 参照 “Build.py”, 实现 “AppBuild.py” ,与 “Build.py” 区别是 “AppBuild.py” 仅实现对 app 的编译和链接工作,见:AppBuild.py
  • 由于使用 “AppBuild.py” 进行编译,所以 “BUILD.json” 配置文件稍微做一下修改,“inc” 项中头文件路径改变,这个是 app 下的头文件路径
{
    "dir" : [
        ],
    "src" : [
        "aentry.asm",
        "app.c"
        ],
    "inc" : [
        "../include"
        ]
}
  • 可以参照 “kentry.asm” 文件实现 “aentry.asm” 文件,作为应用程序入口,其内容如下:
[section .text]
global _start
extern AppInit
_start:
AppEntry:
    push ebp
    mov ebp, esp
    call AppInit
    leave
    ret
  • 注意了,虽然该修改的代码都已经修改了,但是链接时需要用到 “print.o” 这个编译中间文件,由于脚本原因,只链接 “output/app” 下的 “.o” 文件,我们可以手动把 “output” 文件夹下的 “print.o” 文件复制到 “output/app”,这样子再执行脚本就不会出问题了
  • 切换到 “app” 目录下,执行命令:“python AppBuild.py”, 最终生成 “app.bin” 文件

拆分应用程序之内核与应用之间的数据交互

  • 应用程序已经被分离出去了,于是,又产生了新的问题,内核与应用之间的数据又该如何交互呢?比如内核 “task.c” 中就需要获得 app 中下面的两个数据

APP_INFOappInfo[MAX_APP_NUM];

U16appNum;

  • 原本是统一编译成一个程序,直接使用 “extern” 关键字即可,现在内核与应用分离,肯定不能用 “extern” 关键字了。
  • 解决办法:共享内存
  • 既然说到共享内存了,那么顺便也将 app 程序的加载地址也考虑一下,重新规划一下内存使用,把内核与应用间共享数据定义在 0xA800 位置,把 app 加载到地址 0x80000,注意这个地址不要大于 1M,因为在实模式下 x86 处理器能访问的最大地址就是 0xFFFFF(1M内),我一开始就规划过大,发现始终无法将 app 程序加载到那个内存地址处
  • 注意 “AppBuild.py” 中链接地址也要修改为 0x80000

  • 先来考虑内核与应用之间共享内存的使用
  • 在 “app.c” 中往贡献内存中写数据
void AppInit(void)
{
    ...
    // 把应用数据放入共享内存 0xA800 处
    *((volatile U32*)0xA800) = appInfo;
    *((volatile U32*)0xA804) = appNum;
    ...
}
• 在内核 “task.c” 中读共享内存数据
void TaskInit(void)
{
    ...
    APP_INFO* pAppInfo = (APP_INFO *)(*(U32 *)APP_INFO_ADDR);
    U32 appNum = *((U32*)(APP_NUM_ADDR));
    ...
}
  • 想要内核能跳转到应用程序中执行,我们可以使用函数指针的形式实现

typedefvoid(*APP_INIT)(void);

APP_INITAppInit=(APP_INIT)0x80000;   // 0x80000: 应用程序加载地址

AppInit();                              // 应用程序模块初始化

  • 注意:由于内核与应用分离,根目录下的 “BUILD.json” 配置文件中 “dir” 项应去掉 “app”,因为 app 相关工程代码已经在前面单独编译了
  • 代码见:app.ctask.cshare.hmain.c

拆分应用程序之加载应用程序

  • 目前我们已经能利用 “AppBuild.py” 成功编译出 app.bin 程序了,接下来就要把 “app.bin” 写到 “a.img” 中,前面已经实现了将 boot、loader 以及 kernel 写入 “a.img” 中,现在写 app 也是类似的,具体就不再详细介绍了,代码实现见:Build.py
  • “a.img” 制作成功之后,接下来就是在 “loader.asm” 中将 app 数据读到内存 0x80000 地址处,这里有个需要说明的就是 0x80000 地址超过了 16 位寄存器范围,而 rd_disk_to_mem 函数中使用 bx 寄存器传参,想要访问地址 0x80000 ,于是将 ds 段基址寄存器赋值为 0x8000, mov [bx], ax 这条指令其实相当于 mov [ds:bx], ax,ds:bx 这种表示方法其实就等同于 ds*16+bx,ds*16 就等于 ds 左移 4 位,0x8000 左移 4 位即 0x80000
; 将硬盘扇中 app 数据读入到内存  0x80000 处 
mov ax, [0x700]     ; loader 所占扇区数
add ax, [0x702]     ; + kernel 扇区数
add ax, 2           ; + 2 得到 app.bin 起始扇区 
mov cx, [0x704]
; 因为 app 加载地址 0x80000 超过 0xFFFF,通过改动段基址 ds = 0x8000 实现访问内存地址 0x80000
mov dx, 0x8000
mov ds, dx
mov bx, 0x0000
call rd_disk_to_mem
; 需恢复 ds=0, 下面的程序需要 ds 为 0
mov dx, 0x0
mov ds, dx
  • 到这里程序就已经完成了,为了测试,我们稍微修改一下 “aentry.asm”
_start:
AppEntry:
    mov eax, 0x8899      ; 仅用于调试
    mov ebx, 0x5566      ; 仅用于调试
    jmp $                ; 仅用于调试
    push ebp
    mov ebp, esp
    call AppInit
    leave
    ret
  • 好了,跑起来看一看,当然,啥也看不见,程序死在了上面 app 入口的 jmp $ 处
  • 使用 Ctrl+C 退出,使用 “reg” 指令,得到:
eax: 0x00008899 34969
ecx: 0x000007f8 2040
edx: 0x00000000 0
ebx: 0x00005566 21862
esp: 0x00007bdc 31708
ebp: 0x00007be8 31720
esi: 0x00000b04 2820
edi: 0x000009ac 2476
eip: 0x0008000a
eflags 0x00003002: id vip vif ac vm rf nt IOPL=3 of df if tf sf zf af pf cf
  • 从中我们可以看出,eax,ebx 的值已被成功修改为我们想要的值,这说明目前程序已经能从 kernel 跳转到 app 执行了
  • 代码见:loader.asmaentry.asm

异常处理

  • 去掉 “aentry.asm” 中的调试代码,跑起来
_start:
AppEntry:
    push ebp
    mov ebp, esp
    call AppInit
    leave
    ret
  • 果然没有想象中的美好,总要出点问题,貌似只有空闲任务运行,其它任务都没运行起来

  • 从 app 入口开始往里查,程序从 AppEntry 执行到 AppInit,我们在 AppInit 函数开头放个打印信息看看程序有没有进来执行注册 app 数据
void AppInit(void)
{
    printk("AppInit\n");
    ...
}
  • 这回运行一下,现象更奇特了,注意了,注意了,不仔细看差点没发现,打印第一行 “Boot...” 消失了

  • 初步排除 boot 部分代码问题,因为成功加载了 loader,左思右想,那就是打印第一行被其它打印覆盖了,那么肯定怀疑刚加的 AppInit 函数中的打印
  • 继续思考,app 中调用的 print 打印相关函数跟 kernel 中是调用同一个函数吗?
  • 很显然,由于我们独立编译了 app,链接时还复制了 print.o 文件,这说明内核 print 相关函数与 app 的 print 相关函数用的不是同一个地址,仅仅只是名字相同而已,kernel 和 app 中各有一份
  • 于是在 app 中初始化一下,增加打印颜色和设置光标位置,因为我怀疑默认的打印颜色正好是黑色,所以打印出来的字符串看不到
void AppInit(void)
{
    SetCursorPos(0, 3);                     // 设置光标位置: (0, 3)
    SetFontColor(E_FONT_WHITE);             // 设置打印字体颜色: 白色
    printk("AppInit\n");
    ...
}
  • 再次运行看一下现象,果然如上面猜想的一样,这回成功打印出 “AppInit” 字符串了,但是其它几个任务依旧没有运行起来

  • 顺着程序运行往下查呗,接下来就是把应用数据放入共享内存 0xA800 处,这个代码并没有什么问题,那么就是 “task.c” 中获取应用数据出问题了
  • 一看,果然,共享内存地址定义出错了
// #define APP_INFO_ADDR           (SHARE_START_ADDR + 0xA800)
// #define APP_NUM_ADDR            (SHARE_START_ADDR + 0xA804)
#define APP_INFO_ADDR           (SHARE_START_ADDR + 0x800)
#define APP_NUM_ADDR            (SHARE_START_ADDR + 0x804)
  • 修改完成,再次编译运行,哈哈哈,这回终于 OK 了

  • 本次异常问题解决相关代码见:aentry.asmapp.cshare.h
目录
相关文章
|
Prometheus Kubernetes 监控
Kubernetes 性能调优与成本控制
【8月更文第29天】随着 Kubernetes 在企业中的广泛应用,如何有效地管理和优化 Kubernetes 集群的性能和成本成为了一个重要的课题。本篇文章将介绍 Kubernetes 性能监控的基础知识,以及一些实用的成本优化技巧,包括资源配额的设置、Pod 密度的提高和集群规模的合理调整。
722 1
|
人工智能 自然语言处理 Kubernetes
「我的AIGC咒语库:分享和AI对话交流的秘诀——如何利用Prompt和AI进行高效交流?」1
前言 基础介绍 什么是Prompt? 什么是 Prompt Engineering? 为什么需要 Prompt Engineering? 如何进行 Prompt Engineering? Prompt的基本原则 Prompt的编写模式 AI 可以帮助程序员做什么? 技术知识总结 拆解任务 阅读代码/优化代码 代码生成 生成单测 更多 AI 应用/插件
2085 1
|
运维 监控 Shell
掌握100个开箱即用的Shell脚本~(附PDF)
Shell脚本是实现Linux系统管理及自动化运维所必备的重要工具。许多其它岗位的小伙伴也经常使用Shell脚本来实现某项需求。 今天分享《100个shell脚本案例》,共有55页,支持文字搜索定位,代码清晰可复制。
|
缓存
正在等待缓存锁:无法获得锁 /var/lib/dpkg/lock-frontend。锁正由进程 12836(unattended-upgr)持有
正在等待缓存锁:无法获得锁 /var/lib/dpkg/lock-frontend。锁正由进程 12836(unattended-upgr)持有
7788 0
正在等待缓存锁:无法获得锁 /var/lib/dpkg/lock-frontend。锁正由进程 12836(unattended-upgr)持有
|
前端开发 JavaScript 搜索推荐
webpack进阶篇(十七):静态资源内联
webpack进阶篇(十七):静态资源内联
533 0
webpack进阶篇(十七):静态资源内联
|
C++
QML语法之property属性
QML语法之property属性
551 3
|
负载均衡 并行计算 安全
【Qt 线程】探索Qt线程编程的奥秘:多角度深入剖析(三)
【Qt 线程】探索Qt线程编程的奥秘:多角度深入剖析
429 0
|
人工智能 IDE 程序员
【程序员小知识】AndroidStudio 与 IntelliJ IDEA 的版本关系
【程序员小知识】AndroidStudio 与 IntelliJ IDEA 的版本关系
646 0
|
算法 数据可视化 前端开发
第三代软件开发-自定义Slider(一)
欢迎来到我们的 QML & C++ 项目!这个项目结合了 QML(Qt Meta-Object Language)和 C++ 的强大功能,旨在开发出色的用户界面和高性能的后端逻辑。 在项目中,我们利用 QML 的声明式语法和可视化设计能力创建出现代化的用户界面。通过直观的编码和可重用的组件,我们能够迅速开发出丰富多样的界面效果和动画效果。同时,我们利用 QML 强大的集成能力,轻松将 C++ 的底层逻辑和数据模型集成到前端界面中。 在后端方面,我们使用 C++ 编写高性能的算法、数据处理和计算逻辑。C++ 是一种强大的编程语言,能够提供卓越的性能和可扩展性。我们的团队致力于优化代码,减少资
|
SQL 关系型数据库 MySQL
【MySQL用法】MySQL动态SQL语句标签的详细使用方法说明
【MySQL用法】MySQL动态SQL语句标签的详细使用方法说明
528 0