操作系统通过系统调用向用户程序提供服务, 那么
本次课内容:
之前的NEMU/NPC - 由运行时环境加载
现在用户程序要运行在操作系统上, 也需要运行时环境来加载
加载用户程序 =
将用户程序可执行文件中的代码和数据放到M
中正确位置
具体地, 加载程序需要解决如下问题
navy-apps/libs/libos/src/crt0/start/$ISA.S
中的_start()
开始执行
navy-apps/libs/libos/src/crt0/crt0.c
中的call_main()
main()
main()
返回, 则调用exit()
结束运行
exit()
会执行SYS_exit
系统调用,
请求操作系统结束程序main()
返回后的行为很类似需要把用户程序放在操作系统知道的地方
在Nanos-lite看来, 用户程序就像是存放在内存
memcpy()
我们先假设ramdisk中只有一个文件 - 用户程序的ELF文件
ramdisk_read(buf, 0, 1)
即可访问用户程序ELF的首字节ELF中还包含面向加载的segment(段)视角 - RTFM
+-------+---------------+-----------------------+
| |...............| |
| |...............| | ELF file
| |...............| |
+-------+---------------+-----------------------+
0 ^ |
|<------+------>|
| | |
| |
| +----------------------------+
| |
Type | Offset VirtAddr PhysAddr |FileSiz MemSiz Flg Align
LOAD +-- 0x001000 0x03000000 0x03000000 +0x1d600 0x27240 RWE 0x1000
| | |
| +-------------------+ |
| | |
| | | | |
| | | | |
| | +-----------+ --- |
| | |00000000000| ^ |
| | --- |00000000000| | |
| | ^ |...........| | |
| | | |...........| +------+
| +--+ |...........| |
| | |...........| |
| v |...........| v
+-------> +-----------+ ---
| |
| |
Memory
_start()
的第一条指令!用户程序通过发起系统调用请求提供服务
让用户程序通过通用寄存器指定请求何种服务及其参数传递
Context
结构中读出系统调用的参数SYS_yield
__am_irq_handle()
发现是系统调用,
封装成EVENT_SYSCALL
事件,
并调用Nanos-lite注册的回调函数EVENT_SYSCALL
事件,
就调用系统调用处理函数do_syscall()
do_syscall()
发现Context中的a7是1,
就进行SYS_yield
的处理
SYS_yield
只是走马观花, 操作系统真正需要提供哪些服务?
这不就是AM吗!
只不过现在需要由操作系统实现这些API
AM TRM提供的运行时环境:
main(const char *args)
halt()
putch()
我们需要在操作系统上提供类似的服务:
SYS_exit
,
识别后调用halt()
SYS_write
,
识别后调用putch()
SYS_brk
,
目前总是返回0表示成功AM IOE提供的运行时环境:
ioe_read()
和输出函数ioe_write()
我们需要在操作系统上提供类似的服务:
ioe_read()
- SYS_read
ioe_write()
- SYS_write
应该如何设计一种全面的机制?
我们需要一些新的抽象层!
文件又分两部分
不同的文件系统有不同的组织方式
Nanos-lite采用一种非常简单的文件系统 - sfs(Simple File System)
typedef struct {
char *name; // 文件名
size_t size; // 文件大小
size_t disk_offset; // 文件在ramdisk中的偏移
} Finfo;
static Finfo file_table[] = {
// ...
{"/share/music/little-star.ogg", 140946, 3539914},
{"/share/games/pal/sdlpal.cfg", 70, 3680860},
{"/share/games/bird/sfx_hit.wav", 96020, 31510057},
{"/share/games/nes/mario.nes", 40976, 31745613},
{"/share/pictures/projectn.bmp", 49290, 39308528},
{"/bin/menu", 98184, 39357818},
{"/bin/busybox", 158632, 39456002},
{"/bin/dummy", 33592, 39773266},
{"/bin/pal", 459096, 39806858},
{"/bin/cat", 158632, 40265954},
{"/bin/nterm", 114304, 40424586},
{"/bin/bird", 180784, 40697522},
{"/bin/hello", 37560, 40878306},
// ...
};
/
也是文件名的一部分 - 用户程序以为sfs提供了目录功能
最基本的读写操作:
size_t fs_read(const char *filename, void *buf, size_t len);
size_t fs_write(const char *filename, const void *buf, size_t len);
但是操作系统中有一些没有名字的文件
为了统一管理它们, 操作系统一般通过一个编号来表示文件
file_table
的下标作为文件描述符回顾: Linux程序运行时默认打开3个文件, 通过 “文件描述符”来编号
虽然系统调用是用户程序访问资源的唯一方式, 但为不同设备提供专门的系统调用并不是一个好方案
希望对设备功能进行抽象, 向用户程序提供统一的接口
观察 - 设备的功能虽然很多, 但交互的信息都是字节序列!
字节序列 -> 文件 -> 文件操作
对文件系统的概念进行抽象和扩展
实现起来并不难: 为每个文件添加一组读写函数指针即可
typedef struct {
// ...
ReadFn read; // 读函数指针
WriteFn write; // 写函数指针
} Finfo;
static Finfo file_table[] = {
// ...
[FD_STDOUT] = {"stdout", 0, 0, invalid_read, serial_write},
{"/dev/fb", 0, 0, invalid_read, fb_write},
{"/share/pictures/projectn.bmp", 49290, 39308528, ramdisk_read, ramdisk_write},
// ...
};
size_t fs_write(int fd, const void *buf, size_t len) {
return file_table[fd].write(dest, buf, len);
}
// 向用户程序提供若干特殊文件: /dev/events, /proc/dispinfo, /dev/fb, /dev/sb, /dev/sbctl
内联汇编:
asm volatile (
"li a0, 1\n"
"mv a1, %0\n"
"mv a2, %1\n"
"li a7, %2\n"
"ecall\n"
: : "r"("Hello World!\n"), "r"(13), "i"(SYS_write));
navy-apps/libs/libos/src/syscall.c
为用户程序提供了更好的系统调用封装函数
intptr_t _syscall_(intptr_t type, intptr_t a0, intptr_t a1, intptr_t a2) {
register intptr_t _gpr1 asm ("a7) = type;
register intptr_t _gpr2 asm ("a0) = a0;
register intptr_t _gpr3 asm ("a1) = a1;
register intptr_t _gpr4 asm ("a2) = a2;
register intptr_t ret asm (a0);
asm volatile ("ecall":"=r" (ret):"r"(_gpr1), "r"(_gpr2), "r"(_gpr3), "r"(_gpr4));
return ret;
}
系统级I/O库函数:
size_t write(int fd, void *buf, size_t count) {
return _syscall_(...);
}
size_t read(int fd, void *buf, size_t count) {
return _syscall_(...);
}
int close(int fd) {
return _syscall_(...);
}
C标准I/O库函数:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fclose(FILE *stream);
navy-apps采用Newlib项目作为C标准库的实现
库 | 描述 |
---|---|
libc |
C运行库, 移植自Newlib 3.3.0, 同时包含最小的手写C++运行库(但不支持libstdc++) |
libos |
系统调用接口 |
compiler-rt |
移植自llvm, 主要用于在32位ISA上提供64位除法的支持 |
libfixedptc |
提供定点数计算支持, 包含sin ,
pow , log 等初等函数,
可替代范围不大的浮点数 |
libndl |
NDL(NJU DirectMedia Layer), 提供时钟, 按键, 绘图, 声音的底层抽象 |
libbmp |
支持32位BMP格式文件的读取 |
libbdf |
支持BDF字体格式的读取 |
libminiSDL |
基于NDL的多媒体库, 提供部分兼容SDL库的API |
libvorbis |
OGG音频解码器,
仅支持解码成s16le 格式的PCM编码, 移植自stb项目 |
libam |
通过Navy的运行时环境实现的AM API, 可以运行AM程序, 目前仅支持TRM和IOE |
libSDL_ttf |
基于libminiSDL和stb项目中的truetype字体解析器, 支持truetype字体的光栅化 |
libSDL_image |
基于libminiSDL和stb项目中的图像解码器, 支持JPG, PNG, BMP, GIF的解码 |
libSDL_mixer |
基于libminiSDL和libvorbis, 支持多通道混声, 目前仅支持OGG格式 |
libc
实现/多媒体库通过设备文件与设备交互应用 | 描述 |
---|---|
nslider |
NJU幻灯片播放器, 可以播放4:3的幻灯片 |
menu |
启动菜单, 可以选择需要运行的应用程序 |
nterm |
NJU终端, 包含一个简易內建Shell, 也支持外部Shell |
bird |
flappy bird, 移植自sdlbird |
pal |
仙剑奇侠传, 支持音效, 移植自SDLPAL |
am-kernels |
可在libam的支持下运行部分AM应用, 目前支持coremark, dhrystone和打字小游戏 |
fceux |
可在libam的支持下运行的红白机模拟器 |
oslab0 |
学生编写的游戏集合, 可在libam的支持下运行 |
nplayer |
NJU音频播放器, 支持音量调节和可视化效果, 目前仅支持OGG格式 |
lua |
LUA脚本解释器 |
busybox |
BusyBox套件(版本1.32.0), 系统调用较少时只能运行少量工具 |
onscripter |
NScripter脚本解释器, 在Navy环境中可支持Planetarian, Clannad等游戏的运行 |
nwm |
NJU窗口管理器(需要mmap, newlib中不支持, 故目前只能运行在native) |
基于各种库实现应用程序
期待有一天能运行在自己设计的芯片上!
src/main.c
: PAL_Init()
)
src/global/global.c
:
PAL_InitGlobals()
)
src/game/game.c
: PAL_GameMain()
while (1)
PAL_ProcessEvent()
SDL_GetTicks()
PAL_StartFrame()
VIDEO_UpdateScreen()
做完PA2就可以理解
src/device/input.c
:
PAL_KeyPressHandler()
记录按键状态src/scene/scene.c
:
PAL_UpdateParty()
处理移动的逻辑
src/game/script.c
:
PAL_InterpretInstruction()
)
src/game/script.c
:
PAL_RunTriggerScript()
)
while (wScriptEntry != 0 && !fEnded) { ... }
src/game/script.c
:
PAL_RunAutoScript()
)
src/game/play.c
:
PAL_GameUpdate()
)
所有脚本存放在sss.mkf文件中, 游戏引擎初始化时载入
typedef struct {
SHORT sVanishTime;
WORD x, y;
SHORT sLayer;
WORD wTriggerScript;
WORD wAutoScript;
// ...
} EVENTOBJECT;
typedef struct {
WORD wMapNum;
WORD wScriptOnEnter;
WORD wScriptOnTeleport;
// ...
} SCENE;
typedef struct {
WORD wBitmap;
WORD wPrice;
WORD wScriptOnUse;
WORD wScriptOnEquip;
WORD wScriptOnThrow;
// ...
} OBJECT_ITEM;
原来游戏内部也是一个层次化的系统!
做完PA3, 你就会对操作系统内部有一个简单的认识
仙剑其实也没那么神秘
我们正是从这些 “类似”, 开始逐渐理解一个复杂系统