四、Linux系统IO编程—文件IO的内核数据结构与文件原子操作

一、缓存buff说明

一般设置缓存 buff 的大小是有一定的规律的,就是根据磁盘块的大小来定。

Linux下输入命令: df -k 查看磁盘
 在这里插入图片描述
可以用命令查看下 /dev/sda1 磁盘的磁盘说明

sudo tune2fs -l /dev/sda1

在这里插入图片描述
Block size 就是磁盘块的大小,这个磁盘块的大小为 4M ,那么就可以设置缓存 buff 大小为 4096,一次就可以将数据写入。
设置的缓存大小最好与磁盘块的大小保持一致,有利于提升读写文件的效率。

二、文件IO的内核数据结构

一个打开的文件在内核中使用三种数据结构表示:

  • 文件描述符
    • 文件描述符标志
    • 文件表项指针
  • 文件表项
    • 文件状态标志
    • 读、写、追加、同步和非阻塞等状态标志
    • 当前文件偏移量
    • i 节点表项指针
    • 引用计数器
  • i 节点
    • 文件类型和对该文件的操作函数指针
    • 当前文件长度
    • 文件所有者
    • 文件所在的设备、文件访问权限
    • 指向文件数据在磁盘上所在位置的指针等

在这里插入图片描述

注意事项:

  • 文件描述表是相对于进程而言的即不同的进程有自己的文件描述符表(都是从0开始的)但是各进程的文件描述表由系统维护,在不同的进程当中相同的文件描述符有可能指向相同的文件表项,也可能指向不同的文件表项这都是视情况而定的。也就是说:建立在打开的文件基础之上的。
  • 文件表项也称打开的文件表该表的数组下标就是我们熟知的文件句柄。不同的文件有不同的文件句柄也就是说建立在文件基础之上的,不管文件打开多少次在该表中只有一个表项;由于文件偏移量在该表中,这就存在一个问题就是;当不同的进程打开同一文件时共享文件偏移量。

三、文件的原子操作

所谓的原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就是说,它的最小的执行单位,不能有比它更小的执行单元,因此这里的原子实际是使用了物理学里物质微粒的概念。

摘自《Linux/Unix系统编程手册》 所有系统调用都是以原子操作方式执行的。这里是指内核保证了某系统调用中的所有步骤会作为独立操作而一次性执行,其间不会为其它进程或线程所中断。 原子性是某些操作得以圆满成功的关键所在。 特别是它规避了竞争状态(race conditions)。竞争状态是这样一种情形:操作共享资源的两个进程(或线程),其结果取决于一个无法预期的顺序,即这些进程(或线程)获得CPU使用权的先后相对顺序。
文件的原子操作

主要是open 函数中的文件追加和文件创建

  • 文件追加
    打开文件时,使用 O_APPEND 标志,进程对到文件偏移量调整和数据追加成为原子操作
    内核每次对文件写之前,都将进程的当前偏移量设置为该文件的尾端。这样不再需要 lseek 来调整偏移量
  • 文件创建
    对 open 函数的 O_CREAT 和 O_EXCL 的同时使用,而该文件存在,open 将失败,否则创建该文件,并且使得文件是否存在的判定和创建过程成为原子操作。
案例1

两个进程对同一文件进行追加,没有使用 append 的时候。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdlib.h>

int main(int argc,char* argv[])
{
	if(argc<3)
	{
		fprintf(stderr,"usage:%s content destfile\r\n",argv[0]);
		exit(1);
	}

	int fd;
	size_t size;
	int ret;
	/*打开一个新的文件*/
	fd=open(argv[2],O_WRONLY);
	if(fd<0)
	{
		perror("open file fail\r\n");
		exit(1);
	}
	
	/*定位到文件的尾部*/
	ret=lseek(fd,0L,SEEK_END);
	if(ret==-1)
	{
		perror("lseek error\n\r");
		close(fd);
		exit(1);
	}
	sleep(10);
	/*往文件追加内容*/
	size=strlen(argv[1])*sizeof(char);
	if(write(fd,argv[1],size)!=size)
	{
		perror("write error!\n\r");
		close(fd);
		exit(1);
	}
	return 0;
}

在这里插入图片描述

开启两个终端,同时运行编译成功的程序,前一个终端向appen.txt写入aaaaa,后一个终端向append.txt写入AAAAA。当程序运行完成之后,得到append.txt的内容为AAAAA。确认后一个终端写入的内容覆盖了前一个终端写入的内容。
为什么出现这种现象?
第一个进程运行的时候,文件表项中的当前偏移量来源于 i 节点的文件长度(即调用 lseek ),第二个进程运行的时候也是用 lseek 来获取偏移量,两个进程都是在写入数据之前会获得一个偏移量,但是 i 节点中的文件长度没有增加,所以文件表项中的 当前偏移量 依然未变,因此第二个进程追加的内容覆盖掉了第一个进程中的内容。

案例2

要想不覆盖,则要使用原子操作。将 open 和 注释掉 lseek 的代码做修改。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdlib.h>

int main(int argc,char* argv[])
{
	if(argc<3)
	{
		fprintf(stderr,"usage:%s content destfile\r\n",argv[0]);
		exit(1);
	}

	int fd;
	size_t size;
	int ret;
	/*打开一个新的文件*/
	fd=open(argv[2],O_WRONLY|O_APPEND);
	if(fd<0)
	{
		perror("open file fail\r\n");
		exit(1);
	}
	
	/*定位到文件的尾部*/
	/*ret=lseek(fd,0L,SEEK_END);
	if(ret==-1)
	{
		perror("lseek error\n\r");
		close(fd);
		exit(1);
	}
	*/
	sleep(10);
	/*往文件追加内容*/
	size=strlen(argv[1])*sizeof(char);
	if(write(fd,argv[1],size)!=size)
	{
		perror("write error!\n\r");
		close(fd);
		exit(1);
	}
	return 0;
}

在这里插入图片描述

加了 O_APPEND 后,write 函数做了几件事情,此时整个 write 成为一个原子操作,只有当第一个进程的 write 执行完后,第二个进程的 write 才后执行:

  • 从 i 节点中读取文件长度作为当前偏移量
  • 往文件中写入数据
  • 修改 i 节点中文件操作
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值