# 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. `ioctl`、`mmap`、`fcntl` 等系统调用只能通过文件描述符使用 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 — 打开或创建文件 ```c #include #include #include 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 — 关闭文件描述符 ```c #include int close(int fd); ``` **返回值:** 成功返回 `0`,失败返回 `-1` **重要:** 关闭文件描述符会释放该fd对应的内核资源(file struct、inode引用等)。如果不关闭,进程退出时会自动关闭,但长期运行的进程可能耗尽文件描述符。 > **易错点:** 关闭后再使用该fd是未定义行为。如果有其他文件描述符指向同一个打开文件(如dup复制的),关闭一个不影响另一个。 ### 2.2.3 read — 读取数据 ```c #include ssize_t read(int fd, void *buf, size_t count); ``` **参数:** - `fd`:文件描述符 - `buf`:读取数据的缓冲区 - `count`:最多读取的字节数 **返回值:** - 成功:实际读取的字节数(0表示已到文件末尾) - 失败:返回 `-1`,设置 `errno` - 被信号中断:返回 `-1`,`errno` 为 `EINTR`(需要重试) **注意:** `read` 可能返回小于 `count` 的值,原因包括: - 到达文件末尾 - 被信号中断 - 从管道或socket读取时数据不足 ### 2.2.4 write — 写入数据 ```c #include ssize_t write(int fd, const void *buf, size_t count); ``` **参数:** - `fd`:文件描述符 - `buf`:要写入的数据缓冲区 - `count`:要写入的字节数 **返回值:** - 成功:实际写入的字节数 - 失败:返回 `-1`,设置 `errno` > **易错点:** `write` 返回值小于 `count` 是正常的(磁盘快满、信号中断等),需要处理部分写入的情况。 ### 2.2.5 lseek — 文件定位 ```c #include #include 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)` — 跳过某些数据 **特殊用法 — 创建稀疏文件:** ```c lseek(fd, 1024 * 1024, SEEK_SET); // 跳到1MB位置 write(fd, "A", 1); // 写入1字节 // 文件大小为1MB+1,但实际只占用很少的磁盘块 ``` > **易错点:** 管道、socket、某些设备文件不支持 `lseek`(会返回 `-1`,errno为`ESPIPE`)。 ### 2.2.6 ioctl — 设备控制 ```c #include int ioctl(int fd, unsigned long request, ...); ``` **参数:** - `fd`:文件描述符(指向设备文件) - `request`:控制命令(设备特定的宏定义) - `...`:可选参数(取决于request) **用途:** - 获取设备信息 - 配置设备参数 - 控制设备行为(如电机转动、LED闪烁) **常见示例:** ```c #include // 获取 framebuffer 信息 struct fb_var_screeninfo vinfo; int ret = ioctl(fd, FBIOGET_VSCREENINFO, &vinfo); // 获取终端窗口大小 struct winsize ws; ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); ``` **嵌入式应用举例:** ```c // 操作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库版本) ```c #include #include #include #include #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) ```c #include #include #include #include #include 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示例 ```c #include #include #include #include #include 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。