【Linux系统编程】基础IO第一讲——系统文件IO

【Linux系统编程】基础IO第一讲——系统文件IO 文章目录前言1. 背景知识2. 理解文件2.1 狭义理解2.2 广义理解2.3 系统角度3. 回顾C语言文件IO接口3.1 文件的打开与关闭fopen和fclose3.2 向文件写入内容清空写3.3 向文件写入内容追加写3.4 Linux的输出重定向和追加重定向3.5 读文件4. 补充5. stdin stdout stderr6. 系统文件IO6.1 openflags参数详解位掩码bitmask传参示例演示6.2 close6.3 O_CREAT必须设置权限6.4 write清空写追加写6.5 扩展了解前言C语言的时候我们学习过文件操作学了很多文件操作的库函数。这些库函数我们把它叫做C 语言的文件 I/O 接口。当然其它编程语言诸如C、python也都有自己的文件操作相关的接口但是这都是在语言层面仅凭这些内容我们对文件的理解其实是非常肤浅的。仅仅停留在会用C语言进行一些文件操作甚至这里面的大部分接口我们也都忘记了因为学过去之后就很少再使用它们了。那么接下来的几篇文件我们将深入到系统层面对文件进行一个更深的理解。1. 背景知识之前我们讲过文件内容属性元数据即使我们在磁盘上创建一个空文件没有写入任何内容我们看到它的大小是0kb他也依然要占用磁盘空间因为只要这个文件被创建它就一定具有相应的属性诸如文件名、修改日期、文件类型和文件大小等。那属性也是数据也需要被保存起来。所以对文件的操作无非就两种对内容的操作和对属性的操作。第二点我们要访问一个文件必须先把文件打开所以有了fopen这样的函数。那如何理解文件的“打开”呢要访问一个文件必须先把文件打开所以有了fopen这样的函数打开文件其实就是先把文件加载建立程序与文件数据之间的通道让文件内容在合适的时候能够被加载到内存中具体细节后面详谈到内存而我们访问一个文件无非就是对文件的数据进行增删查改最终是CPU执行我们的代码去完成相应的操作而冯诺依曼体系结构规定了CPU只能直接和内存打交道不能直接访问磁盘所以要访问一个文件必须先把文件加载到内存即打开文件。那没打开的文件呢没有被打开的文件那它就完整躺在磁盘上嘛。下面我们会先讨论被打开的文件磁盘上的文件我们等后面讲文件系统的时候探讨那我们平时说的打开一个文件到底是谁去打开了呢我们应该理解成是进程打开了文件。因为我们打开一个文件其实是我们在代码中使用某些文件操作的接口。最终程序被编译链接形成可执行文件可执行文件被执行变成进程然后CPU执行进程中的指令底层调用系统调用fopen底层封装了系统调用后面会讲然后陷入内核操作系统完成相关工作完成了文件的“打开”。所以是我们启动的进程“打开了文件”。操作系统内核协助完成打开操作但主动发起者是进程那系统中可能同时会有多个进程多个进程也可以同时打开多个文件那这么多被打开的文件要不要被管理起来呢当然如何管理先描述再组织后面详谈所以研究“打开的文件”本质上就是研究进程与文件之间的动态关系(建立、维持和解除后面详谈)。2. 理解文件2.1 狭义理解文件存放在磁盘里磁盘是非易失性存储介质文件在磁盘上可以持久保存磁盘是外设即是输出设备也是输入设备对文件的所有操作本质都是对外设的输入和输出简称 I/O2.2 广义理解Linux下一切皆文件键盘、显示器、网卡、磁盘…后面会讲解如何理解2.3 系统角度文件操作本质是进程对文件的操作文件是静态的只有进程运行中的程序才能发起对文件的读写、打开、关闭等动作。没有进程文件只是磁盘上的数据。我们命令行执行的各种操作文件的命令比如cat查看文件内容或者chmod更改文件权限最终这些命令不也变成了进程嘛。磁盘的管理者是操作系统普通进程不能直接访问磁盘硬件如读特定扇区必须通过操作系统提供的系统调用如 read/write由内核中的磁盘驱动程序配合文件系统来管理磁盘的读写、分配、回收等。文件的读写并不是通过 C/C 的库函数来操作的而是通过文件相关的系统调用接口来实现的库函数只是为了用户使用方便其底层也封装了系统调用C 标准库的 fread/fwrite 等函数最终会调用操作系统提供的系统调用如 read/write。库函数提供了缓冲和便捷接口但底层的真正工作是由系统调用完成再由内核操作磁盘3. 回顾C语言文件IO接口我们这里复习这些接口肯定不会像C语言那篇文章讲的那么详细了想要全面复习大家可以看之前的那篇文章我们这里就挑一些与后面讲的内容有关联的接口和知识点重点复习一下。3.1 文件的打开与关闭fopen和fclosefopen和fclose接口打开文件使用fopen第一个参数接收文件名/路径第二个参数接收打开模式。调用成功返回一个文件指针FILE* 类型写下代码以写方式打开一个文件不存在则新建我们没带路径则默认在当前路径创建。最后不用这个文件了使用fclose接口关闭文件参数接收要关闭文件的文件指针。先写这么多。那我们只写方式打开一个文件然后关闭。目前当前路径下不存在这个文件所以指向目前这段代码应该会在当前所在路径下新建一个名为log.txt的文件写方式打开文件不存在则在当前路径下新建那我们进程调用fopen它如何知道当前路径是哪里呢添加几行代码我们看到程序启动之后就变成了一个进程然后调用fopen函数就在当前目录下创建了log.txt文件。我们之前讲过可以通过 /proc 系统文件夹查看进程信息。/proc 目录是 Linux 系统中的一个特殊目录是一个 虚拟文件系统procfs由内核在内存中动态生成不占用实际磁盘空间提供了有关当前运行进程和内核状态的信息一个进程被创建好操作系统会自动在proc目录下创建一个以新增进程的PID命名的文件夹/目录该目录中包含了当前进程的相关属性信息我们进去其中cwd——Current Working Directory它不就记录了进程的当前路径嘛所以上面的例子中进程调用fopen的时候如果不存在要创建文件就能够知道当前所处的路径。那这样的话如果我们更改进程的当前工作路径那以写方式打开一个不存在的文件它创建的路径就也会随之改变。来试一下我们程序的位置不变现在我们在程序中调用fopen之前我们来更改一下当前工作目录上篇文章我们讲了一个系统调用chdir它可以改变当前进程的工作目录没问题。所以打开文件本质是进程打开。因此即便文件不带路径进程知道自己在哪里CWD。由此fopen以写方式打开文件时候OS就能知道要创建的文件放在哪里。当 fopen / open 使用相对路径如 “data.txt” 或 “…/log/out.txt”时操作系统内核会以 CWD 为起点拼接相对路径从而找到或创建文件。3.2 向文件写入内容清空写上面我们复习了文件的打开和关闭那下面我们可以向打开的文件中写入点内容。当然向文件写入内容也有很多接口我们这里选择一个就行了我们来用下fwrite详细介绍大家自行复习返回实际写入的元素个数不是字节数下面我们来写代码向文件中写入一个字符串hello world。看看效果没问题。如果我们连续多运行几次我们的程序会发现文件的内容一直都是一个hello world字符串。那我现在手动打开文件往里面多写入一下内容然后再来运行我们的程序现在本身就存在这个文件那就不会新建而是直接以写方式打开我们会发现我们目前的写入是一种覆盖式写入之前的内容被清空了。而当前我们使用的打开模式是w翻译将文件截断为零长度已存在或创建文本文件以供写入不存在。流定位在文件的开头即从开头开始写入。那如果现在我们就想专门写一个清空文件的工具怎么写呢很简单以w方式打开文件不写入任何内容任何关闭就把它清空了。看看效果没有问题3.3 向文件写入内容追加写现在修改上面的代码把打开的模式由w改成a然后同样写入一个hello world字符串这次我们再来看运行效果执行前先删除之前的文件成功地写入了hello world那我们再来多执行几次我们发现这次是一个追加式的写入翻译可进行追加操作在文件末尾写入。如果文件不存在则创建该文件。流定位在文件末尾。文件可以看作一个“一维数组”文件的读写位置就是下标3.4 Linux的输出重定向和追加重定向之前我们讲解Linux基本命令的时候我们讲过输出重定向本来我们直接echo “hello world”默认输出到显示器但可以这样这叫做输出重定向重新确定了输出的方向把本应该输出到显示器的信息写入到了log.txt文件中甚至还可以这样这不就把这个文件清空了嘛。那这种输出重定向不就对应我们上面演示的覆盖式地向文件写入内容w模式打开文件当然还有追加重定向这不就对应上面的追加式地向文件中写入内容a模式打开文件3.5 读文件下面我们来自己实现一个cat命令那就读取文件的内容然后打开就行了嘛。读文件当然也有很多接口那我们这里使用fread返回值试一下没问题。4. 补充向显示器打印数据无论什么类型底层都会转换成一个个字符然后输出到显示器。即printf 会将所有数据无论是整数、浮点数、字符串还是指针转换成对应的字符序列然后输出到显示器或其他标准输出设备显示器本身只能显示字符图形所以这种转换是必须的。printf 根据格式字符串如 %d、%f、%s将参数转换为人类可读的字符表示例如整数 123 → 字符 ‘1’、‘2’、‘3’所以printf也叫做格式化输出函数。同样地我们通过键盘输入数据的时候站在我们的角度可能输入了一个整数或者字符串啥的但本质也是一个个的字符。scanf 从标准输入通常是键盘读取字符流根据格式字符串如 %d、%f将这些字符转换为相应的数据类型整数、浮点数等并存入变量。例如int x输入 “123\n”scanf(“%d”, x) 会读取字符 ‘1’、‘2’、‘3’然后将其转换为整数 123scanf叫做格式化输入函数。所以键盘、显示器通常也叫做字符设备5. stdin stdout stderrC语言程序在启动的时候默认打开了3个流C语言文件操作讲过stdin-标准输入流在大多数的环境中从键盘输⼊scanf函数就是从标准输入流中读取数据stdout-标准输出流大多数的环境中输出至显示器printf函数就是将信息输出到标准输出流中。stderr-标准错误流大多数环境中输出到显示器界⾯。其实就是三个文件指针看到它们的类型都是FILE*而在C中cin、cout、cerr分别对应标准输入流对象、标准输出流对象、标准错误流对象结论任何一个程序启动时都会默认打开三个流标准输入、标准输出、标准错误。那为什么呢我们启动的进程基本都是为了完成某项任务利用CPU资源做计算处理数据的。那就需要先拿到数据然后计算完毕还需要输出结果结果有可能正确或者出错。所以打开这三种流就被设定成了一种默认行为。任何一个程序启动时都默认打开三个标准流是为了实现“开箱即用”的 I/O 能力、支持管道与重定向、分离错误信息并且不增加程序员的负担。6. 系统文件IO上面我们提过我们平时说的打开一个文件本质是进程打开了文件主动发起者是进程但是底层必须由操作系统内核协助完成打开操作。文件的读写并不是通过 C/C 的库函数来操作的而是通过文件相关的系统调用接口来实现的库函数只是为了用户使用方便其底层也封装了系统调用即高级语言的文件 I/O 接口底层必定封装了操作系统提供的系统调用接口那下面我们就来学习一下系统的文件IO接口6.1 open第一个系统调用——openopen 是 Unix/Linux 系统中用于打开或创建文件的一个基础系统调用。它返回一个文件描述符供后续的 read、write、lseek、close 等操作使用。有两个版本。参数pathname接收要打开或创建的文件路径flags用来指定文件的打开方式待会我们详细来讲mode用来给新创建的文件指定权限如果我们打开的是一个已经存在的文件使用两个参数的版本即可如果打开的文件不存在使用三个参数的版本给文件指定一个权限。返回值成功返回最小的未使用的文件描述符非负整数。失败返回 -1并设置 errno 以指示错误原因。那什么又是文件描述符呢我们后面详细介绍那么接下来我们先来学习一下flags这个参数flags参数详解首先查文档我们会发现这个flags参数有很多的选项没截完当然现在我们还不是很了解这些选项的含义也不懂到底该如何传递。那flags这个参数的类型是int是一个整数但我们不能简单地当作一个整数看待。lags 的本质一组开关的“位掩码”flags 是一个整数32bit但它的每一位bit代表一个独立的“开关”。通过“按位或”|运算符可以将多个开关同时打开。刚才上面我们列出的就是开关的选项每个选项本质都是一个宏。大多数选项只有一个比特位为1通过|可以同时选中多个比特就表示同时使用多种打开方式。位掩码bitmask传参示例这种通过一个整数的不同二进制位bit来表示多个独立选项/标志然后通过|同时传递多个选项的方式通常被称为位掩码bitmask传参有时也通俗地叫做位图传参。当然目前讲到这里大家可能还是比较懵的下面我们来写一个例子帮助大家理解这种传参方式我们来看这样一段代码写这样一个函数。然后通过|就可以同时选中多个选项运行看看效果原理理解了这个例子就理解了open 系统调用中 flags 参数的设计原理。演示那下面我们就来使用一下open系统调用以只写方式打开一个文件不存在的文件open返回文件描述符非负整数。O_WRONLY只写方式打开文件那最后我们一定要关闭文件所以下面学习一下关闭文件的系统调用——close6.2 close准确点说close 是 Unix/Linux 系统中用于关闭一个已打开的文件描述符的系统调用。它会释放该文件描述符所关联的内核资源并可能触发一些清理动作如释放文件锁、刷新缓冲区等参数传入要关闭文件的文件描述符即可返回值成功返回 0。失败返回 -1并设置 errno 以指示错误原因。看看效果现在我们只是打开一个文件然后关闭。并且这个文件不存在那按照我们之前C语言文件接口的理解只写方式打开一个文件不存在应该会新建。运行怎么回事我们看到这里打开文件失败了错误信息没有这个文件或者目录。为什么没有新建呢之前我们只写方式打开一个文件不存在会新建那是C语言的文件IO接口而我们现在用的是操作系统的系统调用6.3 O_CREAT必须设置权限那如果打开一个不存在的文件想要新建有办法吗当然只不过需要我们再多传一个选项。O_CREAT如果文件不存在则创建它此时再来运行我们的代码确实创建了但是这个文件好像有点奇怪我们之前学过文件的基本权限是RWX这里怎么看到一个T原因在于我们没有给新文件设置权限。O_CREAT 被使用时必须提供 mode 参数否则行为是未定义的通常可能会导致栈上随机值被当作权限位使用从而产生意外权限。删掉文件再次运行我们发现文件的权限又是一个结果。所以O_CREAT如果文件不存在则创建它。必须同时提供 mode 参数指定权限。那我们C语言使用fopen并没有指定权限啊那是因为底层调用open的时候以一个默认值自动指定权限。这个权限值通常是 0666所有用户读写。而最终生效的权限是0666再经过进程的umask权限掩码 处理后的结果我们之前讲过所以我们上面也说了如果打开的文件不存在使用三个参数的版本给文件指定一个权限。权限的八进制表示法我们之前也讲过这次再来运行成功创建了。但是权限是666吗怎么是664呢因为最终权限 起始权限 ~umask之前我们讲过的东西都不是白讲的如果想让我们设置的权限不受umask的影响可以将umask改成000使用umask系统调用即可更改umask的值这次我们设置666最终文件的权限就是666。6.4 write那向打开的文件中写入内容呢——write系统调用返回值写一下代码看看效果然后修改代码修改写入的字符串然后再次执行不删除存在的文件怎么回事呢为什么是这样一个结果。没有清空之前的内容覆盖写入但是是一个部分覆盖能覆盖多少覆盖多少没覆盖到的还是原来的内容。那现在想让他清空再写入向C语言接口那样的完全覆盖式写入怎么做呢清空写那我们就再传一个选项O_TRUNC如果文件已存在且是以写O_WRONLY 或 O_RDWR方式打开则将文件长度截断为 0。相当于清空文件内容。这次再来运行就只有我们新写入的123了没加换行。现在这个效果不就和我们上面C语言接口演示的清空写一样了嘛其实它的底层就是这样的。追加写那在看看系统文件IO接口的追加写只需要把上面选项中的O_TRUNC换成O_APPEND即可。O_APPEND每次 write 时自动将文件偏移量移到文件末尾再写入。也就是说所有写入永远追加在最后。看看效果这次就是追加写入6.5 扩展了解