操作系统第八次实验
文件结构
一、实验内容
- 把文件的逻辑结构转换成存储结构
- 设计便于顺序存取和直接存取的文件存储结构。
二、实验目的
- 研究用户概念中的信息组织方式
- 理解文件的逻辑结构、存取结构、存取方式之间的联系
- 模拟设计文件的存储结构
三、实验题目
第一题:
模拟设计MS-DOS操作系统中磁盘文件的存储结构。
-
当用户对记录式文件采用顺序存以方式时,用户总是依次地访问一个个逻辑记录,即当访问了第i个记录后,下次总是访问第i+1个记录。所以,当用户采用顺序存取方式访问文件时,只要给出访问要求(读或写)而无需再指出要访问的记录号。
-
采用链接文件结构,只有读出一个物理块信息后才能从链接字中得知下一个物理块号。所以,当用户要在文件中插入一些信息时,文件系统必须多次地请求启动磁盘读出信息才能做插入工作。
MS-DOS操作系统对链接文件结构作了改进,它是把所有的链接指针集中在一起,存放在文件定位表FAT中。查找链接字时不必读出物理块信息可直接从FAT中得到。
- 其设计思想是:
假定磁盘上共有N个物理块可供使用,FAT就有N项,初始化时为全“0”,表示对应的物理块均可使用,当要存放文件时,从FAT中寻找为“0”的项,其对应的物理块用来存放文件信息,把文件的链接指针(指出物理块号)登记在FAT中,文件第一块块号登记在文件目录中。
- 假定磁盘存储空间共有32个物理块,模拟设计文件定位表FAT。文件定位表可以用一个一维数组FAT[031]来定义,其中一个元素与一个物理块对应。当FAT[i]=0时,表示第i块为空闲块;当FAT[i]=FFF时,表示链接文件到第i块结束;当在0~FFF时,其值指示链接文件中下一个物理块号。
- 每个物理块只能存放一个逻辑记录,设计一个程序把文件的逻辑结构模拟转换成MS-DOS的链接结构。
- 要求保存一个已经在1 1主存中的文件时,给出文件名和文件的逻辑记录长度及个数,对一个已经保存的文件,允许用户插入新记录。用键盘输入来模拟用户的要求,输入信息为:
“存” 文件名 逻辑记录长度 逻辑记录个数“插入” 文件名 逻辑记录号 - 模拟程序的算法如下图所示:
- 假设系统中已经有两个链接文件,其链接情况由FAT表指出(链接情况学生自定),现又要保存一个新文件,然后对已保存的文件插入一个新记录。运行你所设计的程序,观察其结果。
第二题:
模拟便于直接存取的索引文件结构
-
索引文件像链接文件一样,文件的逻辑记录信息可存放在非连续的磁盘存储空间中。但这些存放逻辑记录的存储空间不是按链表的形式链接在一起的,而是采用索引表来指出逻辑记录存放的物理位置。
-
文件目录与索引表的关系如下图所示:
-
建立索引文件的过程是:寻找一些空闲物理块;逻辑记录存入这些物理块中;把逻辑记录与物理块的对应关系登记在索引表中。
思考题:
- 链表文件结构和索引文件结构各自的优缺点是什么?
- 当文件较大时,使得索引表也较大,如果索引表的大小超过了一个物理块,如何进行索引表的存取?
四、实验设计与过程
第一题:MS-DOS磁盘文件存储结构
数据结构和符号说明
//文件定位表 fat
int fat[32];
char phy_file[32];
//主存剩余块的数量
int leftblocks = 32;
//文件目录
struct FD
{
string name[32];
//文件起始块号
int start[32];
//每个文件逻辑长度
int filelength[32];
//文件数量
int filenum;
} fd;
函数说明
一个存文件函数和一个插入文件函数,模拟链表实现。
//初始化函数,重置 fat
void init()
//存文件操作
void save()
//插入文件操作
void insert()
源代码
// MS-DOS 磁盘文件存储结构
#include <bits/stdc++.h>
using namespace std;
//文件定位表 fat
int fat[32];
char phy_file[32];
//主存剩余块的数量
int leftblocks = 32;
//文件目录
struct FD
{
string name[32];
//文件起始块号
int start[32];
//文件数量
int filelength[32];
int filenum;
} fd;
//初始化函数,重置 fat
void init()
{
for (int i = 0; i < 32; i++)
fat[i] = 0;
//将第一位设置为 FDF
fat[0] = -2;
//将第二位设置成所有文件的结束 FFF
fat[1] = -1;
leftblocks -= 2;
fd.filenum = 0;
// cout << fd.name << endl;
fd.name[++fd.filenum] = "AB";
// cout << fd.name[fd.filenum] << endl;
fd.start[fd.filenum] = 2;
fat[2] = 5; //初始化第一个文件
fat[5] = 8;
fat[8] = 11;
fat[11] = 18;
fat[18] = 24;
fat[24] = 1;
fd.filelength[fd.filenum] = 6;
fd.name[++fd.filenum] = "B"; //初始化第二个文件
fd.start[fd.filenum] = 6;
fat[6] = 9;
fat[9] = 14;
fat[14] = 19;
fat[19] = 22;
fat[22] = 28;
fat[28] = 30;
fat[30] = 1;
fd.filelength[fd.filenum] = 7;
}
//寻找相同文件名的条目
//找到就返回在文件目录中的索引
int FindFile(string filename)
{
for (int i = 1; i <= fd.filenum; i++)
if (fd.name[i] == filename)
return i;
return 0;
}
//存文件操作
void save()
{
string filename;
cout << "请输入文件名: ";
cin >> filename;
//首先寻找是不是有同名的文件
//如果有则不能重复插入
int num;
cout << "请输入逻辑记录长度: ";
cin >> num;
if (FindFile(filename) != 0)
{
cout << "********存在同名文件,存操作执行失败!********" << endl;
return;
}
//检查内存剩余的空间是不是够存储
if (num > leftblocks)
{
cout << "********存储的文件过大,无法存储!********" << endl;
return;
}
//记录文件名称
fd.name[++fd.filenum] = filename;
int pos = 0, lastpos;
//找 num 个空的内存块来存储,同时更新索引
for (int i = 1; i <= num; i++)
{
//找到一个空的位置
while (fat[pos] != 0)
pos = (pos + 1) % 32;
//如果是文件的起始块号则直接存储下一个块的索引
if (i == 1)
fd.start[fd.filenum] = pos;
//不是起始块则将上一个块的索引设置成当前的地址
else
fat[lastpos] = pos;
//记录当前地址,实际的复制内容由下一个块的地址决定
lastpos = pos;
//从下一个块开始找,空出当前地址留待使用
pos++;
//剩余的块少一个
leftblocks--;
}
//将文件的结尾执行统一的结束符 FFF
fat[lastpos] = 1;
cout << "********文件存储成功!********" << endl;
}
//插入文件操作
void insert()
{
string filename;
cout << "请输入文件名:";
cin >> filename;
//首先查找文件目录中是否有这个文件
//如果有这个文件则记录在文件目录中的索引
int pos = FindFile(filename);
//如果没有这个文件则插入不能完成
if (pos == 0)
{
cout << "********不存在该文件,插入失败!********" << endl;
return;
}
//将这个块插入到文件的第 num 个位置
int num;
cout << "请输入要插入的文件逻辑位置:";
cin >> num;
//判断是否有空间存储这个插入的块
if (leftblocks == 0)
{
cout << "********无空闲块,插入失败!********" << endl;
return;
}
//找一个空闲的位置来存放要插入的块
int p = 1;
while (fat[p] != 0)
p++;
//如果要在第一个位置插入,则更新起始块号
if (num == 1)
{
fat[p] = fd.start[pos];
fd.start[pos] = p;
}
//不是在第一个位置插入
else if (num >= 2)
{
//找到要插入位置的前一个块号
int temp = fd.start[pos];
for (int i = 1; i < num - 1; i++)
temp = fat[temp];
//将新的位置的下一个索引指向原来的下一个块
fat[p] = fat[temp];
//将上一个块的下一个索引指向新的位置,完成插入
fat[temp] = p;
}
//更新剩余的内存块
leftblocks--;
fd.filelength[pos]++;
cout << "********插入文件块成功!********" << endl;
}
//输出文件目录和 fat
void info()
{
printf("|-------------------------------|\n");
printf("|-----------文件目录------------|\n");
printf("|-------------------------------|\n");
printf("| 文件名 起始位置 逻辑长度 |\n");
for (int i = 1; i <= fd.filenum; i++)
printf("| %6s %8d %9d |\n", fd.name[i].c_str(), fd.start[i], fd.filelength[i]);
printf("|-------------------------------|\n");
printf("\n|--------------------|\n");
printf("|---文件定位表(fat)--|\n");
printf("|--------------------|\n");
printf("| 索引 内容 |\n");
printf("| 0 FDF |\n");
printf("| 1 FFF |\n");
for (int i = 2; i < 32; i++)
printf("| %4d %4d |\n", i, fat[i]);
printf("|--------------------|\n\n");
}
//功能选择清单
void choicelist()
{
cout << "|====================================================================|" << endl;
cout << "|-----------------------MS-DOS磁盘文件存储管理菜单-------------------|" << endl;
cout << "| 1.向fat中存储一个文件: |" << endl;
cout << "| 2.向fat中的文件插入数据: |" << endl;
cout << "| 3.输出当前文件目录以及fat信息: |" << endl;
cout << "| 4.结束操作,退出系统: |" << endl;
cout << "|====================================================================|" << endl;
}
int main()
{
init();
int order;
while (1)
{
//通过字符串输入来选择想要测试的功能
choicelist();
cout << "请输入你要执行的操作的编号: ";
cin >> order;
switch (order)
{
//存 一个文件
case 1:
save();
break;
//在指定的文件中的特定位置插入一个块
case 2:
insert();
break;
//显示当前的系统信息
case 3:
info();
break;
//测试结束退出系统3
case 4:
cout << "系统结束" << endl;
exit(0);
break;
//输入指令不合法
default:
cout << "指令输入错误" << endl;
}
}
return 0;
}
程序初值和运行结果
初态
保存新文件
对新保存的文件进行插入
第二题:索引文件存储结构
数据结构和符号说明
相比于上一题,增加了三个数据结构,分别是索引表、文件目录表、用户表
同时因为其结构是名称和地址的对应关系,故采用了stl数据结构中的map来反映映射关系。
struct Indextab //索引表
{
int tab[32];
int length = 0;
};
struct Filetab//文件目录表
{
map<string, Indextab> filetab;
};
map<string, Filetab> usertab;//用户表
函数说明
相比于上一题,改动之处在于增加了一个是文件结构改变成索引结构的函数。
函数思路为首先输入用户名,然后依次要求用户输入属于自己的文件,将其加入该用户的文件表中。
void change_to_index()
{ //打印当前文件目录
printf("当前文件目录:\n");
printf("|-------------------------------|\n");
printf("|-----------文件目录------------|\n");
printf("|-------------------------------|\n");
printf("| 文件名 起始位置 逻辑长度 |\n");
for (int i = 1; i <= fd.filenum; i++)
printf("| %6s %8d %9d |\n", fd.name[i].c_str(), fd.start[i], fd.filelength[i]);
printf("|-------------------------------|\n");
while (1)
{
printf("请输入用户名,按q退出\n");
string name;
cin >> name;
if (name == "q")
break;
else
{
while (1)
{
printf("请输入当前用户的文件名,按q退出\n");
string file;
cin >> file;
if (file == "q")
break;
else
{
int t = FindFile(file);
if (!t)
{
printf("不存在该文件,请重新输入文件名:");
continue;
}
else
{
usertab[name].filetab[file].length = 0;//初始化用户文件表记录数
int cpos = fd.start[t];
for (int i = 0; i < fd.filelength[t]; i++)
{
usertab[name].filetab[file].tab[i] = cpos;//记录物理块号
cpos = fat[cpos];//到下一个物理块
usertab[name].filetab[file].length++;//记录数加一
}
}
}
}
}
}
}
源代码
// MS-DOS 磁盘文件存储结构
#include <bits/stdc++.h>
using namespace std;
#define FDF -1
#define FFF -2
//文件定位表 fat
int fat[32];
//主存剩余块的数量
int leftblocks = 32;
//文件目录
struct FD
{
string name[32];
//文件起始块号
int start[32];
//文件数量
int filelength[32];
int filenum;
} fd;
struct Indextab //索引表
{
int tab[32];
int length = 0;
};
struct Filetab//文件目录表
{
map<string, Indextab> filetab;
};
map<string, Filetab> usertab;//用户表
//初始化函数,重置 fat
void init()
{
for (int i = 0; i < 32; i++)
fat[i] = 0;
//将第一位设置为 FDF
fat[0] = -2;
//将第二位设置成所有文件的结束 FFF
fat[1] = -1;
leftblocks -= 2;
fd.filenum = 0;
// cout << fd.name << endl;
fd.name[++fd.filenum] = 'A';
// cout << fd.name[fd.filenum] << endl;
fd.start[fd.filenum] = 2;
fat[2] = 5; //初始化第一个文件
fat[5] = 8;
fat[8] = 11;
fat[11] = 18;
fat[18] = 24;
fat[24] = FFF;
fd.filelength[fd.filenum] = 6;
fd.name[++fd.filenum] = 'B'; //初始化第二个文件
fd.start[fd.filenum] = 6;
fat[6] = 9;
fat[9] = 14;
fat[14] = 19;
fat[19] = 22;
fat[22] = 28;
fat[28] = 30;
fat[30] = FFF;
fd.filelength[fd.filenum] = 7;
}
//寻找相同文件名的条目
//找到就返回在文件目录中的索引
int FindFile(string filename)
{
for (int i = 1; i <= fd.filenum; i++)
if (fd.name[i] == filename)
return i;
return 0;
}
/* int finduser(string name)
{
map<string, Filetab>::iterator iter;
iter = usertab.find(name);
if (iter == usertab.end())
return 0;
else
return 1;
} */
void save()
{
string filename;
cout << "请输入文件名: ";
cin >> filename;
//首先寻找是不是有同名的文件
//如果有则不能重复插入
if (FindFile(filename) != 0)
{
cout << "********存在同名文件,存操作执行失败!********" << endl;
return;
}
int num;
cout << "请输入逻辑记录长度: ";
cin >> num;
//检查内存剩余的空间是不是够存储
if (num > leftblocks)
{
cout << "********存储的文件过大,无法存储!********" << endl;
return;
}
//记录文件名称
fd.name[++fd.filenum] = filename;
fd.filelength[fd.filenum] = num;
int pos = 0, lastpos;
//找 num 个空的内存块来存储,同时更新索引
for (int i = 1; i <= num; i++)
{
//找到一个空的位置
while (fat[pos] != 0)
pos = (pos + 1) % 32;
//如果是文件的起始块号则直接存储下一个块的索引
if (i == 1)
fd.start[fd.filenum] = pos;
//不是起始块则将上一个块的索引设置成当前的地址
else
fat[lastpos] = pos;
//记录当前地址,实际的复制内容由下一个块的地址决定
lastpos = pos;
//从下一个块开始找,空出当前地址留待使用
pos++;
//剩余的块少一个
leftblocks--;
}
//将文件的结尾执行统一的结束符 FFF
fat[lastpos] = FFF;
cout << "********文件存储成功!********" << endl;
}
//插入文件操作
void insert()
{
string filename;
cout << "请输入文件名:";
cin >> filename;
//首先查找文件目录中是否有这个文件
//如果有这个文件则记录在文件目录中的索引
int pos = FindFile(filename);
//如果没有这个文件则插入不能完成
if (pos == 0)
{
cout << "********不存在该文件,插入失败!********" << endl;
return;
}
//将这个块插入到文件的第 num 个位置
int num;
cout << "请输入要插入的文件逻辑位置:";
cin >> num;
//判断是否有空间存储这个插入的块
if (leftblocks == 0)
{
cout << "********无空闲块,插入失败!********" << endl;
return;
}
//找一个空闲的位置来存放要插入的块
int p = 1;
while (fat[p] != 0)
p++;
//如果要在第一个位置插入,则更新起始块号
if (num == 1)
{
fat[p] = fd.start[pos];
fd.start[pos] = p;
}
//不是在第一个位置插入
else if (num >= 2)
{
//找到要插入位置的前一个块号
int temp = fd.start[pos];
for (int i = 1; i < num - 1; i++)
temp = fat[temp];
//将新的位置的下一个索引指向原来的下一个块
fat[p] = fat[temp];
//将上一个块的下一个索引指向新的位置,完成插入
fat[temp] = p;
}
//更新剩余的内存块
leftblocks--;
fd.filelength[pos]++;
cout << "********插入文件块成功!********" << endl;
}
void change_to_index()
{ //打印当前文件目录
printf("当前文件目录:\n");
printf("|-------------------------------|\n");
printf("|-----------文件目录------------|\n");
printf("|-------------------------------|\n");
printf("| 文件名 起始位置 逻辑长度 |\n");
for (int i = 1; i <= fd.filenum; i++)
printf("| %6s %8d %9d |\n", fd.name[i].c_str(), fd.start[i], fd.filelength[i]);
printf("|-------------------------------|\n");
while (1)
{
printf("请输入用户名,按q退出\n");
string name;
cin >> name;
if (name == "q")
break;
else
{
while (1)
{
printf("请输入当前用户的文件名,按q退出\n");
string file;
cin >> file;
if (file == "q")
break;
else
{
int t = FindFile(file);
if (!t)
{
printf("不存在该文件,请重新输入文件名:");
continue;
}
else
{
usertab[name].filetab[file].length = 0;//初始化用户文件表记录数
int cpos = fd.start[t];
for (int i = 0; i < fd.filelength[t]; i++)
{
usertab[name].filetab[file].tab[i] = cpos;//记录物理块号
cpos = fat[cpos];//到下一个物理块
usertab[name].filetab[file].length++;//记录数加一
}
}
}
}
}
}
}
void info_change_after()
{
map<string, Filetab>::iterator it;
map<string, Indextab>::iterator fileit;
printf("**********************************\n");
// fflush(stdin);
for (it = usertab.begin(); it != usertab.end(); it++)
{
// fflush(stdin); //重写操作。目的是清除缓存。
printf("\n|-----------------------|\n");
printf("|----用户%4s的索引表---|\n", it->first.c_str());
printf("|-----------------------|\n");
for (fileit = usertab[it->first].filetab.begin(); fileit != usertab[it->first].filetab.end(); fileit++)
{
printf("|--------文件名:%s-------|\n", fileit->first.c_str());
printf("| 记录号 物理块号 |\n");
for (int j = 0; j < fileit->second.length; j++)
printf("| %8d %8d |\n", j + 1, fileit->second.tab[j]);
printf("|-----------------------|\n");
}
}
}
//输出文件目录和 fat
void info()
{
printf("|-------------------------------|\n");
printf("|-----------文件目录------------|\n");
printf("|-------------------------------|\n");
printf("| 文件名 起始位置 逻辑长度 |\n");
for (int i = 1; i <= fd.filenum; i++)
printf("| %6s %8d %9d |\n", fd.name[i].c_str(), fd.start[i], fd.filelength[i]);
printf("|-------------------------------|\n");
printf("\n|--------------------|\n");
printf("|---文件定位表(fat)--|\n");
printf("|--------------------|\n");
printf("| 索引 内容 |\n");
printf("| 0 FDF |\n");
printf("| 1 FFF |\n");
for (int i = 2; i < 32; i++)
printf("| %4d %4d |\n", i, fat[i]);
printf("|--------------------|\n\n");
}
//功能选择清单
void choicelist()
{
cout << "|====================================================================|" << endl;
cout << "|---------------MS-DOS磁盘文件存储管理菜单和索引文件结构-------------|" << endl;
cout << "| 1.向fat中存储一个文件: |" << endl;
cout << "| 2.向fat中的文件插入数据: |" << endl;
cout << "| 3.输出当前文件目录以及fat信息: |" << endl;
cout << "| 4.转换成索引表: |" << endl;
cout << "| 5.输出转换后的索引表: |" << endl;
cout << "| 6.结束操作,退出系统: |" << endl;
cout << "|====================================================================|" << endl;
}
int main()
{
char name[10];
init();
int order;
while (1)
{
//通过字符串输入来选择想要测试的功能
choicelist();
cout << "请输入你要执行的操作的编号: ";
cin >> order;
switch (order)
{
//存 一个文件
case 1:
save();
break;
//在指定的文件中的特定位置插入一个块
case 2:
insert();
break;
//显示当前的系统信息
case 3:
info();
break;
//测试结束退出系统3
case 4:
change_to_index();
break;
case 5:
info_change_after();
break;
case 6:
cout << "系统结束" << endl;
exit(0);
break;
//输入指令不合法
default:
cout << "指令输入错误" << endl;
}
}
return 0;
}
程序初值和运行结果
转化成索引表:
打印当前文件索引表:
插入文件后输出文件fat表
将新插入的文件加入索引结构中:
思考题:
1. 链表文件结构和索引文件结构各自的优缺点是什么?
-
链式结构
优点:
- 动态数据结构:链表是一种动态数据结构,因此它可以在运行时通过分配和取消分配内存来增长和缩小。所以没有必要给出链表的初始大小。
- 易于插入和删除:在链表中进行插入和删除节点真的很容易。在链表中,我们只需要更新节点下一个指针中的地址。
- 磁盘空间利用率、内存利用率高:由于链表的大小可以在运行时增加或减少,因此没有内存浪费。
缺点:
- 存取速度慢,不适于随机存取;
- 当物理块间的连接指针出错时,数据丢失;
- 更多的寻道次数和寻道时间;链接指针占用一定的空间,降低了空间利用率。
-
索引结构
优点:- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性;
- 可以大大加快数据的检索速度,这也是创建索引的最主要的原因;可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义;
- 在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间;
- 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
缺点:
- 创建索引和维护索引要耗费时间,因为这导致了较多的寻道次数和寻道时间,这种时间随着数据量的增加而增加;
- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大;
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
2.当文件较大时,使得索引表也较大,如果索引表的大小超过了一个物理块,如何进行索引表的存取?
如果索引表的大小超过了一个物理块,可以采用间接索引(多重索引)的方式来解决,也就是在索引表所指的物理块中存放的不是文件信息,而是装有这些信息的物理块地址,这样,如果一个物理块可装下n个物理块地址,则经过一级间接索引,可寻址的文件长度将变为n×n块。如果文件长度还大于n×n块,还可以进行类似的扩充,即二级间接索引。
这样就对索引表的长度没有限制了,但是同时也会增加很多空间上的开销。如果是使用分页式管理位示图算法,文件存储的时候是按照块来分割的,内存的空间也是按照块来分割的,所以就一定是能够充分利用内存空间的,不会造成浪费。但是同时,这种算法实现起来复杂度较高,虽然寻找空闲块比较简单,但是文件的读取和存储都是分块进行的,而且位置之间没有顺序性,所以读取和存储的寻址和处理的时间代价很大,算法复杂度较高。另外,这种算法是需要页表来进行维系的,页表也需要存储,会造成空间的浪费,如果内存分块块比较小,数量很多,会导致页表非常大,极限情况下,内存会被页表完全占领,造成极大的浪费,效率受到影响。
五、实验总结
本次实验第一题相对比较容易,通过用数组来模拟链表结构可以成功实现,第二题的难点在于三级索引结构的设计,如果都使用结构体使得代码较为复杂冗长,所以经过思考后,我采用了stl中的map来作为用户名和文件表的映射以及文件名和索引表的映射,这使得代码量大大减小,成功实现该功能。