【Linux初阶】基础IO - 文件管理(深入理解文件描述符) | 重定向


一、文件管理引入

我们在前面的文章中就曾提及过,进程操作的本质:进程与被打开文件的关系

那么问题来了,一个进程可以打开多个文件吗?

可以 -> 系统中一定会存在大量被打开的文件 -> 被打开的文件需不需要被 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);
}

在这里插入图片描述

通过观察上面的代码和对应的运行结果,我们发现了两个问题:

  1. fd获取的是文件操作符,那么为什么文件操作符是从3开始的呢?
  2. 为什么文件操作符是连续的小整数呢,这些连续的小整数是什么?

其实,这些连续的小整数就是数组的下标,至于为什么是数组下标,我们在后面结合其他知识再行讲解。


二、理解文件描述符

我们还是以 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的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值