欢迎光临
我们一直在努力

Linux文件I/O与系统调用详解

摘要

在 Linux 操作系统中,“一切皆文件”(Everything is a file)是极其核心的设计哲学。除了普通的文本与二进制文件外,网络 Socket、管道、硬件设备(如串口、显示屏、触摸屏等)在应用层均被抽象为文件进行操作。

本文将聚焦于 Linux 系统层面的 文件 I/O(File I/O,又称系统调用 I/O),深入讲解其核心 API(open、read、write、lseek、close)、文件描述符(FD)的分配机制,并深度对比标准 I/O与文件 I/O在实现原理、缓冲区机制及适用场景上的本质区别。

一、什么是文件 I/O?

文件 I/O 是 Linux 内核直接提供的文件操作接口,属于系统调用(System Call)。

与 ANSI C 标准库提供的标准 I/O(如 fopen、fread、fwrite)不同,文件 I/O 不经过 C 库缓冲,直接由内核处理,因此实时性更高,常用于设备文件操作或底层的网络编程中。

标准 I/O vs 文件 I/O 架构对比

[file-tag: code-generated-file-fdtablepng]

二、关键概念:文件描述符(File Descriptor, FD)

在标准 I/O 中,我们使用文件流指针 FILE * 来标记打开的文件;而在文件 I/O 中,内核使用 文件描述符(fd) 来表示被打开的文件。

1. 文件描述符的本质

文件描述符是一个小的非负整数。

每当进程打开一个文件,内核会在进程的“文件描述符表”中分配一个索引,并将其返回给应用层。

Linux 操作系统默认可分配的文件描述符范围通常为 0 ~ 1023(共 1024 个)。

2. 系统默认打开的三个文件描述符

每个 Linux 进程在启动时,默认都已经打开了三个文件描述符,分别对应标准 I/O 中的三个流:

标准 I/O 文件流指针

文件 I/O 描述符

宏定义(<unistd.h>)

对应设备

stdin

0

STDIN_FILENO

标准输入(键盘)

stdout

1

STDOUT_FILENO

标准输出(显示屏/终端)

stderr

2

STDERR_FILENO

标准错误(显示屏/终端)

3. FD 的分配原则

最小未被使用原则:操作系统分配 FD 时,总是挑选当前列表中最小的、且未被占用的非负整数。

[file-tag: code-generated-file-fileioarchitecturepng]

三、文件 I/O 核心 API 详解

需要包含的头文件:

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

1. 打开/创建文件:open()

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

参数:

pathname: 要打开的文件路径。

flags: 打开方式(必选其一 + 可选组合):

O_RDONLY: 只读打开

O_WRONLY: 只写打开

O_RDWR: 读写打开

O_CREAT: 若文件不存在则创建

O_TRUNC: 打开时清空文件

O_APPEND: 追加写入

mode: 当创建新文件(指定 O_CREAT)时设置权限(如 0664)。

返回值:成功返回 文件描述符 (fd),失败返回 -1。

标准 I/O 与 文件 I/O 模式对应关系

标准 I/O (fopen)

文件 I/O 标志 (open flags)

说明

"r"

O_RDONLY

只读,文件必须存在

"r+"

O_RDWR

读写,文件必须存在

"w"

O_WRONLY | O_CREAT | O_TRUNC, 0664

只写,不存在则创建,存在则清空

"w+"

O_RDWR | O_CREAT | O_TRUNC, 0664

读写,不存在则创建,存在则清空

"a"

O_WRONLY | O_CREAT | O_APPEND, 0664

只写,不存在则创建,追加写入

"a+"

O_RDWR | O_CREAT | O_APPEND, 0664

读写,不存在则创建,末尾追加

2. 关闭文件:close()

int close(int fd);

功能:关闭指定的文件描述符,释放内核资源。

注意:如果程序中打开文件后未进行 close(),会导致 文件描述符泄漏(进程可用的 FD 资源被耗尽)。

3. 读取文件:read()

ssize_t read(int fd, void *buf, size_t count);

参数:

fd: 打开的文件描述符。

buf: 存放读取数据的内存首地址。

count: 期望读取的字节数。

返回值:

> 0: 实际读取到的字节数。

= 0: 读到文件末尾(EOF)。

-1: 读取失败。

4. 写入文件:write()

ssize_t write(int fd, const void *buf, size_t count);

参数:

fd: 打开的文件描述符。

buf: 待写入数据的内存首地址。

count: 期望写入的字节数。

返回值:成功返回实际写入的字节数,失败返回 -1。

5. 文件读写位置定位:lseek()

off_t lseek(int fd, off_t offset, int whence);

参数:

    • offset: 相对于起始位置的偏移量(字节,可正可负)。
    • whence: 基准位置:

SEEK_SET: 文件开头

SEEK_CUR: 当前读写位置

SEEK_END: 文件末尾

返回值:成功返回当前读写位置到文件开头的总字节数,失败返回 -1。

常用实用技巧:

1.获取文件大小:
off_t filesize = lseek(fd, 0, SEEK_END);

2.创建空洞文件(抢占磁盘空间):
int fd = open("hole.file", O_WRONLY | O_CREAT | O_TRUNC, 0664);
lseek(fd, 1024 * 1024 * 10, SEEK_SET); // 偏移到 10MB 处
write(fd, "\\0", 1); // 写入一个字节,形成空洞文件

四、实战演练:基于文件 I/O 实现文件复制(cp)

以下示例演示如何利用 open / read / write / close 实现高效的文件拷贝工具:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int file_copy(const char *srcfile, const char *dstfile)
{
    // 1. 打开源文件(只读)
    int fdsrc = open(srcfile, O_RDONLY);
    if (fdsrc == -1) {
        perror("open srcfile error");
        return -1;
    }

    // 2. 打开目标文件(只写,不存在则创建,存在则清空,权限0664)
    int fddst = open(dstfile, O_WRONLY | O_CREAT | O_TRUNC, 0664);
    if (fddst == -1) {
        perror("open dstfile error");
        close(fdsrc);
        return -1;
    }

    // 3. 循环读写
    char buff[1024] = {0};
    ssize_t size = 0;
    while ((size = read(fdsrc, buff, sizeof(buff))) > 0) {
        write(fddst, buff, size);
    }

    // 4. 关闭文件描述符
    close(fdsrc);
    close(fddst);

    return 0;
}

int main(int argc, const char *argv[])
{
    if (argc < 3) {
        printf("Usage: %s <srcfile> <dstfile>\\n", argv[0]);
        return -1;
    }

    if (file_copy(argv[1], argv[2]) == 0) {
        printf("File copy successfully!\\n");
    }

    return 0;
}

五、文件 I/O 与 标准 I/O 全方位对比

对比维度

标准 I/O (Standard I/O)

文件 I/O (File I/O)

操作对象

FILE *(文件流指针)

int fd(文件描述符)

实现层级

ANSI C 标准库函数(高层抽象)

Linux 内核系统调用(底层接口)

可移植性

高(支持 POSIX、Windows、macOS 等)

弱(特定于 POSIX / Linux 系统)

缓冲区机制

有缓冲区(行缓冲、全缓冲、无缓冲),减少内核切换,提高小数据频繁读写效率

无用户缓冲区,实时性高

适用场景

普通文本/二进制文件的顺序/格式化读写

设备文件(串口、网络 Socket、驱动硬件)

补充:标准 I/O 的三种缓冲区模式

1.行缓冲(Line Buffered):大小通常为 1024 字节 (1KB)。常见于终端设备(stdin、stdout)。刷新条件:遇到 \\n、缓冲区满、程序结束或调用 fflush()。

2.全缓冲(Fully Buffered):大小通常为 4096 字节 (4KB)。常见于普通磁盘文件操作。刷新条件:缓冲区满、关闭文件、程序结束或调用 fflush()。

3.无缓冲(Unbuffered):数据直接写入目的地,如标准错误流 stderr。

六、辅助拓展函数:格式化与字符串截取

在 Linux C 编程中,结合文件操作常常用到以下辅助函数:

1. 格式化输出函数

fprintf(FILE *stream, const char *format, …):向指定流写入格式化数据。

dprintf(int fd, const char *format, …):向指定文件描述符写入格式化数据。

sprintf(char *str, const char *format, …):格式化输出到字符串数组中,用于字符串拼接与格式转换。

2. 字符串截取:strtok()

char *strtok(char *str, const char *delim);

功能:根据分隔符 delim 切割字符串 str。

注意:strtok 会修改原字符串(将分隔符替换为 \\0),首次调用传 str,后续继续切割需传 NULL。

总结

1.Linux 下一切皆文件,硬件设备、文件、管道、Socket 均可使用文件 I/O 统一操作。

2.文件 I/O 返回并使用的是 文件描述符(fd),分配遵从最小未被使用原则。

3.进行普通文件读写优先考虑标准 I/O(含缓冲区,减少系统调用开销);而在硬件交互、设备驱动、实时性要求高的场景下,则应使用文件 I/O。

赞(0)
未经允许不得转载:171主机测评 » Linux文件I/O与系统调用详解
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址