C文件接口
首先我们需要理解C文件接口中的几个函数fwrite函数、fread函数
对文件格式化读写函数fprintf与fscanf而言,尽管它可以从磁盘文件中读写任何类型的文件,即读写的文件类型可以是文本文件、二进制文件,也可以是其他形式的文件。但是,对二进制文件的读写来说,考虑到文件的读写效率等原因,还是建议尽量使用 fread 和 fwrite 函数进行读写操作。
头文件:#include<stdio.h>
原型:size_t fread(void *buf, size_t size, size_t count, FILE *fp);
size_t fwrite(const void * buf, size_t size, size_t count, FILE *fp);
在上面fread和fwrite函数的原型中:
- 参数size是指单个元素的大小(其单位是字节而不是位,例如,读取一个int型数据就是4字节);
- 参数count支出要读或写的元素个数,这些元素在buf所指的内存空间中连续存放,共占“size*count”个字节。
即 fread 函数从文件 fp 中读出“size*count”个字节保存到 buf 中,而 fwrite 把 buf 中的“size*count”个字节写到文件 fp 中。最后,函数 fread 和 fwrite 的返回值为读或写的记录数,成功时返回的记录数等于 count 参数,出错或读到文件末尾时返回的记录数小于 count,也可能返回 0。
需要注意的是,尽管 fread 和 fwrite 函数可以对数据进行成块读写,但并不是说一次想读写多少数据就能全部读写多少数据,毕竟缓存有限,而且不同的操作系统的缓存大小也可能不一样。
举例:
fwrite函数
fread函数
stdin & stdout & stderr
任何C程序进程默认会打开三个输入输出流,分别是stdin,stdout,stderr.
- stdin表示标准输入,对应设备是键盘。
- stdout表示标准输出,对应设备是显示器。
- stderr表示标准错误,对应设备是显示器,出现的错误显示在显示器上。
- 这三个流的类型都是FILE*,fopen返回值类型,文件指针。
- 任何一个进程默认会打开3个文件描述符:0(代表标准输入),1(代表标准输出),2(代表标准错误)。
关于fopen函数的详细用法参见链接:
http://c.biancheng.net/view/2054.html
除了以上的文件相关操作,还有fseek、ftell、rewind等函数,可百度了解一下。
文件系统I/O
在C语言中,操作文件之前必须先打开文件;所谓“打开文件”,就是让程序和文件建立连接的过程。打开文件之后,程序可以得到文件的相关信息,例如大小、类型、权限、创建者、更新时间等。在后续读写文件的过程中,程序还可以记录当前读写到了哪个位置,下次可以在此基础上继续操作。
操作文件,除了用C接口,我们还可以采用系统接口来进行文件访问。
接口介绍
open,通过man open
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
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
open函数具体使用哪一个,和具体的应用场景相关,如果目标文件不存在,需要open创建,使用有三个参数的open,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
open函数返回值
在认识返回值之前,先认识一下两个概念:系统调用和库函数,前面在讲Linux进程概念的时候提到过系统调用和库函数,现在再来理解一下。
- 上面的fopen,fclose,fread,fwrite都是C标准库当中的函数,我们称之为库函数(libc)。
- 而open,close,read,write,lseek都属于系统提供的接口,称之为调用接口。
- 通过这张图,系统调用接口和库函数的关系就一目了然。因此可以认为,f#系列的函数都是对系统调用的封装,方便二次开发。
open函数的返回值----文件描述符fd
文件描述符是一个比较小的整数。这个整数是从开始的,当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体----表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*file,指向一张表file_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针。所以,本质上,文件描述符就是该数组的下标。因此,只要拿到文件描述符,就可以找到对应的文件。
文件描述符的分配规则
先看一段代码
我们发现输出的是fd: 3
如果我们关闭0或2
如果close(1),则不会结果输出,因为把标准输出关闭了。
从结果可以总结出文件描述符的分配规则:在files_struct数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。因此就可以解释上面的输出结果了。
重定向
我们依然以关闭文件描述符1,即close(1)为例,看代码:
我们发现,"fd: 1"本来应该输出到显示器上的内容,输出到了文件myfile中,这种现象叫做输出重定向。常见的重定向有:>,>>,<
重定向的本质
printf是C库中的IO函数,一般有stdout文件输出,stdout文件底层访问文件的时候,找到是文件描述符fd: 1,但此时fd: 1下标表示的内容已经更改为新文件的地址,不在是显示器的地址,输出的信息都会写入新的文件中,从而完成了输出重定向。
使用dup2系统调用
函数原型:
#include<unistd.h>
int dup2(fd1,int fd2);
先来一段代码:
先通过0号文件描述符标准输入“hello world!”,运行后,我们发现,我们所输入的内容到名为log的文件中去了
原理如下:
dup2的作用就是可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭(应该是关闭fd2和之前文件表项之间的映射关系,然后再让fd2和fd1指向同一个文件表项。)。如若fd1等于fd2, 则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。
FILE
- 因为IO相关的函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
- 因此,C库中的FILE结构体内部,必定封装了fd.
写段代码研究一下:
如果对进程实现输出重定向:./file > log
再cat log
结果似乎有点出乎意料,我们发现printf和fwrite都输出了两次,而write只输出了一次,并且write的位置跑到了最前面。这是为什么呢?很显然,printf和fwrite输出两次肯定与fork有关。解释如下:
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲的。
- printf和fwrite库函数会自带缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
- 我们放在缓冲区中的数据,不会立即刷新出来,甚至fork之后,直到进程退出后,会统一刷新,写入文件当中。
- 但是fork的时候,父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随机产生了两份数据,所以,就输出了两次printf和fwrite。
- write没有变化,说明没有所谓的缓冲,重定向时直接将内容写入文件,没有放入缓冲区。因此,打印的write在第一个位置。
综上所述,printf和fwrite库函数会自带缓冲区,而write是系统调用没有带缓冲区。另外,我们这里说的缓冲区都是用户级缓冲区。为了提升整机性能,操作系统也会提供相关内核级缓冲区。printf和fwrite是库函数,其缓冲区由C标准库提供。因为,库函数在系统调用的上层,是对系统调用的封装,该缓冲区是二次加上的,又因为是C,所以由C标准库提供,而write是系统调用没有缓冲区。
理解文件系统
当我们在Linux中使用ls -l时看到了该目录下所有文件每个文件的一些具体信息。
每行包含7列:
- 模式
- 硬链接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
ls -l读取存储在磁盘上的文件信息,然后显示出来。
文件信息一般有两份,一份在硬盘里,一份在内存里。
除了使用ls -l读取文件信息,还可以通过stat命令看到文件的更多信息。
下面我们需要理解信息中的inode
这里我们先简单了解一下文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是格式化的时候确定的,并且不可以更改。mke2fs的-b选项可以设定block大小为1024、2048或4096字节。但上图中启动块(Boot Block)的大小是确定的。
分别介绍每个区域代表的含义:
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相
同的结构组成。- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表:存放文件属性,如文件大小,所有者,最近修改时间等。
- 数据区:存放文件内容。
我们通过touch创建一个新文件来看看文件袋属性和数据如何分开存放,它的原理是怎样的。
创建一个新文件主要有4个操作:
1.存储属性
内核先找到一个空闲的i节点(这里是51387073)。内核把文件信息记录到其中
2.存储数据
该文件需要存储在三个空闲块:100,500,800。将内核缓冲区的第一块数据复制到100,下一块复制到500,以此类推。
3.记录分配情况
文件内容按顺序100,500,800存放,内核在inode上的磁盘分布区记录了上述块列表。
4.添加文件名到目录
Linux如何在当前的目录中记录这个文件?内核将入口(51387073,file.c)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
理解软硬链接
硬链接
其实真正找到磁盘上文件的并不是文件名,而是inode。在Linux中可以让多个文件名对应于同一个inode
- test和test1的链接状态完全相同,它们被称为指向文件的硬链接。内核记录了这个连接数,inode 51386833的硬链接数为2.
- 我们在删除文件时干了三件事:1.在目录中将对应的记录删除。3.把改文件的inode对应的位图和数据区中对应的数据块的位图由1变成0。3.将硬连接数减1,如果为0,则将对应的磁盘释放。
软链接
硬链接是通过inode引用另一个文件,软连接是通过名字引用另外一个文件,具体操作如下:
其实,link.s是link的快捷方式,但它们两个互相独立。
硬链接生成的test1不是新文件,因为它的inode和test是同一个。
文件的三个时间
- Access 最后访问时间
- Modify 文件内容最后修改时间
- Change 属性最后修改时间
深度理解动静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候讲不再需要静态库。
- 动态库(.so):程序运行的时候才去链接动态库的代码,多个进程共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
- 在可执行文件开始运行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
生成静态库
add.h
add.c
sub.h
sub.c
main.c
其中这条命令是生成静态库,ar是gnu归档工具,rc表示(replace and create)
此条命令是查看静态库中的目录列表。t:列出静态库中的文件。v:verbose 展示详细信息
生成可执行文件a.out。其中选项-L:指定库路径。-l 指定库名
测试目标文件生成后,删除静态库,程序照样可以运行。
库路径搜索
- 从左到右搜索-L指定的目录
- 由环境变量指定的目录(LIBRARY_PATH)
- 由系统指定的目录:1. /usr/lib 2. /usr/local/lib
头文件搜索路径:-I+库搜索路径
ldd+可执行文件名:列出一个程序所需要的动态链接库
生成动态库
- shared:表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
使用动态库
编译选项
- l:链接动态库,只要库名即可(去掉lib及版本号)
- L:链接库所在的路径
运行动态库
- 拷贝.so文件到系统共享库路径下,一般指/usr/lib
- 更改LD_LIBRARY_PATH。
LD_LIBRARY_PATH是Linux环境名,该环境变量主要用于指定查找共享库(动态链接库)时除了默认路径之外的其他路径。
配置/etc/ld.so.conf.d/,ldconfig更新
使用外部库
系统中有很多库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。