• [ Linux ] 重定向的再理解,以及文件系统的理解、inode和软硬链接


    在上篇文章我们了解了Linux中文件描述符和重定向以及缓冲区的理解,本篇文章我们要对了解一下重定向的再理解、文件系统以及引出inode的意义和软硬链接。

    目录

    0.重定向

    0.1标准输出 标准错误

    为什么perror2后面跟了一个success

    1.inode

    1.1 inode理解

    一个inode如何和属于自己的内容关联起来呢?

    文件名算文件的属性吗?

    那么当我们删除一个文件的时候,操作系统做了什么?

    我知道自己所处的目录,就能知道目录的inode吗?

    1.2创建一个新文件的步骤

    2.软硬链接

    2.1 软硬链接的区别

    2.2 软链接的用处

    2.3硬链接的用处

    那么硬链接有什么用呢?

    2.4软硬链接的删除


    0.重定向

    在之前我们实现过一个简易的shell,但是我们当时实现的myshell是不支持重定向的,当我们执行的时候是不认识重定向的。而 ls -a -l 是一个命令 , '>'右边是一个文件。因此当我们在获取用户输入的时候要考虑到这个问题。

    因此我们要对输入的字符串是否重定向要进行分析,这里我们以输出重定向为例。加入我们有一个用户输入的是

    ls -a -l>log.txt

    我们要将次命令转换为

    ls -a -l\0log.txt

    我们使用两个指针,一个指向前半部分的l,一个指向'\0'后面的log.txt,前半部分继续是下面的指令分析,而后半部分是打开文件以及做重定向相关的工作。

    因此首先我们需要判断一下这个指令是否有重定向工作,判断完成后要进行文件操作。以下使我们添加了重定向之后的myshell代码:

    1. #include <stdio.h>
    2. #include <string.h>
    3. #include <stdlib.h>
    4. #include <assert.h>
    5. #include <ctype.h>
    6. #include <sys/wait.h>
    7. #include <sys/stat.h>
    8. #include <fcntl.h>
    9. #include <unistd.h>
    10. #include <string.h>
    11. #include <sys/types.h>
    12. #define SEP " "
    13. #define SIZE 128
    14. #define NUM 1024
    15. #define DROP_SPACE(s) do { while(isspace(*s)) s++;}while(0)
    16. #define NONE_REDIR -1
    17. #define INPUT_REDIR 0
    18. #define OUTPUT_REDIR 1
    19. #define APPEND_REDIR 2
    20. int g_redir_flag = NONE_REDIR;
    21. char *g_redir_filename = NULL;
    22. char command_line[NUM];
    23. char* command_args[SIZE];
    24. char env_buffer[NUM];
    25. extern char** environ;
    26. int ChangeDir(const char* new_path)
    27. {
    28. chdir(new_path);
    29. return 0;//调用成功
    30. }
    31. void PutEnvInMyShell(char* new_env)
    32. {
    33. putenv(new_env);
    34. }
    35. void CheckDir(char* commands)
    36. {
    37. assert(commands);
    38. //只关心有没有 大于或者 小于符号
    39. char* start = commands;
    40. char* end = commands+strlen(commands);//end指向字符串的结束
    41. while(start < end)
    42. {
    43. if(*start == '>')
    44. {
    45. if(*(start+1) == '>')
    46. {
    47. //追加
    48. *start = '\0';
    49. start+=2;
    50. g_redir_flag = APPEND_REDIR;
    51. DROP_SPACE(start);
    52. g_redir_filename = start;
    53. break;
    54. }
    55. else
    56. {
    57. //ls -a -l > log.txt输出重定向
    58. *start = '\0';
    59. start++;
    60. DROP_SPACE(start);
    61. g_redir_flag = OUTPUT_REDIR;
    62. g_redir_filename = start;
    63. break;
    64. }
    65. }
    66. else if(*start == '<')
    67. {
    68. //输入重定向
    69. *start = '\0';
    70. start++;
    71. DROP_SPACE(start);
    72. g_redir_flag = INPUT_REDIR;
    73. g_redir_filename = start;
    74. break;
    75. }
    76. else
    77. {
    78. start++;
    79. }
    80. }
    81. }
    82. int main()
    83. {
    84. //一个shell 本质上就是一个死循环
    85. while(1)
    86. {
    87. g_redir_flag = NONE_REDIR;
    88. g_redir_filename = NULL;
    89. //不关心获取这些属性的接口
    90. //1.显示提示符
    91. printf("[张三@我的主机名 当前目录]# ");
    92. fflush(stdout);
    93. //2.获取用户输入
    94. memset(command_line,'\0',sizeof(command_line)*sizeof(char));
    95. fgets(command_line,NUM,stdin);//获取 输入 stdin
    96. command_line[strlen(command_line) - 1] = '\0';//清空\n
    97. //printf("%s\n",command_line);
    98. // "ls -a -l>log.txt" -> "ls -a -l\0log.txt"
    99. CheckDir(command_line);
    100. //3."ls -l -a -i" --> "ls","-l","-a","-i" 字符串切分
    101. command_args[0] = strtok(command_line, SEP);
    102. int index = 1;
    103. //给ls添加颜色
    104. if(strcmp(command_args[0]/*程序名*/,"ls") == 0)
    105. command_args[index++] = (char*)"--color=auto";
    106. //strtok 截取成功 返回字符串起始地址
    107. //截取失败 返回NULL
    108. while(command_args[index++] = strtok(NULL,SEP));
    109. // for debug
    110. //for(int i = 0;i<index;++i)
    111. //{
    112. // printf("%d:%s\n",i,command_args[i]);
    113. //}
    114. //4.TODO
    115. //如果直接exec*执行cd,最多只是让子进程进行路径切换,
    116. //子进程是一运行就完毕的进程!我们在shell中,更希望
    117. //父进程的路径切换
    118. //如果有些行为必须让父进程shell执行,不想让子进程
    119. //这种情况下不能创建子进程,只能让父进程自己实现对应的代码
    120. //这部分由父shell自己执行的命令称之为内建命令
    121. if(strcmp(command_args[0],"cd") == 0 && command_args[1] != NULL)
    122. {
    123. ChangeDir(command_args[1]);
    124. continue;
    125. }
    126. if(strcmp(command_args[0],"export") == 0 && command_args[1] != NULL)
    127. {
    128. strcpy(env_buffer,command_args[1]);
    129. PutEnvInMyShell(env_buffer);
    130. continue;
    131. }
    132. //5.创建子进程
    133. pid_t id = fork();
    134. if(id == 0)
    135. {
    136. int fd = -1;
    137. switch (g_redir_flag)
    138. {
    139. case NONE_REDIR:
    140. break;
    141. case INPUT_REDIR:
    142. fd = open(g_redir_filename,O_RDONLY);
    143. dup2(fd,0);
    144. break;
    145. case OUTPUT_REDIR:
    146. fd = open(g_redir_filename,O_WRONLY | O_CREAT | O_TRUNC);
    147. dup2(fd,1);
    148. break;
    149. case APPEND_REDIR:
    150. fd = open(g_redir_filename,O_WRONLY | O_CREAT | O_APPEND);
    151. dup2(fd,1);
    152. break;
    153. default:
    154. printf("BUG?\n");
    155. break;
    156. }
    157. //child
    158. //6.程序替换 会影响曾经子进程打开的文件吗? 不影响
    159. execvp(command_args[0],/*里面保存的就是执行的名字*/command_args);
    160. exit(1);//执行到这里一定失败了
    161. }
    162. int status = 0;
    163. pid_t ret = waitpid(id,&status,0);
    164. if(ret>0)
    165. {
    166. printf("等待子进程成功: sig:%d, code:%d\n",status&0x7F,(status>>8)&0xFF);
    167. }
    168. }
    169. return 0;
    170. }

    此时这段代码完成了重定向的添加。

    0.1标准输出 标准错误

    接下来我们看看这段代码

    1. #include <iostream>
    2. #include <cstdio>
    3. using namespace std;
    4. int main()
    5. {
    6. //stdout
    7. printf("hello printf1\n");
    8. fprintf(stdout,"hello fprintf1\n");
    9. fputs("hello fputs1\n",stdout);
    10. //stderr
    11. fprintf(stderr,"hello fprintf2\n");
    12. fputs("hello fputs2\n",stderr);
    13. perror("hello perror2");
    14. //cout
    15. cout<<"hello cout1"<<endl;
    16. //cerr
    17. cerr<<"hello cerr2"<<endl;
    18. return 0;
    19. }

    我们看看他的执行结果

    当我们重定向到文件时,我们惊奇的发现,后面带1的都不见了,我们再cat 一下该文件

    因此我们发现,虽然标准输出标准错误都是显示器文件,意思就是我们虽然都打印到了显示器内,但依旧是通过不同的文件描述符。因此当我重定向的时候,fd不同,当然是互不干扰的。

    那么这里发现,这样的意义是什么呢?为什么要这么干?

    这么干最好的意义是那些是程序日常哪些是程序错误。以便于当我们指向查看程序错误的时候便可以很方便的查看。---日志信息

    如果我们想把所有的输出信息打印在一个文件内,我们可以这样输出

    为什么perror2后面跟了一个success

    在上面的输出我们发现hello perror2后面跟了一个Success ,但是我们的代码里面却没有说明呀,这是什么东西呢?因此我们不得不了解一下了

    1. #include <iostream>
    2. #include <cstdio>
    3. #include <errno.h>
    4. #include <sys/stat.h>
    5. #include <sys/types.h>
    6. #include <fcntl.h>
    7. using namespace std;
    8. int main()
    9. {
    10. //
    11. int fd = open("log.txt",O_RDONLY);//这个方法必定失败的
    12. if(fd < 0 )
    13. {
    14. perror("open");
    15. return 1;
    16. }
    17. return 0;
    18. }

    那我们自己实现一个perror

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. using namespace std;
    9. void my_perror(const char* info)
    10. {
    11. fprintf(stderr,"%s: %s\n",info,strerror(errno));
    12. }
    13. int main()
    14. {
    15. //
    16. int fd = open("log.txt",O_RDONLY);//这个方法必定失败的
    17. if(fd < 0 )
    18. {
    19. //perror("open");
    20. my_perror("my open");
    21. return 1;
    22. }
    23. return 0;
    24. }

    1.inode

    1.1 inode理解

    为了能解释清楚inode我们先简单了解一下文件系统,下图为磁盘文件系统图,磁盘是典型的块设备,磁盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。下图中的BootBlock是启动块。

    1. Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相 同的结构组成。
    2. 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
    3. GDT,Group Descriptor Table:块组描述符,描述块组属性信息,有兴趣的同学可以在了解一下块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
    4. inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用
    5. i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
    6. 数据区:存放文件内容

    1. Data blocks:以块为单位,进行文件内容的保存。大小4KB(4nKB)
    2. inode table:以128字节为单位,进行inode属性的保存,主要用来保存文件的属性。inode属性里面有一个inode编号!具有唯一性,一般而言,一个文件一个inode号。

    1. Block Bitmap:记录那个数据块已经被占用,那个数据块没有被占用。0表示没有被占用1表示已经被占用。因此我们使用比特位的内容表示是否被占用。
    2. inode Bitmap:由于inode是不可以重复的,并且inode Bitmap表示的是inode的使用情况。那么有多少inode,起始的inode编号,有多少个inode被使用,有多少block被使用,还剩多少,你的总group大小是多少。因此我们用一个Grop Descriptor Table(GDT)块组描述符来保存块组属性信息。
    3. Super Block(SB):存放文件系统本身的结构信息,是文件系统的顶级结构。

    一个inode如何和属于自己的内容关联起来呢?

    在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吗?

    答案肯定是不能的,当我们知道自己的目录名是,我们想要知道该目录的inode时我们必须要到父目录去查找对应关系,因此在该目录下是不可以的。

    1.2创建一个新文件的步骤

    至此,当我们创建一个新文件时,操作系统会做一下步骤:

    1. 存储属性:内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
    2. 存储数据:该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
    3. 记录分配情况 :文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
    4. 添加文件名到目录

    新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文

    件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。

    2.软硬链接

    在我们之前所提到过的当我们查询该目录下的文件时,一个文件或者目录会有很多属性,那么下面这个1/2这一列表示的是什么呢?-- 这一列答案是硬链接数

    当我们想读到一个文件更多的属性时,我们可以使用stat 文件名

    那么我们如何创建一个文件的软硬链接呢?

    我们可以使用下面的命令

    ln -s 文件名 链接文件名 (创建软连接) 
    ln 文件名 链接文件名 (创建硬链接)
    

    2.1 软硬链接的区别

    通过我们查找一下能够看到什么呢?

    • 软链接是一个独立文件,有自己的inode和inode编号
    • 硬链接不是一个独立文件,他和目标文件使用同一个inode

    2.2 软链接的用处

    那么软连接有什么用呢? 软链接就相当于Linux下的快捷方式!

    加入我们在dir4目录下写了一个C语言程序,我们会到11-11目录下想执行这个程序时,我们必须带上路径,因此我们可以在11-11目录下创建一个该C语言程序的软连接,我们便可以在11-11路径下直接运行该软连接了。因此,Linux下软链接就和快捷方式一样。

    那么软连接的文件内容是什么?保存的是指向文件的所在路径!

    2.3硬链接的用处

    那么硬链接是什么东西呢,我们刚知道硬链接使用的inode还链接的是原来文件的inode,因此硬链接文件就是单纯的在Linux指定的目录下给指定文件新增文件名和inode编号的映射关系!

    当我们给一个文件创建一个硬连接时,我们发现这个数字变化了,所以至少可以证明,这个数字是改文件的赢连接数。那么什么是硬链接数?其实这里inode编号不就是一个“指针”的概念吗?因此硬连接的本质就是该文件inode属性中的一个计数器count,简而言之就是有几个文件名指向我的inode.

    那么硬链接有什么用呢?

    我们可以看到一个现象,为什么创建普通文件默认硬链接数是1,目录是2

    因为普通的文件名本身就和自己的inode具有映射关系而且只有一个。任何一个目录里面会存在 .和..的隐藏文件,因此一个目录的本身的文件名和自己有映射关系,文件内部的.文件也和改文件有映射关系。因此默认一个目录的硬链接数是2。而..文件是上级目录的inode.

    2.4软硬链接的删除

    我们使用unlink 文件名 可以直接删除链接文件

    (本篇完)

  • 相关阅读:
    HUAWEI(26)——防火墙双机热备
    VS Code如何给Python配置虚拟环境
    宜居行星最新报道
    【锁】悲观锁与乐观锁实现
    利用dockerfile升级flink的curl
    Qt优秀开源项目之九:qTox
    C语言典范编程
    VSCode配置C++环境:g++篇
    为什么有些年轻人大学毕业后那么坦然的在家待业?
    vsFTP简单安装测试
  • 原文地址:https://blog.csdn.net/qq_58325487/article/details/127820742