Linux 基础 IO 初步解析:从 C 库函数到系统调用,理解文件操作本质

一. 重新理解 "文件":不止是磁盘中的文件

在 Linux 中,"文件" 的概念远比我们想象的宽泛,这是理解 IO 的前提:

  • 狭义文件 :磁盘上的永久性存储文件,由 属性(元数据)+ 内容 组成,即使是 0KB 的空文件,也会占用磁盘空间存储属性;
  • 广义文件Linux 下 "一切皆文件",键盘、显示器、网卡、进程等都被抽象为文件,统一通过 IO 接口操作,在之后的学习中会深入理解这一概念;
  • 系统角度 :文件操作的本质是 进程对文件的操作,磁盘由操作系统管理,任何文件读写最终都要通过系统调用接口实现,C 库函数只是封装层。

💡核心结论:无论何种文件,Linux 都通过统一的接口抽象处理,开发者无需关注底层设备差异。

重点看下面这个图来进行理解

二. 回顾 C 库文件 IO:我们常用的文件操作

C 语言提供了一套标准 IO 库函数,封装了底层系统调用,方便开发者使用。以下是最常用的文件操作场景,代码可直接编译运行。

2.1 文件打开与关闭

fopen用于打开文件,返回FILE*类型的文件指针;fclose用于关闭文件,释放资源。

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

int main() 
{
	// 补充:当前路径也是可以修改的
   //chdir("/home/whb");
   //char pwd[64];
   //getcwd(pwd, sizeof(pwd));
   //printf("cwd: %s\n", pwd);
   
    // 以只写模式打开文件,不存在则创建,存在则清空
    FILE *fp = fopen("log.txt", "w");
    if (!fp) 
    { 
    	// 打开失败判断
        perror("fopen");
        return 1;
    }

    // 文件操作...

    fclose(fp); // 关闭文件,必须调用
    return 0;
}
  • 打开模式r(只读)、w(只写,清空创建)、a(追加)、r+(读写)、w+(读写,清空创建)、a+(读写,追加);
  • 注意 :当前路径由进程的cwd(当前工作目录)决定,可通过/proc/[进程ID]/cwd查看。打开文件,本质是进程打开。所以,进程知道自己在哪里,即便文件不带具体路径,进程也知道。由此 OS 就能知道要创建的文件放在哪里。
  • 重点理解下面这些图:可以结合后面的其他示例帮助理解

2.2 文件写入:fwrite 的使用(附带其他几个)

fwrite用于向文件写入数据,适用于二进制文件和文本文件。

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

int main() 
{
    FILE *fp = fopen("load.txt", "w");
    if (!fp) 
    {
        perror("fopen");
        return 1;
    }

    const char *message = "hello Lotso!\n";
    int count = 5;
    // 循环写入5次,参数:数据地址、单次写入字节数、写入次数、文件指针
    while (count--) 
    {
        fwrite(message, strlen(message), 1, fp); // 不用 + 1, 不要管\0才是最佳实践
        // fputs(message, fp);
        // fprintf(fp, "hello Lotso: %d\n", cnt);
    }

    fclose(fp);
    return 0;
}

2.3 文件读取:fread 的使用(附带其它的)

fread用于从文件读取数据,需通过返回值判断读取结果。

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

int main() 
{
    FILE *fp = fopen("load.txt", "r");
    if (!fp) 
    {
        perror("fopen");
        return 1;
    }

    char buf[1024];
    const char *msg = "hello Lotso!\n";
    size_t msg_len = strlen(msg);

    while (1) 
    {
        // 读取数据,参数:缓冲区地址、单次读取字节数、读取次数、文件指针
        size_t s = fread(buf, 1, msg_len, fp);
        if (s > 0) 
        {
            buf[s] = '\0';
            printf("%s", buf);
        }
        // 判断是否到达文件末尾
        if (feof(fp)) 
        {
            break;
        }
    }

    fclose(fp);
    return 0;
}
cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
   FILE *fp = fopen("log.txt", "r");
   if(NULL == fp)
   {
       perror("fopen");
       return 0;
   }

   char inbuffer[1024];
   while(1)
   {
   		// ftell的使用
       // long pos = ftell(fp);
       // printf("pos: %ld\n", pos);
       // int ch = fgetc(fp);
       // if(ch == EOF)
       // {
       //     break;
       // }
       printf("%c\n", ch);
       if(!fgets(inbuffer, sizeof(inbuffer), fp))
       {
           break;
       }
       printf("file : %s", inbuffer);
   }

   fclose(fp);
   return 0;
}

2.4 标准输入输出:stdin、stdout、stderr

C 语言默认打开 3 个标准流,类型均为FILE*,对应系统的 3 个默认文件描述符:

  • stdin:标准输入(键盘),对应文件描述符 0;
  • stdout:标准输出(显示器),对应文件描述符 1;
  • stderr:标准错误(显示器),对应文件描述符 2。

示例:多种方式输出到显示器

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

int main() 
{
    const char *msg = "hello fwrite\n";
    // 三种向stdout输出的方式
    fwrite(msg, strlen(msg), 1, stdout); // 直接写标准输出
    printf("hello printf\n"); // 格式化输出
    fprintf(stdout, "hello fprintf\n"); // 指定stdout输出

    return 0;
}

三. Linux 系统调用 IO:IO 操作的底层接口

C 库函数方便但不是最底层,Linux 内核提供了一套系统调用接口,是所有 IO 操作的基础。以下是与 C 库对应的系统调用接口,功能更接近硬件和内核。

3.1 核心系统调用接口介绍

系统调用接口需要包含<sys/types.h><sys/stat.h><fcntl.h><unistd.h>等头文件,核心接口如下:

系统调用 功能描述 对应 C 库函数
open 打开 / 创建文件 fopen
read 从文件读取数据 fread
write 向文件写入数据 fwrite
close 关闭文件 fclose

3.2 open函数详解(重点, 后面还会再讲的)

open是最核心的系统调用之一(在本篇博客中我们先来简答了解一下,后面我们还会再提到它的),有两个函数原型,适配不同场景:

cpp 复制代码
// 1. 打开已存在的文件,无需创建
int open(const char *pathname, int flags);
// 2. 可能创建文件,需要指定权限
int open(const char *pathname, int flags, mode_t mode);

关键参数说明

  • pathname:文件路径(相对路径或绝对路径);
  • flags :打开方式标志,必须包含以下之一,可搭配其他标志使用:
    • 核心标志:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写);
    • 辅助标志:O_CREAT(文件不存在则创建)、O_APPEND(追加模式)、O_TRUNC(清空文件);
  • mode :文件权限(如06440755),仅当flags包含O_CREAT时有效;
  • 返回值 :成功返回文件描述符(非负整数),失败返回-1

权限说明

mode参数指定的是文件的 "默认权限",最终权限会被umask(权限掩码,以前就学习过了)修正,公式为:最终权限 = mode & ~umask 。举例:默认umask0022,因此mode=0666时,最终权限为0644。在下面我们还会再涉及到这个的,并且提到了一个就近原则。

小 demo 示例:(理解位图传参)

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

#define ONE (1<<0)   // 1
#define TWO (1<<1)   // 1
#define THREE (1<<2) // 4
#define FOUR (1<<3)  // 8
#define FIVE (1<<4)  // 16

void Print(int flags)
{
    if(flags & ONE)
        printf("ONE\n");
    if(flags & TWO)
        printf("TWO\n");
    if(flags & THREE)
        printf("THREE\n");
    if(flags & FOUR)
        printf("FOUR\n");
    if(flags & FIVE)
        printf("FIVE\n");
}

int main()
{
    Print(ONE);
    printf("\n");
    Print(TWO);
    printf("\n");
    Print(ONE | TWO);
    printf("\n");
    Print(ONE | TWO | THREE);
    printf("\n");
    Print(ONE | TWO | THREE | FOUR);
    printf("\n");
    Print(TWO | THREE | FOUR | FIVE);
}

3.3 系统调用实战:实现文件写入

openwriteclose实现与 C 库fopen相同的功能:

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

int main() 
{
    umask(0); // 清除默认权限掩码,确保创建文件权限正确
    // 打开文件:只写模式,不存在则创建,权限0666
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    if (fd < 0) 
    { 
    // 打开失败,fd为-1
        perror("open"); // 打印错误信息
        return 1;
    }

    const char *msg = "hello Lotso!\n";
    int len = strlen(msg);
    int count = 5;
    while (count--) 
    {
        // 写入数据:参数(文件描述符、数据地址、写入字节数)
        write(fd, msg, len);
    }

    close(fd); // 关闭文件,释放文件描述符
    return 0;
}

补充一个追加模式 :(类似于 a)

3.4 系统调用实战:实现文件读取

openreadclose实现文件读取:

cpp 复制代码
#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("load.txt", O_RDONLY);
    if (fd < 0) 
    {
        perror("open");
        return 1;
    }

    const char *msg = "hello Lotso!\n";
    int len = strlen(msg);
    char buf[1024];

    while (1) 
    {
        // 读取数据:参数(文件描述符、缓冲区地址、读取字节数)
        ssize_t s = read(fd, buf, len);
        if (s > 0) 
        { // 成功读取到s个字节
            printf("%s", buf);
        } 
        else 
        { 
        	// s=0表示文件末尾,s<0表示错误
            break;
        }
    }

    close(fd);
    return 0;
}

补充示例

cpp 复制代码
// cat file.txt
int main(int argc, char *argv[])
{
   if(argc != 2)
   {
       printf("Usage: %s filename\n", argv[0]); // ./myfile filename
       return 1;
   }
   int fd = open(argv[1], O_RDONLY);
   if(fd < 0)
   {
       perror("open");
       return 2;
   }

   char inbuffer[128];
   while(1)
   {
       ssize_t n = read(fd, inbuffer, sizeof(inbuffer)-1);
       if(n > 0)
       {
           inbuffer[n] = 0;
           printf("%s", inbuffer);
       }
       else if(n == 0)
       {
           printf("end of file!\n");
           break;
       }
       else
       {
           perror("read");
           break;
       }
   }

   close(fd);
   return 0;
}

四. C 库函数与系统调用的关系

很多人会混淆 C 库 IO 和系统调用,核心区别与联系如下:

  • 层级不同:C 库函数是用户态的封装,系统调用是内核态的接口,C 库函数最终会调用系统调用;
  • 功能不同:C 库函数提供了缓冲区、格式化等便捷功能,系统调用更底层,无额外封装;
  • 效率不同:系统调用涉及用户态与内核态的切换,开销较大,C 库通过缓冲区减少系统调用次数,提升效率;
  • 兼容性不同:C 库 IO 是跨平台的(Windows/Linux 通用),系统调用是 Linux 特有。
cpp 复制代码
用户程序 → C库IO(fopen/fwrite)→ 系统调用(open/write)→ 内核 → 硬件(磁盘/设备)

避坑指南(新手必看):

  • 文件关闭 :无论是fclose还是close,必须在文件操作完成后调用,否则会导致文件描述符泄漏、数据丢失;
  • 打开失败判断fopen返回NULLopen返回-1,必须检查返回值,用perror打印错误信息;
  • 权限问题 :创建文件时mode参数需配合O_CREAT使用,且要考虑umask的影响,避免权限不符合预期;
  • 缓冲区问题:C 库函数有用户态缓冲区,系统调用无缓冲区,后续会详细讲解缓冲区的影响。
相关推荐
袁煦丞 cpolar内网穿透实验室2 小时前
Portainer可视化玩转 Docker 全流程。cpolar 内网穿透实验室第 737 个成功挑战
运维·docker·容器·远程工作·内网穿透·cpolar
广州服务器托管2 小时前
WIN11中将控制面板固定到开始菜单的方法
运维·开发语言·windows·计算机网络·可信计算技术
啦啦啦~~~7542 小时前
文档压缩工具,支持支持PPT、Word、Doc、png图片等格式压缩!无限使用次数!优化效果达到85%杠杠的
服务器·windows·阿里云·电脑
每日学点SEO2 小时前
如何判断网站质量低 & 遭受机器人流量攻击
运维·人工智能·深度学习·机器学习·搜索引擎
暴力求解2 小时前
Linux---磁盘与文件系统(三)
linux·运维·服务器
deng-c-f2 小时前
Linux C/C++ 学习日记(80):Kafka(八):topic会自动创建吗?
linux·c++·学习·karfka
低保和光头哪个先来2 小时前
TinyEditor 篇3:拖拽图片到编辑器并同步上传至服务器
运维·服务器·编辑器
rain_in_spring2 小时前
十、项目:营销中心
linux·运维·服务器
小杍随笔2 小时前
【Rust `lib.rs` 使用方法:模块组织、API导出与最佳实践】
服务器·开发语言·rust