经过之前的linux命令操作、进程相关概念的学习,我们对于文件也并不陌生
首先需要明确以下概念:
以上概念视为对之前的总结, 对文件的操作其本质是进程对文件进行的操作!
如果一个文件没有被打开,可以直接被访问嘛?— 不能,一个文件要被访问就必须先要打开(怎么打开呢? --用户进程调用文件打开接口+OS实现文件打开功能)
所以,综上所述,文件操作的本质就是描述进程和被打开文件的关系
那么,未被打开的文件呢? – 属于文件系统的部分(后序博客中会介绍到)
在C语言的学习过程中,我们学习到大量的文件操作接口并使用,但我们只是了解其如何使用,并未了解操作系统底层是如何实现的
那么对于其他语言(C++/Java/Python/php…)存不存在文件接口呢?
–我们不得而知,但按照常理而言都应该提供文件操作的接口 但是其接口实现都不一样
文件存储在磁盘当中(磁盘属于硬件,要访问硬件就绕不开OS(软硬件资源管理器),必须调用操作系统提供的接口)
所以不管上层语言如何发生变化,库函数底层都必须调用系统调用接口
那么如何降低学习成本呢? – 学习不变的东西(操作系统底层如何实现文件操作)
[hx@hx file_operation]$ cat myfile.c
#include
#include
#define FILE_NAME "log.txt"
int main()
{
FILE *fp = fopen(FILE_NAME,"w");
if(NULL == fp)
{
perror("fopen");
return 1;
}
fclose(fp);
return 0;
}
在我们#define FILE_NAME "log.txt"时,并未指明文件的路径,那么文件会创建在哪里呢? – 当前路径下,进程在执行代码,生成可执行程序时是在当前路径下的,文件也就对应的生成在该路径下(若是更改当前路径那么文件对应的位置也就发生改变)
来回顾一下文件使用方式:
" r " — 以只读的方式打开文件,文件不存在报错
" w " — 以只写的方式打开文件,文件不存在创建文件
" a " — 在文件末尾追加数据,文件不存在创建文件
" r+ " — 读写都可以,文件不存在报错
" w+ " — 读写都可以,文件不存在创建文件
" a+ " — 读写都可以,在文件末尾进行读写,文件不存在创建文件
在当前centos7下echo会默认在末尾添加\n,在其他系统下不一定(看系统配置)
[hx@hx file_operation]$ umask
0002
– 因为是子进程在执行创建文件的命令,子进程umask被置为0,但并不影响父进程(shell)
一个int有多个比特位,操作系统采取比特位传递选项,下面来演示一下:
一个比特位对应一个选项 比特位的位置不能重复
之前提到文件操作的本质是进程和被打开文件之间的关系
struct file{}
这其中包含着文件的大部分属性每一个被打开的文件都有一个struct file ,struct file可以通过链式结构链接起来,只要找到起始地址,操作系统对打开文件的管理就变成对链表的增删查改
open函数的返回值是整数,-1表示文件打开失败,那为啥是从3开始,0,1,2这三个整数去哪了?
现在我们就知道C语言的库函数封装了系统调用接口,C语言的FILE类型的指针也封装了操作系统的文件描述符
(种种迹象表明库函数就是系统调用接口的封装)
我们理解了为啥是从数字开始的,那为啥是0,1,2,3…连续的数组下标呢?
当我们再打开一个文件时,数组从上往下查找没有被占用的描述符,就被分配给刚创建好的struct file对象
将struct file对象的地址填入对应的3号描述符当中,此时3号描述符就执行新打开的文件了
然后我们再将3号描述符通过系统调用给用户返回,用户就得到了数字3
所以,进程在访问文件时,需要传入3号描述符,进程根据传入的值找到进程描述符表对应的地址 文件找到即可对文件进行操作
操作系统是通过数组(文件描述符表)将进程和被打开的文件关联起来
总结:文件描述符的本质就是数组的下标
上图是说明进程与文件的关系
重定向的本质是:上层的fd不变,在内核中更改fd对应的struct file* 的地址,也就是说不是描述符之间的改变,而是改变描述符所对应的内容
上述这种输出重定向的方式是采取关闭stdout标准输出的方式,让1号描述符重新指向fd,如果我们同时将0,1,2都关闭是无法实现输出重定向的,也就是说这种方式是不满足需求的,操作系统提供了专门的重定向接口dup2()
12 #define NONE_REDIR 0
13 #define INPUT_REDIR 1
14 #define OUTPUT_REDIR 2
15 #define APPEND_REDIR 3
16
17 int RedirType = NONE_REDIR;
18 char *redirFile = NULL;
将输出/输入/追加重定向先定义出来,一开始将重定向类型置为0,重定向文件为无 后面对重定向类型进行判断
大致思路就是获取命令行,判断命令行当中是否存在重定向符号
25 void commandCheck(char* commands)
26 {
27 assert(commands);
28 char *start = commands;
29 char *end = commands+strlen(commands);
30
31 while(start<end)
32 {
33 // 追加/输出
34 if(*start == '>')
35 {
36
37 }
38 // 输入
39 else if(*start == '<')
40 {
41
42 }
43 else
44 {
45 start++;
46 }
47 }
48 }
38 // 输入
39 else if(*start == '<')
40 {
41 // " cat < file.txt"
42 *start = '\0';
43 start++;
44 trimSpace(start);
45 // 填写重定向信息
46 RedirType = INPUT_REDIR;
47 redirFile = start;
48 break;
49 }
如果当前start为<,说明是输入重定向,将当前start置为\0(将命令行分割成两字符串),将重定向信息改为输出重定向,文件名就是start所指向的位置
但是此时又面临一个问题,如何获取文件名的首字符,需要将文件名之前的空格都过滤掉
又因为不仅仅是在输入重定向这一种情况需要过滤空格,而且在追加和输出重定向中也需要过滤空格,所以我们可以将其以宏函数的形式编写
17 #define trimSpace(start) do{\
18 while(isspace(*start))\
19 ++start;\
20 }while(0)
这里的代码看上去很复杂,一步步分析
首先,'\'
表示的是续航符
为啥#define定义需要续航符?
因为宏替换是发生在程序的预处理阶段,进行直接替换,取到的是#define后面一行的内容,如果不存在\那么在替换时,不会将上下两行内容合并,所以需要加上续航符将函数当做一个整体进行宏替换
其次用到isspace判断空格++start找到文件名的开始位置,来获取文件名
先判断start是不是>
如果是再判断后一位(start++) 若start等于>,则是追加重定向,若*start不等于>,则是输出重定向
42 // 追加/输出
43 if(*start == '>')
44 {
45 *start = '\0';
46 start++;
47 if(*start == '>')
48 {
49 // "ls -a >> file.txt"
50 RedirType = APPEND_REDIR;
51 start++;
52 }
53 else
54 {
55 // "ls -a > file.txt"
56 redirType = OUTPUT_REDIR;
57 }
58 trimSpace(start);
59 redirFile = start;
60 break;
61 }
判断完重定向类型后需要将对应的重定向类型修改,同样的需要把空格过滤找到文件名对应位置
上面完成了对重定向类型的判断,那么到底如何执行重定向是需要进程来完成的
因为父进程是用来接收子进程退出信息,命令是由子进程进行完成的(shell的原则:王婆派遣实习生的案例)
所以真正的重定向工作是由子进程来完成的
144 // 子进程
145 if(id == 0)
146 {
147 switch(RedirType)
148 {
149 case NONE_REDIR:
150 break;
151 case INPUT_REDIR:
152 break;
153 case OUTPUT_REDIR:
154 break;
155 case APPEND_REDIR:
156 break;
157 default:
158 printf("bug?\n");
159 break;
160 }
149 case NONE_REDIR:
150 // 什么都不做
151 break;
152 case INPUT_REDIR:
153 {
154 int fd = open(redirFile,O_RDONLY);
155 if(fd<0)
156 {
157 perror("open");
158 exit(errno);
159 }
160 //重定向的文件成功打开
161 // dup2进行重定向
162 dup2(fd,0);
163 }
164 break;
调用dup2接口进行重定向,将fd文件指向0(标准输入/stdin)
165 case OUTPUT_REDIR:
166 case APPEND_REDIR:
167 {
168 //使得创建的文件按照自己的权限创建
169 umask(0);
170 int flags = O_WRONLY | O_CREAT;
171 if(RedirType == APPEND_REDIR) flags |= O_APPEND;
172 else flags |= O_TRUNC;
173
174 int fd = open(redirFile,flags,0666);
175 if(fd < 0)
176 {
177 perror("open");
178 exit(errno);
179 }
180 dup2(fd,1);
181 }
182 break;
183 default:
184 printf("bug?\n");
185 break;
186 }
追加重定向和输出重定向的差别就是打开文件的方式不一样,一个是以"a"的方式,一个是以"w"的方式
所以RedirType == APPEND_REDIR
flags就或等上O_APPEND
否则就或等上O_TRUNC
既然重定向文件的工作是由子进程来完成的,那么是不是就跟父进程无关呢?
当然不是,子进程是通过父进程创建(fork)出来的,子进程需要获取父进程对应的代码和数据,所以子进程也就从父进程当中获取到如何重定向文件的信息(到底是需要进行哪种重定向)
总而言之,父进程就是给子进程提供重定向信息的
子进程进行重定向会影响父进程嘛?
执行程序替换时,是否会影响到曾经进程打开的重定向文件呢?
不会,因为以上的结构(fork创建子进程)都是属于内核数据结构当中的模块,而程序替换是将磁盘上新的代码和数据替换到内存当中,完全是两个维度,最直观的表现就是程序替换不会影响内核数据结构当中的PCB(task_struct)
这层struct file是操作系统虚拟出来的文件对象(在Linux当中称之为vfs – 虚拟文件系统),摒弃掉底层设备的差别,统一使用文件接口的方式来进行文件操作
struct file当中包含方法指针,会进行初始化指向对应的方法,还会指向内核中的缓冲区。
假设我们在堆上申请了一块空间,有指针指向这块堆空间,当我们不需要再使用时就释放空间
在使用过程中,可能有多个指针指向这块堆空间,于是我们在区间加上count 用来计数(记录当前有多少指针指向我)
这个被称之为引用计数
当父进程fork创建子进程时,会将PCB和文件描述符表都给子进程创建一份,但是文件并未新建一份,所以此时子进程文件描述符表中的数据还是指向父进程所打开的文件,那么在进程在关闭文件时,是否就直接将文件关闭了?
不是,只是将文件所对应的引用计数–罢了,当文件的引用计数为0时,操作系统才会释放该打开文件
所以我们close文件时,只是告知操作系统我们不再使用该文件了·