前言
1)在Linux下面,一切皆文件,文件=文件内容+文件属性
2)在访问文件是,都得先将文件打开,修改文件的本质其实还是通过执行代码的形式修改。
3)文件是被进程打开的,一个进程可以打开多个文件,操作系统会将打开的文件进行管理,被打开的文件被加载到内存中,称为内存文件,未被打开的文件存放在磁盘上,称为磁盘文件。
本文对进程和文件之间的关系,以及操作系统如何对文件进行管理
一、C语言接口与重定向
在C语言中,提供了打开文件接口fopen,通过查询man手册,其参数有两个,第一个为想要打开的文件名,第二个为打开方式,打开方式有以下12中,其中r表示读,w表示写,a表示追加,b表示二进制,+表示读写,只要带r的打开方式文件不存在就报错,只要带w的打开方式文件不存在就创建。以w形式打开文件,文件会被自动清空,以a方式打开文件,向文件内写入内容,内容追加到文件末尾
打开方式 | 含义 | 文件不存在处理方式 |
r | 只读 | 报错 |
w | 只写 | 创建新文件 |
a | 只写,打开后文件指针指向末尾 | 创建新文件 |
rb | 只读,打开二进制文件 | 报错 |
wb | 只写,打开二进制文件 | 创建新文件 |
ab | 只写,打开二进制文件后指针指向末尾 | 报错 |
r+ | 读写 | 出错 |
w+ | 读写 | 创建新文件 |
a+ | 读写,打开后文件指针指向末尾 | 创建新文件 |
rb+ | 读写,打开一个二进制文件 | 报错 |
wb+ | 读写,打开一个二进制文件 | 创建新文件 |
ab+ | 读写,打开二进制文件后指针指向末尾 | 创建新文件 |
验证重定向的文件打开方式
[root@hcss-ecs-e53a test5]# cat log.txt
hello world
[root@hcss-ecs-e53a test5]# echo "hello">log.txt
[root@hcss-ecs-e53a test5]# cat log.txt
hello
[root@hcss-ecs-e53a test5]# >log.txt
[root@hcss-ecs-e53a test5]# cat log.txt
使用>重定向后,发现原来的内容被覆盖,如果左边为空,重定向结果是文件内容被清空,所以可以推断出,>重定向的打开文件方式是w
[root@hcss-ecs-e53a test5]# echo "hello world" >>log.txt
[root@hcss-ecs-e53a test5]# cat log.txt
hello world
[root@hcss-ecs-e53a test5]# echo "hello world" >>log.txt
[root@hcss-ecs-e53a test5]# echo "hello world" >>log.txt
[root@hcss-ecs-e53a test5]# echo "hello world" >>log.txt
[root@hcss-ecs-e53a test5]# echo "hello world" >>log.txt
[root@hcss-ecs-e53a test5]# cat log.txt
hello world
hello world
hello world
hello world
hello world
使用>>对文件重定向,发现原来的内容没有被覆盖,新的内容追加到了文件末尾,所以推断出,>>重定向的打开方式是a
二、C语言文件读写接口
fgets(char *s, int size, FILE *stream),整行读取,遇到回车换行或结尾停止.在文本方式时使用。且会在末尾追加一个'\0',占据一个字符,所以实际读取字符数为size-1。
fread(void *ptr, size_t size, size_t nmemb, FILE *stream),读取二进制流,参数为写入的指针,读取单元字符大小,字符数量,目标流。末尾会插入一个
fputs(const char*str,FILE*stream),写入字符串流,参数为字符串和需要写入的文件流。
fwrite(const void*ptr,size_t size,size_t nmemb,FILE* stream),写入二进制流,参数为需要写入的内容,基本单元的大小,需要写入几个基本单元,以及需要写入的文件流。返回值为写入了多少个基本单位
三、Linux下文件I/O接口
1.当前路径
进程在启动的时候,会自动记录当前所在的路径,可以在/proc/PID/cwd查询到当前进程所对应的当前路径
2.程序默认打开的文件流
在开发操作过程中,开发人员
stdin:标准输入-->键盘设备
stdout:标准输出-->显示器设备
stderr:标准错误-->显示器设备
所以,可以对stdout和stderr进行写入后显示屏显示指定内容。例如,使用printf对stdout进行写入,使用fprintf对指定流进行写入等。可以通过读取stdin内容读取键盘输入的数据,例如,使用scanf和fread读取标准输入流数据。
3.文件操作
1.通过C语言操作文件
2.通过系统调用操作文件
1)open
open(const char * filename,int flags),以特定的方式打开文件,返回值为文件打开状态。返回值为文件描述符,返回-1表示文件打开失败。
flags | 含义 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
O_CREAT | 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 |
O_APPEND | 追加写 |
O_TRUNC | 清空文件内容 |
在open的flags参数,前三个宏必须存在且仅存在一个,后续选项如有需要直接与前三者进行按位或操作即可。如果文件不存在,则需要在flags参数后面追加mode参数以表示该文件的权限。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(){
int fp = open("log11.txt",O_RDONLY|O_CREAT,0777);
if(fp == -1){
perror("open error\n");
return 1;
}
return 0;
}
通过观察创建后文件权限位可以看到,指定的文件权限为0777,但是创建后文件的权限为0755,这是因为,在设置文件的权限位的时候,不仅受参数影响,同时会受到umask的影响。如果不想收到umask的影响,可以使用系统接口umask,将该进程下umask设置为0,但是不影响其他地方的umask。
设置前:
设置后:
2)close
close(int fd)关闭打开的文件
3)write
ssize_t write(int fd, const void *buf, size_t count);向指定的文件写入内容,参数fd(目标文件的文件描述符)、buf(需要写入的内容)、count(需要写入的字符数)。如果文件原来有内容,则直接进行覆盖而不是将内容清空。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(){
int fd = open("log.txt",O_WRONLY);
if(fd == -1){
perror("open error\n");
return -1;
}
const char* str = "hahaha\n";
write(fd,str,strlen(str));
close(fd);
int fd1 = open("text.txt",O_WRONLY);
if(fd1 == -1){
perror("open error\n");
return -1;
}
write(fd1,str,strlen(str));
close(fd1);
return 0;
}
4)read
ssize_t read(int fd, void *buf, size_t count);读取指定的文件,参数fd(目标文件描述符),buf(内容保存位置),count(表示读取的字符个数)
int main(){
int fd = open("log.txt",O_RDONLY);
if(fd == -1){
perror("error\n");
return -1;
}
char buffer[1024];
ssize_t s = read(fd,buffer,sizeof(buffer));
if(s>0){
buffer[s]=0;
printf("%s\n",buffer);
}
close(fd);
return 0;
}
四、Linux下的文件I\O
1)fd文件描述符
open的返回值是一个int类型的数字表示文件描述符, -1表示打开失败。打开5个文件,观察他们的fd
int main(){
int fd1 = open("log1.txt",O_WRONLY|O_CREAT);
int fd2 = open("log2.txt",O_WRONLY|O_CREAT);
int fd3 = open("log3.txt",O_WRONLY|O_CREAT);
int fd4 = open("log4.txt",O_WRONLY|O_CREAT);
int fd5 = open("log5.txt",O_WRONLY|O_CREAT);
printf("fd1 is %d\n",fd1);
printf("fd2 is %d\n",fd2);
printf("fd3 is %d\n",fd3);
printf("fd4 is %d\n",fd4);
printf("fd5 is %d\n",fd5);
}
发现他们的fd从3开始依次递增。因为0表示标准输入,1表示标准输出,2表示标准错误。
2)Linux进程文件描述
在Linux系统下面,有一个结构体叫做file 的结构体将打开的文件进行描述管理,结构体中包括了文件的属性、方法集和缓冲区。操作系统将所需要打开的文件从磁盘中读取出来,然后以链表的形式串起来进行管理。
在进程控制块结构体中,存在一个files_strucet指针,指向一个files_struct结构体对象,这个对象的作用是将进程打开的文件统一管理起来。在file_struct里面,存在一个叫做struct file*fd_array[]的数组(文件描述符表),用于存放文件指针,即将系统打开的文件列表中的指针。而文件对应的下标就是fd(文件描述符),所以可以通过文件描述符对文件进行操作。
在C语言中,FILE结构体本质就是对struct file进行封装后的结果。
3)文件fd的文件分配规则
在Linux下面,fd为0,1,2分别为标准输入,标准输出和标准错误,所以,在打开文件时,默认是从3开始依次递增,所以下面代码运行结果为3,4。
int main(){
int fd1 = open("log1.txt",O_RDONLY|O_CREAT);
int fd2 = open("log2.txt",O_RDONLY|O_CREAT);
printf("fd1 is:%d\nfd2 is:%d\n",fd1,fd2);
return 0;
}
如果将标准输入关闭,再打开新的文件,结果变成了0,3,分析其原因是stdin打开时,0被占用,当将stdin关闭后,
int main(){
close(0);
int fd1 = open("log1.txt",O_RDONLY|O_CREAT);
int fd2 = open("log2.txt",O_RDONLY|O_CREAT);
printf("fd1 is:%d\nfd2 is:%d\n",fd1,fd2);
return 0;
}
所以得出结论,fd的分配规则按照文件打开的顺序,从0开始依次分配未使用的数组下标,即为fd。
4)文件重定向
在上面的例子中,可以通过fd关闭标准文件流,再打开文件时,原文件流的fd会被赋给其他文件,该现象就称为文件重定向。
int main(){
close(0);
close(1);
int fd1 = open("log1.txt",O_RDONLY);
int fd2 = open("log2.txt",O_WRONLY);
char str[1024];
scanf("%s",str);
printf("%s\n",str);
return 0;
}
在代码中,关闭了标准输入标准输出流,再打开了文件log1.txt和log2.txt,此时这两个文件的fd分别为0和1,再调用scanf和printf函数,发现可以通过这连个函数进行从文件中读和向文件中写。
重定向的底层原理,在操作系统层,将进程对应的文件描述列表对应下标替换为目标文件,而scanf和printf的底层其实是向下标为0和1的文件读取/写入内容。如果以只写的方式打开文件但是不进行写操作,目标文件会被清空。
5)dup2接口
在上面已经可以实现输出和输入重定向,但是存在一点问题,需要将原文件关闭再按照顺序打开文件。在linux下面,有一个接口,dup2可以直接将文件的fd进行替换,从而实现文件重定向。
dup2(int oldfd,int newfd),oldfd(需要拷贝的目标文件的fd),newfd(需要被替换的文件fd)。
所以上面的代码就可以进行以下修改
int main(){
int fd1 = open("log1.txt",O_RDONLY);
int fd2 = open("log2.txt",O_WRONLY);
dup2(fd1,0);
dup2(fd2,1);
char str[1024];
scanf("%s",str);
printf("%s\n",str);
return 0;
}
五、C语言缓冲区
1)什么是缓冲区
文件缓冲区其实就是内存里面的一块空间,在C语言中,其意义是文件操作只需要将需要读写的文件交给缓冲区,再由缓冲区与系统调用直接进行交互,减少了C语言接口调用时间,同时通过暂存的方式,减少了系统调用的次数,从而提高读写效率。
2)缓冲区在哪里
在打开文件时,调用fopen接口,会返回一个FILE*指针,FILE是一个结构体,里面包含了fd和缓冲区指针。即每一个打开的文件都有一个·独立的缓冲区。在调用C语言接口对文件进行读取的时候,有一个FILE*的参数,其实质就是将文件里的内容拷贝到缓冲区里面,再内部决定何时进行缓冲区的刷新。
3)缓冲区刷新策略
1、无刷新
无刷新就是读取一个数据就即时将内容交给系统,
2、行刷新
当数据遇到'\n'或者'\r'时就进行刷新
3、全刷新
等数据完全完成读写后再对缓冲区进行刷新
4)缓冲区现象观察
由于标准输出默认是输出到显示屏,属于行刷新,不便于观察,所以在输出的时候将其重定向到log.txt文件中。
int main(){
const char* s1="111111111write\n";
const char* s2="222222222fprintf\n";
const char* s3="333333333fwrite\n";
write(1,s1,strlen(s1));
fprintf(stdout,"%s",s2);
fwrite(s3,strlen(s3),1,stdout);
}
到目前正常按照顺序将字符串内容写入到log.txt当中,现在在test.c文件末尾加上fork(),再次运行,观察现象。
int main(){
const char* s1="111111111write\n";
const char* s2="222222222fprintf\n";
const char* s3="333333333fwrite\n";
write(1,s1,strlen(s1));
fprintf(stdout,"%s",s2);
fwrite(s3,strlen(s3),1,stdout);
fork();
}
发现后面两个字符串被写入了两次,而第一个字符串只写入一次,第一个字符串使用的是系统接口调用,而后面两个是使用的C语言函数接口。其原因是使用C语言文件接口后,文件内容其实是被写到了文件的缓冲区当中,而并没有直接写入到log.txt文件当中,当fork后,发生写时拷贝,缓冲区以及里面的内容也被子进程拷贝,在进程结束时。子进程和父进程都需要刷新缓冲区,此时就将缓冲区里面的内容通过系统调用写入到了log.txt文件当中,而write系统调用直接就将文件内容写入到了log.txt当中。