TobudOS 中的 MicroPython os 模块实战指南:文件系统访问、VFS 挂载与块设备接口
TobudOS 中的 MicroPython os 模块实战指南:文件系统访问、VFS 挂载与块设备接口
导读
os 模块是 MicroPython 提供"基本操作系统服务"的核心模块,涵盖文件系统访问与挂载、终端(REPL)重定向与复制,以及 uname、urandom 等系统级函数。本文以 TobudOS 仓库中 os.rst 文档为骨架,结合 moduos.c、vfs.c、vfs_tos.c 等源码与 os.py 示例脚本,完整讲解每个 API 的签名、参数含义、返回结构以及底层实现原理。读完本文,你将能够在 TobudOS 的 MicroPython 运行时中熟练完成目录遍历、文件增删改查、文件系统挂载/卸载,并理解如何为自定义存储硬件实现块设备驱动。
一、os 模块功能总览
在 MicroPython 中,os 模块(模块内部名为 uos,通过弱链接暴露为 os)主要提供三大类能力:
- 通用函数:
uname()获取系统信息、urandom(n)获取随机字节; - 文件系统访问:
chdir、getcwd、ilistdir、listdir、mkdir、remove、rmdir、rename、stat、statvfs、sync等; - 文件系统挂载(VFS):
mount、umount,以及VfsFat、VfsLfs1、VfsLfs2三类文件系统类。
从源码层面看,这些函数的注册表定义在 extmod/moduos.c 的 os_module_globals_table 中。值得注意,表中还出现了 unlink(它是 remove 的别名,{ MP_ROM_QSTR(MP_QSTR_unlink), MP_ROM_PTR(&mp_vfs_remove_obj) }),以及 MicroPython 扩展的 dupterm、errno 等条目。
在 TobudOS 的 port/mpconfigport.h 中,该模块的启用条件与 VFS 特性开关如下:
#ifdef MP_USING_VFS
#define MICROPY_PY_IO (1)
#define MICROPY_PY_UOS (1)
#define MICROPY_READER_VFS (1)
#define MICROPY_VFS (1)
#define MICROPY_VFS_TOS (1) // 使用 TobudOS 原生 VFS 层
#define MICROPY_VFS_POSIX (0)
#define MICROPY_VFS_FAT (0) // FAT 由 TobudOS VFS 桥接,而非 MicroPython 自带实现
#else
#define MICROPY_PY_IO (0)
#define MICROPY_READER_VFS (0)
#endif
即在 TobudOS 移植中,os 模块与 VFS 功能默认随 MP_USING_VFS 宏一同开启,并且文件系统能力是桥接到 TobudOS 内核自带的 VFS(MICROPY_VFS_TOS)之上,这一点是理解后文所有源码示例的关键前提。
二、通用函数:uname 与 urandom
2.1 uname() —— 获取系统信息
uname() 返回一个包含底层机器和/或操作系统信息的元组(在支持具名元组的平台上是一个具名元组)。元组共 5 个字段,全部为字符串,顺序固定:
| 字段 | 含义 |
|---|---|
sysname |
底层系统名称 |
nodename |
网络名称(通常与 sysname 相同) |
release |
底层系统版本 |
version |
MicroPython 版本与构建日期 |
machine |
底层硬件标识(如板卡、CPU) |
在 moduos.c 中可以看到该元组的真实构造方式:它通过 MP_DEFINE_ATTRTUPLE 定义了一个包含 sysname/nodename/release/version/machine 五个字段的具名元组,其中:
sysname、nodename取MICROPY_PY_SYS_PLATFORM宏;release取MICROPY_VERSION_STRING;version取MICROPY_GIT_TAG " on " MICROPY_BUILD_DATE组合;machine取MICROPY_HW_BOARD_NAME " with " MICROPY_HW_MCU_NAME组合。
因此在 TobudOS 的 MicroPython REPL 中执行:
import os
print(os.uname())
输出的 machine 字段会形如"某板卡 with 某 MCU",version 会带上 git 标签和构建日期。uname() 由 MICROPY_PY_UOS_UNAME 宏控制,TobudOS 的 mpconfigport.h 中已默认置 1 启用。
2.2 urandom(n) —— 获取随机字节
urandom(n) 返回一个包含 n 个随机字节 的 bytes 对象。只要可能,它由硬件随机数发生器(HRNG)生成;在没有硬件 RNG 的平台上,则由端口实现退化为其他随机源。
需要说明的是:该函数对象 mp_uos_urandom_obj 在 moduos.c 的全局表中被注册,但具体实现由各端口通过 MICROPY_PY_UOS_INCLUDEFILE 机制或直接提供,因此是否真正基于硬件 RNG 取决于目标板卡。在 TobudOS 中,若板卡具备硬件随机数能力,即可用它为会话令牌、密钥盐值等提供随机数据:
import os
token = os.urandom(16) # 生成 16 字节随机数
三、文件系统访问 API 详解
本节 API 在 vfs.c 中实现(如 mp_vfs_chdir、mp_vfs_ilistdir、mp_vfs_mkdir 等),通过 mp_vfs_lookup_path() 先做路径解析,再把调用代理到对应挂载点上的 VFS 对象方法(mp_vfs_proxy_call)。在 TobudOS 中,这个"VFS 对象"即 vfs_tos.c 里的 VfsTos,其方法最终调用 TobudOS 内核 VFS 层的 tos_vfs_* 系列接口。
3.1 目录操作:chdir / getcwd / mkdir / rmdir
| 函数 | 作用 | 参数 |
|---|---|---|
chdir(path) |
切换当前目录 | path:目标目录路径 |
getcwd() |
获取当前目录 | 无参数,返回字符串 |
mkdir(path) |
创建新目录 | path:要创建的目录路径 |
rmdir(path) |
删除目录 | path:要删除的目录路径 |
TobudOS 特定行为说明(依据 vfs_tos.c):
chdir在 vfs_tos 中当前未实现(源码注释FIXME chdir() is not supported by tos),调用会抛出OSError(EPERM);getcwd当前固定返回根路径字符串"/";mkdir实际调用tos_vfs_mkdir(path),失败时通过vfs_err_to_errno_table错误码映射表转换为对应OSError抛出;rmdir实际调用tos_vfs_rmdir(path)(源码注释提示底层 fatfs 对 rmdir 的支持有限)。
3.2 目录列举:ilistdir / listdir
ilistdir([dir]) 返回一个迭代器,逐项产生对应目录条目的元组。无参数时列举当前目录,否则列举 dir 指定的目录。元组格式为 (name, type, inode[, size]):
name:条目名称。若dir是bytes对象,则返回bytes,否则返回str;type:条目类型整数,0x4000表示目录,0x8000表示普通文件;inode:文件 inode 号,对没有该概念的文件系统可能为 0;- 某些平台返回 4 元组并附带
size(文件大小,未知为 -1;对目录条目含义未定义)。
在 TobudOS 的 vfs_tos.c 中,迭代器通过 tos_vfs_readdir 逐条读取目录项,跳过 . 和 ..,构造 3 元组 (name, type, inode):类型映射为 MP_S_IFDIR(即 0x4000)或 MP_S_IFREG(即 0x8000),inode 因 TobudOS VFS 不支持而固定为 0。
listdir(<a href="https://link.gitcode.com/i/33085e46bbe53c60b8b548f6234023a6" target="_blank">dir]) 则是 ilistdir 的便捷封装:在 [vfs.c 中,mp_vfs_listdir 内部先调用 ilistdir 拿到迭代器,再循环取出每个元组的第 0 项(名称)组装成一个 list 返回。
3.3 文件操作:remove / rename
| 函数 | 作用 | 参数 |
|---|---|---|
remove(path) |
删除文件 | path:文件路径(unlink 为其别名) |
rename(old_path, new_path) |
重命名文件 | 旧路径、新路径 |
源码层面:
- vfs.c 的
mp_vfs_rename会分别解析新旧路径,若新旧路径落在不同文件系统(挂载点)上则直接抛OSError(EPERM),禁止跨文件系统重命名; - vfs_tos.c 中
remove实际调用tos_vfs_unlink(path),rename调用tos_vfs_rename(old, new),失败时均转换为对应 errno 的OSError。
3.4 状态查询:stat / statvfs
stat(path) 获取文件或目录的状态。在 TobudOS 的 vfs_tos.c 中,它调用 tos_vfs_stat(path, &st) 得到一个 10 元组,字段与 POSIX stat 语义对应:
| 索引 | 字段 | 说明(TobudOS 取值) |
|---|---|---|
| 0 | st_mode |
文件类型/权限位(来自 vfs_fstat_t.mode) |
| 1 | st_ino |
固定 0(无 inode 概念) |
| 2 | st_dev |
固定 0 |
| 3 | st_nlink |
固定 0 |
| 4 | st_uid |
固定 0 |
| 5 | st_gid |
固定 0 |
| 6 | st_size |
文件大小(字节) |
| 7 | st_atime |
最后访问时间 |
| 8 | st_mtime |
最后修改时间 |
| 9 | st_ctime |
状态变更时间 |
statvfs(path) 获取文件系统状态,返回 10 元组,字段顺序及含义如下:
| 字段 | 含义 |
|---|---|
f_bsize |
文件系统块大小 |
f_frsize |
分片大小 |
f_blocks |
以 f_frsize 为单位的总块数 |
f_bfree |
空闲块数 |
f_bavail |
非特权用户可用空闲块数 |
f_files |
inode 总数 |
f_ffree |
空闲 inode 数 |
f_favail |
非特权用户可用空闲 inode 数 |
f_flag |
挂载标志 |
f_namemax |
最大文件名长度 |
其中 f_files、f_ffree、f_favail、f_flag 在端口实现可能不可用时返回 0。TobudOS 的 vfs_tos.c 中 statvfs 调用 tos_vfs_statfs(path, &st),将 TobudOS 内核 VFS 的 vfs_fsstat_t(定义见 components/fs/vfs/include/tos_vfs_file.h:blk_size/blk_num/blk_free/blk_avail/file_num/file_free/name_len 等)逐项映射到上述 10 元组,f_flag 固定为 0。
利用 statvfs 可以计算存储剩余空间,例如:
import os
info = os.statvfs('/fs')
total = info[2] * info[0] # 总字节 = f_blocks * f_bsize
free = info[3] * info[0] # 可用字节 = f_bfree * f_bsize
print('total: %d, free: %d' % (total, free))
3.5 sync() —— 同步文件系统
sync() 用于同步所有文件系统,确保缓存数据落盘。在 moduos.c 中,sync 受 MICROPY_PY_UOS_SYNC 宏控制,TobudOS 默认构建中该宏未在 mpconfigport.h 中显式置 1,因此该函数是否可用取决于具体构建配置;在使用 fatfs/littlefs 等带缓存的底层文件系统时,写文件后调用 sync() 是防止掉电丢失数据的常见做法。
3.6 完整实操示例:来自仓库的 os.py
TobudOS 仓库自带的 examples/micropython_demo/scripts/os.py 完整演示了上述 API 的组合用法,可直接在设备上运行:
import uos as os
# test mkdir()
print('{:-^40}'.format('test mkdir'))
print(os.listdir())
os.mkdir('test_dir')
print(os.listdir())
# test file open() and write()
print('{:-^40}'.format('test write'))
f = open('test_dir/test_file', 'w')
f.write("Hello TencentOS-tiny\n")
print("Hello MicroPython", file=f)
f.close()
print(os.listdir('test_dir'))
# test file open() and read()
print('{:-^40}'.format('test read'))
f = open('test_dir/test_file', 'r')
print(f.readlines())
f.close()
# test 'with' statement and iterator
print('{:-^40}'.format('test with statement'))
with open('test_dir/test_file', 'r') as file:
for line in file:
print(line)
# test rename()
print('{:-^40}'.format('test rename'))
os.rename('test_dir', 'test_dir2')
print(os.listdir())
print(os.listdir('test_dir2'))
# test unlink()
print('{:-^40}'.format('test unlink'))
os.unlink('test_dir2/test_file')
print(os.listdir('test_dir2'))
os.unlink('test_dir2')
print(os.listdir())
该脚本覆盖了 listdir(无参列出当前目录)、mkdir、文件 open/write/read、with 语句迭代、rename(此处直接重命名目录)、unlink(即 remove 别名,既删文件也删目录)。它演示了一个重要事实:TobudOS 的 MicroPython 文件 I/O 与 os 模块是打通的——open() 通过 vfs.c 的 mp_vfs_open 解析路径后,代理到 VfsTos.open(),最终落到 TobudOS VFS 的文件接口上。
四、终端重定向与复制:dupterm
dupterm(stream_object, index=0, /) 用于在给定的流式(stream-like)对象上复制或切换 MicroPython 终端(REPL):
stream_object必须是原生流对象,或继承自io.IOBase并实现readinto()与write()方法;流应处于非阻塞模式,readinto()在无数据可读时应返回None;- 调用之后,所有终端输出都会在该流上重复输出,流上可用的输入也会被转发到终端输入;
index为非负整数,指定设置的复制槽位;同一端口可能支持多个槽位(槽 0 始终可用),此时终端输入输出会复制到所有已设置的槽位;- 传入
None作为stream_object则取消指定槽位的复制; - 函数返回该槽位上先前的流对象。
例如把 REPL 复制到一个 UART 流上:
import os, machine
uart = machine.UART(1, 115200)
old = os.dupterm(uart) # 复制终端到 uart,old 保存原槽位对象
# 之后 uart 上会镜像 REPL 的输入输出
os.dupterm(None) # 取消复制
在 TobudOS 的 mpconfigport.h 中,MICROPY_PY_OS_DUPTERM 与 MICROPY_PY_UOS_DUPTERM_NOTIFY 均默认为 0,即默认构建未启用 dupterm;需要该特性时需在移植配置中打开对应宏。其通用实现位于 extmod/uos_dupterm.c,负责多槽位的轮询(mp_uos_dupterm_poll)、接收(mp_uos_dupterm_rx_chr)与发送(mp_uos_dupterm_tx_strn)。
五、文件系统挂载:mount / umount 与 VFS 类
5.1 VFS 概念
某些端口提供虚拟文件系统(VFS),并支持在此 VFS 内挂载多个"真实"文件系统。文件系统对象可以挂在 VFS 根目录,也可以挂在根目录下的某个子目录,从而让 Python 程序看到动态、灵活组合的文件系统视图。提供该功能的端口会暴露 mount、umount 函数以及若干以 VFS 类表示的文件系统实现。
在 vfs.c 的 mp_vfs_lookup_path 中可以看到核心机制:MicroPython 维护一张挂载表(vfs_mount_table),路径解析时从根开始逐段匹配挂载点前缀,相对路径则直接委托给"当前卷"(vfs_cur)。所有挂载点被合并成一个从 / 开始的单一虚拟文件系统。
5.2 mount(fsobj, mount_point, *, readonly)
将文件系统对象 fsobj 挂载到 VFS 中由 mount_point 字符串指定的位置:
fsobj可以是带有mount()方法的 VFS 对象,也可以是块设备对象。若为块设备,则文件系统类型会被自动检测(无法识别时抛出异常);mount_point可以是'/'(挂到根),也可以是'/<name>'(挂到根下子目录);readonly=True时以只读方式挂载;- 挂载过程中会调用文件系统对象的
mount()方法; - 若
mount_point已被占用,抛出OSError(EPERM)。
源码细节(vfs.c 的 mp_vfs_mount):
- 除
readonly外还支持mkfs关键字参数(挂载时若文件系统为空则自动格式化); - 若
fsobj没有mount方法,则视为块设备,调用mp_vfs_autodetect自动识别文件系统。识别逻辑(vfs.c)会读取块设备第 0、1 两个块:在偏移 32 处发现"littlefs"魔数判为 LFS1,在偏移 0 处发现则判为 LFS2;都未命中时回退到 FAT; - 挂载完成后将新挂载点插入挂载表,根挂载(
len == 1)始终保持在表尾。
5.3 umount(mount_point)
卸载一个文件系统:
mount_point可以是命名挂载位置的字符串,也可以是先前挂载的文件系统对象;- 卸载过程中会调用文件系统对象的
umount()方法; - 若
mount_point找不到,抛出OSError(EINVAL)。
5.4 三类文件系统类
VfsFat(block_dev):基于 FAT 格式的文件系统对象,存储由 block_dev 提供。静态方法 VfsFat.mkfs(block_dev) 在块设备上构建 FAT 文件系统(对应实现见 extmod/vfs_fat.c,内部调用 FatFs 的 f_mkfs)。FAT 的主要优势是可在支持 USB MSC 的板卡上免驱动被 PC 直接访问,但断电写入时容错较差,易损坏。
VfsLfs1(block_dev, readsize=32, progsize=32, lookahead=32):littlefs v1 格式的文件系统对象。block_dev 必须支持扩展接口。静态方法 VfsLfs1.mkfs(...) 构建 LFS1 文件系统。
VfsLfs2(block_dev, readsize=32, progsize=32, lookahead=32, mtime=True):littlefs v2 格式的文件系统对象,参数含义同上,另加 mtime 参数:
mtime=True时启用文件修改时间戳,通过 littlefs 属性存储;- 该选项可每次挂载时单独开启或关闭,时间戳只会在启用
mtime时被添加或更新,否则保持不变; - 没有时间戳的 LFS2 文件系统无需重新格式化即可使用;一旦文件被打开写入,时间戳会被透明地添加;
mtime启用时,对无时间戳文件执行os.stat会返回 0 作为时间戳值。
littlefs 是为闪存设备设计的文件系统,抗损坏能力远强于 FAT,是无需 USB MSC 场景下的推荐选择。上述类由 extmod/vfs_lfsx.c 实现,其构造函数将 readsize/progsize/lookahead/mtime 参数映射到底层 littlefs 配置(read_size、prog_size、lookahead 缓冲区按 lookahead/8 字节分配)。
5.5 挂载实操:RAM 块设备示例
综合使用 VfsFat.mkfs 与 mount 挂载一个基于 RAM 的磁盘(本示例与 docs/reference/filesystem.rst 中的规范用法一致):
import os
class RAMBlockDev:
def __init__(self, block_size, num_blocks):
self.block_size = block_size
self.data = bytearray(block_size * num_blocks)
def readblocks(self, block_num, buf):
for i in range(len(buf)):
buf[i] = self.data[block_num * self.block_size + i]
def writeblocks(self, block_num, buf):
for i in range(len(buf)):
self.data[block_num * self.block_size + i] = buf[i]
def ioctl(self, op, arg):
if op == 4: # get number of blocks
return len(self.data) // self.block_size
if op == 5: # get block size
return self.block_size
bdev = RAMBlockDev(512, 50)
os.VfsFat.mkfs(bdev) # 格式化
os.mount(bdev, '/ramdisk') # 挂载到 /ramdisk
# 挂载后即可正常读写
with open('/ramdisk/hello.txt', 'w') as f:
f.write('Hello world')
print(open('/ramdisk/hello.txt').read())
5.6 TobudOS 中的真实挂载流程
在 TobudOS 的嵌入式移植中,VFS 挂载通常先在 C 层完成。以 examples/micropython_demo/micropython_demo.c 的 mp_entry 为例:
#ifdef MP_USING_VFS
extern vfs_blkdev_ops_t sd_dev;
extern vfs_fs_ops_t fatfs_ops;
tos_vfs_block_device_register("/dev/sd", &sd_dev); // 注册块设备
tos_vfs_fs_register("fatfs_sd", &fatfs_ops); // 注册文件系统驱动
if (tos_vfs_fs_mount("/dev/sd", "/fs", "fatfs_sd") != 0) { // 挂载到 /fs
printf("mount failed!\n");
}
#endif
C 层把 SD 卡挂载到 TobudOS VFS 的 /fs 之后,Python 脚本里就能直接 os.listdir('/fs')、open('/fs/xxx', 'w') 等。这正是前文所述 MICROPY_VFS_TOS 桥接层的价值:Python 层的 os 文件操作通过 VfsTos 代理到 TobudOS 内核 VFS 的 tos_vfs_* 接口,从而复用同一套块设备与文件系统驱动栈。
六、块设备接口:AbstractBlockDev
块设备(block device)是实现块协议的对象,它让硬件设备能够承载 MicroPython 文件系统。物理硬件由用户自定义类表示;MicroPython 并不真正提供 AbstractBlockDev 类,它只是一个设计模板——真正的块设备类必须实现下述方法。一个具体的块设备实现通常允许访问某块硬件(如闪存)的类内存功能,可以被格式化为任何受支持的文件系统,并用 os 的方法挂载。
6.1 简单接口与扩展接口
readblocks 与 writeblocks 有两种兼容的签名,以支持多种使用场景;一个块设备可以实现其中一种、另一种,或两者同时实现。带 offset 参数的第二种形式称为扩展接口:
readblocks(block_num, buf)
readblocks(block_num, buf, offset)
writeblocks(block_num, buf)
writeblocks(block_num, buf, offset)
- 简单形式:读取/写入对齐的、块大小的整数倍数据。从
block_num索引的块开始,把buf(字节数组)读入/写出;buf长度是块大小的整数倍。写入简单形式要求目标块先被本方法擦除(如必要); - 扩展形式:允许在块内任意偏移、任意长度读写。从块索引
block_num和块内字节偏移offset开始读写buf长度的字节;写入时只应改变被写的字节,调用方必须保证相关块已通过先前的ioctl调用擦除; - 注意:实现方在指定了
offset参数时,绝不能隐式擦除块,即使 offset 为 0 也一样。
某些文件系统(如 littlefs)需要更强的写控制——例如不擦除就写子块区域——因此要求块设备支持扩展接口。这也是 VfsLfs1/VfsLfs2 构造函数要求 block_dev 支持扩展接口的原因。
6.2 ioctl 操作码
ioctl(op, arg) 用于控制块设备并查询其参数,op 为下列整数之一:
| op | 操作 | arg | 返回 |
|---|---|---|---|
| 1 | 初始化设备 | 未使用 | — |
| 2 | 关闭设备 | 未使用 | — |
| 3 | 同步设备 | 未使用 | — |
| 4 | 获取块数量 | 未使用 | 返回整数(块数) |
| 5 | 获取块大小(字节) | 未使用 | 返回整数,或 None(此时使用默认值 512) |
| 6 | 擦除一个块 | 块号 | — |
约束要点:
- 最低要求必须拦截
ioctl(4, ...);对 littlefs 还必须拦截ioctl(6, ...),其余是否实现取决于硬件; - 在每次
writeblocks(block, ...)之前,littlefs 都会先发出ioctl(6, block),以便驱动在硬件需要时先擦除块;驱动也可以拦截ioctl(6, block)并返回 0(成功),此时由驱动自行负责检测擦除需求; - 除非另有说明,
ioctl(op, arg)可以返回None,因此实现可以忽略未使用的op值;对拦截的操作,操作 4 和 5 的返回值如上所述,其余操作成功返回 0,失败返回非零(值为OSErrorerrno 码)。
6.3 同时支持两种接口的块设备模板
同时支持简单与扩展接口的块设备模板(RAM 存储,可配合 littlefs 使用):
class RAMBlockDev:
def __init__(self, block_size, num_blocks):
self.block_size = block_size
self.data = bytearray(block_size * num_blocks)
def readblocks(self, block_num, buf, offset=0):
addr = block_num * self.block_size + offset
for i in range(len(buf)):
buf[i] = self.data[addr + i]
def writeblocks(self, block_num, buf, offset=None):
if offset is None:
# 简单接口:先擦除再写
for i in range(len(buf) // self.block_size):
self.ioctl(6, block_num + i)
offset = 0
addr = block_num * self.block_size + offset
for i in range(len(buf)):
self.data[addr + i] = buf[i]
def ioctl(self, op, arg):
if op == 4: # 块数量
return len(self.data) // self.block_size
if op == 5: # 块大小
return self.block_size
if op == 6: # 擦除块
return 0
import os
bdev = RAMBlockDev(512, 50)
os.VfsLfs2.mkfs(bdev)
os.mount(bdev, '/ramdisk')
在 TobudOS 上为真实闪存芯片实现驱动时,只需按上述协议封装 readblocks/writeblocks/ioctl(块大小通常取 4096 或芯片扇区大小,块数由容量除以块大小得出),即可被 os.mount 自动识别并格式化。
七、结语
os 模块是 TobudOS MicroPython 运行时操作存储与系统的统一入口。本文完整梳理了 os.rst 文档定义的全部 API——从 uname/urandom 到目录与文件操作、dupterm 终端复制、mount/umount 挂载机制,以及 AbstractBlockDev 块设备协议——并逐一对照 moduos.c、vfs.c、vfs_tos.c 等源码,揭示了 Python 层 API 与 TobudOS 内核 VFS 层 tos_vfs_* 接口之间的桥接关系。掌握了这些接口,你就可以在 TobudOS 设备上自由组织文件系统布局、接入自定义存储硬件,并借助 os.py 示例快速验证整套文件操作流程。