openEuler实验二 文件系统

实验目的

• 学习掌握Linux系统中普通文件和目录文件的区别与联系

• 学习掌握Linux管理文件的底层数据结构

• 学习掌握Linux文件存储的常见形式

• 加深学生对读写者问题的理解和信号量的使用

实验任务

完成File.cpp中未完成函数的编写:

addDirUnit(dirTable* myDirTable, char fileName[], int type, int FCBBlockNum)

这是一个工具函数,用于根据参数给定信息,创建目录项挂到给定参数表下。

int addDirUnit(dirTable* myDirTable, char fileName[], int type, int FCBBlockNum)
{
    //检测目录表示是否已满
    if(myDirTable->dirUnitAmount == DIR_TABLE_MAX_SIZE)
    {
        printf("the directory is full! \n");
        return -1;
    }
    //是否存在同名文件
    if(findUnitInTable(myDirTable, fileName) != -1)
    {
        printf("%s existed!\n", fileName);
        return -1;
    }
    //构建新目录项
    dirUnit *op = &myDirTable->dirs[myDirTable->dirUnitAmount ++];
    strcpy(op->fileName, fileName);
    op->startBlock = FCBBlockNum;
    op->type = type;
    return 0;
}

int changeDir(char dirName[])

该函数用于实现文件系统切换工作目录的功能cd,为了实现任意跳转,我在设计时额外设计了一个函数int changeToRealDir(char *dirName),用于将传入的参数dirName转换为以/结尾的绝对路径。然后从根节点开始搜索,最后修改全局的path和当前目录表currentDirTable

int changeToRealDir(char *dirName)
{
  if(dirName[0] == '/'){
        if(dirName[strlen(dirName) - 1] != '/')
        strcat(dirName, "/");
        return 0;
    }
  if(dirName[0] == '.')
  {
    if(dirName[1] == '/')
    {
      char address[200];
      strcpy(address, path);
      strcat(address, dirName + 2);
      strcpy(dirName, address);
      if(dirName[strlen(dirName) - 1] != '/')
        strcat(dirName, "/");
      return 0;
    }


    if(dirName[1] == 0)
    {
      strcpy(dirName, path);
      if(dirName[strlen(dirName) - 1] != '/')
                                strcat(dirName, "/");
      return 0;
    }


    if(dirName[1] == '.')
    {
      char address[200];
      if(dirName[2] == 0)
      {
        strcpy(address, path);
        int i = strlen(address) - 2;
        while(i >= 0 && address[i] != '/') address[i --] = 0;
        strcpy(dirName, address);
      }
      else if(dirName[2] == '/')
      {
                                strcpy(address, path);
                                int i = strlen(address) - 1;
                                while(address[i] != '/') address[i --] = 0;
        strcat(address, dirName + 3);
                                strcpy(dirName, address);
      }
      else{
        return -1;
      }
      if(dirName[strlen(dirName) - 1] != '/')
                                strcat(dirName, "/");
      return 0;
    }
  }
  char address[200];
        strcpy(address, path);
        strcat(address, dirName);
        strcpy(dirName, address);
        if(dirName[strlen(dirName) - 1] != '/')
        strcat(dirName, "/");
        return 0;
}


//TODO 切换目录 cd
int changeDir(char DirName[])
{
    char dirName[200];
    strcpy(dirName, DirName);
    changeToRealDir(dirName);
    //目录项在目录位置
    char *switch_to;
    int i = 1;
    dirTable *tmp = rootDirTable;
    char address[200];
    strcpy(address, dirName);
    while(~i){
        switch_to = dirName + i;
        for(; dirName[i]; i ++)
            if(dirName[i] == '/')
            {
                dirName[i] = 0;
                i ++;
                break;
            }
        if(dirName[i] == 0)
        {
      if(!strlen(switch_to)) break;
            i = -1;
        }
        int dirno = findUnitInTable(tmp, switch_to);
        //文件名不存在
        if(dirno == -1)
        {
            printf("the directory is not exist! \n");
            return -1;
        }
        //不是目录类型
        if(tmp->dirs[dirno].type)
        {
            printf("%s is not a directory! \n", address);
            return -1;
        }
        //修改当前目录(currentDirTable)
        tmp = (dirTable* ) getBlockAddr(tmp->dirs[dirno].startBlock);
    }
        //修改全局绝对路径(path)
        strcpy(path, address);
        currentDirTable = tmp;
        return 0;
}

int changeName(char oldName[], char newName[])

修改名字的操作比较简单,只要能定位到相应的目录或文件,修改对应的目录单元即可。

int changeName(char oldName[], char newName[])
{
    int target = findUnitInTable(currentDirTable, oldName);
    if(target == -1 || target == 0)
    {
        printf("cannot find %s\n", oldName);
        return -1;
    }
    strcpy(currentDirTable->dirs[target].fileName, newName);
    return 0;
}

int creatDir(char dirName[])

在前述功能已经准备好的情况下,创建目录可以先使用addDirUnit挂上一个目录单元,然后使用changDir切换到挂好的目录单元中,再次调用addDirUnit挂上..目录,然后重新使用changeDir(".."),切换回原来的目录。

关于上述功能实现上,可能存在strcpy后段错误的坑,这是由于将长字符串复制给短字符串造成内存溢出所导致的,所以在必要的时候,需要额外重新开一个数组空间存储地址。

int creatDir(char DirName[])
{
    char dirName[200];
    strcpy(dirName, DirName);
    //检测文件名字长度
    if(strlen(dirName) > DIR_TABLE_MAX_SIZE)
    {
        printf("dirname is too long !\n");
        return -1;
    }
    //为目录表分配空间
    int block = getBlock(1);
    //将目录作为目录项添加到当前目录
    addDirUnit(currentDirTable, dirName, 0, block);
    //为新建的目录添加一个到父目录的目录项i
    dirTable* new_one = (dirTable*) getBlockAddr(block);
    new_one -> dirUnitAmount = 0;
    block = getAddrBlock((char *) currentDirTable);
    changeDir(dirName);
    addDirUnit(currentDirTable, "..", 0, block);
    strcpy(dirName, "..");
    changeDir("..");
    return 0;
}

int deleteFile(char fileName[])

删除文件首先要定位文件,定位文件之后判断是否为文件,如果是文件,则调用releaseFile释放文件内存,然后再调用deleteDirUnit从目录表中删除该文件的目录项。

int deleteFile(char fileName[])
{
    //查找文件的目录项位置
    //忽略系统的自动创建的父目录
    int op = findUnitInTable(currentDirTable, fileName);
    if(op == -1)
    {
        printf("file not exist! \n");
        return -1;
    }
    if(!op)
    {
        printf("you cannot remove .. \n");
        return -1;
    }
    //若文件类型为目录,则返回错误
    if(!currentDirTable->dirs[op].type)
    {
        printf("not a file\n");
        return -1;
    }
    //释放文件内存
    releaseFile(currentDirTable->dirs[op].startBlock);
    //从目录表中剔除
    deleteDirUnit(currentDirTable, op);
    return 0;
}

int write_file(char fileName[], char content[])

关于读者写者问题,写者准备书写时,首先按顺序获取read_sem2锁(阻止新的读者、写者进入)和write_sem锁(确保所有读者退出),然后书写,书写完成后释放所获取的锁。

int write_file(char fileName[], char content[])
{
    //获得控制块
    FCB *myFCB = open(fileName);
    if(!myFCB) return -1;
    /* 获得写者锁 */
    char *data = (char *) getBlockAddr(myFCB->dataStartBlock);
    myFCB->write_sem = sem_open("write_sem", 0);
    myFCB->read_sem2 = sem_open("read_sem2", 0);
    if (sem_wait(myFCB->read_sem2) == -1)
        perror("sem_wait error");
    if (sem_wait(myFCB->write_sem) == -1)
        perror("sem_wait error");
    //在不超出文件的大小的范围内写入
    int dataSize = myFCB->dataSize;
    for (int i = myFCB->dataSize, j = 0; i < myFCB -> fileSize * 1024 && j < strlen(content); i++, j++) {
        data[i] = content[j];
  myFCB->dataSize ++;
    }
    /* 模拟编辑器,控制写者不立即退出 */
    printf("> Write finished, press any key to continue....");
    getchar();
    /* 释放写者锁 */
    sem_post(myFCB->write_sem);
    sem_post(myFCB->read_sem2);
    return 0;
}

实验结果测试

使用实验手册中要求的测试序列,获得了与实验手册中完全一致的结果。

ccf181768b567f2b3553cd8722445792.png

74cc1a0ad33601d5a2701677e46aa9a1.png

实验代码改进建议

在实验手册中,关于信号量的命名上,可以从creatFCB函数中看出,它们使用的信号量均采用了相同的命名方式:

//创建FCB
int creatFCB(int fcbdataStartBlock, int filedataStartBlock, int fileSize) {
    //找到fcb的存储位置
    FCB *currentFCB = (FCB *) getBlockAddr(fcbdataStartBlock);
    currentFCB->dataStartBlock = filedataStartBlock;//文件数据起始位置
    currentFCB->fileSize = fileSize;//文件大小
    currentFCB->link = 1;//文件链接数
    currentFCB->dataSize = 0;//文件已写入数据长度
    currentFCB->readPtr = 0;//文件读指针
    currentFCB->read_sem = sem_open("read_sem", O_CREAT, 0644, READER_MAX_NUM);
    if (currentFCB->read_sem == SEM_FAILED) {
        perror("sem_open error");
        exit(1);
    }
    currentFCB->write_sem = sem_open("write_sem", O_CREAT, 0644, 1);
    if (currentFCB->write_sem == SEM_FAILED) {
        perror("sem_open error");
        exit(1);
    }
    currentFCB->read_sem2 = sem_open("read_sem2", O_CREAT, 0644, 1);
    if (currentFCB->read_sem2 == SEM_FAILED) {
        perror("sem_open error");
        exit(1);
    }
    return 0;
}

在每一个信号量创建时,信号量的名称都采用了"read_sem"、"write_sem"、"read_sem2",对于有名信号量而言,访问不同文件时,会尝试获取相同的信号量,这违背了防止互斥的初衷,更加糟糕的是,在删除文件相关的函数int releaseFile(int FCBBlock)里面,对应于下附代码的14、15、16行进行了unlink操作,本意是释放关于这个文件而言的读写锁,但实际上是在系统内删除了该有名信号量。在当前这样的设计下,如果其他文件正在进行读写操作,而另一窗口进行删除文件的操作,这会直接导致异常。

//释放文件内存
int releaseFile(int FCBBlock) {
    FCB *myFCB = (FCB *) getBlockAddr(FCBBlock);
    myFCB->link--;  //链接数减一
    //无链接,删除文件
    if (myFCB->link == 0) {
        //释放文件的数据空间
        releaseBlock(myFCB->dataStartBlock, myFCB->fileSize);
    }
    //释放FCB的空间
    sem_close(myFCB->read_sem);
    sem_close(myFCB->write_sem);
    sem_close(myFCB->read_sem2);
    sem_unlink("read_sem");
    sem_unlink("write_sem");
    sem_unlink("read_sem2");
    //释放FCB空间
    releaseBlock(FCBBlock, 1);
    return 0;
}

使用两个进程去访问相同信号量,然后在一个进程中删除文件:

b383014c1fb6a9a4aee6b8b849b99047.png

修改了showDir函数,使得它会同时输出信号量的值,可以发现,右边获取读者锁后,左边对应信号量减1,右边仍在读写未释放锁,左边进行删除后,信号量已经被unlink,此时,右边同样是无法正常工作的。

由于在实验中,不需要刻意去实现相关错误,因此该现象并不明显,但是显然违背了互斥的初衷,因此,应该在初始化信号量时,按照文件的名称和其他的标识,或者直接使用文件的绝对路径(保证唯一性)作为信号量的名称,从而来避免以上的问题。

21f337b9c9241b3dd3d6e39adc5ae48d.png

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐