前言
本文介绍了系统IO、fd、重定向等内容。
一、再谈文件
- 空文件也要占磁盘空间;
- 文件 = 内容 + 属性;
- 文件操作 = 对内容 or 对属性 or 内容和属性;
- 表示一个文件,必须使用:文件路径 + 文件名(有且·仅有一个);
- 如果我们没有指定对应文件的路径,默认是在当前路径下对该文件进行访问;
- 当我们把fopen,fclose,fread,fwrite等接口写完之后,代码编译形成二进制可执行程序后,在还没有运行的情况下,文件对应的操作有没有起作用?
答:没有,文件的访问是运行时进行访问的。因此,对文件的操作本质是,进程对文件的操作。 - 一个文件如果没有被打开,它可以直接被访问吗?
答:不能!!!一个文件要被访问,必须先被打开fopen —— 文件访问 是 用户进程 + OS。 ——> 文件操作的本质是 : 进程和被打开文件的关系。
二、再谈文件操作
- C语言有文件操作接口,C++有文件操作接口,Java有……这些语言都有文件操作接口,但是他们的操作接口都不一样。
文件在哪里? ——在磁盘 ——是硬件 ——要通过OS访问
所以,要访问磁盘不能绕过OS——要使用OS提供的接口——OS必定要提供文件级别的系统调用接口。注意:操作系统只有一个,在操作系统之上可以运行多种语言,而无论上层语言如何变化:
(1)库函数底层必须调用系统调用接口;
(2)库函数可以千变万化,但是底层不变。
那么,如何降低学习成本呢?
学习不变的东西(上层语言的库函数都是基于系统调用做的不同封装,系统调用就是不变的东西)。 - 操作C语言
C语言中传标记位——int的一个标记位。
但是int整数有32位比特位,因此我们可以通过比特位传递选项。那么如何使用比特位传递选项呢?——一个比特位一个选项,比特位不能重复。
系统调用write是用来往文件中写内容的,其中
那么是谁提供的文件读取的分类(文本类和二进制类)? 是语言本身。
库函数的接口是对系统调用进行了封装的结果。
二、如何理解文件
1.文件操作的本质
文件的本质是进程和被打开文件的关系。
2.管理被打开的文件
进程可以打开多个文件,意味着系统中一定会存在大量的被打开的文件。这些被打开文件也是需要被操作系统管理起来的,我们知道管理的本质是先描述再组织,所以操作系统为了管理对应的被打开文件,必定是要为这些文件创建对应的内核数据结构来标识文件。这个内核数据结构就是struct file{}
结构体(注意:这与C语言的FILE没有关系),包含文件的大部分属性。
三、进程和被打开的文件如何关联
通过文件打开(open)的返回值和文件描述符进行联系。
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<assert.h> 7 #include<string.h> 8 #define FILE_NAME(number) "log.txt"#number 9 int main() 10 { 11 umask(0); 12 int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 13 int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 14 int fd2 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 15 int fd3 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 16 int fd4 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 17 printf("fd: %d\n", fd0); 18 printf("fd: %d\n", fd1); 19 printf("fd: %d\n", fd2); 20 printf("fd: %d\n", fd3); 21 printf("fd: %d\n", fd4); 22 close(fd0); 23 close(fd1); 24 close(fd2); 25 close(fd3); 26 close(fd4); 27 return 0; 28 }
为什么文件描述符fd
是从3开始的?
四、文件描述符fd
1.引入
看到上面的结果,open的返回值为什么从3开始的,那么0,1,2到哪里去了呢?
我们可以发现文件描述符是连续的整数,说到连续的整数我们可以想到数组的下标就是数组的下标就是连续的整数。
C语言阶段,我们知道C程序会默认打开三个标准输入输出流:stdin
(标准输入设备键盘)、stout
(输出设备显示器)、stderr
(显示器)
对于C语言的FILE,我们其实不是很熟悉。C语言的FILE究竟是什么?它本质上是一个结构体。访问文件时,是用底层的open系统调用接口,它访问文件需要用到文件描述符,在C语言中,我们访问文件用的是FILE而不是文件描述符,因此可以推测出,FILE中必定有一个文件描述符的字段。所以C语言不仅在接口上存在封装,它的数据结构FILE也有封装。
当然上面都是我们的推测,我们可以实际看一下标准输入标准输出和标准错误它们的文件描述符具体是多少:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 #include<sys/stat.h> 5 #include<fcntl.h> 6 #include<assert.h> 7 #include<string.h> 8 #define FILE_NAME(number) "log.txt"#number 9 int main() 10 { 11 printf("stdin -> fd :%d\n", stdin -> _fileno); 12 printf("stdout -> fd :%d\n", stdout -> _fileno); 13 printf("stderr -> fd :%d\n", stderr -> _fileno); 14 umask(0); 15 int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 16 int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 17 int fd2 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 18 int fd3 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 19 int fd4 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666); 20 printf("fd: %d\n", fd0); 21 printf("fd: %d\n", fd1); 22 printf("fd: %d\n", fd2); 23 printf("fd: %d\n", fd3); 24 printf("fd: %d\n", fd4); 25 close(fd0); 26 close(fd1); 27 close(fd2); 28 close(fd3); 29 close(fd4); 30 return 0; 31 }
可以看到,0,1,2是默认被stdin,stdout,stderr占用,可以反推我们上面理论是正确的。
2.理解
为什么文件描述符是0,1,2,3……这些整数?它的本质是什么?
文件描述符的本质是数组的下标。
文件是存储在磁盘上,要操作某个文件就要打开该文件,即,将该文件的相关属性信息从磁盘加载到内存中。操作系统中存在着大量的进程,一个进程可以打开多个文件,因此操作系统要将被打开的文件管理起来,还要将每个进程与它打开的文件的关系管理起来。
如何管理?先描述,再组织。
OS为了管理被打开的文件,构造了struct file
结构体用来描述被打开的文件,为了管理进程与文件之间的联系,进程创建了struct file_struct
结构,里面包含了struct file* fd_array[]
指针数组,它存储了描述被打开文件的结构体对象的地址,将进程对应的struct file_struct对象存放在进程的PCB中。
这就是为什么文件操作符读到的是连续的整数,因为文件操作中用来标记进程与文件间的关系的就是文件描述符表,用数组标定文件内容。
3.分配规则
文件描述符本质上就是数组的下标。文件描述符是如何进行分配的呢?
要关闭fd对应的文件,需要调用close,即close(1)就是关闭fd = 1对应的文件。
先看下面这个例子:
我们知道了,系统启动后,默认会打开0,1,2这三个文件符对应的文件,如果我们将这三个中的其中一个关闭,然后去打开新的文件,这个新文件对应的fd会是多少呢?
文件test.c
1 #include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<unistd.h> 5 #include<fcntl.h> 6 int main() 7 { 8 close(1); 9 umask(0); 10 int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 11 if(fd < 0) 12 { 13 perror("open"); 14 return 1; 15 } 16 printf("open fd : %d\n", fd); 17 return 0; 18 }
运行:
可以看到,当我们将fd = 1对应的文件关闭,此时fd = 1就没有被使用,因此新打开的文件就占用了1。
为什么这里
.test
没有直接打印出结果呢?答:fd = 1对应的是标准输出,即我们将要打印的内容写到标准输出。此时,我们关闭了1,将fd = 1重新分配给文件
log.txt
,则要打印出来的内容就会写入文件log.txt
。(close
系统调用改变的是fd与文件的对应关系,而上层调用的fd = 1并不改变,即上层不知道fd = 1被改变了,所以还是将fd = 1认为成标准输出)就导致了./test
不能打印出结果,结果被写入到文件log.txt
中。
分配规则是:从小到大,按照顺序寻找最小的且没有被占用的fd。
如果我们没有关闭1,则新打开文件对应的fd就是3:
文件test.c
1 #include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<unistd.h> 5 #include<fcntl.h> 6 int main() 7 { 8 umask(0); 9 int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 10 if(fd < 0) 11 { 12 perror("open"); 13 return 1; 14 } 15 printf("open fd : %d\n", fd); 16 return 0; 17 }
运行:
五、重定向
1.引入重定向
上面的将fd=1对应的文件关闭,然后将fd = 1重新分配给新文件的例子,就是一个重定向的过程。
关于重定向,我们最先接触的是>输出;>>追加;<输入
重定向最经典的特征就是在上层调用不变的情况下,改变底层的数组内容的指向。比如:调用fwrite(stdout,…);无论底层的指向如何改变,上层都会用到stdin(标准输入),stdout(标准输出),stderr(标准错误)对应的文件描述符0,1,2.当我们将3号描述符对应的文件指针赋值给1号文件描述符时,1号文件描述符就指向了3号对应的文件。
重定向的本质就是,上层使用的fd不变,在内核中修改了fd对应的struct_file*的地址。
2.接口
dup2:
dup2
的作用是在两个文件描述符之间进行拷贝(拷贝的不是文件描述符本身,而是它们在文件描述符表中所对应的文件指针)
dup2
的参数中oldfd和newfd,dup2一旦重定向后,留下的只有oldfd(将oldfd所对应的文件指针,拷贝给newfd对应的数组内容)
输出重定向
例子:
将原来打印在显示器上的内容,写入文件log.txt
上
文件test.c
1 #include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<unistd.h> 5 #include<fcntl.h> 6 int main() 7 { 8 umask(0); 9 int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 10 if(fd < 0) 11 { 12 perror("open"); 13 return 1; 14 } 15 dup2(fd,1); 16 printf("open fd : %d\n", fd); 17 fprintf(stdout, "open fd : %d\n", fd); 18 return 0; 19 }
运行:
3.追加重定向
在打开文件的时候不要情况原来的内容,即将O_TRUNC改为O_APPEND即可。
文件test.c
1 #include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<unistd.h> 5 #include<fcntl.h> 6 int main() 7 { 8 umask(0); 9 //int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 10 int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 11 if(fd < 0) 12 { 13 perror("open"); 14 return 1; 15 } 16 dup2(fd,1); 17 printf("open fd : %d\n", fd); 18 fprintf(stdout, "open fd : %d\n", fd); 19 return 0; 20 }
运行:
4.输入重定向
前提条件是文件必须存在。stdin -> 0,dup(fd,0);//输入重定向文件
文件test.c
1 #include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/stat.h> 4 #include<unistd.h> 5 #include<fcntl.h> 6 int main() 7 { 8 umask(0); 9 //int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); 10 //int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); 11 int fd = open("log.txt", O_RDONLY); 12 if(fd < 0) 13 { 14 perror("open"); 15 return 1; 16 } 17 dup2(fd,0); 18 char line[64]; 19 while(1) 20 { 21 printf(">"); 22 if(fgets(line, sizeof(line), stdin) == NULL) break; 23 printf("%s", line); 24 } 25 close(fd); 26 return 0; 27 }
运行:
总结
以上就是今天要讲的内容,本文介绍了Linux中基础IO的相关概念。本文作者目前也是正在学习Linux的相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!