形成共识原理:
1、文件 = 内容 + 属性
2、文件分为打开的文件和没打开的文件
3、打开的文件:谁打开?进程!--- 本质是研究进程和文件的关系!
文件被打开,必须先被加载到内存!
一个进程可以打开多个文件!操作系统内部,一定存在大量的被打开的文件!--- OS需要管理这些被打开的文件,先描述,再组织 --- 在内核中,一个被打开的文件都必须有自己的文件打开对象,包含文件的很多属性。结构体对象。
4、没打开的文件:在哪里放着呢?在磁盘上,我们最关注什么问题?没有被打开的文件非常多。文件如何被分门别类的放置好 --- 我们要快速的进行 增删查改 --- 快速的找到文件。最终就是谈论如何存储。
一、目标:被打开的文件
1、以C语言为主,先回忆一下C文件接口
fopen,fclose,fprintf,fscanf,fsprintf,fsscanf,fseek,fgetc,fgets,fputc,fputs,fread,fwrite。
当前路径是什么?当前路径指的是进程的当前路径cwd。可以在ls /proc文件中找到对应的pid,进入后就可以看到。如果我更改了当前进程的cwd,就可以把文件新建到更改后的cwd,可以使用chdir这个函数更改工作目录。
w写之前,都会对文件进行清空处理。
文件重定向也可以写入 > >>以追加的方式写入。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <string.h>
int main()
{
//C语言文件操作
FILE* fd = fopen("log.txt","w");
if(fd == NULL)
return 1;
const char* msg = "hello linux file\n";
fwrite(msg,strlen(msg),1,fd);
fclose(fd);
return 0;
}
strlen 不需要 + 1,字符串以‘\0’结尾,是C语言的规定,和文件没有关系。
a 在文件结尾,追加写!
Linux一切皆文件
C++ cin&&cout&&cerr
C stdin&&stdout&&stderr
C程序默认在启动的时候,会打开三个标准输入输出流文件
stdin 键盘设备,stdout 显示器设备,stderr显示器文件
2、过度到系统,认识文件系统调用
文件其实是磁盘上的,磁盘是外部设备,访问文件其实就是在访问硬件!
计算机系统层次。
用户,
程序
系统调用
操作系统
硬件驱动
硬件
几乎所有的库只要是访问硬件设备,必定要封装系统调用!
flag参数对应的的值
这些选项都是宏
题外话:比特位方式的标志位传递方式。
0000 0001 0000 0010 0000 0001 | 0000 00010 = 0000 0011
0000 0001 0000 0010
实现C语言W的功能。
代码:
int main()
{
//C语言文件操作
// FILE* fd = fopen("log.txt","w");
// if(fd == NULL)
// return 1;
// const char* msg = "hello linux file\n";
// fwrite(msg,strlen(msg),1,fd);
// fclose(fd);
//Linux 系统调用
int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
umask(0);
if(fd < 3)
return 1;
//系统调用
const char* msg = "hello linux file\n";
write(fd,msg,strlen(msg));
close(fd);
return 0;
}
mode参数,是被创建文件的权限。以八进制。
设置umask权限掩码 umask(0)。
写入接口
会覆盖但不会清空。
O_TRUNC 清空选项
3、访问文件的本质
返回值类型的区别和联系,FILE* 和 int fd,也就是访问文件的本质。
返回的是int fd 是一个数组的下标 struct files_struct* files里面的一个文件描述符表里的数组下标。
描述文件的结构体:struct file操作系统内描述一个被打开文件的信息。会包含在磁盘的什么位置,基本属性权限,大小,读写位置,谁打开的……,文件的内核缓冲区信息,struct file* next的指针。用双链表链接。文件描述符是从3开始的。0 1 2 被三个标准 stdin stout sterr 占用。这是操作系统的特性。而不是语言方面的特性。
FILE是一个C库自己封装的结构体!里面必须包含文件描述符
struct file里面会包含引用计数count,关闭一个做count--。
4、重定向&&缓冲区
重定向:
文件描述符的分配规则:从0下标开始,寻找最小的没有被使用的数组位置,它的下标就是新文件的文件描述符
代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
//C语言文件操作
// FILE* fd = fopen("log.txt","w");
// if(fd == NULL)
// return 1;
// const char* msg = "hello linux file\n";
// fwrite(msg,strlen(msg),1,fd);
// fclose(fd);
//Linux 系统调用
close(2);
int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
umask(0);
if(!fd)
return 1;
//系统调用
const char* msg = "hello linux file\n";
//这里输出应该为2
printf("fd = %d\n",fd);
write(fd,msg,strlen(msg));
close(fd);
return 0;
}
运行结果:
代码原理:也就是重定向的原理
重定向接口dup2
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
//C语言文件操作
// FILE* fd = fopen("log.txt","w");
// if(fd == NULL)
// return 1;
// const char* msg = "hello linux file\n";
// fwrite(msg,strlen(msg),1,fd);
// fclose(fd);
//Linux 系统调用
// close(2);
// int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
// umask(0);
// if(!fd)
// return 1;
// //系统调用
// const char* msg = "hello linux file\n";
// //这里输出应该为2
// printf("fd = %d\n",fd);
// write(fd,msg,strlen(msg));
// close(fd);
int fd = open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);
umask(0);
if(!fd)
return 1;
//系统调用
const char* msg = "hello linux file\n";
//把3重定向到stdout也就是1
dup2(fd,1);
//结果为3
printf("fd = %d\n",fd);
//还是写到了文件上
write(1,msg,strlen(msg));
close(fd);
return 0;
}
read()
输入重定向和输入重定向原理相同
我们做了重定向工作,后面进行程序替换的时候,难道不影响吗?
这是两个系列的操作,互不影响。进程历史打开的文件与进行的各种重定向关系都和未来进行程序替换无关。
5、其他话题
(1)、stdout和stderr 1 vs 2
代码:
只对1重定向
(2)、如何理解Linux下一切皆文件
类似于我们的多态。
二、缓冲区
exit VS _exit
写一段代码:
创建子进程后,重定向到文件里,我们的打印条数变成了
这一定和fork有关系。
缓冲区:去掉/n后我们的打印就不见了这是为什么?
这个缓冲区一定不在操作系统内部!不是系统级别的缓冲区!
(1)缓冲区刷新问题
a.无缓冲 --- 直接刷新
b.行缓冲 --- 不刷新,直到碰到\n
c.全缓冲 --- 缓冲区满了才刷新
fprintf/fwrite,写到C缓冲区调用对应的write接口刷新到我们系统缓冲区
(2)补充问题
显示器:行缓冲
文件写入:全缓冲
进程退出的时候也会刷新。
为什么会有这个缓冲区
a.解决效率问题 --- 用户的效率问题
b.配合格式化输入输出
这个缓冲区在哪里?
文件操作离不开,FILE struct,FILE里面还有对应打开文件的缓冲区字段和维护信息。
这个FILE对象属于用户呢还是操作系统呢?缓冲区是不是属于用户级缓冲区呢?
FILE对象属于用户,语言都属于用户层。这个缓冲区是用户级缓冲区。在语言层malloc(FILE)
int main()
{
//printf("hello Linux");
//close(1);
//return 0;
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
// C
printf("hello printf\n"); // stdout -> 1
sleep(2);
fprintf(stdout, "hello fprintf\n"); // stdout -> 1
sleep(2);
fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout->1
sleep(2);
// 操作提供的systemcall
write(1, str, strlen(str)); // 1
sleep(5);
//close(1); // ?
fork();
return 0;
}
为什么被打印了两次呢?这是因为首先是打印的write函数的这个,他直接写入到了系统缓冲区,所以会先打印出来,在fork之后,程序要退出的时候,子进程要刷新缓冲区,所以就发生了写时拷贝,这也就导致了,在用户缓冲区中的数据子进程打印了一遍,父进程也打印了一遍
重定向以后,向文件打印,刷新方案变成了全缓冲,遇到\n不再刷新
模拟实现一下,理解所有的理论,C文件的标准库。
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
#define SIZE 1024
//定义刷新方式
//无刷新
#define FLUSH_NOW 1
//行刷新
#define FLUSH_LINE 2
//完全刷新
#define FLUSH_ALL 4
#include <string.h>
typedef struct IO_FILE{
int fileno;
char inbuffer[SIZE];
//输入缓冲区
char outbuffer[SIZE];
int out_pos;
int flag;
}_FILE;
_FILE* _fopen(const char* filename,const char* flag);
int _fwrite(_FILE* fp,const char* s,int len);
void _fclose(_FILE* fp);
#endif
#include "myfile.h"
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
//w a r
#define FILE_MODE 0666
_FILE* _fopen(const char* filename,const char* flag)
{
int f = 0;
int fd = -1;
if(strcmp(flag,"w") == 0)
{
f = (O_CREAT|O_WRONLY|O_TRUNC);
fd = open(filename,f,FILE_MODE);
}
else if(strcmp(flag,"r") == 0)
{
f = (O_RDONLY);
fd = open(filename,f,FILE_MODE);
}
else if(strcmp(flag,"a") == 0)
{
f = (O_CREAT|O_WRONLY|O_APPEND);
fd = open(filename,f,FILE_MODE);
}
if(fd == -1) return NULL;
//创建结构体对象
_FILE* fp = (_FILE*)malloc(sizeof(_FILE));
if(fp == NULL) return NULL;
fp->fileno = fd;
fp->flag = FLUSH_ALL;
fp->out_pos = 0;
return fp;
}
int _fwrite(_FILE* fp,const char* s,int len)
{
memcpy(&fp->outbuffer[fp->out_pos],s,len);
fp->out_pos += len;
if(fp->flag & FLUSH_NOW)
{
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
else if(fp->flag & FLUSH_ALL)
{
if(fp->out_pos == SIZE)
{
printf("全刷新\n");
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
}
else if(fp->flag & FLUSH_LINE)
{
if(fp->outbuffer[fp->out_pos - 1] == '\n')
{
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
}
return len;
}
void _fflush(_FILE* fp)
{
//进程退出时刷新到缓冲区
if(fp->out_pos > 0)
{
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0;
}
}
void _fclose(_FILE* fp)
{
if(fp == NULL) return;
_fflush(fp);
close(fp->fileno);
free(fp);
}
FILE里面的缓冲区的意义是什么,提高效率。
二、文件系统Ext2
一个打开的文件上面已经说过了!
如果一个文件没有被打开呢,磁盘中进行对应的存储。
1、路径问题
2、存储问题
3、获取的问题 (属性 + 文件内容)
4、效率问题
1、认识硬件,磁盘
文件 = 文件内容 + 属性 ->磁盘上存储文件 = 存文件的内容 + 存文件的属性。
文件的内容 --- 数据块
文件属性 --- inode 128字节
结论 :Linux的文件再磁盘中存储,是将属性和内容分开存储的。
唯一的一个机械设备,也是一个外设
2、对硬件进行抽象理解
盘面,磁道,扇区。磁盘被访问的基本单元是扇区 --- 512字节/4KB,我们可以把磁盘看做由无数个扇区构成的存储介质。要把数据存到磁盘,第一个解决的问题是定位一个扇区:哪一面(定位用哪个磁头),哪一个磁道,哪一个扇区。为了提高效率,在软件设计上,一定要把数据放到一起。
LBA地址(逻辑扇区地址) --->CHS(磁道,磁头,扇区),我们就可以把磁盘看为一个线性结构。
回归到硬件,不仅仅CPU有“寄存器” ,其他设备,磁盘也有,控制寄存器,状态寄存器。数据寄存器,地址寄存器。
3、文件系统
我们在操作系统层面对磁盘分区,和进程地址空间划分区间的方式相同,同样是在内核中定义一个结构体。
Block group里面有什么?
datablocks:存文件内容的区域!以块的形式呈现,常见的是4KB大小 --- 文件系统的大小!
inode table:单个文件的所有属性,128字节。一般而言,一个文件,一个inode,有很多的inode,inode有唯一的编号。在Linux中,文件的属性中,不包含文件的名称,在Linux系统里面标识文件用的是inode编号。
inode bitmap 比特位的位置和inode的编号映射起来,比特位的内容inode是否是有效的。
block bitmap:比特位的位置和块号映射起来,比特位的内容,标识该块有没有被使用!删一个文件的时候,不用把块的文件内容清空!
group descriptor table: 描述整个分组的使用情况。
super block:文件系统的基本信息。包含的是整个分区的基本使用情况,一共有多少个组,每个组的大小、inode数量,block数量、起始inode、文件系统的类型与名称等!
每一个分区在被使用之前,都必须提前先将部分文件系统的属性信息提前设置进对应的分区中,方便我们后续使用这个分组。
新建一个文件,系统需要做什么?Linux系统中,一个文件,一个inode,每一个inode都有自己的inode编号(inode的设置,是以分区为单位的,不能跨分区)。inode表示文件的所有属性,但是文件名并不是inode内的属性!
删除一个文件,系统需要做什么?删除等于允许被覆盖。
查找一个文件,系统需要做什么?
修改一个文件,系统需要做什么?这些都是对我们的文件系统属性进行操作
那我们怎么知道一个文件的inode编号?使用者从来不关心inode,用的是文件名!接下来我们了解什么是“目录”?目录也是文件,也有自己的inode,目录也要有自己的属性!目录里有内容,目录也有数据块,内容就是该目录下文件的文件名和对应文件的inode的映射关系。
(1)为什么同一个目录下不能有同名的文件,会造成inode冲突。
(2)目录下,没有写权限,我们无法创建文件,写不到目录文件里
(3)目录下,没有读权限,我们无法查看文件,无法读取目录文件。
(4)目录下,没有执行权限,我们无法进入这个目录。
目录是文件,也有inode编号,通过路径递归到根目录通过根目录寻找。dentry缓存会保存使用过的目录。
软硬链接
ln -s file.txt soft-link
软连接是一个独立的文件,具有独立的inode,该如何理解软链接?
有独立的数据块,它的数据块里面保存的是目标文件的路径。相当于windows快捷方式。
ln test.txt hard-link
硬链接数。任意一个文件无论是目录,还是普通文件都有inode,每一个inode内部都有一个叫做引用计数的计数器,类似于指针,C++里面的share_ptr里面的引用计数。有多少个文件名指向我。删一个文件的时候计数器为0的时候该文件才会被真正的删除。
硬链接不是一个独立的文件,因为他没有独立的inode。该如何理解硬链接?
所谓的建立硬链接,本质其实就是在特定目录的数据块中新增文件名和指向的文件inode编号的映射关系!
他们的应用场景是:
软链接:就是为了创建快捷方式,使我们可以不带路径执行程序。
硬链接:./ ../ 就是硬链接的方式,通常用来路径定位,采用硬链接可以进行目录间的切换。
Linux系统不允许对目录建立硬链接 --- 为什么 ?会产生环路
目录内部有. / .. 不是目录的硬链接吗?操作系统自己建立的。不是我们用户建立的。系统在搜索时不会对./ ../进行搜索。
补充知识:
1、物理内存也需要进行管理,操作系统会有内存管理模块!物理内存为页框,磁盘为页帧。是磁盘和内存之间交换数据的单位。会存在局部性原理,在正在访问区的附近可能也会被访问,基于局部性原理!预加载机制。减少IO的次数,减少访问外设的次数。
2、操作系统怎么管理内存,操作系统可以看到内存的物理地址的。先描述再组织。
struct page{
//page页必要的属性信息
}
struct page mem_array[1048576],对内存的管理变成了对数组的管理!我们要访问一个内存,我们只需要先直接找到这个4kb对应的Page,就能在系统中找到对应物理页框,所有申请内存的操作,都是在访问内存page数组!
3、
4、基数树or基树,字典树
这些知识都是在内存管理中的知识。我们通过地址就可以映射到内存的数据块中,定时向磁盘中刷新,刷新会设计到IO子系统。这就导致了大量的IO,操作系统中就会对大量的IO进行管理,六字真言,先描述,再组织。IO子系统方面需要我们在今后的学习中慢慢了解。这里就不展开讲了。