目录
1.回顾一下文件
2.理解文件
下面就是系统调用的文件操作
文件描述符fd,fd的本质是什么?
读写文件与内核级缓存区的关系
据上理论我们就可以知道:open在干什么
3.理解Linux一切皆文件
4.C语言中的FILE*
1.回顾一下文件
先来段代码回顾C文件接口
写文件
#include <stdio.h> #include <string.h> int main() { FILE* fp = fopen("myfile", "w"); if (!fp) { printf("fopen error!\n"); } const char* msg = "hello\n"; int count = 5; while (count--) { fwrite(msg, strlen(msg), 1, fp); } fclose(fp); return 0; }
读文件#include <stdio.h> #include <string.h> int main() { FILE* fp = fopen("myfile", "r"); if (!fp) { printf("fopen error!\n"); } char buf[1024]; const char* msg = "hello bit!\n"; while (1) { //注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明 size_t s = fread(buf, 1, strlen(msg), fp); if (s > 0) { buf[s] = 0; printf("%s", buf); } if (feof(fp)) { break; } } fclose(fp); return 0; }
输出信息到显示器,你有哪些方法#include <stdio.h> #include <string.h> int main() { const char* msg = "hello fwrite\n"; fwrite(msg, strlen(msg), 1, stdout); printf("hello printf\n"); fprintf(stdout, "hello fprintf\n"); return 0; }
stdin & stdout & stderrC 默认会打开三个输入输出流,分别是 stdin, stdout, stderr。三个流的类型都是 FILE*, fopen 返回值类型,文件指针。打开文件时,需要指定一个打开模式,它决定了程序对文件的访问方式。常见的打开模式包括:
- 只读模式:只允许读取文件内容。
- 只写模式:只允许写入文件内容,通常会清空文件原有内容。
- 追加模式:在文件末尾添加内容,而不会覆盖原有内容。
- 读写模式:既允许读取也允许写入文件内容。
例如在w(只写)操作下:1.如果文件不存在,就在当前的路径下,新建指定的文件 2.文件存在,默认打开文件的时候,就会把文件清空!
这个特性与Linux中的输出重定向(>)是非常相似的,所有输出重定向一定是文件操作!
2.理解文件
结合之前所学的,我们基本知道:文件=属性+内容
1.打开文件,本质其实是进程打开文件。2.文件没有被打开的时候,存放在硬盘中。3.进程是可以一次性打开很多文件的。4.系统中可以存在很多的进程。
因此,在很多情况下,OS内部一定存在大量被打开的文件,OS肯定是需要把它们管理起来的。我们类比进程,OS通过先描述,再组织的方式,用PCB将每个进程都管理起来,那么每打开一个文件,在OS内部,一定存在对应描述文件的结构体,类似PCB,将每个文件都管理起来。以上就是我们目前对文件的全部认识了。
a.操作文件,本质:是将文件的相关属性加载到内存中,并为程序提供对文件的访问接口,以便进行后续的读写操作。这一过程涉及多个步骤和概念,需要深入理解才能确保文件的正确访问和数据的完整性。是进程和文件的关系。
b.文件储存在硬盘中(硬盘是外设,是硬件),向文件中写入本质是向硬件中写入,OS是硬件的管理者,用户是没有权限直接写入的,用户必须要通过OS写入,OS必须为我们提供系统调用(OS不相信任何人),因此我们在c/c++/(其它语言)对文件操作的函数,其实都是对系统调用接口的封装!
下面就是系统调用的文件操作
open():此系统调用用于打开或创建文件。它接受文件路径、打开标志(如只读、只写、读写等)和文件权限等参数,并返回一个文件描述符,该描述符用于后续的文件操作。如果文件成功打开,则返回的文件描述符是一个非负整数;否则,返回-1表示错误。
pathname
参数指定了要打开或创建的文件的路径名。flags
参数用于设置打开文件的方式和属性,比如是否读写、是否创建新文件、是否追加等。常见的flags
包括O_RDONLY
(只读打开)、O_WRONLY
(只写打开)、O_RDWR
(读写打开)、O_CREAT
(如果文件不存在则创建)、O_TRUNC
(如果文件存在并且以写方式打开,则将其长度截断为0),O_APPEND(
每次写入都追加到文件的末尾)
等。这些标志可以通过按位或运算符(|
)组合使用。这里的参数并不是表示int类型,而是32个比特位,用比特位来进行标志位的传递(位图)。这是OS设计很多系统调用接口的常用方法(下面是个简单的示例)运行结果:这就实现了向一个参数传递多种标记位的功能
mode
参数仅在创建新文件时使用,用于指定新文件的权限。它通常与flags
中的O_CREAT
一起使用,以设置新文件的访问权限。我们先来使用一下第一个函数:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("log.txt", O_WRONLY | O_CREAT); if (fd < 0) { perror("open"); return 1; } }
O_WRONLY
:表示文件以只写模式打开。O_CREAT
:表示没有这个文件,就创建这个文件运行结果:log.txt被成功创建(我们可以看到这个文件的权限是有问题的,是乱码),这是因为在Linux中新建一个文件,就要告诉OS这个文件的其实权限是什么 ,这就是第三个参数,mode做的事情
我们加上权限
运行结果:这时就符合我们权限要求了。
但是,请注意,文件的实际权限可能受到运行程序的用户的umask(用户文件创建掩码)的影响。umask是一个权限位掩码,它会从
open
函数中的mode
参数中“减去”相应的权限位,以确定新文件的实际权限。例如,如果umask设置为
0002
,那么新文件的权限将会是0666 & ~0002
,即0664
,意味着所有者有读/写权限,组用户和其他用户只有读权限。这个权限就是0664了为了取消umask的影响,OS为我们提供了系统调用接口,在程序运行的时候可以动态的调整当前进程的umask值
修改后代码:
运行结果:这时候就符合预期了。
向文件写入:
关闭文件:
eg:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("log.txt", O_WRONLY | O_APPEND); if (fd < 0) { perror("open"); return 1; } const char* m = "hello!\n"; write(fd, m, strlen(m)); close(fd); return 0; }
运行结果:成功的写入了
我们重新写入一些内容:
重新编译运行后:
结果为什么是这样的呢?为什么老的内容没有被清空,而是接着从头开始覆盖呢?如果想写入的时候,清空原来文件的内容,在打开文件的时候就要加上
O_TRUNC了。
再重新编译运行:发现达到我们的预期了
如果想在文件末尾追加,不覆盖再打开文件的时候,加上
O_APPEND。(就不做演示了)
在Linux系统中,当
open
函数成功打开一个文件或设备时,它返回的文件描述符是一个正整数。这个正整数的起始值是从3开始的。为什么是从3开始呢?这主要基于以下几个原因:
标准输入、输出和错误流:在Unix和Linux系统中,进程默认拥有三个打开的文件描述符:
0
:标准输入(stdin)键盘1
:标准输出(stdout)显示器2
:标准错误(stderr)显示器
这三个文件描述符是进程创建时自动打开的,用于基本的输入、输出和错误处理。因此,我们是可以直接向1里面输入,就直接打印在显示器上:
运行结果:
我们知道这些整数都表示文件描述符(fd),为什么往这些整数里写,就可以向文件里写呢?
文件描述符fd,fd的本质是什么?
文件储存在磁盘中,OS需要从磁盘中获取。
struct file
是内核中用于表示已打开文件的结构体。当进程打开一个文件时,内核会创建一个file
结构体实例来描述该文件。我们还知道文件=内容+属性,文件的属性会用来初始化struct file结构体
文件内核级缓存:
Linux内核为了提高文件系统的读写性能,会使用一部分内存作为缓存区。struct file中有一个指针指向内核级缓存,文件的内容会写到内核级缓存中,
然后再根据需要从缓存中写入磁盘或从磁盘读取到缓存中。这种缓存机制可以减少CPU上下文的切换和堆栈调用,提高磁盘访问效率。我们知道进程是可以打开多个文件的,进程和文件的关系是 1:N的。为了能够支持进程和文件产生关联,
操作系统在自己的PCB(task_struct)中存在一个属性
struct files_struct* files,他会指向struct files_struct.
struct files_struct中存在一个指针数组
struct file* fd_array,
其元素是指向file
结构体的指针。每个file
结构体代表内核中一个已打开的文件对象。文件描述符(fd)就是该数组的下标,进程可以通过这个下标找到文件的描述信息,进而操作文件。因此fd的本质就是:文件映射关系的数组的下标
读写文件与内核级缓存区的关系
写文件与内核级缓存的关系
当进程执行写文件操作时,数据并不会直接写入磁盘,而是首先写入内核的缓存区。这种缓存机制被称为“回写缓存”。内核会将写请求缓存起来,等待合适的时机(例如缓存区满或定时器触发)再将数据一并写入后端磁盘。这样做的好处是减少了磁盘的写操作次数,提高了写入性能,并且减少了CPU的上下文切换和堆栈调用开销。
读文件与内核级缓存的关系
对于读文件操作,当进程需要读取文件数据时,内核会首先检查缓存区中是否已经有该数据。如果缓存区中已经存在所需数据(这通常是由于之前对该数据的读取或写入操作已经将数据缓存到内存中),则内核会直接从缓存中读取数据,而无需从磁盘读取。这种直接从缓存中读取数据的机制称为“缓存命中”,它可以显著提高读取性能。
从这里我们就可以知道:wirte,read函数,本质是拷贝函数
据上理论我们就可以知道:open在干什么
1.创建file
2.开辟文件缓冲区的空间,加载文件数据(延后)
3.查看进程的文件描述符
4.file地址,填入对应的表下标中
5.返回下标
3.理解Linux一切皆文件
通过上面的例子我们可以知道:硬件也是文件,
标准输入、输出和错误流:在Unix和Linux系统中,
0
:标准输入(stdin)键盘
1
:标准输出(stdout)显示器 ,2
:标准错误(stderr)显示器我们都知道,每个文件被打开都有对应的struct file进行管理。通过
struct file
,Linux内核能够以一种统一的方式处理各种不同类型的文件和设备。无论是磁盘文件、网络套接字、字符设备还是块设备,都可以通过相同的接口和数据结构来表示和操作。这种设计使得Linux系统具有高度的可扩展性和灵活性,能够支持各种不同类型的文件系统和设备驱动程序。
struct file
结构体在Linux内核中用于表示一个打开的文件或设备。它包含了一些通用的字段,如文件描述符、文件操作函数指针、文件类型等。这些字段允许内核以统一的方式处理不同类型的文件和设备,尽管它们的底层实现和行为可能完全不同。具体来说,
struct file
中的f_op
字段是一个指向file_operations
结构体的指针,该结构体包含了一组用于操作文件的函数指针。不同的文件系统或设备驱动程序可以提供自己的file_operations
结构体实例,并在其中定义自己的文件操作函数。当用户空间程序通过系统调用来操作文件时,内核会根据struct file
中的f_op
字段来确定应该调用哪个函数来执行相应的操作。这种设计使得Linux内核能够以统一的方式处理各种不同类型的文件和设备,而无需关心它们的底层实现。例如,无论是磁盘文件、网络套接字还是字符设备,它们都可以通过相同的
read()
和write()
系统调用来进行读写操作。内核会根据struct file
中的f_op
字段来确定应该调用哪个函数来执行这些操作,从而实现多态性的效果。从
struct file
的角度来看,Linux的“一切皆文件”体现在将所有类型的资源(包括文件、设备、套接字等)都抽象为文件,并通过统一的struct file
数据结构来表示和操作这些资源。这种设计简化了内核和用户空间之间的交互,提高了系统的可扩展性和可管理性。从底层角度来看,Linux的“一切皆文件”体现在其文件系统的架构和内核如何处理各种资源的方式上。通过VFS(虚拟文件系统)、文件描述符、设备驱动等机制,Linux将各种资源抽象为文件,并通过统一的接口来管理和访问它们,从而简化了系统设计和编程接口的复杂性。
补充:虚拟文件系统(VFS)
- Linux内核通过虚拟文件系统(VFS)这一层来抽象各种实际文件系统(如EXT4、XFS、Btrfs等)的共性,使得上层应用或用户可以通过统一的接口来访问这些文件系统。
- VFS提供了一个通用的文件系统接口,使得内核可以透明地支持多种文件系统,而不必关心底层文件系统的具体实现。
4.C语言中的FILE*
通过前面的学习我们知道:在Linux中,系统访问文件或设备时主要依赖文件描述符(fd)来进行操作。文件描述符(fd)是进程内部用于唯一标识(在Linux中,系统访问文件的时候,只认文件描述符fd)打开的文件或设备的非负整数,它允许系统以一种统一和抽象的方式来处理各种不同类型的文件和设备。通过系统调用和文件描述符表(fd),进程可以与文件或设备进行交互,并执行各种操作。
为什么C语言函数可以直接调用这些操作呢?
原因在于C语言标准库已经为我们封装(一定封装了fd!!)了底层系统调用的细节。
FILE*
类型,它使得C语言程序员能够以一种更直观、更便捷的方式来进行文件操作。
FILE*
是C语言标准库中的一个结构体类型的指针,指向一个结构体(_IO_FILE)
,这个结构体包含了进行文件操作所需的所有信息,比如文件描述符、缓冲区、错误标志等。当你使用C语言标准库函数(如
fopen()
,fclose()
,fread()
,fwrite()
等)来打开、关闭、读写文件时,这些函数会在内部处理文件描述符。具体来说,当你调用fopen()
打开一个文件时,它会调用系统调用来获取一个文件描述符,并将这个文件描述符以及其他相关信息封装在一个_IO_FILE
结构体中,然后返回一个指向这个结构体的指针(即FILE*
)。后续的文件操作(如读写)都会通过这个FILE*
指针来间接地访问和操作文件描述符。所有的C语言上的文件操作函数,本质底层都是对系统调用的封装。
eg:
_fileno
是 glibc 中的一个内部成员,用于存储与FILE *
关联的文件描述符
C语言为什么要用FILE*进行封装?
FILE*
作为标准库的一部分,其接口在不同的平台上是一致的,从而提高了代码的跨平台性。每个平台下使用的库不一样,但是用的函数是一样的,这样就具有跨平台性了。