
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- [一. 重新理解 "文件":不止是磁盘中的文件](#一. 重新理解 “文件”:不止是磁盘中的文件)
- [二. 回顾 C 库文件 IO:我们常用的文件操作](#二. 回顾 C 库文件 IO:我们常用的文件操作)
-
- [2.1 文件打开与关闭](#2.1 文件打开与关闭)
- [2.2 文件写入:fwrite 的使用(附带其他几个)](#2.2 文件写入:fwrite 的使用(附带其他几个))
- [2.3 文件读取:fread 的使用(附带其它的)](#2.3 文件读取:fread 的使用(附带其它的))
- [2.4 标准输入输出:stdin、stdout、stderr](#2.4 标准输入输出:stdin、stdout、stderr)
- [三. Linux 系统调用 IO:IO 操作的底层接口](#三. Linux 系统调用 IO:IO 操作的底层接口)
-
- [3.1 核心系统调用接口介绍](#3.1 核心系统调用接口介绍)
- [3.2 open函数详解(重点, 后面还会再讲的)](#3.2 open函数详解(重点, 后面还会再讲的))
- [3.3 系统调用实战:实现文件写入](#3.3 系统调用实战:实现文件写入)
- [3.4 系统调用实战:实现文件读取](#3.4 系统调用实战:实现文件读取)
- [四. C 库函数与系统调用的关系](#四. C 库函数与系统调用的关系)
- 结尾:
前言:
文件操作是 Linux 开发的核心基础,无论是磁盘文件读写、设备交互还是网络通信,最终都离不开 IO 操作。我们平时常用的
fopen、fwrite等 C 库函数,本质上都是对系统调用的封装。理解 C 库 IO 与系统调用的关系、文件描述符的分配规则,是掌握 Linux IO 的关键。本文从文件的本质出发,先回顾 C 库文件 IO,再简单初步认识系统调用接口,结合可直接运行的代码示例,帮你理清 IO 操作的底层逻辑,为后续深入理解系统调用、重定向、缓冲区等高级特性打下基础。
一. 重新理解 "文件":不止是磁盘中的文件
在 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 :文件权限(如
0644、0755),仅当flags包含O_CREAT时有效; - 返回值 :成功返回文件描述符(非负整数),失败返回-
1。
权限说明 :
mode参数指定的是文件的 "默认权限",最终权限会被umask(权限掩码,以前就学习过了)修正,公式为:最终权限 = mode & ~umask 。举例:默认umask为0022,因此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 系统调用实战:实现文件写入
用open、write、close实现与 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 系统调用实战:实现文件读取
用open、read、close实现文件读取:
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 特有。

html
用户程序 → C库IO(fopen/fwrite)→ 系统调用(open/write)→ 内核 → 硬件(磁盘/设备)
避坑指南(新手必看):
- 文件关闭 :无论是
fclose还是close,必须在文件操作完成后调用,否则会导致文件描述符泄漏、数据丢失; - 打开失败判断 :
fopen返回NULL,open返回-1,必须检查返回值,用perror打印错误信息; - 权限问题 :创建文件时
mode参数需配合O_CREAT使用,且要考虑umask的影响,避免权限不符合预期; - 缓冲区问题:C 库函数有用户态缓冲区,系统调用无缓冲区,后续会详细讲解缓冲区的影响。
结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:本文覆盖了 Linux 基础 IO 的核心内容:从文件的本质,到 C 库 IO 的常用操作,再到系统调用的底层实现,帮你搭建起 IO 操作的知识框架。理解 C 库与系统调用的关系、open函数的参数细节,是后续学习重定向、缓冲区、高级 IO 的基础。接下来可以深入学习文件描述符的分配规则、重定向原理,以及缓冲区的工作机制。如果需要进一步巩固,可尝试用系统调用实现cat、cp等简单工具。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
