文章目录
- 一、文件管理引入
- 二、理解文件描述符
- 三、文件描述符表
- 四、文件描述符的分配规则
- 五、重定向
- 六、使用 dup2 系统调用实现重定向
- 1.模拟实现 >(输出)
- 2.模拟实现 >>(追加)
- 3.模拟实现 <(输入)
- 结语
一、文件管理引入
我们在前面的文章中就曾提及过,进程操作的本质:进程与被打开文件的关系
。
那么问题来了,一个进程可以打开多个文件吗?
可以 -> 系统中一定会存在大量被打开的文件 -> 被打开的文件需不需要被 OS管理起来呢?要的 -> 如何管理? -> 先描述,在组织 -> 操作系统为了管理对应的打开文件,必定要为文件创建对应的 内核数据结构标识文件 -> files_struct {} -> 包含了大量的文件属性
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define FILE_NAME(number) "log.txt"#number //宏中+#,使两个字符串具有链接特性
int main()
{
umask(0);//设置当前进程的umask值
int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd: %d\n", fd0);
printf("fd: %d\n", fd1);
printf("fd: %d\n", fd2);
printf("fd: %d\n", fd3);
printf("fd: %d\n", fd4);
close(fd0);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
}
通过观察上面的代码和对应的运行结果,我们发现了两个问题:
- fd获取的是文件操作符,那么为什么文件操作符是从3开始的呢?
- 为什么文件操作符是连续的小整数呢,这些连续的小整数是什么?
其实,这些连续的小整数就是数组的下标,至于为什么是数组下标,我们在后面结合其他知识再行讲解。
二、理解文件描述符
我们还是以 open的代码为例,我们知道,open接口调用成功后会返回文件描述符。实际上,文件描述符,就是一个整数
。
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入(stdin)0, 标准输出(stdout)1, 标准错误(stderr)2。0,1,2对应的物理设备一般是:键盘,显示器,显示器。也就是说,在操作系统层面,我们可以把键盘和显示器理解成为文件
。
这就回答了我们上面的第一个问题:fd获取的是文件操作符,那么为什么文件操作符是从3开始的呢?
因为前三个文件描述符被占用了。下面,我们对这个结论进行验证。
C语言的库函数接口中,我们可以看到下面这样的代码
FILE *fd = fopen();
我们知道,fopen底层必须调用系统接口,系统接口调用访问文件,又必须用文件描述符。那这个 FILE又是什么呢?事实上,FILE代表的是一个结构体,通过推导,我们不难得出,这个结构体中,必定有一个字段存储文件描述符。
stdin、stdout、stderr都是 FILE*结构体
对此,我们修改我们上面的代码,可以得到新的运行结果
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#define FILE_NAME(number) "log.txt"#number //宏中+#,使两个字符串具有链接特性
int main()
{
printf("stdin->fd: %d\n", stdin->_fileno);//输出结构体中的文件描述符
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);
umask(0);
int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd: %d\n", fd0);
printf("fd: %d\n", fd1);
printf("fd: %d\n", fd2);
printf("fd: %d\n", fd3);
printf("fd: %d\n", fd4);
close(fd0);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
}
【总结】
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入(stdin)0, 标准输出(stdout)1, 标准错误(stderr)2。因此,我们自己文件的文件操作符通常从 3开始。
三、文件描述符表
我们将在本小节解决开头的第2个问题,为什么文件操作符是连续的小整数,为什么我们可以将这些连续的小整数理解成数组的下标?
下面是进程与文件的关系图解
通过对图的理解,我们现在知道,文件描述符就是从0开始的小整数
。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件(file)。于是就有了 files_struct结构体,表示一个已经打开的文件对象的集合。
而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct(文件描述符表)
,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标
。所以,只要拿着文件描述符,就可以找到对应的文件。
四、文件描述符的分配规则
通过上面的学习,我们知道,我们的文件描述符默认0、1、2都被占用了,假如我们关掉一个会怎么样呢?直接看代码
#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
,可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
。
五、重定向
那如果关闭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);
fflush(stdout);
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>(输出), >>(追加), <(输入)
那重定向的本质是什么呢?
重定向的本质:上层用的 fd不变,在内核中更改 fd对应的 struct file*的地址
。
以上图为例,文件 1代表的是标准输出,但是该文件被我们 close掉了,假如此时我们新创建一个文件,我们的文件就会占用文件描述符 1。
所以,文件描述符 1没有变化,但是它底层struct file*的地址改变了,指向了一个新的文件。
六、使用 dup2 系统调用实现重定向
函数原型如下:
#include <unistd.h>
int dup2(int oldfd, int newfd);//将旧的拷贝到新的,最终我们的内容都和 old一样
【注意】
dup2不是将文件描述符互相拷贝,而是将文件描述符对应的 struct file*的地址做互相拷贝。
1.模拟实现 >(输出)
直接输出在文件中
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//close(0);
//close(2);
//close(1);
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
printf("open fd: %d\n", fd); // printf -> stdout
fprintf(stdout, "open fd: %d\n", fd); // printf -> stdout
fflush(stdout);
close(fd);
return 0;
}
———— 我是一条知识分割线 ————
2.模拟实现 >>(追加)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//close(0);
//close(2);
//close(1);
umask(0);
//int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); //追加
if (fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 1);
printf("open fd: %d\n", fd); // printf -> stdout
fprintf(stdout, "open fd: %d\n", fd); // printf -> stdout
const char *msg= "hello world";
write(1, msg, strlen(msg));//strlen - #include <string.h>
fflush(stdout);
close(fd);
return 0;
}
【注意】
代码中的 “hello world” 没有加换行
———— 我是一条知识分割线 ————
3.模拟实现 <(输入)
可以直接从文件中输入显示器,不用在键盘中输入
示例如下,我们先在 log.txt中写入信息,再运行
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//close(0);
//close(2);
//close(1);
umask(0);
//int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
//int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd = open("log.txt", O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd, 0); //输入重定向
char line[64];
while(1)
{
printf("> ");
if(fgets(line, sizeof(line), stdin) == NULL) break; //stdin->0
printf("%s", line);
}
close(fd);
return 0;
}
结语
🌹🌹 文件管理(深入理解文件描述符) 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪