文章目录
- [Linux 系统编程 文件篇 (一)](#Linux 系统编程 文件篇 (一))
-
- [1 理解文件](#1 理解文件)
-
- [1 初步理解](#1 初步理解)
-
- [1.2 系统角度](#1.2 系统角度)
- [2 回顾一下 C 语言的文件接口](#2 回顾一下 C 语言的文件接口)
- [3 系统文件 I/O](#3 系统文件 I/O)
Linux 系统编程 文件篇 (一)
1 理解文件
1 初步理解
我们以一个问题为切入点开始。假如我现在有一个大小为 0 的文件,要不要再磁盘上占据空间? 答案是要的,这个我们之前也知道, 文件 = 内容 + 属性 。 换句话说,我们对文件进行的操纵,比如说是存取,或者修改,围绕内容和属性展开的。
我们之前学 C 语言里面,要对一个文件进行操作的时候,必须先打开这个文件。比如我现在有一个C程序,里面有一句 fopen ,打开一个文件。但是我不运行这个程序,这个文件有没有被打开?答案肯定是没有的,就想 malloc的时候,没有走的这一步堆空间就不会被申请。所以,是谁打开的文件呢?是进程 !
用户对文件的操作,本质上是进程对文件的操作。
文件是在磁盘里面的,磁盘是外设,是硬件,是永久存储型介质,所以文件在磁盘上的储存性是永久的。所以,我们对磁盘上的文件进行的所有操作其实都是对外设的输入输出, 简称 IO 。这是狭义的理解。
广义上的理解是什么呢?linux 下一切皆文件,后面回去讲如何去理解。
1.2 系统角度
我们之前说,文件是在磁盘里面的,而进程想要去访问文件,本质是在访问硬件,只有操作系统可以访问硬件,因为操作系统是硬件的管理者,所以,我们就可以得到一个结论,我们之前使用的 fopen 还要 C++ 里面的输入输出流,还有一些其它语言的,比如说像 php ,python,等等,类似的文件接口,一定都封装了对应文件的系统调用。 操作系统不相信任何人。
为什么要封装,这个其实是为了语言的可移植性,后期会谈到。
下一个问题,什么叫把文件打开呢?首先,进程可不可以打开多个文件,当然可以,比如说一个进程启动默认打开三个文件 标准输入输出和错误嘛。内存里面肯定有很多进程,每个进程都有自己操作的文件,有的被打开,有的马上要被关闭。
所以,我想说的是,操作系统要不要把对应的文件管理起来? 肯定要,怎么管理? 先描述,在组织。
所以,我们可以断定,内核里面一定有管理文件数据结构。
其实这个文件一共分两类,一类是内存级的文件,一类是磁盘文件,现在往下讲的无特殊说明都是内存文件。
2 回顾一下 C 语言的文件接口
首先就是打开文件, fopen 返回一个 FILE* 的指针,我们之前讲过这是一个结构体,代表这个文件。
我们之前还提到过,为什么以写方式,打开文件会在当前目录下新建一个文件,因为进程的 cwd 里面保存了当前目录,fopen创建文件的时候直接把文件名和这个cwd的路径名拼起来。如何查看之前也提到过,在 /proc 路径下找到当前进程的 pid 同名目录,列一下就可以看到。
关闭文件是 fclose 之类的。
下面,有一个问题,输出信息到显示器,有那些办法。
我们知道,一个进程
默认会打开三个文件,一个是 stdin ,stdout 和 stderror 。可以看到,这个三个东西的类型都是 FILE* ,换句话说,他们都是语言层封装的。
stdin 是标准输入, stdout 是标准输出 ,stderror是标准错误。之前我们说他们分别对应着键盘,显示器,显示器。
现在,修改一下,他们分别对应着键盘文件,显示器文件,显示器文件。
为什么默认打开这三个文件,也比较容易理解,因为程序是做数据处理的,需要输入数据,输出结果,甚至是错误的。
所以呢,我们就可以通过 stdout 文件然后输出出来就好了。
这个输出的接口我们在 C 语言里面也见过:
fprintf 和 fwrite ,对应的文件读的接口就是 fscanf 和 fread 。
还有两个我们之前讲的写的读的接口就是 fnprintf 和 fnscanf ,这个是往字符串里面读和写。

fwrite 是二进制写, 同理 fwrite 是二进制读。会把数据以二进制的形式写出和读取。


相对的是 fprintf 和这个 fscanf ,这个是文本读,和文本写。说到这里,其实,文本就是字符串,文本文件里面存的其实都是字符串。这样一说就好理解一些了。

可以看到这个,fwrite 第一个参数是我们要写的数组,第二个参数是这个每个数据的大小,字节为单位,第三个是这个数据个数,第四个就是这个文件流了。返回值是写入的的数据个数。同理合理类比 fread。
这个二进制读取和这个文本读取有什么区别呢?或者说什么叫二进制读,
比如说, n = 10000 。 如果我使用这个 printf fprintf ,它会把这个数转化为字符串,把每个数都看成是一个字符,打印出来,所以是 '1' , '0', '0','0', '0'。这也是我们习惯的使用的。
而这个二进制写是 10000 转化为二进制是这个 0010 0111 0001 0000, 也就是说,他打印的是这四个字节。但是,我们的文本编辑器,打开二进制文件,是把它当作文本文件,这些数字是ASCII码,强行解释的,所以,就可能会出现一些意料之外的字符或者空白字符,空白字符有时也会是乱码。
之前见过的打印字符或者字符串的 想 fputs , fputc 都和这个 fprintf 的写方式是一样的。
后面,我们会知道,其实linux里面是不分文本读写和二进制读写的。这个后期会谈到。
回顾起这些函数的大致用法之后呢,我们再来回想一下,这个 fopen 的除了传文件路径进去,第二个参数还有这个选项。
比如说 'r' 是只读 'w' 是只写 , 'a' 是追加写,当我们 w 的时候会把文件清空,光标回原位置再写。
当这个 'w' 和 'a' 的时候,如果指定打开的文件没有,会自动创建一个。
这个都是我们之前提到过的。
回想一下,之前我们在学重定向的时候,输入重定向:

可以看到输入重定向的时候,第二次输入是把第一次清空然后再写入。如果没有 log.txt 这个文件的话,就会新建。
所以,我们可以推测,这个重定向一定有这个 'w' 的方式打开这个文件。 那么我们也就可以知道这个追加重定向,是以 'a' 的形式打开的文件。
这里也为后面买个伏笔。

我们可以看到这里的选项不止有 r w a。还有 r+ 和 w+ 和 a+ ,r+ 是读写方式打开文件,不新建。 w+ 是读写方式打开文件新建, a+ 是读追加方式打开文件新建。了解一下就好。
还有一个小知识点,我们用 snscanf 写成字符串,往文件里面写这个字符串的时候,要不要加 \0 ? 往文件里面写不要\0。 \0 是一个不可显字符,在文件里面我们看的时候会变成乱码(^@的形式)。字符串是以 \0 结尾的, 但是文件可不是。
还有一件事。我们会发现在我们刚写了一个文件的时候,直接去读是读不上来的。为什么呢?可以把文件想象成一个数组,下标就是我们之前说的光标,而且只有一个,所以,我们写到一个地方的时候,我们什么都不做,再去读的时候光标位置就不对。
我们也学了一些调整光标位置的接口:

这些就是我们之前 C语言 文件部分学到的调整光标位置的函数。 其实 linux 里面也有一套,

就是这个 lseek 接口。认识一下,留个印象就好了。
其实,有没有发现,我们在写文件的时候是简单的,真正麻烦的是读文件,因为涉及到各种类型转换,int 读进来要是 int ,double 读进来要是 double。这个是比较麻烦的,我们后期会讲。这里提供一种思路就是把这些格式封装成一共结构体,然后读的时候读这个结构体,就可以有格式了。
补充奥,我们之前还学到过判读文件关闭的函数 feof 和 这个 ferror 函数。注意,文件关闭的时候,会自动给这个文件流打上标记,一共是走到文件末尾结束关闭,或者是遇到错误关闭。 feof 是判读这个文件的结束方式是否是遇到了文件末尾,是就返回 0, 不是就返回非零。 ferror 一个道理。
3 系统文件 I/O
这里我们开个头,我们之前也讲过,进程打开文件打开磁盘里面的文件,操作系统是磁盘的管理者,所以必须经过操作系统,也就意味着这些文件接口里面一定封装着系统调用,以打开为例:

可以看到这里的接口,open 第一个参数是路径,和这个 fopen 使用方法一模一样。第二个 flags 的标记位是什么?
没错,就是打开方式。'r' 'w' 'a' 等。 但是,这个为什么是个 int 呢? 其实,这里的打开方式不知有之前提到的那些,会更加的细分,比如说新建,还有追加,清空,都是宏。我们之前用到的 w 就是新建,和这个清空组合。
既然有这么多种宏的选项,这里是怎么只用一个 flags 就全部表示呢? 还是我们之前提到的位图。

这是打开文件更细分的选项,是宏。
我们可以用一个二进制位置的数为 1 来表示有 , 0 表示无,而且二进制位不重复。 这样既可以轻松找到每个数,也可以组合选项。
c
#include <stdio.h>
#define ONE 0001 //0000 0001
#define TWO 0002 //0000 0010
#define THREE 0004 //0000 0100
void func(int flags) {
if (flags & ONE) printf("flags has ONE! ");
if (flags & TWO) printf("flags has TWO! ");
if (flags & THREE) printf("flags has THREE! ");
printf("\n");
}
int main() {
func(ONE);
func(THREE);
func(ONE | TWO);
func(ONE | THREE | TWO);
return 0;
按位与上对应的宏来检查,按位或上别的标记来组合。
! ");
printf("\n");
}
int main() {
func(ONE);
func(THREE);
func(ONE | TWO);
func(ONE | THREE | TWO);
return 0;
按位与上对应的宏来检查,按位或上别的标记来组合。
使用位图可以把要传的很多参数直接合并成一个,是一个很不错的方式。