还记得我们之前调用 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(file descripter)
一、什么是fd ❓
简单地说,文件描述符 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的分配规则
既然我们知道文件描述符的底层结构,那么我们就能知道其实它的分配规则很简单,就是每次遍历一遍 fd_array[],从下标为 0 开始查找第一个还没有被分配的位置,进行分配!
注意:调用 close(1) 后只是将 fd_array[] 中 fd 为 1 处的 file* 置空,本质上 stdout 所对应的 fd 仍然为 1,这个要注意!
接下来我们写代码测试一下,值得关注的是三个标准输入输出流的 fd 是就是其C语言文件结构体 FILE 中的 _fileno!
① 按照顺序分配 fd
#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② 关闭其中的 stdin 和 stderr 再分配 fd
#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 的时候就会扫描空的位置进行分配!
③ 关闭其中的 stdout 再分配 fd
#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 的文件中写入内容,这就是重定向!!!这个我们下面会详细讨论!
3、fd 和 FILE 的关系
那么我们仔细想一下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 的方法有点太粗糙了,显得比较挫,下面我们会给出系统级别的接口替我们完成重定向工作!
并且不同的重定向功能其实本质就是打开文件方式的不同而产生的!
三、重定向接口 dup2()
#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]$ 四、再谈myshell
既然我们学习了重定向,那么我们可以试试看给之前我们写的简易版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 改变指向的阶段,它们是在内核数据结构中,并不会与文件系统产生影响,它们是相互独立!
所以程序替换是不影响内核数据结构的,即不影响曾经进程打开的重定向文件!
重定向拓展
在命令的重定向中, > 表示重定向,0 表示标准输入,1 表示标准输出,2 表示标准错误
command &> file 表示将标准输出 stdout 和标准错误输出 stderr 重定向至指定的文件 file 中。
如果需要将脚本 demo.sh 的标准输出和标准错误输出重定向至文件 demo.log,比较典型的方式是:bash demo.sh 1>demo.log 2>&1。
- 先将标准输出重定向到demo.log文件,然后将标准错误重定向到标准输入(这时候的标准输入已经是指向文件了,所以也就是将标准错误重定向到文件)
![]()
答案选ABCD,解析如下:
A、command &> file 表示将标准输出stdout和标准错误输出stderr重定向至指定的文件file中。
B、与A选项功能雷同
C、比较典型的写法,将标准输出和标准错误都重定向到文件, >demo.log是一种把前边的标准输出1忽略的写法
D、比较直观的一种写法,不秀技,直观的将标准输入和标准错误分别重定向到文件
Ⅳ. ”Linux下一切皆文件“的解释
其实我们在讲文件描述符中也已经涉及到该解释了,就是打开的 struct file 中会有一个函数指针,会作对应的初始化,指向对应驱动层的读写方法,通过读写方法,完成对应设备的读写。
用户站在 struct file(文件的结构体对象)上层看来,所有的设备和文件,统一都是 struct file,至于 struct file 内部是如何使用函数指针来调用对应设备文件的读写方法,用户并不了解也并不关心。并且就算设备更换或者升级等,struct file 中的操作接口都是原来的模样,与硬件之间是非常低耦合的!
所以在用户的视角下,Linux一切皆文件。
"一切皆文件"是 Linux 中一个非常重要的概念,它指的是在 Linux 系统中,所有的设备、文件、目录、进程等等都被视为文件,都可以通过文件系统来访问和管理。
在 Linux 中,每个设备和资源都是被视为一个文件,包括硬盘、光驱、键盘、鼠标、打印机等等,甚至包括网络连接和进程等等。这些文件都被统一的组织在文件系统的树形结构中,通过文件路径来访问和管理。
这个概念的好处是使得 Linux 系统的管理和编程变得非常简单和一致。因为所有的资源都是以文件的形式存在,所以我们可以使用同样的方法来访问和管理这些资源,如读写、复制、移动、删除等等,这样就不需要专门的API或者命令来管理不同类型的资源。
此外,由于一切都是文件,所以 Linux 系统也非常灵活,我们可以使用相同的方法来处理不同类型的资源,这样可以大大简化系统管理和编程的复杂性,提高效率。
总之,"一切皆文件"是 Linux 系统的一个重要概念,它使得 Linux 系统的管理和编程变得简单、灵活和一致。