Linux|内存级文件原理
目录
进程与文件
Linux下的文件系统
文件操作,及文件流
C语言函数
文件流
文件描述符
系统调用操作
系统调用参数
重定向与文件描述符
输出重定向
输入重定向
文件=内容+属性
Linux下一切皆文件
进程与文件
当我们对文件进行操作时,文件必须要被加载到内存中,然后CUP从内存中拿到此文件进行操作,没有打开的文件放在磁盘中存储。
文件的打开其实也是设计到内部某个进程。无论是系统调用,还是专有库中的函数,都是启动进程来进行打开。进程会自动记录目前启动时的当前路径,平常所说的相对路径就是指相对于当前进程路径下的路径。当我们没有特意说明文件路径在此进程中对文件操作时,默认会在此进程的路径下进行。比如我们使用C语言新建文件使用绝对路径,默认就会在此进程的路径下进行,若是此进程的路径发生改变,新建的文件会在改变后的路径下进行,这就是相对路径的原理。
一个进程可以打开多个文件,那么我们知道系统中存在多个进程,系统中必然存在很多被进程打开的文件,OS是一点要管理多个被打开的文件的,如何管理呢,同样是先描述在组织,所以呢内核中一定要有描述被打开文件的结构体,并定义其对象!被打开的文件叫做内存级文件,这也是这篇文章的重点,没打开的我们后续再说,现在我们逐步解开描述被打开文件的结构体的面纱。
Linux下的文件系统
在Linux系统下,我们可以把一切都看成文件(包括硬件)。Linux系统有一个重要特性,即“一切皆文件”的原则。无论是普通的磁盘文件、目录,还是网络套接字、硬件设备,在Linux中都被抽象为文件。
我们平常使用高级语言进行底层文件的调用,本质上是封装了系统调用。因为用户不能直接调用系统硬件,本质上是操作系统进行调用的。我们通常使用语言进行调用实际就是操作系统提供了相应的接口供用户使用。比如以C语言为例,C语言的库函数接口fopen、fclose、fread、fwrite 在某种意义上来讲调用的是系统接口open、close、read、write(这些函数运用跟C中的文件操作相似,可用man指令查看文档),只不过对系统调用进行了封装。系统调用接口和库函数的关系如下:
这里先对linux下一切皆是文件混个眼熟,后面会解释具体原因!!!
文件操作,及文件流
C语言函数
fwrite | size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream); 返回值:写入了多少个基本单位 用于将二进制数据写入文件 |
fgets | char *fgets(char *str, int n, FILE *stream); 返回值 成功时:返回指向存储数据的指针 str。 失败或遇到文件结束(EOF):返回 NULL。 |
fopen | FILE *fopen(const char *filename, const char *mode); 返回值 成功时:返回一个指向文件的 FILE 类型指针。 失败时:返回 NULL,并可通过 perror 或 strerror(errno) 获取错误信息。 |
fputs | int fputs(const char *str, FILE *stream); 返回值 成功:返回非负值(通常为 0)。 失败:返回 EOF(通常为 -1),表示发生错误。 |
这几个函数大家可能都用过,但对于FILE*stream是什么,理解的可能就不透彻了,我们先继续往下看
文件流
下面我们来认识一下文件流操作。程序在启动时,默认会打开三个文件流:stdin、stdout、stderr。这三种流的类型都是文件指针FILE*。
- stdin:标准输入——默认是键盘设备。计算机系统从此文件流中获取数据信息,即从此文件中读取数据。
- stdout:标准输出——默认是显示器设备。将数据输出到此文件流中,即从此文件中输出数据。
- stderr:标准错误——默认是显示器设备。用于输出程序或命令的错误信息,与stdout原理相似。
正因有了标准输入输出流操作(I/O设备操作),才能使得程序能够与用户和其他程序进行有效的交互。
那么问题来了,系统下的所有都是文件,程序系统又是如何找到对应的文件?
其实每个文件都有一个对应的文件描述符进行标志。文件描述符是一个非负整数,与文件名形成了一种索引关系,使得程序可以通过这个整数来访问和操作对应的文件。
文件描述符
文件描述符的范围是0到N,其中0、1、2是特殊文件的文件描述符:0代表标准输入(stdin),1代表标准输出(stdout),2代表标准错误输出(stderr)。一般情况下,文件描述符从3开始数往后分配。因为内部的文件描述符其实就是存放管理文件结构体(struct file:包含三个方面,第一个是能够通过指针让我们找到文件的属性,第二个是对文件操作的一堆方法,第三个是是所提供的缓冲区。打开一个文件系统内部就会创建一个struct file结构体对文件进行管理)的指针数组 fd_array 的下标,此指针数组每个元素都是一个指向打开文件的结构体指针,而task_struct内部存在一个指针,指向存放此指针数组的结构体(struct files_struct)。
总的来说文件描述符就是数组的下标,当使用一个文件时就必须找到此文件的文件描述符,通过文件描述符来找到对应的文件。这里的重点在于文件操作符,只要我们拿到文件操作符fd,就能够通过file_struct结构体内部的fd_array数组指针找到对应管理文件的结构体file,对其文件进行操作。
所以,C标准库中的FILE(文件流:随机读取或写入文件,即与文件操作的底层)其实就是自己封装的一个结构体,里面封装了 stdin、stdout、stderr 的文件描述符0,1,2。之所以系统不直接封装而让语言单独封装是为了保证可移植性。若是系统直接封装,一旦换了平台系统可能就会出问题,导致不可移植。其实不仅仅是流操作,很多有关系统接口也一样,为了保证可移植性,都是在不同语言内部封装不同系统调用的接口和相关的文件接口。
系统调用操作
既然已经知道了文件描述符,我们进一步来了解系统调用。上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而, open close read write lseek 都属于系统提供的接口,为系统调用接口。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
用open打开文件,当文件不存在新建文件时,需要用到第三个参数,设置文件权限,
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
系统调用参数
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
以上三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
注意:Mode给定权限,权限比如给0666但是为了不被初始0002影响,设置一个umask(0)掩码写在文件中,就会按照我们给的权限直接设置了
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);使用open时,我们传递系统调用参数时候用到了|,这代表按位或运算,为什么这么传参数呢,我们来具体分析:
倘若你参数是一个变量,那么多个参数就代表多个变量需要传递,这是一个有点浪费空间的举动,因为一个变量就是多个字节;对于参数来说它只代表一个功能,如果你传递了这个参数就代表希望其有这个功能,其实就是有和没有的区别,那你完全可以用1,0这样的数字来传;进一步节省空间,我们可以利用位图的思维来处理这个事情,及按照bit位的1,0来代表有某个参数或没有,在说的直白些就是有某个功能或没有。我们提前对每个bit位约定好如果是1代表具有哪个功能属性,对最后所有的参数取或操作成为一个集中的参数,转换为2进制,位置上有1就就代表有这个参数的传递,在使用的时候用约定好功能绑定的那个比特位和传进来的参数取异为1就执行这个功能;或起来的整体不就可以用很小的内容传递多个参数了,大大节省空间,提升效率!!!!
下边这个图片解释了,用bite位传递参数,并且使用的的实现例子
1<<0= 00001 等价于 1
1<<1=00010 等价于 2
1<<2==00100等价于 4
以此类推
1<<n 表示将数字 1 左移 n 位。
&:
都为 1 时,结果为 1。
只要有一个为 0,结果为 0。
If中只要不是0都可以输出
linux下一切皆是文件
现在我们来理解这句话,现在我们来理解这句话,linux下一切皆文件,更准确的说是linux下一切皆struct file,屏蔽底层差异。如何理解呢,先看下面这个图片
我们将键盘,显示器,磁盘,网卡都描述为stuct,他们都有系统对应停供的系统接口,读操作,写操作,可是键盘没有写,显示器没有写,磁盘的读写也不会和网卡一模一样,但是我们调用的接口都是一样的!!!
这其实就是上层调用读写任何设备,使用文件对象的函数,指针调用方法,屏蔽硬件底层差异,上层使用统一的接口。所以linux下一切都可以是文件对象,根据各部分实际的不同再来执行不同的操作。对于linux下一切皆文件可能还是不那么清楚,其实这也只是其中的一个,其他的原因也会随着不断深入学习浮出水面
重定向与文件描述符
输出重定向
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
这里关闭了1,也就是stdout:标准输出流,所以这时候我们打开的文件fd就变成了1,默认输出到1,而此时1变成了fd,所以本应该打到显示器上的内容被显示在了文件里。
如果关闭0就是输入重定向,打开的文件就成了0,所以文件里的内容就会被显示在显示屏上,因为默认从0里面获取输入,本来是键盘,变成了文件而已。
文件描述符会按照最小下标分配,以上程序中关掉了系统下标准输出流的文件描述符1。至于fileno(stdout)的调用,由于stdout是C语言的流,非系统专属,当程序启动时C的stdout就默认打开,因此close(1);只是关闭了与stdout关联的文件描述符,与C的stdout内部文件数据没有关系,fileno(stdout)仍会返回原始的文件描述符值(即1),但是 close是系统操作,它会关闭了底层系统文件描述符表索引值1所指向的 stdout 文件。当我们创建文件log.txt时系统会给此文件分配描述符1。C中的输出都是往文件描述符为1所对应的文件中输出的,即一般情况下都是往标准输出流stdout中输出。这里log.txt的文件描述符为1,进程拿到文件描述符后会自动往文件描述符表fd_array中寻找索引值为1对应的文件中输出,所以就会出现以上输出重定向。输入重定向同理,将文件描述符为0的进行重新指向。
输入重定向
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
close(0); //将标准输入重定向(键盘)去除
open("log.txt", O_RDONLY); //此时文件log.txt的文件描述符为0,即成为了输入流
int a = 0;
cin >> a; //从输入流中读取数据
cout << a << endl;
return 0;
}
输出:123456
不难发现,以上类似的程序完成重定向功能比较麻烦——先close关闭再open分配。说白了,重定向功能就是分配到指定的文件描述符,而文件描述符对应指定文件的功能,这里我们可直接让指定文件的文件描述符指向对应功能的文件描述符所指向文件的功能即可。比如将文件描述符为3的指向文件描述符为1所对应的系统文件,即指针数组元素之间的浅拷贝fd_array[1]=fd_array[3]。
#include <unistd.h>
int dup2(int oldfd, int newfd);
注意,这里的oldfd会保留到最后,所以oldfd是我们想重定向的文件名,newfd就是我们想替换的文件名1、2之类的。
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
cout << "fd = " << fd << endl;
return 0;
}
输出:fd = 3
补充命令:
进程在启动的时候会记录的路径是当前路径 命令:cwd,chdir:更改路径