Tutorial: Criando um Sistema de Arquivos Simples na Memória (lkcampfs)

Este tutorial tem como objetivo demonstrar os conceitos básicos para a criação de um sistema de arquivos. Não há implementação de funções para criação ou remoção de arquivos.


Visão Geral

Neste projeto, você irá:

  • Criar um sistema de arquivos simples em memória (lkcampfs)

  • Implementar as funções básicas do VFS (Virtual File System)

  • Compilar e testar o módulo no QEMU


Etapa 1: Preparando o Ambiente

1. Acesse o diretório de filesystems do kernel:

cd linux/fs

2. Crie um novo diretório para o seu sistema de arquivos:

mkdir lkcampfs
cd lkcampfs

3. Copie o Makefile do ramfs:

cp ../ramfs/Makefile .
O Ramfs é também um filesystem que existe na memória, muito mais rápido do que escrever no disco e é inspiração para este tutorial.

4. Crie o arquivo inode.c:

touch inode.c
Como o filesystem deste tutorial existe somente na memória, e não no disco do computador, as operações são feitas usando inode.

Obs: Caso queiram investigar um exemplo de filesystem completo, que faz a escrita no disco, temos o Minix, que consegue rodar um SO inteiro em cima dele.

5. Modifique o Makefile de lkcampfs:

obj-m += lkcampfs.o
lkcampfs-objs += inode.o

6. Atualize o Makefile de linux/fs:

obj-y += lkcampfs/

Etapa 2: Inicialização do Módulo

No inode.c, adicione:

#include <linux/init.h>
#include <linux/module.h>

static int __init lkcampfs_init(void)
{
    pr_info("lkcampfs was successfully loaded\n");
    return 0;
}

static void __exit lkcampfs_exit(void)
{
    pr_info("lkcampfs was successfully unloaded\n");
}

module_init(lkcampfs_init);
module_exit(lkcampfs_exit);

MODULE_AUTHOR("LKCAMP");
MODULE_DESCRIPTION("A simple filesystem implementation");
MODULE_LICENSE("GPL");
Definimos primeiramente as operações de entrada __init() e saída __exit()


Etapa 3: Compilação e Teste Inicial

Compile o kernel:

make -j$(nproc)

Execute o kernel com QEMU:

qemu-system-x86_64 \
  -drive file=../my_disk.raw,format=raw,index=0,media=disk \
  -m 4G -nographic \
  -kernel ./arch/x86_64/boot/bzImage \
  -append "root=/dev/sda rw console=ttyS0 loglevel=6 nokaslr" \
  -fsdev local,id=fs1,path=.,security_model=none \
  -device virtio-9p-pci,fsdev=fs1,mount_tag=shared_folder \
  --enable-kvm \
  -s

Carregue o módulo na VM:

insmod host_folder/fs/lkcampfs/lkcampfs.ko

Verifique com dmesg:

lkcampfs was successfully loaded

Descarregue o módulo:

rmmod host_folder/fs/lkcampfs/lkcampfs.ko


Etapa 4: Registrando o Filesystem

1. Inclua no início do inode.c:

#include <linux/fs.h>

2. Declare as funções:

static void lkcampfs_kill_sb(struct super_block *s);
static struct dentry *lkcampfs_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data);

3. Defina a estrutura do filesystem:

static struct file_system_type lkcamp_fs_type = {
    .owner    = THIS_MODULE, // para o kernel não precisar carregar 2x o módulo na memória
    .name     = "lkcampfs", // para identificar o filesystem
    .mount    = lkcampfs_mount, // nossa função de mount
    .kill_sb  = lkcampfs_kill_sb, // função para desmount
    .fs_flags = FS_USERNS_MOUNT,
};

Etapa 5: Superbloco e Inodes

Para montar um filesystem, precisamos ler um superbloco e a partir da informação lida, criamos um inode e registramos uma dentry para a raiz do novo filesystem.

A função get_inode() recebe o superbloco, o diretório onde o inode existe e as flags de permissão do arquivo.

get_inode()

static struct inode *lkcampfs_get_inode(struct super_block *sb, const struct inode *dir, umode_t mode)
{
    struct inode *inode = new_inode(sb); // aloca um inode

    if (inode) {
        inode->i_ino = get_next_ino(); // aloca um número para nosso inode
        inode_init_owner(&nop_mnt_idmap, inode, dir, mode);
        simple_inode_init_ts(inode); //inicializa o inode

        switch (mode & S_IFMT) { //verificar o tipo setado pro inode
            case S_IFREG: //alocar o inode para um arquivo regular
                break;
            case S_IFDIR:  //alocar o inode para um diretório
                inode->i_op = &simple_dir_inode_operations; //define a tabela de funções do inode
                inode->i_fop = &simple_dir_operations;
                inc_nlink(inode); //incrementa o número de links para o novo inode
                break;
            default:
                pr_err("lkcampfs: unsupported inode type\n");
                return NULL;
        }
    }

    return inode;
}

fill_super()

A função fill_super() é chamada dentro da função mount(). Ela cria um superbloco e um inode com a função de get_inode().

Depois, apontamos a raiz do inode do nosso filesystem para esse inode criado.

#define LKCAMPFS_MAGIC 0x4D43353034

static int lkcampfs_fill_super(struct super_block *sb, void *data, int silent)
{
    struct inode *root_inode;

    sb->s_magic = LKCAMPFS_MAGIC; //define o tipo do filesystem que está sendo montado (em hex
    sb->s_blocksize = PAGE_SIZE; //define o tamanho do bloco do sistema
    sb->s_blocksize_bits = PAGE_SHIFT; //define o número de bits do bloco

    root_inode = lkcampfs_get_inode(sb, NULL, S_IFDIR);
    sb->s_root = d_make_root(root_inode);

    return sb->s_root ? 0 : -ENOMEM;
}

mount()

A função mount() é chamada pelo filesystem a partir do comando mount do usuário no terminal.

static struct dentry *lkcampfs_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data)
{
    struct dentry *ret = mount_bdev(fs_type, flags, dev_name, data, lkcampfs_fill_super);

    if (IS_ERR(ret)) {
        pr_err("lkcampfs: failed to mount\n");
    } else {
        pr_info("lkcampfs: mounted successfully on [%s]\n", dev_name);
    }

    return ret;
}

kill_sb()

Pra finalizar, o código da função kill_sb(), que destrói o superbloco.

Como nosso filesystem só existe na memória, seu código é bem simples e serve somente para que a struct lkcamp_fs_type tenha um ponteiro pruma função de kill superblock.

static void lkcampfs_kill_sb(struct super_block *s)
{
    kill_litter_super(s);
    pr_info("lkcampfs: unmounted successfully\n");
}


Etapa 6: Registro no Kernel

Atualizamos as funções de inicialização e finalização:

static int __init lkcampfs_init(void)
{
    int ret = register_filesystem(&lkcamp_fs_type);
    if (ret)
        pr_err("lkcampfs: registration failed\n");
    else
        pr_info("lkcampfs: registered successfully\n");
    return ret;
}

static void __exit lkcampfs_exit(void)
{
    int ret = unregister_filesystem(&lkcamp_fs_type);
    if (ret)
        pr_err("lkcampfs: unregistration failed\n");
    else
        pr_info("lkcampfs: unregistered successfully\n");
}

Etapa Final: Teste Completo

  1. Compile o novo código:

    make -j$(nproc)
    

  2. Carregue o módulo na VM:

    insmod host_folder/fs/lkcampfs/lkcampfs.ko
    

  3. Crie uma imagem (neste comando, a imagem é criada com 10Mb):

    dd if=/dev/zero of=a.img bs=1M count=10
    

  4. Monte o sistema na nova imagem:

    mount -t lkcampfs -o loop a.img /mnt
    

  5. Verifique com dmesg:

    lkcampfs: mounted successfully on [...]
    


Referências