什么是文件
1. 文件的定义
(1) 传统定义(狭义)
-
存储在存储设备(如磁盘、SSD)上的数据集合,可以是:
-
文本文件(如
.txt
、.sh
) -
二进制文件(如
.exe
、.so
) -
目录(一种特殊文件,存储其他文件的索引)
-
设备文件(如
/dev/sda
代表硬盘) -
管道、套接字等(用于进程通信)
-
(2) Linux 的广义定义
Linux 采用 "一切皆文件" 的设计,几乎所有 I/O 资源 都被抽象成文件,包括:
-
普通文件(-)(文本、二进制)
-
目录文件(d)
-
设备文件(b)(如
/dev/tty
代表终端) -
符号链接(l)(软链接)
-
管道(p)(FIFO)
-
套接字(s)(Socket)
-
procfs/sysfs 虚拟文件(如
/proc/cpuinfo
提供 CPU 信息)
这意味着,无论是读取硬盘数据、操作硬件设备,还是进程间通信,都可以用统一的文件操作接口(open
、read
、write
、close
)来完成。
2. 文件的组成
Linux 中的文件由两部分组成:
-
文件内容(Content)
-
文件的实际数据(如文本、二进制代码)。
-
可以是空的(0KB),但仍会占用少量磁盘空间(至少 1 个 block,通常 4KB)。
-
-
文件属性(Metadata,元数据)
-
文件的额外信息,包括:
-
文件名、大小、权限(
rwx
) -
所有者(User)、所属组(Group)
-
创建/修改时间(
ctime
、mtime
) -
存储位置(inode 编号)
-
文件类型(普通文件、目录、设备文件等)
-
-
3. 文件在 Linux 中的管理方式
(1) 文件如何存储在磁盘?
-
文件数据存储在 磁盘块(Block) 中。(磁盘是外部设备,访问磁盘文件其实是访问硬件)
-
几乎所有的库只要访问硬件设备一定要封装系统调用
-
文件的元数据存储在 inode(索引节点)中,每个文件有唯一的 inode 编号。
-
目录 本质上是一种特殊文件,存储 文件名 → inode 的映射关系。
(2) 文件如何被访问?
-
访问文件前,必须先打开(
open
),由进程打开,内核会:-
在内存中创建 文件描述符(File Descriptor, fd),用于代表该文件。
-
通过 VFS(虚拟文件系统) 找到对应的文件系统(如
ext4
)。 -
读取 inode,找到文件数据在磁盘上的位置。
-
将数据加载到 Page Cache(内存缓存),提高后续访问速度。
-
-
进程通过
read
/write
操作文件,最终由 系统调用 进入内核完成实际 I/O。
(3) 文件描述符(File Descriptor, fd)
-
是一个 非负整数,代表进程打开的文件。
-
标准文件描述符:
-
0
→stdin
(标准输入) -
1
→stdout
(标准输出) -
2
→stderr
(标准错误)
-
-
每个进程有自己的 文件描述符表,记录打开的文件。
补充:
文件分为打开的文件和未打开的文件
进程打开文件(本质是研究进程和文件的关系)
文件被打开必须先被加载到内存
没打开的文件在磁盘上
进程:打开的文件=1:n
操作系统内部存在大量被打开的文件,管理方法:先描述在组织。
回顾C文件接口
打开文件
FILE *fopen(const char *filename, const char *mode);
关闭文件
int fclose(FILE *stream);
写文件
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main()
6 {
7 printf("pid:%d",getpid());
8 FILE* fp = fopen("log.txt","w");
9 if(fp == NULL)
10 {
11 perror("fopen");
12 return 1;
13 }
14
15 const char* message = "abcd";
16 fwrite(message,strlen(message),1,fp);
17 fclose(fp);
18 return 0;
19 }
字符串以“\0”结尾,但是文件不用标定结尾,不需要加1。
fwrite函数
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数说明:
参数 类型 含义 ptr
const void*
数据来源指针,指向要写入文件的数据(可以是任意类型,如 char*
、int*
等)。size
size_t
每个数据项的字节大小(如 sizeof(char)
、sizeof(int)
等)。nmemb
size_t
要写入的数据项数量(即写入 nmemb
个size
字节的数据)。stream
FILE*
文件指针,指向已打开的文件流(由 fopen
返回)。返回值:
成功:返回 实际写入的数据项数量(通常等于
nmemb
,除非发生错误或到达文件尾)。失败:返回值可能小于
nmemb
,需用ferror
或feof
检查错误或文件结束。
读文件
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
printf("pid:%d\n", getpid());
// 1. 打开文件(二进制读取模式)
FILE* fp = fopen("log.txt", "rb"); // "rb" 表示二进制读取
if (fp == NULL) {
perror("fopen");
return 1;
}
// 2. 读取文件内容
char buffer[1024]; // 缓冲区
size_t ret = fread(buffer, 1, sizeof(buffer), fp); // 每次读 1 字节,最多读 sizeof(buffer) 次
if (ret == 0) {
if (feof(fp)) {
printf("Reached end of file.\n");
} else {
perror("fread");
}
} else {
printf("Read %zu bytes: %s\n", ret, buffer); // 打印读取的内容
}
// 3. 关闭文件
fclose(fp);
return 0;
}
fread函数
参数说明:
参数 类型 含义 ptr
void*
目标缓冲区指针,用于存储读取的数据(需提前分配内存)。 size
size_t
每个数据项的字节大小(如 sizeof(int)
、sizeof(char)
等)。nmemb
size_t
要读取的数据项数量(即读取 nmemb
个size
字节的数据)。stream
FILE*
文件指针,指向已打开的文件流(由 fopen
返回)。返回值:
成功:返回实际读取的数据项数量(可能小于
nmemb
,表示读到文件尾或错误)。失败:返回
0
,需用feof
或ferror
检查是文件结束还是错误。
输出信息到显示器,你有哪些方法
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
stdin & stdout & stderr
C默认会打开三个输入输出流,分别是stdin, stdout, stderr(在fp中0,1,2)
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
总结
打开文件的方式
系统文件I/O
关闭文件(引用计数),看引用计数是否为0,为0则系统回收struct对象(文件),让对应数组下标指针位置置空(文件指针)
文件有自己的struct file(多个)描述自己属性并将地址存到struct files_struct中,当task_struct要访问时直接查fd下标
比特位方式的标志位传递方式
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define ONE (1<<0) // 1
#define TWO (1<<1) // 2
#define THREE (1<<2) // 4
#define FOUR (1<<3) // 8
// 根据标志位打印不同消息
void show(int flags)
{
if(flags&ONE) printf("hello function1\n");
if(flags&TWO) printf("hello function2\n");
if(flags&THREE) printf("hello function3\n");
if(flags&FOUR) printf("hello function4\n");
}
int main()
{
printf("-----------------------------\n");
show(ONE);
printf("-----------------------------\n");
show(TWO);
printf("-----------------------------\n");
show(ONE|TWO);
printf("-----------------------------\n");
show(ONE|TWO|THREE);
printf("-----------------------------\n");
show(ONE|THREE);
printf("-----------------------------\n");
show(THREE|FOUR);
printf("-----------------------------\n");
}
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。
io访问磁盘的过程中:新建文件不带路径就在当前路径下创建
原因:进程运行时由进程自己的工作路径决定了新建文件时的路径。
cwd可以显示当前文件所在路径。
chdir可以改变cwd,修改cwd可以把文件新建到其他目录。
>重定向先打开文件(用w打开)所以每次运行(像文件写入)前会先把文件内容清空
硬件只能被操作管理
umask只影响最近进程
a 追加 “>>”
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 // 改变工作目录
11 chdir("/home/diske");
12
13 printf("Pid: %d\n", getpid());
14
15 // 以追加模式打开文件
16 FILE *fp = fopen("log.txt", "a");
17 if(fp == NULL){
18 perror("fopen");
19 return 1;
20 }
21
22 const char *message = "abcd";
23
24 // 多种写入方式
25 // fwrite(message, strlen(message), 1, stdout);
26 // fprintf(stdout, "%s: %d\n", message, 1234);
27 fprintf(stderr, "%s: %d\n", message, 1234);
28
29 fclose(fp);
30
31 // sleep(1000);
32 return 0;
33 }
系统调用接口
open
打开文件系统创建一个内核级别的struct file对象(操作系统内描述一个被打开文件的信息)
open创建一个文件对象,在调用进程的文件描述表里找一个没有使用的下标,把新的文件对象地址填进去,将数组下标返回给用户
int fd:数组的下标,连续的,从3开始(012默认打开的三个流)
FILE中必定包含文件描述符fd(系统调用里封装了库函数)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname
:要打开的文件路径(字符串)。
flags
:文件访问模式(必选)和可选标志(如O_CREAT
)。
mode
(可选):仅在创建文件时(O_CREAT
)有效,指定文件权限(如0666
)。
flags
> 与 >>在底层就是这些区别
用于指定文件的打开方式,必须包含以下三种访问模式之一:
访问模式 | 说明 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 读写 |
可选标志(可组合使用):
可选标志 | 说明 |
---|---|
O_CREAT | 如果文件不存在,则创建它(需配合 mode 参数) |
O_TRUNC | 如果文件存在且可写,清空文件内容(截断为 0 字节) |
O_APPEND | 每次写入时追加到文件末尾 |
O_EXCL | 与 O_CREAT 一起使用,如果文件已存在则返回错误 |
O_NONBLOCK | 非阻塞模式(适用于设备文件、FIFO 等) |
O_SYNC | 每次写入都同步到磁盘(保证数据持久化) |
示例代码
fcntl.h
头文件提供open
的标志(O_WRONLY
等)。
sys/stat.h
定义权限宏(如0666
)。
unistd.h
提供umask
和close
(当前代码未调用close
,需补充)。Linux中新建一个文件必须告诉它权限
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
umask(0); // 确保权限 0666 完全生效
// 以只写模式打开文件,不存在时创建,存在时清空内容
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1)
{
perror("open failed");
return 1;
}
close(fd); // 关闭文件
return 0;
}
返回值
在认识返回值之前,先来认识一下两个概念:
系统调用 和 库函数
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。 而 open close read write lseek 都属于系统提供的接口,称之为系统调用接口 回忆一下我们讲操作系统概念时,画的一张图
系统调用接口和库函数的关系,一目了然。 所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
open
vs fopen
特性 | open (系统调用) | fopen (C 标准库) |
---|---|---|
返回值 | 文件描述符(int ) | 文件指针(FILE* ) |
缓冲 | 无缓冲(直接系统调用) | 带缓冲(提高性能) |
适用场景 | 底层文件操作 | 高级文件操作(如 fprintf ) |
写文件
#include <unistd.h> // write, close
#include <fcntl.h> // open
#include <sys/stat.h> // mode_t
#include <string.h> // strlen
#include <stdio.h> // perror
int main() {
// 1. 打开文件(不存在则创建,权限 0644)
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
return 1;
}
// 2. 写入数据
const char *text = "Hello, Linux System Call!\n";
ssize_t bytes_written = write(fd, text, strlen(text));
if (bytes_written == -1) {
perror("write failed");
close(fd);
return 1;
}
printf("Wrote %zd bytes to file.\n", bytes_written);
// 3. 关闭文件
close(fd);
return 0;
}
读文件
#include <unistd.h> // read, close
#include <fcntl.h> // open
#include <stdio.h> // perror
int main() {
// 1. 打开文件(只读模式)
int fd = open("input.txt", O_RDONLY);
if (fd == -1) {
perror("open failed");
return 1;
}
// 2. 读取数据
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1); // 预留 1 字节给 \0
if (bytes_read == -1) {
perror("read failed");
close(fd);
return 1;
}
buffer[bytes_read] = '\0'; // 手动添加字符串结束符
printf("Read %zd bytes: %s\n", bytes_read, buffer);
// 3. 关闭文件
close(fd);
return 0;
}
接口介绍
open:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的⽬标⽂件
flags: 打开⽂件时,可以传⼊多个参数选项,⽤下⾯的⼀个或者多个常量进⾏“或”运算,构成
flags。
参数: O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定⼀个且只能指定⼀个
O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限
O_APPEND: 追加写
O_TRUNC:打开文件时清空文件
返回值:
成功:新打开的⽂件描述符
失败:-1
mode_t理解:直接 man 手册,比什么都清楚。 open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件 的默认权限,否则,使用两个参数的open。
文件描述符
通过对open函数的学习,我们知道了文件描述符就是一个小整数
0 & 1 & 2
Linux进程默认情况下会有3个缺省打开的文件描述符
分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器 所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf));
if(s > 0){
buf[s] = 0;
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
}
return 0;
}
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来 描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进 程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数 组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件 描述符,就可以找到对应的文件
文件描述符的分配规则
新分配的 fd
总是当前可用的最小整数。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
umask(0); // 设置文件创建权限掩码
// 以追加模式打开多个文件
int fd1 = open("log1.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
int fd2 = open("log2.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
int fd3 = open("log3.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
int fd4 = open("log4.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
if(fd1 < 0) {
printf("open file error\n");
return 1;
}
// 打印文件描述符(不会显示,stdout已关闭)
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
printf("fd4: %d\n", fd4);
// 写入文件
const char *message = "xxx";
write(fd1, message, strlen(message));
close(fd1);
重定向
重定向本质:对进程的指定文件描述符表进行内核级别的对文件描述符表中的文件对象地址作拷贝
cat <- “.txt”输入重定向
打开的文件/进程与文件产生建立关联关系的文件描述符数组
2->&1把一号描述符内容(地址)写到2中,都指向同一个文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
// 1. 关闭标准输出文件描述符(1)
close(1);
// 2. 打开新文件,系统会分配可用的最小文件描述符(此时是1)
int fd = open("myfile", O_WRONLY|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open");
return 1;
}
// 3. 验证文件描述符确实是1
if(fd != 1) {
fprintf(stderr, "Error: Expected fd=1, got fd=%d\n", fd);
close(fd);
return 1;
}
// 4. 现在所有输出到stdout的内容都会写入到"myfile"
printf("这行文字将被写入myfile文件\n");
fprintf(stdout, "文件描述符: %d\n", fd);
fflush(stdout); // 确保缓冲区内容写入文件
// 5. 关闭文件
close(fd);
return 0;
}
此时,我们发现,本来应该输出到显⽰器上的内容,输出到了⽂件 myfile 当中,其中,它的文件描述符 fd=1。这种现象叫做输出重定向。常⻅的重定向有: > ,>> ,< 。
重定向的本质就是修改文件描述符表的某一个下标中存储的内容。如下图:
dup
系统调用
-
函数原型:
int dup(int oldfd);
- 功能:创建一个新的文件描述符,它与
oldfd
指向相同的文件。新文件描述符是最小的未使用的文件描述符。 - 返回值:返回新的文件描述符,如果出错则返回
-1
- 功能:创建一个新的文件描述符,它与
使用 dup2 系统调用
参数说明
oldfd
: 要复制的源文件描述符
newfd
: 指定的新文件描述符返回值
成功时返回新的文件描述符(与
newfd
相同)失败时返回 -1,并设置
errno
工作原理
dup2()
执行以下操作:
如果
newfd
已经打开,会先关闭它创建一个新的文件描述符
newfd
,它指向与oldfd
相同的文件/资源两个文件描述符共享相同的文件偏移量和文件状态标志
dup2(旧,新)
#include<unistd.h>
int dup2(int oldfd, int newfd);
标准输出和错误输出
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FILENAME "log.txt"
int main()
{
printf("===== 标准输出(stdout)和标准错误(stderr)演示 =====\n");
// 向标准输出(stdout)打印消息
fprintf(stdout, "[stdout] 这是标准输出消息1\n");
fprintf(stdout, "[stdout] 这是标准输出消息2\n");
// 向标准错误(stderr)打印消息
fprintf(stderr, "[stderr] 这是错误输出消息1\n");
fprintf(stderr, "[stderr] 这是错误输出消息2\n");
return 0;
}
文件操作
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FILENAME "log.txt"
int main()
{
printf("\n===== 文件操作演示 =====\n");
// 以追加模式打开文件(如果不存在则创建)
int fd = open(FILENAME, O_CREAT | O_WRONLY | O_APPEND, 0666);
if (fd < 0) {
perror("open failed");
return 1;
}
printf("文件描述符fd = %d\n", fd);
// 向文件写入数据
const char *file_msg = "这是直接写入文件的内容\n";
write(fd, file_msg, strlen(file_msg));
// 必须添加的close操作
close(fd); // 关闭文件描述符
return 0;
}
输出重定向
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FILENAME "log.txt"
int main()
{
printf("\n===== 输出重定向演示 =====\n");
// 保存原始的标准输出文件描述符
int stdout_backup = dup(1);
// 打开文件
int fd = open(FILENAME, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}
// 将标准输出重定向到文件
dup2(fd, 1);
printf("这行文字将被重定向到文件中\n");
fprintf(stdout, "使用fprintf输出的内容也会到文件中\n");
// 恢复标准输出
dup2(stdout_backup, 1);
close(stdout_backup);
close(fd); // 关闭文件描述符
printf("标准输出已恢复\n");
return 0;
}
输入重定向
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FILENAME "log.txt"
int main()
{
printf("\n===== 输入重定向演示 =====\n");
// 创建一个临时输入文件
const char *input_data = "这是输入重定向的测试数据\n";
int input_fd = open("input.tmp", O_CREAT | O_WRONLY | O_TRUNC, 0666);
write(input_fd, input_data, strlen(input_data));
close(input_fd);
// 保存原始的标准输入文件描述符
int stdin_backup = dup(0);
// 打开输入文件并重定向标准输入
input_fd = open("input.tmp", O_RDONLY);
dup2(input_fd, 0);
// 从标准输入读取数据(实际从文件读取)
char buffer[256];
fgets(buffer, sizeof(buffer), stdin);
printf("从重定向输入读取: %s", buffer);
// 恢复标准输入
dup2(stdin_backup, 0);
close(stdin_backup);
close(input_fd);
return 0;
}
// ==================== 第五部分:直接I/O操作 ====================
printf("\n===== 直接I/O操作演示 =====\n");
const char *direct_msg = "直接使用write系统调用输出\n";
write(1, direct_msg, strlen(direct_msg));
// ==================== 清理工作 ====================
close(fd);
printf("\n===== 所有操作完成 =====\n");
在自己的shell中添加重定向功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <fcntl.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2
int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char *rdirfilename = NULL;
int rdir = NONE;
// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表
const char *getusername()
{
return getenv("USER");
}
const char *gethostname1()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd, sizeof(pwd));
}
void check_redir(char *cmd)
{
// ls -al -n
// ls -al -n >/</>> filename.txt
char *pos = cmd;
while(*pos)
{
if(*pos == '>')
{
if(*(pos+1) == '>'){
*pos++ = '\0';
*pos++ = '\0';
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=APPEND_RDIR;
break;
}
else{
*pos = '\0';
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=OUT_RDIR;
break;
}
}
else if(*pos == '<')
{
*pos = '\0'; // ls -a -l -n < filename.txt
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=IN_RDIR;
break;
}
else{
//do nothing
}
pos++;
}
}
void interact(char *cline, int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd);
char *s = fgets(cline, size, stdin);
assert(s);
(void)s;
// "abcd\n\0"
cline[strlen(cline)-1] = '\0';
//ls -a -l > myfile.txt
check_redir(cline);
}
int splitstring(char cline[], char *_argv[])
{
int i = 0;
argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
return i - 1;
}
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0){
perror("fork");
return;
}
else if(id == 0){
int fd = 0;
// 后面我们做了重定向的工作,后面我们在进行程序替换的时候,难道不影响吗???
if(rdir == IN_RDIR)
{
fd = open(rdirfilename, O_RDONLY);
dup2(fd, 0);
}
else if(rdir == OUT_RDIR)
{
fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
dup2(fd, 1);
}
else if(rdir == APPEND_RDIR)
{
fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666);
dup2(fd, 1);
}
//让子进程执行命令
//execvpe(_argv[0], _argv, environ);
execvp(_argv[0], _argv);
exit(EXIT_CODE);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
int buildCommand(char *_argv[], int _argc)
{
if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"), "%s", pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "export") == 0){
strcpy(myenv, _argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
if(strcmp(_argv[1], "$?") == 0)
{
printf("%d\n", lastcode);
lastcode=0;
}
else if(*_argv[1] == '$'){
char *val = getenv(_argv[1]+1);
if(val) printf("%s\n", val);
}
else{
printf("%s\n", _argv[1]);
}
return 1;
}
// 特殊处理一下ls
if(strcmp(_argv[0], "ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit){
// 1.
rdirfilename = NULL;
rdir = NONE;
// 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
interact(commandline, sizeof(commandline));
// commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
// 3. 子串分割的问题,解析命令行
int argc = splitstring(commandline, argv);
if(argc == 0) continue;
// 4. 指令的判断
// debug
//for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
//内键命令,本质就是一个shell内部的一个函数
int n = buildCommand(argv, argc);
// 5. 普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
./mytest 1>all.log 2>&1
./mytest >all.log 2>&1
一切皆文件
操作系统给我们在文件层面上封装了一层structfile这样的文件对象,让文件由指针指向不同操作方法,在上层用函数指针方法对底下内容进行汇总
结构体中有函数指针指向方法
c++继承中的虚函数表《——》函数指针数组
缓冲区
PCB-》文件-》文件缓冲区-》磁盘
文件一定要提供自己操作系统级别的缓冲区
有语言缓冲区
_exit不让进行数据刷新直接退出
exit让刷新后退出能看到语言层缓冲区
每一个文件都有自己的缓冲区
什么是缓冲区
在Linux系统中,缓冲区(Buffer)是内存中的一块区域,用于临时存储数据,以提高I/O操作的效率。Linux内核使用多种类型的缓冲区来优化系统性能。
缓冲区的层次
1. 用户级缓冲区(User-Level Buffer)
内核级缓冲区需要调用系统调用进行写入或读取,而频繁的调用系统调用是有一定的成本的(时间和空间),为了提高效率,C语言又封装了一个用户级缓冲区,这块缓冲区就在 FIFE* 指向的 FIFE 结构体中维护,当满足一定条件时,用户级缓冲区的内容会通过系统调用一次性刷新到内核级缓冲区。C语言中封装的 fclose 函数在关闭文件的时候,会自动刷新用户级缓冲区。而系统调用 close 不会刷新用户级缓冲区。当一个进程退出的时候,也会自动刷新用户级缓冲区。
1.1 语言级缓冲区(Language-Level Buffer)
位置:用户空间,由编程语言的标准库(如 C 的
stdio.h
)管理。作用:减少
write/read
系统调用的次数,提高 I/O 效率。示例:
C 语言的
FILE*
结构体中的缓冲区(fread
/fwrite
/printf
)。Python 的
io.BufferedWriter
/BufferedReader
。缓冲策略:
全缓冲:缓冲区满才刷新(默认用于文件)。
行缓冲:遇到
\n
或缓冲区满才刷新(默认用于终端)。无缓冲:立即刷新(如
stderr
)。进程退出时也会刷新
清空也是修改,进行写时拷贝(缓冲区被拷贝一份)
文件打开成功创建文件对象让别人使用
示例代码(C 语言缓冲区)
#include <stdio.h>
int main() {
// 默认全缓冲(写入文件)
FILE *fp = fopen("log.txt", "w");
fprintf(fp, "Hello, World\n"); // 数据先进入用户缓冲区
fclose(fp); // 关闭文件时刷新缓冲区
// 行缓冲(终端输出)
printf("Hello, Terminal\n"); // 遇到 \n 时刷新
return 0;
}
1.2 应用级缓冲区(Application-Level Buffer)
-
位置:用户空间,由应用程序自行管理(如数据库、Web 服务器的缓存)。
-
作用:优化特定场景下的 I/O 性能(如 Redis 的
AOF 缓冲区
、MySQL 的InnoDB Buffer Pool
)。 -
特点:
-
可以自定义刷新策略(如定时刷新、批量提交)。
-
通常比语言级缓冲区更大,管理更复杂。
-
2. 内核级缓冲区(Kernel-Level Buffer)
2.1 页缓存(Page Cache)
-
位置:内核空间,由 Linux 内核管理。
-
作用:缓存文件数据,减少磁盘 I/O。
-
特点:
-
以 4KB 页 为单位缓存文件数据。
-
采用 LRU(最近最少使用) 算法管理。
-
可以被多个进程共享(如多个进程读取同一个文件)。
-
-
触发刷新:
-
内存不足时(由
kswapd
内核线程回收)。 -
调用
fsync()
或sync()
强制刷新。
-
查看页缓存
free -h # 查看内存使用情况
cat /proc/meminfo | grep Cached # 查看缓存大小
2.2 块设备缓存(Buffer Cache)
-
位置:内核空间,用于缓存磁盘块(Block)。
-
作用:优化块设备(如硬盘、SSD)的读写性能。
-
特点:
-
适用于原始块设备访问(如
dd
命令)。 -
在现代 Linux 中,
Buffer Cache
和Page Cache
已合并管理。
-
2.3 文件系统元数据缓存
-
目录项缓存(Dentry Cache):加速路径查找(如
ls /usr/bin
)。 -
inode 缓存:缓存文件元数据(权限、大小、时间戳等)。
-
触发刷新:
-
调用
sync()
或umount
时。 -
内存紧张时由内核自动回收。
-
手动清理内核缓存
# 清理页缓存
echo 1 > /proc/sys/vm/drop_caches
# 清理目录项和 inode 缓存
echo 2 > /proc/sys/vm/drop_caches
# 清理所有缓存
echo 3 > /proc/sys/vm/drop_caches
3. 硬件级缓冲区(Hardware-Level Buffer)
3.1 磁盘缓存(Disk Cache)
-
位置:硬盘或 SSD 的控制器芯片。
-
作用:缓存最近读写的磁盘数据。
-
特点:
-
由硬盘固件管理,操作系统不可见。
-
掉电可能丢失数据(需启用
Write-Back Cache
时注意风险)。
-
3.2 CPU 缓存(CPU Cache)
-
位置:CPU 的 L1/L2/L3 Cache。
-
作用:加速内存访问(包括缓冲区数据)。
-
特点:
-
对 I/O 性能有间接影响(如
mmap
映射的文件可能被 CPU 缓存)。
-
4. 缓冲区刷新机制
缓冲区层级 | 刷新方式 | 控制方法 |
---|---|---|
用户级缓冲区 | fflush() 、fclose() | 由应用程序控制 |
内核页缓存 | fsync() 、sync() | 由内核或 sysctl 参数控制 |
磁盘缓存 | 硬盘固件控制 | 不可控(可禁用 Write-Back) |
示例:强制刷新缓冲区
#include <unistd.h>
#include <stdio.h>
int main() {
FILE *fp = fopen("data.txt", "w");
fprintf(fp, "Test data\n");
fflush(fp); // 刷新用户缓冲区到内核
fsync(fileno(fp)); // 强制内核刷新到磁盘
fclose(fp);
return 0;
}
5. 缓冲区层次总结
层级 | 缓冲区类型 | 管理者 | 典型应用 |
---|---|---|---|
用户空间 | 语言级缓冲区(FILE* ) | 标准库(如 glibc) | fprintf 、fread |
应用级缓冲区(如 Redis AOF) | 应用程序 | 数据库、Web 服务器 | |
内核空间 | 页缓存(Page Cache) | Linux 内核 | 文件读写 |
块缓存(Buffer Cache) | Linux 内核 | 原始磁盘访问 | |
Dentry/Inode 缓存 | Linux 内核 | 文件系统元数据 | |
硬件层 | 磁盘缓存 | 硬盘控制器 | 磁盘 I/O 加速 |
CPU 缓存 | CPU | 内存访问优化 |
引入缓冲区的原因
解决速度不匹配问题
协调高速CPU(纳秒级)与低速I/O设备(毫秒级)的万倍速度差
例:磁盘寻道需10ms,而CPU可执行数百万条指令
减少物理I/O操作
合并多次小数据写入为单次大块写入
例:文本编辑器每按一次键不直接写磁盘,先缓冲后批量保存
降低系统开销
减少用户态/内核态切换(每次系统调用消耗约1μs)
例:C语言
printf
缓冲满后再调用write
,而非每次输出都触发系统调用提高资源利用率
使CPU和I/O设备并行工作
例:视频播放时预加载缓冲数据,避免CPU等待网络传输
优化用户体验
实现平滑的交互响应
例:终端输入行缓冲允许用户整行编辑后再提交
关键价值体现
空间换时间:用少量内存成本换取性能的指数级提升
批处理效应:将随机小I/O转换为顺序大I/O(如SSD写入优化)
预读机制:基于局部性原理提前加载数据(如CPU缓存行、文件预读)
一句话总结
缓冲区通过内存暂存数据,协调不同速度组件间的矛盾,以空间代价换取系统吞吐量、响应速度和资源利用率的全面提升。
示例代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//printf("hello Linux");
//close(1);
//return 0;
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
// C
printf("hello printf\n"); // stdout -> 1
sleep(2);
fprintf(stdout, "hello fprintf\n"); // stdout -> 1
sleep(2);
fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout->1
sleep(2);
// 操作提供的systemcall
write(1, str, strlen(str)); // 1
sleep(5);
//close(1); // ?
fork();
return 0;
}
输出函数对比
代码使用了四种不同的方式向标准输出(stdout
,文件描述符 1
)写入数据:
-
printf
-
C标准库函数,默认使用 行缓冲(line-buffered)(如果输出到终端),遇到
\n
时会刷新缓冲区。 -
如果重定向到文件,则变为 全缓冲(fully buffered),不会立即刷新。
-
-
和fprintf(stdout, ...)
printf
类似,只是可以指定输出流(这里是stdout
)。 -
直接写入数据块,通常使用 全缓冲(除非手动fwrite
fflush
)。 -
系统调用,直接写入文件描述符write(1, ...)
1
(标准输出),无缓冲,立即生效。
2. sleep(2)
的作用
-
让每条输出之间间隔 2秒,方便观察输出顺序。
3. fork()
的影响
-
fork()
会复制当前进程,包括 缓冲区 中的数据。 -
如果缓冲区未刷新,子进程也会复制这些数据,导致 重复输出(尤其是
printf
/fprintf
/fwrite
等缓冲 I/O 操作)。
4. close(1)
(注释掉的代码)
-
如果取消注释,会关闭标准输出(文件描述符
1
),导致后续写入失败(但程序没有检查错误)。
运行结果分析
情况 1:直接运行(输出到终端)
-
由于终端默认是 行缓冲,
printf
、fprintf
遇到\n
会立即刷新,输出顺序正常:hello printf hello fprintf hello fwrite hello write
-
fork()
时缓冲区已刷新,不会导致重复输出。
情况 2:重定向到文件(如 ./a.out > log.txt
)
-
缓冲模式变为 全缓冲,
printf
、fprintf
、fwrite
的数据可能仍留在缓冲区中。 -
fork()
会复制缓冲区,导致 数据被写入两次:(write
是系统调用,不受影响,只输出一次。)
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和 fork有关! 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。 printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据 的缓冲方式由行缓冲变成了全缓冲。 而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后 但是进程退出之后,会统一刷新,写入文件当中。 但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的 一份数据,随即产生两份数据。 write 没有变化,说明没有所谓的缓冲。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统 调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由C标准库提供。
如何避免 fork()
导致的重复输出?
-
在
fork()
前刷新缓冲区:fflush(stdout); // 刷新 C 标准库缓冲区
-
使用无缓冲的
write
(但会失去 C 标准库的便利性)。
总结
-
C标准I/O(
printf
/fprintf
/fwrite
) 有缓冲机制,受fork()
影响。 -
系统调用(
write
) 无缓冲,不受影响。 -
终端 vs 文件 的缓冲策略不同,可能导致不同行为。
-
fork()
会复制未刷新的缓冲区,可能导致重复输出。
这段代码适合用来理解 缓冲机制 和 fork()
的副作用。
自定义文件I/O系统
Mystdio.h
//#pragma once
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#include <string.h>
#define SIZE 1024 // 缓冲区大小
// 刷新缓冲区的三种模式
#define FLUSH_NOW 1 // 立即刷新(每次写入后都刷新)
#define FLUSH_LINE 2 // 行刷新(遇到换行符时刷新)
#define FLUSH_ALL 4 // 全缓冲(缓冲区满时刷新)
// 自定义FILE结构体
typedef struct IO_FILE {
int fileno; // 文件描述符
int flag; // 刷新模式标志
char outbuffer[SIZE]; // 输出缓冲区
int out_pos; // 缓冲区当前写入位置
}_FILE;
// 函数声明
_FILE * _fopen(const char*filename, const char *flag); // 打开文件
int _fwrite(_FILE *fp, const char *s, int len); // 写入文件
void _fclose(_FILE *fp); // 关闭文件
#endif
Mystdio.c
#include "Mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#define FILE_MODE 0666 // 默认文件权限
// 打开文件函数
_FILE * _fopen(const char*filename, const char *flag) {
assert(filename); // 断言文件名不为空
assert(flag); // 断言打开模式不为空
int f = 0; // 用于存储open的标志
int fd = -1; // 文件描述符
// 根据不同的打开模式设置标志
if(strcmp(flag, "w") == 0) {
f = (O_CREAT|O_WRONLY|O_TRUNC); // 写模式,创建/截断文件
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "a") == 0) {
f = (O_CREAT|O_WRONLY|O_APPEND); // 追加模式
fd = open(filename, f, FILE_MODE);
}
else if(strcmp(flag, "r") == 0) {
f = O_RDONLY; // 只读模式
fd = open(filename, f);
}
else
return NULL; // 无效模式
if(fd == -1) return NULL; // 打开文件失败
// 分配_FILE结构体内存
_FILE *fp = (_FILE*)malloc(sizeof(_FILE));
if(fp == NULL) return NULL;
// 初始化_FILE结构体成员
fp->fileno = fd;
fp->flag = FLUSH_ALL; // 默认使用全缓冲模式
fp->out_pos = 0; // 缓冲区位置初始化为0
return fp;
}
// 写入文件函数
int _fwrite(_FILE *fp, const char *s, int len) {
// 将数据复制到缓冲区
memcpy(&fp->outbuffer[fp->out_pos], s, len);
fp->out_pos += len;
// 根据不同的刷新模式决定是否刷新缓冲区
if(fp->flag & FLUSH_NOW) { // 立即刷新模式
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
else if(fp->flag & FLUSH_LINE) { // 行刷新模式
if(fp->outbuffer[fp->out_pos-1] == '\n') { // 检查是否遇到换行符
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
else if(fp->flag & FLUSH_ALL) { // 全缓冲模式
if(fp->out_pos == SIZE) { // 检查缓冲区是否已满
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0;
}
}
return len; // 返回写入的字节数
}
// 强制刷新缓冲区函数
void _fflush(_FILE *fp) {
if(fp->out_pos > 0) { // 如果缓冲区有数据
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0; // 重置缓冲区位置
}
}
// 关闭文件函数
void _fclose(_FILE *fp) {
if(fp == NULL) return;
_fflush(fp); // 刷新缓冲区
close(fp->fileno); // 关闭文件描述符
free(fp); // 释放_FILE结构体内存
}
main.c
#include "Mystdio.h"
#include <unistd.h>
#define myfile "test.txt" // 测试文件名
int main() {
// 以追加模式打开文件
_FILE *fp = _fopen(myfile, "a");
if(fp == NULL) return 1; // 打开失败则退出
const char *msg = "hello world\n"; // 测试消息
int cnt = 10; // 写入次数
// 循环写入10次,每次间隔1秒
while(cnt) {
_fwrite(fp, msg, strlen(msg));
sleep(1); // 暂停1秒
cnt--;
}
_fclose(fp); // 关闭文件
return 0;
}