一、共识原理
进程 : 打开的文件 = 1:n(即一个进程可以打开任意个文件)
由以上两点我们可以得到:操作系统内部,一定存在大量的被打开的文件! —-OS要不要管理这些被打开的文件呢? —肯定是要的,那么怎么管理呢?—-先描述,在组织 —- 所以在内核中,一个被打开的文件都必须有自己的文件打开对象,包含文件的很多属性。struct XXX {文件属性; struct XXX* next};
二、C系列文件接口
第一个参数是路径,第二个参数是打开方式。返回值是FILE*即文件指针
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
return 0;
}
因为我们是以写的方式打开文件,所以如果不存在这个文件,他会自动创建一个这样的文件
注意这里的打开文件的路径和文件名,默认在当前路径下新建一个文件。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("PID:%dn",getpid());
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
sleep(1000);
return 0;
}
在这里,如果我们更改了当前进程的cwd,就可以把文件新建到其他目录了
#include <stdio.h>
#include <unistd.h>
int main()
{
chdir("/home/jby_1");
printf("PID:%dn",getpid());
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
sleep(1000);
return 0;
}
我们可以观察一下运行结果。可以发现这个文件去对应的路径创建了。
我们再看一下当前的目录
它的作用是将nmemb个size大小的ptr处的数据写入到一个文件中
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
printf("PID:%dn",getpid());
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
const char* message = "hello linux message";
fwrite(message,strlen(message),1,fp);
fclose(fp);
return 0;
}
这里我们会发现,原来文件的内容全部不见了。所以说w方法写入之前,都会对文件进行清空处理
这就类似于之前的重定向,就是相当于用w的方法打开了文件。然后写入内容
所以,如果我们使用重定向的时候,前面什么也没有,就相当于清空了这个文件。当我们用w的方法打开一个文件以后,里面什么也都不会有了。
那么现在我们再来看一下上面这个代码
我们前面在这里没有+1,不过c语言中,默认会添加上,那么这里需要加1吗
我们先运行一下,然后我们就会发现,这个文件里面的内容就变成了这样了,出现了一个乱码
所以说,这里是不需要+1的,因为字符串后加上是C语言的规定,与文件有什么关系呢?
不过在打开文件的方式中,有一个方式是a方式,它是在文件的结尾写。如果文件不存在,则创建一个文件
我们试一下下面这个代码
运行结果为
我们知道Linux下一切皆文件。在C语言中默认会打开三个流,stdin,stdout,stderr。如下图所示,这三个流的类型就是文件指针。
其实类似的,C++中也会默认打开三个流:cin && cout && cerr
我们先看下面的代码。使用fprintf,我们也可以实现前面的在文件中打印的操作
对于fprintf,我们也可以将它的第一个流改为stdout
其实
三、从C过渡到系统:文件系统调用
我们知道,文件其实是在磁盘上的,磁盘是外部设备。所以访问文件其实是访问硬件!
我们知道我们是不可直接访问硬件的,必须要自顶向下贯穿访问。而操作系统不相信任何人,所以就需要提供系统调用!
所以几乎所有的库只要是访问硬件设备,必定要封装系统调用。即printf/fprintf/fscanf/fwrite/fread/fgets/gets/…这些都是库函数,他们必定要封装系统调用接口
上面的这个是下面的子集
所以我们先只谈三个参数的open
int open(const char *pathname, int flags, mode_t mode);
在这里,第一个参数是对应文件的路径:可是是绝对/相对都可以。也可以直接是文件名,那么默认当前目录
即flag就是一个打开的模式。必须包含O_RDONLY,O_WRONLY或者O_RDWR。
我们先看如下代码
运行结果为,打开失败了
这是因为,我们刚刚用到的这个O_WRONLY选项它并不会新建文件。我们得告诉操作系统,如果文件不存在,我们需要新建它。所以我们还得加上O_CREAT选项
运行结果为
这是因为在linux中,要创建一个文件必须得告诉权限是什么。所以就需要第三个参数了。设置好权限
不过这里我们发现创建的文件它的权限也不是666,而是664,这是因为我们之前所说的,linux创建一个文件有默认的umask。这是由于这个umask是0002,所以最后一个才出现了一些问题
但是如果我们非要创建一个666的文件。我们就需要用这个umask系统调用了
它可以将代码里面的umask给修改掉。这里只影响该进程,不影响系统的
对于这个open函数,它的返回值为一个int,这个整数我们称为file descriptor,即文件描述符,如果打开失败,则为-1。
它的参数正好就是文件描述符,所以我们可以传入一个文件描述符,就可以关闭对应的文件了
还有一个系统调用是write
运行结果为
如果我们紧接着将字符串改短一些
那么最终的结果为
现象就是,原来的内容都保留着,但是会从文件开始覆盖式的写入,但是并不会清空。
那么如果我们也想做到清空操作呢?
在我们打开文件的时候,即open函数中的第二个参数,我们可以使用O_TRUNC,即清空
此时我们就可以看到,原来的就被清空了
那么如果我们想要实现追加写的功能呢?我们可以使用O_APPEND
运行结果为
这样就实现了追加的功能
所以我们得到的结论是
FILE* fp = fopen("log.txt", "a");
//上面的代码下层一定封装了下面的系统调用接口
int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
FILE* fp = fopen("log.txt", "w");
//上面的代码下层一定封装了下面的系统调用接口
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
所以说,无论是什么语言,最终底层一定会采用同样的接口。底层都是open,只是他们的上层有所区别罢了。
不过我们会好奇的是,上层的函数返回值是指针,而下层的系统调用接口的返回值是int,它们是如何联系起来的呢?
四、访问文件的本质
如下图所示,我们知道,文件都是在磁盘中存储着的,并且文件需要由一个进程来打开,那么进程要打开这个文件。就需要为这个文件创建对应的内核数据结构,即struct file。这个结构体里面存储着一个被打开文件的各种信息。当打开了多个文件的时候,这些结构体就会用一个双向链表连接起来。
也就是先描述在组织,此时对文件的管理就变为了对这个链表的增删查改
可是我们的系统存在多个进程。那么哪一个文件是哪一个进程的呢?所以我们需要建立对应关系
如下图所示,我们的进程PCB结构体里面就有一个指针它指向struct files_struct这样的一个结构体,这个结构体里面,有一个struct file* fd_array[]数组,数组里面存储着很多struct file*指针,然后每当这个进程打开一个文件时,要创建一个struct file结构体,然后将这个结构体的地址放入一个没有被使用的下标中。
而这个表就是文件描述符表。而前面的open系统调用中,这个返回值,就是对应文件描述符表中的下标。
所以这个fd,本质就是一个数组的下标。我们使用write这些接口的时候就需要使用文件描述符来进行辨认文件
我们也许会思考,既然都已经让进程管理起来了文件,为什么要让文件用双链表呢?因为进程也可能会崩掉。
现在我们已经了解了访问文件的本质了,open的返回值其实就是文件描述符表的下标,那么既然如此。我们来验证一下
运行结果为
我们可以在多验证一些
运行结果为
这里返回连续的下标我们也能理解,我们也知道失败会返回-1。那么0,1,2这些下标在哪呢?
我们会注意到,0,1,2刚好是3个。在C语言中刚好要打开三个流
所以每一个被打开的文件,它在底层根本就不存在这个FILE*流,在操作系统中只认fd。
所以我们现在可以验证一下,这三个流就是0,1,2这三个文件
运行结果为
对于这个我们可以用read系统调用接口
运行结果为
注意在这里,由于操作系统并不知道我们读取的是字符串,它最后也不会加上,所以我们需要自己加上
所以当一个C语言程序启动的时候,会打开三个标准输入输出流,这个是C语言的特性吗?
因为我们电脑刚打开,显示器,键盘早就被操作系统打开了。我们在编程的时候,必须得用显示器和键盘输入和查看结果,所以语言默认都能打开。
那么在C语言中这个FILE是什么呢?
这个FILE是C库自己封装的一个结构体,这个结构体里面必须包含文件描述符。因为操作系统只认文件描述符。
我们可以来证明一下
运行结果为
所以现在我们就知道了这里有两种的封装了。
一种是库函数封装了系统调用接口,一种是FILE封装了文件描述符
如果我们直接将1号文件给关了
我们会发现什么也没有了
因为一号就是显示器文件。而printf里面必然调用了这个1号文件描述符。
如果我们将代码改为下面的
那么结果为
因为我们用的是2号文件去写的。我们关的只是一号文件
还有一点是,在struct file结构体里面,其实还有一个信息是引用计数count。因为可能多个文件描述符指向同一个文件。一个文件描述符指向就是1,两个指向就是2.
所以我们关闭文件去调用close的时候,它的工作其实很简单,只需要引用计数减减,然后将这个指针位置置空。然后判断这个引用计数是否为0,如果不为空则什么也不用做到,如果为空,那么就在去回收这个struct file对象。
所以这就是我们刚刚关闭了1号文件,2号文件还能继续打印的原因。因为仅仅只是引用计数减减了。
原文地址:https://blog.csdn.net/jhdhdhehej/article/details/134628700
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_18587.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!