每一个不曾起舞的日子,都是对生命的辜负。
一. 重新谈论文件
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呢?
- 连续的小整数->数组->数组下标
在回答这个问题之前,我们需要了解三个标准的输入输出流:stdin,stdout,stderr!
FILE* fp = fopen();
这个FILE实际上是一个结构体,而对于上面的三个输入输出流,实际上也是FILE的结构体:
对于这个结构体必有一个字段–>文件描述符,下面就看一下这个文件描述符的值是什么:
2. 文件描述符fd
- 通过对open函数的学习,我们知道了文件描述符就是一个小整数,即open的返回值
因此这也就解释了为什么文件描述符默认是从3开始的,因为0,1,2默认被占用。我们的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一切皆文件
一张图描述:
即我们利用虚拟文件系统就可以摒弃掉底层设备之间的差别,统一使用文件接口的方式进行文件操作。
转载:https://blog.csdn.net/NEFUT/article/details/128576198