每一个不曾起舞的日子,都是对生命的辜负。
在这篇正式开始之前,大家要有一些共识性的问题:
那是不是所有磁盘的文件都被打开呢?显然不是这样!因此我们可以将文件划分成两种:a.被打开的文件;b.没有被打开的文件 。对于文件操作,一定是被打开的文件才能进行操作,本篇文章只会讲解被打开的文件。
复习一下:下面fp按顺序对应以下三个操作依次:写入文件、打印文本信息、追加文本信息到文件中。
对于C语言调用的fopen打开文件,实际上底层调用的是操作系统的接口open,其他语言也是这样,只不过语言级别的接口是多了一些特性,下面就看看:man 2 open
对于flag标记位,一般来说对于C语言,一个int类型代表一个标记位,那如果要传10个标记位呢?对于整形来说,实际上有32个比特位,那是不是可以将每一个比特位赋予特定的含义,通过比特位传递选项,从而实现对应的标记呢?一定是可以的。因此在介绍open函数之前,先来介绍一下标记位的实现:
因此我们再看这个open函数,就明白了是什么含义,就是通过不同的flags,传入不同的标记位,那接下来看看open函数怎么用:
不废话,我们知道了上面用的头文件就直接开始使用了:
int open(const char* pathname, int flags )
int open(const char* pathname, int flags, mode_t mode )
第一个函数是在文件已经存在的基础上使用的,如果不存在源文件,那么就需要用第二个函数,即第二个函数如果文件不存在就会自动创建文件。
这样就能创建出权限为0666的log.txt了。
fd的值也是有讲究的,这里看一下fd的值:
这个值暂时记住,下面将会讲到。
对于C语言来讲,除了打开关闭,还有写入fwrite等函数接口,因此对于OS也存在一个接口:write
无论这个buf是什么类别,在OS看来都是二进制!至于这个类别是文本还是图片,都是由语言本身决定的。
可以看出,对于C语言中的w,封装了文件接口的标识符:O_WRONLY(写)
、O_CREAT(不存在就创建文件)
、O_TRUNC(清空文件)
,以及权限。
想要把清空变成追加,只需要将open内部的最后一个清空标识符改成追加的标识符:
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
将标识符换成读的标识符
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 |
即库函数接口是封装了系统调用接口的,所有语言的库函数都存在系统调用的影子。
进程可以打开多个文件,那是不是意味着系统中一定会存在大量的被打开的文件,被打开的文件要不要被操作系统管理起来呢?答案是一定的。
那么OS如何管理呢? 先描述,再组织。因此操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构标识文件:struct file{}
(包含了文件的大部分属性) 因此将结构体链式链接,通过找到链表的首地址从而实现对链表内容的增删查改。
在回答这个问题之前,我们需要了解三个标准的输入输出流:stdin,stdout,stderr!
FILE* fp = fopen();
这个FILE实际上是一个结构体,而对于上面的三个输入输出流,实际上也是FILE的结构体:
对于这个结构体必有一个字段–>文件描述符,下面就看一下这个文件描述符的值是什么:
因此这也就解释了为什么文件描述符默认是从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,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
直接看代码:
#include
#include
#include
#include
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
#include
#include
#include
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的分配规则。
对于上面的例子,我们关闭了文件描述符0和2对应的文件吗,那么如果关闭1呢?
#include
#include
#include
#include
#include
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);
再看看:
即当所有现象都符合我们的预期时,这种现象就是重定向。
struct file*
的地址。常见的重定向有:>(输入), >>(追加), <(输出)。
在上面演示的无论是分配规则还是重定向,直接以close关闭的操作非常的挫,因为这样的close操作不够灵活,所以现在介绍一个系统调用的重定向接口:dup2
int dup2(int oldfd, int newfd);//newfd的内容最终会被oldfd指向的内容覆盖
那就来演示一下刚才的功能:打印到文件里
可以发现,这样操作简化了刚才的操作,另外,fd的值也不会被改变。
输出重定向演示完了,那我们就可以实现我们刚才提到的三个重定向剩下的追加、输入重定向了。
1. 追加重定向
2. 输入重定向
上面是从键盘中读取,如果不想从键盘读,我们可以重定向到向指定文件中读取:
在之前的学习中,我们模拟过shell部分功能的实现,在这里为了理解这三个常见的重定向,用shell模拟实现这三个重定向:代码链接:lesson18/myshell/myshell.c · 每天都要进步呀/Linux - 码云 - 开源中国 (gitee.com)
此外,这几个常见的重定向的使用方法:文章链接
一张图描述:
即我们利用虚拟文件系统就可以摒弃掉底层设备之间的差别,统一使用文件接口的方式进行文件操作。
文件的引用计数:(1条消息) Linux文件引用计数的逻辑_sherlock-wang的博客-CSDN博客