目录
目录
1.先来回顾C文件接口
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
写文件
读文件
- 标准输入文件(stdin),通常对应终端的键盘;
- 标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件都对应终端的屏幕。
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
2.系统调用的文件操作
2.1.接口
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_TRUNC:打开文件后截断,O_APPEND:以追加方式打开; 需要组合使用时,可以采用按位或方式“|”
- O_RDONLY、O_WRONLY、O_RDWR……这些都是系统定义的宏,这些参数只占一个int整形中的一个比特位
返回值:
成功:新打开的文件描述符 失败:-1
所以:open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open(一般设为0644)
write():写入文件
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd: 文件描述符
buf: 存放要写入的数据的缓冲区首地址
count: 想要写入的字节数
返回值:
>=0:成功写入的字节数,0表示什么都没写入
-1: 写入失败,并设置全局变量errno
read():读取文件
ssize_t read (ind fd, char *buf, size_t count);
buf: 数据存储空间,读到的数据存到哪
count: 最大读取内容,一般是指buf空间大小减1
lseek():根据文件指针的位置和偏移量来定位文件指针
off_t lseek(int fd, off_t offset, int whence);
参数
offset: 偏移量
whence: 定位文件流指针 SEEK_SET,SEEK_END,SEEK_CUR
close():退出文件
close(int fd);
系统调用 和 库函数
- 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
- open close read write lseek 都属于系统提供的接口,称之为系统调用接口
系统调用接口和库函数的关系,一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
3.文件描述符 fd
3.1 什么是文件描述符
根据上面的了解,我们知道了文件描述符就是一个小整数
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是
标准输入: 0,(物理设备一般是:键盘)
标准输出: 1,(物理设备一般是:显示器)
标准错误: 2. (物理设备一般是:显示器)
文件描述符就是从0开始的小整数。当打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针files_struct*, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。只要拿着文件描述符,就可以找到对应的文件
3.2文件描述符的分配规则
我们这样发现fd输出为3,那么我们试试将 0(标准输出)关闭
发现关闭0后,输出的fd变为了0;文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
那要是我们关闭1,就是重定向的原理
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
追加重定向:>>
那么重定向的本质就是这样:
那么1和2都是往显示器上打印,那么他们有什么不同呢?
使用输出重定向的时候,只是把标准输出的写入了文件中,而标准错误打印在了屏幕上,输出重定向只是对标准输出的重定向,也就是对文件描述符1的重定向,而2没有改变。
如果没有显式地进行重定向,命令通过文件描述符0从屏幕读取输入,通过文件描述符1和2将输出和错误信息输出到屏幕。但如果我们想从其他文件(I/O设备在Linux中也是文件)读取输入或产生输出,就需要对0、1、2使用重定向了。其语法如下:
重定向的使用有如下规律:
1)标准输入0、输出1、错误2需要分别重定向,一个重定向只能改变它们中的一个。
2)标准输入0和标准输出1可以省略。(当其出现重定向符号左侧时)
3)文件描述符在重定向符号左侧时直接写即可,在右侧时前面加&。
4)文件描述符与重定向符号之间不能有空格!
3.4 FILE
- 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
- 所以C库当中的FILE结构体内部,必定封装了fd,也维护了与C缓冲区相关的内容。
因为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
};
4. 缓冲区
不关闭fd
我们发现在不关闭fd时,会将内容写入到test2.txt文件中
那我们关闭fd
我们发现,此时既没有在显示器上打印,也没写入到test2.txt文件中
但是当我们加上fflush(stdout)后
我们发现又能将内容写入test2.txt文件中了,这是为什么呢?
通过上运行结果发现,在关闭文件描述符后,重定向的操作失败了,其本质原因就是数据是暂存在缓冲区(用户级缓冲区)的。在操作系统内部也是存在一个内核缓冲区(OS缓冲区)的。
用户缓冲区到内核缓冲区的刷新策略有如下几种:
- 立即刷新:不缓冲
- 行刷新(行缓冲 \n),比如,显示器打印
- 缓冲区满了,才刷新(全缓冲),比如,往磁盘文件中写入
当我们向磁盘,显示器等设备写入数据时,一般的流程为,进程运行起来后,数据先是暂存到用户级缓冲区,通过系统调用接口,数据又被暂存到了内核缓冲区,当进程结束时,会自动刷新内核缓冲区的数据到相应的外设中;(从C缓冲区到内核缓冲区也一定是需要fd的)
显示器是行缓冲,即遇到'\n'就会刷新数据到显示器;磁盘是全缓冲,当缓冲区满了以后,才会刷新数据到磁盘上;
当我们在重定向时,其数据的刷新策略也会发生变化,(上面的代码中)本来我们是行缓冲的,但是重定向后就变成了全缓冲;两者都是要通过系统调用接口(open)来完成数据的写入; 在没有关闭文件描述符fd时,我们能看到重定向后的结果,本质是进程结束了,刷新了缓冲区;在关闭文件描述符fd后,既没有向显示器打印,也没有向文件中打印,本质就是,它要通过系统调用接口先将数据暂存到内核缓冲区,待进程结束后,才刷新到相应的外设中,但是fd已经关闭,就不会刷新到内核在刷新到硬件,所以就看不到任何数据;
我们接着看下面的
我们可以看到,在直接运行test3时,write,printf和fwrite都只打印了一次,但是,在重定向到test3.txt文件后,write所打印的虽然也是一次,但是printf和fwrite所打印的却分别打印了2次,这是为什么呢?
原因:
一开始打印在显示器上的刷新策略是:行刷新,在没有fork创建子进程的时候,内容已经刷新到了内核缓冲区。
而当我们重定向刷新策略变为:全缓冲,在fork创建子进程的时候,C语言缓冲区还没有刷新到内核缓冲区,此时fork创建也会拷贝一份缓冲区里的数据,此时fork出来的数据也要写入缓冲区,会发生写时拷贝,所以会有两份数据重定向到文件中,而系统调用不会受到影响。
注:如果想要在重定向情况下也只打印一份,只需要在fork()之前用fflush(stdout)强制刷新缓冲区就行。
4. 理解文件系统
文件在没有被打开的时候,都是被放在磁盘上的,那看一下文件是怎么放在磁盘上的
磁盘主要是通过机械臂上的磁头来读取磁盘上的数据,磁盘由一个或者多个圆盘组成,它们围绕着一根中心主轴旋转,磁盘被组织成磁道,磁道是单个盘片上的同心圆,所有盘面上半径相同的磁道构成了柱面,每一个磁道又按512个字节为单位划分为等分,叫做扇区, 向磁盘读取和写入数据时,要以扇区为单位。
我们使用ls -l的时候看到的除了看到文件名,还看到了文件元数据;每行包含7列
ls -l读取存储在磁盘上的文件信息,然后显示出来
4.1 inode
为了能解释清楚inode我们先简单了解一下文件系统
在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):存放文件内容
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过touch一个新文件来看看如何工作
创建一个新文件主要有一下4个操作
1. 存储属性
内核先找到一个空闲的i节点(这里是788107)。内核把文件信息记录到其中。
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据
复制到300,下一块复制到500,以此类推。
3. 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(788107,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
4.2 硬链接
创建硬链接
ln 命令:创建链接(软连接和硬链接)
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode
硬链接本质上不是一个独立的文件,而是一个文件名和inode编号的映射关系,因为自己没有独立的inode,且删除也不会影响原本的文件
那么为什么默认创建一个的文件的硬链接数(引用计数)是1,而创建的空目录的连接数为2呢?
因为,文件的本身的文件名和它对应的inode是一组映射关系 , 而目录除了本身文件名和对应的inode形成一组映射关系外,目录中所带的隐藏文件 [.] 这个文件也会与自己产生映射
当我们在一个空目录下在新建一个目录,会发现这个原来的空目录的引用计数变为了3,因为在新建的目录中,会有一个隐藏文件[..]与它产生映射。
4.3软连接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法
软连接就相当于我们在window下的快捷方式
总结:软硬链接的区别:本质是是否是独立文件,有无独立inode;用途:软链接可以指向特定的文件方便进行快速索引,硬链接是能进行相对路径设置
补充:
- 软链接文件是一个独立的文件有自己的inode节点,通过数据中保存的源文件路径访问源文件
- 硬链接是文件的一个目录项,与源文件共用同一个inode节点,直接通过自己的inode节点访问源文件
- 不同分区有可能有不同文件系统,因此硬链接不能跨分区建立;软连接可以跨文件系统进行连接,硬链接不可以
- 当删除源文件时,软链接文件失效
- ln生成符号链接文件指的是 ln -s 生成软链接文件
文件的ACM
下面解释一下文件的三个时间:
- Access 最后访问时间(在较新的Linux内核当中,Access时间不会被立即更新,而是有一定时间的间隔,OS才会自动进行更新时间)
- Modify 文件内容最后修改时间(修改文件的内容可能会修改文件的属性:比如改变文件的大小)
- Change 属性最后修改时间
5.动态库和静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
5.1 静态库
但是如果 .o 文件太多,我们使用不方便,那么我们就将这些 .o文件一起合并成一个包,这就是静态库
那么我们用Makefile来自动生成静态库
那么我们怎么让别人能用这个库呢
还有一种方法将对应的库和头文件拷贝到默认路径下(/usr/bin,、lib64, /usr/include),但自己写的库是不建议写入系统路径下,可能会污染系统默认路径下的头文件与库,所以这种方法严重不推荐
5.2 动态库
形成动态库
用Makefile同时生成静态库与动态库
在使用动态库时
那么要怎么办呢?
方法一.将路径导入 LD_LIBRARY_PATH 这个环境变量
LD_LIBRARY_PATH 该环境变量主要用于指定查找共享库(动态链接库)时除了默认路径之外的其他路径。
记住:LD_LIBRARY_PATH是内存级的环境变量,在我们退出后在进入时会重新在内存去拿,那么我们之前写入的就被清理掉了,所以这只是个临时方案
那有什么永久的方案呢?
方法2:ldconfig 配置/etc/ld.so.conf.d/,ldconfig更新(修改系统的配置文件)
方法三:拷贝.so文件到系统共享库路径下, 一般指/usr/lib(不建议)
方法四:软连接方案
注意:
如果只有静态库,我们创建可执行文件时,只能静态链接到我们的程序中
如果只有动态库,我们创建可执行文件时,只能动态态链接到我们的程序中
如果动态链接与静态链接都存在,就提供两种版本的库,那么当我们创建可执行文件时,一般默认是进行动态链接(gcc和g++优先链接动态库),如果想强制进行静态链接,就需要我们在生成可执行文件的语句后加 -static