
1. 为什么需要文件系统在 PA3-3 中我们使用 ramdisk 存放用户程序但程序一多管理它们的位置就成了问题。更重要的是用户程序自己也需要处理数据比如读取图片、音频它们不可能直接知道数据在 ramdisk 的哪个偏移。因此操作系统必须提供更高层的抽象——文件。文件本质上是字节序列附带一些属性名字、大小、存储位置等。文件系统就是负责管理这些映射并向上提供统一的操作接口。在 Nanos-lite 中我们实现一个极其简化的文件系统sfsSimple File System它有以下限制每个文件大小固定写文件不能超过原有大小文件数量固定不能创建或删除没有目录目录分隔符/只是文件名的一部分这些限制极大降低了实现难度。2. sfs 的存储布局所有文件一个接一个地存放在 ramdisk 中0 --------------------------------------------- | file0 | file1 | ...... | filen | --------------------------------------------- \ / \ / \ / size0 size1 sizen 为了记录每个文件的名字、大小和偏移我们维护一个文件记录表。在代码中它是一个Finfo数组由nanos-lite/src/files.h自动生成通过 Makefile 脚本从navy-apps/fsimg/目录内容生成。例如// nanos-lite/src/files.h (自动生成){/bin/hello,37664,0},{/bin/bird,182976,830504},...3. 文件记录表与文件描述符在fs.c中file_table数组的前几项是特殊文件enum{FD_STDIN,FD_STDOUT,FD_STDERR,FD_EVENTS,FD_DISPINFO,FD_FB};staticFinfo file_table[]__attribute__((used)){[FD_STDIN]{stdin,0,0,0,invalid_read,invalid_write},[FD_STDOUT]{stdout,0,0,0,invalid_read,serial_write},[FD_STDERR]{stderr,0,0,0,invalid_read,serial_write},[FD_EVENTS]{/dev/events,0,0,0,events_read,invalid_write},[FD_DISPINFO]{/proc/dispinfo,0,0,0,dispinfo_read,invalid_write},[FD_FB]{/dev/fb,0,0,0,invalid_read,fb_write},#includefiles.h// 普通文件};Finfo结构体定义了文件的所有信息typedefstruct{char*name;// 文件名size_tsize;// 文件大小size_tdisk_offset;// 在 ramdisk 中的偏移size_topen_offset;// 当前读写偏移量ReadFn read;// 读函数指针NULL 则使用默认 ramdisk 读WriteFn write;// 写函数指针NULL 则使用默认 ramdisk 写}Finfo;disk_offset在初始化时就固定了。open_offset记录文件当前读写位置每次读写后增加。read/write函数指针用于实现虚拟文件系统VFS让不同文件有不同的读写方式普通文件直接用 ramdisk设备文件则调用设备驱动。4. 文件操作实现4.1fs_openintfs_open(constchar*pathname,intflags,intmode){for(inti0;iLENGTH(file_table);i){if(strcmp(file_table[i].name,pathname)0){file_table[i].open_offset0;// 重置偏移returni;// 返回文件描述符即数组下标}}return-1;// 未找到根据需求可触发 panic}因为文件数量固定我们直接遍历文件表匹配名字后返回下标作为文件描述符fd。flags和mode在 sfs 中忽略。4.2fs_readsize_tfs_read(intfd,void*buf,size_tlen){if(fdFD_STDERR)return0;// stdin/stdout/stderr 不支持读// 对于特殊文件有自定义 read 函数直接调用if(file_table[fd].read!NULL)returnfile_table[fd].read(buf,file_table[fd].open_offset,len);// 普通文件从 ramdisk 读取size_treal_lenmin(len,file_table[fd].size-file_table[fd].open_offset);ramdisk_read(buf,file_table[fd].disk_offsetfile_table[fd].open_offset,real_len);file_table[fd].open_offsetreal_len;returnreal_len;}边界检查不能读取超过文件大小的数据。使用open_offset实现顺序读写同时支持lseek调整位置。4.3fs_writesize_tfs_write(intfd,constvoid*buf,size_tlen){if(fdFD_STDIN)return0;// 特殊文件有 write 函数指针if(file_table[fd].write!NULL)returnfile_table[fd].write(buf,file_table[fd].open_offset,len);// 普通文件size_treal_lenmin(len,file_table[fd].size-file_table[fd].open_offset);ramdisk_write(buf,file_table[fd].disk_offsetfile_table[fd].open_offset,real_len);file_table[fd].open_offsetreal_len;returnreal_len;}4.4fs_lseeksize_tfs_lseek(intfd,size_toffset,intwhence){if(fdFD_STDERR)return0;longnew_offfile_table[fd].open_offset;switch(whence){caseSEEK_SET:new_offoffset;break;caseSEEK_CUR:new_offoffset;break;caseSEEK_END:new_offfile_table[fd].sizeoffset;break;}if(new_off0)new_off0;if(new_off(long)file_table[fd].size)new_offfile_table[fd].size;file_table[fd].open_offsetnew_off;returnnew_off;}4.5fs_closeintfs_close(intfd){return0;// sfs 没有打开/关闭状态直接返回成功}5. 让 loader 使用文件原先 loader 直接调用ramdisk_read现在改为使用文件系统接口intfdfs_open(/bin/hello,0,0);fs_read(fd,ehdr,sizeof(ehdr));...更换用户程序时只需修改naive_uload()中传入的文件名即可。6. 一切皆文件与虚拟文件系统VFS6.1 思想Unix 哲学中“一切皆文件”设备、管道、进程信息等都可以视为字节序列通过统一的文件接口open/read/write/close操作。Nanos-lite 将 IOE 设备也抽象成文件串口 →/dev/tty通过stdout/stderr描述符键盘事件 →/dev/events帧缓冲 →/dev/fb屏幕信息 →/proc/dispinfo这样做的好处是用户程序无需知道设备类型一律使用read()/write()访问通过 VFS 分派到具体的驱动函数。6.2 VFS 的实现函数指针Finfo中的read和write函数指针是实现 VFS 的关键。如果指针为NULL则认为是普通文件走 ramdisk 读写否则调用自定义函数。init_fs()中会初始化/dev/fb的大小voidinit_fs(){AM_GPU_CONFIG_T cfgio_read(AM_GPU_CONFIG);file_table[FD_FB].sizecfg.width*cfg.height*4;}7. 设备抽象的具体实现7.1 串口输出stdout和stderr的写函数都指向serial_writesize_tserial_write(constvoid*buf,size_toffset,size_tlen){for(size_ti0;ilen;i)putch(((constchar*)buf)[i]);returnlen;}offset被忽略因为串口是字符设备没有位置概念。7.2 时钟我们提供一个gettimeofday系统调用用于获取当前时间。在 Nanos-lite 内核中size_tsys_gettimeofday(structtimeval*tv,structtimezone*tz){AM_TIMER_UPTIME_T uptimeio_read(AM_TIMER_UPTIME);tv-tv_secuptime.us/1000000;tv-tv_usecuptime.us%1000000;return0;}NDL 库中基于它实现NDL_GetTicks()uint32_tNDL_GetTicks(){structtimevaltv;gettimeofday(tv,NULL);returntv.tv_sec*1000tv.tv_usec/1000;}7.3 键盘事件/dev/events按键事件用文本表示kd 键名\n按下或ku 键名\n松开。在events_read中实现size_tevents_read(void*buf,size_toffset,size_tlen){AM_INPUT_KEYBRD_T keyio_read(AM_INPUT_KEYBRD);if(key.keycodeAM_KEY_NONE)return0;constchar*namekeyname[key.keycode];inttotal_lensnprintf(buf,len,%s %s\n,key.keydown?kd:ku,name);returntotal_len;}该文件不需要lseek字符设备。7.4 屏幕信息/proc/dispinfosize_tdispinfo_read(void*buf,size_toffset,size_tlen){AM_GPU_CONFIG_T cfgio_read(AM_GPU_CONFIG);returnsnprintf(buf,len,WIDTH:%d\nHEIGHT:%d\n,cfg.width,cfg.height);}NDL 中NDL_OpenCanvas通过读取这个文件获取屏幕分辨率例如voidNDL_OpenCanvas(int*w,int*h){intfdopen(/proc/dispinfo,0,0);charbuf[128];read(fd,buf,sizeof(buf));sscanf(buf,[WIDTH] : [%d]\n[HEIGHT] : [%d],screen_w,screen_h);if(*w0*h0){*wscreen_w;*hscreen_h;}}7.5 帧缓冲/dev/fb与像素绘制详解帧缓冲framebuffer本质上是一块连续内存每个像素用 4 字节32 位颜色00RRGGBB表示所有像素按行优先顺序线性排列。屏幕坐标系以左上角为原点(0,0)x 轴向右增大y 轴向下增大屏幕坐标系与字节偏移 (0,0) ────────────────────────────────────► x │ │ (x,y) ●──────────────────────┐ │ │ │ │ │ 矩形区域 │ h │ │ (宽度 w) │ │ │ │ │ └──────────────────────┘ │ (xw-1, yh-1) │ │ 从原点(0,0)开始每行 W 个像素每个像素 4 字节。 │ 像素 (x,y) 的字节偏移为 │ offset (y * W x) * 4 │ ▼ y反过来给定字节偏移offset可反推坐标pixel_index offset / 4; x pixel_index % W; y pixel_index / W;用户程序调用NDL_DrawRect(pixels, x, y, w, h)时该函数逐行将像素写入/dev/fbvoidNDL_DrawRect(uint32_t*pixels,intx,inty,intw,inth){intfdopen(/dev/fb,0,0);for(inti0;ih;i){intoffset4*((yi)*screen_wx);lseek(fd,offset,SEEK_SET);write(fd,(void*)((uint8_t*)pixelsi*w*4),w*4);}close(fd);}内核收到write后VFS 将请求交给fb_write它把字节偏移转换为屏幕坐标并调用 AM 绘图接口size_tfb_write(constvoid*buf,size_toffset,size_tlen){AM_GPU_CONFIG_T cfgio_read(AM_GPU_CONFIG);intx(offset%(cfg.width*4))/4;intyoffset/(cfg.width*4);io_write(AM_GPU_FBDRAW,x,y,(void*)buf,len/4,1,true);return0;}AM 层将像素数据拷贝到硬件的帧缓冲地址void__am_gpu_fbdraw(AM_GPU_FBDRAW_T*ctl){intxctl-x,yctl-y,wctl-w,hctl-h;uint32_t*pixelsctl-pixels;uint32_t*fb(uint32_t*)(uintptr_t)FB_ADDR;uint32_tscreen_winl(VGACTL_ADDR)16;for(inti0;ih;i){uint32_t*dstfb(yi)*screen_wx;uint32_t*srcpixelsi*w;memcpy(dst,src,w*sizeof(uint32_t));}if(ctl-sync)outl(SYNC_ADDR,1);}完整调用链如图所示应用层 (Navy-apps / NDL) ┌─────────────────────────────────────────────────────────────────────────────┐ │ bmp-test 用户程序 │ │ │ │ void *bmp BMP_Load(/share/pictures/projectn.bmp, w, h); │ │ NDL_OpenCanvas(w, h); ← 获取屏幕尺寸 │ │ NDL_DrawRect(bmp, 0, 0, w, h); ← 绘制整张图片 │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ NDL_DrawRect(pixels, x, y, w, h) │ │ │ │ int fd open(/dev/fb, 0, 0); │ │ for (int i 0; i h; i) { │ │ int offset 4 * ((y i) * screen_w x); ← 计算第 i 行字节偏移 │ │ lseek(fd, offset, SEEK_SET); ← 定位到该行起始位置 │ │ write(fd, pixels i*w*4, w*4); ← 写入整行像素数据 │ │ } │ │ close(fd); │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ┌────────────────┼────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ open() │ │ lseek() │ │ write() │ │ 系统调用 │ │ 系统调用 │ │ 系统调用 │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 内核层系统调用分发 (do_syscall) │ │ │ │ case SYS_open: → fs_open(/dev/fb, ...) → 返回 fdFD_FB (即 5) │ │ case SYS_lseek: → fs_lseek(fd, offset, SEEK_SET) → 设置 open_offset │ │ case SYS_write: → fs_write(fd, buf, len) │ │ │ │ │ ▼ │ │ file_table[fd].write ! NULL ? │ │ │ │ │ ▼ │ │ fb_write(buf, open_offset, len) │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ fb_write(buf, offset, len) │ │ │ │ AM_GPU_CONFIG_T cfg io_read(AM_GPU_CONFIG); ← 获取屏幕宽度 │ │ int x (offset % (cfg.width * 4)) / 4; ← 字节偏移 → x 坐标 │ │ int y offset / (cfg.width * 4); ← 字节偏移 → y 坐标 │ │ io_write(AM_GPU_FBDRAW, x, y, buf, len/4, 1, true); │ │ │ │ │ │ │ │ │ │ │ └─ sync true │ │ │ │ └─ w 1 (一次一行) │ │ │ └─ h len/4 (像素数) │ │ └─ 起始坐标 (x, y) │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ AM 层__am_gpu_fbdraw (被编译成 RISC-V 指令) │ │ │ │ uint32_t *fb (uint32_t *)(uintptr_t)FB_ADDR; ← FB_ADDR 0xa1000000 │ │ uint32_t screen_w inl(VGACTL_ADDR) 16; ← VGACTL_ADDR 0xa0000100│ │ │ │ for (int i 0; i h; i) { │ │ uint32_t *dst fb (y i) * screen_w x; ← 计算目标地址 │ │ uint32_t *src pixels i * w; ← 计算源地址 │ │ memcpy(dst, src, w * sizeof(uint32_t)); ← 写入显存通过 MMIO │ │ } │ │ │ │ if (ctl-sync) outl(SYNC_ADDR, 1); ← SYNC_ADDR 0xa0000104通知刷新 │ └────────────────────────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 硬件层 (NEMU 模拟的 VGA) │ │ │ │ vmem 数组接收像素数据 │ │ vga_update_screen() 检测到 sync 标志 │ │ → SDL_UpdateTexture / SDL_RenderPresent │ │ → 宿主机窗口刷新显示 │ └─────────────────────────────────────────────────────────────────────────────┘8. 系统调用接口在do_syscall()中增加以下分支caseSYS_open:c-GPRxfs_open((constchar*)a[1],a[2],a[3]);break;caseSYS_read:c-GPRxfs_read(a[1],(void*)a[2],a[3]);break;caseSYS_write:c-GPRxfs_write(a[1],(constvoid*)a[2],a[3]);break;caseSYS_lseek:c-GPRxfs_lseek(a[1],a[2],a[3]);break;caseSYS_close:c-GPRxfs_close(a[1]);break;用户层 libos 中的_write、_read等会通过_syscall_调用它们从而让应用程序可以使用标准 C 库的文件操作。9. 测试与运行通过以下命令生成整合了 Navy-apps 全部文件的 ramdisk 镜像makeARCHriscv32-nemu update然后运行文件系统测试makeARCHriscv32-nemu run并在 Nanos-lite 中加载/bin/file-test若一切正确会输出PASS!!!。10. 总结PA3-4 的简易文件系统 sfs 虽然简陋但它体现了文件系统的核心概念文件抽象、描述符、偏移量管理、VFS 与设备文件。通过将串口、键盘、屏幕等设备建模为文件Nanos-lite 实现了一切皆文件的 Unix 哲学为上层应用程序提供了统一简洁的接口。同时VFS 中函数指针的使用展示了用 C 语言实现多态的方法也为后续扩展更复杂的文件系统奠定了基础。