Linux 系统编程:文件 I/O 操作详解 (read/write/open)
本笔记基于视频内容整理,并结合 Linux 标准文档(man pages)进行了技术细节的校正与补充。内容涵盖 read、write、open 函数的核心用法、参数详解、返回值处理、文件权限标志以及命令行参数传递的最佳实践。
1. read 函数详解
read 函数用于从文件描述符对应的文件中读取数据。
1.1 函数原型与头文件
根据 man 2 read 手册,函数定义如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- 头文件:
#include <unistd.h>(与close函数相同,无需重复添加)。 - 返回值类型:
ssize_t(有符号整型,通常在 Linux 中被定义为int或long)。
1.2 参数说明
1. int fd
- 文件描述符(File Descriptor)。
- 来源:必须是之前通过
open函数成功打开文件后返回的有效描述符。
2. void *buf
- 缓冲区指针,用于存储读取到的数据。
- 类型兼容性:虽然原型是
void *,实际使用时通常传递char型数组的首地址。C 语言允许隐式强制转换,因此传入char数组是合法的。 - 注意:必须确保该内存空间已分配且足够大,防止缓冲区溢出。
3. size_t count
- 期望读取的最大字节数。
- 限制:不能超过
buf指向的缓冲区实际大小。如果尝试读取超过缓冲区容量的数据,会导致数据丢失或程序异常(Segmentation Fault)。
1.3 返回值处理
read 的返回值至关重要,用于判断操作状态:
> 0:成功读取的实际字节数。0:读到文件末尾(End Of File, EOF)。表示没有更多内容可读,但并非错误。-1:出错。此时全局变量errno会被设置以指示具体错误原因。- 视频中提到
EOF宏定义通常为-1,判断出错时可以直接比较-1或使用EOF。
- 视频中提到
1.4 读取逻辑与循环
由于文件内容可能超过单次 read 的缓冲区大小,通常需要使用循环读取直到文件结束。
推荐模式(while 循环):
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
// 处理读取到的数据,例如打印
write(STDOUT_FILENO, buf, n);
}
if (n == -1) {
// 处理错误
perror("read error");
}
- 逻辑解释:
- 当
n > 0时,继续循环读取。 - 当
n == 0时,循环终止,表示文件读完。 - 当
n < 0时,循环终止,表示发生错误。
- 当
- 视频中的优化点:
- 即使文件为空(第一次 read 就返回 0),循环也不会进入,但程序逻辑应能区分“未执行读取”和“读取到 0 字节”。
- 建议增加计数器变量,累计读取的总字节数,以便调试和验证。
1.5 示例代码:安全读取文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <filename>\n", argv[0]);
return -1;
}
int fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("open error");
return -1;
}
char buf[20];
ssize_t n;
int total = 0;
// 循环读取直到 EOF 或 error
while ((n = read(fd, buf, sizeof(buf))) > 0) {
write(STDOUT_FILENO, buf, n); // 输出到屏幕
total += n;
}
if (n == -1) {
perror("read error");
} else {
printf("\nRead finished. Total bytes: %d\n", total);
}
close(fd);
return 0;
}
2. write 函数详解
write 函数用于向文件描述符对应的文件中写入数据。
2.1 函数原型与头文件
根据 man 2 write 手册:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
2.2 参数说明
1. int fd
- 目标文件的描述符,必须通过
open以写权限(O_WRONLY或O_RDWR)打开。
2. const void *buf
- 要写入的数据源地址。
- 可以是字符串常量(如
"Hello World")、字符数组首地址等。 - 由于是
const,函数内部不会修改缓冲区内容。
3. size_t count
- 期望写入的字节数。
- 计算方式:
- 固定长度:直接使用数字(如
20)。 - 动态长度:使用
strlen()函数计算字符串长度(需包含<string.h>)。 - 注意:
strlen不包含结尾的\0,而sizeof包含。写入文本文件时通常使用strlen或手动加 1。
- 固定长度:直接使用数字(如
2.3 返回值处理
> 0:成功写入的实际字节数。- 注意:返回值可能小于请求的
count(例如磁盘空间不足),健壮的程序应检查是否写完。
- 注意:返回值可能小于请求的
-1:写入失败,设置errno。
2.4 写入逻辑
视频中演示了使用 do-while 或 while 循环确保数据完全写入的逻辑,但在简单示例中,单次写入通常能完成。
健壮写入模式:
size_t total_written = 0;
while (total_written < count) {
ssize_t n = write(fd, buf + total_written, count - total_written);
if (n == -1) {
perror("write error");
break;
}
total_written += n;
}
2.5 示例代码:写入字符串
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <filename>\n", argv[0]);
return -1;
}
// 使用 O_WRONLY | O_CREAT | O_TRUNC 确保可写且创建文件
int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open error");
return -1;
}
char *msg = "Hello World\n";
ssize_t n = write(fd, msg, strlen(msg));
if (n == -1) {
perror("write error");
} else {
printf("Successfully wrote %zd bytes\n", n);
}
close(fd);
return 0;
}
3. open 函数标志位与权限控制
文件打开方式直接决定了后续 read 和 write 操作的合法性。
3.1 常见标志位 (Flags)
O_RDONLY:只读模式。- 允许:
read - 禁止:
write(若尝试写入,write返回-1,程序可能陷入错误循环)。
- 允许:
O_WRONLY:只写模式。- 允许:
write - 禁止:
read(若尝试读取,read返回-1)。
- 允许:
O_RDWR:读写模式。- 允许:
read和write。
- 允许:
- 组合标志:
O_CREAT:如果文件不存在则创建。O_TRUNC:如果文件存在且可写,将长度截断为 0。O_APPEND:每次写入前将文件偏移量移动到文件末尾。
3.2 权限验证实验结论
视频中通过修改 open 的标志位进行了测试:
- 只读 (
O_RDONLY):- 执行
read:成功。 - 执行
write:失败(返回-1),程序若未检查返回值可能陷入死循环或逻辑错误。
- 执行
- 只写 (
O_WRONLY):- 执行
read:失败。 - 执行
write:成功。
- 执行
- 读写 (
O_RDWR):- 执行
read和write:均成功。
- 执行
最佳实践:在 open 时必须根据后续操作需求选择合适的标志位,并在代码中检查 read/write 的返回值以处理权限不足的情况。
4. 命令行参数处理 (argc & argv)
为了使程序更灵活,视频演示了通过命令行传递文件名,而不是硬编码在源代码中。
4.1 参数结构
int main(int argc, char *argv[])
argc(Argument Count):参数个数。- 至少为 1(程序本身名称
argv[0])。
- 至少为 1(程序本身名称
argv(Argument Vector):参数字符串数组。argv[0]:程序执行名称(如./a.out)。argv[1]:第一个用户传入参数(如文件名test.txt)。
4.2 安全性检查
在访问 argv[1] 之前,必须检查 argc 的值。
- 错误做法:直接使用
open(argv[1], ...)。如果用户未传参,argv[1]为NULL,会导致段错误(Segmentation Fault)。 - 正确做法:
if (argc < 2) {
printf("Usage: %s <filename>\n", argv[0]);
return -1; // 提前退出,避免非法内存访问
}
4.3 编译与运行
- 编译:
gcc main.c -o main - 运行:
./main test.txt - 注意:视频中出现
./a.out是默认生成的可执行文件名。
5. 综合示例:读写完整流程
结合视频内容,以下是一个完整的、经过优化的文件读写程序示例,包含了错误处理、参数检查和权限管理。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
// 1. 参数检查
if (argc < 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
return EXIT_FAILURE;
}
// 2. 打开文件 (使用 O_RDWR 以便同时测试读写)
// 若文件不存在则创建,权限设为 0644
int fd = open(argv[1], O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open failed");
return EXIT_FAILURE;
}
// 3. 写操作
const char *write_buf = "Hello Linux IO\n";
ssize_t write_n = write(fd, write_buf, strlen(write_buf));
if (write_n == -1) {
perror("write failed");
close(fd);
return EXIT_FAILURE;
}
printf("Written %zd bytes\n", write_n);
// 4. 重置文件偏移量 (重要:写完后指针在末尾,读之前需移回开头)
lseek(fd, 0, SEEK_SET);
// 5. 读操作
char read_buf[1024];
ssize_t read_n;
printf("Reading content:\n");
while ((read_n = read(fd, read_buf, sizeof(read_buf))) > 0) {
write(STDOUT_FILENO, read_buf, read_n);
}
if (read_n == -1) {
perror("read failed");
}
printf("\nRead operation finished.\n");
// 6. 关闭文件
close(fd);
return EXIT_SUCCESS;
}
6. 常见问题与调试建议 (基于视频讨论)
1. 读取返回 0 的问题
- 现象:程序运行后提示“读了 0 个字符”。
- 原因:文件本身为空,或者文件偏移量已经在文件末尾。
- 解决:检查文件内容;如果是多次读写,注意使用
lseek重置偏移量。
2. 死循环风险
- 现象:在使用
while循环写入时,如果权限不足(如只读打开),write返回-1,若循环条件判断不当(如while(n >= 0)),可能导致死循环。 - 解决:严格判断返回值
if (n == -1) break;。
3. 缓冲区初始化
- 建议:定义字符数组后,最好进行初始化(如
char buf[20] = {0};),避免打印时出现乱码(尽管read返回了长度,但调试打印字符串时\0很重要)。
4. 头文件依赖
read/write/close:<unistd.h>open:<fcntl.h>strlen:<string.h>printf:<stdio.h>
7. 总结
- 核心流程:
open->read/write(循环处理) ->close。 - 返回值检查:必须检查系统调用的返回值,区分成功、EOF 和错误。
- 权限匹配:
open的标志位必须与后续操作(读/写)相匹配。 - 健壮性:通过
argc检查命令行参数,通过循环处理不定长数据,通过错误处理应对异常情况。