还记得我们之前调用 open()
接口的时候返回值叫做什么 文件描述符(file descripter
) 吗,虽然我们还没有学习它,但是因为我们每次去调用 write()
和 read()
甚至是 close()
的时候,都离不开这个文件描述符参数,所以它肯定和文件之间有着密切关联💥 💥 💥
下面看看这段代码,看看不同文件打开时候的文件描述符分别是什么:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd5 = open("log5.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
printf("fd5:%d\n", fd5);
return 0;
}
// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file
fd1:3
fd2:4
fd3:5
fd4:6
fd5:7
[liren@VM-8-2-centos cfile]$
大概可以看出来,fd
是按照顺序排列的,那么为什么第一个文件的 fd
不是 0
或者 1
呢,而是从 3
开始 ❓❓❓
还记得我们曾经学C语言的文件操作时候说过,每次我们打开一个 C
程序或者这么说,Linux
进程默认情况下会有 3
个缺省打开的文件描述符,默认会打开三个标准输入输出流,分别为 stdin
(键盘)、stdout
(显示器)、stderr
(显示器),并且它们按照以上顺序拥有 fd
,也就是说 stdin:0、stdout:1、stderr:2 。
所以我们创建这些新文件,都是从 fd
为 3
开始,那么我们也另外可以发现,fd
是 从 0
开始的,我们就可以推测其实这是一个数组下标!!!
#include <stdio.h>
int main()
{
printf("stdin->fd: %d\n", stdin->_fileno);
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);
return 0;
}
// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file
stdin->fd: 0
stdout->fd: 1
stderr->fd: 2
[liren@VM-8-2-centos cfile]$
下面就来介绍一下文件描述符!!!
简单地说,文件描述符 fd
的本质就是数组的下标,若一个文件被打开,那么肯定会有一个相对应的文件描述符字段给到该文件,用于标识该文件!
既然是数组的下标,那么是什么数组呢 ❓❓❓
其实在 linux
底层中是这样子的,每个进程的进程管理块 task_struct
中包含着一个 struct files_struct* files
指针指向 files_struct
这个结构体,这个结构体也称为 文件描述符表
,其中这个结构体中包含着一个指针数组 struct file* fd_array[]
,其中数组中每个类型都是 struct file*
类型也就是指向各自加载进内存中的文件属性和内容的指针。
不管是 stdin
、stdout
、stderr
还是 log.txt
等等,其本质就是 struct file
结构体, 方便操作系统管理!
而每次 linux
进程都会默认生成三个文件描述符分别指向 stdin
、stdout
、stderr
,下标分别为 0
、1
、2
,这也是为什么我们每次打开或者创建一个新文件的时候,fd
都是从 3
开始的原因!所以,只要拿着文件描述符,就可以找到对应的文件,找到文件后就可以对文件进行操作了!
下面就是这个大概的结构:
既然我们知道文件描述符的底层结构,那么我们就能知道其实它的分配规则很简单,就是 每次遍历一遍 fd_array[]
,从下标为 0
开始查找第一个还没有被分配的位置,进行分配!
注意:调用 close(1)
后只是将 fd_array[]
中 fd
为 1
处的 file*
置空,本质上 stdout
所对应的 fd
仍然为 1
,这个要注意!
接下来我们写代码测试一下,值得关注的是三个标准输入输出流的 fd
是就是其C语言文件结构体 FILE
中的 _fileno
!
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("stdin->fd: %d\n", stdin->_fileno);
printf("stdout->fd: %d\n", stdout->_fileno);
printf("stderr->fd: %d\n", stderr->_fileno);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
return 0;
}
// 调用结果:
stdin->fd: 0
stdout->fd: 1
stderr->fd: 2
fd1:3
fd2:4
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(0); // 关闭stdin
close(2); // 关闭stderr
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
return 0;
}
// 调用结果:
fd1:0
fd2:2
因为我们将 fd_array[]
中两个元素也就是 stdin
、stderr
关掉了,那么它们就变成 null
了,所以分配 fd1
和 fd2
的时候就会扫描空的位置进行分配!
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
// close(0); // 关闭stdin
// close(2); // 关闭stderr
close(1); // 关闭stdout
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
return 0;
}
// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file
[liren@VM-8-2-centos cfile]$
会发现结果并没有显示出来,其实是因为我们将标准输出流关闭了,而标准输出流是向屏幕打印的一个渠道,关闭了它自然我们就看不到结果,但是不代表我们就没有分配文件描述符给新文件哦!
那么这个时候我们就能发现如果我们把本来应该是向 stdout
也就是打印到屏幕上面的内容,我们经过关闭 close(1)
后,我们原本向屏幕打印的内容就变成了向某个占据给位置也就是 fd
为 1
的文件中写入内容,这就是 重定向!!!这个我们下面会详细讨论!
那么我们仔细想一下C语言中是不是存在一个叫做 FILE
的结构体,并且我们在使用C语言文件操作时候是这样子的: FILE* fp = ...
,可以发现并没有使用到文件描述符 fd
,但是我们在系统调用中的过程是这样子的: int fd = ...
,我们就能知道虽然说 FILE
指针帮我们在创建文件的时候不需要用到 fd
,但是底层还是存在 fd
的,因为 语言级别的调用是离不开系统级别的调用的!
所以我们就可以知道 FILE
结构体内一定有一个字段是文件描述符 fd
!对于 FILE
结构体来说,这只是语言级别的包装了系统中文件结构体的关系!
下面我们来看看 linux
中的 files_struct
内核源码:
对于 file_operations
,不同硬件是有不同的方法的,大部分情况方法是和你的硬件驱动匹配的,虽然如此,但是最终文件通过函数指针实现你要打开的是磁盘,那就让所有的方法指向磁盘的方法,你要打开的是其它硬件,那就让所有的方法指向其它硬件的方法,而这里底层的差异,在上层看来,已经被完全屏蔽了,也就是达到了低耦合的作用。
所以对进程来讲,对所有的文件进行操作,统一使用一套接口(现在我们明确了它是一组函数指针)。换言之,对进程来说,你的操作和属性接口统一使用 struct file
来描述,所以在进程看来,就是 “ 一切皆文件 ”。
还记得上面我们在测试代码时候 close(1)
后我们再怎么使用 printf()
函数都没有用了吗,这里我们就来解释一番:
其实是因为 printf
默认就是向显示器中打印内容数据,而当我们把 stdout
所对应的在 fd_array[]
中的位置置空了之后,我们这个进程就无法找到 stdout
这个文件了,这个时候自然就没办法向屏幕打印内容,接下来我们看下面的代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
close(1); // 关闭stdout
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd:%d\n", fd);
fprintf(stdout, "fd:%d\n", fd); // 使用fprintf打印看看效果
fflush(stdout); // 刷新一下缓冲区
close(fd);
return 0;
}
通过上述运行结果我们能看到信息没有打印在屏幕上面,反而是打印到了或者说是拷贝到了 log.txt
中,这其实不奇怪,因为我们 close(1)
之后,fd_array[1]
就空了,那么新文件 log.txt
自然就占据了这个位置,那么其实这个指向就改变了,但是 stdout
中的文件描述符还是 1
,这个没变,所以当我们调用 fprintf(stdout, ....)
的时候其实调用的就不是 stdout
这个文件了,而是 stdout
位置处的新文件,如下图所示:
这其实也就是重定向!具体一点叫做 输出重定向!
本质:上层使用的文件描述符 fd
不变(就像 stdout->_fileno
一样),在内核中更改 fd
所对应位置的 struct file*
的地址。
重定向分为以下三种:
cat
指令使用) 当然像上面那种关闭 stdout
的方法有点太粗糙了,显得比较挫,下面我们会给出系统级别的接口替我们完成重定向工作!
并且 不同的重定向功能其实本质就是打开文件方式的不同而产生的!
#include <unistd.h>
int dup2(int oldfd, int newfd);
// 功能:复制一个文件描述符,将oldfd拷贝给newfd(duplicate a file descriptor)
// 返回值:成功的话返回newfd(一般用于备份),失败的话返回-1,并且设置errno为对应错误信息
// 参数:
// 1、oldfd代表要进行重定向的目标文件描述符
// 2、newfd代表即将要被覆盖的文件描述符
🚗 注意:dup2()
它是将文件标识符中的内容进行交换,而不是交换文件标识符。
这个接口的参数填写的时候经常会搞错,下面举个例子来加以解释两个参数:
就以我们上面那串代码为例,我们要输出重定向到 log.txt
,所以我们要覆盖的就是 stdout
,也就是说这里的 newfd
就是要被覆盖的,那么就是 stdout
的文件描述符也就是 1
,而 log.txt
的 fd
就是 oldfd
,这个记得不要搞错!下面我们就用代码来实现一下上面的例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
// 调用dup2拷贝文件描述符
int newfd = dup2(fd, stdout->_fileno);
// 判断是否拷贝成功
if(newfd != -1)
{
// 使用fprintf打印看看效果
fprintf(stdout, "fd:%d\n", fd);
// const char* str = "lirendada";
// write(fd, str, strlen(str));
}
close(fd);
return 0;
}
// 调用结果:
[liren@VM-8-2-centos cfile]$ cat log.txt
fd:3
[liren@VM-8-2-centos cfile]$
如果想要实现 追加重定向,非常简单,只需要将文件的打开方式变成 O_APPEND 即可~
那如果要实现 输入重定向 怎么办呢,下面我们来实现一下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
dup2(fd, stdin->_fileno);
// 将本来要从键盘读取的过程转化为从log.txt中读取
char line[64];
while(1)
{
// 从stdin对应的文件描述符指向的文件读取
if((fgets(line, sizeof(line), stdin)) == NULL)
break;
printf("> %s", line);
}
close(fd);
return 0;
}
// 调用结果:
[liren@VM-8-2-centos cfile]$ ./file
> fd:3
[liren@VM-8-2-centos cfile]$
既然我们学习了重定向,那么我们可以试试看给之前我们写的简易版shell添加重定向的功能:
比如说我们输入的指令是这样子的:“ls -a -l -i > file.txt”
,那么我们就要判断一下指令中是否存在重定向的符号:>
、<
、>>
,然后各自执行相对于的重定向工作!
为了方便区分新增的功能,以前实现的内容的注释我就去掉了,新增的内容才有注释:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define NUM 1024
#define OPTION_NUM 64
char lineCommand[NUM];
char* myargv[OPTION_NUM];
int lastSig = 0;
int lastCode = 0;
#define NONE_REDIR 0 // 非重定向
#define INPUT_REDIR 1 // 输入重定向
#define OUTPUT_REDIR 2 // 输出重定向
#define APPEND_REDIR 3 // 追加重定向
int redirType = NONE_REDIR; // 重定向类型默认为非重定向
char* redirFile = NULL; // 重定向的文件,默认为空
// 检测是否重定向和分割函数
void commandCheck(char* commands)
{
assert(commands);
char* start = commands;
char* end = commands + strlen(commands);
while(start < end)
{
if(*start == '>')
{
// 比如"ls -a -l > file.txt"或者"ls -a -l >> file.txt"
*start = '\0';
start++;
if(*start == '>') // 判断是否为追加重定向
{
redirType = APPEND_REDIR;
start++;
}
else
{
redirType = OUTPUT_REDIR;
}
while(*start == ' ') // 跳过多余空格
start++;
// 填写重定向信息
redirFile = start;
break;
}
else if(*start == '<')
{
// 比如"cat < file.txt"
*start = '\0';
++start;
while(*start == ' ') // 跳过多余空格
start++;
// 填写重定向信息
redirType = INPUT_REDIR;
redirFile = start;
break;
}
else
{
start++;
}
}
}
int main()
{
// 因为shell是循环输入的,所以要套在死循环里面
while(1)
{
// 记得更新一下重定向状态
redirFile = NULL;
redirType = 0;
errno = 0;
printf("[%s@%s %s]~ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
fflush(stdout);
char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
assert(s != NULL);
lineCommand[strlen(lineCommand) - 1] = '\0';
(void)s;
// 比如"ls -a -l > file.txt"分割为"ls -a -l"和"file.txt"
// 只需要找到重定向符号,将它们置为'\0'即可!
commandCheck(lineCommand);
myargv[0] = strtok(lineCommand, " ");
int i = 1;
if(myargv[0] != NULL && strcmp("ls", myargv[0]) == 0)
myargv[i++] = (char*)"--color=auto";
while(myargv[i++] = strtok(NULL, " "))
{}
if(myargv[0] != NULL & strcmp("cd", myargv[0]) == 0)
{
if(myargv[1] != NULL)
chdir(myargv[1]);
continue;
}
if(myargv[0] && myargv[1] && strcmp(myargv[0], "echo") == 0)
{
if(strcmp(myargv[1], "$?") == 0)
printf("退出状态:%d,终止信号:%d\n", lastCode, lastSig);
else
printf("%s\n", myargv[1]);
continue;
}
#ifdef DEBUG
for(int i = 0; myargv[i]; ++i)
printf("myargv[%d]:%s\n", i, myargv[i]);
#endif
// 执行命令
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
// 因为命令是子进程执行的,真正重定向的工作一定是子进程来完成的
// 如何重定向,是父进程要给子进程提供信息的
// 又因为进程独立性,子进程重定向后其files_struct是不会影响父进程的
// 并且因为被打开文件是共享的,所以就能达到子进程重定向,父进程不受影响且看到效果的目的
if(redirType == NONE_REDIR)
{
// 什么都不做
}
else if(redirType == INPUT_REDIR)
{
int fd = open(redirFile, O_RDONLY);
if(fd != -1)
dup2(fd, stdin->_fileno);
else
{
perror("open");
exit(errno);
}
}
else if(redirType == OUTPUT_REDIR)
{
int fd = open(redirFile, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
perror("open");
exit(errno);
}
dup2(fd, stdout->_fileno);
}
else if(redirType == APPEND_REDIR)
{
int fd = open(redirFile, O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
perror("open");
exit(errno);
}
dup2(fd, stdout->_fileno);
}
execvp(myargv[0], myargv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
assert(ret > 0);
(void)ret;
lastCode = (status >> 8) & 0xFF;
lastSig = status & 0x7F;
}
return 0;
}
答案是肯定不会!程序替换是在磁盘与内存阶段,程序替换是将代码进行覆盖。重定向是在 PCB
与 files_struct
改变指向的阶段,它们是在内核数据结构中,并不会与文件系统产生影响,它们是相互独立!
所以 程序替换是不影响内核数据结构的,即不影响曾经进程打开的重定向文件!