mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2005 字
5 分钟
用户工具:ls、cat、cp 等命令
2021-11-15

Shell 能解析命令了,但还没有多少命令可以执行。本章实现几个基础工具——ls 列出目录、cat 显示文件、cp 复制文件、echo 写入文件——让系统具备基本的文件操作能力。这些工具本质上是文件系统 API 的应用层封装。

重要说明:本章是内核演示程序,不是真正的用户态工具。这些工具作为内核线程实现,直接调用内核文件系统 API,目的是展示工具的核心工作原理,而非实现完整的用户态程序。

用户空间工具概述#

用户空间工具是操作系统中用户与系统交互的重要接口。Linux 系统中的 ls、cat、cp、echo 等命令,本质上是独立的可执行程序,通过系统调用接口访问内核服务。理解这些工具的实现原理,有助于我们:

  1. 掌握文件系统 API 的实际应用
  2. 理解命令行工具的基本工作流程
  3. 区分内核态与用户态程序的差异
  4. 为后续实现真正的用户态工具打下基础

工具实现概览#

工具函数实现#

在真正的操作系统中,ls、cat、cp、echo 等工具是独立的可执行文件,运行在用户态,通过系统调用与内核交互。但在我们的学习阶段,直接在内核中模拟这些工具可以:

  • 避免复杂的用户态程序构建流程
  • 专注于工具的核心逻辑而非系统调用机制
  • 快速演示文件系统 API 的使用方法
  • 为后续章节实现真正的用户态工具做准备

每个工具函数都是一个简单的 C 函数,直接调用内核的文件系统 API(fs_* 函数)完成文件操作:

graph TD A[工具函数] -->|直接调用| B[fs_open] A -->|直接调用| C[fs_read] A -->|直接调用| D[fs_write] A -->|直接调用| E[fs_opendir] A -->|直接调用| F[fs_readdir] B --> G[SimpleFS 文件系统] C --> G D --> G E --> G F --> G G --> H[ATA 硬盘]

这些工具函数通过演示任务(demo_task)被调用,按照预设的流程执行文件操作,展示工具的实际效果。

内存文件系统#

本章使用 SimpleFS,这是一个基于内存的文件系统实现。与基于磁盘的文件系统相比,内存文件系统具有:

  • 更快的读写速度
  • 更简单的实现逻辑
  • 无需处理磁盘 I/O 的复杂性
  • 适合演示和学习环境

SimpleFS 在内存中维护文件和目录的元数据,所有文件操作都在内存中完成。这使得工具演示更加快速和稳定,也避免了磁盘驱动可能引入的复杂性。

代码实现#

文件结构#

18.userspace-tools/
├── boot/
│ ├── mbr.S
│ └── loader.S
├── kernel/
│ └── kernel.c # 工具演示函数
└── Makefile

注意:与真正用户态工具不同,本章没有 bin/ 或 libc/ 目录,所有工具代码都直接在内核源文件中。

// 文件系统 API 使用的数据结构
typedef struct {
char name[256];
uint8_t type; // FILE_TYPE_DIRECTORY 或 FILE_TYPE_FILE
// ... 其他字段
} dirent_t;
flowchart TD A[启动内核] --> B[初始化文件系统] B --> C[创建演示任务] C --> D[创建目录结构] D --> E[使用 echo 写入文件] E --> F[使用 ls 列出目录] F --> G[使用 cat 显示文件] G --> H[使用 cp 复制文件] H --> I[验证复制结果] I --> J[任务完成]

tool_ls - 列出目录内容#

static void tool_ls(const char *path)
{
int dir = fs_opendir(path ? path : "/");
if (dir < 0) {
vga_printf("ls: cannot open '%s'\n", path);
return;
}
dirent_t *de;
while ((de = fs_readdir(dir)) != NULL) {
vga_printf("%-4s %s\n",
de->type == FILE_TYPE_DIRECTORY ? "dir" : "file",
de->name);
}
fs_closedir(dir);
}

解析

  • 使用 fs_opendir 打开目录,失败时输出错误信息
  • 通过 fs_readdir 逐个读取目录项,循环直到返回 NULL
  • 对每个目录项,根据类型显示 “dir” 或 “file”,并输出文件名
  • 最后使用 fs_closedir 关闭目录
  • 默认路径为 ”/“,如果传入参数为 NULL

tool_cat - 显示文件内容#

static void tool_cat(const char *path)
{
int fd = fs_open(path, FS_OPEN_READ);
if (fd < 0) {
vga_printf("cat: %s: not found\n", path);
return;
}
char buf[128];
ssize_t n;
while ((n = fs_read(fd, buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
vga_printf("%s", buf);
}
fs_close(fd);
}

解析

  • 使用 fs_open 以只读模式打开文件
  • 创建 128 字节的缓冲区,逐块读取文件内容
  • 每次读取后在缓冲区末尾添加 ‘\0’,确保字符串正确终止
  • 使用 vga_printf 输出文件内容
  • 最后关闭文件描述符

tool_cp - 复制文件#

static void tool_cp(const char *src, const char *dst)
{
int sfd = fs_open(src, FS_OPEN_READ);
if (sfd < 0) {
vga_printf("cp: %s: not found\n", src);
return;
}
int dfd = fs_open(dst, FS_OPEN_WRITE | FS_OPEN_CREATE);
if (dfd < 0) {
fs_close(sfd);
vga_printf("cp: cannot create %s\n", dst);
return;
}
char buf[256];
ssize_t n;
while ((n = fs_read(sfd, buf, sizeof(buf))) > 0) {
fs_write(dfd, buf, n);
}
fs_close(sfd);
fs_close(dfd);
vga_printf("cp: %s -> %s\n", src, dst);
}

解析

  • 打开源文件(只读)和目标文件(写入+创建)
  • 使用 256 字节缓冲区进行块拷贝
  • 逐块读取源文件内容,写入目标文件
  • 注意错误处理:打开目标文件失败时需要关闭源文件
  • 最后关闭两个文件描述符并输出复制成功信息

tool_echo_to_file - 写入文件#

static void tool_echo_to_file(const char *text, const char *path)
{
int fd = fs_open(path, FS_OPEN_WRITE | FS_OPEN_CREATE);
if (fd < 0) {
vga_printf("echo: cannot open %s\n", path);
return;
}
fs_write(fd, text, strlen(text));
fs_write(fd, "\n", 1);
fs_close(fd);
}

解析

  • 以写入模式打开文件(如果不存在则创建)
  • 使用 strlen 计算文本长度
  • 先写入文本内容,再写入换行符
  • 简洁的实现,适合演示文件写入操作

演示任务 - 整合所有工具#

static void demo_task(void *arg)
{
vga_printf("\n--- Userspace Tools Demo ---\n\n");
/* 创建目录结构 */
vga_printf("== mkdir ==\n");
fs_mkdir("/home", FS_PERM_READ | FS_PERM_WRITE);
fs_mkdir("/home/user", FS_PERM_READ | FS_PERM_WRITE);
fs_mkdir("/tmp", FS_PERM_READ | FS_PERM_WRITE);
vga_printf("Created /home, /home/user, /tmp\n\n");
/* echo > file */
vga_printf("== echo > file ==\n");
tool_echo_to_file("Hello from MyOS", "/home/user/hello.txt");
tool_echo_to_file("System configuration", "/home/user/config.txt");
tool_echo_to_file("Temporary data", "/tmp/temp.txt");
vga_printf("Created 3 files\n\n");
/* ls */
vga_printf("== ls / ==\n");
tool_ls("/");
vga_printf("\n== ls /home/user ==\n");
tool_ls("/home/user");
/* cat */
vga_printf("\n== cat /home/user/hello.txt ==\n");
tool_cat("/home/user/hello.txt");
/* cp */
vga_printf("\n== cp ==\n");
tool_cp("/home/user/hello.txt", "/tmp/hello_copy.txt");
/* verify copy */
vga_printf("\n== cat /tmp/hello_copy.txt ==\n");
tool_cat("/tmp/hello_copy.txt");
/* ls to verify */
vga_printf("\n== ls /tmp ==\n");
tool_ls("/tmp");
vga_printf("\n=== All Userspace Tools Demo Complete ===\n");
while (1) schedule_yield();
}

解析

  • 演示任务按照逻辑顺序执行所有工具
  • 先创建目录结构(/home, /home/user, /tmp)
  • 使用 echo 创建三个测试文件
  • 使用 ls 列出根目录和用户目录
  • 使用 cat 显示文件内容
  • 使用 cp 复制文件
  • 验证复制结果(cat 和 ls)
  • 最后进入无限循环,定期让出 CPU

与真正用户态工具的区别#

特性本章实现真正的用户态工具
运行环境内核态(Ring 0)用户态(Ring 3)
调用方式直接调用 fs_* API通过系统调用(syscall)
文件组织内核代码一部分bin/ 目录下的独立可执行文件
库支持直接使用内核函数使用 libc 标准库
参数解析函数参数(const char*)argc/argv 命令行参数
输出方式vga_printfprintf/write 到标准输出
权限检查跳过(内核态)内核在系统调用中检查
错误处理简单的错误输出更完善的错误处理和退出码

为什么这样设计?#

本章的设计是有意为之,目的是:

  1. 降低复杂度:避免涉及用户态程序构建、系统调用、内存保护等复杂概念
  2. 聚焦核心逻辑:专注于文件操作的基本流程,而非系统调用的实现细节
  3. 教学目的:让读者先理解工具的工作原理,再逐步过渡到完整的用户态实现
  4. 渐进式学习:为后续章节实现真正的用户态工具和系统调用打好基础

运行与验证#

编译运行#

cd 18.userspace-tools
make clean && make all && make run

预期输出#

运行后,你应该看到以下输出:

=== Chapter 18: Userspace Tools ===
Starting scheduler...
--- Userspace Tools Demo ---
== mkdir ==
Created /home, /home/user, /tmp
== echo > file ==
Created 3 files
== ls / ==
dir home
dir tmp
== ls /home/user ==
file hello.txt
file config.txt
== cat /home/user/hello.txt ==
Hello from MyOS
== cp ==
cp: /home/user/hello.txt -> /tmp/hello_copy.txt
== cat /tmp/hello_copy.txt ==
Hello from MyOS
== ls /tmp ==
file temp.txt
file hello_copy.txt
=== All Userspace Tools Demo Complete ===

踩坑记录#

  1. 为什么不用系统调用?

    • 系统调用需要用户态和内核态的完整切换机制,涉及 TSS、特权级转移等复杂概念。本章专注于文件操作的基本流程,将系统调用的实现放在下一章。
  2. 为什么工具函数是 static 的?

    • 这些函数仅供演示任务内部使用,不需要外部链接,使用 static 可以避免命名冲突。
  3. 如何扩展更多工具?

    • 可以参考现有工具的实现模式,添加新的函数(如 tool_rm、tool_mv 等),在 demo_task 中调用即可。
  4. 缓冲区大小为什么是 128/256?

    • 这些是演示用的固定大小缓冲区。在实际系统中,应该使用更大的缓冲区(如 4KB 或 8KB)来提高 I/O 效率。
  5. 错误处理为什么这么简单?

    • 演示代码为了简洁,只输出错误信息而不进行复杂的错误恢复。真正的用户态工具需要更完善的错误处理和退出码机制。

小结#

ls、cat、cp、echo 四个工具围绕文件系统 API 构建,核心流程是打开→读写→关闭。这些工具运行在内核态,核心逻辑与真正的用户态工具相似,但缺少权限检查和并发安全。下一章将把所有组件集成为一个可启动的完整系统——从散落的模块到统一的启动镜像。 5. 本章是内核演示,真正的用户态工具需要系统调用支持

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

用户工具:ls、cat、cp 等命令
https://blog.souloss.com/posts/os/user-tools-ls-cat-cp/
作者
Souloss
发布于
2021-11-15
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时