C语言文件操作
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针。
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码:
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
mode_t理解:直接 man 手册查看,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数。
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
通过以上这张图,系统调用和库函数之间的关系就一目了然了,
可以认为,所有的f*系列的函数都是对系统调用接口进行了封装,方便二次开发,因为系统调用接口用起来的难度很大,所以库函数做了封装使用起来就变得更简单一些。
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器。
所以输入输出还可以采用如下方式:
文件描述符的含义: 我们现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了struct file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。所以每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件,进行访问了。
在files_struct结构体中维护的struct file* fd_array数组当中,找到当前没有被使用的最小的一个下标,作为新打开文件的文件描述符。
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
使用场景:
printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。那追加和输入重定向是如何完成呢?追加重定向的原理和输出重定向的原理是一样的,仅仅是open打开文件时的方式不同,追加重定向是不清空文件,输出重定向是先清空文件再输出。
追加重定向:
追加重定向和输出重定向的代码在其它的地方没有一点区别。
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write(系统调用) 只输出了一次。为什么呢?肯定和fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf 和 fwrite 等库函数会自带缓冲区(之前的进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。所以我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后,但是进程退出之后,会统一刷新,写入文件当中。
但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,即子进程也会刷新缓冲区,所以printf和fwite会产生两份数据。write 没有变化,说明系统调用接口没有所谓的缓冲区。
综上: printf fwrite 等库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们这的讨论范围之内。
那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C库函数,所以该缓冲区由C标准库提供。
在/usr/include/libio.h中有FILE结构体的定义:
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。例如学校管理学院一样。
1、超级块(Super Block):存放文件系统本身的结构信息,是描述整个分区的所有基本信息的。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了,但是一般在多个Block Group中都保存着Super Block,所以某个Super Block信息被破环,整个文件系统也不会立即被破坏了。
2、GDT,Group Descriptor Table:块组描述符,描述块组属性信息,例如该块组中有多少个inode被使用了,剩余多少个,有多少个data block被使用了,剩余多少个,inode编号是从多少开始的,该分组一共多大等关于该块组的所有基本信息。
3、块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
4、inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
5、inode table:存放文件属性的结构体数组,数组中的每一个元素是一个inode结构体,结构体存文件的信息, 如 文件大小,所有者,最近修改时间等。
6、Data blocks :存放着以块为单位的内存块,用于保存文件内容,一个块等于8个扇区。
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。
test.txt和hard_link的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 657073的硬连接数为2。
我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则在对应的磁盘释放。
硬链接不是一个独立的文件,因为它没有独立的inode,建立硬链接只是在该目录下增加了一个文件名与inode的映射关系而已。所以硬链接数起始就是指有多少个文件名指向同一个inode的意思。
软链接是一个独立的文件,因为它有独立的inode,软链接文件中的内容是指向的文件的路径,在这里可以认为soft_link中保存的内容是file.txt的路径信息。
你学会了吗?如果感觉到有所收获,那就点点小心心点点关注呗!后期还会持续更新Linux操作系统的相关知识哦!我们下期见!