铺垫
a.文件=内容+属性(对于空文件,内容是空的,但是属性不是空的,所以在磁盘中占据空间)
b.访问文件前,都得先打开。修改文件,都是通过执行代码的方式完成修改,文件必须要加载到内存中,通过CPU在内存中对文件修改。
c.谁打开文件?进程在打开文件。
d.一个进程可以打开多个文件。
e.系统中是不是所有的文件都被进程打开了?不是。没有被打开的文件在磁盘中。
正在被打开的文件——内存级文件,没有被打开的文件——磁盘文件
一段时间内,系统中存在多个进程,也可能同时存在更多的被打开的文件,OS要不要管理多个被进程打开的文件呢?肯定的。如何管理?先描述在组织。
首先打开一个文件
1 #include<stdio.h>
2
3 int main()
4 {
5 FILE *fp = fopen("./log.txt","w");//第二参数为打开的方式
6 if(fp == NULL)
7 {
8 perror("fopen");
9 return 1;
10 }
11 fclose(fp);
12 return 0;
13 }
w:以写write方式打开文件,该文件内容会被自动清空。 >
a:以追加append方式打开文件,文件内容不会被清空。>>
fwrite(字符串,长度,写入基本单元个数,文件指针)
返回值是向文件写入基本单元的个数。
什么叫当前路径?
进程在启动时,会自动记录自己启动时所在的路径,所以文件创建时会创建在当前路径下。
进程启动路径决定文件创建的位置
例子:
1 #include<stdio.h>
2 #include<string.h>
3 #include<sys/types.h>
4 #include<unistd.h>
5 int main()
6 {
7 FILE *fp = fopen("./log.txt","w");
8 if(fp == NULL)
9 {
10 perror("fopen");
11 return 1;
12 }
13
14 const char* str = "hello world\n";
15 int cnt = 5;
16 while(cnt)
17 {
18 int n = fwrite(str,strlen(str),1,fp);
19 printf("write %d block,pid is : %d\n",n,getpid());
20 cnt--;
21 sleep(20);
22 }
23
24 fclose(fp);
25 return 0;
26 }
运行后获取进程id
找到该进程的位置
发现该进程中存有当前路径
程序默认打开的文件流
程序在启动时默认打开的文件流
stdin 标准输入
stdout 标准输出
stderr 标准错误
stdin,stdout,stderr可以直接被使用!
例子:
1 #include<stdio.h>
2 #include<string.h>
3 int main()
4 {
5 printf("hello printf\n");
6 fputs("hello fputs\n",stdout);
7 const char* msg = "hello fwrite\n";
8 fwrite(msg,1,strlen(msg),stdout);
9 fprintf(stdout,"hello fprintf\n");
10 return 0;
11 }
printf函数代码实现的过程中封装了stdout,使得字符串直接写入到了stdout,也就是c语言的写入接口都用到了它们,有参就直接用它们,无参,内部就封装了它们。
系统调用接口——open,write,close,read
访问文件不仅仅有c语言上的文件接口,OS必须提供文件的系统调用接口,语言上的文件接口封装了系统调用的接口。
open
#include <fcntl.h> // 标志位定义
#include <sys/stat.h> // mode_t 权限定义int open(const char* pathname,int flags)//如果已有文件,按照flags方式打开文件
int open(const char* pathname,int flags,mode_t mode)//没有文件,要新建文件,需要给予权限,第三个参数是权限位
1.pathname(路径名)
要打开或创建的文件路径(绝对路径或相对路径)。
2.flags(标志位)
关于flags参数
O_RDONLY: 只读方式打开文件。
O_WRONLY: 只写方式打开文件。O_RDWR: 读写方式打开文件。
O_CREAT: 如果文件不存在则创建新文件。
O_TRUNC: 如果文件存在且以写模式打开,则将其长度截断为零(就是将文件清空)。
O_APPEND: 以追加模式打开文件,即所有写入操作都将在文件末尾进行。O_NONBLOCK: 以非阻塞模式打开文件。
O_SYNC: 以同步写入方式打开文件,确保写操作完成后数据被写入磁盘。
注:这些参数本质上是宏定义的一些整形,通过按位或( | )连接从而发生作用。
3.mode(权限位)
a.当 flags 包含 O_CREAT时需指定,定义新文件的权限(八进制数或宏,如 0644
)。
b.权限受 umask 影响,实际权限为mode & (~umask)。
操作 | 权限变化 |
---|---|
设置 mode = 0666 | 期望权限:用户、组、其他人可读写( |
若 umask = 0000 | 实际权限:用户可读写,组和其他人只读( |
若umask = 0002 | 实际权限:用户和组可读写,其他人只读( |
返回值
成功:返回文件描述符(非负整数),通常从 3
开始(0
、1
、2
为标准输入、输出、错误)。
失败:返回 -1,并设置全局变量 errno 表示错误类型。
例子:
1 #include<stdio.h>
2 #include<fcntl.h>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5 int main()
6 {
7 umask(0);//设置权限掩码为000,默认权限掩码是0002
8 int fd = open("log.txt",O_WRONLY|O_CREAT,0666);//这时真正的权限为0666&(~权限掩码)
9 if(fd==-1)
10 {
11 perror("open");
12 return 1;
13 }
14 return 0;
15 }
结果是创建了一个权限为rw-rw-rw-的log.txt的文件
注意open文件后应当检查
int fd = open(pathname, flags, mode);
if (fd == -1) {
perror("open failed");
// 根据 errno 处理
}
close
#include <unistd.h>
int close(int fd);
关闭一个文件,需要传入的参数是要关闭文件的文件描述符fd。
返回值
成功:返回 0
。
失败:返回 -1
,并设置全局变量 errno
表示错误类型。
常见错误类型(errno)
EBADF
:fd
不是有效的文件描述符。
EINTR
:操作被信号中断(需特殊处理)。
EIO
:底层 I/O 错误(如磁盘故障)。
注意close文件后检查
if (close(fd) == -1) {
perror("close failed");
}
write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
1.fd(文件描述符)
2.buf(缓冲区指针):指向用户空间中待写入数据的缓冲区地址。
3.count(字节数):期望写入的字节数的大小。
返回值
成功:返回实际写入的字节数(可能小于 count,甚至为 0)。
失败:返回 -1,并设置全局变量 error 表示错误类型。
read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
1.fd(文件描述符)
2.buf(缓冲区指针):指向用户空间读取数据的缓冲区地址。
3.count(字节数):期望读取的最大字节数的大小。
返回值
成功:返回实际读取的字节数(可能小于 count,甚至为 0)。
失败:返回 -1,并设置全局变量 error 表示错误类型。
例子:
从标准输入读取数据:
char buffer[1024];
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
通过系统调用接口open模拟实现c语言中fopen的功能
a.模拟FILE *fp = fopen("./log.txt","w");
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
b.模拟FILE *fp = fopen("./log.txt","a");
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
注:本质上c语言中的fopen接口封装了系统调用接口open,推而广之,很多c语言的接口都封装了系统调用接口!
FILE是c语言封装的结构体,里面封装了文件描述符(fd)
例子:
int main()
{
printf("%d\n",stdin->_fileno);//stdin的类型是FILE*,成员变量_fileno就是文件描述符
printf("%d\n",stdout->_fileno);
printf("%d\n",stderr->_fileno);
return 0;
}
结果发现,原因是stdin,stdout,stderr是默认打开的,实际上这三个都对应了相应的fd。
数据流 | 文件描述符(fd) |
stdin 标准输入流(键盘文件) | 0 |
stdout 标准输出流(显示器文件) | 1 |
stderr 标准错误流(显示器文件) | 2 |
以后再新建文件时其文件描述符依次从2向后递增。
为什么C语言要封装?
为了保证可移植性。不同平台有不同平台的系统调用(Windows,linux……),用某一平台的系统调用写的一份代码,在其他平台下不一定能使用,如果用c语言对不同平台进行系统调用接口的封装,这样只用c语言的接口就可以在不同平台上使用同一份代码,保证了可移植性。
认识fd
用一张图来解释
这就解释了为什么访问文件,用系统调用接口,都必须传入文件描述符fd这个参数
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);
原因:进程调用系统接口时,传入文件描述符fd,这样通过进程的地址找到进程中对应的文件列表,根据fd,找到指针数组的对应的地址,从而找到需要操作的文件。
结论:文件描述符的本质就是数组的下标!
如何理解linux系统下一切皆是文件?
不同文件只要通过结构体中的函数指针指向对应的读写方法,不在乎硬件的差别,消除了底层硬件的差异,进程看待一切硬件都是以结构体struct file的形式,这就是linux系统下一切皆是文件。这种设计理念与C++多态也有关系。
文件描述符fd的分配规则
分配规则:最小的没有被使用的数组下标,会分配给最新的打开的文件。
利用分配规则来实现重定向。
例子:
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd: %d\n",fd);
return 0;
}
结果是向log.txt文件中写入了fd:1,而不是打印在屏幕上。
原因: fd=1对应的文件是显示器文件,将显示器文件关闭后,再打开一个新的文件,根据fd的分配规则会将fd=1分配给log.txt,而printf默认向显示器文件中输出,实际上是向fd=1的文件输出所以,最后向log.txt中输出。
例子:
int main()
{
close(1);//shuchu
open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("printf\n");
fprintf(stdout,"fprintf\n");
return 0;
}
两种方法都是向log.txt文件中写入。注意这里虽然fprintf中的参数是stdout但是仍然是向fd=1这个文件中输入。
另一种简单的重定向方法
OS系统提供相应的拷贝接口,只要把相应文件的拷贝到fd=1位置即可。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
int dup3(int oldfd, int newfd, int flags);
介绍dup2
int dup2(int oldfd, int newfd);
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary
注意:newfd成为oldfd的拷贝,newfd变成oldfd。
例子:
1 #include<stdio.h>
2 #include<fcntl.h>
3 #include<unistd.h>
4 int main()
5 {
6 int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
7 dup2(fd,1);//将fd位置的文件拷贝到fd=1(显示器文件)的位置
8 printf("printf\n");
9 return 0;
10 }
例子:
1 #include<stdio.h>
2 #include<fcntl.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 int fd = open("log.txt",O_RDONLY);
8 dup2(fd,0);
9 char buffer[1024];
10 while(1)
11 {
12 char* s = fgets(buffer,sizeof(buffer),stdin);
13 if(s==NULL)break;
14 printf("file content:%s",buffer);
15 }
16 return 0;
17 }
缓冲区问题
缓冲区本质就是一块内存.
为什么要有缓冲区?
提高使用者的效率。缓冲区聚集数据,一次拷贝,提高整体效率。
我们平常说的缓冲区和内核中的缓冲区没有关系,我们平时说的缓冲区是语言层面的缓冲区,是C语言自带的缓冲区。
我们知道fwrite/printf/fputs……底层封装了系统调用接口write……,但是fwrite/printf/fputs…… 并不是直接调用系统调用接口,而是达成某种条件。其原理如下
语言层缓冲区的刷新策略主要是有以下几种:
1.无刷新,无缓冲(几乎没有使用)。
2.行刷新,行缓冲——显示器(stdout),xxx\n,遇到换行刷新,也可以手动刷新或缓冲区满了刷新。
3.全刷新,全缓冲——普通文件,缓冲区被写满才刷新。
a.强制刷新(fflush函数)。
b.进程退出的时候,要自动刷新。
C语言的缓冲区具体在哪里?
File *fp = fopen(“log.txt”,"w");
File是一个结构体,里面不仅封装了fd,还会维护一段缓冲区,所以每一个文件都有一个缓冲区
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fputs(const char *s, FILE *stream);
通过例子证明c语言存在缓冲区
1.一段代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main()
6 {
7 //使用系统调用
8 const char* s1 = "write\n";
9 write(1,s1,strlen(s1));
10
11 //使用C语言接口
12 const char* s2 = "fprintf\n";
13 fprintf(stdout,"%s",s2);
14
15 const char* s3 ="fwrite\n";
16 fwrite(s3,strlen(s3),1,stdout);
17
18 return 0;
19 }
代码分析:
1. write 是系统调用,无缓冲,直接刷新。
2.fprintf 是C语言库中的函数,使用 stdout 的缓冲区。stdout输出到屏幕时刷新方案是行缓冲,遇到 \n 自动刷新缓冲区,字符串中包含‘\n’会刷新。
3.fwrite 是C语言库中的函数,写入数据到 stdout ,stdout输出到屏幕时刷新方案是行缓冲。字符串中包含‘\n’会刷新。
结果分析:
当输入到屏幕时,stdout的刷新策略是行缓冲,字符串结尾有‘\n’,所以结果就是都打印一遍。
当输入到文件时,stdout的刷新策略是全缓冲,所以是先打印write,fprintf与fwrite在c缓冲区中储存。当程序退出时自动刷新,所以结果一样。
直接运行打印到屏幕,没有问题。
重定向到文件里,也没有问题。
2.然后在代码最后加上fork()。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int main()
6 {
7 //使用系统调用
8 const char* s1 = "write\n";
9 write(1,s1,strlen(s1));
10
11 //使用C语言接口
12 const char* s2 = "fprintf\n";
13 fprintf(stdout,"%s",s2);
14
15 const char* s3 ="fwrite\n";
16 fwrite(s3,strlen(s3),1,stdout);
17
18 fork();
19 return 0;
20 }
代码分析与上面一致。
结果分析:
直接运行打印到屏幕时,stdout的刷新策略仍是行缓冲,所有输出在fork()调用前已刷新,父进程和子进程的缓冲区均为空,fork()不会复制任何未刷新数据,因此无重复输出。
当输出重定向到文件时,stdout 变为全缓冲模式,仅在三种情况下刷新:1.缓冲区填满 2.程序退出 3.显示调用 fflush(stdout)。注意:代码中的‘\n’不会触发刷新!
fork()前:
1.write是系统调用,不受语言级别的缓冲区影响,直接写入文件。
2. fprintf 和 fwrite 的输出会暂时存在于 stdout 的缓冲区中,没有被刷新。
fork()后:
fork()会复制父进程的缓冲区到子缓冲区,导致父缓冲区和子缓冲区都有一份未刷新的数据。
当程序退出时,父进程和子进程会分别刷新自己的缓冲区,导致 fprintf 和 fwrite 的输出被写入文件两次。
所以有以下结果:
直接运行打印到屏幕,没有问题。
但是重定向到文件,出现了差别。
注:对于C语言中的printf与scanf,当使用时,输入的内容实际上储存在C语言的缓冲区中,当达成某种条件时(强制刷新),才会通过系统调用接口进入系统内部的缓冲区,从而通过硬件输入或输出。
附录
c标准库的一些函数
fputs
#include <stdio.h>
int fputs(const char *str, FILE *stream);
参数说明
1.str(字符串):要写入的字符串(以 \0 结尾)。
2.stream(目标文件流指针):要打开或使用的文件流(如stdout)。需通过 fopen 打开或使用标准流。
返回值
成功:返回非负整数(通常是 0
或正值)。
失败:返回 EOF(通常是 -1
),需通过 ferrror 或 errno 进一步检查错误原因。
例子:
fputs("Hello, World!\n", stdout); // 输出到屏幕
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w");
if (!file) {
perror("fopen failed");
return 1;
}
const char *text = "This is a line of text.\n";
if (fputs(text, file) == EOF) {
perror("fputs failed");
fclose(file);
return 1;
}
fclose(file); // 关闭文件会刷新缓冲区
return 0;
}
fgets
#include <stdio.h>
char *fgets(char *str, int size, FILE *stream);
参数说明
1.str:用于存储读取数据的字符数组(缓冲区)。必须预先分配足够内存(通常为 size 字节)
2.size:最大读取字符数(包括结尾的 \0),通常设为缓冲区长度。实际最多读取 size - 1 个字符,最后一个位置自动填充 \0。
3.stream:输入文件流指针(如 stdin 、文件指针等)。注意需通过 fopen
打开或使用标准输入流。
返回值
成功:返回 str 的指针。
失败 或 到达文件末尾:返回 NULL,需通过ferror区分原因。
例子:
#include <stdio.h>
int main() {
char buffer[100];
printf("Enter a line: ");
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
printf("You entered: %s", buffer);
} else {
printf("Error or EOF reached.\n");
}
return 0;
}
fwrite
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数说明
1.ptr:指向待写入数据的内存地址(可以是数组、结构体、基本类型变量等)。
2.size:每个数据单元的字节大小(如:sizeof(int))。
3.nmemb:要写入的数据单元数量(如:int a[10]有10个单元)。
误区 1:
nmemb
必须等于数组长度
错误:认为
nmemb
只能用于数组。正确:
nmemb
表示逻辑单元数量,可以是任意分割方式。例如:int array[100]; // 将数组分为 4 个块,每块 25 个 int fwrite(array, sizeof(int)*25, 4, file); // nmemb = 4(每个单元 25 个 int)
误区 2:
size
必须等于数据类型大小
错误:认为
size
必须匹配单个变量的大小。正确:
size
可以是任意值,只要与nmemb
组合后匹配总字节数。例如:char buffer[4096]; fwrite(buffer, 512, 8, file); // 将 4096 字节分为 8 个 512 字节的块
4.stream(目标文件流指针):要使用的文件流,注意需通过 fopen 打开文件流。
返回值
成功:返回实际写入的数据单元数量(若完整写入,返回值等于nmemb)。
失败:返回值小于nmemb,需通过ferror检查错误或文件结束。
例子:
#include <stdio.h>
typedef struct {
int id;
char name[32];
float score;
} Student;
int main() {
Student students[] = {
{1, "Alice", 90.5},
{2, "Bob", 85.0}
};
FILE *file = fopen("students.dat", "wb");
if (!file) {
perror("fopen failed");
return 1;
}
// 写入整个结构体数组
size_t num = sizeof(students) / sizeof(Student);
size_t written = fwrite(students, sizeof(Student), num, file);
if (written != num) {
perror("fwrite failed");
fclose(file);
return 1;
}
fclose(file);
return 0;
}
fread
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数说明
1.ptr:指向存储读取数据的内存地址,要预留至少 size * nmemb 字节空间。
2.size:每个数据单元的字节大小。
3.nmemb:要读取的数据单元数量(如数组长度)。
4.stream:输入文件流指针。
返回值
成功:返回实际读取的数据单元数量(若完整读取,返回值等于nmemb)。
失败或文件结束:返回值小于 nmemb,需通过 ferror 进一步判断原因。
例子:
从 stream中读取 size*nmemb 字节数据到 ptr 指向的内存。
int data[10];
fread(data, sizeof(int), 10, file); // 读取 10 个 int
fprintf
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);
参数说明
1.stream(目标文件流指针):要使用的文件流,注意需通过 fopen 打开文件流。
2.format:格式化字符串,包含普通字符和格式说明符(如 %d,%s)。格式说明符定义后续参数如何转换和输出。
通过格式说明符处理不同类型的数据:
格式符 | 类型 | 示例 |
---|---|---|
%d | 整数 | int x = 10; |
%f | 浮点数 | float y = 3.14; |
%s | 字符串(以 \0 结尾) | char *s = "text"; |
%c | 单个字符 | char c = 'A'; |
%p | 指针地址 | void *ptr; |
3. …(可变参数):根据 format 中的格式说明符,传入对应的变量或值。
返回值
成功:返回写入的字符总数(不包括结尾的 \0
)。
失败:返回负值(通常为 -1
),可通过 ferror 或 errno 检查错误。
例子:
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w");
if (!file) {
perror("fopen failed");
return 1;
}
int count = 5;
double value = 3.1415;
fprintf(file, "Iteration: %d\nValue: %.2f\n", count, value); // 保留两位小数
fclose(file);
return 0;
}
fflush
#include <stdio.h>
int fflush(FILE *stream);
参数说明
1.stream:要刷新的文件流指针 或 特殊值 NULL
:刷新所有打开的输出流(标准未强制要求,依赖具体实现)。
返回值
成功:返回0。
失败:返回EOF,并设置全局变量 error 表示错误类型。
例子:
刷新输出流:将缓冲区内的数据立即写入文件或设备。
示例:强制显示未换行的 printf
输出:
printf("Processing...");
fflush(stdout); // 立即显示到终端