06_实验六_读文件和写文件

发布时间 2023-12-07 23:30:34作者: 彬彬zhidao

读文件和写文件

实验目的

  • 了解在EOS应用程序中读文件和写文件的基本方法。
  • 通过为FAT12文件系统添加写文件功能,加深对FAT12文件系统和磁盘存储器管理原理的理解。

文件系统驱动程序的作用

用户对文件的读写请求转换为 对磁盘扇区的读写请求,并负责对磁盘扇区进行管理。

实验内容

编写代码调用 EOS API 函数读取文件中的数据

image-20231204220603223

按照下面的步骤查看 EOS 应用程序读取文件中数据的执行结果:

  1. 使用 OS Lab 打开在本实验 3.1 中创建的 EOS 应用程序项目。

  2. 在“项目管理器”窗口中双击 Floppy.img 文件,使用 FloppyImageEditor 工具打开此软盘镜像。

  3. 将本实验文件夹中的 a.txt 文件添加到软盘镜像的根目录中。打开 a.txt 文件查看其中的数据。

  4. 点击 FloppyImageEditor 工具栏上的保存按钮,关闭该工具。

  5. 使用 FileApp.c 文件中的源代码替换 EOS 应用程序项目中 EOSApp.c 文件内的源代码。

  6. 按 F7 生成修改后的 EOS 应用程序项目。

  7. 按 F5 启动调试。自动运行 EOS 应用程序 EOSApp.exe 时,会由于输入的命令行参数无效而失败。

  8. 在 EOS 控制台中输入命令“A:\EOSApp.exe A:\a.txt”后按回车,EOSApp.exe 会读取 a.txt 文

件中的内容并显示在屏幕上,如图

image-20231207104632365

  1. 结束此次调试。

// 启动调试 EOS 应用程序前要特别注意下面的问题:
//
// 1、如果要在调试应用程序时能够调试进入内核并显示对应的源码,
// 必须使用 EOS 核心项目编译生成完全版本的 SDK 文件夹,然
// 后使用此文件夹覆盖应用程序项目中的 SDK 文件夹,并且 EOS
// 核心项目在磁盘上的位置不能改变。
//

调试 FAT12 文件系统的读文件功能

为FAT12文件系统添加写文件功能

由于写文件功能会涉及到为文件分配新的簇、修改文件大小等问题,所以这里首先完成一个最简单的情况:向一个空文件中写入数个字节的数据。

  1. 使用 OS Lab 打开本实验 3.1 中创建的 EOS 内核项目。
  2. 从“项目管理器”窗口中打开源文件 io/driver/fat12.c,目前 fat12.c 中的函数 FatWriteFile(第 824 行)为空。
  3. 将本实验文件夹中的FatWriteFile.c文件拖动到OS Lab窗口中打开,使用该文件中FatWriteFile函数的函数体替换 fat12.c 文件中 FatWriteFile 函数的函数体。
  4. 在“项目管理器”窗口中双击 Floppy.img 文件,使用 FloppyImageEditor 工具打开此软盘镜像。
  5. 打开本实验 3.1 中创建的 EOS 应用程序项目文件夹,将 Release 文件夹中的 EOSApp.exe(没有调试信息)添加到软盘镜像中。
  6. 将本实验文件夹中的 a.txt、b.txt、c.txt 和 d.txt 文件添加到软盘镜像中。
  7. 点击 FloppyImageEditor 工具栏上的保存按钮,关闭该工具。
  8. 按 F7 生成修改后的 EOS 内核项目。注意,要使用 Debug 配置。
  9. 按 F5 启动调试。

image-20231207111840116

image-20231207112054060

image-20231207112340705

image-20231207112523299

实验步骤

EOS中FAT12文件系统相关源代码分析

分析EOS中FAT12文件系统的相关源代码,简要说明EOS实现FAT12文件系统的方法,包括主要数据结构与文件基本操作的实现等。

文件系统是建立在磁盘等块设备之上的一个逻辑层,读写文件时,文件系统驱动会将文件读写请求转换为对磁盘扇区的读写请求,最终由磁盘驱动程序完成对磁盘扇区的读写。

EOS为应用程序提供了一组可以操作文件的API函数,包括打开文件(CreateFile)、关闭文件(CloseHandle)、读文件(ReadFile)和写文件(WriteFile)

打开文件时使用的CreateFile定义如下

 EOSAPI HANDLE CreateFile(
 IN PCSTR FileName,
 IN ULONG DesiredAccess,
 IN ULONG ShareMode,
 IN ULONG CreationDisposition,
 IN ULONG FlagsAndAttributes
 )

该函数被调用后会依次调用FatCreate、FatOpenExistingFile、FatOpenFile和FatOpenFileDirectory。

CloseHandle函数定义如下

 EOSAPI BOOL CloseHandle(
 IN HANDLE Handle)

该函数被调用后会依次调用FatClose、FatCloseFile。

读文件函数ReadFile定义如下

 EOSAPI BOOL ReadFile(
 IN HANDLE Handle,
 OUT PVOID Buffer,
 IN ULONG NumberOfBytesToRead,
 OUT PULONG NumberOfBytesRead)

该函数被调用后会依次调用ObRead、IopReadFileObject、FatRead和FatReadFile。

写文件函数ReadFile定义如下

 EOSAPI BOOL WriteFile(
 IN HANDLE Handle,
 IN PVOID Buffer,
 IN ULONG NumberOfBytesToWrite,
 OUT PULONG NumberOfBytesWritten)

该函数被调用后会依次调用ObWrite、IopWriteFileObject、FatWrite和FatWriteFile。

EOS中FAT12文件系统读文件过程的跟踪

简要说明在本部分实验过程中完成的主要工作,包括对读文件的跟踪等,总结EOS中读文件的实现方法

替换EOSApp.c为学生包中的FileApp.c的内容,随后将a.txt复制到img镜像中,生成项目并调试输入A:\eosapp.exe A:\a.txt,得到结果如下

结束调试,将fat12.c拖入IDE窗口,在EOSApp.c文件中的ReadFile函数代码行添加断点,开始调试,输入A:\EOSApp.exe A:\a.txt,在fat12.c文件中FatReadFile函数的开始处添加一个断点。随后继续运行调试,停下后对Vcb和File进行监视,得到以下结果

同时观察到offset值为0,BytesToRead为256。

打开调用堆栈监视流程

继续单步调试观察相关变量变化,最后观察到虚拟机正常输出

为EOS的FAT12文件系统添加写文件功能

给出实现方法的简要描述、源代码、测试及结果等

首先导入学生包中的代码并测试

观察到能正常实现文件读写

修改代码使其支持跨扇区写文件,实现思路为将写入数的数据分为三部分:头扇区、中间扇区和尾扇区,在写入头扇区和尾扇区的中间,插入一个循环,用于写入中间跨越的多个中间扇区,其特点为均是写入满扇区,写入的数据均为512个字节,在此之前计算出跨越的扇区个数即可。

代码及测试如下

STATUS
FatWriteFile(
	IN PVCB Vcb,
	IN PFCB File,
	IN ULONG Offset,
	IN ULONG BytesToWrite,
	IN PVOID Buffer,
	OUT PULONG BytesWriten
	)
	{
	STATUS Status;

	// 由于在将新分配的簇插入簇链尾部时,必须知道前一个簇的簇号,
	// 所以定义了“前一个簇号”和“当前簇号”两个变量。
	USHORT PrevClusterNum, CurrentClusterNum;
	USHORT NewClusterNum;
	ULONG ClusterIndex;
	ULONG FirstSectorOfCluster;
	ULONG OffsetInSector;
	
	ULONG i;

	// 写入的起始位置不能超出文件大小(并不影响增加文件大小或增加簇,想想原因?)
	if (Offset > File->FileSize)
		return STATUS_SUCCESS;

	// 根据簇的大小,计算写入的起始位置在簇链的第几个簇中(从 0 开始计数)
	ClusterIndex = Offset / FatBytesPerCluster(&Vcb->Bpb);
	
	//
	// 计算起始地址在簇中的第几个字节
	ULONG BytesNumOfCluster = Offset - ClusterIndex * FatBytesPerCluster(&Vcb->Bpb);
	
	ULONG NeedSplit;							// 分割标志
	ULONG ByteFommer = BytesToWrite;			// 前一半需要写入的字节
	ULONGByteLatter = 0;						// 后一半需要写入的字节
	ULONG SpanOfSector = 0;						// 跨过的扇区数
	
	PCHAR FrontBuffer;							// 		 前
	PCHAR MidBuffer;							// 缓冲区 中
	PCHAR LatterBuffer;							//		 后
	
	// 如果写入的内容没有跨扇区,则无需切分
	if((512 - BytesNumOfCluster)>= BytesToWrite){
		NeedSplit = 0;
	}
	//否则要分成两半
	else{
		NeedSplit = 1;
		ByteFommer = 512 - BytesNumOfCluster;
		NumOfLatterBuffer = (BytesToWrite - ByteFommer) % 512;
		SpanOfSector = (BytesToWrite - ByteFommer)/512;
	}	
	

	if(NeedSplit == 0){
		FrontBuffer=Buffer;
	}
	else{
		//划分缓存区
		FrontBuffer = Buffer;
		MidBuffer = &Buffer[ByteFommer];
		LatterBuffer = &Buffer[ByteFommer + SpanOfSector * 512];
	}
	
	// 分为三个部分写入,先写入头一个扇区,然后循环写入中间满扇区(均是512字节),最后写入尾扇区
	// 写入头扇区
	// 顺着簇链向后查找写入的起始位置所在簇的簇号。
	PrevClusterNum = 0;
	CurrentClusterNum = File->FirstCluster;
	for (i = ClusterIndex; i > 0; i--) {
		PrevClusterNum = CurrentClusterNum;
		CurrentClusterNum = FatGetFatEntryValue(Vcb, PrevClusterNum);	
	}

	// 如果写入的起始位置还没有对应的簇,就增加簇
	if (0 == CurrentClusterNum || CurrentClusterNum >= 0xFF8) {

		// 为文件分配一个空闲簇
		FatAllocateOneCluster(Vcb, &NewClusterNum);

		// 将新分配的簇安装到簇链中
		if (0 == File->FirstCluster)
			File->FirstCluster = NewClusterNum;
		else
			FatSetFatEntryValue(Vcb, PrevClusterNum, NewClusterNum);
		
		CurrentClusterNum = NewClusterNum;
	}

	// 计算当前簇的第一个扇区的扇区号。簇从 2 开始计数。
	FirstSectorOfCluster = Vcb->FirstDataSector + (CurrentClusterNum - 2) * Vcb->Bpb.SectorsPerCluster;
	
	// 计算写位置在扇区内的字节偏移。
	OffsetInSector = Offset % Vcb->Bpb.BytesPerSector;

	// 为了简单,暂时只处理一个簇包含一个扇区的情况。
	// 并且只处理写入的数据在一个扇区范围内的情况。
	Status = IopReadWriteSector( Vcb->DiskDevice,
									FirstSectorOfCluster,
									OffsetInSector,
									(PCHAR)FrontBuffer,
									ByteFommer,
									FALSE );

	if (!EOS_SUCCESS(Status))
		return Status;

	// 如果文件长度增加了则必须修改文件的长度。
	if (Offset + BytesToWrite > File->FileSize) {
		File->FileSize = Offset + BytesToWrite;
		
		// 如果是数据文件则需要同步修改文件在磁盘上对应的 DIRENT 结构
		// 体。目录文件的 DIRENT 结构体中的 FileSize 永远为 0,无需修改。
		if (!File->AttrDirectory)
			FatWriteDirEntry(Vcb, File);
	}

	// 循环写入中间满扇区
	Offset += ByteFommer;
	
	ULONG k = SpanOfSector;
	while(k > 0){
		// 顺着簇链向后查找写入的起始位置所在簇的簇号。
		PrevClusterNum = 0;
		CurrentClusterNum = File->FirstCluster;
		// 根据簇的大小,计算写入的起始位置在簇链的第几个簇中(从 0 开始计数)
		ClusterIndex = (Offset) / FatBytesPerCluster(&Vcb->Bpb);
		for (i = ClusterIndex; i > 0; i--) {
			PrevClusterNum = CurrentClusterNum;
			CurrentClusterNum = FatGetFatEntryValue(Vcb, PrevClusterNum);
			//CurrentClusterNum = FatGetFatEntryValue(Vcb,CurrentClusterNum);	
		}
		
	
		// 如果写入的起始位置还没有对应的簇,就增加簇
		if (0 == CurrentClusterNum || CurrentClusterNum >= 0xFF8) {
	
			// 为文件分配一个空闲簇
			FatAllocateOneCluster(Vcb, &NewClusterNum);
	
			// 将新分配的簇安装到簇链中
			if (0 == File->FirstCluster)
				File->FirstCluster = NewClusterNum;
			else
				FatSetFatEntryValue(Vcb, PrevClusterNum, NewClusterNum);
			
			CurrentClusterNum = NewClusterNum;
		}
		// 计算当前簇的第一个扇区的扇区号。簇从 2 开始计数。
		FirstSectorOfCluster = Vcb->FirstDataSector + (CurrentClusterNum - 2) * Vcb->Bpb.SectorsPerCluster;
		
		// 计算写位置在扇区内的字节偏移。
		OffsetInSector = (Offset) % Vcb->Bpb.BytesPerSector;
		
		// 并且只处理写入的数据在一个扇区范围内的情况。
		Status = IopReadWriteSector( Vcb->DiskDevice,
										FirstSectorOfCluster,//当前簇的第一个扇区的扇区号
										OffsetInSector,//写位置在扇区内的字节偏移
										(PCHAR)MidBuffer,//缓存区
										512,//要写入多少字节
										FALSE );
	
		if (!EOS_SUCCESS(Status))
			return Status;
	
		// 如果文件长度增加了则必须修改文件的长度。
		if (Offset + 512 > File->FileSize) {
			File->FileSize = Offset + BytesToWrite;
			
			// 如果是数据文件则需要同步修改文件在磁盘上对应的 DIRENT 结构
			// 体。目录文件的 DIRENT 结构体中的 FileSize 永远为 0,无需修改。
			if (!File->AttrDirectory)
				FatWriteDirEntry(Vcb, File);
		}
		MidBuffer = &MidBuffer[512];
		Offset += 512;
		k--;
	}

	
	// 写入尾扇区
	if(NeedSplit == 1 &&ByteLatter > 0){
		// 顺着簇链向后查找写入的起始位置所在簇的簇号。
		PrevClusterNum = 0;
		CurrentClusterNum = File->FirstCluster;
		// 根据簇的大小,计算写入的起始位置在簇链的第几个簇中(从 0 开始计数)
		ClusterIndex = (Offset) / FatBytesPerCluster(&Vcb->Bpb);
		for (i = ClusterIndex; i > 0; i--) {
			PrevClusterNum = CurrentClusterNum;
			CurrentClusterNum = FatGetFatEntryValue(Vcb, PrevClusterNum);
		//CurrentClusterNum = FatGetFatEntryValue(Vcb,CurrentClusterNum);	
		}
	
		// 如果写入的起始位置还没有对应的簇,就增加簇
		if (0 == CurrentClusterNum || CurrentClusterNum >= 0xFF8) {
	
			// 为文件分配一个空闲簇
			FatAllocateOneCluster(Vcb, &NewClusterNum);
	
			// 将新分配的簇安装到簇链中
			if (0 == File->FirstCluster)
				File->FirstCluster = NewClusterNum;
			else
				FatSetFatEntryValue(Vcb, PrevClusterNum, NewClusterNum);
			
			CurrentClusterNum = NewClusterNum;
		}
		// 计算当前簇的第一个扇区的扇区号。簇从 2 开始计数。
		FirstSectorOfCluster = Vcb->FirstDataSector + (CurrentClusterNum - 2) * Vcb->Bpb.SectorsPerCluster;
		
		// 计算写位置在扇区内的字节偏移。
		OffsetInSector = (Offset) % Vcb->Bpb.BytesPerSector;
	
		// 为了简单,暂时只处理一个簇包含一个扇区的情况。
		// 并且只处理写入的数据在一个扇区范围内的情况。
		Status = IopReadWriteSector( Vcb->DiskDevice,
										FirstSectorOfCluster,//当前簇的第一个扇区的扇区号
										OffsetInSector,//写位置在扇区内的字节偏移
										(PCHAR)LatterBuffer,//缓存区
										NumOfLatterBuffer,//要写入多少字节
										FALSE );
	
		if (!EOS_SUCCESS(Status))
			return Status;
	
		// 如果文件长度增加了则必须修改文件的长度。
		if (Offset +ByteLatter > File->FileSize) {
			File->FileSize = Offset + BytesToWrite;
			
			// 如果是数据文件则需要同步修改文件在磁盘上对应的 DIRENT 结构
			// 体。目录文件的 DIRENT 结构体中的 FileSize 永远为 0,无需修改。
			if (!File->AttrDirectory)
				FatWriteDirEntry(Vcb, File);
		}
	}
	
	// 返回实际写入的字节数量
	*BytesWriten = BytesToWrite;

	return STATUS_SUCCESS;

}

思考与练习

1、结合 FAT12 文件系统,说明“文件大小”和“文件占用磁盘空间大小”的区别,并举例说明文件的这两个属性值变化的方式有什么不同

文件占用磁盘空间是以簇为单位的,而文件大小是以字节为单位的,所以文件大小小于等于文件占用的磁盘空间(因为一个簇就包含512个字节,每占用一个簇占用的磁盘空间就增加512个字节)。

当文件增大时,并一定需要增加磁盘空间,比如一个文件大小只有1个字节,此时所占磁盘空间为1个簇,当他再增加1个字节的时候,大小变为了2个字节,所占磁盘空间仍然为1个簇,只有当他增加到超过1个簇的大小时,才会增加新的磁盘占用空间。

2、EOS 应用程序在读写文件时,缓冲区大小设置为512的倍数比较合适,说明原因。

在内核中读取文件都是通过IopReadWriteSector函数完成的,该函数每次最多只能读取512个字节,在读取多个扇区时效率会降低。如果使用小于512的缓冲区大小,如100或者200或者256,在读取两个扇区时可能就会调用4次或以上的函数,而设置为512只需调用两次。因此大小设置不合适的话会增加对该函数的调用次数。

3、分析使用链式分配方式管理磁盘空间的优缺点。

优点:存储不连续,解决了连续存储带来的碎片化问题,可以显著提高对磁盘空间的利用率,并且不用事先知道文件的长度,只需要根据文件的当前需要为他分配当前的磁盘块,动态增长时可以动态增加分配。同时“增删改”也十分方便。

缺点:“查”很不方便。因为链式存储随机访问效率低,要访问某一磁盘时必须从头遍历。并且只要其中某一块的任一个指针出现问题,整个链就会全盘崩塌。