目录
[1.> 和 >>](#1.> 和 >>)
一、文件基础认识
- 文件 = 内容 + 属性。换句话说,如果在电脑上新建了一个空白文档,它虽然没有内容,但也是占据磁盘空间的。
- 想要修改一个文件的内容,比如用WPS这样的软件操作文件内容,本质上都需要CPU完成相关的指令,而CPU又只与内存交互,所以,打开文件的含义其实就是把文件加载到内存中。
- 在我们眼里,我们双击了一个文件就是打开了文件,但是在操作系统看来,并不是我们打开了文件,而是某一个正在运行的进程,文件是由进程打开的。
- 一个进程可以打开多个文件。
- 操作系统管理多个被打开文件,必然也会像操作系统管理多个进程一样,利用面向对象和数据结构,因此,内核中必然定义了结构体来描述被打开的文件。
- 从操作系统管理文件的角度看,文件被区分为被打开的文件(在内存中)和没有打开的文件(在磁盘中)。
二、C语言操作文件的接口
fopen 以**"w"**方法打开一个文件。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("aaa.txt","w");
if(pf == NULL)
{
perror("fopen:");
return 1;
}
const char* str = "aaaaaaaaaaaaaaaaaaaaaa\n";
fputs(str,pf);
fclose(pf);
return 0;
}
bash
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ ./a.out
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
aaaaaaaaaaaaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$
结果显示,文件aaa.txt中已经写入了一段字符串。修改源代码,将写入字符串的代码删除后,再执行编译运行一次。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("aaa.txt","w");
if(pf == NULL)
{
perror("fopen:");
return 1;
}
// const char* str = "aaaaaaaaaaaaaaaaaaaaaa\n";
// fputs(str,pf);
fclose(pf);
return 0;
}
bash
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
aaaaaaaaaaaaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ gcc file.c
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ ./a.out
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$
结果表明,aaa.txt 文件中的内容都消失了。原因在于fopen打开文件的方式"w" ,使用man 手册查看fopen打开文件方式的说明。
"w"方式打开文件时,会先清空文件中的所有内容。如果想保留文件中原来的内容做写入操作,就应该使用"a"的方式打开文件。
1.> 和 >>
bash
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo aaaaaaaaaaaa > aaa.txt
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
aaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo bbbbbbbbb > aaa.txt
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
bbbbbbbbb
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$
通过echo 做重定向操作向aaa.txt 文件中先后写入两次,最终效果并不是有两段字符串,说明重定向操作符**">"** 打开文件的方式本质上也是**"w"** 的方式。(需要一提的是,echo重定向到文件中,本质上也要修改文件的内容,所以一定会打开文件)。
bash
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo aaaaaaaaaaaaaaaa >> aaa.txt
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
aaaaaaaaaaaaaaaa
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ echo bbbbbbbbbbbbbbbb >> aaa.txt
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$ cat aaa.txt
aaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbb
utocoo@utocoo-virtual-machine:~/Desktop/linux/241121$
而追加重定向操作符**" >> "** 先后向aaa.txt 文件写入两次后,最终效果是两段字符串都被保留了下来,说明**" >> "** 其实和**"a"**方式类似,是一种追加的形式。
2.理解"当前路径"
在使用C接口操作文件的时候,经常会听到说,"如果没有这个文件,则在当前路径下新建这个文件",如何理解这个**"当前路径"**?
最简单直接的理解,就是我们当前程序的路径。
cpp
//file.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("aaa.txt","w");
if(pf == NULL)
{
perror("fopen:");
return 1;
}
fclose(pf);
return 0;
}
当前路径就是file.c 文件所在路径,编译运行前,该路径下没有aaa.txt 文件,编译运行后,该路径下存在名为aaa.txt的文件。
bash
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ ll
总计 16
drwxrwxr-x 2 utocoo utocoo 4096 11月 22 12:22 ./
drwxrwxr-x 16 utocoo utocoo 4096 11月 22 12:19 ../
-rw-rw-r-- 1 utocoo utocoo 233 11月 22 12:19 file.c
-rw-rw-r-- 1 utocoo utocoo 64 11月 22 12:21 Makefile
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ make
gcc -o file file.c
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ ./file
utocoo@utocoo-virtual-machine:~/Desktop/linux/241122$ ll
总计 32
drwxrwxr-x 2 utocoo utocoo 4096 11月 22 12:23 ./
drwxrwxr-x 16 utocoo utocoo 4096 11月 22 12:19 ../
-rw-rw-r-- 1 utocoo utocoo 0 11月 22 12:23 aaa.txt
-rwxrwxr-x 1 utocoo utocoo 16048 11月 22 12:23 file*
-rw-rw-r-- 1 utocoo utocoo 233 11月 22 12:19 file.c
-rw-rw-r-- 1 utocoo utocoo 64 11月 22 12:21 Makefile
在文件基础认识部分,已经提到过,文件是由进程打开的,那么新建一个文件也是由进程完成,进程是如何知道在哪条路径下新建一个文件呢。
在源代码中打印出进程的PID,运行后,再在**/proc**路径下找到对应进程的所在目录。
cpp
while(1)
{
printf("PID:%d\n",getpid());
sleep(2);
}
bash
PID:2930
PID:2930
PID:2930
PID:2930
PID:2930
PID:2930
PID:2930
当前路径在进程的属性中其实已经保存好了,是cwd这条信息。因此新建一个文件要被存放到哪里也是确定的。但是进程的工作路径是可以修改的,虽然进程的前身是一个可执行程序,可执行程序的路径是确定,但是当可执行程序被操作系统管理起来后变成进程,进程的工作路径是可以通过chdir指令修改的,那么修改路径后,再新建一个文件,这个文件的所在路径不再是修改前的路径了,而是修改后的路径。
这就表明,所谓的当前路径,其实是进程在运行的时候的工作路径,这个路径是由进程自己记录的,就是那条cwd信息。
三、相关系统调用
系统默认打开三个流,stdin,stdout,stderr,这三个流对应的外设分别为键盘、显示器、显示器。而Linux管理外设,是以文件的方式,即必然存在系统调用system call。因此,C语言的fopen、fclose、fwrite等函数本质是调用了system call。
下面就来认识Linux下文件相关的system call。
1.open
- pathname就是路径,传参方法和C语言的fopen的参数差不多。
- flags类型为int,传参的可选项如下所示
这些值都是C语言定义的宏,目的是为了实现,**只定义一个函数,却可以同时"传两个参数"。**比如
cpp#include <stdio.h> #define ONE 1 #define TWO (1<<1) #define THREE (1<<2) #define FOUR (1<<3) #define FIVE (1<<4) void Print(int flags) { if(flags & ONE)printf("1\n"); if(flags & TWO)printf("2\n"); if(flags & THREE)printf("3\n"); if(flags & FOUR)printf("4\n"); if(flags & FIVE)printf("5\n"); } int main() { Print(ONE); printf("-----------------\n"); Print(TWO); printf("-----------------\n"); Print(ONE|TWO); printf("-----------------\n"); Print(ONE|FOUR|FIVE); return 0; }
如果使用两个形参的open接口,一般是操作已经存在了的文件,比如bbb.txt文件必须存在,否则会报错。
cpp
int main()
{
int fd = open("bbb.txt",O_WRONLY);
if(fd == -1)
{
perror("open\n");
return 1;
}
close(fd);
return 0;
}
由于bbb.txt不存在,则fd=-1
用open接口实现fopen的"w"方式,文件如果不存在,则新建。而新建一个文件会有权限的初始化,一般普通用户新建一个文件的权限是0666(-rw-rw-rw-),而普通用户的权限掩码umask为0002,实际权限等于初始化权限减去权限掩码,即(-rw-rw-r--)
mode即初始化权限码,一般传0666,只有flags带O_CREAT时,mode传参才有效。
一般新建一个文件,在open的第二个参数上,应该传新建、可写、写入时清零,等同于fopen的"w"方式。
cpp
int main()
{
int fd = open("bbb.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd == -1)
{
perror("open\n");
return 1;
}
const char* msg = "this is open to w\n";
write(fd,msg,strlen(msg));
close(fd);
return 0;
}
原来不存在的文件bbb.txt被创建了出来,并且o的权限少了w,符合预期。
2.文件描述符
再来理解open的返回值------文件描述符(int fd)------Linux用整型值描述被打开的文件。
这些整型值其实是数组下标,我们知道系统默认打开三个流,其实是三个文件,stdin、stdout、stderr,它们的下标对应为0、1、2,如果先后有序的打开1.txt、2.txt、3.txt,则它们的下标也是有序的为3、4、5。
这段话似乎让你很懵,不过我马上就要阐述具体的内容。
在此之前,要明确,操作文件只能由操作系统来做,因此有C语言的fopen封装open接口,有C语言定义的FILE指针的流封装文件描述符fd。
实际上,FILE类型是结构体类型,也是封装了文件描述符int fd。
对int fd的理解。
文件描述符的本质,就是数组下标。
- OS管理进程,这一板块叫做进程管理,有PCB,Linux下被定义为task_struct。
- OS管理文件,这一板块叫做文件管理,在之前介绍了,文件区分为内存中的文件和磁盘中的文件,被加载到内存中的文件,OS要对它们做管理,就必然做"面向对象"和"数据结构"的工作,"面向对象"就是定义结构体,"数据结构"就是把对象存储到链表或者其他数据结构里面。Linux下把这个结构体类型定义为file,结构体内容大致有属性、方法集、缓冲区、mode(权限码)、flag、pos以及指向下一个结点的next等。
- 进程管理和文件管理是两个独立的板块,但是又有关联。进程可以打开多个文件,那么一个进程打开了哪些文件,该进程必然要做记录。于是Linux下,task_struct结构体中有一个结构体指针,指向的结构体类型为files_struct,而这个结构体中,有一个数组,数组的每个元素类型为结构体指针,指针指向的结构体类型为file,这个数组被称为文件描述符表。
一个进程打开文件后,进程在这个数组中保存指向这个文件的指针,默认这个数组的前三个位置已经被stdin、stdout、stderr这三个文件占用了。
而数组下标,就是文件描述符,为什么close、write等这些接口都用int类型的文件描述符来操作文件,原因很简单,数组下标式访问,仅仅是O(1)复杂度。
3.一切皆文件
硬件一层,由于各种原因,设备的操作方法各不相同,因此每台计算机都需要装载相应的驱动。而对于每台设备的操作函数,它们的函数类型相同,函数内容各不相同。
file结构体定义了方法集,本质就是函数指针。
- 每一台设备被视为一个结构体,方法集指向了该设备的操作方法。
- 当系统调用read读取某个外设的内容,实际上就是函数回调的形式,用函数指针调用外设的读函数。
4.再次理解重定向
文件描述符的分配规则:一定会把最小的数组下标利用起来,如果存在没有被利用的较小下标,则会分配给最新打开的文件,比如打开b文件前,将已经打开的a文件关闭,则打开b文件后,a文件的较小fd会分配给b文件。
上面这段话,其实就是重定向的实现原理。
输出重定向:本该输出到屏幕的语句却输出到了bbb.txt。
cpp
int main()
{
close(1);
int fd = open("bbb.txt",O_WRONLY);
printf("这段话本该输出到屏幕\n");
return 0;
}
原因就是在执行完close(1)语句后,当前进程的文件描述符表中数组下标为1的位置不再是指向屏幕文件的指针,而又打开了bbb.txt文件,则1号下标的指针指向了bbb.txt文件,printf 底层封装的write 传参的fd值还是1,因此,这句字符串被写进了1位置指向的bbb.txt文件的缓冲区。
所以,重定向的本质,就是文件指针在文件描述表中的下标发生了变化。
有一个专门用来拷贝文件描述符的系统调用------dup
想把打印到屏幕的内容重定向到bbb.txt,可以用dup2来实现。
大致意思是用oldfd的值覆盖到newfd。
cpp
int main()
{
int fd = open("bbb.txt",O_WRONLY);
dup2(fd,1);
printf("----\n");
return 0;
}