Linux系统调用IO(也叫"不带缓冲的IO"或"原始IO")是操作系统提供的最底层文件操作接口。每个 open/read/write/close 调用都会直接陷入内核,由内核完成实际的IO操作。
与C库IO的本质区别:
为什么系统调用IO重要:
/dev/input/event0、/dev/fb0)必须用系统调用IO操作ioctl、mmap、fcntl 等系统调用只能通过文件描述符使用file_operations 接口的前提文件描述符(File Descriptor):
#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,设置 errnomode权限值(八进制):
| 权限 | 含义 |
|---|---|
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,用户读写,组和其他只读)
#include <unistd.h>
int close(int fd);
返回值: 成功返回 0,失败返回 -1
重要: 关闭文件描述符会释放该fd对应的内核资源(file struct、inode引用等)。如果不关闭,进程退出时会自动关闭,但长期运行的进程可能耗尽文件描述符。
易错点: 关闭后再使用该fd是未定义行为。如果有其他文件描述符指向同一个打开文件(如dup复制的),关闭一个不影响另一个。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符buf:读取数据的缓冲区count:最多读取的字节数返回值:
-1,设置 errno-1,errno 为 EINTR(需要重试)注意: read 可能返回小于 count 的值,原因包括:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd:文件描述符buf:要写入的数据缓冲区count:要写入的字节数返回值:
-1,设置 errno易错点:
write返回值小于count是正常的(磁盘快满、信号中断等),需要处理部分写入的情况。
#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)。
#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
参数:
fd:文件描述符(指向设备文件)request:控制命令(设备特定的宏定义)...:可选参数(取决于request)用途:
常见示例:
#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向用户空间暴露各种控制功能。
#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库版本的关键差异:
#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权限运行)
#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
| 序号 | 坑点 | 说明 |
|---|---|---|
| 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由驱动实现,行为取决于驱动,不一定遵循普通文件语义 |
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。