【Linux基础IO】

C文件接口回顾

在学习LInux的基础IO之前,我们先来回顾以下C语言的文件接口,C文件的接口有许多,比如 fopen fclose fprintf 等等,下面我们对C的文件接口做以汇总:

文件操作函数功能
fopen打开文件
fclose关闭文件
fputc写入一个字符
fgetc读取一个字符
fputs写入一个字符串
fgets读取一个字符串
fprintf格式化写入数据
fscanf格式化读取数据
fwrite向二进制文件写入数据
fread从二进制文件读取数据
fseek设置文件指针的位置
ftell计算当前文件指针相对于起始位置的偏移量
rewind设置文件指针到文件的起始位置
ferror判断文件操作过程中是否发生错误
feof判断文件是否读取到文件末尾

test.c 写文件

示例 C写文件操作

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 
  4 int main ()
  5 {
  6   FILE* fp = fopen("log.txt","w");
  7   if(fp == NULL)
  8   {
  9     perror("open");
 10     return 1;
 11   }
 12 
 13   int count = 5;
 14   while(count)
 15   {
 16     fprintf(fp,"Hello qq\n");
 17     count--;
 18   }
 19   return 0;                                                                                                                                                                                                                      
 20 } 

这里使用了 fprintf() 来写入文件
在这里插入图片描述

FILE *stream: 指向一个 FILE 对象的指针,该对象标识了一个输出流。
const char *format 是一个格式字符串,指定了后续参数如何呗格式化和输出。例如:%d 表示整数, %s 代表字符串。

上述代码向 log.txt 文件打印 5Hello qq
当前路径不存在log.txt
在这里插入图片描述
运行了可执行文件后,在当前路径出现了 log.txt ,使用 cat log,txt 可查看是否写入成功。
在这里插入图片描述
那我们多运行几次呢?
在这里插入图片描述

所以我们可知 w 的文件打开方式的原则是

  1. 如果不存在,就在当前路径下,新建指定的文件
  2. 默认打开文件的时候,就会先把目标文件清空!

如果用 a 文件打开方式呢?

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 
  4 int main ()
  5 {
  6   FILE* fp = fopen("log.txt","a");
  7   if(fp == NULL)                                                                                                                                                                                                                 
  8   {
  9     perror("open");
 10     return 1;
 11   }
 12 
 13   int count = 5;
 14   while(count)
 15   {
 16     fprintf(fp,"Hello qq\n");
 17     count--;
 18   }
 19	  fclose(fp);
 20   return 0;
 21 }

在这里插入图片描述

所以我们可知 a 的文件打开方式的原则是

  1. 如果不存在,就在当前路径下,新建指定的文件
  2. 默认打开文件的时候,就会继续追加打印!

test.c读文件

示例C读文件

在这里插入图片描述

fscanf 的用法与fprintf类似

 4 int main ()
  5 {
  6   FILE* fp = fopen("log.txt","r");
  7   if(fp == NULL)
  8   {
  9     perror("fopen");
 10     return 1;
 11   }
 12   char buffer[128];
 13   fscanf(fp,"%s",buffer);
 14   printf("%s",bufferf);
 15   fclose(fp);
 16   return 0;

在这里插入图片描述
因为 log.txt 里面存储的是 Hello qq Hello qq ...,又当 fscanf 碰到空格就停止读入了,故打印出 Hello

> 和 >>

我们之前介绍过 echo 则是直接将信息打印到了显示屏上,如下图:
在这里插入图片描述
那如果我们加上 >>> 呢?
在这里插入图片描述
可以看见,当使用 echo hello > log.txt 时,我们再次 log.txt 里面的信息会被清空再打印。

在这里插入图片描述
而当我们使用 echo hello >> log.txt 则是会直接继续追加到原有的文本后面。

上述的 >>> 叫作输出重定向 ,在命令行界面中用于输出重定向,它们允许你将命令的输出从标准输出(通常是终端或控制台窗口)重定向到文件中。
输出重定向一定是文件操作!

打开文件:本质就是 进程 打开文件! 文件没有被打开的时候,文件是被存储在磁盘中的。
进程可以打开很多文件!而一个系统中可以存在很多的进程,所以在很多情况下,OS内部,一定存在大量的被打开文件。

而对于这些被打开的文件则是必须要管理的,就类似之前提到过的进程管理,要 先描述,再组织。
我们可以在这预言一波:每一个被打开的文件,在OS内部,一定要存在对应的描述文件属性的结构体,类似PCB。

理解文件

首先,我们先抛出一个问题:操作文件,本质是进程在操作文件。那进程和文件的关系到底是什么样的呢?
在这里插入图片描述
电脑上的所有信息是存储在磁盘上的,文件也不例外,用户对磁盘的写入即是对硬件的写入,但是由上面的这个图可以看出用户是无法与硬件直接接触的,OS是硬件的管理者,用户可以通过OS对上层用户提供的各种接口来进行写入。
而我们平时使用到的各种接口比如:fopen , fwrite , fread . cin , cout 等等都是语言层次的接口,他们都不是原生的OS提供的接口而是对OS提供接口的封装!

那我们可以通过系统提供的接口来访问文件吗?

stdin & stdout & stderr

在 C 语言中,当程序启动时,操作系统(或运行时环境) 默认会打开三个标准的流(文件)。这三个流分别是 标准输入(stdin)标准输出(stdout)标准错误输出(stderr)。这些流是预先打开的,因此程序可以立即使用它们,而无需任何显式的文件打开操作。它们分别用于 程序的输入、输出和错误消息输出, 是通过 <stdio.h> 头文件中定义的 FILE 指针来访问的。

标准输入(stdin)

stdin 用于程序的标准输入。默认情况下,它与程序运行的控制台或终端的输入相关联。你可以使用如 scanf、fgets 等函数从 stdin 读取输入。

标准输出(stdout)

stdout 是程序的标准输出流。默认情况下,它与程序运行的控制台或终端的输出相关联。函数如 printf、puts 默认向 stdout 输出数据。

标准错误输出(stderr)

stderr 是用于程序的错误消息的标准输出流。尽管默认情况下它也与控制台或终端相关联,但它是未缓冲的,这意味着所有写入到 stderr 的数据都会立即输出,而不是存放在缓冲区中等待。这对于报告错误信息特别有用,确保即使程序崩溃或意外终止,错误信息也能被立即显示出来。使用 fprintf 等函数可以向 stderr 输出错误消息。

示例

#include <stdio.h>

int main() {
    // 向标准输出写入
    fprintf(stdout, "Hello, world!\n");

    // 从标准输入读取
    char buffer[100];
    fprintf(stdout, "Enter your name: ");
    fgets(buffer, 100, stdin);

    // 向标准输出写入
    fprintf(stdout, "Hello, %s", buffer);

    // 向标准错误输出写入
    fprintf(stderr, "This is an error message.\n");

    return 0;
}

在这里插入图片描述

系统文件I/O

先来看一下这段代码:
test.c 写文件
在这里插入图片描述

这里我们使用了OS的原生接口来实现我们上面实现的第一个代码。
在这里插入图片描述
test.c 读文件
在这里插入图片描述
在这里插入图片描述

接口介绍

open

man 2 open
在这里插入图片描述
函数原型:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname

open函数的第一个参数是pathname,表示要打开或创建的目标文件。

  • 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
  • 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
flags

open函数的第二个参数是flags,表示控制打开文件的操作模式。这是一系列常量的组合,主要包括以下几类:

访问模式(必选一):

  1. O_RDONLY:只读模式打开。
  2. O_WRONLY:只写模式打开。
  3. O_RDWR:读写模式打开。

文件创建和状态(可选):

  1. O_CREAT:如果文件不存在,则创建。使用这个标志时通常需要提供 mode 参数指定新文件的权限。
  2. O_EXCL:与 O_CREAT 同时使用,确保调用者创建文件。如果文件已存在,调用失败。
  3. O_TRUNC:如果文件已存在且为只写或读写成功打开,则其长度被截断为 0。
  4. O_APPEND:写入时总是追加到文件末尾。

其他标志:

  1. O_NONBLOCK 或 O_NDELAY:非阻塞模式打开文件(这对特殊类型的文件如设备文件特别有用)。
  2. O_SYNC:写操作等待物理I/O 完成。
  3. O_NOFOLLOW:如果 pathname 指定的是一个符号链接,那么不会跟随它。 其他更多的标志可以查看具体的系统文档。

系统接口open的第二个参数flags是整型,有32比特位,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。

实际上传入flags的每一个选项在系统当中都是以宏的方式进行定义的:
在这里插入图片描述
当使用 open 函数时,flags 参数中的每个选项都对应着不同的位。例如,O_RDONLY 可能定义为 0x0000,O_WRONLY 为 0x0001,O_RDWR 为 0x0002,O_CREAT 为 0x0100 等。这些值通常在系统级别的头文件中定义(如 <fcntl.h>),并且是针对特定操作系统平台优化的。

组合使用 flags
当需要同时指定多个选项时,可以使用位或操作(|)来组合它们。例如,如果你想以读写方式打开一个文件,并且如果该文件不存在,则创建它,你可以这样调用 open:

int fd = open("filename", O_RDWR | O_CREAT, 0666);

这里,O_RDWR | O_CREAT 的操作实际上是将 O_RDWR 和 O_CREAT 对应的二进制位进行位或操作,合成一个新的整数值,这个值同时包含了 O_RDWR 和 O_CREAT 的信息。

每一个 flags 的选项都是由单独的位或位组合来表示的,设计成这样主要是为了两个目的:

  1. 节省空间:通过位来表示不同的选项,每一个选项只需要改变一个位的状态,就可以表示一个完全不同的操作模式。
  2. 提高效率:位操作在硬件级别上是非常高效的,因为这直接对应于处理器的基本操作。
mode

这个参数是在创建新文件时设置文件权限的。它是一个八进制数,表示文件的访问权限。
mode 实际上是受到进程的 umask 值的影响。umask 定义了一个掩码,用来屏蔽掉文件模式创建权限的一部分。实际文件权限是 mode 减去 umask 的结果。
例如,将mode设置为0666,则文件创建出来的权限如下:
在这里插入图片描述

但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
在这里插入图片描述

若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。

umask(0); //将文件默认掩码设置为0

open的返回值

我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
	umask(0);
	int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 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;
}

在这里插入图片描述
我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败。

#include <stdio.h>                                                                                       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    int fd = open("test.txt", O_RDONLY);
    printf("%d\n", fd);
    return 0;
}

在这里插入图片描述

close

系统接口中使用close函数关闭文件,close函数的函数原型如下:

int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

write

系统接口中使用write函数向文件写入信息,write函数的函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

我们可以使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。

  • 如果数据写入成功,实际写入数据的字节个数被返回。
  • 如果数据写入失败,-1被返回。

示例:

 1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/stat.h>
  6 #include<fcntl.h>
  7 #include<string.h>
  8 
  9 int main ()
 10 {
 11   int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
 12   if(fd < 0)
 13   {
 14     perror("open");
 15     return 1;
 16   }
 17   const char* msg = "hello syscall\n";
 18   for(int i = 0; i < 5 ;i++)
 19   {                                                                                                                                                                                                                              
 20     write(fd,msg,strlen(msg));
 21   }
 22   close(fd);
 23   return 0;
 24 }

运行程序后,在当前路径下就会生成对应文件,文件当中就是我们写入的内容。
在这里插入图片描述

read

系统接口中使用read函数从文件读取信息,read函数的函数原型如下:

ssize_t read(int fd, void *buf, size_t count);

我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。

  • 如果数据读取成功,实际读取数据的字节个数被返回。
  • 如果数据读取失败,-1被返回。
  1 #include<stdio.h>  
  2 #include<stdlib.h>  
  3 #include<unistd.h>  
  4 #include<sys/types.h>  
  5 #include<sys/stat.h>  
  6 #include<fcntl.h>  
  7 #include<string.h>  
  8   
  9 int main ()  
 10 {  
 11   int fd = open("log.txt",O_RDONLY);  
 12   if(fd < 0)  
 13   {  
 14     perror("open");  
 15     return 1;  
 16   }  
 17   char ch;  
 18   while(1)  
 19   {  
 20     ssize_t s = read(fd,&ch,1);  
 21     if(s <= 0)  
 22     {  
 23       break;
 24     }
 25     write(1,&ch,1);    //向文件描述符为1的文件写入数据,即为显示器写入.
 26   }
 27   close(fd);
 28   return 0;
 29 }

运行程序后,就会将我们刚才写入文件的内容读取出来,并打印在显示器上。
在这里插入图片描述

文件描述符fd

文件描述符(File Descriptor,简称FD)是UNIX和类UNIX(包括Linux)操作系统中用于访问文件的一个抽象概念。

在UNIX哲学中,一切都是文件:普通文件、目录、链接、设备(如键盘、显示器)、管道、套接字等。这种设计使得操作系统的接口简单且一致,因为大部分I/O操作都可以使用相同的系统调用,比如read()、write()和close()等。

操作系统内部维护了一个文件描述符表(或称为文件描述符数组),这个表是进程级别的资源。**每当一个进程打开或创建文件时,内核就会创建一个文件描述符与之对应,并将其添加到当前进程的文件描述符表中。**每个文件描述符在它所属的进程中是唯一的,并且通常是按顺序分配的(即每次分配最小的未使用描述符)。

UNIX和类UNIX系统为每个进程自动打开三个基本的文件描述符,分别是:

0:标准输入(stdin),通常是键盘输入或另一个程序的输出。
1:标准输出(stdout),通常是终端(命令行界面)或另一个程序的输入。
2:标准错误(stderr),用于输出错误信息,通常也是显示在终端上。

文件描述符与文件的关系
文件描述符表: 对于每个进程,内核都维护一个文件描述符表。这个表是索引(文件描述符)到文件句柄的映射。文件句柄包含了操作系统用于管理打开文件所需的信息,如文件位置、访问模式(读、写等)和锁状态。
文件表:除了每个进程的文件描述符表,内核还维护了一个全局的文件表。当多个进程打开同一个文件时,它们会有各自独立的文件描述符,但这些描述符会指向同一个文件表项。
v-node表:文件表项指向一个v-node(虚拟节点),它包含了文件的元数据(如文件类型、权限、大小等)和指向实际数据的指针。对于特殊文件类型(如设备文件),v-node还可能包含设备驱动程序提供的操作函数。

进程和文件之间的对应关系是如何建立的?

我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。
在这里插入图片描述
而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符。
当进程打开log.txt文件时,我们需要先将该文件从磁盘当中加载到内存,形成对应的struct file,将该struct file连入文件双链表,并将该结构体的首地址填入到fd_array数组当中下标为3的位置,使得fd_array数组中下标为3的指针指向该struct file,最后返回该文件的文件描述符给调用进程即可。
在这里插入图片描述

因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。

文件描述符的分配规则

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/stat.h>
  6 #include<fcntl.h>
  7 #include<string.h>
  8 
  9 int main()
 10 {
 11   int fd = open("log.txt",O_RDONLY);
 12   if(fd < 0)
 13   {
 14     perror("open");
 15     return 1;
 16   }
 17   printf("fd: %d\n",fd);
 18 
 19   close(fd);                                                                                                                                                                                                                     
 20   return 0;                                                                                                                                                                                                             
 21 }   

在这里插入图片描述
因为我们之前说过UNIX和类UNIX系统为每个进程自动打开三个基本的文件描述符 分别是 0 1 2 ,所以下一个打开的文件描述符就是 3 了。

那我们可以关闭一下0 或者 2 再试试呢
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
发现是结果是: fd: 0 或者fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。

重定向

我们在上面只演示了关闭 0 和 2 ,那关闭 1 会是怎么样的呢?
在这里插入图片描述
我们运行程序发现并没有在显示屏上打印,而是打印在了log.txt里面
在这里插入图片描述

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。
常见的重定向有:>, >>, < 那重定向的本质是什么呢?

输出重定向就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中。
在这里插入图片描述
注意:

  1. printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
  2. C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。

追加重定向原理

追加重定向和输出重定向的唯一区别就是,输出重定向是覆盖式输出数据,而追加重定向是追加式输出数据。

例如,如果我们想让本应该输出到“显示器文件”的数据追加式输出到log.txt文件当中,那么我们应该先将文件描述符为1的文件关闭,然后再以追加式写入的方式打开log.txt文件,这样一来,我们就将数据追加重定向到了文件log.txt当中。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	close(1);
	int fd = open("log.txt", O_WRONLY|O_APPEND|O_CREAT, 0666);
	if(fd < 0){
		perror("open");
		return 1;
	}
	printf("hello Linux\n");
	printf("hello Linux\n");
	printf("hello Linux\n");
	printf("hello Linux\n");
	printf("hello Linux\n");
	fflush(stdout);
	close(fd);
	return 0;
}

在这里插入图片描述

输入重定向原理
输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。
在这里插入图片描述
例如,如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
	close(0);
	int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
	if (fd < 0){
		perror("open");
		return 1;
	}
	char str[40];
	while (scanf("%s", str) != EOF){
		printf("%s\n", str);
	}
	close(fd);
	return 0;
}

在这里插入图片描述
scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。

标准输出流和标准错误流对应的都是显示器,它们有什么区别?

我们来看看下面这段代码,代码中分别向标准输出流和标准错误流输出了两行字符串。

#include <stdio.h>
int main()
{
	printf("hello printf\n"); //stdout
	perror("perror"); //stderr

	fprintf(stdout, "stdout:hello fprintf\n"); //stdout
	fprintf(stderr, "stderr:hello fprintf\n"); //stderr
	return 0;
}

在这里插入图片描述
在显示屏打印出了三个语句。

到目前为止,还未发现两者的区别,当我们使用重定向输出到其他文件中就能发现区别:
在这里插入图片描述
我们发现 stdout 语句输入到了 log,txt 里面 但是perror并没有。
重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。
stdout 可以通过重定向到文件中来实现输出的保存和记录,但 perror 不可以被重定向到文件中,这是为了确保程序的错误信息能够及时显示在终端上,帮助用户发现和解决问题。

dup2

dup2是UNIX和类UNIX操作系统(如Linux)中的一个系统调用函数,用于复制文件描述符。通过这个函数,可以使newfd成为oldfd的一个副本,如果newfd已经打开,dup2会先将其关闭(如果需要,关闭前会刷新newfd相关的缓冲区),然后将oldfd的副本分配给newfd。

函数原型

int dup2(int oldfd, int newfd);

oldfd: 要复制的文件描述符。
newfd: 目标文件描述符。如果这个文件描述符已经打开,它会被dup2关闭(除非oldfd是newfd,这种情况下dup2不做任何操作,并返回newfd)。

返回值

成功: 返回新的文件描述符(即newfd)。
失败: 返回-1,并设置errno以指示错误。

dup2经常用在需要重定向标准输入、输出和错误输出的场景中。比如,一个进程想要将标准输出重定向到一个文件,可以使用dup2将该文件的文件描述符复制到STDOUT_FILENO (即1)。

  1 #include <unistd.h>
  2 #include <fcntl.h>
  3 #include <stdio.h>
  4 #include <stdlib.h>
  5 
  6 int main() {
  7     int file = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  8     if (file < 0) {
  9         perror("open");
 10         return EXIT_FAILURE;
 11     }
 12 
 13     // 将标准输出重定向到file指向的文件
 14     if (dup2(file, 1) < 0) {
 15         perror("dup2");
 16         close(file);
 17         return EXIT_FAILURE;
 18     }
 19 
 20     // 从这点开始,所有的标准输出都会写入output.txt
 21     printf("This will be written to the file 'output.txt'\n");
 22 
 23     // 清理
 24     close(file); // STDOUT_FILENO仍然是打开状态的
 25 
 26     return EXIT_SUCCESS;
 27 }

在这里插入图片描述

添加重定向功能到minishell

在myshell当中添加重定向功能的步骤大致如下:

  1. 对于获取到的命令进行判断,若命令当中包含重定向符号>、>>或是<,则该命令需要进行处理。
  2. 设置type变量,type为0表示命令当中包含输出重定向,type为1表示命令当中包含追加重定向,type为2表示命令当中包含输入重定向。
  3. 重定向符号后面的字段标识为目标文件名,若type值为0,则以写的方式打开目标文件;若type值为1,则以追加的方式打开目标文件;若type值为2,则以读的方式打开目标文件。
  4. 若type值为0或者1,则使用dup2接口实现目标文件与标准输出流的重定向;若type值为2,则使用dup2接口实现目标文件与标准输入流的重定向。

代码实现如下:

#include <stdio.h>
#include <fcntl.h>
#include <ctype.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 //命令最大长度
#define NUM 32 //命令拆分后的最大个数
int main()
{
	int type = 0; //0 >, 1 >>, 2 <
	char cmd[LEN]; //存储命令
	char* myargv[NUM]; //存储命令拆分后的结果
	char hostname[32]; //主机名
	char pwd[128]; //当前目录
	while (1){
		//获取命令提示信息
		struct passwd* pass = getpwuid(getuid());
		gethostname(hostname, sizeof(hostname)-1);
		getcwd(pwd, sizeof(pwd)-1);
		int len = strlen(pwd);
		char* p = pwd + len - 1;
		while (*p != '/'){
			p--;
		}
		p++;
		//打印命令提示信息
		printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
		//读取命令
		fgets(cmd, LEN, stdin);
		cmd[strlen(cmd) - 1] = '\0';

		//实现重定向功能
		char* start = cmd;
		while (*start != '\0'){
			if (*start == '>'){
				type = 0; //遇到一个'>',输出重定向
				*start = '\0';
				start++;
				if (*start == '>'){
					type = 1; //遇到第二个'>',追加重定向
					start++;
				}
				break;
			}
			if (*start == '<'){
				type = 2; //遇到'<',输入重定向
				*start = '\0';
				start++;
				break;
			}
			start++;
		}
		if (*start != '\0'){ //start位置不为'\0',说明命令包含重定向内容
			while (isspace(*start)) //跳过重定向符号后面的空格
				start++;
		}
		else{
			start = NULL; //start设置为NULL,标识命令当中不含重定向内容
		}

		//拆分命令
		myargv[0] = strtok(cmd, " ");
		int i = 1;
		while (myargv[i] = strtok(NULL, " ")){
			i++;
		}
		pid_t id = fork(); //创建子进程执行命令
		if (id == 0){
			//child
			if (start != NULL){
				if (type == 0){ //输出重定向
					int fd = open(start, O_WRONLY | O_CREAT | O_TRUNC, 0664); //以写的方式打开文件(清空原文件内容)
					if (fd < 0){
						error("open");
						exit(2);
					}
					close(1);
					dup2(fd, 1); //重定向
				}
				else if (type == 1){ //追加重定向
					int fd = open(start, O_WRONLY | O_APPEND | O_CREAT, 0664); //以追加的方式打开文件
					if (fd < 0){
						perror("open");
						exit(2);
					}
					close(1);
					dup2(fd, 1); //重定向
				}
				else{ //输入重定向
					int fd = open(start, O_RDONLY); //以读的方式打开文件
					if (fd < 0){
						perror("open");
						exit(2);
					}
					close(0);
					dup2(fd, 0); //重定向
				}
			}

			execvp(myargv[0], myargv); //child进行程序替换
			exit(1); //替换失败的退出码设置为1
		}
		//shell
		int status = 0;
		pid_t ret = waitpid(id, &status, 0); //shell等待child退出
		if (ret > 0){
			printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码
		}
	}
	return 0;
}

FILE

FILE当中的缓冲区

看下面这段代码:

  1 #include <unistd.h>
  2 #include <fcntl.h>
  3 #include <stdio.h>
  4 #include <stdlib.h>
  5 #include <string.h>
  6 
  7 int main ()
  8 {
  9   const char *msg0 = "hello printf\n";
 10   const char *msg1 = "hello fwrite\n";
 11   const char *msg2 = "hello write\n";
 12 
 13   printf("%s",msg0);
 14   fwrite(msg1,strlen(msg0),1,stdout);
 15   write(1,msg2,strlen(msg2));
 16   
 17   fork();
 18                                                                                                                                                                                                                                  
 19   return 0;                                                                                                                           
 20 }      

结果如下:
在这里插入图片描述
但如果对进程实现输出重定向呢?./myFile >file, 我们发现结果变成了:
在这里插入图片描述
我们发现 printffwrite库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和 fork 有关!

而对于上面这个问题。我们就得了解一下 缓冲区 这个概念了:

缓冲区(Buffer) 是计算机内存中的一个临时存储区,用于在数据在发送者和接收者之间传输时 暂时存储数据 。缓冲区的使用可以提高数据处理的效率,减少读写操作的次数,从而优化程序的性能。在不同的应用场景中,缓冲区的实现和使用方式可能会有所不同,但其基本目的和原理是一致的。

缓冲区的类型

  • 全缓冲(Fully Buffered):数据在缓冲区满时才进行I/O操作。通常用于与磁盘文件的交互。
  • 行缓冲(Line Buffered):数据在遇到换行符时进行I/O操作。通常用于终端输入输出,便于用户按行处理数据,一般显示器采用的就是行缓冲。
  • 无缓冲(Unbuffered):数据立即进行I/O操作,不在缓冲区中积累。例如,stderr通常是无缓冲的,以便于立即输出错误信息。

对于上一个问题的解释:

这个现象的根本原因在于库函数(printf 和 fwrite)系统调用(write) 在处理缓冲区时的不同。

当你在终端运行这个程序时,printffwrite

  1. 输出到标准输出(stdout),这时标准输出是行缓冲的(因为标准输出关联到一个终端设备)。行缓冲意味着缓冲区会在遇到换行符、缓冲区满或者流被关闭时刷新(即,真正写出到终端或文件)。因此,在调用fork()之前,printf
    和 fwrite 中的数据被发送到stdout并立即显示。
  2. 另一方面,write 系统调用直接写入到文件描述符(在这个例子中是标准输出,即文件描述符1),绕过了标准C库的缓冲机制。因此,write的输出不会被缓存,而是直接写出

当你重定向输出到文件时,标准输出变成了全缓冲模式。这意味着printffwrite的输出会存储在标准库的缓冲区中而不是立即写到文件中。 只有 当缓冲区满、缓冲区被显式刷新(例如,通过fflush()),或者正常关闭流时(例如,程序结束), 缓冲区的内容才会被写出。

现在,考虑fork()在这里的作用
fork()调用创建了一个子进程,这个子进程是父进程的副本包括文件描述符和标准库的缓冲区。在这个例子中,当fork()被调用时,printffwrite 的缓冲区中的内容(如果有的话)被复制到子进程中。因为程序没有显式刷新缓冲区(使用fflush(stdout);),这些缓冲区的内容在父进程和子进程中都存在,所以当它们终止时,这些内容被写入到了文件中,导致printf和fwrite输出看起来重复了两次
然而,由于write 直接操作文件描述符,绕过了缓冲区,所以它的输出没有被复制到子进程的缓冲区中,因此只输出了一次。

为了解决这个问题(即,避免输出重复),可以在调用fork()之前手动刷新缓冲区,使用fflush(stdout);。这样可以确保缓冲区的内容在fork()之前被写出,避免了复制未刷新缓冲区到子进程,从而避免了重复输出的问题。

综上: printf fwrite 库函数会自带缓冲区,而write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,
都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统
调用的“封装”,但是write 没有缓冲区,而printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是
C,所以由C标准库提供。

FILE当中的文件描述符

因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。

首先,我们在/usr/include/stdio.h头文件中可以看到下面这句代码,也就是说FILE实际上就是struct _IO_FILE结构体的一个别名。

typedef struct _IO_FILE FILE;

而我们在/usr/include/libio.h头文件中可以找到struct _IO_FILE结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为_fileno的成员,这个成员实际上就是封装的文件描述符。

struct _IO_FILE {
	int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

	//缓冲区相关
	/* The following pointers correspond to the C++ streambuf protocol. */
	/* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
	char* _IO_read_ptr;   /* Current read pointer */
	char* _IO_read_end;   /* End of get area. */
	char* _IO_read_base;  /* Start of putback+get area. */
	char* _IO_write_base; /* Start of put area. */
	char* _IO_write_ptr;  /* Current put pointer. */
	char* _IO_write_end;  /* End of put area. */
	char* _IO_buf_base;   /* Start of reserve area. */
	char* _IO_buf_end;    /* End of reserve area. */
	/* The following fields are used to support backing up and undo. */
	char *_IO_save_base; /* Pointer to start of non-current get area. */
	char *_IO_backup_base;  /* Pointer to first valid character of backup area */
	char *_IO_save_end; /* Pointer to end of non-current get area. */

	struct _IO_marker *_markers;

	struct _IO_FILE *_chain;

	int _fileno; //封装的文件描述符
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
	/* 1+column number of pbase(); 0 is unknown. */
	unsigned short _cur_column;
	signed char _vtable_offset;
	char _shortbuf[1];

	/*  char* _save_gptr;  char* _save_egptr; */

	_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

那从底层来看 fopen 的大致逻辑是什么呢?

fopen函数是C语言标准I/O库中用于打开文件的函数。它封装了底层的文件打开操作,提供了一个高级的接口来处理文件。当你调用fopen时,它会执行以下主要步骤来完成文件的打开操作,并为后续的文件读写操作做准备:

  1. 分配FILE结构体
    首先,fopen为要打开的文件分配一个FILE结构体。这个结构体包含了管理文件所需的所有信息,包括缓冲区的位置、文件的当前位置、错误状态标志等。
  2. 打开文件
    fopen通过调用底层的open系统调用(或在某些系统中可能是其他等效的调用),根据指定的路径和模式打开文件。如果open调用成功,它会返回一个文件描述符(file descriptor),这是一个非负整数,作为对打开文件的引用。
  3. 初始化FILE结构体
    一旦文件成功打开,fopen会使用返回的文件描述符来初始化FILE结构体的各个字段。这包括将文件描述符存储在_fileno字段中,以便后续操作(如读写)可以通过这个文件描述符来引用文件。
    此外,fopen还会设置FILE结构体中与缓冲区相关的字段。例如,它会分配内存作为文件的缓冲区,并初始化指针如_IO_buf_base和_IO_buf_end来指向这个缓冲区的起始和结束位置。这样,后续的读写操作就可以使用这个缓冲区来提高效率。
  4. 返回FILE指针
    最后,fopen返回一个指向已分配并初始化的FILE结构体的指针。这个FILE *指针之后会被用于其他标准I/O函数(如fread、fwrite、fclose等)来执行实际的文件操作。

理解文件系统

磁盘概念

在一个电脑中,存在非常多的文件,但是被打开的文件确实很少的。
那么没有被打开的文件,在哪里存放呢? 那就是存放在磁盘里了。 要想找到一个文件我们需要 文件路径+文件名
。 那如何在磁盘文件中找到我们的文件呢?首先先来了解一下磁盘文件。

下面就是一个磁盘的内部照片
在这里插入图片描述

磁盘是计算机中用于存储数据的主要设备之一,它是一种永久性存储介质,可以持久保存数据,即使在断电后也能保持数据不丢失。
我们今天主要介绍的是机械硬盘(HDD)

磁盘主要构成要素有盘片、磁头、磁道、扇区、柱面,如下图。
在这里插入图片描述
1、盘片
硬盘首先会有多个盘片构成,类似很多个独立的光盘合并在一起,每个盘片都有2个面,每个盘片都有一个对应的磁头,一般硬盘会有5个盘片构成,盘片盘便面自下往上顺序进行编号,如5个盘片那么编号自下而上就是10个盘面的编号。

2、扇区和磁道
每个盘片会分成若干个相同同心圆的磁道,磁道从外围开始编号,从0开始,每个磁道又会划分成若干个扇区,扇区是硬盘的最小存储单元,一般是存储512字节(byte)
在这里插入图片描述
在这里插入图片描述

3、磁头和柱面
磁头用于读取盘面中磁道内的扇区中存储的数据。一个盘片有上下2个盘面对应2个磁头。
柱面是所有盘面中相同磁道的柱面,形成的是一个立体的柱体形状,磁盘的柱面数和磁道数是相等的,盘面数等于总的磁头数。

在这里插入图片描述

总结:
盘片: 可读可写可擦除,一片两面都可以写,一个面一个磁头。
磁盘本质就是一种机械设备,所以要轻拿轻放,以免对磁盘的内部造成损坏
磁盘中的盘片为什么要旋转呢? 因为要定位扇区。
磁盘中的磁头为什么要左右摆动呢? 因为要定位磁道。

那如何找到一个指定位置的扇区?
这里我们简单的了解一下CHS定址法。

CHS(Cylinder-Head-Sector)定址法是一种传统的硬盘数据存储定位方式,通过磁柱(Cylinder)、磁头(Head)、扇区(Sector)三个参数来指定硬盘上的存储位置。

  1. Cylinder(磁柱):硬盘由多个磁盘片组成,每个磁盘片的同一半径位置形成一个圆形轨道,这些同一半径的轨道堆叠在一起形成一个磁柱。磁柱是硬盘读写操作的基本单位之一。
  2. Head(磁头):磁头用于读写硬盘上的数据。在CHS定址中,"Head"指的是用于访问数据的磁头编号。每个磁盘面有一个磁头,所以磁头的数量通常等于磁盘面的数量。
  3. Sector(扇区):扇区是硬盘上最小的数据存储单位,每个磁道被细分为多个扇区。传统上,每个扇区存储512字节的数据。

要定位硬盘上的一个特定扇区,系统需要知道该扇区所在的磁柱号(C)、磁头号(H)和扇区号(S)。例如,通过CHS值(10, 2, 5),我们可以定位到第10个磁柱、第2个磁头下的第5个扇区。

如果想更深了解磁盘的相关知识,可以查看这篇文章磁盘详解(一文搞懂磁盘)

对磁盘的存储进行逻辑抽象

磁盘的直线模型

整体视图: 在EXT2文件系统中,我们可以想象把一个磁盘的所有磁道拉直,排列成一行。每个扇区在这条线上都有一个唯一的位置,就像在一个非常长的珠串上每个珠子都有其特定的位置一样。

在这里插入图片描述

抽象处理后我们可以得到这样的一副长条状直线模型,其中它是由每一个磁面拼接在一起而构成的。
在这里插入图片描述
那么我们就可以进一步吧这些拼接好的直线模型看成一个连续的数组,将每一个扇面当作一个单位来管理,假设一个扇面有10个磁道,每条磁道有100个扇区,那么这10条磁道就总共有1000个扇区,也就是说这个扇面也就总共包含1000个扇区。那么这样处理之后呢我们如何去找文件呢?
在这里插入图片描述

当我们设我们要找的文件的序列为 i n d e x index index 时,以下是对应的公式表示:

  1. 首先计算磁面的位置 H H H
    i n d e x 1000 = H //因为一个磁面有1000个扇面, i n d e x / 1000  就可以得到在第几个扇面从而找到  H  磁头的位置 \frac{index}{1000} = H \quad \text{//因为一个磁面有1000个扇面,$index/1000$ 就可以得到在第几个扇面从而找到 $H$ 磁头的位置} 1000index=H//因为一个磁面有1000个扇面,index/1000 就可以得到在第几个扇面从而找到 H 磁头的位置

  2. 接着计算磁道的位置 C C C
    i n d e x m o d    1000 100 = C //找到磁道的位置 \frac{index \mod 1000}{100} = C \quad \text{//找到磁道的位置} 100indexmod1000=C//找到磁道的位置

  3. 最后计算扇区的位置 S S S
    i n d e x m o d    100 = S //找到扇区的位置 index \mod 100 = S \quad \text{//找到扇区的位置} indexmod100=S//找到扇区的位置

举例:假设 i n d e x = 500 index = 500 index=500

  • 计算磁面的位置: 500 / 1000 = 0 500 / 1000 = 0 500/1000=0 (0号磁面)
  • 计算磁道的位置: 500 m o d    1000 = 500 500 \mod 1000 = 500 500mod1000=500 500 / 100 = 5 500 / 100 = 5 500/100=5 (5号磁道)
  • 计算扇区的位置: 500 m o d    100 = 0 500 \mod 100 = 0 500mod100=0 (0号扇区)

一般而言,OS在和磁盘交互的时候,基本单位不是512byte,而是4kb.
4kb8 * sector.
4kb就是8个连续的扇区。
我们将这每4kb在磁盘上的空间就叫做块。
一个文件是由很多个块构成的。
在这里插入图片描述
当磁盘完成分区后,我们还需要对磁盘进行格式化。磁盘格式化就是对磁盘中的分区进行初始化的一种操作,这种操作通常会导致现有的磁盘或分区中所有的文件被清除。
磁头通过在磁性介质上产生磁场来表示和读取数据,其中磁性介质的磁化方向代表了0和1的二进制信息。

在这里插入图片描述

EXT2文件系统的存储方案

EXT2(第二扩展文件系统)是Linux操作系统中常见的文件系统之一,它采用了一种层次化的存储结构来管理文件和目录。以下是EXT2文件系统的存储方案的详细解释:

计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
在这里插入图片描述
注意: 启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。

其次,每个组块都有着相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。
在这里插入图片描述

  1. 超级块(Superblock):
    · EXT2文件系统的第一个数据块包含了超级块,其中存储着文件系统的关键参数和元数据信息,如文件系统大小、块数、每块大小、inode数等。
    · 超级块的备份存储在文件系统中不同的位置,以防止损坏导致的数据丢失。

  2. 组描述符表(Group Descriptor Table):
    · 文件系统被分成多个组(group),每个组包含一定数量的数据块和inode。组描述符表存储了每个组的信息,如组的起始块号、块位图位置、inode位图位置等。

  3. 数据块位图和inode位图(Block Bitmap inode Bitmap):
    · 数据块位图记录了数据块的使用情况,每个位代表一个数据块,如果为1表示已经被占用,为0表示空闲。
    · inode位图记录了inode的使用情况,同样每个位代表一个inode的状态。

  4. inode表:(inode Table)
    · inode是文件系统中的索引节点,用于存储文件和目录的元数据信息,如文件大小、权限、所有者、时间戳等。
    · inode表存储了所有inode的信息,每个inode对应一个唯一的索引号。

  5. 数据块区域:(Data Blocks)
    · 数据块区域存储文件和目录的实际内容,它们被组织成多级块组织结构,包括直接块、间接块、双间接块和三间接块。
    · 直接块直接存储文件内容,间接块存储指向数据块的指针,双间接块和三间接块用于间接地指向大量数据块。

  6. 目录结构:
    · 目录被组织成特殊的文件,其中包含了文件名和对应的inode号的映射关系。每个目录项的格式包括文件名、inode号和文件类型等信息。

注意:

  1. 其他块组当中可能会存在冗余的Super Block,当某一Super Block被破坏后可以通过其他Super Block进行恢复。
  2. 磁盘分区并格式化后,每个分区的inode个数就确定了。

理解创建空文件的步骤

  1. 系统先搜索inode位图,锁定一个未使用的inode用于新文件。
  2. 定位到这个空闲inode在inode表中的位置,并在该inode结构里记录文件的基本信息。
  3. 在父目录的数据块中,加入新文件的名称及其对应的inode编号。

如何理解对文件写入信息?

  1. 通过文件的inode编号找到对应的inode结构。
  2. 通过inode结构找到存储该文件内容的数据块,并将数据写入数据块。
  3. 若不存在数据块或申请的数据块已被写满,则通过遍历块位图的方式找到一个空闲的块号,并在数据区当中找到对应的空闲块,再将数据写入数据块,最后还需要建立数据块和inode结构的对应关系。

说明一下:
一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。

如何理解删除一个文件?

  1. 将该文件对应的inode在inode位图当中置为无效。
  2. 将该文件申请过的数据块在块位图当中置为无效。

因为此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。
为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了。

为什么拷贝文件的时候很慢,而删除文件的时候很快?

因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可,无需真正的删除文件,因此拷贝文件是很慢的,而删除文件是很快的。

这就像建楼一样,我们需要很长时间才能建好一栋楼,而我们若是想拆除一栋楼,只需在这栋楼上写上一个“拆”字即可。

如何理解目录

  1. 都说在Linux下一切皆文件,目录当然也可以被看作为文件。
  2. 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
  3. 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。

注意: 每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。

软硬链接

软链接

我们可以通过以下命令创建一个文件的软连接。

[qq@iZ0jl65jmm6w9evbwz2zuoZ 4_10]$ ln -s myFile myFile-s

在这里插入图片描述
通过ls -i -l命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。
在这里插入图片描述
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,所以软链接文件的大小要比源文件小得多。软链接就类似于Windows操作系统当中的快捷方式。
在这里插入图片描述
但是软链接文件只是其源文件的一个标记,当删除了源文件后,链接文件不能独立存在,虽然仍保留文件名,但却不能执行或是查看软链接的内容了。
在这里插入图片描述

硬链接

我们可以通过以下命令创建一个文件的硬连接。
在这里插入图片描述

通过ls -i -l命令我们可以看到,硬链接文件的inode号与源文件的inode号是相同的,并且硬链接文件的大小与源文件的大小也是相同的,特别注意的是,当创建了一个硬链接文件后,该硬链接文件和源文件的硬链接数都变成了2。
在这里插入图片描述
硬链接文件就是源文件的一个别名,一个文件有几个文件名,该文件的硬链接数就是几,这里inode号为924344的文件有myproc和myproc-h两个文件名,因此该文件的硬链接数为2。

与软连接不同的是,当硬链接的源文件被删除后,硬链接文件仍能正常执行,只是文件的链接数减少了一个,因为此时该文件的文件名少了一个。
在这里插入图片描述
总之,硬链接就是让多个不在或者同在一个目录下的文件名,同时能够修改同一个文件,其中一个修改后,所有与其有硬链接的文件都一起修改了。

为什么刚刚创建的目录的硬链接数是2?

我们创建一个普通文件,该普通文件的硬链接数是1,因为此时该文件只有一个文件名。那为什么我们创建一个目录后,该目录的硬链接数是2?在这里插入图片描述

在Linux和类Unix系统中,当你创建一个新目录时,这个目录自动包含两个特殊的条目:.和…。这里的.代表当前目录本身,而…代表其父目录。这种设计意味着每个新建目录初始时都至少包含这两个引用。

因此,每个新建的目录不只是单独存在;它通过.与自身绑定,并通过…与父目录建立联系,导致其硬链接数从一开始就是2。硬链接数的概念反映了有多少条目指向同一个inode号,而在这种情况下,目录的.和它的名称(如dir)实际上是指向同一个inode的不同名称。这解释了为什么目录的硬链接计数起始于2,而不是1。

使用文件系统的命令(如ls -li)可以验证这一点:你会看到目录本身和其内的.条目具有相同的inode号,证实了它们确实指向相同的文件系统位置。
在这里插入图片描述

小技巧: 一个目录下相邻的子目录数等于该目录的硬链接数减2。

软硬链接的区别

  1. 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
  2. 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录。

文件的三个时间

在Linux当中,我们可以使用命令stat 文件名来查看对应文件的信息。
在这里插入图片描述

这其中包含了文件的三个时间信息:

  1. Access: 文件最后被访问的时间。
  2. Modify: 文件内容最后的修改时间。
  3. Change: 文件属性最后的修改时间。
    当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
    我们若是想将文件的这三个时间都更新到最新状态,可以使用命令touch 文件名来进行时间更新。

注意: 当某一文件存在时使用touch命令,此时touch命令的作用变为更新文件信息。

  • 16
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值