3. 鸿蒙内核文件系统接口:open/read/write/close/ioctl 实战解析
好,咱们进入正题。这一章我打算聊聊 OpenHarmony 内核的文件操作 API。说白了,就是 open、read、write、close、ioctl 这些你天天打交道的函数。
很多从 Linux 转过来的朋友,第一反应是:“这不就是 POSIX 那套吗?” 嗯,对,也不全对。我在做第一个鸿蒙移植项目时,就踩过这个坑——想当然地以为完全一样,结果在 ioctl 上卡了两天。今天我把这些异同点掰开揉碎了讲给你听。
核心观点:鸿蒙的 VFS 层借鉴了 Linux 的设计思想,但实现上做了大量精简和重构。它更轻量,更适合资源受限的 IoT 设备。
3.1 文件操作 API 全景
先看一张图,这是我画的内核文件操作调用链。你注意看,用户态和内核态之间,其实隔了一层 VFS 抽象。
这张图你看懂了吗?从上到下,用户态调用经过系统调用,再到 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 参数传递,这两个地方最容易出问题。