文件描述符和Bash中的重定向
Tags: bash, redirection, descriptor
Published: 2024-05-29 07:21:51

## 前言
Linux中的文件描述符(File Descritor)和Bash中的重定向(Redirections)网上资料有很多描述，各种各样的都有，有的描述文档内容，有的很深入，但是很理论，本文将理论和实践结合，使用各种例证思考Bash的重定向操作实质。

## 文件描述符
### 底层关系结构
一切皆文件，文件描述符自然举足轻重，下面上一张经典图片

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xitpgpjc3as7da4fpyhc.png)

很清晰的描述了文件描述符底层运作。最左边一列为进程范围内的数据结构，每个进程都有一张文件描述符结构表(`struct fdtable`)，每一栏包含`close_on_exec`的flags和指向OFD(Open File Descriptor)的指针；第二栏(OFD)和第三栏(Inode Table)是系统级别的，其中第二栏包含打开的文件描述符的属性，状态和inode指针，第三栏包含文件的物理信息等。
1. 每个进程都有自己的进程表，`ls -l /dev/fd`或者`ls -l /proc/$$/fd`显示当前进程下打开的文件描述符，如下图所示，标准输入输出和错误都指向当前`$(tty)`
```
lrwx------ 1 username usergroup 64 10月 26 14:01 0 -> /dev/pts/0
lrwx------ 1 username usergroup 64 10月 26 14:01 1 -> /dev/pts/0
lrwx------ 1 username usergroup 64 10月 26 14:01 2 -> /dev/pts/0
lrwx------ 1 username usergroup 64 10月 26 14:01 255 -> /dev/pts/0
```
如果想过滤fd，除了使用上述命令扩展下外，还可以使用比如`lsof -P -n -p $$ -a -d 0,1,2,10`  
2. 查看打开的文件表描述符信息`cat /proc/$$/fdinfo/2`
```
pos:	0
flags:	02000002
mnt_id:	26
ino:	3
```

### 各种操作的影响
1. `dup`操作属于process内部fd操作，根据输入fd复制新建fd，如图所示比如会在第一列`ProcessA`中新生成一行，新旧的`file ptr`指向OFD同一栏，比如`fd1`和`fd20`都指向23，所有她们具有相同的文档`offset`等属性。
3. `fork`会生成子进程，子进程默认继承父进程的描述符表，比如`ProcessA`的`fd2`和`ProcessB`的`fd2`，同时指向73的OFD。
4. 不同进程中的`open`操作会生成各自的OFD行，但是会同时指向相同的inode信息，比如`ProcessA`中的`fd0`和`ProcessB`中的`fd3`，最后都指向了inode table中的1976，所以他们具有不同的file offset信息。

下面使用`dup`举例说明，打开一个文件，`dup`后，使用后者修改后，查看前者信息：
```c
// gcc -Wall -Wextra -pedantic -o exapmle example.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

void show(int fd1)
{
	int flags;
	long int offset = 0;
	flags = fcntl(fd1, F_GETFL);

	if (flags & O_APPEND)
	{
		fprintf(stdout, "%d has O_APPEND\n", fd1);
	}
	else
	{
		fprintf(stdout, "%d doesn't have O_APPEND attribute\n", fd1);
	}

	offset = lseek(fd1, 0, SEEK_CUR);
	if (offset == -1)
	{
		fprintf(stderr, "lseek failed: %s\n", strerror(errno));
	}
	fprintf(stdout, "file offset: %ld\n", offset);
	fprintf(stdout, "------------\n");
}

int main(int argc, char *argv[])
{
	int fd1, fd2;
	int flags;

	if (argc != 2)
	{
		fprintf(stderr, "Usage: %s file_path\n", argv[0]);
		exit(1);
	}

	fd1 = open(argv[1], O_RDWR);
	fd2 = dup(fd1);

	printf("fd1: %d, fd2: %d\n", fd1, fd2);

	show(fd1);

	flags = fcntl(fd2, F_GETFL);
	flags |= O_APPEND;
	fcntl(fd2, F_SETFL, flags);

	if (lseek(fd2, 3, SEEK_SET) == -1)
	{
		fprintf(stderr, "lseek set failed: %s\n", strerror(errno));
		exit(-1);
	}

	show(fd1);

	close(fd1);
	close(fd2);
	return 0;
}
```

## Bash中的重定向(Redirectons)
Bash中的重定向就是操作文件和描述符的关系，自己的理解，符号化文件操作，天才设计。一行简单的命令，文件描述符的处理在命令执行之前。
### 顺序问题
官方文档关于重定向有个[命令](https://www.gnu.org/software/bash/manual/bash.html#Redirections)
```bash
ls > dirlist 2>&1
ls 2>&1 > dirlist
```
上述两个命令重定向先后顺序重要性，后者是不能满足1，2定向到dirlist文件要求，为什么呢，讲的很清楚，正确的前者是先执行`> dirlist`，将output指向`dirlist`文件，然后将error指向output，而此时output指向了`dirlist`，所有1，2均指向了文件`dirlist`。前面说到，标准的1，2，3是软链接到`tty`的，错误的命令先将2指向1，就是说2指向了1的终极指向tty，后面再将1指向了dirlist，没有达到目的。下面咱们验证下理论：
```bash
ls -l /dev/fd/ > test1.txt 2>&1
ls -l /dev/fd/ 2>&1 > test2.txt
```
```
cat test1.txt
lrwx------ 1 username usergroup 64 10月 26 20:18 0 -> /dev/pts/0
l-wx------ 1 username usergroup 64 10月 26 20:18 1 -> /home/shouhua/test.txt
l-wx------ 1 username usergroup 64 10月 26 20:18 2 -> /home/shouhua/test.txt
lr-x------ 1 username usergroup 64 10月 26 20:18 3 -> /proc/7756/fd
```
```
cat test2.txt
lrwx------ 1 username usergroup 64 10月 26 20:18 0 -> /dev/pts/0
l-wx------ 1 username usergroup 64 10月 26 20:18 1 -> /home/shouhua/test.txt
lrwx------ 1 username usergroup 64 10月 26 20:18 2 -> /dev/pts/0
lr-x------ 1 username usergroup 64 10月 26 20:18 3 -> /proc/7759/fd
```
注意上述使用`/dev/fd/`而不是`/proc/$$/fd/`，前者是unix系统先出现的，后者算是部分系统支持，Bash操作的是前者，如果使用后者，则没有变化。  
### 文件offset影响
使用文件描述符处理文件需要注意file offset(pos)，比如下面例子','加到了offset=6的地方：
```bash
echo hello world > test.txt
exec 10<> test.txt
read -n 5 -u 10
echo $REPLY
cat /proc/$$/fdinfo/10 # 查看输出的pos参数
echo -n ',' >&10
cat test.txt
exec 10>&-
```
### redirection各种操作
`&`用于分割`>`和`<`与fd，不然就不知道解析`1>2`，为算数表达式？还是重定向？所以涉及到两端为数字时候，想到`&`，有个特殊的情况，`echo hello >& test.txt`，但是这种情况也可以这样 `echo hello &> test.txt`。默认情况下10以上的fd可能被系统占用。
```bash
# 基础文件操作
echo hello >test.txt
echo hello >>test.txt
echo hello &>test.txt
cat < test.txt
# here document
# here string
# duplicate file descriptor，注意下面的`&`，下面两个效果一样，不同的是当11没有写的时候，前者默认0，后者默认1
exec 11<&10
exec 11>&10
# move file descriptor
exec 11>&10- # 复制10到11， 并且关闭10
exec 11<&10-
# open file for reading and writing
exec 10<>test.txt
# 系统分配fd
exec {fd}<>test.txt
echo $fd
```

### 重定向对应文件描述符各种操作
1. `exec 11>&10`复制，相当于dup，复制后，具有相同的OFD(Open File Descriptor)项，因此具有相同的offset
2. 不同的terminal同时使用同一个fd打开相同的文件，类似第三种情况，不同的OFD项，但是指向相同的inode table项
3. 使用`ls -l /dev/fd/ > test.txt`时，类似上面第二种fork情况，ls会使用子进程执行，复制父进程fd table，所以能通过test.txt文件看到父进程中的fd信息