文件基础
Linux下一切皆文件。系统中的空文件也要在磁盘占据空间,文件=内容+属性。所以如果对文件对文件进行操作,可以进行两方面的操作:1、对属性操作,chmod u+x filename 2、对内容操作。其中每次对文件进行操作的时候,都会对文件中的三个时间Access time , Modify time,change time,产生影响文件中的三个时间:
Access time:当对文件进行访问的时候(有时候多次使用cat,more)的时候。
Modify time:修改内容的时候,Modify time会被改变,此时Change time也会被改变。
Change time:修改内容或者属性 chmod命令会改变这个时间。
标定一个文件,必须使用:文件路径+文件名(唯一性)。如果没有指明对应的文件路径,默认实在当前路径(进程当前路径)下进行文件访问。
当我们把fopen,fclose,fread,fwrite等接口写完之后,代码编译之后,形成二进制可执行程序之后,但是没运行,文件对应的操作没有被执行,程序对文件的操作本质是进程对文件的操作!所以文件操作的本质是 进程和被打开文件的关系。
重新理解文件
文件存储在磁盘中,而磁盘是硬件,想要访问硬件必须得借助OS,所以OS必须提供一系列的系统调用接口帮助访问磁盘上的文件。
由上述可知:文件操作的本质 进程和被打开文件的关系,系统中一定存在大量的被打开的文件,这些文件都被OS管理,如何管理:先描述后组织的方式进行管理。操作系统为了管理对应的打开文件,必定要为文件创建对应的 内核数据结构标识文件:struct file{}。其中包含了大部分的属性。
1、文件的相关函数
C/C++中不乏有许多文件相关的函数,例如fopen(),fclose(),fwrite(),fread()等相关的函数,但是想要访问文件必须经过OS,所以这些只是在OS提供的函数上加了一层封装。
//系统调用相关代码
//Open 相关代码
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
//write
ssize_t write(int fd, const void *buf, size_t count);
//read
ssize_t read(int fd, void *buf, size_t count);
//close
int close(int fd);
其中C/C++的库函数接口
//C库函数调用
FILE *fopen(const char *path, const char *mode);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fclose(FILE *fp);
对于C的函数而言,封装了系统调用接口,其中系统调用接口主要是使用文件描述符!所以C语言的File类里面必定存在一个文件描述符对象用于系统函数接口。
2、文件描述符
文件描述符是用来标识文件唯一性的字段,从我们打开的文件描述符来看,一般是从3开始的,是因为0,1,2被标准输入,标准输出以及标准错误三个流占了。
#include<stdio.h>
#include<unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
printf("stdin->fd : %d\n",stdin->_fileno); //其中这个fileno参数是一个参数,表示文件描述符
printf("stdout->fd : %d\n",stdout->_fileno);
printf("stderror->fd : %d\n",stderr->_fileno);
umask(0); //设置最初始得umake的值,这里改的是子进程继承下来的umask而不是shell的umask
// w -> O_WRONLY | O_CREAT |O_TRUNC O_TRUNC 是清空之前的内容
int fp0 = open(FILE_NAME(0),O_WRONLY | O_CREAT |O_TRUNC ,0666);
int fp1 = open(FILE_NAME(1),O_WRONLY | O_CREAT |O_TRUNC ,0666);
int fp2 = open(FILE_NAME(2),O_WRONLY | O_CREAT |O_TRUNC ,0666);
int fp3 = open(FILE_NAME(3),O_WRONLY | O_CREAT |O_TRUNC ,0666);
int fp4 = open(FILE_NAME(4),O_WRONLY | O_CREAT |O_TRUNC ,0666);
printf("fd0 : %d\n",fp0);
printf("fd1 : %d\n",fp1);
printf("fd2 : %d\n",fp2);
printf("fd3 : %d\n",fp3);
printf("fd4 : %d\n",fp4);
close(fp0);
close(fp1);
close(fp2);
close(fp3);
close(fp4);
return 0;
}
实验结果:
从上述的实验结果可以看到,文件描述符是一组连续的下标,说明文件描述符的本质就是数组(进程的文件描述表)的下标,里面的内容对应着文件的地址属性等访问手段。进程的文件描述表将进程和被打开的文件联系起来。
文件描述符的分配规则:从小到大,按照循环顺序寻找最小且没有被占用的文件描述符。
文件打开的本质从磁盘加载进入内存之中,每个被打开的文件,在内核中都有对应的file对象,保存了文件相关的inode元信息,然后占用一个文件描述符。
3、文件重定向
重定向的本质是:上层用的fd保持不变,在内核中更改fd对应的struct file*的地址。
重定向的函数:oldfd指向的是更改后输出后的文件描述符,newfd指向的是被重定向的文件描述符。
#include <unistd.h>
int dup2(int oldfd, int newfd);
例如下列代码:
#include<stdio.h>
#include<unistd.h>
#define FILE_NAME(number) "log.txt"#number
int main()
{
close(0);
close(1);
int fd = open("log.txt",O_RDONLY); //输入重定向
if(fd<0)
{
perror("OPen");
return 1;
}
dup2(fd,0); //从标准输入中读取,将从键盘中读取,改为从文件log,txt中读取
char line[64];
while(1)
{
printf(">");
if(fgets(line,sizeof(line),stdin)==NULL)
break;
printf("%s",line);
}
int fd1 = open("log.txt",O_CREAT | O_WRONLY | O_APPEND); //输出重定向
if(fd1<0)
{
perror("OPen");
return 1;
}
dup2(fd1,1); //将本该显示在屏幕上的输出显示到所写的文件之中
printf("fd-->%d\n",fd);
fprintf(stdout,"open fd: %d\n",fd); //printf -->stdout
fprintf(stdout,"您好\n");
fprintf(stdout,"V五块\n");
fflush(stdout);
return 0;
}
4、父子进程中的文件系统
对于fork函数在创建子进程之后,复制了父进程PCB,file_struct等,但是对于文件部分,不用给子进程拷贝一份,因为这是OS的文件系统进行管理的,不需要进行拷贝。
缓冲区
缓冲区本质就是一段内存,OS申请的,是属于进程的,缓冲区能够帮进程节省IO的时间,进程只执行自己的任务即可。所以关于一些fwrite,write函数,与其将其理解为写入到文件的函数,不如将他们理解为是拷贝函数!!!将数据从进程,拷贝到缓冲区或者外设中。
缓冲区的刷新策略:缓冲器一定会结合具体的设备,定制自己的刷新策略:a.立即刷新---无缓冲;b.行刷新--行缓冲---显示器;c.缓冲区满--全缓冲--磁盘文件。当然用户也可以使用fflush进行强制刷新,并且当进程退出的时候,一般都要进行缓冲区刷新。
缓冲区的位置
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("./log.txt",O_WRONLY);
if(fd==0)
{
perror("eror");
exit(-1);
}
close(1);
dup2(fd,1);
//C接口
printf("C Printf\n");
const char* msg = "hello wrold\n";
fputs("hello puts\n",stdout);
fprintf(stdout,msg);
//系统接口
const char* msg2 = "hello os write\n";
write(1,msg2,strlen(msg2));
fork();
return 0;
}
当将输出到屏幕的语句重定向到文件之后,发现C接口的语句在文件中显示了两次,而系统结构的文件输出的语句只有一次 ,所以这种现象一定跟缓冲区有关,缓冲区一定不在内核中,因为如果在内核中,write也应该打印两次,所注意这里的缓冲区都是指的是用户级语言层面给用户提供的缓冲区。所以语言级别的缓冲区是C语言给用户提供的FILE结构体里,包含了对应的缓冲区,进行所有的C语言上的文件操作,fgets,fputs是会被写进FILE缓冲区,因为缓冲区这里封装了文件描述符,所以C语言会在合适的时候把缓冲区的内容刷新到外设(带有/n的语句,fflush强制刷新的语句,以及缓冲区慢的时候)。
所以对于上述代码的现象,代码结束之前进行创建子进程:
1.如果没有进行重定向,只会看到4条消息,因为stdout默认使用的是行刷新,在进程fork之前,三条c语言函数已经将数据进行打印输出到显示器上,FILE内部,进程内部不存在对应的数据了。
2.如果进行了重定向操作,写入文件而不是写入显示器,采用的刷新策略是全缓冲,之前的3条c语言显示函数,虽然带了/n,但是不足与将stdout的缓冲区写满,数据被没有被刷新。在执行fork的时候,stdout属于父进程,创建子进程的时候,紧接着是进程退出!谁先退出,一定要进行缓冲区刷新,因为创建子进程的时候,会对父进程的PCB等进行复制一份,刷新也就是修改的时候,会发生写时拷贝,数据最终就会显示两份。
3.write因为是系统提供的函数,没有C提供的缓冲区,用的是fd,所以write的数据只有一份。
磁盘
一个文件如果没有被打开,只能静静的在磁盘上放着,磁盘上面有大量的文件,必须被静态管理起来,主要是文件系统负责。磁盘是计算机中为一个机械结构,由马达控制盘片旋转,磁头,硬件电路,伺服系统组成。注意磁头和盘面是没有接触的。
盘面
一个磁盘
(如一个 1T 的机械硬盘)由多个盘片
(如下图中的 0 号盘片)叠加而成。盘片的表面涂有磁性物质
,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。对应的磁头也有两个。磁头=盘面数。
磁道,扇区
磁盘的盘面被划分成一个一个的磁道,每个磁道也会被划分成一个一个的扇区,各个扇区的存放的数据量是一样的。磁道一般是从外到内进行编号。
磁盘寻址的时候,基本单位不是bit,也不是byte,而是扇区(512byte),在磁盘上定位一个扇区:首先磁头来回摆动确认在哪一个磁道(cylinder),然后定位磁头(head)(定位盘面),最后盘面旋转的时候就让磁头定位了扇区(sector)。这样的方法就叫CHS定位法。
磁盘的逻辑结构
磁盘物理是圆形,但在逻辑上可以想象成一个线性的结构,即sector arr[n],对磁盘进行管理就是对数组进行管理。
从上述图可以知道,只要知道这个扇区的 下标 就算定位一个扇区,在操作系统的内部,称这种地址位LBA地址。假设有一个磁盘有4个盘面,每个盘面有10个磁道,每个磁道上有100个扇区,那么这个抽象出来的数组的范围是[0,4*10*100],总容量是4*10*100*512byte。所以LBA[123]的具体位置在 123/1000 = 0号盘面(H),123/100=1号磁道,123%100=23号扇区。
为什么OS要进行逻辑抽象呢?为什么不直接用CHS寻址?1、为了便于管理;2、不想让OS的代码与硬件强耦合。
虽然对应的磁盘访问的基本单位是512字节,但是依旧很小,OS的文件系统定制的进行多个扇区的读取以4kb的大小为基本单位,必须将4kb的内存load进入内存,进行读取或者修改,如果必要的话,在写回磁盘。局部性原理:内存是被划分成了4KB的空间(页框),磁盘中的文件,尤其是可执行文件,也是按照4KB的大小进行划分的块(页帧)。如下图的IO BLOCK的大小就是4KB的大小。
Linux系统中的文件系统组成,首先是对内存进行分区,将100GB的大小分到合适的大小,然后对这一块儿空间进行管理即可,其他分组亦是如此:
Boot block:启动快,加电之后,首先识别磁盘的分区情况,然后再跳转到操作系统所在分区,加载OS,再加载图像化界面等。
文件=内容+属性,Linux的文件属性和文件内容时分批存储的,保存文件属性的是inode,一般是128字节(主要是看文件系统有关),inode是固定的大小,一个文件一个inode,主要包含了文件几乎所有属性,文件的大小,文件的权限,所属组,文件的时间信息等。inode为了区分彼此,每一个inode都有自己的ID。如下图的第一列就是每个文件的inode
文件名并不在inode存储。文件内容主要是在data block内存储的,随着应用类型的变化,大小也在变化。
Super Block:保存到是整个文件系统的信息,这个分区有多少块,结束块和开始块是多少等一系列信息。Super Block 存在于这个区的好几个group上,数据都是同步的,主要是备份操作,防止一个Super Block坏了,直接从其他Super Block拷贝过来即可。格式化的本质:重新写入文件系统即Super Block进行重写。
inode table:保存了分组内部所有的可用(已经使用+没有使用)inode。文件的属性主要是放在inode里边。
Data blocks:保存的是分组内部所有文件的数据块,数据块主要是4kb大小。文件的内容主要放进了Data blocks中。
inode Bitmap:inode对应的位图结构,位图中比特位的位置和当前文件对应的inode的位置是一一对应的。
Block Bitmap:数据块对应的位图结构,位图中的比特位的位置跟当前data block对应的数据块是一一对应的关系。
Group Descriptor Table:GDT快速组描述表:对应分组的宏观的属性信息。inode和data block被用了多少等等信息。
struct inode
{
mode_t mode;
uid;
gid;
...
int blocks[15]
}
inode的属性里边包含了一个数据块的数组,这个数组存储的说data里面数据块的地址,当然如果只有15大小的话,只能存储60kb的大小,所以后面的数据块可以指向一个存储着其他数据块编号的数据块,这就叫二级索引,同时二级索引的数据块也可以存储其他数据块儿的inode,这就是三级索引。
删除文件:把inode Bitmap中该文件的inode置为0即可,即对应的属性也无效,block bitmap中的该位置为0即可。
查找一个文件的时候,统一使用的是inode,任何一个文件都是在一个目录下面,目录是一个文件,目录也会有自己属性,内容,也就有自己的数据块,目录数据块就会放置当前目录下文件名和 Inode的映射关系。所以同一个目录下不能存在相同的文件名,因为文件名是个key值。创建一个文件必须要有写入权限,ls必须要有读权限。