详谈重定向

文件 fd 谈完访问文件的本质,进一步谈论与 fd 相关的重定向。


1. 文件描述符的分配规则

文件 fd 这篇文章里面我们说过,操作系统会默认打开键盘、显示器文件,进程也会默认填充 0 1 2 号文件描述符,即一输入、两输出。所以在不手动关闭 0 1 2 号文件描述符时,我们打开的文件,一定是从 3 开始的。那如果我关闭了呢??关闭一个,关闭两个,打开的文件,其文件描述符是怎么分配的。

close(0);
int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
printf("fd: %d\n", fd); 

在这里插入图片描述

close(2);

在这里插入图片描述

关闭 0 号文件描述符,那么我们打开的文件 fd 就是 0,关闭 2 号,fd = 2,说明文件描述符是从 0 号下标开始,寻找没有被使用的最小的数组下标,将打开的文件对象的地址以指针的形式存储,这个下标就是新文件的文件描述符。

2. 重定向的本质

2.1 基本原理

	close(1);
    int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);    
    printf("fd: %d\n", fd);      
    int cnt = 3;    
    while(cnt)    
    {    
        const char* s = "hello, linux\n";    
        write(1, s, strlen(s));    
        cnt--;    
    }    
    close(fd);   

在这里插入图片描述

根据 fd 的分配规则,我关闭了 1 号显示器文件,那么我打开的 log.txt 文件的 fd 就变为了 1 号,所以在后面我像 1 号 write 写入文件时,就不再是往显示器写入了。上层并不知道 fd = 1 的不是显示器了,它只认 fd,谁的 fd 是 1,就往谁可里面写,因此写入 log.txt 文件!这个就是重定向的原理!

重定向的本质,就是在对文件描述符索引处的指针做修改,使其指向新的文件对象的地址,这就是重定向!

而正常实现重定向,不会采用先 close,再 open 的方式(上述只是为了探讨 fd 的分配规则)。重定向也会有系统调用,那系统调用是如何实现的呢??

2.2 系统调用

// 重定向的系统调用
int dup2(int oldfd, int newfd);

DESCRIPTION
	dup2() makes newfd be the copy of oldfd, closing newfd first if neces‐sary, 
	but note the following:
		*  If  oldfd  is  not a valid file descriptor, then the call fails, and newfd is not closed.
        
        *  If oldfd is a valid file descriptor, and newfd has the same value as oldfd, 
           then dup2() does nothing, and returns newfd.

       	  
说明:使 newfd 成为 oldfd 的副本,也就是会将 oldfd 拷贝一份覆盖在 newfd 上。
但是这里的 newfd 和 oldfd 的代表对象有点反常逻辑。
以 2.1 代码为例,打开的文件是 log.txt,其 fd = 3,认为 fd=3 为 oldfd,fd=1 才是 newfd,
把 fd=3 里面的指针拷贝覆盖写入到 fd=1 处下标位置,必要时关闭 newfd(其fd=1)。
这样以来,1号文件描述符就不再指向显示器文件了,1号描述符就是指向的 log.txt。

需要注意的是,dup2() 这个系统调用做的拷贝,拷贝的并不是文件描述符,文件描述符只是一个进程内核中特定数组的一个下标!拷贝的是这个下标内存储的指针数据!

2.1 输出重定向

	int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);    
    printf("fd: %d\n", fd);      
    dup2(fd, 1);  // fd重定向到1号文件描述符
    int cnt = 3;    
    while(cnt)    
    {    
        const char* s = "hello, linux\n";    
        write(1, s, strlen(s));    
        cnt--;    
    }    

在这里插入图片描述

fd = 1 处内容本应该是一个指向显示器文件的指针,重定向后,指向了 log.txt,因此数据都往 fd = 1 指向的文件写入(即 log.txt),而 printf 底层肯定是原来的 fd = 1(即指向显示器文件),所以重定向后,1号不再是指向显示器文件,printf 的内容也自然就不会打印到显示器上。

上面是重定向,执行一次就会覆盖写入一次,如果想要追加重定向,只需要修改 open 的 flag 参数即可

	int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);    

2.2 输入重定向

    dup2(fd, 0);  // fd重定向到1号文件描述符                                                   
    
    char input[1024] = {0};    
    // read 返回值:实际读取的字符个数,sizeof(input) 是因为留一个位置给 C 规定的字符串结尾 \0
    ssize_t s = read(0, input, sizeof(input) - 1);    
    if(s > 0)    
    {    
        input[s] = '\0';    
        printf("echo: %s\n", input);    
    }   

在这里插入图片描述

fd = 0 处 read,本应该是从键盘读取,但是重定向了打开文件的文件描述符,因此 fd_array[ ] 0号下标存储的不再是指向键盘文件的指针,而是指向我们打开文件的对象的地址的指针。因此就等价于 cat < log.txt,这就是输入重定向!

  • 重定向之后,做进程替换工作,会影响重定向这件事吗??
    不影响。 进程地址空间(二) 就曾经说过,进程管理与内存管理是解耦的,诸如进程的PCB、进程地址空间、页表映射关系中的虚拟地址,与一个程序是如何被加载到内存中互不影响。所以同理!进程的 PCB 中的字段 struct file_struct* files 指向 fd_array[ ] 数组,内部元素指向打开文件对象的地址,重定向只是修改了其指向,而进程替换,是将新程序的代码和数据覆盖写入到原程序的代码和数据位置处,最多就是修改一下进程地址空间给该程序分配的内存大小,再修改一下页表的映射关系,与 进程的 PCB 中的字段 struct file_struct* files 指向 fd_array[ ] 数组 有什么关系呢??简言之,进程替换是内存管理与进程管理之间的事情!不涉及到进程管理与文件管理那部分,因此在这两件事上是解耦的,互不影响的。进程替换不影响文件访问,进程历史打开的各种文件与各种重定向关系也都和后续的程序替换无关!!

3. 标准输出 && 错误输出

    fprintf(stdout, "this is a normal message!\n");    
    fprintf(stderr, "this is a error message!\n");    

以 C 为例,stdout 和 stderr 对应的就是操作系统中文件描述符的 1 和 2,它们都是指向的显示器文件,这两条输出信息最终也会被写到显示器上,所以文件描述符 1 和 2 有什么区别呢??

在这里插入图片描述

不同点:当我将程序的运行结果重定向到文件中,stdout 输出信息正常重定向写入文件,而 stderr 错误输出则没有被写入到文件中。这是因为,> 重定向指令在底层默认只对标准输出做了重定向,即 dup2(fd, 1) (fd为目标文件的文件描述符)。

如果想要对错误输出也做重定向,则需要

 ./test > normal.txt 2> error.txt	# 这是简写,因为 > 默认对标准输出做重定向了
 ./test 1> normal.txt 2> error.txt	# 标准写法,指定 1 做重定向到 normal, 2 重定向到 error

在这里插入图片描述

如果想要标准输出和错误输出都重定向到一个文件,可以这样写

 ./test > all.txt 2>&1		# 简写
 ./test 1> all.txt 2>&1		# 标准写法
 # 其中的 2>&1 就与系统调用 dup2()的实现思路同理,讲文件描述符1拷贝覆盖到2中
 # 这样一来,1和2都是标准输出,做重定向时,就都指向的是同一个文件了。

在这里插入图片描述

4. 再谈 linux 下一切皆文件

Linux下,一切皆文件 之前我们就对 “linux下一切皆文件” 这个话题讲过一篇理解性的文章,那么这次,在介绍了完进程篇,加上访问文件的内容之后,我想再次带大家谈一下 “linux下一切皆文件” 是如何实现的。

访问各种硬件的本质,都是通过 open 打开一个文件来访问的,这些操作系统的管理理念(不让用户直接访问底层数据)。任何访问文件的操作,都需要先打开文件,而打开文件都是通过调用系统调用来实现的,程序运行起来最终都会变为进程。换言之,进程是操作系统帮助用户完成工作的主要渠道,对文件的操作也都需要依赖于进程。

一台计算机会有各种各样的硬件设备(外设),有磁盘,显示器,键盘,网卡等。而操作系统作为管理化软件,需要通过管理好软硬件资源来对上为用户提供良好稳定高效安全的运行环境。所以硬件也是要被管理的,管理就都是以 “先描述,后组织” 的理念。

怎么描述硬件,还是 struct 结构体(记录每个硬件的各种属性)。而各种硬件无非就是读写功能(向磁盘写入、读取,读取网卡等),虽然每个硬件的读写方法肯定是不同的,但是每个硬件都要提供类似的读写接口。

在linux下,一切皆文件,在操作系统的视角,底层的任何东西都是普通文件;我们打开的显示器、键盘、鼠标等各种硬件,都是由进程打开的(任何文件的访问都是以进程为载体的),所以各种硬件的访问,都可以通过文件的方式打开(例如 open)。

在打开各种硬件时,操作系统内核就会创建诸如 struct file 这样的文件结构体对象,其中就有一个字段 struct file_operations* f_op 这样的指针,指向一个 struct file_operations 结构体

struct file_operations
{
	int (*writep) {};		// 指向硬件的写方法地址	
	int (*readp) {};		// 指向硬件的读方法地址	
	......
}

而在进程层面上,就有 task_struct --> *files --> fd_array[ fd ] --> file --> *f_op --> writep,通过进程的 PCB 里面的files 字段,找到文件描述符表,通过下标索引找到指向的文件对象,在从中的 *f_op 指针找到读写的方法集合,通过函数指针所指向的不同的指向,访问不同的设备的读写方法!

在这里插入图片描述

这一理念的实现过程中,通过指针指向的不同设备,来实现对不同设备的访问,这不就是面向对象语言中的多态吗??
因此,市面上各种主流语言,都是面向对象的。为什么?? 因为这是历史趋势,操作系统在涉及一切皆文件这个理念时,那会只有 C 语言,所以这种多态的思想,只能靠函数指针来实现,而后续开发各种大型软件,软硬件分层时,都会发现有这么一个需求,所以为何不干脆开发出一套支持这种多态的思想的语言呢?所以面向对象是历史必然的!


如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值