目录
C语言文件相关操作
写入文件操作
读取文件操作
C语言所对应的输入输出流
系统文件
open函数
write函数
read
文件描述符
linux一切皆是文件
文件描述符分配规则
重定向
输出重定向
追加重定向
输入重定向
stdout和stderr的区别
系统函数dup2
FILE
C语言的FILE结构体
FILE结构中的缓冲区
了解缓冲区
编辑
探秘缓冲区
fflush强制刷新缓冲区
编辑
文件系统
磁盘
磁盘的组成
1.磁盘的分区
软硬连接
1.软链接
2.硬链接
- extern FILE *stdin //对应输入,键盘
- extern FILE*stdout//标准输出,显示器
- extern FILE*stderr //标准输出,显示器
我们无论是写文件还是读文件,文件都是来源于硬件(磁盘...),硬件是由操作系统管理的;用户不能够直接跳过操作系统将文件写入,必须贯穿整个操作系统。那么访问操作系统就需要调用系统接口来实现文件向硬件写入的操作;也就是你在C语言或其他语言上使用的文件相关库函数,其底层都是要调用系统接口的;如下图所示:
系统接口使用open打开文件
参数解读:
pathname:要打开的文件路径,没有则自动创建
flags:打开方式
- O_RDONLY ---只读方式打开
- O_WRONLY---只写方式打开
- O_APPEND---追加方式打开
- O_RDWR---读写方式打开
- O_CREAT---文件的创建
MODE:文件权限设置
open函数返回值fd-文件描述符(成功返回对应的fd,失败返回-1)
结果可以看到文件描述符从3开始一次递增,这表明着0,1,2这三个文件描述符是被占用了,这就对应了前面提到三个默认输入输出流~
向文件描述符写入数据,
一个进程可以打开很多文件,当操作系统存在大量文件的时候,这时候就需要有东西来对这些文件进行管理,设计一些结构体数据来管理这些文件,把这些结构体数据,使用双链表链接起来,这样会很方便管理
每个进程都有自己的PCB,进程控制块中又有一个指向struct files_struct结构的指针,struct files_struct中包含一个指针数组,数组中的指针都指向数组下表所对应的文件信息~
对于我们的外设(IO设备),在驱动层一定对应了相应的驱动程序,包括他们各自的读写方法;他们的读写方法是不一样的;
在操作系统层面上,对于底层的键盘、显示器、磁盘等外设,需要打开时,操作系统就会给这些外设创建一个struct file的结构体进行维护,这些结构体就包含了相关外设的属性信息,再将它们用双链表管理起来。再与上层的进程结合起来既可以执行对对应的操作了。这里就是所谓的虚拟文件系统(VFS)
我们在C++的学习中,对多态的概念有所了解,就是多个子类继承了相同的父类,每个子类的方法都是不一样的,只要父类的指针或引用调用对应的子类,就去实现对应子类的方法。在C语言中,想要实现多态,我们的方法是通过函数指针;
在这个struct file的结构中,就包含了读写方法的函数指针,对应到了每个外设;在上层看来,所有的文件只要调用对应外设的读写方法即可,根本不关心你到底是什么文件。
本质上,所谓的一切皆文件,就是站在struct file的层面上看待的
连续打开多个文件,他们的文件描述符都是连续的,而且0,1,2被占用,从三开始依次递增。我们在操作之前关闭0,那么打开文件的文件描述符就不会和原来的一样了,就会从结构体指针数组中找一个没有使用过的来占用
本来应该写到显示器的数据,被写到了文件中,这种行为叫做重定型
这段代码关掉了1号文件描述符,我们想向屏幕打印信息。屏幕上却没有显示,将文件数据写入到了这个文件中,这叫输出重定向~1号文件描述符被关之后,需要将这些数据给输出出来,就会寻找一个新的没有被占用的,将数据放到里面。
printf函数本质是向stdout输出数据的,而stdout是一个struct FILE*类型的指针,FILE是C语言层面上的结构体,该结构体当中有一个存储文件描述符fd,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
C语言的数据并不是立马写到操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。
追加重定向不会覆盖原来的数据
本应该从这个文件读取的数据,变成了从另一个文件读取。输入重定向
略
他们两个都作为输出显示器,有什么区别呢?
我们同时向标准输出和标准错误中输出数据,都是能够打印到键盘上的;当我们将其重定向到文件中去时,却发现只有stdout的内容重定向到了文件中。实际上我们在使用重定向的时候,是把文件描述符1的标准输出流重定向了,而不会对标准错误流重定向。
以上的操作我们都是在关闭标注输入和标准输出后完成重定向的,显得很麻烦,如果标准输入和标准输出都被占用(已经打开了),我们如何去完成重定向呢?要完成重定向我们就可以将fd_array数组中的元素进行拷贝即可;例如:我们将fd_array[3]中的内容拷贝到fd_array[1]中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt 。
在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。本质上dup2就是将进程中文件描述表中的需要重定向的内容进行相关的拷贝,dup2的函数原型如下:
参数解读,将fd_arr[ildfd]的内容拷贝到fd_arr[newfd]中。
成功返回0,失败-1;
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
我们可以使用vim 打开 usr/include/stdio.h 的文件查看FILE。
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
1.不关闭文件描述符
2.关闭文件描述符
关闭文件描述符之后,显示器没有打印数据,但是log。txt文件中也没有这是为啥?
通过上面两次的运行结果发现,在关闭文件描述符后,重定向的操作失败了,其本质原因就是数据是暂存在缓冲区(用户级缓冲区)的。在操作系统内部也是存在一个内核缓冲区的,用户缓冲区到内核缓冲区的刷新策略有如下几种:
- 立即刷新:不缓冲
- 行刷新(行缓冲 \n),比如,显示器打印
- 缓冲区满了,才刷新(全缓冲),比如,往磁盘文件中写入
当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来后,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)
显示器是行缓冲,即遇到'\n'就会刷新数据到显示器;磁盘是全缓冲,当缓冲区满了以后,才会刷新数据到磁盘上;
当我们在重定向时,其数据的刷新策略也会发生变化,(上面的代码中)本来我们是行缓冲的,但是重定向后就变成了全缓冲;两者都是要通过系统调用接口(open)来完成数据的写入; 在没有关闭文件描述符fd时,我们能看到重定向后的结果,本质是进程结束了,刷新了缓冲区;在关闭文件描述符fd后,既没有向显示器打印,也没有向文件中打印,本质就是,它要通过系统调用接口先将数据暂存到内核缓冲区,待进程结束后,才刷新到相应的外设中,但是fd已经关闭,就不会刷新到内核在刷新到硬件,所以就看不到任何数据;
通过上面的运行结果,再结合之前所说,我们这里不是关闭了1吗?为什么还是能够打印出来呢?我们可以看到这里四条输出语句都是向显示器上打印的,并且都有'\n',表明是行刷新,在关闭1之前就已经刷新到显示器上了。
标准错误不会重定向我们能够理解,但是其他三条语句应该是重定向到文件中呀,而这里运行结果只有一条hello stdout。这是因为,我们的msg1是直接通过系统调用接口,把数据暂存到内核缓冲区,不会把数据暂存到上层的用户级缓冲区,所以关闭1根本就不会影响这个数据刷新到文件;但是下面两个语句由于重定向的原因,刷新策略发生了变化(行缓冲->全缓冲),数据暂存到用户级缓冲区后,本来是等待进程结束后刷新到文件中去的,但是这个过程中却把1关闭了,才导致这两条数据并没有被刷新到文件中;
文件在没有被打开的时候,都是被放在磁盘上的,我峨嵋你看一下他是怎么放在磁盘上的
如下图所示,磁盘主要是通过机械臂上的磁头来读取磁盘上的数据,磁盘由一个或者多个圆盘组成,它们围绕着一根中心主轴旋转,磁盘被组织成磁道,磁道是单个盘片上的同心圆,所有盘面上半径相同的磁道构成了柱面,每一个磁道又按512个字节为单位划分为等分,叫做扇区, 向磁盘读取和写入数据时,要以扇区为单位。
磁盘由一个或者多个圆盘组成,我们可以把它想象成小时候都接触过的磁带,把磁盘展开成一条直线。(线性结构)磁盘是相当大的,如果对整个磁盘进行管理成本很高,那么就需要对磁盘进行分区操作。
在Linux ext系列文件系统中,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。
Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
GDT,Group Descriptor Table:块组描述符,描述块组属性信息;Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。i节点表:存放文件属性 如 文件大小,所有者,最近修改时间等
数据区(Data block):存放文件内容
ln -s log.txt log//创建软连接
unlink log//删除软连接
软连接充当索引作用,但是软连接的索引和本文件是不一样的,软链接有独立的inode,是个独立的文件,可以跨文件系统
ln log.txt hard