# 一切皆文件

## 一切皆文件

我们已经提供了完整的文件系统, 用户程序已经可以读写普通的文件了. 想想我们在AM上运行的打字游戏, 读入按键/查询时钟/更新屏幕其实也是用户程序的合理需求, 操作系统也需要提供支持. 一种最直接的方式, 就是为每个功能单独提供一个系统调用, 用户程序通过这些系统调用, 就可以直接使用相应的功能了. 然而这种做法却存在不少问题:

* 首先, 设备的类型五花八门, 其功能更是数不胜数,

  要为它们分别实现系统调用来给用户程序提供接口, 本身就已经缺乏可行性了;
* 此外, 由于设备的功能差别较大, 若提供的接口不能统一, 程序之间的交互就会变得困难.

我们在上一小节中提到, 文件的本质就是字节序列. 事实上, 计算机系统中到处都是字节序列(如果只是无序的字节集合, 计算机要如何处理?), 我们可以轻松地举出很多例子:

* 内存是以字节编址的, 天然就是一个字节序列, 因而我们之前使用的ramdisk作为字节序列也更加显而易见了
* 管道(shell命令中的`|`)是一种先进先出的字节序列, 本质上它是内存中的一个队列缓冲区
* 磁盘也可以看成一个字节序列:

  我们可以为磁盘上的每一个字节进行编号, 例如第x柱面第y磁头第z扇区中的第n字节,

  把磁盘上的所有字节按照编号的大小进行排列, 便得到了一个字节序列
* socket(网络套接字)也是一种字节序列, 它有一个缓冲区, 负责存放接收到的网络数据包,

  上层应用将socket中的内容看做是字节序列, 并通过一些特殊的文件操作来处理它们
* 操作系统的一些信息可以以字节序列的方式暴露给用户, 例如CPU的配置信息
* 操作系统提供的一些特殊的功能, 如随机数生成器, 也可以看成一个无穷长的字节序列
* 甚至一些非存储类型的硬件也可以看成是字节序列:

  我们在键盘上按顺序敲入按键的编码形成了一个字节序列,

  显示器上每一个像素的内容按照其顺序也可以看做是字节序列...

既然文件就是字节序列, 那很自然地, 上面这些五花八门的字节序列应该都可以看成文件. Unix就是这样做的, 因此有"一切皆文件"(Everything is a file)的说法. 这种做法最直观的好处就是为不同的事物提供了统一的接口: 我们可以使用文件的接口来操作计算机上的一切, 而不必对它们进行详细的区分: 例如 `nanos-lite/Makefile`中通过管道把各个shell工具的输入输出连起来, 生成文件记录表

```bash
wc -c $(FSIMG_FILES) | grep -v 'total$$' | sed -e 's+ $(FSIMG_PATH)+ +' |
  awk -v sum=0 '{print "\x7b\x22" $$2 "\x22\x2c " $$1 "\x2c " sum "\x7d\x2c";sum += $$1}' > src/files.h
```

以十六进制的方式查看磁盘上的内容

```bash
head -c 512 /dev/sda | hd
```

查看CPU的配置信息

```bash
cat /proc/cpuinfo | vim -
```

而

```c
#include "/dev/urandom"
```

则会将urandom中的内容包含到源文件中: 由于urandom是一个长度无穷的字节序列, 提交一个包含上述内容的程序源文件将会令一些检测功能不强的Online Judge平台直接崩溃.

"一切皆文件"的抽象使得我们可以通过标准工具很容易完成一些在Windows下不易完成的工作, 这其实体现了Unix哲学的部分内容: 每个程序采用文本文件作为输入输出, 这样可以使程序之间易于合作. GNU/Linux继承自Unix, 也自然继承了这种优秀的特性. 为了向用户程序提供统一的抽象, Nanos-lite也尝试将IOE抽象成文件.

首先当然是来看输出设备. 串口已经被抽象成`stdout`和`stderr`了, 我们无需担心. 至于VGA, 程序为了更新屏幕, 只需要将像素信息写入VGA的显存即可. 于是, Nanos-lite需要做的, 便是把显存抽象成文件. 显存本身也是一段存储空间, 它以行优先的方式存储了将要在屏幕上显示的像素. Nanos-lite和Navy-apps约定, 把显存抽象成文件`/dev/fb`(fb为frame buffer之意), 它需要支持写操作和lseek, 以便于用户程序把像素更新到屏幕的指定位置上.

除此之外, 用户程序还需要获得屏幕大小的信息, 然后才能决定如何更好地显示像素内容. Nanos-lite和Navy-apps约定, 屏幕大小的信息通过`/proc/dispinfo`文件来获得, 它需要支持读操作. `/proc/dispinfo`内容的一个例子如下:

```
WIDTH:640
HEIGHT:480
```

需要注意的是, `/dev/fb`和`/proc/dispinfo`都是特殊的文件, 文件记录表中有它们的文件名, 但它们的实体并不在ramdisk中. 因此, 我们需要在`fs_read()`和`fs_write()`的实现中对它们进行"重定向", 以`fs_write()`为例:

```c
ssize_t fs_write(int fd const void *buf, size_t len) {
  // ...
  switch (fd) {
    case FD_STDOUT:
    case FD_STDERR:
      // call _putc()
      break;

    case FD_FB:
      // write to frame buffer
      break;

    default:
      // write to ramdisk
      break;
}
```

最后我们来看输入设备. 输入设备有键盘和时钟, 我们需要把它们的输入包装成事件. 一种简单的方式是把事件以文本的形式表现出来, 我们定义以下事件, 一个事件以换行符`\n`结束:

* `t 1234`: 返回系统启动后的时间, 单位为毫秒;
* `kd RETURN` / `ku A`: 按下/松开按键, 按键名称全部大写, 使用AM中定义的按键名

我们采用文本形式来描述事件有两个好处, 首先文本显然是一种字节序列, 这使得事件很容易抽象成文件; 此外文本方式使得用户程序可以容易可读地解析事件的内容. Nanos-lite和Navy-apps约定, 上述事件抽象成文件`/dev/events`, 它需要支持读操作, 用户程序可以从中一次读出一个输入事件. 需要注意的是, 由于时钟事件可以任意时刻进行读取, 我们需要优先处理按键事件, 当不存在按键事件的时候, 才返回时钟事件, 否则用户程序将永远无法读到按键事件.

## 运行仙剑奇侠传

原版的仙剑奇侠传是针对Windows平台开发的, 因此它并不能在GNU/Linux中运行(你知道为什么吗?), 也不能在NEMU中运行. 网友weimingzhi开发了一款基于SDL库, 跨平台的仙剑奇侠传, 工程叫[SDLPAL](https://github.com/CecilHarvey/sdlpal). 你可以通过`git clone`命令把SDLPAL克隆到本地, 然后把仙剑奇侠传的数据文件(我们已经把数据文件上传到提交网站上)放在工程目录下, 执行`make`编译SDLPAL, 编译成功后就可以玩了. 更多的信息请参考SDLPAL工程中的README说明.

我们的框架代码已经把SDLPAL移植到Navy-apps中了. 移植的主要工作就是把应用层之下提供给仙剑奇侠传的所有API重新实现一遍, 因为这些API大多都依赖于操作系统提供的运行时环境, 我们需要根据Navy-apps提供的运行时环境重写它们. 主要包括以下三部分内容:

* C标准库
* 浮点数
* SDL库

Navy-apps中的newlib已经提供了C标准库的功能, 我们无需额外移植. 关于浮点数的移植工作, 我们会在PA5中再来讨论, 目前先忽略它. 为了移植SDL库相关的代码, Navy-apps把时钟, 键盘, 显示的功能封装成NDL(NJU DirectMedia Layer)多媒体库, 其中封装了我们之前实现的`/dev/fb`和`/dev/events`的读写. 为了用NDL的API来替代原来SDL的相应功能, 移植工作需要对SDLPAL进行了少量修改, 包括去掉了声音, 修改了和按键相关的处理, 把我们关心的与NDL相关的功能整理到`hal/hal.c`中, 一些我们不必关心的实现则整理到`unused/`目录下. 框架代码已经把这些移植工作都做好了, 目前你不需要编写额外的代码来进行移植.
