我以前刚开始学Linux编程的时候,一直以为文件IO 就是几个函数:open()、read()、write()、close(),最多再加一个 lseek()。
可真正一边写程序、一边调试、一边踩坑之后,我才发现: 文件 IO 远不只是“会用几个函数”这么简单,而是 Linux 编程里最基础、也最关键的一套思维方式。
我要怎么理解文件描述符,怎么处理返回值,怎么判断文件读完了没有,怎么理解当前位置,甚至把 write() 放在循环里还是循环外,都会直接决定程序是不是“看起来能跑,但其实有问题”。
所以今天打算写一篇日志,分享一下自己以前学习Linux文件IO操作时的一些心得。
首先,第一个就是文件描述符 “fd” 。
在linux里面,我们打开一个文件之后,内核不会直接把“文件对象”给到我们,而是返回一个整数,这个整数就叫文件描述符。
例如: 0:标准输入 stdin, 1:标准输出 stdout, 2:标准错误 stderr。
使用 open() 打开的文件,通常会从 3 开始:
int fd = open("test.txt", O_RDONLY);如果打开成功,fd 可能是 3。以后我们对这个文件的读写,都是通过这个 fd 来操作。
第二个就是文件IO最核心的4个动作: open 打开文件,拿到文件描述符。 read 从文件描述符里读数据到内存。 write 把内存中的数据写到文件描述符。 close 关闭文件描述符,释放资源。
第三个就是lseek,它是用来干嘛的呢? lseek(用来移动“文件读写位置”);说人话就是:比如你打开一个文件,刚开始读写位置在开头。你读了 10 个字节后,位置就往后走 10 个字节。如果你想跳到文件末尾,或者回到开头,或者跳到中间某个位置,就用 lseek()。
废话不多说,直接实操:
例子1:打开一个文件并读取里面内容
逻辑很简单,比如打开一个test.txt文件,读取里面的内容,然后打印到终端显示出来。
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <stdlib.h> int main(void) { int fd; // 文件描述符,用来标识打开的文件 char buf[128]; // 定义一个 128 字节的缓冲区,用于存放读取到的数据 ssize_t ret; // 保存 read() 的返回值,表示实际读取的字节数或错误码 fd = open("test.txt", O_RDONLY); // 以只读方式打开当前目录下的 test.txt 文件if (fd < 0) // 如果返回值小于 0,说明打开文件失败 { perror("open"); // 输出 open 失败的原因return 1; // 返回 1 表示程序异常结束 }while (1) // 进入死循环,不断读取文件内容 { ret = read(fd, buf, sizeof(buf)); // 从文件中读取最多 sizeof(buf) 字节到 buf 中if (ret < 0) // 如果返回值小于 0,说明读取失败 { perror("read"); // 输出 read 失败的原因 close(fd); // 关闭已经打开的文件描述符,避免资源泄漏return 1; // 返回 1 表示程序异常结束 }elseif (ret == 0) // 如果返回值等于 0,说明已经读到文件末尾 {break; // 跳出循环,结束读取 } write(STDOUT_FILENO, buf, ret); // 将读取到的 ret 个字节写到标准输出(终端) } write(STDOUT_FILENO, "\n", 1); // 输出一个换行符,让终端显示更整齐 close(fd); // 关闭文件描述符,释放系统资源return 0; // 返回 0 表示程序正常结束}例子2:在键盘输入数据写入文件
逻辑就是程序运行后,我在键盘随便输入一些数据(数字、字母等),程序从键盘读取这些数据后,把这些数据写入文件。
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <stdlib.h> int main(void) { int fd; // 文件描述符,用来表示打开的文件 char buf[128]; // 定义一个 128 字节的缓冲区,用于暂存输入的数据 ssize_t ret; // 保存 read() 的返回值,表示读取到的字节数或错误信息 fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); // 以只写方式打开 output.txt,不存在则创建,存在则清空,权限为 0644if (fd < 0) // 如果返回值小于 0,说明文件打开失败 { perror("open"); // 输出 open 失败的原因return 1; // 返回 1,表示程序异常结束 }printf("please input some text:\n"); // 提示用户输入内容while (1) // 进入死循环,不断从标准输入读取数据 { ret = read(STDIN_FILENO, buf, sizeof(buf)); // 从标准输入(键盘)读取最多 sizeof(buf) 字节到缓冲区 buf 中if (ret < 0) // 如果返回值小于 0,说明读取失败 { perror("read"); // 输出 read 失败的原因 close(fd); // 关闭已打开的文件,防止资源泄漏return 1; // 返回 1,表示程序异常结束 }elseif (ret == 0) // 如果返回值等于 0,说明输入结束 {break; // 跳出循环,停止读取 }if (write(fd, buf, ret) != ret) // 将读取到的 ret 个字节写入文件,如果写入字节数不等于 ret 说明写入失败 { perror("write"); // 输出 write 失败的原因 close(fd); // 关闭文件描述符return 1; // 返回 1,表示程序异常结束 } } close(fd); // 关闭文件描述符,释放系统资源return 0; // 返回 0,表示程序正常结束}例子3:从一个文件读取文件,然后写入另一个文件
比如:打开src源文件(只读),打开dst目标文件(不存在就创建,存在就清空),循环从src源文件读取,把读到的数据写入dst目标文件,直到读完,最后关闭这两个文件。
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <stdlib.h> int main(int argc, char *argv[]) { int fd_src; // 源文件的文件描述符 int fd_dst; // 目标文件的文件描述符 char buf[128]; // 定义一个 128 字节的缓冲区,用来临时存放读取到的数据 ssize_t ret; // 保存 read() 的返回值,表示读取的字节数或错误信息if (argc != 3) // 如果命令行参数个数不是 3(程序名 + 源文件名 + 目标文件名) {printf("usage: %s <src> <dst>\n", argv[0]); // 提示用户正确的使用方法return 1; // 返回 1,表示程序参数错误,异常结束 } fd_src = open(argv[1], O_RDONLY); // 以只读方式打开命令行中给出的源文件if (fd_src < 0) // 如果返回值小于 0,说明源文件打开失败 { perror("open src"); // 输出打开源文件失败的原因return 1; // 返回 1,表示程序异常结束 } fd_dst = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644); // 以只写方式打开目标文件,不存在则创建,存在则清空,权限为 0644if (fd_dst < 0) // 如果返回值小于 0,说明目标文件打开失败 { perror("open dst"); // 输出打开目标文件失败的原因 close(fd_src); // 关闭已经打开的源文件描述符,防止资源泄漏return 1; // 返回 1,表示程序异常结束 }while (1) // 进入死循环,不断从源文件读取数据 { ret = read(fd_src, buf, sizeof(buf)); // 从源文件读取最多 sizeof(buf) 字节到缓冲区 buf 中if (ret < 0) // 如果返回值小于 0,说明读取失败 { perror("read"); // 输出 read 失败的原因 close(fd_src); // 关闭源文件 close(fd_dst); // 关闭目标文件return 1; // 返回 1,表示程序异常结束 }elseif (ret == 0) // 如果返回值等于 0,说明已经读到源文件末尾 {break; // 跳出循环,结束复制 }if (write(fd_dst, buf, ret) != ret) // 将读取到的 ret 个字节写入目标文件,如果写入字节数不等于 ret,说明写入失败 { perror("write"); // 输出 write 失败的原因 close(fd_src); // 关闭源文件 close(fd_dst); // 关闭目标文件return 1; // 返回 1,表示程序异常结束 } } close(fd_src); // 关闭源文件描述符,释放系统资源 close(fd_dst); // 关闭目标文件描述符,释放系统资源printf("copy success\n"); // 提示用户文件复制成功return 0; // 返回 0,表示程序正常结束}例子4: lseek 原型
off_t lseek(int fd, off_t offset, int whence);参数: fd:文件描述符
offset:偏移量
whence:从哪里开始偏移
whence 有 3 种
SEEK_SET:从文件开头开始
SEEK_CUR:从当前位置开始
SEEK_END:从文件末尾开始
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <stdlib.h> int main(void) { int fd; // 文件描述符,用来表示打开的文件 char ch; // 每次读取 1 个字符,存放当前读到的字符 off_t pos; // 保存文件偏移量,也就是当前读写位置 ssize_t ret; // 保存 read() 的返回值,表示读取的字节数或错误信息 fd = open("lseek_file.txt", O_RDONLY); // 以只读方式打开 lseek_file.txt 文件if(fd < 0) // 如果返回值小于 0,说明打开文件失败 { perror("open"); // 输出 open 失败的原因return 1; // 返回 1,表示程序异常结束 }while(1) // 进入死循环,不断读取文件内容 { pos = lseek(fd, 0, SEEK_CUR); // 获取当前文件偏移量,不移动位置,SEEK_CUR 表示以当前位置为参考if(pos == (off_t)-1) // 如果返回 -1,说明 lseek 调用失败 { perror("lseek"); // 输出 lseek 失败的原因 close(fd); // 关闭文件描述符,防止资源泄漏return 1; // 返回 1,表示程序异常结束 } ret = read(fd, &ch, 1); // 从文件中读取 1 个字节到变量 ch 中if(ret < 0) // 如果返回值小于 0,说明读取失败 { perror("read"); // 输出 read 失败的原因 close(fd); // 关闭文件描述符return 1; // 返回 1,表示程序异常结束 }elseif(ret == 0) // 如果返回值等于 0,说明已经读到文件末尾 {break; // 跳出循环,结束读取 }if(ch == '\n') // 如果当前读取到的字符是换行符printf("pos %ld : \\n\n", (long)pos); // 输出该字符所在的位置,并显示为 \nelse // 如果当前字符不是换行符printf("pos %ld : %c\n", (long)pos, ch); // 输出该字符所在的位置和字符本身 } close(fd); // 关闭文件描述符,释放系统资源return 0; // 返回 0,表示程序正常结束}小结: Linux 程序不是在“直接操作文件”,而是在通过文件描述符,结合当前位置和返回值,一步一步和系统完成数据交互。
这才是文件 IO 真正重要的地方。
它看起来只是入门知识,但后面的管道、设备、网络、串口,甚至很多驱动层逻辑时,都会不断重新遇见这套思维。
所以文件 IO 这部分,值得反复打磨。
因为我们要理解的不只是“怎么读写文件”,而是要懂得: Linux 程序,到底是怎么工作的。
See you next time~ 下次见