文章目录
前言
在上一篇文章中简单的介绍了文件I/O的概念以及open、read、write、close等函数。
今天这篇文章在上一篇的基础上进行了一些拓展。
一、Linux系统下文件如何管理
静态文件
扇区(sector)、块(block),扇区是磁盘存储的单位,一个块包含多个扇区
。
inode
磁盘分区:
管理表项(inode表) | 数据区 |
inode table,也就是inode表存放在我们磁盘的inode区,在表中有很多的inode节点,它是一个数据结构,记录了文件的相关信息,每一个文件都会对应到一个inode节点
(需要注意的是文件的文件名不记录在inode中
)。
可以用ls -i 来查看文件的inode。
补充说明:在Linux系统内部是使用inode来辨别不同文件,而不是使用文件名,文件名只是对于用户而言一种容易记得inode别称
而已,文件名称记录在文件数据块(data block)中。
PCB
PCB(Process control Block),进程控制块,是一个数据结构。
内核
会为每一个进程设置一个PCB数据块
。目的是为了方便管理进程
。其记录了进程状态,进程的运行特征等。
二、错误编号errno
errno变量
它是一个全局变量,需要包含头文件<errno.h>。它是一个int型的变量,作用是用来记录错误的编号
。大部分系统函数在失败的时候也会设置errno
,可以通过man手册查看确认是否会设置errno以及设置值。
strerror()函数
如果只是得到一个错误编号而不知道错误信息是没有用的。这个函数就是你给它一个错误编号,它能给你返回其对应的信息
。
perror函数
这是我们最常用的,不需要我们调用print去打印
,可以直接得到错误编号的描述信息并打印出来
,并且不需要传入错误信息
。
使用示例:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(void)
{
int fd;
printf("errno = %d\n",errno);
fd = open("./test.txt",O_RDONLY);
if(-1 == fd)
{
printf("errno = %d\n",errno);
perror("");
perror("open error");
return 1;
}
return 0;
}
编译后运行结果如下:
可以看到,perror(“”)这一语句的结果是将错误信息“No such flie or directory”打印出来,而perror(“open error”)的结果是在错误信息前面加上我们传入的字符串“open error”,并且在我们传入的字符串和错误信息之间还贴心的帮我们加上了冒号。
三、空洞文件
示意图:
出现原因:
lseek可将指针指向文件尾部之后,如果在这时候再向其写入数据,中间部分便出现了空洞,包含空洞的文件较为空洞文件。如示意图所示,文件里面有1k数据,但是通过lseek,将读写指针移动到2k处,此时再写文件,便会从2k处开始,中间就留下了一个1k的空洞。
空洞文件的应用场景
在多线程下对文件的写操作,每个线程分别负责自己的区域,同时写入、互不干扰。只要文件还没有被写完,那它就是一个空洞文件
,当所有线程都把自己的区域写完后,这些空洞就被填满了。
ls和du命令查看空洞文件大小
ls查到的大小是文件的大小,包括了空洞部分的大小和实际数据大小
;
du查到的大小是文件的实际大小
。
四、O_TRUNC和O_APPEND标志
O_TRUNC标志
截断文件、丢弃文件中的内容,将文件大小变为0。类似于删除并新建一个文件
。
O_APPEND标志
保证每次调用write()时都是从文件末尾开始写入。移动指针到末尾和写入是原子操作
,是一个整体,不能被分割。
注:O_APPEND标志对write()函数造成影响,但不会影响read操作;
加上标志后lseek操作就不会影响write操作了(无论你将指针移到哪,我都从最后一个开始操作
)
五、同一文件被多次打开
多次open打开同一文件:同一个文件产生多个文件描述符
。
要注意的是,文件描述符与文件并不一定是1对1的关系,而是n对1的关系
,一个文件可以对应多个文件描述符
,使用其对应的任意一个文件描述符都可以对它进行读、写操作
。(常见于文件共享)
使用多个文件描述符对同一个文件进行写操作
需要注意的问题:需要注意数据的覆盖问题。
如何解决问题:使用O_APPEND标志,保证每次写入操作都是在文件末尾开始写入,其原子操作特性
能避免数据被覆盖。
注意:在同一文件被多次打开的时候,每一个文件描述符(fd)都要记得用close关闭。
请看下面一段代码。
int fd1;
int fd2;
fd1=open("./test.txt",O_WRONLY | O_TRUNC | O_APPEND);
if(-1 == fd1)
{
perror("open error");
return 1;
}
fd2=open("./test.txt",O_WRONLY | O_APPEND);
if(-1 == fd2)
{
perror("open error");
close(fd1);
return 1;
}
close(fd1);
close(fd2);
需要注意的点是,如果fd2打开失败,那么需要关闭fd1再return出去,因为在前面fd1已经成功打开了,而在if(-1==fd2)这判断语句中的return,会直接跳出函数不再执行return后面的代码,所以需要在此处补上close(fd1)。
六、文件描述符的复制
文件描述符的原理
对文件描述符进行复制,得到一个它的副本。通过复制得到的副本与被复制的文件描述符两者之间是平等的
,它们两个都会指向同一个文件表
(注意与前面同一文件被多次打开,多个文件描述符指向同一个文件的区别,前面那种情况是每个文件描述符对应一个文件表,只是表中inode指针指向了相同的inode节点
),inode指针指向同一个inode,即指向同一个文件。
dup()函数
函数原型:
int dup(int oldfd);
作用:对文件描述符的复制操作。
参数分析:顾名思义,oldfd就是旧的文件描述符的意思,即你想要复制的文件描述符,传入oldfd,对目标文件描述符进行复制。
返回值:成功时返回newfd,即新的文件描述符,失败的时候返回-1。
dup2()函数
函数原型:
int dup2(int oldfd,int newfd);
作用:dup()函数的升级版,与dup的区别在于用户可以自己指定得到的新的文件描述符。如果指定的数字已经被使用,会从这个数字往后找,找到最小的、可以使用的文件描述符
。
返回值:与dup一样,成功时返回newfd,失败时候返回-1。
七、文件共享
文件共享指的是同一个文件(同一个inode)被多个独立的读写体同时进行I/O操作。
文件共享的常见实现方式:
- 多个不同的进程间实现共享;
- 同一个进程中的多个线程间共享。
- 当两个不同文件描述符指向两个文件表,文件表中的inode指针指向同一个inode节点,这样就实现了共享文件
- 使用dup操作进行文件的复制,两个文件描述符指向同一个文件表,这样也能实现共享文件。
如图所示:
文件共享存在竞争冒险:
在文件共享的时候,它们对同一个文件进行写操作,这时候就会出现一种竞争关系
。处于竞争状态操作共享资源的俩个进程(线程)其操作之后得到的结果往往是不可预期的,这些进程(线程)获得CPU的使用权先后顺序是不可预期的
,完全由操作系统调配,这就是竞争状态。
原子操作规避竞争冒险:
可以使用原子操作来规避竞争冒险的发生。
- O_APPEND标志:
这个标志添加后,每次在调用write操作的时候,系统都会将移动指针ptr移动至文件末尾再写入数据,移动指针和写入数据是一个原子操作,要么全做要么全不做。这样可以保证不会覆盖其他进程(线程)的数据
。 - pread()和pwrite()函数
功能作用和read、write函数一样,区别在于这两者可以实现原子操作
,在调用的时候需要传入一个位置偏移量offset
,会在读写操作前先将读写指针移动到offset指向的位置之后再进行读写操作,这个过程也是一个原子操作。
函数原型:
ssize_t pread(int fd,void *buf,size_t count,off_t offset);
ssize_t pwrite(int fd,const void*buf,size_t count,off_t offset);
与read、write的区别:
①调用pread、pwrite函数时,无法中断其定位和读操作
(原子操作);
②不更新文件表中的当前位置偏移量
。
- O_EXCL标志
通常和O_CREAT标志一起使用
,如果文件存在会返回-1。添加这个标志使得创建文件和判断文件是否存在合为一个原子操作
。在文件共享中,多个文件打开共享文件可能也会存在竞争冒险,若没有加O_EXCL标志,假如A、B进程都判断了文件不存在,此时A创建文件比B早,等B再创建的时候就会造成错误。所以将这两个操作合并为一个原子操作就可以避免这种情况发生
。
八、截断文件
截断操作概念:
当文件的大小大于参数所指定的大小
,那么多出来的那一部分丢弃
,像是将文件截断;当文件的大小小于参数所指定的大小
,则对其进行扩展
,对扩展部分进行读取将得到空字节"\0"
。
truncate()函数:
函数原型:
int truncate(const char*path,off_t length);
参数分析:path是需要进行操作的文件其路径;length是需要截断的长度。
返回值:成功返回0,失败返回-1。
ftruncate()函数:
函数原型:
int ftruncate(int fd,off_t length);
与上一个函数作用相同,区别在于它是通过文件描述符对文件进行截断操作,通常用于已经通过open函数打开过的文件
。
如果文件还没被打开过,就用truncate,反之用ftruncate。
注意:在使用ftruncate()函数进行文件截断操作之前,必须调用open()函数打开该文件得到描述符,并且必须要具备有可写权限,即在open()打开文件时需要指定O_WRONLY或者O_RDWR
。
九、fcntl和ioctl函数
fcntl()函数:
函数原型:
int fcntl(int fd,int cmd,.../*arg*/);//可变参函数
作用:可以对fd做一系列的控制操作
,例如复制一个文件描述符(相当于dup、dup2)、获取/设置文件描述符标志、获取/设置文件状态标志等,类似于一个多功能文件描述符管理工具箱。
参数含义:
- fd为文件描述符;
- cmd为操作命令,表示我们对fd要进行的操作,cmd支持许多操作命令,可以打开man手册查看操作命令的详细介绍,这些命令都是以“F_”开头的,如F_DUPFD、F_GETFD、F_SETFD等。cmd操作命令
大致可以分为以下五种功能
:
- 复制文件描述符(cmd=F_DUPFD或cmd=F_DUPFD_CLOEXEC);
- 获取/设置文件描述符标志(cmd=F_GETFD或cmd=F_SETFD);
- 获取/设置文件状态标志(cmd=F_GETFL或cmd=F_SETFL);
- 获取/设置异步IO所有权(cmd=F_GETOWN或cmd=F_SETOWN);
- 获取/设置记录锁(cmd=F_GETLK或cmd=F_SETLK);
- fcntl是一个
可变参函数
,第三个参数需要根据不同的cmd来传入对应的实参
返回值:执行成功时其返回值与cmd有关,执行失败时返回-1。
ioctl()函数:
ioctl可以被认为是一个文件IO操作的杂物箱,可以处理的事情非常杂且不统一,一般用于操作特殊文件或硬件外设,如可以通过ioctl获取LCD相关信息等。
函数原型:
int ioctl(int fd,unsigned long request,...);
参数含义:
- fd是文件描述符;
- request参数与具体要操作的对象有关,没有统一值,表示向文件描述符请求相应的操作。
- 该函数是可变参函数,第三个参数可以根据request参数来决定。
返回值:成功返回0,失败返回-1。