1.理解一下struct file内核对象
上一篇文章(文件详解)我们一直在谈,一个文件要被访问就必须要先被打开,打开之前就必须要先把文件加载到内存,同时呢我们的操作系统为了管理文件也会为我们的文件创建相对应的struct file对象,那么这个struct file对象里面应该有什么?
其实struct file里面最核心的两个:一个叫做内容,另一个叫做属性。
因为我们前面说了文件= 内容+属性。所以一个文件被打开之后最重要的要么是内容,要么是属性。
如果我们要读一个文件,那么读文件一定是由进程来读的,其中进程的PCB中包含了一个文件描述符表指针指向struct file * fd_array[]数组的一个指针,正常情况下标准输入,标准输出,标准错误三个流是默认被打开的,也就是对应着文件描述符0,1,2,所以新打开的文件一般是把新打开创建的struct file的地址填到文件描述符为3的空间里,然后将该文件描述符作为返回值传递给上层,上层拿到了文件描述符为3的文件就可以对该文件进行一系列操作了。如果我们要对文件进行读数据,首先要将文件加载至内存,如果要对文件进行写数据时我们不能在磁盘中对文件进行写入,而是要把文件加载到缓冲区(内存空间)才能进行写入操作。所以说无论读写都要先把文件加载到文件缓冲区中!
那么我们在应用层进行数据的读写本质是什么呢?其实本质是将内核缓冲区中的数据进行来回拷贝!
下面我们看一看Linux内核源代码中关于文件的结构体字段的描述
2.文件描述符fd的分配规则
下面我们先看一下以写的方式打开文件如果不存在就创建文件的方式进行代码测试:
运行后:
既然我们说默认文件描述符为0,1,2的文件流默认是打开的,那么我们直接使用文件描述符为0的文件对其进行操作即可验证这一说法,然后改写代码为:
运行之后:
既然我们可以对标准输入进行读,那么自然也可以从标准输出进行写操作,下面我们将代码修改:
运行后:
读了abcdefg,同时写入了abcdefg.
进程默认已经打开了0,1,2,我们可以直接使用0,1,2进行数据访问!
下面我们在进行一些测试:
我直接把文件描述符为0的标准输入给关了
运行之后:
发现我们以写的方式打开的新的文件的文件描述符不再是3了,而是变成了0,一个该现象并不足以说明问题,下面我们把2号标准错误流给改了,看是否打开的新文件的文件描述符会发生变化。
运行结果:
我们发现果然,文件描述符变成了2,这说明了什么问题呢?
当我们打开一个新的文件时,会从文件描述符表中从上往下扫描,寻找文件描述符最小且没有被使用的文件描述符分配为为新的文件的文件描述符。
那么下面我们把文件描述符为1的标准输出流给关了之后会发生什么现象呢?
运行之后:
我们发现什么都没有???原因就在于我们原本printf是要将内容打印到显示上的,但是我们在代码中已经把文件描述符为1的标准输出流给关闭了,所以没有打印出来。
如果我们再把代码改成这样呢?
然后运行之后,再查看一下log.txt文件中的内容:
怎么我们要打印的内容在log.txt文件里面去了?
其实很容易理解,根据我们上面的结论,文件描述符为1的标准输出流关闭了,所以log.txt打开时会被分配最小的没有被使用的文件描述符,也就是1,所以当前情况下log.txt的文件描述符为1,而这些内容会写到log.txt里面原因就在于这个过程叫做输出重定向
3.重定向
printf只认stdout,而stdout只认_fileno=1也就是文件描述符为1所指向的文件,如果文件描述符为1指向的是显示器那就往显示器上打,如果文件描述符为1指向的是log.txt文件,那就往log.txt上打,所以说重定向的本质,其实就是修改特定文件fd的下表中的内容(也就是打开文件的地址)。
下面我们再使用一个与printf比较相似的fprintf来测试:
先看一下fprintf的手册:
编写如下代码:
运行之后:
下面我们把打开方式改一下:
运行之后:
所以我们就很容易的实现了追加重定向,每次运行都是在log.txt后面进行追加。
下面我们再测试一个输入重定向:
查看一下fread的相关手册
修改代码为如下:
运行结果:
我们发现原本应该要从键盘上读取数据,最后是从log.txt的文件上进行读取的。
所以上面就说明了,上层fd不变,底层fd指向的内容在改变。
关于上述的重定向我们感觉太麻烦了,因为它还需要我们关闭相对应文件描述符指向的文件,有没有一种方法是直接把新打开的文件的地址拷贝到对应文件描述符的内容里面去完成重定向呢?也就是通过文件描述符级别的数组内容的拷贝!
答案是有的,一下就是那个接口:
这个接口的用法是将oldfd的内容拷贝到,newfd上,也就是最后两处的文件描述符都是指向oldfd所指向的内容,而这会导致两个文件描述符如何关闭的问题,其实这个问题是文件结构体中会存在一个引用计数,也就是有几个指针指向该文件,如果有多个指针就对计数那个字段进行数量控制,如果有一个指向该文件的文件描述符关闭了,那么该字段就减减,直到减到0才关闭。下面我们以输出重定向为例来测试一下dup2这个接口:
运行结果:
这就叫做重定向。
那么我们之前在命令行里面的重定向是怎么用的呢?
这就是我们在命令行中使用的重定向,那么命令行中的重定向与我们前面用c语言写的文件重定向之间有什么关系呢?
任何命令行在执行的时候都是bash的子进程,我们输入的命令行都叫做命令行字符串,所以我们要先做的是对命令行字符串进行解析,首先要识别的是,一旦发现有一个大于(>) ,两个大于(>>),还是一个小于(<) 这样的符号之后紧接着我们立马就能判定出来,这条指令是需要有重定向功能的,一旦识别出需要重定向我们就可以判断出来,前半部分是我们要执行的指令,后半部分是需要重定向到的目标文件,进程在进行程序替换之前会先进行重定向工作,所以未来要执行这条指令时就可以把原本打到显示器中的内容打到文件中去。
为了说明程序替换会不会影响重定向功能,下面我们修改一下代码来进行测试,以下是我们用c语言模拟实现的一个mybash的代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
//#define Debug 1
//redir
#define NoneRedir 0
#define OutputRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir = NoneRedir;
char *filename = NULL;
char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;
char *homepath()
{
char *home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return "none";
}
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char *getCwd()
{
const char *cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin); // 最终你还是会输入\n
if(r == NULL) return -1;
// "abcd\n" "\n"
command[strlen(command) - 1] = '\0'; // 有没有可能越界?不会
return strlen(command);
}
void commandSplit(char *in, char *out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while( out[argc++] = strtok(NULL, SEP));
#ifdef Debug
for(int i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
#endif
}
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
int fd = 0;
if(redir == InputRedir)
{
fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir == OutputRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir == AppendRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
//do nothing
}
// exec command
execvp(argv[0], argv); // cd ..
exit(1);
}
else // father
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0){
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
void cd(const char *path)
{
chdir(path);
char tmp[1024];
getcwd(tmp, sizeof(tmp));
sprintf(cwd, "PWD=%s", tmp); // bug
putenv(cwd);
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char *argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char *path = NULL;
if(argv[1] == NULL) path=homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval, argv[1]);
putenv(enval); // ???
return 1;
}
else if(strcmp(argv[0], "echo") == 0)
{
if(argv[1] == NULL){
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) > 1){
char *val = argv[1]+1; // $PATH $?
if(strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
const char *enval = getenv(val);
if(enval) printf("%s\n", enval);
else printf("\n");
}
return 1;
}
else {
printf("%s\n", argv[1]);
return 1;
}
}
else if(0){}
return 0;
}
#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)
void checkRedir(char usercommand[], int len)
{
// ls -a -l > log.txt
// ls -a -l >> log.txt
char *end = usercommand + len - 1;
char *start = usercommand;
while(end>start)
{
if(*end == '>')
{
if(*(end-1) == '>')
{
*(end-1) = '\0';
filename = end+1;
SkipSpace(filename);
redir = AppendRedir;
break;
}
else
{
*end = '\0';
filename = end+1;
SkipSpace(filename);
redir = OutputRedir;
break;
}
}
else if(*end == '<')
{
*end = '\0';
filename = end+1;
SkipSpace(filename); // 如果有空格,就跳过
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
int main()
{
while(1){
redir = NoneRedir;
filename = NULL;
char usercommand[NUM];
char *argv[SIZE];
// 1. 打印提示符&&获取用户命令字符串获取成功
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue;
// "ls -a -l > log.txt" -> 判断 -> "ls -a -l" redir_type "log.txt"
// 1.1 检测是否发生了重定向
checkRedir(usercommand, strlen(usercommand));
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command
n = doBuildin(argv);
if(n) continue;
// 4. 执行对应的命令
execute(argv);
}
}
其中实现我们平常用>,<,>>来进行重定向的重要代码是:
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
int fd = 0;
if(redir == InputRedir)
{
fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir == OutputRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir == AppendRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
//do nothing
}
// exec command
execvp(argv[0], argv); // cd ..
exit(1);
}
else // father
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0){
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)
void checkRedir(char usercommand[], int len)
{
// ls -a -l > log.txt
// ls -a -l >> log.txt
char *end = usercommand + len - 1;
char *start = usercommand;
while(end>start)
{
if(*end == '>')
{
if(*(end-1) == '>')
{
*(end-1) = '\0';
filename = end+1;
SkipSpace(filename);
redir = AppendRedir;
break;
}
else
{
*end = '\0';
filename = end+1;
SkipSpace(filename);
redir = OutputRedir;
break;
}
}
else if(*end == '<')
{
*end = '\0';
filename = end+1;
SkipSpace(filename); // 如果有空格,就跳过
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
运行之后:
我们发现我们模拟实现的一个bash命令行解释器实现了重定向功能。文章来源:https://www.toymoban.com/news/detail-792666.html
其实这些都不是很大的问题,问题比较大的是,程序替换会不会影响曾经的重定向呢?答案是不会。因为程序替换并没有创建新进程,而重定向只是在内核的层面上将文件描述符表数组对应的下标的内容进行了拷贝,而不会影响在内存中进行的程序替换,所以说这两者并不会互相影响,各自做各自的事文章来源地址https://www.toymoban.com/news/detail-792666.html
到了这里,关于Linux操作系统——重定向与缓冲区的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!