1. 理解"文件"
1.1 狭义理解
- 文件在磁盘里
- 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的
- 磁盘是外设
- 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,简称IO
1.2 广义理解
- Linux下一切皆文件
- 对于 0KB 的空文件是占用磁盘空间的(文件 = 属性 + 内容)
1.3 系统角度
- 对文件的操作本质是进程对文件的操作
- 磁盘的管理者是操作系统
- 文件的读写本质不是通过 C语言/ C++ 的库函数来操作的,而是通过文件相关的系统调用接口来实现的
2. C文件接口
2.1 打开文件
cs
#include <stdio.h>
#include <errno.h> // 包含errno定义(perror依赖)
int main()
{
FILE* fd = fopen("log.txt", "w");
if (!fd) {
perror("fopen failed"); // 传入字符串作为提示
return 1;
}
fclose(fd);
return 0;
}
2.2 写入文件
write 和 fwrite 函数
cs
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
write参数说明
fd:文件描述符(非负整数),标识打开的文件 / 设备 /socket。常见值:0:标准输入(STDIN_FILENO)1:标准输出(STDOUT_FILENO)2:标准错误(STDERR_FILENO)- 其他值由
open()函数返回(打开文件后获得)
buf:指向要写入数据的缓冲区(const 修饰,不可修改)count:请求写入的字节数 (size_t是无符号整数)
返回值
- 成功:返回 实际写入的字节数 (可能小于
count,如磁盘满、网络拥堵) - 失败:返回
-1,并设置errno(错误码,需用<errno.h>解析)
cs
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite参数说明
ptr:指向要写入文件的数据的起始地址(即数据源的指针)size:单个数据块的字节数 (如sizeof(int)、sizeof(struct Student))。nmemb:请求写入的数据块个数。stream:文件指针(由fopen()返回),常见值:stdout:标准输出stderr:标准错误- 其他值由
fopen()打开文件获得(如FILE *fp = fopen("test.txt", "wb"))。
返回值
- 成功:返回 实际写入的完整数据块数 (而非字节数)。
- 总写入字节数 = 实际块数 ×
size。
- 总写入字节数 = 实际块数 ×
- 失败 / 到达文件尾:返回 小于
nmemb的数 (需通过ferror()或feof()判断原因
cs
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fd = fopen("log.txt", "w");
if (!fd) {
perror("fopen");
return 1;
}
const char *msg = "abcd"; // 要写入的有效字符串
int cnt = 5;
while (cnt--) {
fwrite(msg, strlen(msg), 1, fd);
}
fclose(fd);
return 0;
}
2.3 读取文件
fread 和 read 函数
cs
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
- read的核心:从一个「数据来源」读取字节流,存入程序的内存缓冲区
- read参数说明:
fd:文件描述符(非负整数,如open返回的 fd,标准输入是 0,标准输出是 1,标准错误是 2);buf:接收数据的缓冲区指针;count:要读取的最大字节数。
- 返回值:成功:返回实际读取的字节数 (可能小于
count,如文件尾、管道无足够数据);失败:返回 -1(errno存储错误码,如EBADF表示 fd 无效);0:表示到达文件尾(EOF
cs
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
- fread参数说明:
ptr:接收数据的缓冲区指针;size:每个元素的字节数 (如sizeof(int));nmemb:要读取的元素个数;stream:文件流指针(如fopen返回的 FILE*)。
- 返回值:成功读取的元素个数 (不是字节数!);若到达文件尾,返回值可能小于
nmemb;若出错,返回值不确定,需用ferror(stream)检查
cs
#include<stdio.h>
#include<unistd.h>
#include <string.h>
int main()
{
FILE* fd = fopen("log.txt","r");
if(!fd){
perror("fopen");
}
char buff[1024];
const char *msg ="abcd";
while(1)
{
ssize_t s= fread(buff,1,strlen(msg),fd);
if(s>0)
{
buff[s]=0;
printf("%s",buff);
}
if (feof(fd)) {
break;
}
}
fclose(fd);
return 0;
}
2.4 stdin & stdout & stderr
-
C默认会打开三个输⼊输出流,分别是stdin, stdout, stderr
-
这三个流的类型都是FILE*, fopen返回值类型,文件指针
cs#include <stdio.h> extern FILE *stdin; extern FILE *stdout; extern FILE *stderr;2.5 打开文件的方式
|------|---------------------|-------------------|
| "r" | 只读打开 | 读取已存在的文件 |
| "w" | 只写打开 | 新建文件并写入,或覆盖已有文件 |
| "a" | 追加写入 | 向文件末尾添加内容 |
| "r+" | 读写打开 | 既要读又要写已存在的文件 |
| "w+" | 读写打开 | 新建文件读写,或覆盖已有文件后读写 |
| "a+" | 追加读写(append + read) | 向文件末尾追加内容,同时可读取文件 |
3. 系统文件I/O
3.1 一种传递标志位的方法
cs
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flags) {
if (flags & ONE) printf("flags has ONE! ");
if (flags & TWO) printf("flags has TWO! ");
if (flags & THREE) printf("flags has THREE! ");
printf("\n");
}
int main() {
func(ONE);
func(THREE);
func(ONE | TWO);
func(ONE | THREE | TWO);
return 0;
}
3.2 写文件
cs
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10 umask(0); // 消除系统默认umask的影响
11 int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
12 if(fd<0)
13 {
14 perror("open");
15 return 1;
16 }
17 int cnt =5;
18 const char* msg="straykids";
19
20 while(cnt)
21 {
22 write(fd,msg,strlen(msg));
23 cnt--;
24 }
25 close(fd);
26 return 0;
27 }
3.3 读文件
cs
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10 int fd=open("log.txt",O_RDONLY);
11 if(fd<0)
12 {
13 perror("open");
14 return 1;
15 }
16
17 char buffer[100];
18 while(1)
19 {
20 ssize_t s =read(fd,buffer,sizeof(buffer));
21 if(s>0)
22 {
23 printf("%s",buffer);
24 }
25 else{
26 break;
27 }
28 }
29 close(fd);
30 return 0;
31 }
3.4 接口open介绍
man 2 open:
cs
1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <fcntl.h>
4 int open(const char *pathname, int flags);
5 int open(const char *pathname, int flags, mode_t mode);
5 pathname: 要打开或创建的⽬标⽂件
6 flags: 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏"或"运算,构成flags。
7 参数:
8 O_RDONLY: 只读打开
9 O_WRONLY: 只写打开
10 O_RDWR : 读,写打开
11 (这三个常量,必须指定⼀个且只能指定⼀个
12 O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限
13 O_APPEND: 追加写
14 返回值:
15 成功:新打开的⽂件描述符
16 失败:-1
17 mode_t :
18 是 C 语言中用于表示文件权限和文件类型的标准数据类型
3.5 open返回值
在认识返回值之前,先来认识两个概念:系统调用和 库函数
- 库函数: fopen fclose fread fwrite
- 系统调用接口:open close read write lseek

- 可以认为, f# 系列的函数,都是对系统调用的封装,方便二次开发
3.6 文件描述符 fd
1) 文件描述符的概念
文件描述符是从0开始的小整数 。当我们打开文件时,操作系统在内存中会创建相应的数据结构来描述目标文件,于是就有了file结构体,表示一个已经打开的文件对象
2) 进程与文件的关联
进程执行open系统调用时,必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分是一个指针数组 ,每个元素都是一个指向开文件的指针
3) 文件描述符的本质
本质上,文件描述符就是该数组的下标。因此,只要持有文件描述符,就可以找到对应的文件。
4) 0 & 1 & 2
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
- 0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
cs
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 int main()
9 {
17 char buffer[100];
18 while(1)
19 {
21 ssize_t s =read(0,buffer,sizeof(buffer)-1);
22 if(s>0)
23 {
24 buffer[s]=0;
25 printf("%s",buffer);
26 }
27 else{
28 break;
29 }
30 }
return 0;
}
如何理解read() 是默认读取标准输入(stdin,文件描述符 0)?
- read() 其核心作用是从一个 「数据来源」 中读取数据 ,存入程序的内存缓冲区, 它的核心特点是:"输入"= 程序的 "数据输入来源",但这个来源不是固定的 ------ 默认是「键盘」,但可以被替换(重定向)
- 简单说:read读取 "标准输入",本质就是读取「当前进程的 stdin 数据流」中的数据,而 "输入" 就是这个数据流里的内容
如何理解write() 是默认标准输出(stdout,文件描述符 1)?
- write() 核心作用是:把程序内存中的字节数据,写入到一个「数据目的地」------ 这个目的地同样可以用**文件描述符(fd)**标识
- 简单说:"输出" 本质是程序从内存中向外传递的字节数据,而 stdout 是程序 "说话的默认渠道",write函数就是 "把这话通过渠道传出去" 的工具
5) 文件描述符的分配规则
观察如下代码:
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
**6)**重定向
当关闭1时:
cs
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
此时,本来应该输出到显示器上的内容被输出到了文件 myfile 中 ,其中 fd=1。这种现象称为输出重定向
常见的重定向符号包括:
>:覆盖输出到文件>>:追加输出到文件<:从文件读取输入
7) 重定向本质

- 这是"重定向"的底层逻辑,核心是 "换个文件描述符的指向"
- 文件描述符表(fd_array)是属于进程自己的、存在于进程控制块(task_struct)里的一个表结构,文件描述符表是 "存指针的数组",文件描述符是这个数组的 "下标",通过下标能快速找到数组里的指针,进而操作文件
- 简单说:每个 "打开的文件",在内核里都对应一个
file对象,文件描述符表里的file* 指针,就是指向这些file对象的 "链接"------ 通过这个链接,进程才能找到要操作的文件
8) 使用dup2系统调用
dup2函数原型:
cs
#include <unistd.h>
int dup2(int oldfd, int newfd);
oldfd:源文件描述符(必须是已打开且有效的文件描述符,否则调用失败)newfd:目标文件描述符(需要被替换的文件描述符)
cs
//实例
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 int main()
9 {
10 int fd = open("log.txt",O_CREAT,O_RDWR);
11 if(fd<0)
12 {
13 perror("open");
14 return 1;
15 }
16 close(1);
17 dup2(fd,1);
18 while(1)
19 {
20 char buff[1024];
21 ssize_t read_size =read(0,buff,sizeof(buff)-1);
22 if(read_size<0)
23 {
24 perror("read");
25 break;
26 }
27 printf("%s",buff);
28 fflush(stdout);
29 }
30 return 0;
31 }
9)在minishell中添加重定向功能
https://gitee.com/dwaekkiyo/my_linux_code/edit/master/251206/myshell.c
补充:标准错误
标准错误(stderr,文件描述符为 2)的默认输出目标是终端屏幕,和标准输出的默认目标一致,就解释了为什么平时执行命令时,正常结果(stdout)和错误提示(stderr)会混在一起显示在屏幕上。
可以把标准输出和标准错误分开打印:
4. 理解"一切皆文件"
-
核心设计:广泛的文件抽象相较于 Windows,Linux 的文件概念更宽泛:不仅 Windows 中的文件在 Linux 中仍是文件,进程、磁盘、显示器、键盘等硬件设备,以及管道、后续会接触的网络编程中的 socket(套接字),也都被抽象成文件,可通过访问文件的方式获取其信息。
-
核心优势:统一的开发接口:该设计让开发者只需掌握一套 API 和开发工具,就能调用 Linux 系统中绝大多数资源 。例如,读操作(读文件、读系统状态、读管道)几乎都可通过
read函数实现,更改操作(改文件、改系统参数、写管道)几乎都可通过write函数实现。 -
底层管理:file 结构体的作用 当在 Linux 中打开一个文件时,操作系统会创建一个
file结构体用于管理该文件,该结构体定义在/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h路径下,以下为部分该结构:csstruct file { ... struct inode *f_inode; //缓存的 inode 指针 const struct file_operations *f_op; ... atomic_long_t f_count; // 文件引用计数,多个文件指针指向该文件时数值会增加 unsigned int f_flags; // 表⽰打开⽂件的权限 fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所有 的标志在头⽂件<fcntl.h> 中定义 loff_t f_pos; // 当前文件的读写位置 ... } __attribute__((aligned(4))); // 结构体按 4 字节对齐 -
关键关联:
f_op指针与file_operations结构体 :struct file中的f_op是指向file_operations结构体的指针(二者均定义于fs.h);file_operations结构体的成员中,除struct module *owner外,其余均为函数指针 -
**
file_operations**结构体:cs#include <linux/module.h> #include <linux/fs.h> #include <linux/aio.h> #include <linux/mm.h> struct file_operations { struct module *owner; // 指向拥有该模块的指针 loff_t (*llseek) (struct file *, loff_t, int); // llseek 方法用作改变文件中的当前读/写位置,并且新位置作为(正的)返回值 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 用来从设备中获取数据。若该指针为 NULL,read 系统调用会以 -EINVAL("Invalid argument")失败; // 非负返回值代表成功读取的字节数(返回值是 signed size 类型,通常是目标平台本地的整数类型) ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 发送数据给设备。若该指针为 NULL,-EINVAL 会返回给调用 write 系统调用的程序; // 非负返回值代表成功写入的字节数 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); // 初始化一个异步读------可能在函数返回前不结束的读操作 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); // 初始化设备上的一个异步写 int (*readdir) (struct file *, void *, filldir_t); // 对于设备文件,该成员应当为 NULL;它用来读取目录,仅对文件系统有用 unsigned int (*poll) (struct file *, struct poll_table_struct *); // 实现 I/O 多路复用的轮询操作(支撑 select/poll/epoll 等系统调用) int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); // 设备控制接口(兼容老版本,需持有大内核锁) long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // 无锁版设备控制接口(推荐使用,无需持有大内核锁) long (*compat_ioctl) (struct file *, unsigned int, unsigned long); // 32 位兼容版 ioctl(用于 64 位内核运行 32 位用户程序) int (*mmap) (struct file *, struct vm_area_struct *); // 用来请求将设备内存映射到进程的地址空间。若该方法为 NULL,mmap 系统调用返回 -ENODEV int (*open) (struct inode *, struct file *); // 打开一个文件/设备(初始化相关资源) int (*flush) (struct file *, fl_owner_t id); // flush 操作在进程关闭它的设备文件描述符的副本时调用 int (*release) (struct inode *, struct file *); // 在文件结构被释放时调用该操作。如同 open,release 可以为 NULL(无额外释放逻辑时) int (*fsync) (struct file *, struct dentry *, int datasync); // 用户调用用来刷新任何挂起(未同步)的数据到存储介质 int (*aio_fsync) (struct kiocb *, int datasync); // 异步刷新挂起数据(配合异步读写操作使用) int (*fasync) (int, struct file *, int); // 异步 I/O 通知的注册/注销操作 int (*lock) (struct file *, int, struct file_lock *); // 用来实现文件加锁;加锁对常规文件是必不可少的特性,但设备驱动几乎从不实现它 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); // 高效发送文件页数据(如网络传输场景) unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); // 获取进程地址空间中未映射的虚拟地址区域(配合 mmap 操作使用) int (*check_flags)(int); // 校验文件打开标志的有效性 int (*flock) (struct file *, int, struct file_lock *); // BSD 风格的文件加锁(兼容 flock 系统调用,设备驱动一般不实现) ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); // 管道到文件/设备的高效数据传输(零拷贝) ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); // 文件/设备到管道的高效数据传输(零拷贝) int (*setlease)(struct file *, long, struct file_lock **); // 设置文件租约(用于控制文件并发访问,防止冲突) }; -
file_operations是 Linux 内核中连接用户层系统调用 与底层设备驱动实现的关键数据结构,是 "一切皆文件" 理念在设备驱动中的核心落地载体。它的核心作用是:将标准化的系统调用(如
read、write、ioctl等),通过函数指针映射到底层驱动程序中对应的具体实现函数 ,让内核能够统一调度不同设备的驱动逻辑,同时让用户层无需关注设备差异,只需调用统一 API 即可操作各类设备,一张图总结:
总结:
file和**file_operations** 二者共同支撑"一切皆文件",s**truct file管理 "已打开文件 / 设备的实例状态",struct file_operations提供 "操作该实例的统一接口集合",前者通过指针引用后者,实现 "状态 + 操作" 的绑定**
5. 缓冲区
5.1 什么是缓冲区
缓冲区是内存中预留的一块存储空间,专门用于缓存输入或输出的数据,本质是内存的一部分。根据对应设备类型,可分为输入缓冲区 (适配输入设备)和输出缓冲区(适配输出设备)
5.2 引入缓冲区的核心目的
核心是解决 "设备速度不匹配" 和 "减少系统调用开销" 两大问题,提升整体效率:
- 减少系统调用与上下文切换损耗:直接操作磁盘等设备需频繁执行系统调用,每次调用会涉及用户空间与内核空间的 CPU 状态切换,耗时较高。缓冲区可一次性读取 / 写入大量数据,减少系统调用次数,降低切换损耗。
- 协调高速 CPU 与低速 I/O 设备 :CPU 运算速度远高于磁盘、打印机等 I/O 设备,缓冲区可作为数据 "中转站":
- 读操作:先从低速设备(如磁盘)读取大量数据到缓冲区,CPU 后续从缓冲区快速获取,无需等待设备;
- 写操作:CPU 先将数据写入缓冲区,低速设备(如打印机)再自行从缓冲区读取处理,CPU 可同步执行其他任务,避免被低速设备占用
5.3 标准 I/O 的 3 种缓冲类型
标准 I/O 库提供 3 类缓冲区,适配不同使用场景,核心区别在于 "触发 I/O 系统调用的时机":
| 缓冲类型 | 触发系统调用的条件 | 典型应用场景 |
|---|---|---|
| 全缓冲区 | 缓冲区被填满时 | 磁盘文件操作(如普通文件读写) |
| 行缓冲区 | 遇到换行符,或缓冲区填满(默认大小 1024 字节) | 终端相关流(如标准输入 stdin、标准输出 stdout) |
| 无缓冲区 | 不缓存数据,直接执行系统调用 | 标准出错 |
缓冲区额外刷新场景
除上述默认触发条件外,以下情况会主动刷新缓冲区:
-
缓冲区数据填满时
-
主动执行
flush语句时 -
示例如下:
cs// 示例1:stdout重定向到文件(全缓冲,未flush导致内容未写入) void test_no_flush() { // 关闭1号描述符(stdout,默认对应显示器) close(1); // 打开log1.txt,fd自动分配为1(最小未使用fd),实现stdout重定向 int fd = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd < 0) { perror("open log1.txt failed"); exit(1); } // printf写入stdout(已重定向到文件,缓冲类型变为全缓冲) printf("test_no_flush: hello world, fd=%d\n", fd); // 未手动flush,内容未填满全缓冲区,数据仅存于内存 close(fd); } // 示例2:stdout重定向+fflush强制刷新(内容成功写入) void test_with_flush() { close(1); int fd = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd < 0) { perror("open log2.txt failed"); exit(1); } printf("test_with_flush: hello world, fd=%d\n", fd); fflush(stdout); // 强制刷新stdout缓冲区,数据写入文件 close(fd); } // 示例3:stderr重定向(无缓冲,无需flush直接写入) void test_stderr_no_buffer() { // 关闭2号描述符(stderr,标准错误流) close(2); // 打开log3.txt,fd自动分配为2,实现stderr重定向 int fd = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd < 0) { perror("open log3.txt failed"); exit(1); } // perror默认写入stderr(无缓冲特性),直接触发系统调用 perror("test_stderr_no_buffer: hello world"); close(fd); } int main() { // 依次执行三个测试用例 test_no_flush(); test_with_flush(); test_stderr_no_buffer(); return 0; }5.4 FILE
- 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通 过fd访问的
- 所以C库当中的FILE结构体内部,必定封装了fd
验证代码:
cs
#include <stdio.h>
#include <string.h>
#include <unistd.h> // 包含 fork() 声明
int main() {
const char *msg0 = "hello printf\n";
const char *msg1 = "hello fwrite\n";
const char *msg2 = "hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg1), 1, stdout);
write(1, msg2, strlen(msg2));
// 创建子进程,触发写时拷贝
fork();
return 0;
}
结果对比
场景 1:直接输出到显示器(无重定向)
cs
运行命令
./hello
cs
输出结果
hello printf
hello fwrite
hello write
原因解析
- 输出目标为显示器时,
stdout的缓冲类型是行缓冲; msg0和msg1均包含换行符\n,触发行缓冲立即刷新,数据直接输出到显示器;fork执行时,库函数的缓冲区已为空,父子进程无额外数据可刷新,因此无重复输出;write是系统调用,无缓冲区,数据直接写入,仅输出 1 次
场景 2:重定向到文件(./hello > file)
Delphi
运行命令
./hello > file
cat file
Delphi
输出结果
hello write
hello printf
hello fwrite
hello printf
hello fwrite
原因解析(核心逻辑)
- 缓冲类型切换 :重定向到普通文件后,
stdout的缓冲类型从行缓冲 变为全缓冲; - 库函数数据未刷新 :
printf和fwrite的输出内容较短,未填满全缓冲区,数据暂存于用户级缓冲区(由 C 标准库提供),未触发系统调用; - fork 的写时拷贝 :
fork创建子进程时,会复制父进程的所有内存数据(包括用户级缓冲区)。此时父子进程各自持有一份相同的缓冲区数据(未刷新); - 进程退出时刷新缓冲区:main 函数返回后,父子进程相继退出,C 标准库会自动刷新缓冲区,将数据写入文件。因此库函数的内容被输出 2 次;
- 系统调用无缓冲 :
write是系统调用,无用户级缓冲区,数据直接写入内核,fork时无数据可复制,因此仅输出 1 次 - 为了避免重复 :
fork前用fflush(stdout)手动刷新缓冲区
5.5 简单设计lib库
mystdio.h
cs
1 #pragma once
2 #include<stdio.h>
3
4 #define MAX 1024
5 #define NONE_FLUSH (1<<0)
6 #define LINE_FLUSH (1<<1)
7 #define FULL_FLUSH (1<<2)
8
9 typedef struct IO_FILE
10 {
11 int fileno; //文件描述符
12 int flag; //文件状态(r,w,a..
13 char outbuffer[MAX]; //输出缓冲区
14 int bufferlen; //缓冲区长度
15 int flush_method;
16
17 }MyFile;
18
19 MyFile* MyFopen(const char *path,const char *mode);
20 void MyFclose(MyFile*);
21 int MyFwrite(MyFile*,void* str,int len);
22 void MyFFlush(MyFile*);
mystdio.c
cs
1 #include "mystdio.h"
2 #include <string.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include<unistd.h>
7 #include <fcntl.h>
8 static MyFile* BuyFile(int fd,int flag)
9 {
10 MyFile *f =(MyFile*)malloc(sizeof(MyFile));
11 if(f==NULL)
12 return NULL;
13 f->bufferlen=0;
14 f->fileno=fd;
15 f->flag=0;
16 f->flush_method=LINE_FLUSH;
17 memset(f->outbuffer,0,sizeof(f->outbuffer));
18 return f;
19 }
20
21 MyFile* MyFopen(const char *path,const char *mode)
22 {
23 int fd = -1;
24 int flag = 0;
25 if(strcmp(mode,"w")==0)
26 {
27 flag = O_CREAT | O_WRONLY | O_TRUNC;
28 fd =open(path,flag,0666);
29 }
30 else if(strcmp(mode,"a")==0)
31 {
32 flag = O_CREAT | O_WRONLY | O_APPEND;
33 fd =open(path,flag,0666);
34 }
35 else if(strcmp(mode,"r")==0)
36 {
37 flag= O_RDWR;
38 fd =open(path,flag);
39 }
40 else
41 {
42 //
43 }
44 if(fd<0)
45 return NULL;
46 return BuyFile(fd,flag);
47 }
48 void MyFclose(MyFile* file)
49 {
50 if(file->fileno<0) return ;
51 MyFFlush(file);
52 close(file->fileno);
53 free(file);
54 }
55 int MyFwrite(MyFile* file,void* str,int len)
56 {
57 //1.拷贝
58 memcpy(file->outbuffer+file->bufferlen,str,len);
59 file->bufferlen +=len;
60 //2.尝试判断是否满足刷新条件
61 if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1]=='\n')
62 {
63 MyFFlush(file);
64 }
65 return 0;
66 }
67 void MyFFlush(MyFile* file)
68 {
69 if(file->fileno<=0)return ;
70 int n = write(file->fileno,file->outbuffer,file->bufferlen);
71 //fsync(file->fileno); // 刷新到外设
72 file->bufferlen=0;
73 }
main.c
cs
1 #include"mystdio.h"
2 #include<string.h>
3
4 int main()
5 {
6 MyFile* filep= MyFopen("./log.txt","a");
7 if(!filep)
8 {
9 printf("fopen error\n");
10 return 1;
11 }
12 char *msg=(char*)"hello myfile";
13 MyFwrite(filep,msg,strlen(msg));
14
15 MyFclose(filep);
16 return 0;
17 }
