4. 文件操作基础:open()、close() 与文件描述符

在 Linux 系统中,一切皆文件——帧缓冲设备 /dev/fb0 也不例外。操作帧缓冲的第一步,就是通过标准的文件 I/O 接口打开和关闭设备文件。本章将深入剖析 open() 与 close() 系统调用的行为,并解释文件描述符这一核心抽象概念,为后续的 mmap() 映射和双缓冲机制打下坚实基础。

4.1 文件描述符:内核的整数句柄

文件描述符(File Descriptor, FD)是一个非负整数,它是用户空间程序与内核打开文件表之间的索引。每当进程成功打开一个文件或设备,内核就会分配一个最小的可用整数作为该文件的描述符。

关键理解: 文件描述符不是文件本身,也不是文件指针,而是内核维护的“打开文件表”的索引。用户程序通过这个整数告诉内核“我要操作哪个已打开的文件”。
文件描述符与内核文件表结构 用户空间 进程 A 文件描述符表 0 → stdin 1 → stdout 2 → stderr 3 → /dev/fb0 4 → config.txt ... 内核空间 系统打开文件表 fd 3 → 文件对象 A (inode: /dev/fb0) 偏移: 0 标志: O_RDWR fd 4 → 文件对象 B (inode: /etc/config.txt) 偏移: 1024 inode 缓存 /dev/fb0 主设备号: 29 次设备号: 0 驱动: fb_ops config.txt 磁盘块: 0x3A2F 文件描述符 3 指向内核打开文件表中的一个文件对象,该对象再指向 /dev/fb0 的 inode

4.2 open() 系统调用

open() 是访问帧缓冲设备的入口。它返回一个文件描述符,后续所有操作(mmap()、ioctl()、close())都通过这个描述符进行。

4.2.1 函数原型

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

4.2.2 常用 flags 参数

标志 值 含义 帧缓冲场景
O_RDONLY 0 只读打开 仅读取屏幕内容(不常用)
O_WRONLY 1 只写打开 仅写入屏幕(不常用)
O_RDWR 2 读写打开 标准做法:需要读写帧缓冲
O_SYNC 0x1000 同步 I/O 帧缓冲通常不需要
O_CLOEXEC 0x80000 执行 exec 时自动关闭 推荐用于帧缓冲,防止泄露
最佳实践: 打开帧缓冲设备时,始终使用 O_RDWR 标志。虽然只写也能工作,但某些 ioctl() 操作(如查询可变屏幕信息)需要读权限。

4.2.3 打开帧缓冲的标准写法

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(void)
{
    int fbfd;
    
    /* 打开帧缓冲设备 */
    fbfd = open("/dev/fb0", O_RDWR);
    if (fbfd == -1) {
        perror("open /dev/fb0");
        exit(EXIT_FAILURE);
    }
    
    printf("成功打开 /dev/fb0,文件描述符: %d\n", fbfd);
    
    /* ... 后续操作 ... */
    
    close(fbfd);
    return 0;
}

4.3 close() 系统调用

当进程不再需要访问帧缓冲设备时,必须调用 close() 释放文件描述符。这不仅是资源管理的好习惯,更是防止文件描述符泄露的关键。

4.3.1 函数原型

#include <unistd.h>

int close(int fd);
  • 参数:fd —— 要关闭的文件描述符
  • 返回值:成功返回 0,失败返回 -1 并设置 errno

4.3.2 close() 的内部行为

  1. 从进程的文件描述符表中移除该条目
  2. 递减内核文件对象的引用计数
  3. 当引用计数降为 0 时,释放文件对象并执行清理操作(如刷新缓冲区、释放驱动资源)
常见陷阱: 如果进程在 mmap() 之后关闭文件描述符,映射区域仍然有效。内核通过 mmap 结构维护对文件对象的引用,直到 munmap() 被调用。但为了代码清晰,建议在 mmap() 之后立即 close() 文件描述符。

4.4 文件描述符的分配规则

内核分配文件描述符时遵循“最小可用”原则:从 0 开始扫描,找到第一个未被占用的整数。这解释了为什么标准流(stdin=0, stdout=1, stderr=2)总是最先被分配。

文件描述符分配流程 调用 open() 扫描 FD 表 找到最小空闲 返回 分配示例:连续打开三个文件 1. 打开 /dev/fb0 → 扫描 FD 表:0,1,2 已占用 → 分配 3 2. 打开 config.txt → 扫描 FD 表:0,1,2,3 已占用 → 分配 4 3. 关闭 fd 3 → FD 表条目 3 变为空闲 4. 打开 data.bin → 扫描 FD 表:0,1,2,3(空闲),4 → 分配 3(重用)

4.5 帧缓冲操作中的典型错误处理

错误场景 errno 可能原因 解决方案
open() 返回 -1 EACCES 权限不足(未以 root 运行) 使用 sudo 或将用户加入 video 组
open() 返回 -1 ENOENT /dev/fb0 不存在 检查内核是否启用帧缓冲驱动
open() 返回 -1 ENODEV 设备节点存在但无对应驱动 检查 dmesg 确认驱动加载状态
close() 返回 -1 EBADF 传入的文件描述符无效 检查是否重复关闭或从未成功打开

4.6 完整示例:打开与关闭帧缓冲

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

#define FB_DEVICE "/dev/fb0"

int main(void)
{
    int fbfd;
    
    /* 打开帧缓冲设备 */
    fbfd = open(FB_DEVICE, O_RDWR);
    if (fbfd == -1) {
        fprintf(stderr, "打开 %s 失败: ", FB_DEVICE);
        switch (errno) {
        case EACCES:
            fprintf(stderr, "权限不足,请以 root 运行\n");
            break;
        case ENOENT:
            fprintf(stderr, "设备节点不存在\n");
            break;
        default:
            perror("");
        }
        exit(EXIT_FAILURE);
    }
    
    printf("成功打开 %s,文件描述符: %d\n", FB_DEVICE, fbfd);
    printf("标准输入: 0, 标准输出: 1, 标准错误: 2\n");
    printf("帧缓冲分配到了描述符: %d\n", fbfd);
    
    /* 模拟帧缓冲操作(后续章节会展开) */
    /* ... */
    
    /* 关闭设备 */
    if (close(fbfd) == -1) {
        perror("close");
        exit(EXIT_FAILURE);
    }
    
    printf("帧缓冲设备已关闭\n");
    return 0;
}
运行观察: 在终端执行上述程序,输出将显示 /dev/fb0 被分配的文件描述符。如果程序是第一个打开额外文件的进程,通常得到 fd 3。如果之前有其他文件已打开,则可能得到更大的数字。

4.7 本章小结

  • 文件描述符是内核提供的整数句柄,用于索引进程的打开文件表
  • open() 打开帧缓冲设备时使用 O_RDWR 标志,返回文件描述符
  • close() 释放文件描述符,递减内核文件对象的引用计数
  • 文件描述符遵循最小可用分配原则,关闭后会被重用
  • 帧缓冲操作中,open() 失败通常由权限不足或设备不存在引起