Diretórios e resolução de paths no ChrisFS¶
Sobre este capítulo Estruturas e recuperação do ChrisFS
Neste capítulo
- Diretórios e resolução de paths no ChrisFS
- Escopo
- Modelo de inode de diretório
- ABI persistente de dirent
- Entries por block
- Capacidade estrutural máxima
- Representação de entry vazia
- Campo type redundante
- Flags do dirent
- Comparação de nomes
- Estrutura de path
- Tamanho máximo do path
- Limite por componente
- Profundidade máxima
- Sintaxe de root
- Sintaxe rejeitada
- Sem entries dot
- Traversal enraizado
- Permissão WALK
- Lookup de diretório
- Mascaramento de erro durante scan
- Complexidade de lookup
- Campo size do diretório
- Inserção
- Reuso de slots
- Remoção
- Teste de vazio
- Criação de diretório
- Criação de arquivo
- Listing
- Lacuna de permissão no diretório alvo
- Semântica de rename
- Rename é add-then-remove
- Bug de ciclo em rename de diretório
- Unlink
- rmdir
- Traversal de diretórios no fsck
- fsck percorre apenas direct directory blocks
- Duplicate-name check reinicia por block
- Sem cross-check de type
- Sem accounting completo de reachability
- Evidência de validação
- Perfil de complexidade
- Limitações atuais
- Fronteira de roadmap
- Mapa de source e revisão
Escopo¶
Diretórios do ChrisFS são inodes do tipo directory cujos data blocks contêm directory entries de tamanho fixo.
Não existe B-tree separado, hash table de nomes, formato especial de inode para diretório ou índice de diretórios. Lookup de nomes é scan linear sobre blocks e entries.
A camada atual combina quatro responsabilidades:
- codificação persistente de dirents;
- parsing de path;
- traversal enraizado em root;
- mutação do namespace.
A implementação é compacta, mas essa simplicidade faz a correção depender de ordem de scan, estado dos inodes e serialização de nível superior.
Este capítulo documenta ChrisOS e05a17fd76333114a3fb5c2452f38ca747d4ac56.
Modelo de inode de diretório¶
Diretório usa a mesma CfsInode de arquivo.
O campo que o distingue é:
Os dados vivem em blocks normais da data region alocados por file_lba.
A implementação limita diretórios a:
Logo, diretórios usam apenas direct e single-indirect addressing.
Double e triple indirection não fazem parte do contrato de traversal de diretório.
ABI persistente de dirent¶
Cada CfsDirent ocupa:
Layout on-disk:
| Offset | Tamanho | Campo |
|---|---|---|
| 0 | 4 | inode |
| 4 | 1 | type |
| 5 | 1 | name_len |
| 6 | 2 | flags |
| 8 | 64 | name |
| 72 | 8 | padding reservado zerado |
O encoder zera os 80 bytes antes de gravar os campos.
A área persistente do nome possui exatamente:
Nomes são byte sequences delimitadas por tamanho, não strings NUL-terminated no disk.
Entries por block¶
Com setores de 512 bytes:
Seis entries consomem:
restando:
em cada setor de diretório.
Esses 32 bytes finais não pertencem a nenhum dirent.
Capacidade estrutural máxima¶
Com 140 blocks e seis entries por block:
Esse é um ceiling estrutural.
O filesystem inteiro possui somente 2047 inode slots não-root, então o total global de objetos também limita a capacidade prática.
Representação de entry vazia¶
O runtime considera uma entry ativa quando:
Dirent removido é totalmente zerado.
Como inode 0 é reservado para root e root não aparece como child dirent, usar inode zero como marcador de vazio é compatível com o namespace atual.
Campo type redundante¶
Cada dirent grava um byte de type.
dir_add coloca nele o tipo do inode filho.
Porém lookup e listing não o tratam como autoridade:
dir_finddevolve o inode number;- callers carregam o inode;
list_dir_inodeusainode.type.
O fsck também não compara hoje:
com:
Logo, esse type pode ficar stale sem detecção.
Flags do dirent¶
O campo flags de 16 bits é codificado e decodificado, mas não possui semântica ativa no namespace atual.
Entries novas são zeradas e normalmente ficam com flags zero.
Não há feature negotiation para flags não zero.
Comparação de nomes¶
name_equal executa:
- comparação exata de tamanho;
- comparação byte a byte.
Assim, nomes são:
- case-sensitive;
- byte-sensitive;
- sem Unicode normalization;
- sem locale.
Os testes verificam explicitamente que:
são nomes diferentes.
Estrutura de path¶
O parser preenche PathParts:
Ele não aloca memória nem copia os componentes.
Offsets e tamanhos apontam diretamente para a string original do caller.
Portanto a string precisa permanecer válida durante a operação.
Tamanho máximo do path¶
O máximo aceito é:
Um path com exatamente 512 bytes não-NUL é aceito se os componentes também forem válidos.
513 bytes retornam:
O terminador NUL não conta no limite.
Limite por componente¶
O máximo é:
64 bytes são aceitos.
65 retornam:
Profundidade máxima¶
O número máximo de componentes é:
Um path com 33 componentes retorna:
O teste host cobre esse caso.
Sintaxe de root¶
O parser aceita:
como root.
Paths comuns podem ser:
ou:
Ambos começam em root.
ChrisFS não possui current working directory nesta camada.
Path sem slash inicial continua sendo root-relative.
Sintaxe rejeitada¶
O parser rejeita:
- separadores repetidos, como
A//B; - trailing slash, como
A/B/; - backslash;
.;..;- componentes em excesso;
- componentes maiores que 64 bytes.
Depois de remover um slash inicial, outro slash imediato é visto como componente vazio.
Logo:
é inválido.
Sem entries dot¶
Diretórios não contêm:
O parser rejeita esses componentes.
Também não existe parent pointer persistido no inode de diretório.
Isso simplifica o formato, mas relações de ancestralidade precisam ser inferidas percorrendo entries a partir do root.
Traversal enraizado¶
Todo lookup começa em:
walk_full resolve todos os componentes.
walk_parent resolve todos exceto o leaf e devolve:
- parent inode ID;
- índice do componente final.
Create, unlink, mkdir, rmdir e rename usam parent traversal porque o leaf pode ainda não existir.
Permissão WALK¶
Em componentes intermediários, traversal exige:
no diretório filho que será atravessado.
Para:
WALK é testado em A e B.
Não há teste explícito de WALK em root antes do primeiro lookup.
O objeto final não precisa de WALK se nenhum componente vier depois.
Lookup de diretório¶
dir_find:
- carrega o directory inode;
- verifica type;
- percorre block numbers 0..139;
- resolve cada block por
file_lba(..., alloc=0); - lê cada block existente;
- percorre seis dirents;
- compara name length e bytes;
- devolve o primeiro inode correspondente.
Se o inode ID armazenado ultrapassa a tabela fixa, retorna:
Mascaramento de erro durante scan¶
Vários scans fazem:
Isso ocorre em helpers como:
dir_find;dir_count;dir_add;dir_remove;list_dir_inode.
O problema é que file_lba(..., alloc=0) pode falhar não apenas porque o logical block não está alocado, mas também por corrupção ou falha de I/O em pointer table.
Assim, alguns erros podem ser transformados em:
CFS_ENOENT;- contagem menor;
- listing incompleto;
- tentativas posteriores de allocation.
A camada precisaria distinguir explicitamente “logical block ausente” de erro de corrupção/I/O.
Complexidade de lookup¶
Não existe índice de nomes.
Pior caso:
Para path de profundidade d:
no pior caso simples.
O limite fixo mantém o custo limitado, mas continua linear.
Campo size do diretório¶
O size do inode não representa bytes de storage alocados.
dir_add soma:
por entry adicionada.
dir_remove subtrai 80 quando possível.
Portanto é essencialmente:
e não o tamanho físico do diretório.
Lookup, listing e emptiness scan não usam esse campo como única autoridade.
Inserção¶
dir_add primeiro procura um slot vazio em todos os blocks existentes.
Guarda o primeiro encontrado.
Se não houver slot, faz novo scan 0..139 e chama file_lba(..., alloc=1) no primeiro block ausente.
Ao alocar block novo:
- persiste o inode do diretório para salvar o pointer novo;
- escolhe slot 0 como destino.
O dirent recebe:
- inode ID;
- inode type;
- name length;
- name bytes.
Depois o parent size aumenta em 80 e o inode é persistido.
Reuso de slots¶
Entries removidas são zeradas, mas o block permanece alocado.
Uma inserção futura reutiliza o primeiro slot vazio.
Isso reduz churn, mas significa que blocks de diretório só crescem até o inode inteiro ser removido.
Não há compaction.
Remoção¶
dir_remove procura linearmente o nome.
Ao encontrar:
- zera os 80 bytes do dirent;
- grava o setor;
- reduz directory size em 80 quando possível;
- grava o inode.
Ele não:
- desloca entries;
- libera block que ficou vazio;
- poda pointer block que deixou de ser necessário.
Por isso diretório pode manter vários blocks alocados mesmo depois de perder quase todas as entries.
Teste de vazio¶
dir_count percorre os blocks e conta entries com:
cfs_rmdir exige count zero.
É mais robusto que confiar somente em:
quando size e contents divergirem.
Criação de diretório¶
cfs_mkdir:
- parseia path;
- resolve parent;
- exige WRITE no parent;
- garante que destino não existe;
- inicia journal;
- aloca inode;
- muda type de FILE para DIR;
- adiciona entry no parent;
- incrementa filesystem generation;
- faz commit.
Se inserir no parent falha, tenta zerar o inode recém-alocado.
Diretório novo começa sem data block; o primeiro só é alocado quando aparece o primeiro filho.
Criação de arquivo¶
cfs_create segue lógica semelhante, mas atualmente não envolve inode allocation + dirent insertion em jnl_begin/jnl_commit.
Assim, create possui cobertura de crash mais fraca que mkdir.
Há rollback local tentando zerar o inode se dir_add falhar.
Listing¶
cfs_list chama:
para listar root.
cfs_list_at resolve o path e chama list_dir_inode.
Para cada dirent ativo:
- copia o nome para buffer temporário NUL-terminated;
- carrega child inode;
- chama callback com:
- name;
- inode size;
- inode type.
Se callback retorna não zero, esse valor é propagado imediatamente.
Isso permite interrupção antecipada da listagem.
Lacuna de permissão no diretório alvo¶
Traversal exige WALK apenas nos diretórios intermediários.
Depois de resolver o target, list_dir_inode não chama:
no diretório listado.
Logo, listing não exige explicitamente READ ou WALK no próprio target, desde que o path até ele possa ser resolvido.
É uma lacuna na política de permissões.
Semântica de rename¶
cfs_rename suporta rename no mesmo diretório e move entre diretórios dentro do mesmo ChrisFS.
O fluxo:
- resolve source/destination parents;
- verifica WRITE no source parent;
- verifica WRITE no destination parent se for diferente;
- resolve source;
- rejeita destination existente;
- carrega source inode;
- adiciona dirent no destino apontando para o mesmo inode;
- remove dirent original;
- incrementa generation.
Dados e block tree do inode não são copiados.
Rename é add-then-remove¶
A ordem é:
Não há journal envolvendo a operação e não há rollback se a remoção falhar.
Falha após o destination add pode deixar dois nomes apontando para o mesmo inode.
Como ChrisFS não define hard links como feature normal, isso é estado parcial de rename.
Bug de ciclo em rename de diretório¶
Não existe ancestor check antes de mover diretório.
Considere:
Um rename como:
pode resolver destination parent A/B, inserir em B uma entry para inode A e então remover A do root.
O resultado cria ciclo:
Como não existe parent pointer persistido, prevenção exige teste explícito de descendência antes do move.
Esse check não existe atualmente.
Unlink¶
cfs_unlink aceita somente regular file.
Exige:
- WRITE no parent;
- WRITE no target file.
Inicia journal, remove dirent, libera inode e blocks, incrementa generation e commit.
Aplicado a diretório retorna:
rmdir¶
cfs_rmdir exige:
- target diferente de root;
- target type DIR;
- WRITE no parent;
- WRITE no diretório alvo;
dir_count == 0.
Depois remove o dirent e libera o inode.
Diferente de unlink/mkdir, rmdir não está dentro de journal begin/commit.
A limitação de release de diretórios que já chegaram ao single-indirect está documentada no capítulo de inodes: data blocks indiretos podem permanecer leaked após rmdir.
Traversal de diretórios no fsck¶
O fsck executa um graph walk separado.
Ele valida:
- inode ID de dirent;
- name length;
- checksum do child inode;
- ciclos de diretório;
- nomes duplicados em parte do namespace percorrido.
Porém há lacunas importantes.
fsck percorre apenas direct directory blocks¶
check_dirents usa:
ou seja:
Runtime permite 140 blocks por diretório.
Entries no single-indirect não passam pelo namespace walk do fsck.
Os blocks em si ainda podem aparecer no traversal genérico de ownership, mas seus dirents não são verificados quanto a:
- inode ID inválido;
- erro de nome;
- recursão;
- cycle.
Duplicate-name check reinicia por block¶
Dentro de cada direct block, fsck reseta o contador local de nomes.
Assim, duplicatas dentro do mesmo setor podem ser encontradas.
Porém o conjunto não é mantido entre blocks.
Dois nomes iguais em blocks diferentes podem escapar desse check.
No runtime, dir_find devolverá o primeiro encontrado na ordem de scan.
Sem cross-check de type¶
fsck carrega o inode referenciado, mas não compara seu type com o byte type do dirent.
Dirent pode dizer FILE e apontar para DIR, ou vice-versa, sem esse mismatch ser reportado.
Listing usa o inode type real, ocultando o stale type do dirent para callers.
Sem accounting completo de reachability¶
fsck percorre todos os inodes para ownership de blocks e separadamente percorre diretórios a partir do root.
Ele não mantém um mapa completo exigindo que todo inode alocado não-root seja alcançável do namespace.
Um inode válido, alocado e sem dirent pode não ser reportado diretamente como orphan.
Da mesma forma, múltiplos dirents podem apontar para um mesmo file inode sem existir um link-count invariant, porque ChrisFS não tem link-count field.
Evidência de validação¶
tools/test_cfs_paths.c cobre:
- root directories;
- nested paths;
- slash inicial opcional;
- nomes case-sensitive;
- root/nested listing;
- rename no mesmo diretório;
- rename entre diretórios;
- unlink;
- rejeição de rmdir não-vazio;
- rmdir vazio;
- rejeição de
.; - rejeição de
..; - rejeição de 33 componentes;
- persistência após remount;
- fsck final.
Não cobre hoje:
- boundary 64/65 de componente;
- boundary 512/513 de path;
- repeated slash;
- trailing slash;
- capacidade perto de 840 entries;
- diretórios usando blocks acima dos 12 direct;
- nomes duplicados entre blocks após corrupção;
- rename de diretório para seu próprio descendente;
- permissão do diretório alvo em listing;
- falha de I/O injetada em pointer table durante scan.
Perfil de complexidade¶
Lookup¶
com ceiling estrutural de 840 entries.
Add¶
No pior caso faz um scan por slots livres e outro por block ausente:
com constante maior que lookup.
Remove¶
Path resolution¶
Para profundidade d:
Não existe dentry/path cache na camada ChrisFS.
Limitações atuais¶
Na revisão documentada:
- nomes são bytes case-sensitive;
- sem Unicode normalization;
- sem
.ou..; - sem current working directory;
- sem parent pointer;
- componente máximo de 64 bytes;
- path máximo de 512 bytes;
- profundidade máxima de 32;
- lookup linear;
- ceiling estrutural de 840 entries;
- dirent type e flags com semântica fraca/redundante;
- sem checksum por block de diretório;
- sem índice;
- sem compaction/shrink de blocks;
- alguns erros de
file_lbasão mascarados durante scans; - listing não exige permissão explícita no target dir;
- create/rmdir/rename não são journaled uniformemente;
- rename pode deixar links duplicados em falha parcial;
- rename pode criar cycle ao mover diretório para descendente;
- fsck valida namespace somente nos 12 direct blocks;
- duplicate-name detection do fsck é local a cada block;
- fsck não compara dirent type com inode type;
- fsck não exige reachability completa de todos os inodes a partir do root;
- rmdir pode vazar indirect directory blocks, como descrito no capítulo de inodes.
Fronteira de roadmap¶
Uma camada de diretórios mais forte deveria considerar:
- resultado distinto para “logical block não alocado” em vez de mascarar erros;
- lookup indexado ou hash;
- permission checks explícitos no target directory;
- create/rmdir/rename transacionais;
- rollback de rename;
- prevenção de descendant cycle;
- compaction/pruning de directory blocks;
- fsck cobrindo single-indirect directory blocks;
- validação de nomes duplicados no diretório inteiro;
- consistency check entre dirent type e inode type;
- reachability de inodes alocados;
- link-count explícito se múltiplos nomes vierem a ser suportados;
- testes de boundary/corruption para parser e diretórios grandes.
Esses itens permanecem roadmap até serem implementados e cobertos por testes reproduzíveis.
Mapa de source e revisão¶
kernel/fs/cfs_format.h define ABI persistente do dirent de 80 bytes. kernel/fs/cfs.h define limites de path e PathParts. kernel/fs/cfs.c implementa parsing, traversal, lookup, listing e mutações do namespace. kernel/fs/cfs_fsck.c implementa a validação atual do graph de diretórios. tools/test_cfs_paths.c fornece a principal evidência host-side.
Todas as afirmações sobre comportamento atual neste capítulo foram reconciliadas com ChrisOS e05a17fd76333114a3fb5c2452f38ca747d4ac56.