read 是 Linux/Unix 系统中最基本、最重要的系统调用之一,用于从文件描述符(file descriptor)中读取数据。

(图片来源网络,侵删)
函数原型
read 函数在 <unistd.h> 头文件中声明。
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count);
参数详解
-
int fd: 文件描述符- 这是一个非负整数,是内核为了管理一个打开的文件(或设备、套接字等)而返回的句柄。
- 它是
open,socket,pipe,accept等函数的返回值。 - 当一个程序启动时,Linux 默认会打开三个文件描述符:
0: 标准输入1: 标准输出2: 标准错误
- 你可以用
0来从键盘读取数据,用1向屏幕写入数据。
-
void *buf: 缓冲区- 这是一个指向内存区域的指针,
read会将从fd读取的数据存放到这个缓冲区中。 - 调用者必须确保这个缓冲区足够大,能够容纳最多
count字节的数据,否则会导致缓冲区溢出,非常危险。
- 这是一个指向内存区域的指针,
-
size_t count: 读取的字节数
(图片来源网络,侵删)- 这是你希望从
fd读取的最大字节数。 read并不保证一定能读取到count个字节,它可能会返回更少的字节数。
- 这是你希望从
返回值
read 的返回值类型是 ssize_t,这是一个有符号的整数类型,有以下几种情况:
-
成功时:
- 返回实际读取到的字节数,这个值会小于或等于
count。 - 如果返回
0,表示已经到达了文件的末尾,没有更多数据可读了。
- 返回实际读取到的字节数,这个值会小于或等于
-
出错时:
- 返回
-1,errno会被设置为一个特定的错误码(你可以用perror函数打印出错误信息)。
- 返回
-
特殊情况:
- 阻塞与非阻塞: 如果文件描述符被设置为阻塞模式(默认),并且当前没有数据可读(从网络套接字或管道读取时),
read调用会阻塞,暂停程序的执行,直到有数据到达或发生错误。 - 如果文件描述符被设置为非阻塞模式,当没有数据可读时,
read会立即返回-1,并将errno设置为EAGAIN或EWOULDBLOCK。
- 阻塞与非阻塞: 如果文件描述符被设置为阻塞模式(默认),并且当前没有数据可读(从网络套接字或管道读取时),
错误码
当 read 返回 -1 时,errno 可能被设置为以下常见值:
EAGAIN/EWOULDBLOCK: 文件描述符被设置为非阻塞模式,并且当前没有数据可读。EBADF:fd不是一个有效的文件描述符,或者它没有被打开为只读。EINTR: 读取操作被一个信号中断。EIO: 发生了 I/O 错误。EINVAL: 参数count为 0 或负数。
一个简单的示例:从标准输入读取
这个例子演示了如何从标准输入(通常是键盘)读取用户输入,并将其打印到标准输出(通常是屏幕)。
#include <stdio.h>
#include <unistd.h> // for read
#include <string.h> // for strlen
#include <errno.h> // for errno
#define BUFFER_SIZE 1024
int main() {
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
// 从标准输入 (fd=0) 读取数据,最多读取 BUFFER_SIZE-1 个字节
// 留一个字节给字符串结束符 '\0'
bytes_read = read(STDIN_FILENO, buffer, BUFFER_SIZE - 1);
// 检查 read 是否出错
if (bytes_read == -1) {
// 使用 perror 打印一个描述性的错误信息
perror("read error");
return 1; // 返回非零表示错误
}
// 检查是否到达文件末尾 (用户按了 Ctrl+D)
if (bytes_read == 0) {
printf("End of input (EOF).\n");
return 0;
}
// 成功读取,为字符串添加结束符 '\0'
buffer[bytes_read] = '\0';
// 打印读取到的内容
printf("You entered: %s", buffer);
printf("Number of bytes read: %zd\n", bytes_read);
return 0;
}
如何编译和运行:
- 将代码保存为
read_example.c。 - 编译:
gcc read_example.c -o read_example - 运行:
./read_example - 在终端输入一些文字,然后按回车,程序会打印你输入的内容和读取的字节数。
- 尝试直接按
Ctrl+D(发送 EOF 信号),程序会提示 "End of input"。
重要注意事项和最佳实践
-
循环读取:
read不会一次性读取你请求的所有数据,尤其是在读取网络套接字或管道时,一个健壮的程序应该在一个循环中调用read,直到读取到期望数量的字节或到达文件末尾。// 一个更健壮的读取循环 char buffer[BUFFER_SIZE]; size_t total_bytes_to_read = 100; size_t total_bytes_read = 0; while (total_bytes_read < total_bytes_to_read) { ssize_t bytes_read = read(fd, buffer + total_bytes_read, total_bytes_to_read - total_bytes_read); if (bytes_read == -1) { // 错误处理 perror("read failed"); break; } else if (bytes_read == 0) { // EOF printf("EOF reached.\n"); break; } total_bytes_read += bytes_read; } -
缓冲区大小:
count参数是请求读取的最大字节数,而不是保证读取的字节数,不要假设read一定会填满你的缓冲区。 -
文本 vs. 二进制模式: 在 Linux 上,
read总是进行二进制模式的读取,它不会对数据进行任何转换(比如把\r\n转成\n),这与 Windows 的 C 库函数(如fread)在文本模式下有所不同,这使得read在处理二进制文件(如图片、可执行文件)时非常可靠。 -
文件指针:
read会自动更新与文件描述符fd相关联的“文件指针”(或叫“当前偏移量”),下一次读取会从上一次读取结束的位置继续,你可以使用lseek系统调用来移动这个指针。
read 与标准 I/O 库函数(fread)的区别
| 特性 | read (系统调用) |
fread (C 标准库函数) |
|---|---|---|
| 层次 | 底层,直接与内核交互 | 高层,在用户空间实现缓冲 |
| 性能 | 每次调用都有系统开销,适合大块数据读写 | 通过缓冲区减少了系统调用次数,通常更快,特别是对小文件或频繁读写 |
| 缓冲 | 无用户空间缓冲,数据直接在内核空间和用户空间之间传递。 | 有用户空间缓冲,数据先读到库的缓冲区,再由库按需填充到你的缓冲区。 |
| 可移植性 | POSIX 标准,在所有 Unix-like 系统上可用 | C 标准库,在几乎所有操作系统上可用 |
| 控制粒度 | 直接操作文件描述符,更灵活,易于与 I/O 多路复用(select, poll, epoll)结合 |
操作 FILE* 流,更简单,功能更丰富(如格式化 I/O printf/scanf) |
何时使用哪个?
- 使用
fread/fwrite: 当你进行常规的文件读写,特别是处理文本文件或对性能要求不是极致时,这是绝大多数情况下的首选,因为它更简单、通常性能更好。 - 使用
read/write:- 当你需要处理网络套接字、管道、终端等非文件设备时。
- 当你进行二进制文件的精确读写,不希望有任何意外的数据转换时。
- 当你需要与I/O 多路复用机制(如
epoll)配合使用时,因为这些机制只监控文件描述符,不监控FILE*流。 - 当你在编写系统编程、驱动程序或需要极致控制性能的底层代码时。
read 是 Linux C 程序员必须掌握的核心工具,理解其行为和返回值是编写健壮、高效程序的基础。
