文章目录
- 文件操作符
- 系统调用接口
- 文件接口的简单实用
- 实验一:打开文件写入信息
- 实验二:read接口读取文件
- 返回值和fd
- 联想到数组下标
- OS怎么管理文件呢?先描述再组织。
- 一切皆文件
- 012的操作
- 文件描述符的分配规则:
- 输出重定向的原理。
- 追加重定向是怎么实现的呢?
- 输入重定向:
- 验证FILE的结构体中,有一个fileno
- 是不是非得关闭之后才能重定向呢?不是,用dup2()
- dup2()完成输入重定向
- 执行exec*程序替换时,会不会影响曾经打开的所有文件?
- 缓冲区
- 用户到OS的刷新策略:
- 解决重定向有区别的问题:
- 再来:write&&printf
- 问题三:刷新策略改变之下的写时拷贝导致的C缓冲区打印两次
文件操作符
所有的文件操做表现上都是进程执行对应的函数。就是进程对文件的操作,要操作文件需要先打开文件,就是将文件相关的属性信息加载到内存。
OS系统中会存在大量的进程,进程可以打开多个文件=1:n.系统中更多的存在打开的文件,那么OS管理起来,先描述在组织。用strcut file{}包含各种属性。
C语言层面上的:fopen() fputs() fgets();
w:是清空原来文件重写,a:这个写入方式是 append 追加fscanf 等。
C程序默认会打开三个输入输出流 stdin stdout stderr,FILE* stdin:键盘。stdout:显示器。stderr:显示器
输出重定向,本质是只把stdout的内容重定向到文件中。所以上面两个显示器是不一样的。
C++中cin cout cerr,fputs向一般文件或者硬件设备都能写入,一般文件都在磁盘上也是硬件,一切皆文件。
最终都是访问硬件,OS是硬件的管理者,所有的语言上的对文件的操作都必须贯穿OS。
操作系统不相信任何人,访问操作系统是需要通过系统调用接口的
几乎所有的语言fopen fclose fread fwrite fgets fputs fgetc等底层一定需要使用os 提供的系统接口调用!!
所以要学习文件的系统调用接口:
系统调用接口
-
int open(文件名,打开方式,设立文件权限信息);
int flags,传递标志位,int 32bit ,一个bit 代表一个标志。
-
int close();
- 返回的int是文件描述符
所有的文件操作,表现上是进程执行对应的函数,进程对文件的操作。
要操作文件必须先打开文件!本质就是将文件相关的属性信息加载到内存。
系统中是不是存在大量的进程,代表着进程可以打开多个文件,进程:文件=1:n.
系统当中存在更多的,打开的文件,那么OS要把打开的文件(内存中的)管理起来
-
如何管理打开的文件呢?先描述再组织。
C语言中的struct{//包含了打开文件的相关属性//链接属性}
未打开的文件就在磁盘上由文件管理器管理。
文件接口的简单实用
实验一:打开文件写入信息
int fd = fopen(“./log.txt”,O_CREAT | 0_WRNOLY,0644)
当前路径下的log.txt文件如果没有就创建一个,只写的方式
设置权限是0644
ssize_t write(int fd,const void * buf,size_t count)
写入字符串时你不需要写入\0,作为字符串结束只是C语言的规定,文件更注重的是内容。
实验二:read接口读取文件
文件中并不需要C/C++中的/0作为分隔符,所以我们写入的时候并不加。在读取的时候读少一个位置,那个位置因为我们读取出来多行字符是将他作为字符串来看待,所以需要手动的添加结尾/0用的。
返回值和fd
- 当我一次打开多个文件时,返回的返回值是什么值。
-
原因:
当我们的程序运行起来时,变成进程之后,默认情况下OS会帮助我们打开三个标标准输入输出:
0 标准输入,键盘。1 标准输出,显示器。2 标准输出,显示器。
而open的返回值是OS给的。联想到数组下标
进程:文件 =1:n; OS有很多进程对应更多的文件被打开,OS应该对文件进行管理。
- 如果一个文件没被打开,那么这个文件在磁盘上。进程也是一个可执行文件,没被打开也在磁盘上。
- 创建空文件占不占磁盘空间?空文件没有内容但是有属性,也是数据,所以占空间。
所以,磁盘文件=文件内容+文件属性。之前的文件操作可分为:对于文件内容操作(fclosefopenread),对于文件属性操作(更改属性,重命名)
OS怎么管理文件呢?先描述再组织。
设计结构体struct file{
文件的相关的属性信息
}
-
双链表的形式将文件结构体相连,哪些是某个进程的呢?
所以添加了一个容纳数组的结构
struct file_struct{} ,他当中有一个指针数组
struct file* fd_array[]; 数组元素就是指向各个文件结构体的指针,fd就是指针在数组中的下标。这个数组的前三位下标为012的三个位置,储存的是默认打开的三个文件 标准输入 标准输出 标注错误
所以,其他我们新创建的文件的文件描述符都是从下标3开始记录储存的。
而某一个进程PCB中有一个指向这个结构的指针struct file_struct* fs;指向容纳这个数组的结构。
所以在进程操作文件的过程,比如write read 等系统调用接口都需要传fd
就是通过这个结构体指针找到指针数组,通过我们传的文件描述符fd,找到对应下标下的
文件结构体指针,通过文件指针在很多的文件结构体中找到相应的文件结构体,然后在对文件进行读写等操作。
小结:fd本质是内核中是进程和文件关联的数组的下标。
一切皆文件
内存和外设进行打交道的时候涉及到IO,外设都具有读写属性。键盘写入是什么意思?具备IO,键盘可以不实现某些功能。外设不同,在底层的实现上一定是不一样的。
各种硬件的读写方法等全部都是在驱动层完成的,帮助我们实现对于各种硬件程序的读写。
所以大家的读写方法是完全不一样的。
- 那如何做到一切皆文件的呢?
文件虚拟层,VFS(virtual file system) ,刚才的struct file就是一个例子。
每一个硬件或者文件要被打开,OS都会为其生成一个类似struct file{}的文件先进行描述,用双向链表来组织。
- C++多态?父类提供一个函数接口,子类继承接口并且进行相应的函数实现。父类只需要有一个指针指向某一个子就可以实现对于子类的调用。(用动物吃的功能帮助理解)
- C语言上如何实现上述方法?可以在每一个struct file中包含很多函数对应的指针
那么在上层看这一层的文件时,底层的键盘的响应方法在这一层都有函数指针呼应。
那我要写,我就直接调用这个接口指针指向的方法就行,不用管你是什么文件,底层是啥样的也不关心。
上层只需要直到调用键盘的读接口,就能实现键盘的输入就行了,怎么写的不用关心。
所以在VFS往上看,就实现了一切皆文件。
如果基类是动物,所有的子类都继承并实现动物的函数接口,完成调用,那就可以看做是一切皆动物。多态就可以看作是一切皆的高级版本。
用户要实现某种读写方法,由文件描述符表,找到对应下标下的文件描述结构体的方法,方法接口再找到对应的子类的函数实现,函数再来实现对于外设或者文件的读写等操作。
- file operation文件操作结构体中就有一个函数表,包含read,write等函数类似于基类虚函数表。
VFS中会包含每一个描述打开文件的结构体file_struct{},里面包含很多的函数指针,在上一层看待就是一切皆文件。
012的操作
默认的012也是可以直接读写的。write可以直接向12直接进行写入,有什么区别?
- 验证read可以直接0读写呢?
文件描述符的分配规则:
给新文件的fd是数组中最小的没有被使用的fd。close(0)close(2)之后,新打开的文件fd就是0或者2.
但是close(1)关闭标准输出时,本来应该显示到显示器中,但是却被显示到你打开的文件内部,就叫做输出重定向。
原因:printf打印是stdout进行打印,stdout是FILE*类型。
FILE是C语言层面上的结构体
struct FILE{
//一定包含了一个整数,是在对应的系统层面的,这个文件的打开对应的fd(1)。
}
printf是向stdout中写,这个stdout中包含数字1,就是照着这个fd(1)指向的文件使劲打印,只认fd1,语言层只认识012,这样就给重定向提供了操作空间,所以刚才还指向显示器标准输出,后来close(1)关闭了显示器文件又更改为新创建的文件log.txt时,就改变了输出的方向,改为写入到log.txt文件中。这就是
输出重定向的原理。
printf fprintf cin --都是语言层。调用系统调用,open write fd 都是系统层面。所有的语言,进行文件操作时,都需要系统层面的fd,这样语言和系统就产生了关系。
命令行形式输入echo"hello world">log.txt
,就是一条输出重定向语句。
追加重定向是怎么实现的呢?
C语言中的append,a 选项,在尾部进行写入O_APPEND。
输入重定向:
把fd=0的标准输入文件替换成我们新的文件,将新的文件里面的内容进行写入。采用fgets()按行读取文件内容。
验证FILE的结构体中,有一个fileno
是不是非得关闭之后才能重定向呢?不是,用dup2()
- 输出显示器->1,如何在已经打开的情况下再完成输出重定向?
int dup2(int oldfd,int newfd);fd 1 数组内容全部变成oldfd,将oldfd赋值给newold。将3里面的内容覆盖到1里面。本来应该显示到显示器的内容写入到新的3号文件中。让newfd是oldfd的一份拷贝,两个都变成了oldfd。想让1指向原来3指向的文件。
dup2()是在系统层面上将fd转换,三种语言层面的写入根本不关心系统,只认文件描述符表中的1.
dup2()完成输入重定向
执行exec*程序替换时,会不会影响曾经打开的所有文件?
内核数据结构替换的时候只会替换代码和数据。
命令行式重定向:重定向符号时就会发生输出重定向,将子进程fd拷贝给1,然后再程序替换。
fork()创建子进程时,内核层面的东西都重新生成一份给子进程,都是一样的,也会给他创建新的file_struct{},拷贝父进程的,实现两份完全一样的文件描述符表。
但是打开的文件,文件不会拷贝,还是原来的那些文件 ,会出现父进程子进程可能指向一个文件。
那么,父进程如果曾经打开了标准输入等呢?子进程也会继承下去!!!
-
为什么所有的进程都会默认打开012呢?
因为所有进程的父进程是bash,他是命令行啊!bash打开,必然导致012的打开让你输入输出错误,
子进程全部继承父进程,所以也会默认打开012。 -
引用计数,数一共多少个进程。因为一个文件可以被多个进程打开,引入了C++中的引用计数。
缓冲区
重定向会将fd=1的打印到文件中,fd=2的标准错误还是会打印到显示器上。
./redir 2>&1
就可以将标准错误也打印到文件当中。
& 是将1的数据进行写入的功能,就是将已经重定向的1里面的重定向的新的文件描述符拷贝到2里面,然后2就跟着1也指向到新文件当中了。
最后是否添加close(1)的区别:
因为C语言也提供了缓冲区。如果将文件close(1)之后,就无法完后续打印到显示器和重定向的功能。
我们知道,printf就是向stdout中打印,stdout是FILE*类型,指向一个struct 结构体,结果就是将内容写到C语言提供的用户缓冲区中,定期刷新到OS缓冲区,然后再刷新到磁盘。struct结构体,这个结构体里面一方面封装了一个fd,文件描述符,另一方面与维护了C缓冲区相关的内容。而我们将字符串等在语言层面的操作到文件,都是暂时储存到了这个FILE结构体内的缓冲区里面。
遇到/n等操作时,或者进程退出的时候,会刷新FILE里面的数据到OS缓冲区。
用户到OS的刷新策略:
- 立即刷新(不缓冲)
- 行刷新,比如显示器打印就是\n这种行刷新
- 全缓冲,缓冲区满了才刷新,比如我们往磁盘文件中写入。然后OS->硬件上也是同样使用的。
解决重定向有区别的问题:
显示器 ->log.txt重定向时,本来是行缓冲,现在变成了全缓冲。
- 就是为什么调用close(1),重定向的时候并没有打印到文件当中
从显示器重定向到文件,刷新方式从行缓冲到全缓冲,
如果在最开始并没有close(1),进程在结束的时候,会自动根据fd将用户层面的缓冲区刷新到文件中,就会看到内容在文件中。
但是如果先close(1),新打开的文件被分配到的fd=1,重定向,触发全缓冲。在进程结束之前,再写上**close(fd)**就将文件等操作关闭了,在进程结束的时候就没有了fd来帮助将用户的缓冲区内容刷新到OS文件中,仍然停止在用户缓冲区。所以就在文件中不可见。
如果在调用close(fd)时,在关闭之前强制刷新一下,fflush(stdout)
就将依据还存在的fd,将用户缓冲区内容刷新到OS缓冲区,这样文件中就可见了。
再来:write&&printf
未加close(1);不构成重定向在进程结束时会将1的用户缓冲区数据全部刷新到OS的缓冲区。
加上close(1);之后,构成重定向,只在文件中有局部内容(比如第一句话)是因为write()是系统调用接口,跳过用户缓冲区直接到OS中文件的系统缓冲区。而C语言提供的接口printf,fprintf()中的内容保存在用户缓冲区,重定向之后,从行缓冲到全缓冲,因为还未达到全缓冲的条件,所以还未送到OS中的文件缓冲区,所以文件中没有。
C语言的接口是打印到stdout,对应的是FILE 类型的结构体,结构体里面有fd ,和用户缓冲区buffer
后续根据fd 将缓冲区里面的东西根据fd刷新涉及到刷新策略,文件类型决定到系统文件缓冲区。
问题三:刷新策略改变之下的写时拷贝导致的C缓冲区打印两次
C语言提供的接口重复出现,系统接口并不受影响。
如果没有发生重定向,都是行缓冲, 并且每条语句都有\n刷新,正常可见。
C将内容写到用户层缓冲区,在发生重定向时,行缓冲变为全缓冲使得数据暂时在C缓冲区。
然后fork(),这些内容属于父进程,但是子进程也会进行写时拷贝将代码数据什么的自己也有一份
然后等进程退出时,C缓冲区强制刷新到OS缓冲区,两个进程都需要刷新到缓冲区,从而达到写入到文件中两次的效果。
所以fork()还是可能影响上文代码效果。提前刷新缓冲区就行了fflush(stdout)
。重定向时写实拷贝,但是缓冲区中没数据,就不会打印两次。
write系统调用接口是没有缓冲区的。
- 总结:stdout,cin cout iostream fstream CC++流的类(流也是类)里面都是包含缓冲区的。std::endl;的作用就C++中类似\n的作用,就是刷新缓冲区。