【Linux系统编程】基础IO:从文件本质到系统操作


❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生

✨专注 C/C++ Linux 数据结构 算法竞赛 AI

🏞️志同道合的人会看见同一片风景!

👇点击进入作者专栏:

《算法画解》

《linux系统编程》

《C++》

🌟《算法画解》算法相关题目点击即可进入实操🌟

感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!

文章目录

  • [📖 前言](#📖 前言)
  • [📁 一、深入理解文件](#📁 一、深入理解文件)
    • [1.1 文件的独特认识](#1.1 文件的独特认识)
    • [1.2 文件操作的认识](#1.2 文件操作的认识)
  • [💻 二、C文件接口使用](#💻 二、C文件接口使用)
    • [2.1 文件的打开与路径](#2.1 文件的打开与路径)
    • [2.2 文件的写入操作](#2.2 文件的写入操作)
    • [2.3 文件的读取操作](#2.3 文件的读取操作)
    • [2.4 输出信息到显示器的多种方法](#2.4 输出信息到显示器的多种方法)
    • [2.5 标准输入输出流](#2.5 标准输入输出流)
  • [🚗 三、系统文件I/O](#🚗 三、系统文件I/O)
    • [3.1 标志位传递方法](#3.1 标志位传递方法)
    • [3.2 系统级文件写操作](#3.2 系统级文件写操作)
    • [3.3 系统级文件读操作](#3.3 系统级文件读操作)
    • [3.4 系统文件I/O接口详解](#3.4 系统文件I/O接口详解)
    • [3.5 open函数返回值与文件描述符](#3.5 open函数返回值与文件描述符)
    • [3.6 综合示例:文件复制工具](#3.6 综合示例:文件复制工具)
  • [🎓 总结](#🎓 总结)

📖 前言

在计算机科学的核心领域,文件系统扮演着承上启下的关键角色------它既是应用程序数据存储的物理载体,也是操作系统资源管理的抽象接口。当我们谈及文件操作时,很多人会停留在C语言库函数的表面应用,却鲜少深入探究其背后的系统机制。从"一切皆文件"的Linux设计哲学,到文件描述符的精妙抽象,再到系统调用与库函数的本质区别,文件I/O的世界远比表面看起来更加深邃。本文将从底层原理到实践应用,层层剖析文件系统的奥秘,带你穿越抽象屏障,直抵操作系统内核,建立对文件I/O的完整认知体系,为后续深入系统编程奠定坚实基础。

📁 一、深入理解文件

1.1 文件的独特认识

1.狭义:

文件在磁盘中;磁盘是永久性的存储介质,从而文件在磁盘上的存储是永久的;磁盘是外设(输入/输出设备);对磁盘上文件的操作本质是对文件的操作,也都是对外设的输入输出:IO。

2,广义:

"Linux下一切皆文件"是核心抽象设计: 将硬件、进程、网络等资源统一抽象为具有路径、可通过标准文件操作(open/read/write/close)访问的对象。

主要体现: 设备文件(/dev/):磁盘(/dev/sda)、终端(/dev/tty)、输入设备等。

进程信息(/proc/):每个进程以目录形式暴露其状态、内存、命令行参数。

系统控制(/sys/):内核参数、设备属性可通过文件读写动态调整。

实现原理: 内核通过虚拟文件系统(VFS) 层统一接口,驱动或子系统实现 read、write 等标准操作,将底层操作转化为文件语义。

1.2 文件操作的认识

文件 = 属性 + 内容

因此,所有对文件的操作本质是对文件内容操作文件属性操作

从系统角度看,对文件的操作本质是进程 对文件的操作;而磁盘的管理者是操作系统

文件的读写本质不是通过C语言/C++的库函数(语言层)来实现操作的,而是通过通过文件相关的系统调用接口 来实现的,库函数 是为了用户提供方便。

库函数也都封装了底层的文件系统调用;并且OS要把被打开的文件管理起来。

💻 二、C文件接口使用

2.1 文件的打开与路径

示例代码:打开文件

c 复制代码
#include <stdio.h>

int main()
{
    FILE *fp = fopen("myfile", "w");
    if(!fp){
        printf("fopen error!\n");
    }
    while(1);  // 为了让进程持续运行,方便查看信息
    fclose(fp);
    return 0;
}

问题:打开的myfile文件在哪个路径下?

答案: 在程序的当前工作目录下

如何查看程序的当前路径?

在Linux系统中,可以通过查看/proc/[进程id]目录来获取进程的相关信息:

首先获取进程ID:

bash 复制代码
ps aux | grep hello
查看进程信息:
bash 复制代码
ls /proc/[进程id] -l
关键符号链接:

cwd:指向当前进程运行目录的符号链接(current working directory)

exe:指向启动当前进程的可执行文件的完整路径

理解:

当进程创建时,它会继承父进程的当前工作目录。当进程使用相对路径打开文件时,操作系统会将该相对路径与进程的当前工作目录拼接,形成完整的文件路径。

2.2 文件的写入操作

示例代码:hello.c写文件

c 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    // 以写入模式("w")打开文件,如果文件不存在则创建,存在则清空
    FILE *fp = fopen("myfile", "w");
    if(!fp){
        printf("fopen error!\n");
        return 1;
    }
    
    const char *msg = "hello bit!\n";
    int count = 5;
    
    // 循环写入5次
    while(count--){
        // fwrite参数:数据指针,每个元素大小,元素个数,文件指针
        fwrite(msg, strlen(msg), 1, fp);
    }
    
    fclose(fp);
    return 0;
}

关键点:

"w"模式:如果文件存在则清空内容,不存在则创建

fwrite()函数将数据写入文件

写入完成后必须调用fclose()关闭文件,释放资源

2.3 文件的读取操作

示例代码:hello.c读文件

c 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    // 以只读模式("r")打开文件
    FILE *fp = fopen("myfile", "r");
    if(!fp){
        printf("fopen error!\n");
        return 1;
    }
    
    char buf[1024];
    const char *msg = "hello bit!\n";
    
    while(1){
        // fread参数:缓冲区,每个元素大小,元素个数,文件指针
        // 注意:这里使用strlen(msg)作为读取大小,可能不是最优选择
        ssize_t s = fread(buf, 1, strlen(msg), fp);
        
        if(s > 0){
            buf[s] = '\0';  // 添加字符串结束符
            printf("%s", buf);
        }
        
        // 检查是否到达文件末尾
        if(feof(fp)){
            break;
        }
    }
    
    fclose(fp);
    return 0;
}

改进版:实现简单cat命令

c 复制代码
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        printf("Usage: %s <filename>\n", argv[0]);
        return 1;
    }
    
    FILE *fp = fopen(argv[1], "r");
    if(!fp){
        printf("fopen error!\n");
        return 2;
    }
    
    char buf[1024];
    while(1){
        // 改进:使用sizeof(buf)确保读取完整缓冲区
        int s = fread(buf, 1, sizeof(buf), fp);
        
        if(s > 0){
            buf[s] = '\0';
            printf("%s", buf);
        }
        
        if(feof(fp)){
            break;
        }
    }
    
    fclose(fp);
    return 0;
}

2.4 输出信息到显示器的多种方法

c 复制代码
#include <stdio.h>
#include <string.h>

int main()
{
    const char *msg = "hello fwrite\n";
    
    // 方法1:使用fwrite向stdout写入
    fwrite(msg, strlen(msg), 1, stdout);
    
    // 方法2:使用printf函数
    printf("hello printf\n");
    
    // 方法3:使用fprintf向stdout写入
    fprintf(stdout, "hello fprintf\n");
    
    // 方法4:使用puts函数(自动添加换行符)
    puts("hello puts");
    
    // 方法5:使用putchar逐个字符输出
    const char *str = "hello putchar\n";
    while(*str){
        putchar(*str++);
    }
    
    return 0;
}

2.5 标准输入输出流

标准流的声明

c 复制代码
#include <stdio.h>

// 这三个是C语言默认打开的标准流
extern FILE *stdin;   // 标准输入流(默认连接到键盘)
extern FILE *stdout;  // 标准输出流(默认连接到显示器)
extern FILE *stderr;  // 标准错误流(默认连接到显示器)

标准流的特点 自动打开:程序启动时自动打开,无需手动fopen

不可关闭:尝试关闭这些流可能导致未定义行为

可重定向:可以通过shell重定向到文件或其他设备

bash 复制代码
./program > output.txt      # 标准输出重定向到文件
./program 2> error.txt      # 标准错误重定向到文件
./program < input.txt       # 标准输入重定向从文件读取
模式 描述 文件存在 文件不存在 读写位置
"r" 只读 打开成功 打开失败 文件开头
"r+" 读写 打开成功 打开失败 文件开头
"w" 只写 清空内容 创建新文件 文件开头
"w+" 读写 清空内容 创建新文件 文件开头
"a" 追加 打开成功 创建新文件 文件末尾
"a+" 读和追加 打开成功 创建新文件 读-开头,写-末尾

文件位置操作函数:

c 复制代码
#include <stdio.h>

int main()
{
    FILE *fp = fopen("test.txt", "r+");
    if(!fp) return 1;
    
    // ftell:获取当前文件位置
    long pos = ftell(fp);
    printf("Current position: %ld\n", pos);
    
    // fseek:移动文件位置指针
    // SEEK_SET:从文件开头
    // SEEK_CUR:从当前位置
    // SEEK_END:从文件末尾
    fseek(fp, 10, SEEK_SET);  // 移动到离文件开头10字节处
    fseek(fp, -5, SEEK_END);  // 移动到离文件末尾5字节处
    
    // rewind:将文件位置指针重置到文件开头
    rewind(fp);
    
    fclose(fp);
    return 0;
}

🚗 三、系统文件I/O

3.1 标志位传递方法

在深入系统文件I/O之前,我们需要先理解一种在系统编程中常用的技巧:通过位运算传递多个标志位。这种方法在系统文件IO接口中被广泛使用。

c 复制代码
#include <stdio.h>

// 使用二进制位定义标志位(注意:这里应该使用16进制或8进制)
#define ONE    0x01    // 0000 0001
#define TWO    0x02    // 0000 0010
#define FOUR   0x04    // 0000 0100
#define EIGHT  0x08    // 0000 1000

void func(int flags) {
    // 使用位与(&)运算检查特定标志位是否被设置
    if (flags & ONE)   printf("flags has ONE! ");
    if (flags & TWO)   printf("flags has TWO! ");
    if (flags & FOUR)  printf("flags has FOUR! ");
    if (flags & EIGHT) printf("flags has EIGHT! ");
    printf("\n");
}

int main() {
    // 测试单一标志位
    func(ONE);
    // 输出: flags has ONE!
    
    // 测试另一个单一标志位
    func(FOUR);
    // 输出: flags has FOUR!
    
    // 测试多个标志位组合(使用位或|运算)
    func(ONE | TWO);
    // 输出: flags has ONE! flags has TWO!
    
    // 测试三个标志位的组合
    func(ONE | FOUR | EIGHT);
    // 输出: flags has ONE! flags has FOUR! flags has EIGHT!
    
    return 0;
}

关键点解析:

使用二进制位表示不同的标志位,每个位代表一个独立的选项

通过|(位或)运算组合多个标志位

通过&(位与)运算检查特定标志位是否被设置

这种方法可以高效地传递多个布尔选项,节省内存和参数传递开销

3.2 系统级文件写操作

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 设置文件创建时的权限掩码
    // umask(0)表示不清除任何权限位
    umask(0);
    
    // 打开文件:O_WRONLY只写模式 | O_CREAT如果不存在则创建
    // 第三个参数0644表示文件权限:rw-r--r--
    int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
    if(fd < 0){
        perror("open");  // 打印系统错误信息
        return 1;
    }
    
    int count = 5;
    const char *msg = "hello bit!\n";
    int len = strlen(msg);
    
    // 循环写入5次
    while(count--){
        // write系统调用:
        // fd: 文件描述符(后面详细介绍)
        // msg: 要写入数据的缓冲区首地址
        // len: 期望写入的字节数
        // 返回值: 实际写入的字节数(可能小于len)
        ssize_t ret = write(fd, msg, len);
        if(ret < 0){
            perror("write");
            break;
        }
    }
    
    close(fd);  // 关闭文件描述符
    return 0;
}

3.3 系统级文件读操作

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    // 以只读模式打开文件
    int fd = open("myfile", O_RDONLY);
    if(fd < 0){
        perror("open");
        return 1;
    }
    
    const char *msg = "hello bit!\n";
    char buf[1024];
    
    while(1){
        // read系统调用:
        // fd: 文件描述符
        // buf: 读取数据的缓冲区
        // strlen(msg): 期望读取的字节数
        // 返回值: 实际读取的字节数(0表示文件末尾,-1表示错误)
        ssize_t s = read(fd, buf, strlen(msg));
        
        if(s > 0){
            buf[s] = '\0';  // 确保字符串正确结束
            printf("%s", buf);
        } else if(s == 0) {
            // 到达文件末尾
            break;
        } else {
            // 读取错误
            perror("read");
            break;
        }
    }
    
    close(fd);
    return 0;
}

3.4 系统文件I/O接口详解

open函数

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// open函数的两种形式
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

参数解析:

pathname: 要打开或创建的目标文件路径

flags: 打开文件的标志位(使用位或运算组合)

常用flags标志位:

c 复制代码
// 基本访问模式(必须且只能指定一个)
O_RDONLY    // 只读打开
O_WRONLY    // 只写打开
O_RDWR      // 读写打开

// 可选标志位(可以组合使用)
O_CREAT     // 文件不存在时创建
O_APPEND    // 追加模式(总是在文件末尾写入)
O_TRUNC     // 如果文件存在且为普通文件,将其长度截断为0
O_EXCL      // 与O_CREAT一起使用,如果文件存在则失败
O_NONBLOCK  // 非阻塞模式
O_SYNC      // 同步写入(数据立即写入磁盘)
mode: 当使用O_CREAT创建新文件时,指定文件的访问权限

权限模式说明:

权限使用8进制表示,如0644

0644表示:所有者rw-(6),组用户r--(4),其他用户r--(4)

实际权限 = mode & ~umask(umask为权限掩码)

返回值:

成功:返回新的文件描述符(非负整数)

失败:返回-1,并设置errno

其他系统文件I/O接口

c 复制代码
#include <unistd.h>

// 写入数据
ssize_t write(int fd, const void *buf, size_t count);

// 读取数据
ssize_t read(int fd, void *buf, size_t count);

// 关闭文件描述符
int close(int fd);

// 移动文件读写位置
off_t lseek(int fd, off_t offset, int whence);
// whence取值:
// SEEK_SET:从文件开头偏移
// SEEK_CUR:从当前位置偏移
// SEEK_END:从文件末尾偏移

3.5 open函数返回值与文件描述符

系统调用 vs 库函数:

特性 系统调用) 库函数)
定义 操作系统内核提供的接口 编程语言标准库提供的函数
示例 open, read, write, close fopen, fread, fwrite, fclose
位置 位于操作系统内核空间 位于用户空间库中
开销 较大(需要上下文切换) 较小(在用户空间执行)
封装关系 库函数通常封装系统调用 系统调用是更底层的接口

文件描述符(File Descriptor)

c 复制代码
int main() {
    // 打开一个文件,返回文件描述符
    int fd = open("test.txt", O_RDONLY);
    printf("File descriptor: %d\n", fd);  // 通常为3
    
    // 标准文件描述符(始终存在)
    printf("stdin fd: %d\n", STDIN_FILENO);   // 0
    printf("stdout fd: %d\n", STDOUT_FILENO); // 1
    printf("stderr fd: %d\n", STDERR_FILENO); // 2
    
    close(fd);
    return 0;
}

文件描述符的本质:

数组索引:文件描述符是进程文件描述符表(file descriptor table)的索引

进程私有:每个进程有自己的文件描述符表

从0开始:0,1,2已被标准输入、输出、错误占用

资源管理:操作系统通过文件描述符管理打开的文件

文件描述符表结构:

c 复制代码
// 伪代码表示进程的文件描述符表
struct process_fd_table {
    struct file_descriptor *entries[1024];  // 通常1024个条目
    int next_free_fd;                      // 下一个可用的文件描述符
};

// 当open成功时
int open_file(const char *pathname, int flags) {
    // 1. 在内核中创建或找到对应的文件对象
    // 2. 在进程的文件描述符表中分配一个空闲位置
    // 3. 将文件对象指针存储到该位置
    // 4. 返回该位置的索引(文件描述符)
}

3.6 综合示例:文件复制工具

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_SIZE 4096

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
        return 1;
    }
    
    // 打开源文件(只读)
    int src_fd = open(argv[1], O_RDONLY);
    if (src_fd < 0) {
        perror("open source file");
        return 1;
    }
    
    // 打开目标文件(创建或截断,读写权限)
    int dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (dst_fd < 0) {
        perror("open destination file");
        close(src_fd);
        return 1;
    }
    
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read, bytes_written;
    
    // 循环读取和写入
    while ((bytes_read = read(src_fd, buffer, sizeof(buffer))) > 0) {
        bytes_written = write(dst_fd, buffer, bytes_read);
        if (bytes_written != bytes_read) {
            perror("write");
            close(src_fd);
            close(dst_fd);
            return 1;
        }
    }
    
    if (bytes_read < 0) {
        perror("read");
    }
    
    // 关闭文件描述符
    close(src_fd);
    close(dst_fd);
    
    printf("File copied successfully!\n");
    return 0;
}

🎓 总结

本文系统性地构建了对Linux文件操作的全景认知。从"一切皆文件"的核心抽象哲学出发,揭示了文件不仅是数据存储单元,更是操作系统统一管理各类资源的接口。通过对比C语言标准库函数与系统调用,我们深入理解了文件操作从用户空间到内核空间的完整流程。文件描述符机制作为进程与文件之间的桥梁,完美体现了操作系统的资源管理智慧。无论是基础的读写操作,还是高级的标志位组合使用,系统文件I/O都展现出比语言层接口更强大的灵活性和控制力。掌握这些知识,不仅是学习系统编程的基础,更是理解操作系统工作原理的关键一步。

加油!志同道合的人会看到同一片风景。

看到这里请点个赞关注 ,如果觉得有用就收藏一下吧。后续还会持续更新的。 创作不易,还请多多支持!

相关推荐
GS8FG2 小时前
鲁班猫2,lubancat2,linux sdk4.19整编出现的镜像源的问题修复
linux
_OP_CHEN2 小时前
【Linux系统编程】(二十六)一文吃透 Ext 系列文件系统软硬链接:原理、实战与底层逻辑揭秘
linux·操作系统·文件系统·c/c++·硬链接·软链接·ext2文件系统
RisunJan2 小时前
Linux命令-lp(打印文件或修改排队的打印任务)
linux·运维·服务器
奋斗者1号2 小时前
OpenClaw 部署方式对比:云端、WSL、Mac 本机、Ubuntu 虚拟机(2026年2月最新主流实践)
linux·ubuntu·macos
一只自律的鸡2 小时前
【Linux驱动】环境搭建和开发板操作 上篇
linux·服务器
一个人旅程~2 小时前
电脑启动分区表MBR到GPT以及BIOS到UEFI如何区分操作?
linux·windows·电脑
空空空空空空空空空空空空如也2 小时前
QT通过编译宏区分x86 linux arm的方法
linux·开发语言·qt
小猪写代码2 小时前
Linux核心梳理
linux·运维·服务器
Cx330❀2 小时前
深入理解 Linux 基础 IO:从 C 库到系统调用的完整剖析
linux·运维·服务器·c语言·数据库·人工智能·科技