2. 系统调用IO.md 13 KB

2 系统调用IO

2.1 基本概念

Linux系统调用IO(也叫"不带缓冲的IO"或"原始IO")是操作系统提供的最底层文件操作接口。每个 open/read/write/close 调用都会直接陷入内核,由内核完成实际的IO操作。

与C库IO的本质区别:

  • C库IO:用户空间有缓冲区,攒够数据再调用系统调用(减少陷入内核次数)
  • 系统调用IO:每次操作都直接陷入内核(无用户空间缓冲)

为什么系统调用IO重要:

  1. 嵌入式开发中,设备文件(如 /dev/input/event0/dev/fb0)必须用系统调用IO操作
  2. ioctlmmapfcntl 等系统调用只能通过文件描述符使用
  3. 理解系统调用IO是理解驱动 file_operations 接口的前提
  4. 多路复用(select/poll/epoll)基于文件描述符工作

文件描述符(File Descriptor):

  • 进程打开文件时,内核返回一个非负整数作为文件描述符(fd)
  • 每个进程有一个文件描述符表,fd是该表的索引
  • 标准输入 fd=0,标准输出 fd=1,标准错误 fd=2
  • 新打开的文件通常从fd=3开始分配

2.2 核心API详解

2.2.1 open — 打开或创建文件

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

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

参数:

  • pathname:文件路径
  • flags:打开标志(必须指定以下之一)
  • mode:文件权限(仅在创建新文件时有效,受 umask 影响)

必选标志(三选一):

标志 含义
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开

可选标志(按位或组合使用):

标志 含义
O_CREAT 文件不存在则创建(需要mode参数)
O_TRUNC 打开时清空文件内容(等效"w")
O_APPEND 追加模式,每次写入自动定位到末尾
O_NONBLOCK 非阻塞模式(设备文件、管道、socket常用)
O_EXCL 与O_CREAT组合使用,文件已存在则报错(原子创建)
O_SYNC 每次write等待数据写入磁盘后才返回
O_NOCTTY 如果pathname是终端设备,不将其设为控制终端

返回值:

  • 成功:返回文件描述符(非负整数)
  • 失败:返回 -1,设置 errno

mode权限值(八进制):

权限 含义
S_IRUSR 用户读(0400)
S_IWUSR 用户写(0200)
S_IXUSR 用户执行(0100)
S_IRGRP 组读(0040)
S_IWGRP 组写(0020)
S_IXGRP 组执行(0010)
S_IROTH 其他读(0004)
S_IWOTH 其他写(0002)
S_IXOTH 其他执行(0001)

常用组合:S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH(0644,用户读写,组和其他只读)

2.2.2 close — 关闭文件描述符

#include <unistd.h>

int close(int fd);

返回值: 成功返回 0,失败返回 -1

重要: 关闭文件描述符会释放该fd对应的内核资源(file struct、inode引用等)。如果不关闭,进程退出时会自动关闭,但长期运行的进程可能耗尽文件描述符。

易错点: 关闭后再使用该fd是未定义行为。如果有其他文件描述符指向同一个打开文件(如dup复制的),关闭一个不影响另一个。

2.2.3 read — 读取数据

#include <unistd.h>

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

参数:

  • fd:文件描述符
  • buf:读取数据的缓冲区
  • count:最多读取的字节数

返回值:

  • 成功:实际读取的字节数(0表示已到文件末尾)
  • 失败:返回 -1,设置 errno
  • 被信号中断:返回 -1errnoEINTR(需要重试)

注意: read 可能返回小于 count 的值,原因包括:

  • 到达文件末尾
  • 被信号中断
  • 从管道或socket读取时数据不足

2.2.4 write — 写入数据

#include <unistd.h>

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

参数:

  • fd:文件描述符
  • buf:要写入的数据缓冲区
  • count:要写入的字节数

返回值:

  • 成功:实际写入的字节数
  • 失败:返回 -1,设置 errno

易错点: write 返回值小于 count 是正常的(磁盘快满、信号中断等),需要处理部分写入的情况。

2.2.5 lseek — 文件定位

#include <sys/types.h>
#include <unistd.h>

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

参数:

  • fd:文件描述符
  • offset:偏移量(字节)
  • whence:基准位置
    • SEEK_SET:文件开头
    • SEEK_CUR:当前位置
    • SEEK_END:文件末尾

返回值: 成功返回新的文件偏移量(距文件开头的字节数),失败返回 -1

常见用途:

  • lseek(fd, 0, SEEK_END) — 获取文件大小
  • lseek(fd, 0, SEEK_SET) — 回到文件开头
  • lseek(fd, offset, SEEK_CUR) — 跳过某些数据

特殊用法 — 创建稀疏文件:

lseek(fd, 1024 * 1024, SEEK_SET); // 跳到1MB位置
write(fd, "A", 1);                 // 写入1字节
// 文件大小为1MB+1,但实际只占用很少的磁盘块

易错点: 管道、socket、某些设备文件不支持 lseek(会返回 -1,errno为ESPIPE)。

2.2.6 ioctl — 设备控制

#include <sys/ioctl.h>

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

参数:

  • fd:文件描述符(指向设备文件)
  • request:控制命令(设备特定的宏定义)
  • ...:可选参数(取决于request)

用途:

  • 获取设备信息
  • 配置设备参数
  • 控制设备行为(如电机转动、LED闪烁)

常见示例:

#include <linux/fb.h>

// 获取 framebuffer 信息
struct fb_var_screeninfo vinfo;
int ret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo);

// 获取终端窗口大小
struct winsize ws;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);

嵌入式应用举例:

// 操作GPIO设备文件
int fd = open("/dev/gpio0", O_RDWR);
int value = 1;
ioctl(fd, GPIO_SET_VALUE, &value);  // 设置GPIO高电平
close(fd);

ioctl是嵌入式Linux应用开发的核心接口之一,驱动通过ioctl向用户空间暴露各种控制功能。

2.3 完整代码示例

示例1:系统调用IO文件复制(对比C库版本)

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

#define BUF_SIZE 4096

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "用法: %s <源文件> <目标文件>\n", argv[0]);
        return 1;
    }

    // 打开源文件
    int src_fd = open(argv[1], O_RDONLY);
    if (src_fd < 0) {
        perror("open src");
        return 1;
    }

    // 创建目标文件(权限0644)
    int dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (dst_fd < 0) {
        perror("open dst");
        close(src_fd);
        return 1;
    }

    char buf[BUF_SIZE];
    ssize_t n;
    while ((n = read(src_fd, buf, BUF_SIZE)) > 0) {
        ssize_t written = 0;
        // 处理部分写入
        while (written < n) {
            ssize_t w = write(dst_fd, buf + written, n - written);
            if (w < 0) {
                perror("write");
                close(src_fd);
                close(dst_fd);
                return 1;
            }
            written += w;
        }
    }

    if (n < 0) {
        perror("read");
    }

    close(src_fd);
    close(dst_fd);
    printf("复制完成\n");
    return 0;
}

编译:gcc -o syscall_copy syscall_copy.c

与C库版本的关键差异:

  1. 没有缓冲区概念,每次read/write都是系统调用
  2. write需要处理部分写入(循环写入直到全部写完)
  3. 错误处理中需要手动关闭已打开的fd

示例2:读取设备信息(ioctl)

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>

int main() {
    int fd = open("/dev/fb0", O_RDWR);
    if (fd < 0) {
        perror("打开framebuffer设备失败");
        return 1;
    }

    struct fb_var_screeninfo vinfo;
    if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) < 0) {
        perror("ioctl FBIOGET_VSCREENINFO");
        close(fd);
        return 1;
    }

    printf("分辨率: %dx%d\n", vinfo.xres, vinfo.yres);
    printf("色深: %d bpp\n", vinfo.bits_per_pixel);
    printf("红通道: %d-%d bits\n", vinfo.red.offset, vinfo.red.length);

    close(fd);
    return 0;
}

编译:gcc -o fb_info fb_info.c(需要root权限运行)

示例3:非阻塞IO示例

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

int main() {
    // 以非阻塞方式打开设备文件
    int fd = open("/dev/input/event0", O_RDONLY | O_NONBLOCK);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    char buf[256];
    while (1) {
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 非阻塞模式下没有数据,稍后重试
                printf("暂无数据,等待中...\n");
                usleep(100000); // 100ms
                continue;
            }
            perror("read");
            break;
        }
        if (n == 0) {
            printf("设备关闭\n");
            break;
        }
        printf("读取到 %zd 字节数据\n", n);
    }

    close(fd);
    return 0;
}

编译:gcc -o nonblock nonblock.c

2.4 注意事项与易错点

序号 坑点 说明
1 O_TRUNC的危险 O_WRONLY \| O_TRUNC 会清空文件,等同于"w"模式
2 read返回EINTR 被信号中断时read返回-1且errno=EINTR,不是错误,需要重试
3 write部分写入 write可能写入比请求少的字节数,必须循环写入
4 O_NONBLOCK配合select/poll/epoll 非阻塞IO通常与IO多路复用配合使用
5 fd最小可用原则 open返回当前未使用的最小fd编号
6 errno不是全局变量 errno是线程局部的(TLS),多线程中每个线程有自己的errno
7 O_EXCL原子创建 O_CREAT \| O_EXCL 保证文件不存在时才创建,避免竞态条件
8 O_SYNC的性能 O_SYNC每次write都等待磁盘写入完成,性能很低,嵌入式中慎用
9 lseek与管道 管道、socket、FIFO不支持lseek,返回-1且errno=ESPIPE
10 设备文件的read/write 设备文件的read/write由驱动实现,行为取决于驱动,不一定遵循普通文件语义

2.5 面试要点

Q: open的flags参数中O_WRONLY和O_RDWR有什么区别? A: O_WRONLY只允许写操作,O_RDWR允许读写。对于某些设备(如只读设备),使用错误的flags会导致open失败。驱动通过fops->open检查flags来验证访问权限。

Q: read返回-1一定是错误吗? A: 不一定。errno=EINTR表示被信号中断,应该重试;errno=EAGAIN/EWOULDBLOCK表示非阻塞模式下没有数据,也应该重试。只有其他errno值才是真正的错误。

Q: 文件描述符是什么?内核如何管理? A: 文件描述符是进程文件描述符表的索引,每个fd指向一个内核中的file struct。进程通过fd操作文件,内核通过file struct找到对应的inode和驱动操作。进程退出时自动关闭所有fd。

Q: write写入不完整怎么处理? A: write返回值小于请求的count是正常的。应该用循环写入:记录已写入的字节数,每次write的buf指针偏移,count减小,直到全部写完。

Q: 系统调用IO和C库IO什么时候用哪个? A: 普通文件操作优先用C库IO(有缓冲,效率高);设备文件操作用系统调用IO(需要ioctl/mmap等);需要精确控制IO时机(如O_SYNC)用系统调用IO。