Linux - 文件系统、基础IO

目录

C语言文件IO

接口汇总

当前路径

默认打开的三个流

系统文件I/O

open

close

write

read

文件描述符fd

文件描述符的分配规则

重定向

重定向原理

dup2

FILE

FILE的文件描述符

FILE的缓冲区

文件系统

初识inode

磁盘的概念

磁盘分区与格式化介绍

EXT2文件系统的存储方案

软硬链接

软链接

硬链接

区别

文件的三个时间


C语言文件IO

接口汇总

以下是C语言中的文件操作函数:

当前路径

在编程中,当我们提到"当前路径"时,通常是指程序正在执行时所在的目录。这个路径可以是绝对路径,也可以是相对路径。绝对路径是从根目录开始的完整路径,而相对路径是相对于当前工作目录的路径。

例如,我们在BasicIO目录下运行可执行程序myproc,那么该可执行程序创建的log.txt文件会出现在BasicIO目录下。

那么,这里提到的'当前路径'是否指的是'程序当前运行所在的目录'呢? 为了验证这一点,我们可以先删除之前由程序生成的log.txt文件,然后进行一项测试:返回到程序的上一级目录,并从那里启动程序。

此时,我们注意到,当可执行程序运行完毕,它并没有在BasicIO文件夹内创建log.txt文件,而是在当前工作目录中生成了该文件。

一旦该可执行程序启动并成为一个进程,我们可以通过获取其进程ID(PID),进而在系统的/proc目录下查询该进程的详细信息。

在这个目录中,我们可以观察到两个符号链接:cwd和exe。cwd链接指向的是进程执行时所在的目录,而exe则指向可执行文件的路径。

综上所述,我们所讨论的'当前路径'实际上是指程序作为进程运行时用户所处的目录,而非程序文件本身的存储位置。

默认打开的三个流

在Linux系统中,一切实体都以文件的形式存在,这意味着即使是显示器和键盘这样的硬件设备,也可以通过文件的方式来进行交互。我们能够在屏幕上看到信息,是因为系统向特定的'显示器文件'发送了数据。同样,当我们在键盘上输入时,电脑实际上是从'键盘文件'中读取了我们输入的字符。

在Linux系统中,我们通常不需要显式打开"显示器文件"或"键盘文件"来进行数据的写入和读取。

这是因为在任何进程启动时,系统会自动为我们打开三个基本的输入输出流:标准输入(stdin)标准输出(stdout)标准错误输出(stderr)

在C语言中,这些流分别对应stdinstdoutstderr标准输入流 连接到键盘,允许程序读取用户的输入;而标准输出流标准错误流则连接到显示器,用于显示程序的输出和错误信息。这种设计使得程序开发更为便捷,因为开发者无需手动管理这些基本的输入输出操作。

在查阅man手册时,我们会发现stdinstdoutstderr 这三个重要的流实际上是**FILE***类型的对象。这意味着它们在C语言中被当作文件来处理,尽管它们代表的是标准输入、输出和错误输出的抽象概念。

extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;

当我们的C程序运行 时,操作系统会自动通过C语言的接口打开 这三个输入输出流:stdinstdoutstderr 。这样,我们就可以调用如scanfprintf 等函数,实现向键盘和显示器的输入输出操作。

stdinstdoutstderr 与我们通过打开文件获得的文件指针在概念上是相同的。例如,当我们使用fputs 函数,并将它的第二个参数设置为stdout 时,fputs 函数会将数据直接显示到显示器上。

#include <stdio.h>
int main()
{
	fputs("hello stdin\n", stdout);
	fputs("hello stdout\n", stdout);
	fputs("hello stderr\n", stdout);
	return 0;
}

答案是肯定的 ,这时我们实际上是在使用fputs 函数向"显示器文件 "写入数据,也就是将信息显示在显示器上。

注意:标准输入流、输出流和错误流 的概念不仅限于C语言,它们在C++中以cin、cout和cerr的形式存在,并且几乎所有编程语言都提供了类似的机制。这些特性并非特定于某一种编程语言,而是操作系统层面的支持。

系统文件I/O

处理文件时,除了使用C语言、C++或其他编程语言提供的接口,操作系统本身也提供了一套底层的系统调用接口来访问文件。

与C语言库函数或其它语言的库函数相比,这些系统调用接口更接近硬件层面。实际上,各种语言的库函数都是对这些底层系统调用进行了抽象和封装。

当在Linux环境中编写和执行C代码时,C库函数实际上是对Linux系统调用的封装;相应地,在Windows环境中,这些C库函数则封装了Windows的系统调用。这种设计策略不仅增强了C语言的跨平台兼容性 ,而且也促进了代码的可维护性和可扩展性

open

在系统接口中,我们通过调用open函数来访问文件。该函数的定义如下:

int open(const char *pathname, int flags, mode_t mode);
  1. 第一个参数:pathname,指定要操作的文件路径或文件名。

    • 如果提供路径,则文件将在指定路径下创建。
    • 如果仅提供文件名,则文件将在当前工作目录下创建。
  2. 第二个参数:flags,定义文件打开的模式。

    扩展说明

    • flags 参数是一个整型值,通常有32位,每个位代表一个标志。
    • 系统定义的标志位(如O_RDONLYO_WRONLYO_RDWRO_CREAT)都是以宏的形式存在,每个宏定义中只有一个位是1,其余位是0。这使得在open函数内部可以通过位与运算(&)来检测特定的标志位是否被设置。
    • 常用的选项包括:

      O_RDONLY:以只读方式打开文件。
      O_WRONLY:以只写方式打开文件。
      O_APPEND:以追加方式打开文件。
      O_RDWR:以读写方式打开文件。
      O_CREAT:如果文件不存在,则创建文件。
      
    • 这些选项可以通过位运算符"或"(|)组合使用。例如,要打开文件以只写方式,并在文件不存在时创建它,可以使用:O_WRONLY | O_CREAT

  3. 第三个参数:mode,指定创建文件时的默认权限。

    注意 :如果不需要创建文件,可以省略open函数的第三个参数。

    • 例如,设置mode0666,意味着文件将被创建为可读写的权限。

    • 实际创建的文件权限会受到umask(文件模式创建掩码)的影响。umask的默认值通常为0002,因此,设置mode0666时,实际权限为06640666 & ~0002)。

    • 若要创建文件的权限不受umask影响,可以在调用open之前设置umask0

      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;
}

执行程序后可以观察到,这些文件的文件描述符是从数字3开始的,并且是连续递增的。

接下来,我们可以试着打开一个不存在的文件,这将导致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;
}

在程序执行后,我们可以注意到,当文件打开操作失败时,返回的文件描述符值为-1。

实际上,所谓的文件描述符本质上是一个指向指针数组中特定位置的索引,这个数组中的每个指针都指向一个打开的文件的相关信息。通过文件描述符,我们可以访问到对应的文件信息。

open函数成功打开一个文件时,指针数组中的指针数量会增加,并将新增加的指针在数组中的索引作为文件描述符返回。如果打开文件失败,则直接返回-1。因此,当连续成功打开多个文件时,每个文件的文件描述符会是连续且递增的。

在Linux系统中,每个进程默认会有三个预先打开的文件描述符,分别对应标准输入(0)、标准输出(1)和标准错误(2)。这解释了为什么新打开的文件描述符是从3开始分配的。

close

在系统接口中,我们使用close函数来关闭一个已经打开的文件。该函数的声明如下:

int close(int fd);

当调用close函数时,你需要提供要关闭的文件的文件描述符作为参数。如果文件成功关闭,函数将返回0;如果关闭操作失败,则返回-1。

write

在系统接口中,write 函数用于将数据写入文件。该函数的定义如下:

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

通过调用 write 函数,我们可以将从 buf 指针开始的 count 字节数据写入到文件描述符为 fd 的文件中。

  • 成功写入:函数返回实际写入的字节数。
  • 写入失败 :函数返回 -1

read

在系统接口中,read 函数用于从文件中读取数据。该函数的声明如下:

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

利用 read 函数,我们可以从文件描述符为 fd 的文件中读取最多 count 字节的数据,并将其存储到 buf 指向的缓冲区。

  • 成功读取:函数返回实际读取的字节数。
  • 读取失败 :函数返回 -1

文件描述符fd

文件是在进程运行期间被打开的,单个进程可以打开多个文件,而系统中同时运行着众多进程,这意味着在任何给定时刻,系统中都可能存在大量打开的文件。

因此,操作系统必须对这些打开的文件进行有效管理 。操作系统为每个打开的文件创建一个对应的 struct file 结构体,并使用这些结构体构建一个双向链表,从而实现对文件的增删查改等管理操作。

为了区分哪些文件是由特定进程打开的,操作系统还需要建立一个映射,将进程与它们打开的文件关联起来。这种映射允许操作系统跟踪每个进程打开的文件,确保文件访问的安全性和有效性。

如何建立进程与文件之间的关联?

当程序开始执行时,操作系统负责将程序的代码和数据载入内存,并为该程序创建必要的数据结构,如task_structmm_struct和页表等,以建立虚拟地址到物理地址的映射。

task_struct结构体中,存在一个指向files_struct的指针。files_struct内部包含一个名为fd_array的指针数组,数组的索引对应于文件描述符。

以打开log.txt文件为例,操作系统首先将文件内容从磁盘读入内存,创建一个struct file结构体,并将此结构体加入到文件管理的双向链表中。随后,将struct file的地址设置到fd_array数组的特定索引(例如3),使得该索引处的指针指向对应的struct file。完成这些步骤后,操作系统将文件描述符返回给请求的进程。

有了文件描述符,进程就能够访问关联的文件信息,并执行如读写等I/O操作。

额外说明:在向文件写入数据时,数据首先被写入文件的内存缓冲区,之后系统会定期将这些缓冲区中的数据写入磁盘。

当提及进程创建时默认打开的文件描述符0、1、2,这指的是:

  • 0:标准输入(stdin),通常关联到键盘。
  • 1:标准输出(stdout),通常关联到显示器。
  • 2:标准错误(stderr),同样关联到显示器。

这些设备,如键盘和显示器,都是硬件设备,操作系统能够识别它们。在进程创建时,操作系统会为这些设备创建相应的struct file结构体,并将这些结构体加入到文件管理的双向链表中。然后,操作系统将这些struct file的地址分别赋值给fd_array数组的索引0、1、2,从而实现了对标准输入、输出和错误流的默认打开。

磁盘文件与内存文件的区别是什么?

文件存储在硬盘上时被称为磁盘文件 ,而一旦这些文件被读入到计算机的内存中,它们就被称为内存文件。这两者之间的关系可以类比于程序与进程的关系:程序在执行时转变为进程,同样,磁盘文件在被加载到内存中后转变为内存文件。

磁盘文件由两部分组成:

  1. 文件内容:即文件中存储的数据本身。
  2. 文件属性 :包括文件的基本信息,如文件名、大小、创建时间等,这些信息也被称为元数据

在文件被加载到内存的过程中,通常首先加载的是文件的元数据。当需要进行文件内容的读取或其他I/O操作时,文件数据会根据需要被延迟加载到内存中。这种机制有助于优化资源使用,尤其是在处理大型文件时。

文件描述符的分配规则

如果你尝试依次打开五个文件,你将观察到每个文件获取的文件描述符是连续递增的,且从数字3开始。

#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;
}

这是因为文件描述符本质上是文件数组索引的表示,而当一个进程被创建时,它已经默认打开了三个文件:标准输入(stdin,文件描述符0)、标准输出(stdout,文件描述符1)和标准错误(stderr,文件描述符2)。这三个位置已经被占用,因此新的文件描述符从下一个可用的索引,即3开始分配。随着更多文件的打开,文件描述符会依次递增。

当你在打开五个文件之前关闭了特定的文件描述符,文件描述符的分配情况会相应地变化。以下是详细说明:

  1. 关闭文件描述符0

    • 执行close(0);后,文件描述符0被释放。
    • 此时,当你尝试打开一个新的文件,该文件将获取到文件描述符0,因为0是当前可用的最小文件描述符。
    • 接下来打开的文件将从下一个最小的未使用文件描述符开始分配,即3。
  2. 关闭文件描述符0和2

    • 执行close(0);close(2);后,文件描述符0和2被释放。
    • 打开的第一个文件将获取文件描述符0,因为它是当前可用的最小文件描述符。
    • 打开的第二个文件将获取文件描述符2,因为这是下一个最小的可用文件描述符。
    • 从第三个文件开始,文件描述符将从3开始依次递增,因为0和2已经被占用。

结论 :文件描述符的分配是从当前可用的最小文件描述符开始的。操作系统会检查fd_array数组,找到第一个未使用的最小下标,并将其分配给新打开的文件。这种机制确保了文件描述符的有效利用,同时避免了文件描述符的浪费。

重定向

重定向原理

重定向的本质就是修改文件描述符下标对应的struct file*的内容。

输出重定向原理:

输出重定向是一种将程序的标准输出(通常是屏幕)重定向到一个文件或其他位置的技术。这样做可以让你将数据保存到文件中,而不是直接显示在屏幕上。

例如 ,如果你希望将原本显示在屏幕上的信息保存到名为log.txt的文件中,你可以采取以下步骤:

  1. 关闭标准输出:首先,你需要关闭与标准输出(通常是显示器)关联的文件描述符,这通常是文件描述符1。

  2. 打开目标文件 :接着,打开log.txt文件。由于你已经关闭了标准输出,打开log.txt时,它将被分配文件描述符1。

    #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_CREAT, 0666);
    if (fd < 0){
    perror("open");
    return 1;
    }
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    fflush(stdout);

     close(fd);
     return 0;
    

    }

运行结果后,我们发现显示器上并没有输出数据,对应数据输出到了log.txt文件当中。

说明:

  • printf函数 是 C 语言中用于格式化输出的函数,它默认将数据发送到标准输出流(stdout)。在 C 语言中,标准输出流通常与显示器关联,但可以通过重定向改变其输出目标。
  • stdout 是一个指向 FILE 结构体的指针,这个结构体包含了与文件操作相关的信息,包括一个文件描述符。在大多数操作系统中,stdout 的文件描述符默认为 1,这意味着 printf 函数实际上是向文件描述符为 1 的文件(通常是显示器)输出数据。
  • C语言的缓冲机制:在 C 语言中,输出到标准输出的数据并不是立即写入到操作系统的文件中,而是首先被存储在缓冲区中。这样做可以提高效率,因为减少了对操作系统的直接调用次数。但是,这也意味着在某些情况下,如果你不显式地刷新缓冲区,数据可能不会立即出现在屏幕上或被写入到文件中。
  • fflush函数 :为了确保缓冲区中的数据被立即写入到文件或显示在屏幕上,可以使用 fflush 函数。这个函数的调用可以强制清空缓冲区,将其中的数据发送到指定的输出流。这在你需要立即看到输出结果,或者在程序异常退出前确保数据被写入文件时非常有用。

追加重定向原理:

在处理数据输出时,追加重定向和输出重定向的主要区别在于它们对文件内容的处理方式。输出重定向 会将文件中原有的数据覆盖,而追加重定向则会在文件末尾添加新的数据。

例如 ,如果你希望将原本显示在屏幕上的信息保存到log.txt文件中,而不是覆盖文件中的内容,你应该首先关闭文件描述符为1的文件(通常是标准输出),然后以追加模式打开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;
}

运行结果后,我们发现对应数据便追加式输出到了log.txt文件当中。

输入重定向原理:

输入重定向是一种技术,它允许我们将程序原本从标准输入(通常是键盘)读取的数据改为从另一个文件中读取。

例如 ,如果你希望让scanf函数,它通常用于从键盘读取输入,改为从log.txt文件中读取数据,你应该在打开log.txt文件之前关闭文件描述符为0的文件(即标准输入)。这样,当你打开log.txt文件时,它将被分配到文件描述符0,从而使得scanf函数从该文件而不是键盘读取数据。

重点是,通过关闭和重新分配文件描述符,你可以改变程序的输入源。

#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函数将log.txt文件当中的数据都读取出来了。

说明: scanf 函数默认情况下会从标准输入(stdin)中获取数据。在C语言中,stdin 是一个指向 FILE 结构体的指针,该结构体关联了文件描述符0。这意味着,当使用 scanf 函数时,它实际上是从文件描述符编号为0的输入源读取数据,也就是标准输入流。
尽管标准输出流(stdout)和标准错误流(stderr)在大多数情况下都会在显示器上显示,但它们在处理输出时有着本质的不同。 有什么区别呢?

#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;
}

在直接运行这段程序时,你将在显示器上看到四行输出。然而,当你尝试将程序的输出重定向到一个文件,比如 log.txt,你会发现只有标准输出流的内容被写入文件,而标准错误流的内容仍然显示在显示器上。

这是因为在进行输出重定向时,通常只重定向了文件描述符为1的stdout,而文件描述符为2的stderr并不受影响。这意味着,即使程序的输出被重定向到了文件,错误信息仍然会被发送到原始的目的地,即显示器,以便用户能够立即注意到。

重点:

  • 标准输出流(stdout):用于程序的正常输出,可以通过重定向写入文件。
  • 标准错误流(stderr):用于错误和警告信息的输出,通常不会被重定向,直接显示在显示器上。

dup2

要实现输出重定向,我们可以通过操作文件描述符数组(如 fd_array)来实现。具体来说,可以通过拷贝数组中的元素来改变标准输出流(stdout)的指向。

例如,如果我们想要将标准输出重定向到一个文件(如 log.txt),我们可以将文件描述符数组 fd_array 的特定索引(假设为3)的内容复制到另一个索引(如1,即stdout对应的索引)。这样,原本向标准输出流发送的数据就会被重定向到指定的文件中。

在Linux操作系统中,dup2 系统调用提供了一种方便的方法来实现输出重定向。dup2 函数的原型定义如下:

int dup2(int oldfd, int newfd);

函数功能: dup2 函数将文件描述符数组 fd_array 中索引为 oldfd 的内容复制到索引为 newfd 的位置。在执行此操作之前,如果需要,应先关闭文件描述符为 newfd 的文件。

函数返回值: 如果 dup2 调用成功,它将返回 newfd;如果调用失败,则返回 -1

在使用 dup2 时,需要特别注意以下两点:

  1. 如果 oldfd 不是一个有效的文件描述符,dup2 调用将失败,并且不会关闭文件描述符为 newfd 的文件。
  2. 如果 oldfd 是一个有效的文件描述符,但 newfdoldfd 的值相同,则 dup2 不执行任何操作,并直接返回 newfd

例如,如果我们将打开 log.txt 文件时获得的文件描述符与数字1(即标准输出流stdout的文件描述符)一起传递给 dup2 函数,那么 dup2 将把 fd_array[fd] 的内容复制到 fd_array[1] 中。在代码中,我们向 stdout 输出数据,而 stdout 是向文件描述符为1的文件输出数据。因此,原本应该显示在显示器上的数据将被重定向输出到 log.txt 文件中。

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

代码运行后,我们即可发现数据被输出到了log.txt文件当中。

FILE

FILE的文件描述符

库函数通常作为系统调用的抽象层,使得文件访问等操作更加方便和安全。在C语言中,所有对文件的访问操作实际上是通过文件描述符(fd)来实现的。因此,C语言标准库中的 FILE 结构体内部封装了文件描述符。

在C标准库的实现中,FILE 结构体实际上是 struct _IO_FILE 的别名。这一点可以在 /usr/include/stdio.h 头文件中找到证据:

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
};

现在再让我们深入探讨C语言中 fopen 函数的工作机制。

当调用 fopen 函数时,它在上层为用户分配一个 FILE 结构体变量,并返回这个结构体的指针(即 FILE*)。在底层,fopen 通过系统调用 open 来打开指定的文件,并获取相应的文件描述符(fd)。随后,这个文件描述符被存储在 FILE 结构体的 _fileno 成员变量中,从而完成文件的打开过程。

FILE *fopen(const char *filename, const char *mode);

函数功能:

  • filename:指定要打开的文件的路径。
  • mode:指定文件打开的模式(如 "r" 表示只读,"w" 表示只写等)。

其他文件操作函数: 在C语言中,其他文件操作函数,如 freadfwritefputsfgets 等,都是基于 FILE 结构体进行操作的。这些函数的工作流程通常如下:

  1. 根据传入的文件指针找到对应的 FILE 结构体。
  2. FILE 结构体中获取文件描述符。
  3. 通过文件描述符对文件进行读取、写入或其他操作。

FILE的缓冲区

让我们分析下面这段代码,它演示了如何使用C库函数和系统调用向显示器输出内容,并在最后调用了 fork 函数。

#include <stdio.h>
#include <unistd.h>

int main() {
    // 使用C库函数
    printf("hello printf\n");
    fputs("hello fputs\n", stdout);
    // 使用系统调用
    write(STDOUT_FILENO, "hello write\n", 12);
    // 创建子进程
    if (fork() == 0) {
        // 子进程代码
    } else {
        // 父进程代码
    }
    return 0;
}

在这段代码中,printffputs 都是C标准库提供的函数,用于向标准输出流(stdout)输出字符串。write 系统调用则直接向文件描述符为1的文件(即stdout)写入数据。这些函数和系统调用都成功地将内容输出到了显示器上。

然而,当我们尝试将程序的输出重定向到 log.txt 文件时,我们可能会发现文件中的内容与直接在显示器上看到的输出有所不同。这是因为重定向操作可能只对某些函数有效,而对其他函数或系统调用无效。

要理解为什么在重定向输出到文件时,C库函数的输出内容会出现两份,而系统调用的输出内容保持不变,

我们需要先了解缓冲机制的三种类型:

  1. 无缓冲:数据直接写入目标,不经过任何缓冲。
  2. 行缓冲 :数据在每次遇到换行符(\n)时刷新到目标,通常用于控制台输出。
  3. 全缓冲 :数据在缓冲区填满或特定条件下(如调用 fflush)才刷新到目标,通常用于文件写入。

在直接运行程序并将输出打印到控制台时,printffputs 函数使用的是行缓冲。由于代码中的每条输出都以换行符结束,数据会立即刷新到控制台。

但是,当输出被重定向到文件 log.txt 时,C库函数的缓冲策略变为全缓冲。这意味着 printffputs 函数的输出首先被存储在C库的缓冲区中。当程序执行 fork 函数创建子进程时,由于进程间内存空间是隔离的,C库的缓冲区也会被复制一份给子进程。当父进程或子进程最终刷新缓冲区内容时,由于写时复制(copy-on-write)机制,缓冲区中的数据会被复制两份:一份属于父进程,一份属于子进程。这就是为什么重定向到文件后,printffputs 的输出内容会出现两份。

与此相反,write 系统调用直接将数据写入文件描述符,不经过C库的缓冲区,因此它不受进程创建和缓冲区复制的影响,输出内容保持一份。

缓冲区由谁提供?

缓冲区的提供者是C语言标准库,而非操作系统。C语言标准库为文件操作提供了缓冲机制,这是为了提高文件I/O操作的效率。在C语言中,printffputs 等函数使用标准库提供的缓冲区来暂存输出数据,而不是直接写入文件描述符。

缓冲区在哪?

当我们讨论 printf 函数或其他类似的C库函数时,它们通常将数据输出到 stdoutstdout 是一个指向 FILE 结构体的指针,而这个 FILE 结构体内部包含了用于管理输出缓冲区的成员。

缓冲区的位置:

  • FILE结构体 :在C语言中,FILE 结构体不仅保存了文件描述符,还维护了与用户缓冲区相关的信息。这意味着,当使用 printf 或其他C库函数进行输出时,数据首先被写入到由 FILE 结构体管理的缓冲区中。

FILE结构体的成员:

  • 文件描述符FILE 结构体中的 _fileno 成员变量存储了与文件关联的文件描述符。
  • 缓冲区信息FILE 结构体还包含了指向缓冲区的指针,以及记录缓冲区状态的其他成员变量,如缓冲区的大小、当前位置等。

这种设计使得C语言的文件I/O操作更加高效,因为数据可以先在内存中的缓冲区累积,然后在适当的时机(如缓冲区满或遇到换行符时)一次性写入文件描述符,减少了对磁盘或终端的直接I/O操作次数。

操作系统有缓冲区吗?

操作系统确实提供了自己的缓冲区机制,这是为了优化硬件资源的使用并提高系统的整体性能。当应用程序通过用户缓冲区(如C语言中的 FILE 结构体所维护的缓冲区)输出数据时,这些数据并不是直接写入到硬件设备,如磁盘或显示器,而是首先被发送到操作系统的缓冲区。

操作系统缓冲区的作用:

  • 数据暂存:操作系统的缓冲区作为中间层,暂存从用户缓冲区来的数据。
  • 硬件交互:操作系统负责管理硬件资源,包括决定何时将缓冲区中的数据写入到具体的硬件设备。

操作系统的刷新机制:

  • 自动刷新:操作系统有自己的刷新机制,它会根据系统的性能和资源使用情况来决定何时将数据从操作系统缓冲区刷新到硬件设备。
  • 用户不干预:通常,应用程序开发者不需要关心操作系统缓冲区的具体刷新规则,这些由操作系统自动管理。

层状结构图: 在现代操作系统中,通常存在一个层状结构,其中用户空间的应用程序通过系统调用与内核空间交互。数据从用户区流向具体的外设,必须经过操作系统内核的缓冲区。这个流程可以简化为以下几个步骤:

  1. 应用程序将数据写入用户缓冲区。
  2. 用户缓冲区的数据被刷新到操作系统缓冲区。
  3. 操作系统缓冲区将数据传输到硬件设备。

文件系统

在计算机系统中,文件主要分为两大类:磁盘文件和内存文件。我们已经讨论了内存文件的相关内容,现在让我们深入了解磁盘文件。

初识inode

磁盘文件由两大部分构成:文件内容和文件属性。

这两部分共同定义了文件的结构和特性。

  1. 文件内容:这是文件存储的主体数据,可以是文本、图片、程序代码等。文件内容是用户实际需要保存和处理的信息。

  2. 文件属性:也称为元数据,包含了描述文件特征的基本信息。这些信息虽然不直接构成文件的数据内容,但对于文件的管理、检索和使用至关重要。常见的文件属性包括:

    • 文件名:用于标识和区分文件的名称。
    • 文件大小:文件内容所占存储空间的大小。
    • 创建时间:文件被创建的时间戳。
    • 修改时间:文件内容上次被修改的时间戳。
    • 访问权限:定义了谁可以读取、写入或执行文件。
    • 所有者信息:文件的所有者或创建者。

命令行当中输入ls -l,即可显示当前目录下各文件的属性信息。

在Linux操作系统中,文件的元数据和数据内容是分开存储的。这种设计允许系统更有效地管理文件,并提高文件操作的性能。文件的元数据,也就是文件的属性集合,被存储在一个名为inode的结构中。

inode的作用:

  • 属性集合:inode包含了文件的元数据,如文件大小、权限、所有者、创建和修改时间等。
  • 唯一标识:每个inode都有一个唯一的编号,称为inode号。这个编号用于在文件系统中唯一标识一个文件,即使两个文件具有相同的文件名,它们的inode号也不同。

inode的重要性:

  • 文件管理:inode使得文件系统能够有效地管理和检索文件,即使在文件数量庞大的情况下。
  • 数据保护:通过inode,文件系统可以在文件名变更或移动的情况下保持文件数据的完整性。

inode的编号:

  • 唯一性:每个inode都有一个独一无二的编号,这个编号在整个文件系统中是唯一的。
  • 索引功能:inode编号作为索引,帮助文件系统快速定位到文件的元数据。

inode与文件内容的关系:

  • 分离存储:文件的内容和inode存储在不同的位置。文件内容通常存储在磁盘上的数据块中,而inode存储在文件系统的inode表中。
  • 链接关系:文件名实际上是指向对应inode的指针。通过inode,系统可以访问到文件的内容。
    注意: 无论是文件内容还是文件属性,它们都是存储在磁盘当中的。

磁盘的概念

磁盘作为计算机系统中的一种关键永久性存储介质,承担着数据长期保存的角色。它与内存形成鲜明对比,因为内存是一种易失性存储介质,一旦电源关闭,存储在内存中的数据就会丢失。

磁盘的特性:

  • 永久性存储:磁盘能够长期保存数据,即使在断电的情况下也能保持数据不丢失。
  • 机械设备:在现代计算机中,磁盘是为数不多的机械设备之一,它的读写操作依赖于物理机械运动。

内存与磁盘的对比:

  • 易失性:与磁盘不同,内存是易失性的,这意味着电源断开后,内存中的数据会丢失。
  • 速度:内存的访问速度通常比磁盘快得多,因为它不需要物理机械运动来读写数据。

磁盘在冯·诺依曼体系结构中的角色:

  • 输入/输出设备:在冯·诺依曼体系结构中,磁盘既可以作为输入设备,将数据读入到计算机的内存中,也可以作为输出设备,将数据从内存写入到磁盘上。
  • 数据处理:磁盘是计算机处理数据的重要环节,它允许用户存储和检索大量数据。

磁盘寻找方案

在进行磁盘读写操作时,需要通过一系列精确的定位步骤来找到数据存储的确切位置。这个过程通常包括以下几个关键步骤:

确定盘面(磁头选择)

首先,需要确定数据位于磁盘的哪个盘面上。磁盘由多个盘面组成,每个盘面都有其对应的读写磁头。

确定柱面(磁道定位)

接下来,要确定数据位于盘面的哪个柱面。柱面是所有盘面上相同位置的磁道组成的虚拟圆柱体,是数据存储的纵向位置。

确定扇区(数据块定位)

最后,需要精确到扇区级别,即确定数据位于柱面内的具体扇区。扇区是磁盘存储的最基本单位,每个扇区存储固定大小的数据。

磁盘分区与格式化介绍

要深入理解文件系统,我们可以将磁盘视为一种线性存储介质。想象一下磁带,当它被卷起时,磁带是圆形的,类似于磁盘的形状。但如果我们将磁带拉直,它就变成了一条线性的带子,这有助于我们理解磁盘数据存储的连续性。

磁盘分区的概念:

  • 块设备:磁盘通常被视为块设备,数据以块或扇区为单位存储,每个扇区通常大小为512字节或更大。
  • 扇区划分:以一个512GB的磁盘为例,如果每个扇区大小为512字节,那么这个磁盘可以被划分为超过十亿个扇区。

磁盘分区的目的:

  • 管理效率:计算机通过分区来更有效地管理磁盘空间。分区是使用分区工具在物理磁盘上创建的逻辑分割。
  • 数据组织:一旦磁盘被分区,不同的文件和目录可以存储在不同的分区中,这有助于根据文件的类型和用途进行更细致的组织。

分区的实践:

  • 文件系统分区:在Windows操作系统中,常见的做法是将磁盘分为C盘和D盘等,每个分区都有自己的文件系统,并且可以独立管理。
  • 文件管理:分区允许用户根据文件的性质和使用频率,将它们存储在不同的位置,从而优化存取速度和数据安全性。

在Linux操作系统中,我们也可以通过以下命令查看我们磁盘的分区信息:

在磁盘分区完成后,接下来的步骤是对磁盘进行格式化。格式化是初始化磁盘分区的过程,它为磁盘分区设置必要的系统文件和结构,使其能够被操作系统识别和使用。值得注意的是,格式化操作会清除分区上现有的所有数据。

格式化的目的:

  • 初始化分区:格式化为分区设置文件系统所需的基础结构,包括文件分配表、目录结构等。
  • 数据清除:格式化过程中,分区上原有的数据会被彻底清除,以确保新文件系统的正确建立。

文件系统的作用:

  • 管理信息:文件系统决定了格式化时写入分区的管理信息类型。这些信息对于文件的存储、检索和管理至关重要。
  • 系统差异:不同的文件系统在格式化时会写入不同的管理信息。例如,EXT2、EXT3、XFS 是Linux系统常用的文件系统,而NTFS是Windows系统常用的文件系统。

格式化过程:

  1. 低级格式化:这是最接近硬件层面的格式化,通常由磁盘制造商完成,涉及设置磁盘的物理特性。
  2. 分区表创建:在磁盘上创建分区表,定义每个分区的大小和位置。
  3. 文件系统创建:在分区上创建文件系统,包括初始化文件分配表、目录结构等。

常见文件系统:

  • EXT2/EXT3:Linux系统中广泛使用的文件系统,支持大文件和长文件名。
  • XFS:高性能的文件系统,适用于大型文件系统和高负载系统。
  • NTFS:Windows系统中广泛使用的文件系统,支持文件加密和压缩等高级特性。

EXT2文件系统的存储方案

为了提高磁盘管理的效率,计算机系统通常会对磁盘进行分区。每个分区的开始部分都包含一个启动块(Boot Block),这是用于系统启动的重要区域。在EXT2文件系统中,除了启动块之外,分区的剩余空间被划分为多个块组(Block Group),每个块组负责存储文件系统的一部分数据。

块组的特点:

  • 启动块:固定大小,用于存储启动加载程序。
  • 块组大小:在格式化时确定,一旦设置则不可更改。

块组的内部结构: 每个块组内部都包含以下关键组件,它们共同维护文件系统的运行:

  1. 超级块(Super Block)

    • 存储文件系统的全局信息,如总数据块和inode数量、未使用的数据块和inode数量、数据块和inode的大小等。
    • 记录文件系统的挂载时间、最后写入时间和最后检查时间等。
    • 如果超级块损坏,整个文件系统的结构和数据可能会丢失。
  2. 块组描述符表(Group Descriptor Table)

    • 描述每个块组的属性,如块组内的空闲和已用数据块数量。
  3. 块位图(Block Bitmap)

    • 记录数据块的使用情况,标记哪些数据块被占用,哪些是空闲的。
  4. inode位图(inode Bitmap)

    • 记录inode的使用情况,标记哪些inode被分配给文件,哪些是空闲的。
  5. inode表(inode Table)

    • 存储文件的属性信息,如文件的权限、所有者、大小、创建时间等。
  6. 数据块(Data Blocks)

    • 存储文件的实际内容。

软硬链接

软链接

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

ln -s myproc myproc-s

通过ls -i -l命令我们可以看到,软链接文件的inode号与源文件的inode号是不同的,并且软链接文件的大小比源文件的大小要小得多。

软链接,也称为符号链接,是文件系统中的一种特殊类型的文件。它与源文件相比具有独立性,拥有自己的inode编号,但实质上它仅存储了对源文件路径的引用。

软链接的特点:

  1. 独立性:软链接文件在文件系统中是作为一个独立的文件存在的,它有自己的inode和元数据。
  2. 大小差异:由于软链接文件仅包含对目标文件的路径引用,因此它的大小远小于源文件。
  3. 类似快捷方式:在功能上,软链接类似于Windows操作系统中的快捷方式,提供了对源文件的间接访问。

软链接的工作方式:

  • 当访问软链接文件时,文件系统会解析链接中存储的路径,并定位到源文件,然后进行相应的文件操作。

软链接的用途:

  • 简化文件路径:通过创建软链接,可以简化复杂或深层的文件路径,便于访问。
  • 跨文件系统链接:软链接可以跨越不同的文件系统,链接到其他位置的文件。
  • 版本控制:在软件版本更新时,可以使用软链接来切换不同版本的文件。

软链接(符号链接)提供了一种便捷的文件引用方式,但它仅仅是对源文件的一个指针。这意味着软链接文件并不包含源文件的数据,而只是存储了源文件的路径信息。

硬链接

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

ln myproc myproc-h

在Linux系统中,使用 ls -i -l 命令可以查看文件的inode信息以及详细列表。这个命令揭示了硬链接文件与源文件之间的一些关键关系:

  1. 相同的inode号:硬链接文件和它所链接的源文件共享同一个inode号。这意味着它们在文件系统中指向同一个物理位置。

  2. 相同的文件大小:由于硬链接文件和源文件实际上是同一个文件的不同引用,它们的大小自然是相同的。

  3. 硬链接数的增加:创建硬链接时,文件的硬链接计数会增加。例如,如果一个文件原本只有一个名称(硬链接数为1),创建一个硬链接后,硬链接数变为2。

硬链接的本质

  • 别名:硬链接可以视为源文件的一个别名。在文件系统中,一个inode可以有多个文件名指向它,每个文件名都被视为一个硬链接。

硬链接数的含义

  • 计数 :一个文件的硬链接数实际上就是有多少个不同的文件名指向这个inode。例如,如果一个文件有两个文件名(如 myprocmyproc-h),则该文件的硬链接数为2。

重点

  • inode共享:硬链接文件和源文件共享相同的inode号。
  • 大小一致:硬链接文件的大小与源文件相同。
  • 硬链接计数:硬链接数反映了指向同一inode的不同文件名的数量。

硬链接提供了一种在文件系统中引用相同数据的多个路径的方式,这对于文件管理和数据备份非常有用。然而,需要注意的是,硬链接不能跨越不同的文件系统,且不支持对目录的链接。

区别

在文件系统中,软链接(符号链接)和硬链接是两种不同的文件引用方式,它们在实现机制和使用场景上有所区别。

  1. 软链接(符号链接)

    • 独立性:软链接是一个独立的文件,拥有自己的inode和文件元数据。
    • 路径引用:软链接文件的内容是对目标文件路径的引用,类似于Windows中的快捷方式。
    • 灵活性:软链接可以跨越不同的文件系统,可以指向文件或目录,甚至可以指向不存在的文件(称为"死链接")。
  2. 硬链接

    • 非独立性:硬链接并不是一个独立的文件实体,它没有自己的inode,而是直接与源文件共享同一个inode。
    • 别名:硬链接实际上是为已存在的文件创建了一个别名或额外的文件名,它们指向同一个inode,因此访问的是相同的文件数据。
    • 限制:硬链接不能跨越文件系统,也不能用于目录,只能用于已经存在的文件。
      软硬链接的比较
  • inode:软链接有独立的inode,而硬链接与源文件共享inode。
  • 创建方式:软链接创建的是一个新的文件,它包含对目标文件的路径引用;硬链接则是在文件系统中为现有文件创建一个新的引用名称。
  • 删除行为:删除软链接不会影响源文件,因为它们是独立的;删除硬链接实际上只是减少了文件的链接计数,只有当链接计数降至0时,文件数据才会被删除。
  • 目录支持:软链接可以指向目录,而硬链接不能。

文件的三个时间

在Linux当中,我们可以使用命令stat 文件名来查看对应文件的信息。

stat 命令是一个强大的工具,它可以显示文件的详细状态信息,包括文件的三种关键时间信息:

  1. Access Time (atime):文件最后一次被访问的时间。
  2. Modify Time (mtime):文件内容最后一次被修改的时间。
  3. Change Time (ctime):文件的元数据(如权限、所有者等)最后一次被修改的时间。

时间信息的行为:

  • 当文件内容被修改时,通常会更新Modify时间。如果文件大小发生变化,这通常意味着内容已被修改。
  • Change时间记录的是文件属性的更改,如权限或所有权的变更,并不一定涉及文件内容的更改。

使用touch命令更新时间:

  • touch 命令可以用来更新文件的Access和Modify时间。当文件已存在时,使用touch命令会将Access和Modify时间设置为当前时间。
  • 如果需要同时更新Change时间,可以使用touch命令的-c--no-create选项。

touch命令的作用:

  • 当文件不存在时,touch命令会创建一个新文件。
  • 当文件已存在时,touch命令会更新文件的时间戳。

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

相关推荐
WZF-Sang1 小时前
Linux权限理解【Shell的理解】【linux权限的概念、管理、切换】【粘滞位理解】
linux·运维·服务器·开发语言·学习
小橞1 小时前
Linux搭建简易路由转发
linux·运维·服务器
嵌入式DZC1 小时前
搭建内网文件服务器(FTP),以及实现内网Gitee
运维·服务器
robot_大菜鸟1 小时前
linux-L7-linux 查看json文件
linux·运维
Flying_Fish_roe1 小时前
linux-网络管理-网络抓包
linux·网络·php
小技与小术3 小时前
lvs-nat模式实验详解
linux·运维·服务器·网络·lvs
FreeBuf_3 小时前
SSHamble:一款针对SSH技术安全的研究与分析工具
运维·安全·ssh
繁依Fanyi3 小时前
828华为云征文|华为Flexus云服务器搭建OnlyOffice私有化在线办公套件
服务器·开发语言·前端·python·算法·华为·华为云
py.鸽鸽4 小时前
Linux2-mkdir,touch,cat,more
linux
aabbcc456aa4 小时前
ubuntu安装mysql 8.0忘记root初始密码,如何重新修改密码
linux·mysql·ubuntu