目录
一、回顾c语言文件操作
创建一个文件是在磁盘中创建的在没有写入时,它的大小是0,但这个文件任然占据了磁盘空间。**文件=属性+内容。**文件需要属性来描述它的大小、修改时间、权限等,占据磁盘空间的就是文件的属性。
我们在使用c语言的fopen打开文件会返回一个FILE的结构体指针,在语言角度上这个FILE就是这个文件,我们通过对FILE的操作达到对文件的管理。
fopen有三种重要的打开方式:
如果忘了这些文件的操作可以看一下我前面的博客:CSDN
对于打开文件无论是vim打开,还是执行代码打开,本质上都是进程在打开文件。
二、系统调用的文件操作
文件是再硬盘上的,对硬件的管理只能由OS完成,我们使用OS必须通过OS提供的系统调用。
显然fopen的实现肯定是使用了系统调用接口,我们打开文件也可以使用系统调用接口。
系统调用文件接口
open:
打开文件
参数介绍:
pathname:要打开或创建的目标文件(和fopen打开文件所传第一个参数一样)
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算,构成flags(fopen那些w的不同打开方式,就是使用是flags不同的值完成的)。
flags参数:
- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR : 读,写打开
- 上面这三个常量,必须指定一个且只能指定一个
- O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND: 追加写
mode:创建文件时需要mode来设置文件的权限,不需要创建文件就不需要mode。
返回值:成功,返回文件描述符,失败返回-1。
close:
关闭文件,传入文件描述符关闭对应的文件。
write:
输出函数
参数介绍:
fd:文件描述符
buf:需要输出的对象
const:输出对象的空间大小
代码测试:
使用上面这些函数达到和使用fopen打开一个不存在的文件,并写入一些内容的效果:
cpp
int main()
{
int fp = open("test1.txt",O_WRONLY | O_CREAT);
const char* message = "hello file\n";
write(fp,message,strlen(message));
close(fp);
return 0;
}
传字符串大小时不要把'\0'也传进去,最好用strlen拿到大小。
文件是创建出来了,但它的权限确实两个x一个T?这也就无法读文件。这是因为没有使用带mode参数的open,mode是设置文件权限,没有mode创建出来的文件的权限介绍一堆乱码。
现在文件就可以被打开了,但仔细看test1.txt的权限就会发现和我们设置的权限是不一样的。这是因为umask它会改变文件的权限,在c语言中也可以设置当前程序的umask。
umask是系统函数,不需要管它的返回值,当前程序设置完umask,这个umask的值就只会在当前进程中使用,不会改变bash的umask。
cpp
int main()
{
umask(0000);
int fp = open("test1.txt",O_WRONLY | O_CREAT,0666);
const char* message = "hello file\n";
write(fp,message,strlen(message));
close(fp);
return 0;
}
read:
输入函数,读文件。
它的参数和使用方法跟write是一样的,不过是从给文件数据,变成了拿文件数据。
语言和系统函数间的关系:
通过上面的使用就能感觉出来,c语言文件类的函数是封装了系统文件的函数,这有两个好处:
1、使用上变的更简单了,学习成本得到了降低。
2、Linux有自己的系统调用函数,window也有自己的调用函数,可能实现思路是一样的,但参数、命名和一些小功能上是会存在差异的。c语言(各个语言都是)编写库的时就会将各个系统之间的文件函数(不限于文件函数),单独做处理,封装成统一的函数让我们使用,语言这时就有了跨平台性,我们在不同系统下编写代码就不需要重新学习不同的文件函数了。
flags的实现思路
flags是让文件以不同的方式打开,每种方式就有开和关两种状态,这里是使用了一个整形,整形的每一个二进制位都对应的是不同的方式(不是32位都使用了,状态也没那么多),通过判断当前位上是否为1,决定执行这一位的功能是否执行。
实现一段简单的代码来完成上面描述的功能:
cpp
#include<stdio.h>
#define one 1
#define tow 2
#define three 3
#define four 4
void print(int flags)
{
if(flags>>0 & 1) //右边第一位是否右值
{
printf("flags : first place\n");
}
if(flags>>1 & 1) //右边第二位
{
printf("flags : second place\n");
}
if(flags>>2 & 1) //第三位
{
printf("flags : third place\n");
}
if(flags>>3 & 1) //四
{
printf("flags : fourth place\n");
}
}
int main()
{
print(one);
printf("\n");
print(one | three);
printf("\n");
print(tow | three | four);
return 0;
}
把函数内的printf换成文件打开的不同功能,就能实现处fopen的不同方式打开文件。
三、OS内文件的管理
语言角度理解文件描述符
fopen返回的是一个结构体FILE,open返回的却是一个整形,先来看看这个整形存放的是上面。
cpp
int main()
{
int fp1 = open("test1.txt",O_WRONLY | O_CREAT,0664);
printf("fp1 : %d\n",fp1);
int fp2 = open("test2.txt",O_WRONLY | O_CREAT,0664);
printf("fp2 : %d\n",fp2);
int fp3 = open("test3.txt",O_WRONLY | O_CREAT,0664);
printf("fp3 : %d\n",fp3);
int fp4 = open("test4.txt",O_WRONLY | O_CREAT,0664);
printf("fp4 : %d\n",fp4);
close(fp1);
close(fp2);
close(fp3);
close(fp4);
return 0;
}
可以观察到打开的文件返回值就像排列好的一样,但它并不是从0开始的。c语言默认是打开三个输入输出流:stdin(输入流:键盘)、stdout(输出流:显示器)、stderr(标准错误,输出流:显示器) 。Linux下一切皆文件 ,这里也可以用到c语言上,它将显示器和键盘看做了文件,我们使用的printf和scanf也就是将数据写入stdout和读取stdin。这三个文件在底层自然也是使用open打开,它们分别对应的就是0(标准输入:键盘)、1(标准输出:显示器)、2(标准错误:显示器),这个进程打开其他的文件也就依次跟在后面,每个文件都对应的是唯一的值,我们对文件的操作就变成了对这些值的操作,这些值被称作文件描述符。
代码测试
对1文件进行写入:
cpp
int main()
{
const char* s= "hahaha\n";
write(1,s,strlen(s));
return 0;
}
结果就和printf打印一样。
现在知道了文件描述符本质是文件的映射,我们在来站在进程的角度看看是如何映射的。
进程角度理解文件描述符
文件打开的存储方式
文件只能被进程打开,进程是可以打开多个文件,那这些文件肯定是要被管理起来的,Linux中管理这种存在很多统一信息类型,但值不同的对象,会采用先描述,再组织的理念。
描述文件的信息,那不就是文件的属性吗?根据冯诺依曼体系,哪怕是只打开文件也需要将文件加载到内存中,OS就给文件创建了一个struct file的结构体描述文件,再使用链表的形式组织起来。但文件的数据不会存放到内存中,OS会给每个文件开辟一块空间文件内存级的缓存,我们无论是读还是写,都需要将进程的数据放到里面或是磁盘文件的数据拷贝到里面,间接进行对文件的修改。
但是每个进程的文件描述符都是像下标一样连续的,bash内的进程有很多个,进程不可能组织着去打开文件让进程所需文件连在一起。所以OS就给每个进程创建了一个struct files_struct*指向一个结构体,这个结构体里存放了一个struct file*的数组,这个数组前三个元素自然就是0、1、2三个流文件,每打开一个文件就将这个文件放入到数组中,文件描述符也就是这个数组的下标,对应的是不同文件。
文字还是很难理体会到它是如何储存的,来通过图片感受一下:
再来看open打开文件是在做什么:
- 创建file
- 开辟文件内存级的缓存
- 查看进程的文件描述符标(struct file*[])
- 把file地址填入对应的表中
- 返回下标
代码测试
文件描述符的变化:
cpp
int main()
{
close(0);
close(2);
int fp = open("test1.txt",O_WRONLY);
printf("fp : %d\n",fp);
close(fp);
return 0;
}
发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
把硬件当文件开看待
虽然Linux内一切皆文件,但把硬件这种实质性的东西当文件来处理,还是有些抽象。硬盘中的文件创建file,是有文件属性可以填写的,硬件呢?
OS是通过驱动层来管理硬件的,驱动层一般由各个硬件厂商提供接口,这些接口有硬件的基本信息(品名、型号、电压电流大小等)和修改硬件数值的函数(比如修改鼠标的DPI)。file内这些基本信息使用struct device封装,函数使用函数指针去指向它们,将这些函数指针称为方法表。
有的设备没有读写或其他一些功能,那file内对应函数的函数指针就置为空,表示没有这种功能。