文件读写及文件描述符
文件存储在磁盘中,而磁盘是永久性的存储介质,因此文件在磁盘上的存储是永久性的,本质上对文件的所有操作,都是对外设的输入和输出,简称 IO
而在 Linux 下一切皆文件,所谓的键盘、显示器、网卡、磁盘等等都是抽象化的过程,在后面博客的讲解中将会慢慢理解这一点
即便是 0KB 的空文件也是会占用磁盘空间的,因为文件 = 属性 + 内容,所有的文件操作本质上是对文件内容的操作和文件属性的操作,而这种操作本质上又是进程对文件的操作,
磁盘的管理者是操作系统,C语言 等高级语言的库函数中的文件读写本质上是对操作系统给出的文件读写的系统调用接口的包装,方便用户使用
C语言文件操作回顾
在 C 语言中,fopen、fclose、fwrite 和 fread 是标准 I/O 库(<stdio.h>)提供的文件操作函数,用于文件的打开、关闭、写入和读取操作。以下是它们的详细参数介绍:
1.fopen - 打开文件
函数原型
FILE *fopen(const char *filename, const char *mode);
参数说明
| 参数 | 说明 |
|---|---|
filename |
要打开的文件名(路径可以是相对路径或绝对路径) |
mode |
文件打开模式,决定文件如何被访问(读、写、追加等) |
mode 的常见取值
| 模式 | 说明 |
|---|---|
"r" |
只读(文件必须存在) |
"w" |
只写(如果文件存在则清空,否则创建) |
"a" |
追加(如果文件存在则在末尾追加,否则创建) |
"r+" |
读写(文件必须存在) |
"w+" |
读写(如果文件存在则清空,否则创建) |
"a+" |
读写(如果文件存在则在末尾追加,否则创建) |
"b" |
二进制模式(可与上述模式组合,如 "rb"、"wb+") |
返回值
- 成功:返回
FILE*文件指针(用于后续操作) - 失败:返回
NULL(如文件不存在或权限不足)
示例
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("Failed to open file");
exit(1);
}
2. fclose - 关闭文件
函数原型
int fclose(FILE *stream);
参数说明
| 参数 | 说明 |
|---|---|
stream |
要关闭的文件指针(由 fopen 返回) |
返回值
- 成功:返回
0 - 失败:返回
EOF(通常意味着缓冲区写入失败)
示例
if (fclose(fp) != 0) {
perror("Failed to close file");
}
3. fwrite - 写入数据
函数原型
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
参数说明
| 参数 | 说明 |
|---|---|
ptr |
指向要写入数据的缓冲区(如数组、结构体等) |
size |
每个数据项的字节大小(如 sizeof(int)) |
count |
要写入的数据项数量 |
stream |
目标文件指针 |
返回值
- 返回 实际写入的数据项数量(通常等于
count,否则可能出错) - 如果返回值
< count,可能是磁盘满或写入错误
示例
int data[] = {1, 2, 3, 4};
size_t written = fwrite(data, sizeof(int), 4, fp);
if (written < 4) {
perror("Failed to write all data");
}
4. fread - 读取数据
函数原型
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
参数说明
| 参数 | 说明 |
|---|---|
ptr |
存储读取数据的缓冲区 |
size |
每个数据项的字节大小 |
count |
要读取的数据项数量 |
stream |
源文件指针 |
返回值
- 返回 实际读取的数据项数量(可能
< count,如遇到文件末尾) - 可用
feof()检查是否到达文件末尾,ferror()检查是否出错
示例
int data[4];
size_t read = fread(data, sizeof(int), 4, fp);
if (read < 4 && !feof(fp)) {
perror("Failed to read data");
}
| 函数 | 作用 | 关键参数 | 返回值 |
|---|---|---|---|
fopen |
打开文件 | filename, mode |
FILE*(失败返回 NULL) |
fclose |
关闭文件 | FILE* |
0(成功)或 EOF(失败) |
fwrite |
写入数据 | ptr, size, count, FILE* |
实际写入的数据项数量 |
fread |
读取数据 | ptr, size, count, FILE* |
实际读取的数据项数量 |
下面是 C语言 文件操作的一个综合代码示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
// 打开文件
FILE *fp = fopen("myfile", "w+");
if(!fp){
printf("fopen error\n");
}
const char *str = "hello, linux!";
// 写操作
fwrite(str, strlen(str), 1, fp);
char output[1024];
// 指针重置
rewind(fp);
// 读操作
int n = fread(output, sizeof(char), 1024, fp);
if(n > 0)
{
output[n] = '\0';
printf("%s\n", output);
exit(0);
}else{
printf("fread error\n");
}
// 关闭文件
fclose(fp);
return 0;
}
运行结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/lesson3
╰─➤ ./basic_io.o
hello, linux!
文件描述符
在 Linux 操作系统中,文件操作通过 open 函数等来实现,在 Linux 当中,操作系统对文件的管理同样遵循先描述再组织的方法,操作系统会维护一个文件结构体的双向链表,
而在进程结构体中同样有一个维护该进程的文件的结构体,这个结构体当中通过数组的方式维护一个个文件,而系统会将这些文件所对应的数组下标给用户使用,通过这些数组下标
就可以管理每一个文件,而这个数组下标就被称为文件描述符

这个图详细的表述了进程 PCB 中对文件的管理方式,每个进程的 PCB 中都会有一个 files_struct 结构体
文件描述符中 0 表示标准输入文件,1 表示标准输出文件,2 表示标准错误文件,这是因为这三个外设在进程启动的时候就直接是打开的,因此每个文件在运行过程中默认
是有权利和这几个文件打交道的,因此,当我们在不关闭文件描述符的情况下通过 open 函数打开文件的时候默认获得的文件描述符是 3
下面是对这三个默认文件描述符的介绍:
标准文件描述符
0(STDIN_FILENO):标准输入(键盘输入,默认来源)。1(STDOUT_FILENO):标准输出(屏幕输出,默认目标)。2(STDERR_FILENO):标准错误(屏幕输出,用于错误消息)。
文件描述符分配规则
- 文件描述符是进程级的,由内核动态分配,遵循 最小可用原则(从 0 开始查找第一个未被占用的 fd)。
- 如果
0、1、2未被关闭,新打开的文件会分配到3。 - 如果
0、1、2中有被关闭的,新打开的文件的 fd 会优先填补空缺。
下面来介绍一下 Linux 系统的文件相关函数:
1. open - 打开文件
函数原型
#include <fcntl.h>
#include <sys/stat.h>
int open(const char *pathname, int flags, mode_t mode);
参数说明
| 参数 | 说明 |
|---|---|
pathname |
文件路径(可以是相对路径或绝对路径) |
flags |
文件打开方式(必选 O_RDONLY、O_WRONLY、O_RDWR 之一,并可组合其他标志) |
mode |
文件权限(仅在 O_CREAT 时有效,如 0644) |
flags 常用选项
| 标志 | 说明 |
|---|---|
O_RDONLY |
只读 |
O_WRONLY |
只写 |
O_RDWR |
读写 |
O_CREAT |
如果文件不存在,则创建 |
O_TRUNC |
如果文件存在,清空内容 |
O_APPEND |
追加模式(写入时自动到文件末尾) |
O_NONBLOCK |
非阻塞模式(适用于设备文件或管道) |
返回值
- 成功:返回 文件描述符(fd)(最小可用的整数,通常从
3开始)。 - 失败:返回
-1,并设置errno(如ENOENT文件不存在,EACCES权限不足)。
示例
int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
exit(1);
}
需要注意的是,open 函数给的权限会受到权限掩码的影响,但我们可以给进程自定义 umask
2. close - 关闭文件
函数原型
#include <unistd.h>
int close(int fd);
参数说明
| 参数 | 说明 |
|---|---|
fd |
要关闭的文件描述符(由 open 返回) |
返回值
- 成功:返回
0。 - 失败:返回
-1(如EBADF表示无效的 fd)。
示例
if (close(fd) == -1) {
perror("close failed");
}
3. write - 写入数据
函数原型
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数说明
| 参数 | 说明 |
|---|---|
fd |
目标文件描述符 |
buf |
要写入的数据缓冲区 |
count |
要写入的字节数 |
返回值
- 成功:返回 实际写入的字节数(可能
< count,如磁盘满)。 - 失败:返回
-1(如EBADF无效 fd,ENOSPC磁盘空间不足)。
示例
const char *str = "Hello, Linux!";
ssize_t bytes_written = write(fd, str, strlen(str));
if (bytes_written == -1) {
perror("write failed");
}
4. read - 读取数据
函数原型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数说明
| 参数 | 说明 |
|---|---|
fd |
源文件描述符 |
buf |
存储读取数据的缓冲区 |
count |
要读取的最大字节数 |
返回值
- 成功:返回 实际读取的字节数(
0表示文件末尾)。 - 失败:返回
-1(如EBADF无效 fd,EINTR被信号中断)。
示例
char buf[1024];
ssize_t bytes_read = read(fd, buf, sizeof(buf));
if (bytes_read == -1) {
perror("read failed");
} else if (bytes_read == 0) {
printf("End of file\n");
} else {
buf[bytes_read] = '\0'; // 添加字符串终止符
printf("Read: %s\n", buf);
}
总结如下:
| 函数 | 作用 | 关键参数 | 返回值 |
|---|---|---|---|
open |
打开文件 | pathname, flags, mode |
文件描述符(fd)或 -1 |
close |
关闭文件 | fd |
0 或 -1 |
write |
写入数据 | fd, buf, count |
实际写入的字节数或 -1 |
read |
读取数据 | fd, buf, count |
实际读取的字节数或 -1 |
注意事项
文件描述符(fd):
0(STDIN_FILENO)、1(STDOUT_FILENO)、2(STDERR_FILENO)默认打开。- 新打开的 fd 从
3开始(除非标准 fd 被关闭)。
错误处理:
- 所有函数失败时返回
-1,并通过errno表示具体错误(用perror打印)。
- 所有函数失败时返回
阻塞与非阻塞:
- 默认是阻塞 I/O(可使用
O_NONBLOCK改为非阻塞模式)。
- 默认是阻塞 I/O(可使用
二进制 vs 文本:
- 这些函数直接操作字节流,适合二进制文件(如
write(&data, sizeof(data), 1, fd))。
- 这些函数直接操作字节流,适合二进制文件(如
缓冲区管理:
read/write不保证一次性读完/写完所有数据,需循环处理(尤其是网络编程)。
下面是一个对文件操作的综合示例:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
int main() {
// 权限掩码设置为 000
umask(0);
// 1. 打开文件(读写模式,不存在则创建)
int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
perror("open failed");
exit(1);
}
// 2. 写入数据
const char *str = "hello, Linux!";
ssize_t written = write(fd, str, strlen(str));
if (written == -1) {
perror("write failed");
}
// 3. 重置文件指针到开头(否则 read 会从末尾读取)
lseek(fd, 0, SEEK_SET);
// 4. 读取数据
char buf[1024];
ssize_t bytes_read = read(fd, buf, sizeof(buf));
if (bytes_read == -1) {
perror("read failed");
} else {
buf[bytes_read] = '\0';
printf("File content: %s\n", buf);
}
// 5. 关闭文件
if (close(fd) == -1) {
perror("close failed");
}
return 0;
}
打印结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/lesson3
╰─➤ ./basic_io.o
File content: hello, Linux!
查看文件权限验证权限是否受到了进程代码中 umask 函数的影响:
╭─ljx@VM-16-15-debian ~/linux_review/lesson3
╰─➤ ll
total 32
-rw-r--r-- 1 ljx ljx 1017 Jul 17 22:41 basic_io.c
-rwxr-xr-x 1 ljx ljx 16408 Jul 17 22:40 basic_io.o
-rw-r--r-- 1 ljx ljx 13 Jul 17 22:12 myfile
-rw-rw-rw- 1 ljx ljx 13 Jul 17 22:40 test.txt