在上篇文章我们了解了Linux中文件描述符和重定向以及缓冲区的理解,本篇文章我们要对了解一下重定向的再理解、文件系统以及引出inode的意义和软硬链接。
目录
在之前我们实现过一个简易的shell,但是我们当时实现的myshell是不支持重定向的,当我们执行的时候是不认识重定向的。而 ls -a -l 是一个命令 , '>'右边是一个文件。因此当我们在获取用户输入的时候要考虑到这个问题。
因此我们要对输入的字符串是否重定向要进行分析,这里我们以输出重定向为例。加入我们有一个用户输入的是
ls -a -l>log.txt
我们要将次命令转换为
ls -a -l\0log.txt
我们使用两个指针,一个指向前半部分的l,一个指向'\0'后面的log.txt,前半部分继续是下面的指令分析,而后半部分是打开文件以及做重定向相关的工作。
因此首先我们需要判断一下这个指令是否有重定向工作,判断完成后要进行文件操作。以下使我们添加了重定向之后的myshell代码:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <assert.h>
- #include <ctype.h>
- #include <sys/wait.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <string.h>
- #include <sys/types.h>
-
- #define SEP " "
- #define SIZE 128
- #define NUM 1024
-
- #define DROP_SPACE(s) do { while(isspace(*s)) s++;}while(0)
-
- #define NONE_REDIR -1
- #define INPUT_REDIR 0
- #define OUTPUT_REDIR 1
- #define APPEND_REDIR 2
-
- int g_redir_flag = NONE_REDIR;
- char *g_redir_filename = NULL;
- char command_line[NUM];
- char* command_args[SIZE];
- char env_buffer[NUM];
- extern char** environ;
-
- int ChangeDir(const char* new_path)
- {
- chdir(new_path);
- return 0;//调用成功
- }
-
- void PutEnvInMyShell(char* new_env)
- {
- putenv(new_env);
- }
-
- void CheckDir(char* commands)
- {
- assert(commands);
- //只关心有没有 大于或者 小于符号
- char* start = commands;
- char* end = commands+strlen(commands);//end指向字符串的结束
- while(start < end)
- {
- if(*start == '>')
- {
- if(*(start+1) == '>')
- {
- //追加
- *start = '\0';
- start+=2;
- g_redir_flag = APPEND_REDIR;
- DROP_SPACE(start);
- g_redir_filename = start;
- break;
- }
- else
- {
- //ls -a -l > log.txt输出重定向
- *start = '\0';
- start++;
- DROP_SPACE(start);
- g_redir_flag = OUTPUT_REDIR;
- g_redir_filename = start;
- break;
-
- }
- }
- else if(*start == '<')
- {
- //输入重定向
- *start = '\0';
- start++;
- DROP_SPACE(start);
- g_redir_flag = INPUT_REDIR;
- g_redir_filename = start;
- break;
- }
- else
- {
- start++;
- }
- }
- }
-
- int main()
- {
- //一个shell 本质上就是一个死循环
- while(1)
- {
- g_redir_flag = NONE_REDIR;
- g_redir_filename = NULL;
- //不关心获取这些属性的接口
- //1.显示提示符
- printf("[张三@我的主机名 当前目录]# ");
- fflush(stdout);
- //2.获取用户输入
- memset(command_line,'\0',sizeof(command_line)*sizeof(char));
- fgets(command_line,NUM,stdin);//获取 输入 stdin
- command_line[strlen(command_line) - 1] = '\0';//清空\n
- //printf("%s\n",command_line);
-
- // "ls -a -l>log.txt" -> "ls -a -l\0log.txt"
- CheckDir(command_line);
-
- //3."ls -l -a -i" --> "ls","-l","-a","-i" 字符串切分
- command_args[0] = strtok(command_line, SEP);
- int index = 1;
- //给ls添加颜色
- if(strcmp(command_args[0]/*程序名*/,"ls") == 0)
- command_args[index++] = (char*)"--color=auto";
- //strtok 截取成功 返回字符串起始地址
- //截取失败 返回NULL
- while(command_args[index++] = strtok(NULL,SEP));
- // for debug
- //for(int i = 0;i<index;++i)
- //{
- // printf("%d:%s\n",i,command_args[i]);
- //}
-
- //4.TODO
- //如果直接exec*执行cd,最多只是让子进程进行路径切换,
- //子进程是一运行就完毕的进程!我们在shell中,更希望
- //父进程的路径切换
- //如果有些行为必须让父进程shell执行,不想让子进程
- //这种情况下不能创建子进程,只能让父进程自己实现对应的代码
- //这部分由父shell自己执行的命令称之为内建命令
- if(strcmp(command_args[0],"cd") == 0 && command_args[1] != NULL)
- {
- ChangeDir(command_args[1]);
- continue;
- }
-
- if(strcmp(command_args[0],"export") == 0 && command_args[1] != NULL)
- {
- strcpy(env_buffer,command_args[1]);
- PutEnvInMyShell(env_buffer);
- continue;
- }
-
- //5.创建子进程
- pid_t id = fork();
- if(id == 0)
- {
- int fd = -1;
- switch (g_redir_flag)
- {
- case NONE_REDIR:
- break;
- case INPUT_REDIR:
- fd = open(g_redir_filename,O_RDONLY);
- dup2(fd,0);
- break;
- case OUTPUT_REDIR:
- fd = open(g_redir_filename,O_WRONLY | O_CREAT | O_TRUNC);
- dup2(fd,1);
- break;
- case APPEND_REDIR:
- fd = open(g_redir_filename,O_WRONLY | O_CREAT | O_APPEND);
- dup2(fd,1);
- break;
- default:
- printf("BUG?\n");
- break;
- }
- //child
- //6.程序替换 会影响曾经子进程打开的文件吗? 不影响
- execvp(command_args[0],/*里面保存的就是执行的名字*/command_args);
-
- exit(1);//执行到这里一定失败了
- }
- int status = 0;
- pid_t ret = waitpid(id,&status,0);
- if(ret>0)
- {
- printf("等待子进程成功: sig:%d, code:%d\n",status&0x7F,(status>>8)&0xFF);
- }
- }
-
- return 0;
- }
此时这段代码完成了重定向的添加。
接下来我们看看这段代码
- #include <iostream>
- #include <cstdio>
- using namespace std;
- int main()
- {
- //stdout
- printf("hello printf1\n");
- fprintf(stdout,"hello fprintf1\n");
- fputs("hello fputs1\n",stdout);
-
- //stderr
- fprintf(stderr,"hello fprintf2\n");
- fputs("hello fputs2\n",stderr);
- perror("hello perror2");
-
- //cout
- cout<<"hello cout1"<<endl;
-
- //cerr
- cerr<<"hello cerr2"<<endl;
-
- return 0;
- }
我们看看他的执行结果
当我们重定向到文件时,我们惊奇的发现,后面带1的都不见了,我们再cat 一下该文件
因此我们发现,虽然标准输出标准错误都是显示器文件,意思就是我们虽然都打印到了显示器内,但依旧是通过不同的文件描述符。因此当我重定向的时候,fd不同,当然是互不干扰的。
那么这里发现,这样的意义是什么呢?为什么要这么干?
这么干最好的意义是那些是程序日常哪些是程序错误。以便于当我们指向查看程序错误的时候便可以很方便的查看。---日志信息
如果我们想把所有的输出信息打印在一个文件内,我们可以这样输出
在上面的输出我们发现hello perror2后面跟了一个Success ,但是我们的代码里面却没有说明呀,这是什么东西呢?因此我们不得不了解一下了
- #include <iostream>
- #include <cstdio>
- #include <errno.h>
- #include <sys/stat.h>
- #include <sys/types.h>
- #include <fcntl.h>
- using namespace std;
- int main()
- {
- //
- int fd = open("log.txt",O_RDONLY);//这个方法必定失败的
- if(fd < 0 )
- {
- perror("open");
-
- return 1;
- }
- return 0;
- }
那我们自己实现一个perror
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- using namespace std;
-
- void my_perror(const char* info)
- {
- fprintf(stderr,"%s: %s\n",info,strerror(errno));
- }
-
- int main()
- {
- //
- int fd = open("log.txt",O_RDONLY);//这个方法必定失败的
- if(fd < 0 )
- {
- //perror("open");
- my_perror("my open");
- return 1;
- }
- return 0;
- }
为了能解释清楚inode我们先简单了解一下文件系统,下图为磁盘文件系统图,磁盘是典型的块设备,磁盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。下图中的BootBlock是启动块。
在inode Table内部包括问价的所有属性,其中会有一个blocks数组,其中直接保存的就是改文件对应的blocks编号!通过这个编号就可以直接找到自己的文件内容。
答案是当然算,但是inode里面,并不保存文件名,其实在Linux下,底层实际都是通过inode编号来标识文件的!要找到文件,必须找到文件的inode编号 !那么谁来帮我找inode编号呢?那么目录是文件吗??答案当然也是,因此目录一定也有自己的inode,那么目录的数据块放什么?之前我们提到过,进入一个目录需要执行权限。创建一个文件需要w权限,查看文件名需要r权限。那么为什么? 这是因为目录文件内存的是inode编号的映射关系!
因此Linux同一个目录下是不能创建同名文件的,因为文件名本身就是一个具有Key值的东西!
创建一个文件的时候,一定是在一个目录下!!我们拿到新建文件的inode,找到自己所处的目录,根据目录的inode找到目录的dataBlock,将文件名和inode编号的映射关系写入到目录的数据块中!
其实也是非常简单的,当我们删一个文件的时候,在inode位图下将1置为0就可以了。将标记改文件的内容和属性将1置为0即可。那么操作系统有没有真正的清除数据呢?答案是没有的,我们只是将1置为0。
答案肯定是不能的,当我们知道自己的目录名是,我们想要知道该目录的inode时我们必须要到父目录去查找对应关系,因此在该目录下是不可以的。
至此,当我们创建一个新文件时,操作系统会做一下步骤:
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文
件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
在我们之前所提到过的当我们查询该目录下的文件时,一个文件或者目录会有很多属性,那么下面这个1/2这一列表示的是什么呢?-- 这一列答案是硬链接数
当我们想读到一个文件更多的属性时,我们可以使用stat 文件名
那么我们如何创建一个文件的软硬链接呢?
我们可以使用下面的命令
ln -s 文件名 链接文件名 (创建软连接) ln 文件名 链接文件名 (创建硬链接)
通过我们查找一下能够看到什么呢?
那么软连接有什么用呢? 软链接就相当于Linux下的快捷方式!
加入我们在dir4目录下写了一个C语言程序,我们会到11-11目录下想执行这个程序时,我们必须带上路径,因此我们可以在11-11目录下创建一个该C语言程序的软连接,我们便可以在11-11路径下直接运行该软连接了。因此,Linux下软链接就和快捷方式一样。
那么软连接的文件内容是什么?保存的是指向文件的所在路径!
那么硬链接是什么东西呢,我们刚知道硬链接使用的inode还链接的是原来文件的inode,因此硬链接文件就是单纯的在Linux指定的目录下给指定文件新增文件名和inode编号的映射关系!
当我们给一个文件创建一个硬连接时,我们发现这个数字变化了,所以至少可以证明,这个数字是改文件的赢连接数。那么什么是硬链接数?其实这里inode编号不就是一个“指针”的概念吗?因此硬连接的本质就是该文件inode属性中的一个计数器count,简而言之就是有几个文件名指向我的inode.
我们可以看到一个现象,为什么创建普通文件默认硬链接数是1,目录是2
因为普通的文件名本身就和自己的inode具有映射关系而且只有一个。任何一个目录里面会存在 .和..的隐藏文件,因此一个目录的本身的文件名和自己有映射关系,文件内部的.文件也和改文件有映射关系。因此默认一个目录的硬链接数是2。而..文件是上级目录的inode.
我们使用unlink 文件名 可以直接删除链接文件
(本篇完)