【Linux修炼】12.深入了解系统文件
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
每一个不曾起舞的日子都是对生命的辜负。
文件fd
一. 重新谈论文件
1. 共识的问题
在这篇正式开始之前大家要有一些共识性的问题
- 空文件也要在磁盘占据空间
- 文件 = 内容 + 属性
- 文件操作 = 对内容+对属性+对内容和属性的操作
- 标定一个问题必须使用文件路径+文件名【唯一性】
- 如果没有指明对应的文件路径默认是在当前路径进程当前路径进行文件访问
- 当我们把fopen、fclose、fread、fwrite等接口写完之后代码编译之后形成二进制可执行程序之后但是没有运行文件对应的操作有没有被执行呢没有对文件的操作的本质是进程对文件的操作因此没有运行不存在进程所以不会被执行。
- 一个文件如果没有被打开可以直接进行文件访问吗不能一个文件要被访问就必须先被打开打开的方式用户进程+OS
那是不是所有磁盘的文件都被打开呢显然不是这样因此我们可以将文件划分成两种a.被打开的文件b.没有被打开的文件 。对于文件操作一定是被打开的文件才能进行操作本篇文章只会讲解被打开的文件。
- 文件操作的本质进程和被打开文件的关系
2. 重谈C语言文件操作
2.1 概要
- 语言有文件操作接口、C++有文件操作接口Java、Python、php、go、shell都有文件操作接口他们实际上的底层都是相同的函数接口因为都需要通过OS调用。
- 文件在磁盘上磁盘是硬件只有操作系统有资格访问所有人想访问磁盘都不能绕过操作系统必须使用操作系统调用的接口即OS会提供文件级别的系统调用接口。
- 所以上层语言无论如何变化库函数底层必须调用系统调用接口。
- 库函数可以千变万化但是底层不变因此这样能够降低学习成本—>学习不变的东西。
2.2 C语言文件实操
复习一下下面fp按顺序对应以下三个操作依次写入文件、打印文本信息、追加文本信息到文件中。
- 细节问题以w方式单纯的打开文件c会自动清空内部的数据。
对于C语言调用的fopen打开文件实际上底层调用的是操作系统的接口open其他语言也是这样只不过语言级别的接口是多了一些特性下面就看看man 2 open
对于flag标记位一般来说对于C语言一个int类型代表一个标记位那如果要传10个标记位呢对于整形来说实际上有32个比特位那是不是可以将每一个比特位赋予特定的含义通过比特位传递选项从而实现对应的标记呢一定是可以的。因此在介绍open函数之前先来介绍一下标记位的实现
- 注意一个比特位一个选项不能重复。标记位传参
因此我们再看这个open函数就明白了是什么含义就是通过不同的flags传入不同的标记位那接下来看看open函数怎么用
2.3 OS接口open的使用比特位标记
不废话我们知道了上面用的头文件就直接开始使用了
int open(const char* pathname, int flags )
int open(const char* pathname, int flags, mode_t mode )
第一个函数是在文件已经存在的基础上使用的如果不存在源文件那么就需要用第二个函数即第二个函数如果文件不存在就会自动创建文件。
这样就能创建出权限为0666的log.txt了。
fd的值也是有讲究的这里看一下fd的值
这个值暂时记住下面将会讲到。
2.4 写入操作
对于C语言来讲除了打开关闭还有写入fwrite等函数接口因此对于OS也存在一个接口write
无论这个buf是什么类别在OS看来都是二进制至于这个类别是文本还是图片都是由语言本身决定的。
下面开始
可以看出对于C语言中的w封装了文件接口的标识符O_WRONLY(写)
、O_CREAT(不存在就创建文件)
、O_TRUNC(清空文件)
以及权限。
2.5 追加操作
想要把清空变成追加只需要将open内部的最后一个清空标识符改成追加的标识符
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
2.6 只读操作
将标识符换成读的标识符
int fd = open(FILE_NAME, O_RDONLY);
这就是以上接口open/close/write/read
分别对应C语言的fopen/fclose/fwrite/fread
此外对于读还有lseek对应C语言的fseek。
系统调用接口 | 对应的C语言库函数接口 |
---|---|
open | fopen |
close | fclose |
write | fwrite |
read | fread |
lseek | fseek |
即库函数接口是封装了系统调用接口的所有语言的库函数都存在系统调用的影子。
二. 如何理解文件
- 文件操作的本质进程和被打开文件的关系
1. 提出问题
进程可以打开多个文件那是不是意味着系统中一定会存在大量的被打开的文件被打开的文件要不要被操作系统管理起来呢答案是一定的。
那么OS如何管理呢? 先描述再组织。因此操作系统为了管理对应的打开文件必定要为文件创建对应的内核数据结构标识文件struct file{}
(包含了文件的大部分属性) 因此将结构体链式链接通过找到链表的首地址从而实现对链表内容的增删查改。
同时创建多个文件并打印其返回值
- 为什么从3开始0、1、2呢
- 连续的小整数->数组->数组下标
在回答这个问题之前我们需要了解三个标准的输入输出流stdinstdoutstderr!
FILE* fp = fopen();
这个FILE实际上是一个结构体而对于上面的三个输入输出流实际上也是FILE的结构体
对于这个结构体必有一个字段–>文件描述符下面就看一下这个文件描述符的值是什么
2. 文件描述符fd
- 通过对open函数的学习我们知道了文件描述符就是一个小整数即open的返回值
因此这也就解释了为什么文件描述符默认是从3开始的因为012默认被占用。我们的C语言的这批接口封装了系统的默认调用接口。同时C语言的FILE结构体也封装了系统的文件描述符。
那为什么是0,1,2,3,4,5……呢下面就来解释
PCB中包含一个files指针他指向一个属于进程和文件对应关系的一个结构体struct files_struct
而这个结构体里面包含了一个数组叫做struct file* fd _array[]
的指针数组因此如图前三个0、1、2被键盘和显示器调用这也就是为什么之后的文件描述符是从3开始的然后将文件的地址填入到三号文件描述符里此时三号文件描述符就指向这个新打开的文件了。
再把3号描述符通过系统调用给用户返回就得到了一个数字叫做3所以在一个进程访问文件时需要传入3通过系统调用找到对应的文件描述符表从而通过存储的地址找到对应的文件文件找到了就可以对文件进行操作了。因此文件描述符的本质就是数组下标。
而现在知道文件描述符就是从0开始的小整数。当我们打开文件时操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组每个元素都是一个指向打开文件的指针所以本质上文件描述符就是该数组的下标。所以只要拿着文件描述符就可以找到对应的文件
三. 文件fd的分配规则
直接看代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
输出发现是 fd: 3
关闭0或2再看
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
发现是结果是 fd: 0
或者 fd 2
因此我们知道了文件fd的分配规则就是将这个array数组从小到大按照循序寻找最小的且没有被占用的fd这就是fd的分配规则。
四. 重定向
1. 什么是重定向
对于上面的例子我们关闭了文件描述符0和2对应的文件吗那么如果关闭1呢
#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);
fprintf(stdout, "fd :%d\n", fd);//与上面打印是一样的功能
close(fd);
return 0;
}
根据上面讲到的文件描述符的分配规则这段代码我们按照顺序进行解释:
首先关闭文件描述符1所对应的stdout(标准输出输出到显示器)然后通过f分配这个文件的fd会从小到大扫描发现1的位置没有被使用于是就会将这个新创建的文件myfile与对应的指针进行连接因此当我们的printf以及fprintf打印到stdout时由于上层的文件描述符stdout对应的还是1就会在内核中找到array[1]中对应的文件进行操作但此时1对应的已经不是标准输出到显示器而是myfile文件因此我们在打印时也就不会在显示器中看到fd的值而是在myfile文件中。
那我们来看看是不是这样
在log.txt没有打印是由于缓冲区的问题在fprintf的下面加上fflush(stdout);
再看看
即当所有现象都符合我们的预期时这种现象就是重定向。
- 重定向的本质上层用的fd不变在内核中更改fd对应的
struct file*
的地址。
常见的重定向有:>输入, >>追加, <输出。
2. dup2 系统调用的重定向
在上面演示的无论是分配规则还是重定向直接以close关闭的操作非常的挫因为这样的close操作不够灵活所以现在介绍一个系统调用的重定向接口dup2
int dup2(int oldfd, int newfd);//newfd的内容最终会被oldfd指向的内容覆盖
- dup2的返回值也就是fd的文件描述符失败返回-1
那就来演示一下刚才的功能打印到文件里
可以发现这样操作简化了刚才的操作另外fd的值也不会被改变。
输出重定向演示完了那我们就可以实现我们刚才提到的三个重定向剩下的追加、输入重定向了。
1. 追加重定向
2. 输入重定向
上面是从键盘中读取如果不想从键盘读我们可以重定向到向指定文件中读取
3. 理解>、>>、<
在之前的学习中我们模拟过shell部分功能的实现在这里为了理解这三个常见的重定向用shell模拟实现这三个重定向代码链接lesson18/myshell/myshell.c · 每天都要进步呀/Linux - 码云 - 开源中国 (gitee.com)
此外这几个常见的重定向的使用方法文章链接
- 注文件是共享的不会因为进程不同而权限不同因为文件是磁盘上的与进程之间是独立的。即当子进程被创建并且发生写时拷贝时原来的文件并不会再次被拷贝一次。
五. 如何理解Linux一切皆文件
一张图描述
即我们利用虚拟文件系统就可以摒弃掉底层设备之间的差别统一使用文件接口的方式进行文件操作。