重谈文件
文件 = 内容 + 属性, 所有对文件的操作都是: a.对内容操作 b.对属性操作
关于文件
一:
即使文件的内容为空,该文件也会在磁盘上也会占空间,因为文件不仅仅只有内容还有文件对应的属性,文件的内容会占用空间, 文件的属性也会占用空间,比如:
创建了一个文件没有往文件里面写入任何内容, 但文件的大小为0代表文件内容为空, 不代表文件本身不占用空间, 因为文件 = 内容 + 属性, 内容是数据, 属性也是数据 ---存储文件必须既存储内容又存储属性----默认就是磁盘上的文件.
二
对文件的操作等于对文件的属性进行操作或者对文件的属性和内容进行操作, 因为改变文件的属性不一定会改变文件的内容, 但是改变文件的内容一定会改变文件的属性, 比如:
修改之前的test.c文件:
对test.c文件的内容进行修改, 多输入一行printf语句:
对文件的内容做修改, 文件的内容和属性都发生变化.
只对文件的属性进行操作, 比如说改变文件的拥有者, 所属组, 文件的权限和时间等等都是对文件的属性进行的操作:
三
进行文件操作时需要使用路径加文件名的方式以确保唯一性, 因为在不同的路径下可能会存在同名文件, 比如在上级目录下创建一个空文件test.c, 然后在其他路径下使用另外一个可执行程序来向test.c文件写入内容:
路径加文件名可以帮我们指明确定的文件.
四:
如果没有指名对应的文件路径, 默认是在当前路径进行访问, 这里的当前路径就是进程当前的路径,就是在哪个路径下执行的文件, 那么这个路径就是进程的当前路径。
test.c:
之前说过在根目录下proc文件夹里面存放许多文件夹, 这些文件夹以系统中的各种进程名为名, 并装着对应进程的各种信息:
此时就会显示两个链接文件, 链接文件exe表示的是该进程对应的文件在磁盘中的位置,链接文件cwd表示的是该进程的当前路径:
如果我们在其它路径下执行的这个程序,, 那么该程序的cwd就会变化,比如在家目录:
五:
当我们把fopen, fclose, fwrite等接口写完之后, 代码编译之后, 形成二进制可执行程序之后如果程序被没有执行, 则对应的文件操作是不会被执行的, 所以对文件的操作本质上就是进程对文件的操作.
我们访问一个文件时, 都是要先把这个文件先打开的, 这里的"我们" 其实是进程, 是进程要访问这个文件. 而文件打开前是一个普通的磁盘文件, 而CPU只与内存打交道, 所以文件打开后其实把文件加载到了内存, 加载磁盘上的文件, 一定涉及到访问硬件磁盘设备, 所以文件打开操作是由操作系统来做的.
六:
磁盘上有很多的文件, 但是并不是所有的文件都被打开了, 文件按照是否被打开, 分为: 被打开的文件(内存中) 和 未被打开的文件(磁盘中). 所以研究文件操作的本质就是进程与被打开文件的关系.
一个进程是可以打开多个文件的, 也就是说加载到内存中的文件可能存在多个, 操作系统要不要管理这些打开的文件呢? 如何管理? 先描述,再组织, 一个文件要被打开, 一定要在内核中形成被打开的文件对象, 所以对文件的管理转化为对链表的增删查改.
struct XXX
{
//文件属性
struct XXX* next;
};
C语言文件调用函数
在谈系统调用接口之前先来谈谈c语言里面的文件调用函数.
fopen
操作文件之前首先需要打开文件, 在c语言里面打开文件使用fopen函数:
第一个参数表示要打开文件的文件名, 如果文件名没有带路径的话, 该函数就会在当前路径下查找并打开这个文件, 如果带了路径就会到指定路径下查找并打开这个文件.
第二个参数表示的是打开文件的方式, r表示只读也就是只能从文件中读取数据, w表示只写也就是只能往文件中写入数据, a表示的是往文件的后面尾插数据, 以w的形式打开文件如果文件名不存在的话会在当前路径下创建文件, 并且w方式打开文件会清空文件中原来的数据, 比如说下面的操作:
而之前在指令中使用过的输出重定向 > 和追加重定向 >>, 实际上是分别以w方式和a方式打开文件:
文件以w方式打开, 会先清空文件内容, 而使用 echo "hello world" > log.txt 是以输出重定向的方式向log.txt中写入hello world, 每次打开文件都会先清空, 所以如果直接 > log.txt , 可以直接清空该文件, 虽然什么内容都没有向文件内输入, 但是重定向会以"w"的方式打开文件, 打开文件就会清空文件.
文件以a方式打开, 是从文件结尾处开始写入, 是追加. 使用echo "hello world" >> log.txt, 以追加重定向的方式向log.txt中写入hello world, 每次写入之前的内容都不会被清除, 所以 >> 追加重定向是以"a"的方式打开文件.
fclose
既然有函数能够打开文件那么就会有函数关闭文件, 那这里的函数就是fclose函数, 该函数的介绍如下:
这个函数只有一个参数, 所以使用这个函数的时候直接将文件打开时创建的那个FILE*变量传给这个函数就可以关闭对应文件了 .
fwrite
size_t fwrite(const void* buffer,size_t size,size_t count,FILEstream);
从内存的变量中获取二进制数据,放到文件中
const void buffer表示获取数据的位置,size_t size一个变量类型的大小,size_t count表示读多少个这样类型的数据,FILE* stream为文件指针
fread
size_t fread( void buffer, size_t size, size_t count, FILE stream );
从文件中获取二进制数据, 放到内存的变量中
const void buffer表示获取数据的位置,size_t size一个变量类型的大小,size_t count表示读多少个这样类型的数据,FILE stream为文件指针
认识系统接口
c/c++都有文件操作接口, 每个语言都有文件操作接口, 并且每个语言操作接口都还不一样, 但是这些接口本质上都是调用操作系统提供的文件级别的系统调用接口来访问的文件, 而操作系统的接口只有一套, 不管库函数再怎么变化. 底层是不变的.
文件是在硬盘上的, 硬盘是外设, 外设是被操作系统管理的, 所有人想要访问磁盘都无法绕操作系统,所以访问文件一定要有系统调用接口, C语言打开文件的接口, 底层一定封装了系统调用接口.
标记位
宏喜欢用来作为标记位, 标记位的作用就是表明某件事情是否发生/存在, 如果发生了就传一个标记位, 在c语言中一般以一个整型变量作为标记位, 但是如果需要10个标记位的话那就得传10个变量用来表示10件事情已经发生这就有点麻烦, 所以结合之前学过的位图去判断是否存在的问题就十分合适了,用一个整形变量可以充当32个标记位.
首先标记位是一个宏, 这个宏实际上就是一个数字, 并且其对应的二进制位只有一位是1, 所以我们就可以采用这样的形式来创建标记位:
1 #include <stdio.h>
2
3 #define Print1 (1<<0) //0001
4 #define Print2 (1<<1) //0010
5 #define Print3 (1<<2) //0100
6 #define Print4 (1<<3) //1000
7
8 void Print(int flags)
9 {
10 if(flags & Print1) printf("hello 1\n");
11 if(flags & Print2) printf("hello 2\n");
12 if(flags & Print3) printf("hello 3\n");
13 if(flags & Print4) printf("hello 4\n");
14 }
15
16 int main()
17 {
18 Print(Print1); //hello 1
19 Print(Print1 | Print2); //hello 1\n hello 2
20 Print(Print1 | Print2 | Print4); //hello 1\n hello 2\n hello 4
21 return 0;
22 }
利用位图的原理, 想要执行哪条代码就输入对应的宏并用或运算符连接起来, 因为每一个宏都对应唯一的一个二进制1.
open
c语言里面是通过函数fopen来以各种不同的形式打开文件, 其实fopen是通过对系统调用接口open进行封装来实现的, open函数的介绍:
open函数有三个参数, 第一个参数pathname是文件路径, 第二个参数flags是打开方式也就是标志位, 第三个参数是权限, 如果文件已经存在, 只需要前两个参数. 文件不存在需要设置第三个参数, 否则创建出的文件没有权限.
部分标识符:
选项宏 | 功能 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读,写打开 |
O_CREAT | 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限 |
O_TRUNC | 当源文件中存在内容时会将文件的内容进行清空 |
O_APPEND | 追加写 |
注:O_RDONLY、O_WRONLY和O_RDWR只能三选一使用 .
其中第一个参数pathname可以添加路径也可以不添加路径, 不添加路径的话该函数就会在当前路径下查找文件. flags可以传很多的标志位, 先举两个例子, O_RDONLY表示只读标记符, O_WRONLY表示只写标记符, 和fopen里的 r 和 w 有点像.
我们可以通过下面的例子来证明一下两者的区别:
首先c语言中的w有两个特性:
1.如果源文件存在内容的话那么以只写方式打开文件的话, 会将源文件的内容清空。
2.如果打开的文件不存在的话, 那么fopen函数会自己创建一个文件.
我们来看看open函数的只写有没有上述的功能比如说下面的操作, 在当前路径下创建一个文件:
并没有自动创建文件
所以再介绍open函数的两个标记位: O_CREAT, O_TRUNC, 其中O_CREAT的作用就是当文件不存在时会自动创建一个文件, O_TRUNC的作用就是当源文件中存在内容时会将文件的内容进行清空(create是创建, trunc是截断), 所以使用c语言的fopen函数以w只读的形式打开文件时, 在底层就会调用open函数并以O_CREAT | O_TRUNC|O_WRONLY 作为标记位进行传参:
发现可以自动创建一个log.txt文件, 但是这个文件目前是红色的, 因为此时的文件里面都是乱码无法正常的使用, 造成这种现象的原因是因为在创建文件的时候没有给对应的权限, 也就是上面提到的遗漏了该函数的第三个参数, 当使用open函数创建文件时需要用第三个参数给创建的文件一个起始权限.
这里给了起始权限是0666, 但是这里创建出来的权限是0664, 因为这里创建的文件也遵循umask的原则, 所以创建出的文件的权限可能不是我们想要的.
这里要是想让创建的文件就是我们给的起始权限的话就可以使用umask函数, 可以在创建文件前先用umask(0), 先把权限掩码设置为0, 设置了权限掩码就会用设置的, 没设置umask就是系统默认的.但是程序中调用的umask函数不会影响命令行的umask值.
open函数返回值称为: 文件描述符fd.
使用open函数打开文件时open函数会返回一个整数, 这个整数就是文件描述符fd, 在其他函数里面就可以根据这个文件描述符来确定要写入的文件, 如果open返回的值为负数的话就表明此时文件打开失败.
我们可以看到此时打印的文件描述符为3,在后面的程序就可以使用这个文件描述符3来代表要被操作的文件mytest。
write
将一个文件以写的打开之后就可以往这个文件里面写入内容, 那么这里的写入就要用到write函数, 该函数的参数如下:
第一个参数fd是文件描述符表示要将内容写入哪个文件.
第二个参数表示写入文件的内容来自于哪个缓冲区, 这个指针会指向空间开始的位置, 并且指针的类型为void说明不管要写入的数据类型是什么这个函数都可以将数据写到文件里面, 就是因为在计算机看来所有的数据全部都是二进制, 我们平时说的二进制数据和文本数据只不过语言进行的封装罢了.
第三个参数表示写入文件的内容有多少个字节, 当这个函数执行完之后就会返回实际写入文件的字节个数.
比如说下面的代码:
问题: write函数第三个参数需不需要传strlen(buf)+1 ?
outbuffer中有一段字符串abcde\n, 这个字符串的长度为6大小为6个字节而我们却想往文本里面写入7个字节的内容, 所以当函数写入的时候就会自动的在字符串后面添加上一个\0来补齐这7个字节的大小, 我们之所以这么认为是因为在c语言当中字符串是以\0作为字符串结束的标志符, 防止出现一些越界访问的错误, 可是字符串以\0结尾是c语言规定的和文件有关系吗?
没关系, 文件中的字符串不是以\0结尾的往文件中写入的时候只需要字符串的有效内容就可以, 除非就想往文件中写入\0不然不要在这里的第三个参数+1.
再介绍一个标志位O_APPEND, 如果想要在文件的尾部插入内容, 达到fopen中"a"的效果, 就要将open函数第二个参数O_TRUNC替换为O_APPEND.
在刚才的基础上追加了一遍原来的内容.
所以标记位 O_WRONLY|O_APPEND|O_CREAT 组合到一起就是c语言中fopen中a的功能
read
既然可以将缓冲区(数组)里面的内容通过write函数输出到文件里面, 那么这里也可以通过read函数读取文件里面的内容并放到缓冲区里面, read函数的参数如下:
与write函数相似,
第一个参数fd表示要读取哪个文件的内容.
第二个参数buf表示将读取的内容放入程序的哪个缓冲区中.
第三个参数count表示你要读取多少个字节的内容.
read函数的返回值表示如果读取成功了就返回读取的字符个数, 如果读取失败了或者没有内容就返回0.
read函数的第二个参数类型是void*, 表明read函数在读取内容的时候也没有数据类型的概念,不管文件里面装的是图片还是视频还是一些文本数据等等, 它读的都是二进制数据, 这些数据具体如何处理那都是使用者自己决定的.
比如:
使用read函数读取文本中的数据, 因为这里采用的是系统提供的函数读取数据, 并且我们想让数据以字符串的形式放入到数组里面, 所以read函数里面的读取字符的个数得是sizeof(buffer)-1留下来一个空间以免缓冲区满了装不下\0, 读取完数据之后就要在缓冲区的有效内容的后面手动添加一个\0用来表示此时的数据内容是字符串.
把刚才写入的内容再读出来并打印.
close
close函数就是用来关闭open函数打开的文件的, 这个函数的参数如下:
将文件描述符传给close函数就可以.
所以其实fopen fclose fwrite fread fseek等函数在底层其实都是用操作系统提供的接口实现的, 这些函数与open close write read lseek函数一一对应.