c语言IO基础操作:系统文件I/O
open函数返回值
文件描述符fd文件描述符的分配规则总结: 重定向
总结: 如何理解linux下一切皆文件?FILE使用dup2系统调用
dup2原理: 理解文件系统
inode文件创建、删除、查找过程创建一个新文件主要有一下4个操作:理解硬链接软链接硬链接 c语言IO基础操作:
在我们学习C语言的时候唯一对IO流有过的接触的就是c语言的文件操作了,回顾一下c语言对文件的操作
练习1:向一个文本文件写入5个hello word
#include
将刚写入文件的hello word存储回buf 数组中
#include
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码
open接口介绍
#include
flags参数:
grep -ER 'O_CREAT | O_APPEND | O_RDONLY)' /usr/include//usr/include/bits/fcntl-linux.h //flags对应参数所在头文件
系统标准头文件中:/usr/include/bits/fcntl-linux.h
如果理解flags
int fd = open("w.txt", O_WRonLY | O_CREAT | O_APPEND, 0644);
其实当我们写出这行代码的时候,会去调用系统调用open,open函数的第一个
参数代表的是要打开的目标文件,而 O_WRonLY | O_CREAT | O_APPEND
这是一个位运算表达式,而按位或(|)的结果值表示的是:需要使用open函数
打开目标文件后,执行的功能,其实每一个选项(O_WRonLY | O_CREAT | O_APPEND)他都有对应的一个值,这个值被包含在/usr/include/bits/fcntl-linux.h这个头文件中(也就是上述图)。
程序演示:
#include
read文档:
read接口介绍:
#include
程序演示:
#include
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口回忆一下我们讲操作系统概念时,画的一张图:
系统调用接口和库函数的关系,一目了然。所以,可以认为,在c语言中 f#系列的函数,都是对系统调用的封装,方便二次开发
代码:
#include
程序运行结果:
从程序的运行结果来看,确实当我们的程序运行起来之后,会将会打开"word.txt"文件,并在执行read函数的时候将"word.txt"文件中的内容写入到buf这个字符数组中去,另外需要关心的是open在执行成功之后带回来一个val值为3的返回值。这是为什么呢?
为了一探究竟接下来我们可以写一个程序用来测试fd的返回值
#include
程序运行结果:
现象1:从fd的返回值来看,他是连续打印的,fd每次的返回值也是顺序递增的,并且fd的返回值默认会从3开始打印
现象2:open("word1.txt", O_WRonLY | O_CREAT);执行打开文件的时候,带了两个选项,而我们使用c语言的fopen打开文件的时候只需要带”w“
文件描述符的分配规则疑问?为什么fd的返回值默认从3开始?
其实c语言的程序一旦执行起来就会默认打开三个IO流,分别是stdio、stdin、stderr标准输入输出流。而后面的3、4、5、6、7、8其实是文件描述符,文件描述符在系统层面就是一个整数,这个整数的取值范围是【0,n】,
那么之前的0~2数字其实依次代表的是:
0:标准输入(stdin) 1:标准输出(stdout) 2:标准错误(stderr)
其实【0,n】的数字本质上是数组的下标。
对于进程来讲,默认的文件描述符从3开始分配(因为0~2是被标准输入输出和stderr占用了)。
总结:1、open是属于封装出来的系统调用接口
2、open函数的返回值是一个文件描述符fd
3、文件描述符有他的分配规则,站在进程的角度,文件描述符默认只会分配最小值。
使用程序证明, 如果stdin流被关闭了,那么返回的fd会是多少?
#include
程序运行结果:
重定向结论:给文件描述符分配时,默认从最小开始分配。
有趣的实验:
#include
程序运行结果:
我们会发现原本应该应该打印到显示屏的hhhhhhh....1却被输出到了文件中,那么为什么呢?其实一开始的标准输出流是被打开了的,可是我们却close关闭了stdout流,那么1这个文件标识符的值就会空着,而当使用open函数打开"word.txt"文件时,系统就会将原本属于stdout的文件标识符1分配给"word.txt"文件,所以导致原本应该输出到屏幕的值却输出到了文件"word.txt"上,而这也是重定向的原理
1、当fd等于1,这种现象也叫做重定向(常见的重定向有:>, >>, <)
2、重定向的本质其实是,当原本属于stdout的文件描述符1,被分配给了被open打开文件的fd,并在输出的过程将原本应该输出到stdout流的内容被重定向到了被open打开的文件
问题? 文件被打开了那么需不需要被管理?交给谁管理?
文件其实还是交给OS管理的,而OS管理的原则是:先描述,再组织,当文件被加载到内存中之后,操作系统会以某种数据结构的方式管理起文件,而PCB中有一个task_struct类型的结构体,里面会存放一个指针,该指针会指向一个指针数组,获得他的首地址。
那如果我们需要写入一些数据进入文件的话,操作系统会怎么做呢?
操作系统会使用task_struct中的数组指针通过获取struct file *arr[0]的地址,通过数组是线性存储的特性,通过下标索引找到对应的文件,再向该文件中写入数据
问题使用open()函数打开文件,返回的fd值又是谁的?
答案是指针数组的索引位置
下面的代码均摘自linux 2.6.24。
struct files_struct在
struct files_struct { atomic_t count; struct fdtable *fdt; struct fdtable fdtab; int next_fd; struct embedded_fd_set close_on_exec_init; struct embedded_fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; };
如何理解linux下一切皆文件?在我们实际使用硬盘、显示器、键盘的时候,不管是读取还是写入的过程其实调用的接口分别是:
(h_read()、h_write() )硬盘读写
(x_read()、x_write() )显示器读写
(j_read()、j_write() )键盘读写
其实struct file结构体中会包含open、write、read的函数指针,而驱动层也会有对应的几个函数接口,由于他们的参数是相同的,所以可以通过struct file结构体中的函数指针指向驱动层的函数接口,达到open、write、read可以分别使用硬盘、显示器、键盘。(读者可以理解为是类似于多态,因为同一个函数可以实现不同的功能) ,由于操作系统站在上层的角度,所以并不需要关心底层的实现,操作系统真正关心的是使用了软件封装出来的虚拟层,所以在操作系统看来一切皆文件!
以下是该结构体 在内核2.6.5中看起来的样子:
struct file_operations {undefined struct module *owner; loff_t(*llseek) (struct file *, loff_t, int); ssize_t(*read) (struct file *, char __user *, size_t, loff_t *); ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t); ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *); ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long, unsigned long);};
FILE有趣实验:
#include
当我们将标准输出流关闭之后,我们原本需要输出到屏幕的信息全都被输出到了文件中,其实这个现象博主上面也解释过了。那么还有一些知识需要大家掌握。
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
所以C库当中的FILE结构体内部,必定封装了fd
其实struct FILE 结构体是被存放在系统头文件libio.h中的,而这个系统头文件的路径是:/usr/include/libio.h 。
FILE其实只是struct _IO_FILE 结构体类型的别名
其实stdout也是属于FILE类型结构体的指针,既然是FILE类型,那么stdout中一定会包含_fileno
对比write(1, str, strlen(str)) 这句代码能够向文件中写入可以理解,但是为什么 fprintf(stdout, str2);这句代码也能向文件中写入呢?因为stdout中存放的_fileno中的值并没有被改变他还是1!所以这两个代码的功能是一样的!
当我们看完他的相同点之后,再来看他的不同点,这里还是用一段小程序测试:
#include
从程序的运行结果我们不难发现,write只被执行了一次,而printf和fprintf却被执行了两次
write和printf/fprintf他们之间的区别就是write是系统调用接口,而printf / fprintf确实库函数,其实向显示器上输出结果这种方式也是有缓冲区的,这种缓冲方式是行缓冲(遇到n就刷新,因为需要被立马看到),而如果是往文件中输出内容并不需要被用户立马就看见,所以文件是以全缓冲(缓冲区存不下了才会往硬盘中的文件输出)
而这个重定向其实引起来的最大的变化是:在重定向的时候,我们写入数据的目的地发生了变化,没有重定向本身是要往显示器上写,而重定向之后就必须要往文件中写,所以一旦重定向之后是影响缓冲方式的!!
我们发现 printf 和 fprintf (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork执行后的fflush(stdout);有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。printf fprintf 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后但是进程退出之后,会统一刷新,写入文件当中。但是fork的时候,父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。write 没有变化,说明没有所谓的缓冲。
使用dup2系统调用综上:
printf fwrite 库函数会自带缓冲区,而 write系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢?printf fwrite是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
Dup2()使newfd成为oldfd的副本(使用oldfd拷贝出newfd),必要时先关闭newfd‐sary。
如果想要stdout(标准输出流)打印到屏幕的信息写入文件当中,完成标准输出重定向到文件中的操作,我们需要将指向stdou这个文件的指针去指向myfile这个被创建出来的file对象就行,中间的过程是将index位置为3的数组中的指针拷贝给index下标位置为1中,那么就会让index位置1处的*file去指向新打开的myfile文件所对应的对象(也就是所谓的oldfd拷贝给newfd,那么index位置为1处的指针就会被更新为指向myfile文件所关联的file对象)
#include
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据
每行包含7列:
模式硬链接数文件所有者组大小最后修改时间文件名
ls -l读取存储在磁盘上的文件信息,其实本质上是将文件的信息放到内存,然后显示出来。
其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息
上面的执行结果有几个信息需要解释清楚
为了能解释清楚inode我们先简单了解一下文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
Boot Block其实是一个启动块,启动块一般是第一个物理盘的扇区,这个启动块会包含硬盘的分区表这样的信息, 并且这个分区也会指明你的操作系统盘在哪个分区,并且他能找到操作系统
文件创建、删除、查找过程Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。政府管理各区的例子超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了,注意:Super Block 结构在每一个块组里面都有一个Super Block,并且是一样的,这样做的目的是为了防止Super Block 被破快导致剩下的空间块无法使用,GDT,Group Descriptor Table:块组描述符,描述块组属性信息,并且里面记录了inode的使用情况和inode的总个数。块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用(记录inode哪些被使用,哪些没有被使用)inode位图(inode Bitmap):inode Bitmap它其实是一个位图,他是利用了一种哈希的原理将每个bit表示一个inode是否空闲可用。i节点表(inode table):存放文件属性 如 文件大小,所有者,最近修改时间等, 其中inode:存放文件的属性, inode必须包含data blocks对应的映射关系,inode id会 标识一个inode
数据区(data blocks):存放文件内容文件和inode的关系是1对1,文件和data blocks的关系是1对n
如何创建一个文件?
其实创建一个文件的过程包含以下:先给文件分配inode,但是需要在inode Bitmap该表中找一个没有被分配的比特位,它代表的是一个inode,分配成功后会将它置为1,而文件的属性会包含大小、最后修改时间、文件名、所以需要将文件的属性填入inode table,通过inode table和data blocks所对应的映射关系将数据写入到这个数据块中。
如何查找一个文价?
先找到文件所在的块组,在inode table中找到对应的inode信息,找到inode对应的信息就能够找到属性,根据他的块组映射找到对应的数据块,将数据提取出来
如何删除一个文件?
将需要删除的文件所对应的inode编号在位图inode Bitmap当中,将1置为0。
如何恢复一个文件?
根据inode将对应的inode Bitmap位置置为1,再根据inode table和data blocks所对应的映射关系置为1,这个文件就被恢复了。
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作。
我们将上图简化:
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
复制到300,下一块复制到500,以此类推。记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文
件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来
问题? 目录算是文件吗?
是,目录也是由inode和数据块构成的,而一个数据块中保存的其实是文件名和inode id,所以目录中的数据存放的是文件名到inode的映射关系。
理解硬链接了解硬链接:
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -1i
abc def 263466 abc 263466 def
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个
abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。 软链接
具有独立的inode,因为log.txt和log.txt的软链接都是只有一个inode,既然各自都有一个inode那么它就是一个独立的文件,
其实硬链接h_link和log、txt文件共享的是同一个inode,所以硬链接并不是一个独立的文件,那么硬链接是什么? 其实h_link在本质上还是一个文件名,只是它使用的是log、txt的inode编码,其实test这个当前目录中也是会存放文件名和inode的映射关系,而h_link只是一个特定文件名和inode的映射关系,
为什么目录的链接数是2?其实文件名dir和1835039( inode id )属于一组映射关系,还有的就是dir目录中的隐藏文件 、会和1835039属于一组映射关系,所以dir的连接数是2
但是dir中也会存放当前目录和上级目录,如果dir的连接数为3,那么dir和1835039属于一组映射关系,而dir中的隐藏文件 .和1835039属于一组映射关系,tmp中的隐藏文件、.和1835039属于一组映射关系。所以dir中就会有3个连接数