【C语言有什么用?①】从零开始撸一个用户态模拟文件系统

简介: 【C语言有什么用?①】从零开始撸一个用户态模拟文件系统

☘写在前面☘

学习一个语言最好的方法是做一个小项目,这个项目不需要多么复杂,但是一定能激发你的学习兴趣。让我们话不多说,开始吧


本文将带你手撸一个磁盘组织方式的模拟,你将学到


📝 c语言指针的使用

📝 linux的系统调用

📝 磁盘的组织方式?

📝 超级块存储哪些信息?

📝 inode存储哪些内容?

📝 位示图有啥用?

全文大约阅读时间: 30min


🧑🏻作者简介:一个从工业设计改行学嵌入式的年轻人

🔒资源下载:gitee仓库

✨联系方式:2201891280(QQ)


📔全文目录📔

一、基本要求介绍

🧭目标

🥙形式要求

🥪实现约束

二、解决方法思路

✍参考文章

🧇主体实现思路

三、linux系统调用简介

📃open & close

📃write & read

📃lseek

四、磁盘组织方式简介

文件访问的过程

五、主函数实现

提示信息的输出

类终端功能实现

六、文件功能定义

主要数据结构

创建文件系统

格式化

低级格式化

创建文件

删除文件

七、写在最后

一、基本要求介绍

🧭目标

完成用户态环境下的磁盘模拟功能,提供磁盘基本信息查询与格式化功能。


🥙形式要求

基于C/C++,完成上述完整功能


🥪实现约束

利用一个大文件(128MB)来模拟磁盘块设备,基于固定分片大小实现文件系统的超级块区、inode节点区、数据分片区的管理,具备磁盘格式化、文件系统查询(如:fdisk -l)功能,支持文件的inode节点和对应数据分片的分配、回收。


注:此为楼主的一次linux作业,鉴于还没到提交作业的截至日期,所以如果你 借鉴 此篇文章,请修改一定量以防大家都没分数0.0


二、解决方法思路

拿到一道要求时,最重要的是学会找资源,找到一些思路,然后自己去做实现。

所以我平时很喜欢问思路的同学,但是知道思路还不知道代码怎么写呢,我就觉得这是态度问题了,希望大家还是只借鉴思路,千万别直接复制粘贴,这对自己一点好处都没有。


✍参考文章

1.文件系统简单模拟

这篇文章实现的功能非常复杂,所以我只是借鉴了主程序的写法,主要是给自己做一个主程序的基础架构。


2.ext2文件系统详解

这篇文件非常详细的介绍了ext2文件系统的实现方式,这是已有的真实文件系统的结构,我们可以对其进行适当的精简,做出我们的想要的功能。


🧇主体实现思路

模块框架设计

分片大小:4KB 则一共有32K(128M/4K)个分片

保留区:三个分片,分别为超级块,inode位示图,数据区位示图

inode节点:128B,每个分片有32个inode节点,32K个分片需要1K个分片保存inode节点信息,分配inode节点区为4MB。

对应的分配方式如下图


主程序设计

做一个类似于终端的执行方式,输入相应的命令执行相应的操作。

基本的效果如下


三、linux系统调用简介

为了细化上面提到一些细节实现,我们需要了解linux的对文件的系统调用。这部分有点枯燥,毕竟都是系统调用函数,但是也不多,熟悉这部分的同学直接跳过啦0.0


📃open & close

int open (const char *name, int flags, mode_t mode);
int close(int fd);


上面对应的就是打开和open和close系统函数定义

其中open有三个传入参数


第一个就很好理解就是文件名称

第二个是flags表示打开方式,这次用到的只有三个 O_CREAT、O_WRONLY、

O_RDONLY分别表示如果不存在就创建,只读和只写。其中如果我想获取读写权限可以用 O_WRONLY|O_RDONLY 来表示。

第三个参数我是再第二个参数有O_CREAT时候使用,是定义文件的权限的。因为是为了学习,我就简单的定义的所有人可读可写可执行S_IRWXO|S_IRWXG|S_IRWXU。

返回值是文件描述符,表示在文件打卡表项的位置(在内存中,保存着打开文件的位置信息),是系统为我们维护的方便使用的一个符号。如果返回-1表示打开失败

相对来说close就简单很多


只有一个传入参数 就是open的返回值文件描述符,是一个大于等于0的整数可以看下图

📃write & read

size_t read(int fd, void *buf, size_t count);
size_t write(int filedes,const char* buf,size_t nbytes);


这两个函数很类似,只不过功能一个是读一个是写,我就放在一起讲了。都有三个参数


第一个参数是一个文件打开表项,其实就是我们刚才说的open返回值。

第二个是一个指针,如果忽略他的类型可以理解为一片内存空间,其中read就是往这片空间写入数据,wirte就是将对应的空间的数据写入文件。

第三个数据是读或者写的数据大小,单位是字节。千万别超过第二个参数中指针指向的数据大小,不然会写入未知内存,造成程序未知错误,建议直接就sizeof(第二个元素)

这两个函数可以说对于c语言指针的使用极度舒适,直接操作内存。

需要注意一个点就是每次读写系统会将我们的文件偏移量往后推移读到元素个数


📃lseek

#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);


上面说到每次read和wirte都会推进文件偏移量。所以为了改变文件偏移量,我们就有了这个函数。


第一个参数还是文件打开表项,就是open返回那个

第二个参数就是偏移量。单位是字节,可前可后,所以就可以正也可以负值。

第三个参数是相哪里的位置 有三个可选项SEEK_SET、SEEK_CUR、SEEK_END,分别表示相对文件头、相对当前位置以及相对文件末尾。我们只需要前两个就好了。

上面就是所有需要的基本系统调用函数,接下来介绍一些烧脑的文件结构。


四、磁盘组织方式简介

图片是一个常见的文件系统的图片,我们需要实现的只有单分区就好了,不那么复杂。

这张图就是一个最简单的文件系统,但是作业要求我们有inode和数据块的分配回收,所以我们得加上inode位示图和数据块的位示图。

文件访问的过程

当我们尝试访问一个文件\123\456.txt的时候基本过程是

从这个过程中我们知道


超级块(一般是第一块)

主要保存文件系统的基本信息,数据分片的大小、inode节点大小和多少,根目录的位置等

inode节点

**文件的基本信息,**文件大小、文件类型、数据块索引

目录项

保存目录信息 主要内容是 文件名、inode节点号

五、主函数实现

主函数主要是为了打开文件系统以及给一定的提示信息。

提示信息的输出

void help(){
    printf("****************************************************\n");
    printf("*欢迎来到xinglei 的文件系统!!!                   \n");
    printf("*主要的功能介绍                                     \n");
    printf("*    0.创建系统  :create_filesystem                 \n");
    printf("*    1.格式化    :init                              \n");
    printf("*    2.低级格式化:low_init                          \n");    
    printf("*    3.查看信息  :info                              \n");   
    printf("*    4.创建文件  :mkfile                            \n");
    printf("*    5.查看位示图:weishitu                          \n");
    printf("*    6.删除文件  :delfile                           \n");
    printf("*    8.帮助信息  :help                              \n");
    printf("*    9.显示文件  :ls                                \n");   
    printf("*    10.退出     :quit                              \n"); 
    printf("****************************************************\n");
}


主要就是打印一些提示信息,方便用户使用这个系统。


类终端功能实现

char* command[] = {"create_filesystem","init","low_init","info","mkfile","weishitu","delfile","deldir","help","ls","quit"};
int commandnum = sizeof(command)/sizeof(char *);
char path[100] = "file_system";//文件系统
while(1){
        printf("%s$ ",path);
        scanf("%s",com);
        choice = i;
        for(i=0; i<commandnum; ++i)
                    if(strcmp(com,command[i])==0)
                        break;
        choice = i;
        if(Disk == -1 && (!(choice == 0|| choice == 10))){//未创建文件只能退出或者创建文件
            printf("文件系统未创建,请创建文件系统。");
            continue;
        }
        int a;
        read(Disk,&a,sizeof(int));
        if(Disk != -1 &&a == 0 && !(choice == 1 || choice == 2)){
            printf("未格式化,请格式化.");
            lseek(Disk,-sizeof(int),SEEK_CUR);
            continue;
        }
        lseek(Disk,-sizeof(int),SEEK_CUR);
        switch(choice){
            case 0://创建系统
                create_filesystem();
                break;
            case 1://格式化
                init(1);
                break;
            case 2://低级格式化
                low_init();
                break;
            case 3://查看信息
                info();
                break;
            case 4://创建文件
                printf("输入你要创建的文件名: ");
                char filename[20];
                scanf("%s",filename);
                create_file(filename);
                break;
            case 5://查看位示图
                weishi();
                break;
            case 6://删除文件
                printf("请输入你要删除的文件:");
                char delname[20];
                scanf("%s",delname);
                delfile(delname);
                break;
            case 8://帮助信息
                help();
                break;
            case 9://显示文件
                ls();
                break;
            case 10:    //退出系统
                quit = 1;
                break;
            default:
                printf("%s command not found\n",com);
        }


很显然,我定义了一个while1,然后根据输入命令的比较结果来选择执行哪个分支,默认就是重新回到开头。在程序运行的时候我已经做了打开文件的操作,因为我第超级块的前4个字节的数据是固定的不为0,所以我可以用它来判断是否已经格式化,然后其它的就是跳转到相应的功能中就好了0.0


这只是基础程序框架,我们其它功能使用另外一个文件来实现

六、文件功能定义

上面我们已经实现了基础的程序功能,这部分我们要深入细节,完成每一个小功能的实现。


主要数据结构✨超级块

就是像刚才说的保存基础信息


typedef struct{
    int inodes_count,   blocks_count;               //超级块和indoe总数
    int free_inodes_count,  free_blocks_count;          //未使用block和inode量
    short block_size,   inode_size;             //block和indoe大小 B
    int first_data_block,   first_inode_block;          //第一块数据块第一块inode节点位置
    int inode_wei,      block_wei;              //位示图所在块号
    short inodes_per_block;                     //每片中的inode数目
    short frag_size;                        //每片大小
    int root_inode;                         //根节点inode号
    int size;                           //总大小B
}chao;      //超级块定义


✨inode节点

这应该是最简陋的inode节点了把?


typedef struct{
    int i_mode;     //文件类型,1表示文件,0文件表示目录项
    int i_size;     //文件的大小单位为B
    int i_blocks;       //文件所占块数 
    int i_block[15];    //索引节点 12个直接索引 1个一级索引 2个二级索引
}inode;     //inode节点定义


✨目录项

目录项不用特别复杂


typedef struct{
    char name[28];
    int inode;
}mulu;      //目录项定义


✨位示图

就简单的一个位就好了。


unsigned int weishitu[1024];//位示图定义


创建文件系统

这个功能就很简单,就创建一个128MB的文件就好了。注意判断是否已经存在文件避免覆盖。

我用了一个4K的数组循环32K次写入来达到128MB0.0


void create_filesystem(){
    if(Disk != -1){
        printf("已经存在文件系统\n");
        return;
    }
    Disk = open(DISK,O_CREAT|O_WRONLY,S_IRWXO|S_IRWXG|S_IRWXU);
    int a[1024] = {0};
    for(int i = 0;i < 1<<15;i++) write(Disk,(void *)a,sizeof(a));//创建128MB文件
    printf("创建文件系统成功\n");
}


格式化

这部分就亿点点复杂需要做的事情有一些多。

就是重写超级块、重写位示图、创建根节点信息。

代码就不放了 因为太长了-.-

可以在仓库中找到,我相信你们看得懂


低级格式化

其实我们平时也有用到低级格式化,其实就将所有数据置为0。然后再执行高级格式化就好了。


void low_init(){
    int temp;
    read(Disk,&temp,sizeof(int));
    if(temp){
        printf("此操作会清空磁盘,且不可恢复,确定?Y/N  ");
        char s[10];
        while(scanf("%s",s) != EOF && !(!strcmp(s,"Y") || !strcmp(s,"N")))
            printf("请输入正确的字符");
        if(strcmp(s,"N") == 0){
            printf("用户取消\n");
            return ;
        }
    }
    close(Disk);
    Disk = open(DISK,O_WRONLY);
    int a[1024] = {0};
    for(int i = 0;i < 1<<15;i++) write(Disk,a,sizeof(a));//全部位置置0
    close(Disk);
    Disk = open(DISK,O_RDONLY);
    printf("低级格式化完成\n");
    init(0);
}


这里其实和创建文件很类似,但是要注意判断是否已有系统给与对应提示。


创建文件

这里已经简化了很多了,只做了创建文件,并不写入数据,所以就是找空间分配空间,写入就好了。

但是要实现找inode节点、找数据节点等多个方法,所以请看源码。。


删除文件

相比较而言删除文件就简单很多直接将inode和数据区进行回收就好了。

好了,由于很多细节放代码过于复杂今天就写到这里了。


七、写在最后

这只是这次作业的的一部分,有点难懂,而且做成的也只能在电脑上跑跑看而已,第二部分是一个多线程词频统计工具,可以对某个文件目录所有英文单词的出现频率进行统计打印,就有用很多。

如果点赞破百的话,我交作业前(11.30)肯定能更新出来0.0

海报我都做好了-.-

相关文章
|
存储 C语言
|
存储 C语言
|
程序员 编译器 数据库
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
11天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
27 6
|
1月前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
36 10
|
24天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
30天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
60 7
|
30天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
30 4
|
1月前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。