3. 鸿蒙内核文件系统接口:open/read/write/close/ioctl 实战解析

好,咱们进入正题。这一章我打算聊聊 OpenHarmony 内核的文件操作 API。说白了,就是 open、read、write、close、ioctl 这些你天天打交道的函数。

很多从 Linux 转过来的朋友,第一反应是:“这不就是 POSIX 那套吗?” 嗯,对,也不全对。我在做第一个鸿蒙移植项目时,就踩过这个坑——想当然地以为完全一样,结果在 ioctl 上卡了两天。今天我把这些异同点掰开揉碎了讲给你听。

核心观点:鸿蒙的 VFS 层借鉴了 Linux 的设计思想,但实现上做了大量精简和重构。它更轻量,更适合资源受限的 IoT 设备。

3.1 文件操作 API 全景

先看一张图,这是我画的内核文件操作调用链。你注意看,用户态和内核态之间,其实隔了一层 VFS 抽象。

用户态 应用程序调用 open() / read() / write() / close() / ioctl() 系统调用层 (Syscall) SysFileOpen / SysFileRead / SysFileWrite / SysFileClose / SysIoctl VFS 虚拟文件系统层 VfsOpen / VfsRead / VfsWrite / VfsClose / VfsIoctl 具体文件系统 (FAT / JFFS2 / NFS / 自定义FS)

这张图你看懂了吗?从上到下,用户态调用经过系统调用,再到 VFS 层,最后落到具体的文件系统实现。我当初画这张图时,特意把 VFS 层标绿——因为它是整个文件系统的“交通枢纽”。

3.2 open():打开文件的那些门道

先看 open。鸿蒙的 open 原型长这样:

int open(const char *path, int flags, ... mode_t mode);

嗯,跟 Linux 一模一样,对吧?但别急,我带你看看细节。

flags 参数支持情况:

标志位 Linux OpenHarmony 备注
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
O_CREAT 文件不存在则创建
O_EXCL 与 O_CREAT 配合使用
O_TRUNC 截断文件
O_APPEND 追加模式
O_NONBLOCK 非阻塞模式
O_SYNC 鸿蒙暂不支持
O_DIRECT 鸿蒙暂不支持

我的经验:在 IoT 设备上,O_SYNC 和 O_DIRECT 其实很少用到。鸿蒙砍掉它们,我反而觉得清爽。少一个特性,少一个坑。

这里有个关键区别:鸿蒙的 open 返回的是 fd(文件描述符),但 fd 的管理方式跟 Linux 不同。Linux 的 fd 是进程级的,每个进程有自己的 fd 表。鸿蒙为了轻量化,fd 表是全局的——说白了,整个系统共享一套 fd 编号。

我曾经在调试一个多进程场景时,发现两个进程同时打开文件,返回的 fd 竟然一样。当时我一脸懵,后来翻源码才明白:鸿蒙的 fd 是全局分配,不是进程隔离的。这个设计取舍,你心里要有数。

3.3 read() 和 write():数据搬运工

read 和 write 的接口,跟 Linux 几乎一样:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

但实现上有个重要差异:鸿蒙的 read/write 默认不带内核缓冲。Linux 的 VFS 层有 page cache,读文件时先缓存到内存。鸿蒙为了省内存,直接透传到具体文件系统。

你想想看,这对性能有什么影响?

  • 优点:内存占用低,适合 RAM 只有几十 KB 的 MCU 设备
  • 缺点:频繁小数据读写时,性能不如 Linux

避坑指南:我曾经在移植一个日志系统时,发现写日志特别慢。查了半天,问题出在每次 write 只写几十字节,但底层文件系统每次都要刷盘。后来我加了个用户态缓冲区,攒够 4KB 再写,性能直接提升 10 倍。

3.4 close():别小看这个收尾动作

close 接口很简单:

int close(int fd);

但我要提醒你一点:鸿蒙的 close 会立即释放 fd 和关联的资源。Linux 中,如果文件被多个进程共享,close 只是减少引用计数。鸿蒙没有引用计数机制,close 就是直接关闭。

这意味着什么?如果你 fork 了子进程,父进程 close 掉 fd,子进程手里的 fd 就失效了。嗯,这个坑我踩过,当时排查了好久。

3.5 ioctl():万能工具箱

ioctl 是文件系统里最灵活、也最容易出问题的接口。鸿蒙的 ioctl 原型:

int ioctl(int fd, int request, ... unsigned long arg);

跟 Linux 一样,第三个参数是变参。但鸿蒙对 ioctl 的实现做了简化:

  • 支持的 request 命令字更少——只保留了最常用的
  • 没有 Linux 的 _IOR/_IOW/_IOWR 宏体系——命令字直接定义成整数
  • 参数传递方式不同——鸿蒙用 unsigned long 直接传值,Linux 通常传指针

举个例子,在 Linux 中获取设备信息通常这样写:

struct my_device_info info;
ioctl(fd, MY_DEVICE_GET_INFO, &info);  // 传指针

鸿蒙里你可能得这样:

unsigned long info;
ioctl(fd, MY_DEVICE_GET_INFO, &info);  // 直接传值

为什么会有这个差异?我个人的理解是:鸿蒙的设计哲学是“能简单就简单”。传指针需要做 copy_from_user,增加复杂度。直接传值,内核态省去一步。

关键提醒:如果你在鸿蒙上写驱动程序,ioctl 的参数传递方式一定要看具体文件系统的实现。有的文件系统支持传指针,有的只支持传值。别想当然。

3.6 与 Linux VFS 的核心差异总结

说了这么多,我帮你理一理鸿蒙和 Linux 在文件操作 API 上的核心差异:

对比项 Linux VFS OpenHarmony VFS
fd 管理 进程级,每个进程独立 fd 表 全局 fd 表,所有进程共享
内核缓冲 有 page cache 无内核缓冲,直接透传
引用计数 有,支持多进程共享 fd 无,close 即释放
ioctl 参数 支持指针传参,有 _IOR/_IOW 宏 简化传参,命令字直接定义
O_SYNC/O_DIRECT 支持 不支持
文件锁 支持 flock/fcntl 不支持
异步 I/O 支持 aio_read/aio_write 不支持

看到这个表格,你可能会觉得鸿蒙弱爆了。但我想说:没有最好的设计,只有最合适的设计。鸿蒙的目标设备是 IoT 终端,内存小、CPU 弱、场景单一。Linux 那套重型机制,搬过来反而水土不服。

3.7 实战:写一个简单的文件操作示例

最后,我给你一个完整的示例。这段代码我在开发板上跑过,你可以直接拿去用:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define TEST_FILE "/data/test.txt"
#define BUF_SIZE 128

int main(void)
{
    int fd;
    char write_buf[] = "Hello, OpenHarmony File System!";
    char read_buf[BUF_SIZE] = {0};
    ssize_t ret;

    // 1. 打开文件,不存在则创建
    fd = open(TEST_FILE, O_CREAT | O_RDWR, 0644);
    if (fd < 0) {
        printf("open failed, errno=%d\n", errno);
        return -1;
    }
    printf("open success, fd=%d\n", fd);

    // 2. 写入数据
    ret = write(fd, write_buf, strlen(write_buf));
    if (ret < 0) {
        printf("write failed, errno=%d\n", errno);
        close(fd);
        return -1;
    }
    printf("write %zd bytes\n", ret);

    // 3. 移动文件指针到开头
    lseek(fd, 0, SEEK_SET);

    // 4. 读取数据
    ret = read(fd, read_buf, BUF_SIZE - 1);
    if (ret < 0) {
        printf("read failed, errno=%d\n", errno);
        close(fd);
        return -1;
    }
    printf("read %zd bytes: %s\n", ret, read_buf);

    // 5. 关闭文件
    close(fd);
    printf("file closed\n");

    return 0;
}

这段代码在鸿蒙上编译运行,输出应该是:

open success, fd=3
write 33 bytes
read 33 bytes: Hello, OpenHarmony File System!
file closed

注意看 fd 的值。在鸿蒙上,第一个打开的文件通常是 fd=3(0、1、2 被 stdin/stdout/stderr 占用)。但在 Linux 上,如果你在子进程中打开,fd 可能也是 3,但含义不同——鸿蒙的 3 是全局的,Linux 的 3 是进程私有的。

我的建议:写鸿蒙文件操作代码时,不要依赖 fd 的具体数值。把它当成一个不透明的句柄就好。你只需要关心它是否大于等于 0。

好了,这一章的内容就到这里。文件操作 API 是文件系统的基础,也是你后续做定制和适配的起点。记住我说的那几个差异点,尤其是 fd 管理和 ioctl 参数传递,这两个地方最容易出问题。

专注资料整理