4. 文件与目录操作API:文件创建与打开、读写、定位、关闭及目录操作

各位同学,咱们今天聊点实在的。文件操作,说白了就是嵌入式系统里数据存取的“基本功”。你想想看,不管是存日志、写配置、还是记录传感器数据,最终都得落到文件上。VxWorks 在这方面提供了非常经典的 POSIX 接口,跟 Linux 很像,但又有它自己的脾气。我刚开始从 Linux 转过来时,就踩过几个坑,今天一并讲清楚。

4.1 文件创建与打开:open() 与 creat()

在 VxWorks 里,操作文件的第一步就是拿到一个“文件描述符”。这玩意儿就是个整数,内核用它来跟踪你打开的文件。我个人习惯用 open(),因为它功能更全。

核心函数原型:

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

flags 参数是关键。常用的有:

  • O_RDONLY:只读打开
  • O_WRONLY:只写打开
  • O_RDWR:读写打开
  • O_CREAT:文件不存在则创建
  • O_TRUNC:打开后清空文件内容
  • O_APPEND:追加模式写入

举个例子:

int fd;
fd = open("/sd0/data.log", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == ERROR) {
    /* 处理错误,我建议用 errno 定位问题 */
    printf("open failed, errno = %d\n", errno);
}

避坑指南: 我曾经在创建文件时忘了加 O_CREAT 标志,结果文件不存在时直接返回 -1。还有,creat() 函数其实等价于 open()O_WRONLY | O_CREAT | O_TRUNC。但它只能写,不能读。所以我现在基本只用 open(),更灵活。

4.2 文件读写:read() 与 write()

读写操作是文件系统的核心。VxWorks 的 read()write() 接口非常直接,但有几个细节你得注意。

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

返回值说明:

返回值含义
正数实际读/写的字节数
0读到文件末尾(EOF)
-1出错,检查 errno

实际代码片段:

char buffer[256];
int nread;

nread = read(fd, buffer, sizeof(buffer));
if (nread < 0) {
    /* 读取出错,我记得有一次是因为文件系统满了 */
    printf("read error\n");
} else if (nread == 0) {
    printf("end of file\n");
} else {
    /* 成功读取 nread 个字节 */
    write(STDOUT_FILENO, buffer, nread);
}

重要提醒: read()write() 不保证一次就能读写完你指定的字节数。尤其是在网络文件系统或慢速存储设备上,可能会返回少于请求的字节数。我建议你写个循环,直到读够所需数据或遇到错误为止。说白了,就是“能读多少读多少,别指望一次搞定”。

4.3 文件定位:lseek()

文件操作时,内核维护着一个“当前读写位置”。lseek() 就是用来移动这个位置的。嗯,这里要注意,它只适用于可定位的文件(比如普通文件),对管道或 socket 无效。

off_t lseek(int fd, off_t offset, int whence);

whence 参数有三种:

  • SEEK_SET:从文件开头算起
  • SEEK_CUR:从当前位置算起
  • SEEK_END:从文件末尾算起

实用场景:

/* 跳到文件开头 */
lseek(fd, 0, SEEK_SET);

/* 跳到文件末尾,常用于获取文件大小 */
off_t file_size = lseek(fd, 0, SEEK_END);

/* 从当前位置向后移动 100 字节 */
lseek(fd, 100, SEEK_CUR);

个人经验: 我在项目中遇到过需要修改配置文件中间某个字段的情况。先 lseek 到指定位置,然后 write 覆盖写入。但要注意,这种操作不会自动插入或删除数据,它只是覆盖。如果你想在文件中间插入内容,那就得自己把后面的数据先读出来,再写回去。说白了,VxWorks 的文件系统没有“插入”操作,你得自己动手。

4.4 文件关闭:close()

操作完文件,一定要记得关闭。这不仅是好习惯,更是资源管理的基本要求。

int close(int fd);

关闭文件会做三件事:

  1. 释放文件描述符,供后续 open() 重用
  2. 刷新内核缓冲区,确保数据真正写入存储设备
  3. 释放与该文件相关的内核资源

血的教训: 我曾经在写一个长时间运行的数据采集程序时,忘了 close() 文件描述符。结果跑了几天后,系统报“打开文件过多”的错误。VxWorks 默认每个任务能打开的文件数量是有限的(通常 20 或 50 个)。不关闭文件,描述符会泄漏,最终导致程序崩溃。所以,我建议你养成“谁打开谁关闭”的习惯,最好在函数返回前确保所有打开的文件都被关闭。

4.5 目录操作:opendir、readdir、mkdir、rmdir

文件系统不只是文件,还有目录。VxWorks 提供了一组标准的目录操作函数,跟 POSIX 完全兼容。

4.5.1 创建与删除目录

int mkdir(const char *path, mode_t mode);
int rmdir(const char *path);

mkdir()mode 参数指定权限,但在 VxWorks 的某些文件系统(如 dosFs)中,权限可能被忽略。不过,我建议你还是写上,保持代码的可移植性。

/* 创建目录,如果已存在则忽略错误 */
if (mkdir("/sd0/data", 0755) == ERROR) {
    if (errno != EEXIST) {
        printf("mkdir failed\n");
    }
}

/* 删除目录,目录必须为空 */
if (rmdir("/sd0/data") == ERROR) {
    printf("rmdir failed, maybe not empty?\n");
}

4.5.2 遍历目录

遍历目录是文件管理的基本操作。VxWorks 使用 opendir()readdir()closedir() 三件套。

DIR *dir;
struct dirent *entry;

dir = opendir("/sd0");
if (dir == NULL) {
    printf("opendir failed\n");
    return;
}

while ((entry = readdir(dir)) != NULL) {
    printf("name: %s, type: %d\n", entry->d_name, entry->d_type);
}

closedir(dir);

struct dirent 结构体包含:

成员说明
d_name文件名(最长 256 字节)
d_type文件类型(DT_REG 普通文件,DT_DIR 目录)

避坑指南: 我记得有一次遍历目录时,发现 readdir() 返回了 ... 这两个特殊目录。如果你不想处理它们,记得在代码里过滤掉。另外,opendir() 失败的原因很多,比如路径不存在、权限不足、或者文件系统未挂载。我建议你每次调用后都检查返回值,并用 errno 定位问题。

4.6 综合示例:一个简单的日志文件操作

最后,我给大家展示一个完整的例子。这个例子创建目录、打开日志文件、写入数据、然后读取并打印。

#include <stdio.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>

void log_example(void) {
    int fd;
    char *log_data = "Sensor reading: 25.3°C\n";
    char read_buf[128];
    int nread;

    /* 1. 创建日志目录 */
    if (mkdir("/sd0/logs", 0755) == ERROR) {
        if (errno != EEXIST) {
            printf("mkdir failed\n");
            return;
        }
    }

    /* 2. 打开日志文件(追加模式) */
    fd = open("/sd0/logs/sensor.log", O_RDWR | O_CREAT | O_APPEND, 0644);
    if (fd == ERROR) {
        printf("open failed, errno = %d\n", errno);
        return;
    }

    /* 3. 写入数据 */
    if (write(fd, log_data, strlen(log_data)) == ERROR) {
        printf("write failed\n");
        close(fd);
        return;
    }

    /* 4. 定位到文件开头,读取并打印 */
    lseek(fd, 0, SEEK_SET);
    while ((nread = read(fd, read_buf, sizeof(read_buf) - 1)) > 0) {
        read_buf[nread] = '\0';
        printf("%s", read_buf);
    }

    /* 5. 关闭文件 */
    close(fd);
}

这个例子涵盖了今天讲的所有 API。你想想看,从创建目录到打开文件,再到读写和定位,最后关闭,这就是一个完整的数据存储流程。在实际项目中,你可能会在这个基础上加上错误重试、缓冲区管理、或者多任务同步。但核心思想不变——用好这些基础 API,你就能构建出稳定可靠的数据存储系统。

好了,今天就到这里。下一章我们聊聊文件系统的挂载与卸载,那是另一个容易踩坑的地方。