跳转至

fs - 文件系统额外操作

作者:王棚嶙

一、概述

fs库是LuatOS的核心文件系统操作模块,提供对设备存储空间的底层访问能力。该库支持对嵌入式设备的Flash存储器,外部存储器(sd卡)进行空间查询,获取文件大小的操作功能。

PS:现fs库功能均可在io库内接口实现,强烈建议后续全部使用io库功能。

io.fsstat(path)替代fs.fsstat(path)

io.exists(path)替代fs.fsize(path)

1.1 存储系统概述

LuatOS的存储系统采用清晰的分层架构设计,其核心目标是为应用层提供统一、简洁的文件操作接口,从而屏蔽底层不同存储介质和文件系统的差异。开发者无需关心数据具体存储在何种硬件上,也无需关心底层文件系统的具体实现,只需调用统一的API即可完成文件操作。

该系统的层次结构与各组件间的协作关系如下图所示,其数据流与依赖关系自上而下贯穿各层:

image.png

1.1.1 物理硬件层

这是数据存储的物理载体,LuatOS支持多种常见的嵌入式存储硬件:

1、SPI/SDIO TF卡:通过高速SPI或SDIO接口连接的块存储设备,特点是容量大、可插拔,适用于存储大量数据(如音频、图片文件);

2、SPI NOR Flash:通过SPI接口访问的NOR型闪存。其特点是读写速度快,通常用于存放固件代码或需要快速读写的关键数据;

3、SPI NAND Flash:通过SPI接口访问的NAND型闪存。其特点是容量大、成本低,适用于存储日志等容量需求较大的数据;

1.1.2 文件系统实现层(fatfs&lf)

文件系统负责管理存储设备的空间,组织文件和目录结构。LuatOS根据硬件特性,适配了两种主流的嵌入式文件系统:

1、FATFS:一个兼容FAT12/FAT16/FAT32标准的通用文件系统。它通常与SD/TF卡配对使用,;

2、LFS(LittleFS):它通常与SPI NOR Flash和SPI NAND Flash配对使用,非常适合在裸Flash设备上运行;

1.1.3 统一API层 (io)

fs库不推荐新客户使用,该库仅对已经使用过的老客户进行保留,其功能已经完全移植到io库中,且更加完善,新客户建议都参考io核心库:

1、fs:提供文件系统的额外操作不建议使用,请使用io库进行替代;

2、io:提供标准的数据流操作接口,用于文件的打开、读写、关闭等,是处理文件内容的主要手段,包含fs库的所有功能,推荐使用;

1.1.4 应用层(luatOS应用脚本)

客户通过调用io库,fatfs库,lf库以及fs库中的api接口,来实现自己的业务逻辑。

1.1.5 推荐使用

你的需求 推荐使用的库和函数 理由
快速检查一个文件的大小 io.exists(path) 最简洁、最直接,一行代码搞定。
快速查看文件系统的整体使用情况(如剩余空间) io.fsstat(path) 提供通用信息,代码兼容性好。
需要对FAT格式的U盘、SD卡进行深度管理 fatfs 库 (如fatfs.getfree(), fatfs.mount()) 只有它才能提供FAT文件系统的专业控制和详细信息。
要读取、写入文件内容,或进行复杂的文件操作 io 库 (如 io.open, file:read, file:write) 这是进行文件I/O操作的标准和核心方式,不可替代。

对于FAT文件系统需求,使用 fatfs

对于文件内容操作需求,使用 io

1.2 关于"块"(block )的概念解释

块(Block)是文件系统管理存储的基本单位,但需要注意:

1.2.1 块的数量是整数

fs.fsstat 返回的"总空间"和"已用空间"值表示的是块的数量这些都是整数。

1.2.2 块的大小也是整数

每个块的大小是固定的,通常是512字节或4096字节等2的幂次方。

1.2.3 为什么需要单独返回块大小

1、不同的文件系统可能使用不同的块大小

2、同一设备上的不同分区可能使用不同的块大小

3、知道块大小才能将块数量转换为实际字节数

1.2.4 计算实际空间

1、总空间(字节) = 总块数 × 块大小

2、已用空间(字节) = 已用块数 × 块大小

3、空闲空间(字节) = (总块数 - 已用块数) × 块大小

1.2.5 为什么会有这样的设计?

这种设计(返回块数和块大小而不是直接返回字节数)有以下几个原因:

1、效率:文件系统内部是以块为单位管理空间的,直接返回块数更高效

2、精确性:避免浮点数运算可能带来的精度问题

3、灵活性:适应不同块大小的文件系统

二、核心示例

1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;

2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;

3、更加完整和详细的 demo,请参考 LuatOS 仓库 中各个产品目录下的 demo/fs_io;

local mount_ok, mount_err = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
if mount_ok then
    log.info("fatfs.mount", "挂载成功", mount_err)
else
    log.error("fatfs.mount", "挂载失败", mount_err)
end

-- ########## 获取SD卡的可用空间信息并打印。 ########## 
local data, err = fatfs.getfree("/sd")
if data then
    -- 打印SD卡的可用空间信息
    log.info("fatfs", "getfree", json.encode(data))
else

-- 执行tfcard文件操作演示
log.info("文件操作", "===== 开始文件操作 =====")

dir_path = "/sd/io_test"

--  创建目录
if io.mkdir(dir_path) then
    log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)


-- 创建并写入文件
local file_path = dir_path .. "/boottime"
local file = io.open(file_path, "wb")
if file then
    file:write("这是io库API文档示例的测试内容")
    file:close()
    -- 在LuatOS文件操作中,执行file:close()是必须且关键的操作,它用于关闭文件句柄,释放资源,并确保数据被正确写入磁盘。
    -- 如果不执行file:close(),可能会导致数据丢失、文件损坏或其他不可预测的问题。
    log.info("文件创建", "文件写入成功", "路径:" .. file_path)
else
    log.error("文件创建", "文件创建失败", "路径:" .. file_path)
end

-- === fs函数测试开始 ===
log.info("=== fs函数测试开始 ===")

-- 测试fs.fsstat
-- 日志打印fs.fsstat成功: 总空间=244264960块 已用=2304块 块大小=512字节 类型=fatfs

log.info("测试fs.fsstat('/sd'):")
local ok, total, used, block_size, fs_type = fs.fsstat("/sd")
if ok then
  log.info("fs.fsstat成功:", 
            "总空间=" .. total .. "块", 
            "已用=" .. used .. "块", 
            "块大小=" .. block_size.."字节",
            "类型=" .. fs_type)
else
    -- 错误信息在total中
    log.info("fs.fsstat失败:", total) 
end

-- 测试fs.fsize

local test_file = "/sd/io_test/test_fs.txt"
log.info("测试fs.fsize('" .. test_file .. "'):")

-- 先创建测试文件
local f = io.open(test_file, "w")
if f then
    f:write("Hello LuatOS!")
    f:close()

    -- 测试fs.fsize 
    local file_size = fs.fsize(test_file)
    if file_size and file_size > 0 then
    -- 日志打印fs.fsize成功: 文件大小=13字节
        log.info("fs.fsize成功:", "文件大小=" .. file_size .. "字节")
    else
        log.info("fs.fsize失败:", "返回值=" .. tostring(file_size))
    end

    -- 清理测试文件
    os.remove(test_file)
else
    log.info("无法创建测试文件")
end
-- === fs函数测试结束 ===

三、常量详解

核心库常量,顾名思义是由合宙 LuatOS 内核固件中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;

fs核心库无常量

四、函数详解

4.1 fs.fsstat(path)

功能

获取文件系统状态信息;

注意事项

1、路径参数可选,默认为根目录"/",表示模组内置Flash中的文件系统分区;

2、目前在传入错误路径的情况下,会自动调整为可查的文件系统分区根目录,强烈建议使用io.fsstat(path);

3、返回多个值表示文件系统状态信息;

参数

path

参数含义:文件系统路径;
数据类型:string
取值范围:有效文件系统路径;
是否必选:否;
注意事项:默认值为根目录"/",表示模组内置Flash中的文件系统分区,若路径无效可能返回失败;
参数示例:"/"

返回值

local success, total_blocks, used_blocks, block_size, fs_type = fs.fsstat(path);

存在五个返回值:success, total_blocks, used_blocks, block_size, fs_type;

success

含义说明:操作是否成功;
数据类型:boolean
取值范围:true  false
注意事项:获取成功返回true,否则返回false
返回示例:true

total_blocks

含义说明:总的block(块)数量;
数据类型:number
取值范围:≥0的整数;
注意事项:文件系统总容量指标,失败时第一个返回值success为false0,每个块的大小都是一致的;
返回示例:244264960

used_blocks

含义说明:已使用的block(块)数量;
数据类型:number
取值范围:≥0的整数;
注意事项:文件系统已使用容量指标,失败时第一个返回值success为false,每个块的大小都是一致的;
返回示例:2304

block_size

含义说明:block(内存块)的大小;
数据类型:number
取值范围:≥0的整数;
注意事项:单位:字节;
返回示例:512

fs_type

含义说明:文件系统类型;
数据类型:string
取值范围:文件系统标识字符串,如"lfs""fatfs"
注意事项:理论上失败时返回空字符串"",目前实际会自动识别能够识别到的文件系统类型;
返回示例:"fatfs"

示例

-- ########## 获取SD卡的可用空间信息并打印。 ########## 
local data, err = fatfs.getfree("/sd")
if data then
    -- 打印SD卡的可用空间信息
    --fatfs getfree {"free_sectors":244263168,"total_kb":122132480,"free_kb":122131584,"total_sectors":244264960}
    log.info("fatfs", "getfree", json.encode(data))

--  创建目录
if io.mkdir(dir_path) then
    log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)
-- 创建并写入文件
local file_path = dir_path .. "/boottime"
local file = io.open(file_path, "wb")
if file then
    file:write("这是io库API文档示例的测试内容")
    file:close()
    -- 在LuatOS文件操作中,执行file:close()是必须且关键的操作,它用于关闭文件句柄,释放资源,并确保数据被正确写入磁盘。
    -- 如果不执行file:close(),可能会导致数据丢失、文件损坏或其他不可预测的问题。
    log.info("文件创建", "文件写入成功", "路径:" .. file_path)
else
-- === fs函数测试开始 ===
log.info("=== fs函数测试开始 ===")

-- 测试fs.fsstat
-- 日志打印fs.fsstat成功: 总空间=244264960块 已用=2304块 块大小=512字节 类型=fatfs

log.info("测试fs.fsstat('/sd'):")
local success, total_blocks, used_blocks, block_size, fs_type = fs.fsstat("/sd")
if ok then
  log.info("fs.fsstat成功:", 
            "总空间=" .. total_blocks .. "块", 
            "已用=" .. used_blocks .. "块", 
            "块大小=" .. used_blocks.."字节",
            "类型=" .. fs_type)
else
    -- 错误信息在total中
    log.info("fs.fsstat失败:", total) 
end
--[[
我使用的是128GB的新的sd卡,我们验证下数据,fatfs.getfree 得到的空间信息是否正确:
total_sectors = 244,264,960 个扇区
每个扇区通常是 512 字节(这是FAT文件系统的标准)
总容量 = 244,264,960 × 512 = 125,063,659,520 字节
转换为KB = 125,063,659,520 / 1024 = 122,132,480 KB (与日志中的 total_kb 一致)
转换为GB = 122,132,480 / 1024 / 1024 ≈ 116.5 GB
128GB TF卡显示为116.5GB是正常的,因为:
1.厂商使用的十进制计算(1GB = 10⁹字节)vs 系统使用的二进制计算(1GB = 2³⁰字节)
2.文件系统本身会占用一些空间
3.可能有隐藏的系统分区

fs.fsstat('/sd') 给出的空间是否正确?
总块数 = 244,264,960
块大小 = 512 字节
总容量 = 244,264,960 × 512 = 125,063,659,520 字节 ≈ 116.47GB
已用块数 = 2,304
已用空间 = 2,304 × 512 = 1,179,648 字节 ≈ 1.15 MB
这与 fatfs.getfree 的结果一致
]]

4.2 fs.fsize(path)

功能

获取文件大小;

注意事项

1、必须提供有效的文件路径;

2、无法判断文件是否存在,如要判断请使用io.exists(path);

参数

path

参数含义:文件系统路径;
数据类型:string
取值范围:有效文件系统路径;
是否必选:是;
注意事项:暂无;
参数示例:"/main.luac"

返回值

local size = fs.fsize(path)

有一个返回值 size

size

含义说明:文件大小;
数据类型:number
取值范围:≥0的整数;
注意事项:获取一个文件的大小时:
         1、首先使用io.exists(path)接口判断下文件是否存在,如果文件不存在,则不需             要继续判断,根据文件不存在的结论,执行自己的业务逻辑即可;如果文件存               在,继续执行第2步判断;
         2、使用fs.fsize(path)获取返回值,返回值为0表示获取失败,返回值大于0表示              获取成功(此时的返回值就是文件大小);
返回示例:13

示例

local test_file = "/sd/io_test/test_fs.txt"
log.info("测试fs.fsize('" .. test_file .. "'):")

-- 先创建测试文件
local f = io.open(test_file, "w")
if f then
    f:write("Hello LuatOS!")
    f:close()

    -- 测试fs.fsize 
    local size = fs.fsize(test_file)
    if size and size > 0 then
    -- 日志打印fs.fsize成功: 文件大小=13字节
        log.info("fs.fsize成功:", "文件大小=" .. size .. "字节")
    else
        log.info("fs.fsize失败:", "返回值=" .. tostring(size))
    end

其他情况示例

-- 测试1: 已存在的空文件
local test_file = "/test_fs.txt"
log.info("测试1 - fs.fsize('" .. test_file .. "') 已存在的空文件:")

local f = io.open(test_file, "w")
if f then
    -- 创建空文件,不写入任何内容
    f:close()  
    log.info("空文件创建成功")

    local size = fs.fsize(test_file)
    if size then
        -- fopen test_fs.txt rb
        -- fs.fsize结果: 文件大小=0字节
        log.info("fs.fsize结果:", "文件大小=" .. tostring(size) .. "字节")

-- 测试2: 不存在的路径文件
local non_exist_file = "/non_exist_file.txt"
log.info("测试2 - fs.fsize('" .. non_exist_file .. "') 不存在的文件:")

local size = fs.fsize(non_exist_file)
if size then
    -- fopen non_exist_file.txt rb
    -- fopen /non_exist_file.txt rb not found
    -- fs.fsize结果: 文件大小=0字节
    log.info("fs.fsize结果:", "文件大小=" .. tostring(size) .. "字节")

-- 测试3: 空路径
log.info("测试3 - fs.fsize('') 空路径:")
local size = fs.fsize("")
if size then
    -- fopen  rb
    -- fopen  rb not found
    -- fs.fsize结果: 文件大小=0字节
    log.info("fs.fsize结果:", "文件大小=" .. tostring(size) .. "字节")

五、产品支持说明

支持 LuatOS 开发的所有产品都支持 fs 核心库,但是不建议使用,请使用io核心库接口实现业务逻辑。