文件读写及文件描述符

文件读写及文件描述符

七月 17, 2025 次阅读

文件存储在磁盘中,而磁盘是永久性的存储介质,因此文件在磁盘上的存储是永久性的,本质上对文件的所有操作,都是对外设的输入和输出,简称 IO

而在 Linux 下一切皆文件,所谓的键盘、显示器、网卡、磁盘等等都是抽象化的过程,在后面博客的讲解中将会慢慢理解这一点

即便是 0KB 的空文件也是会占用磁盘空间的,因为文件 = 属性 + 内容,所有的文件操作本质上是对文件内容的操作和文件属性的操作,而这种操作本质上又是进程对文件的操作,
磁盘的管理者是操作系统,C语言 等高级语言的库函数中的文件读写本质上是对操作系统给出的文件读写的系统调用接口的包装,方便用户使用

C语言文件操作回顾

在 C 语言中,fopenfclosefwritefread 是标准 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 当中,操作系统对文件的管理同样遵循先描述再组织的方法,操作系统会维护一个文件结构体的双向链表,
而在进程结构体中同样有一个维护该进程的文件的结构体,这个结构体当中通过数组的方式维护一个个文件,而系统会将这些文件所对应的数组下标给用户使用,通过这些数组下标
就可以管理每一个文件,而这个数组下标就被称为文件描述符

fd

这个图详细的表述了进程 PCB 中对文件的管理方式,每个进程的 PCB 中都会有一个 files_struct 结构体

文件描述符中 0 表示标准输入文件,1 表示标准输出文件,2 表示标准错误文件,这是因为这三个外设在进程启动的时候就直接是打开的,因此每个文件在运行过程中默认
是有权利和这几个文件打交道的,因此,当我们在不关闭文件描述符的情况下通过 open 函数打开文件的时候默认获得的文件描述符是 3

下面是对这三个默认文件描述符的介绍:

标准文件描述符

  • 0(STDIN_FILENO):标准输入(键盘输入,默认来源)。
  • 1(STDOUT_FILENO):标准输出(屏幕输出,默认目标)。
  • 2(STDERR_FILENO):标准错误(屏幕输出,用于错误消息)。

文件描述符分配规则

  • 文件描述符是进程级的,由内核动态分配,遵循 最小可用原则(从 0 开始查找第一个未被占用的 fd)。
  • 如果 012 未被关闭,新打开的文件会分配到 3
  • 如果 012 中有被关闭的,新打开的文件的 fd 会优先填补空缺。

下面来介绍一下 Linux 系统的文件相关函数:

1. open - 打开文件

函数原型

#include <fcntl.h>
#include <sys/stat.h>

int open(const char *pathname, int flags, mode_t mode);

参数说明

参数 说明
pathname 文件路径(可以是相对路径或绝对路径)
flags 文件打开方式(必选 O_RDONLYO_WRONLYO_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

注意事项

  1. 文件描述符(fd)

    • 0STDIN_FILENO)、1STDOUT_FILENO)、2STDERR_FILENO)默认打开。
    • 新打开的 fd 从 3 开始(除非标准 fd 被关闭)。
  2. 错误处理

    • 所有函数失败时返回 -1,并通过 errno 表示具体错误(用 perror 打印)。
  3. 阻塞与非阻塞

    • 默认是阻塞 I/O(可使用 O_NONBLOCK 改为非阻塞模式)。
  4. 二进制 vs 文本

    • 这些函数直接操作字节流,适合二进制文件(如 write(&data, sizeof(data), 1, fd))。
  5. 缓冲区管理

    • 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