文章目录
- 一、重定向
- 1.文件描述符对应的分配规则
- 2.重定向的接口
- 二、再次实现myshell
- 1.实现细节
- 2.盘点文件与进程替换的一个细节
- 3.代码
- 三、1号文件和2号文件的区别
- 四、如何理解“一切皆文件?”
一、重定向
1.文件描述符对应的分配规则
我们先看如下代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#define filename "log.txt"
int main()
{
int fd = open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd : %d\n",fd);
const char* msg = "hello linux\n";
int cnt = 5;
while(cnt)
{
write(fd,msg,strlen(msg));
cnt--;
}
close(fd);
return 0;
}
运行结果为,一切都符合我们的预期
紧接着,我们将代码改为如下
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#define filename "log.txt"
int main()
{
close(0);
int fd = open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd : %d\n",fd);
const char* msg = "hello linux\n";
int cnt = 5;
while(cnt)
{
write(fd,msg,strlen(msg));
cnt--;
}
close(fd);
return 0;
}
运行结果为
如果我们将代码改为如下,即关闭一号文件
那么运行结果为,将没有任何东西可以打印出来
这是因为1号文件对应的是stout输出流,而printf里面是用到了这个流的。当我们关闭了以后,自然就出现问题了
如果我们关闭的是二号文件
那么结果为
我们可以发现如下现象
当我们关0的时候,为这个新文件分配的文件描述符是0
当我们关1的时候,为这个新文件分配的文件描述符是1
当我们关2的时候,为这个新文件分配的文件描述符是2
这就说明,文件描述符的分配规则很简单,从0下标开始,寻找最小的没有使用的数组位置,它的下标就是新文件的文件描述符
我们来看下面的代码
最终运行结果为
这是因为我们的东西并没有写到这个文件中,而是写入到了显示器文件中。所以才会打印出来。
我们再来看以下代码
运行结果为
这是因为,我们关闭了一号文件,而由于我们又打开了一个新文件,那么最终这个新文件的文件描述符变为了1。所以最终变为了向该文件写入
而这里,我们会发现,本来应该写入到显示器上的内容写入到了文件中,这不就是输出重定向吗?
如下图所示,是我们一开始的状态
后来我们关闭了1号文件,然后打开了一个新的文件。就会将原来的引用计数减减,然后将该指针置空。随后我们创建新文件的时候,会让1号下标的位置指向log.txt这个文件中
而在我们前面的代码中,上层并不知道我们已经将1号文件给改掉了。它只知道要向一号文件写,所以最终变为了向log.txt文件中去写
而上面所说的就是重定向的原理。
所以重定向的本质就是对文件描述符表里面的数组的内容进行修改
2.重定向的接口
我们会发现上面的方法其实有点麻烦,因为我们还需要关闭文件之后,才去打开一个新的文件。
所以操作系统本身就提供了系统调用
int dup2(int oldfd, int newfd);
它的作用是直接将新的文件描述符表数组中的oldfd下标的内容直接拷贝到newfd处。即newfd是要被oldfd所覆盖的
如下图所示,fd代表的是oldfd,1代表的是newfd。
fd的内容最终被拷贝到1号的内容当中。最终保留的就是fd的内容
所以我们就可以写出这个代码了
运行结果为
这样就同样实现了重定向的效果
如果我们将打开的方式换为了O_APPEND
我们会发现这个其实就是追加重定向
我们再来看下面的这段代码
注意这个接口的意思是:从fd中读取count字节个数据到buf中,count是期望读取的数量,返回值是实际读取的数量。
注意它的读取之后,最终不会加上’\0’字符,而fread是C语言的接口,它会自己加上的。所以我们最终需要自己加上这个’\0’字符。
ssize_t read(int fd, void *buf, size_t count);
代码为
运行结果为
然后我们让log.txt的内容如下
代码如下
运行结果为
我们会发现直接读取了,因为我们当前的文件内本身就有内容,所以就默认从文件中读取了
我们会发现它就相当于输入重定向
所以重定向的本质就是对进程的指定文件描述符表中内容拷贝的问题
如果我们的代码是这样子的
运行结果为,符合我们的预期
如果我们将代码改为这样的
那么结果为
他是符合我们的预期的
所以C语言的printf,和fprintf都是往1号文件里写的,不过我们已经提前改了一号文件了。所以就会显示如上的结果
如果我们改为O_APPEND
那么结果也是一样的,符合我们的预期
二、再次实现myshell
1.实现细节
我们知道,像我们平时在命令行中写的重定向是这样的
那么它与我们前面所演示的重定向有什么关系呢?
我们知道,我们前面的代码中,myshell并没有实现重定向功能
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char cline[],char* _argv[])
{
if(strcmp(cline,"") == 0) return 0;
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int BuildCommand(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?") == 0)
{
printf("%d\n",last_code);
last_code = 0;
}
else if(*_argv[1] == '$')
{
char* val = getenv(_argv[1] + 1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(_argc > 0 && strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
int n = BuildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
那么我们现在可以为他添加上重定向功能,要解决重定向,我们可以交互函数函数中进行处理一下字符串,当该指令进行执行的时候,处理即可。
如下代码所示
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
char* rdirfilename = NULL;
int rdir = NONE;
const char* getusername()
{
return getenv("USER");
}
const char* Gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void check_rdir(char* cmd)
{
char* pos = cmd;
while(*pos!='\0')
{
if(*pos == '>')
{
if(*(pos + 1) == '>')
{
*pos++ = '\0';
*pos++ = '\0';
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=APPEND_RDIR;
break;
}
else
{
*pos = '\0';
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir = OUT_RDIR;
break;
}
}
else if(*pos == '<')
{
*pos = '\0'; //ls -a -l < file.txt
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir = IN_RDIR;
break;
}
else
{
// do nothing
}
pos++;
}
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),Gethostname(),pwd);
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
check_rdir(cline);
}
int splitstring(char cline[],char* _argv[])
{
if(strcmp(cline,"") == 0) return 0;
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
int fd = 0;
if(rdir == IN_RDIR)
{
fd = open(rdirfilename,O_RDONLY);
dup2(fd,0);
}
else if(rdir == OUT_RDIR)
{
fd = open(rdirfilename,O_WRONLY|O_CREAT|O_TRUNC,0666);
dup2(fd,1);
}
else if(rdir == APPEND_RDIR)
{
fd = open(rdirfilename,O_WRONLY|O_CREAT|O_APPEND,0666);
dup2(fd,1);
}
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int BuildCommand(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?") == 0)
{
printf("%d\n",last_code);
last_code = 0;
}
else if(*_argv[1] == '$')
{
char* val = getenv(_argv[1] + 1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(_argc > 0 && strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
rdirfilename = NULL;
rdir = NONE;
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
int n = BuildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
在上面的代码中,当我们输入完指令字符串以后,然后去寻找是否存在重定向的符号,如果有,改变当前状态为输入/输出/追加重定向,然后将重定向的文件名给记录下来。最终达到分开指令与文件的目的。
分开以后,当我们进行指令的执行的时候,我们暂时只考虑子进程,如果是存在重定向的话,那么就打开对应的文件,然后将该文件的文件描述符放到对应的输入或输出位置上。就可以了。
最终的结果为如下
2.盘点文件与进程替换的一个细节
我们在前面的代码中
在后面我们做了重定向的工作,后面我们在进行程序替换的时候,难道不会影响吗???
在我们之前,我们已经了解了如下的东西
当一个可执行程序加载到内存的时候,会创建出对应的PCB结构体,在tash_struct这个结构体里面,有一个指针,会指向进程地址空间,然后进程地址空间根据页表找到实际的物理内存。
然后CPU就会去找到这个进程,从而进行去调度
而现在,我们知道当我们打开一个文件的时候,会创建出它的结构体struct file
随后为了管理起来,task_struct中有一个指针,指向文件描述符表,在这个表中的下标对应着每一个文件。
其中左侧的这一堆,我们都把他叫做,内核数据结构
当我们再度创建一个文件的时候
而我们上面进程替换替换是右边的部分,并不会对左边的部分有影响
这里就是内存管理与文件管理的解耦
进程历史打开的文件与进行的各种重定向关系都和未来进行程序替换无关
程序替换,并不影响文件访问
3.代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
char* rdirfilename = NULL;
int rdir = NONE;
const char* getusername()
{
return getenv("USER");
}
const char* Gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void check_rdir(char* cmd)
{
char* pos = cmd;
while(*pos!='\0')
{
if(*pos == '>')
{
if(*(pos + 1) == '>')
{
*pos++ = '\0';
*pos++ = '\0';
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir=APPEND_RDIR;
break;
}
else
{
*pos = '\0';
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir = OUT_RDIR;
break;
}
}
else if(*pos == '<')
{
*pos = '\0'; //ls -a -l < file.txt
pos++;
while(isspace(*pos)) pos++;
rdirfilename = pos;
rdir = IN_RDIR;
break;
}
else
{
// do nothing
}
pos++;
}
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),Gethostname(),pwd);
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
check_rdir(cline);
}
int splitstring(char cline[],char* _argv[])
{
if(strcmp(cline,"") == 0) return 0;
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
int fd = 0;
if(rdir == IN_RDIR)
{
fd = open(rdirfilename,O_RDONLY);
dup2(fd,0);
}
else if(rdir == OUT_RDIR)
{
fd = open(rdirfilename,O_WRONLY|O_CREAT|O_TRUNC,0666);
dup2(fd,1);
}
else if(rdir == APPEND_RDIR)
{
fd = open(rdirfilename,O_WRONLY|O_CREAT|O_APPEND,0666);
dup2(fd,1);
}
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int BuildCommand(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?") == 0)
{
printf("%d\n",last_code);
last_code = 0;
}
else if(*_argv[1] == '$')
{
char* val = getenv(_argv[1] + 1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(_argc > 0 && strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
rdirfilename = NULL;
rdir = NONE;
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
int n = BuildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
三、1号文件和2号文件的区别
当我们使用如下代码的时候
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#define filename "log.txt"
int main()
{
fprintf(stdout,"hello normal message\n");
fprintf(stdout,"hello normal message\n");
fprintf(stdout,"hello normal message\n");
fprintf(stdout,"hello normal message\n");
fprintf(stdout,"hello normal message\n");
fprintf(stderr,"hello error message\n");
fprintf(stderr,"hello error message\n");
fprintf(stderr,"hello error message\n");
fprintf(stderr,"hello error message\n");
fprintf(stderr,"hello error message\n");
return 0;
}
运行结果为,我们可以看到,这两个文件都是往显示器上打印的,似乎没有什么区别
但是如果我们这样做
我们会发现一部分重定向了,一部分没有重定向
在一开始的时候是这样的
随后发生了重定向,此时这个重定向仅仅是对于1号的文件的重定向
这样的话,凡是原来往1里面写的,就会写进这个normal.log文件了
往2里面写的就往显示屏上打印了
如果我们在命令行中的是这样的
其实上面是一个简写
下面是完整的,代表1重定向到normal.log,2重定向到err.log
./mytest 1>normal.log 2>err.log
这样的话就可以分开了
如果我们就想要重定向到一个文件中,那么可以这样做
./mytest 1>all.log 2>&1
同理,前面的这个1也是可以省略的
这个命令中
2>&1的意思是把1号文件描述符里面的内容写到2号文件描述符中。
这个的前提是已经把前半部分指令的操作做完了。而1已经指向这个这个all了。
所以最终1和2都指向这个文件了。所以就全部写入到一个文件中了
四、如何理解“一切皆文件?”
我们知道系统中有很多设备,如下图所示
这些外设,几乎都要去提供他们的读写方法,只不过对于键盘而言,写方法为空,对于显示器而言,读方法为空而已。但是他们都有读写方法
他们都可以用同一种结构体来表示。然后用类似的读写接口
对于这些硬件,我们都可以以文件的方式用open打开。然后创建对应的struct file内核数据结构。
然后由于我们要对这些设备进行读写
所以linux内核提供了另外一个方法表的数据结构
struct operation_func()
{
int (*wirtep)();
int (*readp)();
}
当我们想要访问某个设备的时候。会创建这个方法表的数据结构,然后提供一个指针指向这个方法表,方法表中的函数指针就指向对应的接口
如此一来,task_struct就会通过文件描述符表中的数组从而去调用对应的方法数据结构,最终达到调用read函数
这样就可以实现上层都是调用一样的函数了,而根据下层的不同动态的调用不同的方法了
所以所谓的一切皆文件,就是相当于在文件这一层封装了一个文件对象,让文件对象中的指针指向不同设备的函数方法,然后通过上层的一个方法数据结构,来对这些进行汇总。使得我们压根就不需要关心下层是如何的。只需要知道,以后要读取这个文件就调用这个方法即可
所以从文件对象这一层,往上就有了一切皆文件,这一层我们也叫做,VFS(虚拟文件系统)
而我们似乎就可以发现,这不就是C++中的多态吗?
上层都是基类,下层就是派生类。
所以面向对象就是历史的必然!