                           Escrevendo uma classe GEOM

  Ivan Voras

       <ivoras@FreeBSD.org>
     

   Revisao: c3479f8285

   FreeBSD is a registered trademark of the FreeBSD Foundation.

   Intel, Celeron, Centrino, Core, EtherExpress, i386, i486, Itanium,
   Pentium, and Xeon are trademarks or registered trademarks of Intel
   Corporation or its subsidiaries in the United States and other countries.

   Many of the designations used by manufacturers and sellers to distinguish
   their products are claimed as trademarks. Where those designations appear
   in this document, and the FreeBSD Project was aware of the trademark
   claim, the designations have been followed by the "(TM)" or the "(R)"
   symbol.

   2020-08-05 22:00:43 +0000 por Danilo G. Baio.
   Resumo

   Este texto documenta alguns pontos de partida no desenvolvimento de
   classes GEOM e modulos do kernel em geral. Supoe-se que o leitor esteja
   familiarizado com a programac,ao C do userland.

   [ Documento HTML em partes / Documento HTML completo ]

     ----------------------------------------------------------------------

   Indice

   1. Introduc,ao

   2. Preliminares

   3. Programac,ao do kernel do FreeBSD

   4. Programac,ao GEOM

1. Introduc,ao

  1.1. Documentac,ao

   A documentac,ao sobre programac,ao do kernel e escassa - e uma das poucas
   areas na qual nao ha quase nada de tutoriais amigaveis, e a frase "usa a
   fonte!" realmente e verdadeira. No entanto, existem alguns pedac,os
   (alguns deles seriamente desatualizados) flutuando por ai e que devem ser
   estudados antes de comec,ar a codificar:

     * O Manual do Desenvolvedor do FreeBSD - parte do projeto de
       documentac,ao, ele nao contem nenhum informac,ao especifica para a
       programac,ao do kernel, mas possui algumas informac,oes gerais uteis.

     * O Manual de Arquitetura do FreeBSD - tambem do projeto de
       documentac,ao, contem descric,oes de varias instalac,oes e
       procedimentos de baixo nivel. O capitulo mais importante e o 13,
       Escrevendo drivers de dispositivo FreeBSD.

     * A sec,ao Blueprints do site do FreeBSD Diary contem varios artigos
       interessantes sobre os recursos do kernel.

     * As paginas de manual na sec,ao 9 - para documentac,ao importante sobre
       as func,oes do kernel.

     * A pagina man geom(4) e os Slides sobre o GEOM de PHK - para uma
       introduc,ao geral do subsistema GEOM.

     * Paginas de manual g_bio(9), g_event(9), g_data(9), g_geom(9),
       g_provider(9) g_consumer(9), g_access(9) & outros ligados a partir
       deles, para documentac,ao sobre funcionalidades especificas.

     * A pagina do manual style(9) - para documentac,ao sobre as convenc,oes
       de estilo de codificac,ao que devem ser seguidas para qualquer codigo
       que se destine a ser incorporado na arvore do FreeBSD.

2. Preliminares

   A melhor maneira de fazer o desenvolvimento do kernel e ter (pelo menos)
   dois computadores separados. Um deles conteria o ambiente de
   desenvolvimento e o codigo fonte, e o outro seria usado para testar o
   codigo recem escrito, inicializando por meio da rede e montando seu
   sistema de arquivo a partir do primeiro computador. Desta forma, se o novo
   codigo contiver erros e travar a maquina, isso nao ira atrapalhar o codigo
   fonte (e nem nenhum outros dado "vivo"). O segundo sistema nem sequer
   requer um monitor adequado. Em vez disso, ele pode ser conectado por meio
   de um cabo serial ou KVM ao primeiro computador.

   Mas, como nem todo mundo tem dois ou mais computadores `a mao, ha algumas
   coisas que podem ser feitas para preparar um sistema "vivo " para
   desenvolver codigo para o kernel. Esta configurac,ao tambem e aplicavel
   para desenvolvimento em uma maquina virtual criada com o VMWare ou com o
   QEmu (a proxima melhor coisa depois de uma maquina de desenvolvimento
   dedicada).

  2.1. Modificando um sistema para desenvolvimento

   Para qualquer programac,ao do kernel, um kernel com a opc,ao INVARIANTS
   ativada e obrigatorio. Entao, digite estas linhas no seu arquivo de
   configurac,ao do kernel:

 options INVARIANT_SUPPORT
 options INVARIANTS

   Para ter um maior nivel de depurac,ao, voce tambem devra incluir o suporte
   ao WITNESS, o qual ira alerta-lo sobre erros relacionados a bloqueios
   (locking):

 options WITNESS_SUPPORT
 options WITNESS

   Para depurar despejos de memoria, e necessario um kernel com simbolos de
   depurac,ao:

   makeoptions    DEBUG=-g

   Com a maneira usual de instalar o kernel (make installkernel) o kernel de
   depurac,ao nao sera instalado automaticamente. Ele e chamado de
   kernel.debug e fica localizado em /usr/obj/usr/src/sys/KERNELNAME/. Por
   conveniencia, deve ser copiado para /boot/kernel/.

   Outra conveniencia e habilitar o depurador do kernel para que voce possa
   examinar o panic do kernel quando isso acontece. Para isso, insira as
   seguintes linhas no seu arquivo de configurac,ao do kernel:

 options KDB
 options DDB
 options KDB_TRACE

   Para que isso funcione, voce pode precisar definir um sysctl (se ele nao
   estiver ativado por padrao):

   debug.debugger_on_panic=1

   Kernel panics acontecerao, portanto, deve-se ter cuidado com o cache do
   sistema de arquivos. Em particular, ter o softupdates habilitado pode
   significar que a versao mais recente do arquivo pode ser perdida se um
   panic ocorrer antes de ser committed para armazenamento. Desativar o
   softupdates produz um grande impacto na performance e ainda nao garante a
   consistencia dos dados. A montagem do sistema de arquivos com a opc,ao
   "sync" e necessaria para isso. Para um compromisso, os atrasos do cache de
   softupdates podem ser encurtados. Existem tres sysctl's que sao uteis para
   isso (melhor ser configurado em /etc/sysctl.conf):

 kern.filedelay=5
 kern.dirdelay=4
 kern.metadelay=3

   Os numeros representam segundos.

   Para depurar os panics do kernel, os dumps do nucleo do kernel sao
   necessarios. Como um kernel panic pode tornar os sistemas de arquivos
   inutilizaveis, esse despejo de memoria e primeiramente gravado em uma
   partic,ao bruta. Normalmente, esta e a partic,ao de swap. Essa partic,ao
   deve ser pelo menos tao grande quanto a RAM fisica na maquina. Na proxima
   inicializac,ao, o despejo e copiado para um arquivo normal. Isso acontece
   depois que os sistemas de arquivos sao verificados e montados e antes que
   o swap seja ativado. Isto e controlado com duas variaveis /etc/rc.conf:

 dumpdev="/dev/ad0s4b"
 dumpdir="/usr/core

   A variavel dumpdev especifica a partic,ao de swap e dumpdir informa ao
   sistema onde no sistema de arquivos ele devera realocar o dump principal
   na reinicializac,ao.

   A gravac,ao de core dumps e lenta e leva muito tempo, entao se voce tiver
   muita memoria (>256M) e muitos panics, pode ser frustrante sentar e
   esperar enquanto isso e feito (duas vezes - primeiro para gravar para o
   swap, depois para realoca-lo para o sistema de arquivos). E conveniente
   limitar a quantidade de RAM que o sistema usara atraves de uma variavel do
   /boot/loader.conf:

   hw.physmem="256M"

   Se os panics sao frequentes e os sistemas de arquivos sao grandes (ou voce
   simplesmente nao confia em softupdates + background fsck), e aconselhavel
   desligar o fsck em background atraves da variavel /etc/rc.conf:

   background_fsck="NO"

   Dessa forma, os sistemas de arquivos sempre serao verificados quando
   necessario. Observe que, com o fsck em segundo plano, um novo panic pode
   acontecer enquanto ele esta verificando os discos. Novamente, a maneira
   mais segura e nao ter muitos sistemas de arquivos locais, usando o outro
   computador como um servidor NFS.

  2.2. Comec,ando o projeto

   Para o proposito de criar uma nova classe GEOM, um subdiretorio vazio deve
   ser criado sob um diretorio arbitrario acessivel pelo usuario. Voce nao
   precisa criar o diretorio do modulo em /usr/src.

  2.3. O Makefile

   E uma boa pratica criar Makefiles para cada projeto de codificac,ao nao
   trivial, o que obviamente inclui modulos do kernel.

   Criar o Makefile e simples grac,as a um extenso conjunto de rotinas
   auxiliares fornecidas pelo sistema. Em suma, aqui esta um exemplo de como
   um Makefile minimo para um modulo do kernel se parece:

 SRCS=g_journal.c
 KMOD=geom_journal

 .include <bsd.kmod.mk>

   Este Makefile (com nomes de arquivos alterados) serve para qualquer modulo
   do kernel, e uma classe GEOM pode residir em apenas um modulo do kernel.
   Se mais de um arquivo for necessario, liste-o na variavel SRCS, separado
   com espac,o em branco de outros nomes de arquivos.

3. Programac,ao do kernel do FreeBSD

  3.1. Alocac,ao de memoria

   Veja o malloc(9). A alocac,ao basica de memoria e apenas ligeiramente
   diferente do seu userland equivalente. Mais notavelmente, malloc() e
   free() aceitam parametros adicionais conforme descrito na pagina do
   manual.

   Um "malloc type" deve ser declarado na sec,ao de declarac,ao de um arquivo
   fonte, assim:

   static MALLOC_DEFINE(M_GJOURNAL, "gjournal data", "GEOM_JOURNAL Data");

   Para usar esta macro, os cabec,alhos sys/param.h, sys/kernel.h e
   sys/malloc.h devem ser incluidos.

   Existe outro mecanismo para alocar memoria, o UMA (Universal Memory
   Allocator). Veja uma(9) para detalhes, mas ele e um tipo especial de
   alocador usado principalmente para alocac,ao rapida de listas compostas de
   itens do mesmo tamanho (por exemplo, matrizes dinamicas de estruturas).

  3.2. Listas e filas

   Veja queue(3). Ha MUITOS casos quando uma lista de coisas precisa ser
   mantida. Felizmente, essa estrutura de dados e implementada (de varias
   maneiras) por macros C incluidas no sistema. O tipo de lista mais usado e
   o TAILQ, porque e o mais flexivel. E tambem aquele com os maiores
   requisitos de memoria (seus elementos sao duplamente vinculados) e tambem
   o mais lento (embora a variac,ao de velocidade seja mais da ordem de
   varias instruc,oes da CPU, portanto, ela nao deve ser levada a serio).

   Se a velocidade de recuperac,ao de dados for muito importante, veja
   tree(3) e hashinit(9).

  3.3. BIOS

   A estrutura bio e usada para todas e quaisquer operac,oes de Input/Output
   relativas ao GEOM. Ele basicamente contem informac,oes sobre qual
   dispositivo ('provedor') deve satisfazer a solicitac,ao, tipo de pedido,
   offset, comprimento, ponteiro para um buffer e um monte de sinalizadores
   "especificos do usuario" e campos que podem ajudar a implementar varios
   hacks.

   O importante aqui e que os bios sao tratados de forma assincrona. Isso
   significa que, na maior parte do codigo, nao ha nenhum analogo as chamadas
   read(2) e write(2) que nao retornam ate que uma solicitac,ao seja feita.
   Em vez disso, uma func,ao fornecida pelo desenvolvedor e chamada como uma
   notificac,ao quando a solicitac,ao e concluida (ou resulta em erro).

   O modelo de programac,ao assincrona (tambem chamado de "orientado a
   eventos") e um pouco mais dificil do que o imperativo muito mais usado no
   userland (pelo menos leva um tempo para se acostumar com isso). Em alguns
   casos, as rotinas auxiliares g_write_data() e g_read_data() podem ser
   usadas, mas nem sempre. Em particular, elas nao podem ser usadas quando um
   mutex e mantido; por exemplo, o mutex de topologia GEOM ou o mutex interno
   mantido durante as func,oes .start() e .stop().

4. Programac,ao GEOM

  4.1. Ggate

   Se o desempenho maximo nao for necessario, uma maneira muito mais simples
   de fazer uma transformac,ao de dados e implementa-lo na area do usuario
   por meio do recurso ggate (GEOM gate). Infelizmente, nao existe uma
   maneira facil de converter ou ate mesmo compartilhar codigo entre as duas
   abordagens.

  4.2. Classe GEOM

   Classes GEOM sao transformac,oes nos dados. Essas transformac,oes podem
   ser combinadas em uma forma de arvore. Instancias de classes GEOM sao
   chamadas de geoms.

   Cada classe GEOM possui varios "metodos de classe" que sao chamados quando
   nao ha nenhuma instancia geom disponivel (ou simplesmente nao estao
   vinculados a uma unica instancia):

     * .init e chamada quando o GEOM toma conhecimento de uma classe GEOM
       (quando o modulo do kernel e carregado).

     * .fini e chamada quando o GEOM abandona a classe (quando o modulo e
       descarregado)

     * .taste e chamada next, uma vez para cada provedor que o sistema tiver
       disponivel. Se aplicavel, essa func,ao geralmente criara e iniciara
       uma instancia geom.

     * .destroy_geom e chamada quando o geom deve ser desfeito

     * .ctlconf e chamado quando o usuario solicita a reconfigurac,ao do geom
       existente

   Tambem sao definidas as func,oes de evento GEOM, que serao copiadas para a
   instancia geom.

   O campo .geom na estrutura g_class e uma LISTA de geoms instanciados a
   partir da classe.

   Estas func,oes sao chamadas a partir da thread g_event do kernel.

  4.3. Softc

   O nome "softc" e um termo legado para "dados privados do driver". O nome
   provavelmente vem do termo arcaico "bloco de controle de software". No
   GEOM, ele e uma estrutura (mais precisamente: ponteiro para uma estrutura)
   que pode ser anexada a uma instancia geom para armazenar quaisquer dados
   que sejam privados para a instancia geom. A maioria das classes GEOM
   possui os seguintes membros:

     * struct g_provider *provider : O "provedor" que este geom instancia

     * uint16_t n_disks : Numero de consumidores que este geom consome

     * struct g_consumer **disks: Array de struct g_consumer*. (Nao e
       possivel usar apenas uma unica via indireta porque o struct
       g_consumer* e criado em nosso nome pela GEOM).

   A estrutura softc contem todo o estado da instancia geom. Cada instancia
   geom possui seu proprio softc.

  4.4. Metadados

   O formato dos metadados e mais ou menos dependente da classe, mas DEVE
   comec,ar com:

     * Buffer de 16 bytes para uma assinatura de terminac,ao nula (geralmente
       o nome da classe)

     * ID da versao uint32

   Assume-se que as classes geom sabem como lidar com metadados com ID de
   versao menores que os deles.

   Os metadados estao localizados no ultimo setor do provedor (e, portanto,
   devem caber nele).

   (Tudo isso depende da implementac,ao, mas todo o codigo existente funciona
   assim, e e suportado por bibliotecas.)

  4.5. Rotulando/criando um GEOM

   A sequencia de eventos e:

     * o usuario chama o utilitario geom(8) (ou um de seus equivalentes
       hardlinked)

     * o utilitario descobre qual classe geom ele e suposto manipular e
       procura pela biblioteca geom_CLASSNAME.so (geralmente em /lib/geom).

     * ele dlopen(3)-s a biblioteca, extrai as definic,oes dos parametros da
       linha de comandos e func,oes auxiliares.

   No caso da criac,ao/rotulac,ao de um novo geom, isso e o que acontece:

     * O geom(8) procura no argumento da linha de comando pelo comando
       (geralmente label) e chama uma func,ao auxiliar .

     * A func,ao auxiliar verifica parametros e reune metadados, que sao
       gravados em todos os provedores envolvidos.

     * Este "estraga" geoms existentes (se existirem) e inicializa uma nova
       rodada de "degustac,ao" dos provedores. A classe geom pretendida
       reconhece os metadados e carrega o geom.

   (A sequencia de eventos acima e dependente da implementac,ao, mas todo o
   codigo existente funciona assim, e e suportado pelas bibliotecas.)

  4.6. Estrutura do Comando GEOM

   A biblioteca helper geom_CLASSNAME.so exporta a estrutura class_commands,
   que e uma matriz dos elementos struct g_command. Os comandos sao uniformes
   no formato e se parecem com:

   verb [-options] geomname [other]

   Verbos comuns sao:

     * label - para gravar metadados em dispositivos para que eles possam ser
       reconhecidos em degustac,oes e criados em geoms

     * destroy - para destruir metadados, para que as geoms sejam destruidas

   Opc,oes comuns sao:

     * -v : be verbose

     * -f : force

   Muitas ac,oes, como rotular e destruir metadados, podem ser executadas no
   userland. Para isso, struct g_command fornece o campo gc_func que pode ser
   definido para uma func,ao (no mesmo .so) que sera chamada para processar
   um verbo. Se gc_func for NULL, o comando sera passado para o modulo do
   kernel, para a func,ao .ctlreq da classe geom.

  4.7. Geoms

   Geoms sao instancias de classes GEOM. Eles possuem dados internos (uma
   estrutura softc) e algumas func,oes com as quais eles respondem a eventos
   externos.

   As func,oes de evento sao:

     * .acess: calcula permissoes (leitura / escrita / exclusiva)

     * .dumpconf: retorna informac,oes formatadas em XML sobre o geom

     * .orphan: chamado quando algum provedor subjacente e desconectado

     * .spoiled: chamado quando algum provedor subjacente e gravado

     * .start: lida com I/O

   Estas func,oes sao chamadas a partir da thread g_down do kernel e nao pode
   haver sleeping neste contexto, (veja a definic,ao de sleeping em outro
   lugar) o que limita um pouco o que pode ser feito, mas forc,a o manuseio a
   ser rapido .

   Destes, a func,ao mais importante para fazer o trabalho util real e a
   func,ao .start(), que e chamada quando uma requisic,ao BIO chega para um
   provedor gerenciado por uma instancia da classe geom.

  4.8. Threads GEOM

   Existem tres threads de kernel criados e executados pelo framework GEOM:

     * g_down : trata de solicitac,oes provenientes de entidades de alto
       nivel (como uma solicitac,ao do userland) no caminho para dispositivos
       fisicos

     * g_up : lida com respostas de drivers de dispositivos para
       solicitac,oes feitas por entidades de nivel superior

     * g_event : lida com todos os outros casos: criac,ao de instancias geom,
       contagem de acessos, eventos "spoil", etc.

   Quando um processo do usuario emite um pedido de "leitura de dados X no
   deslocamento Y de um arquivo", isto e o que acontece:

     * O sistema de arquivos converte o pedido em uma instancia struct bio e
       o transmite para o subsistema GEOM. Ele sabe o que a instancia geom
       deve manipular porque os sistemas de arquivos sao hospedados
       diretamente em uma instancia geom.

     * A requisic,ao termina como uma chamada para a func,ao .start() feita
       para a thread g_down e atinge a instancia geom de nivel superior.

     * Essa instancia geom de nivel superior (por exemplo, o segmentador de
       partic,oes) determina que a solicitac,ao deve ser roteada para uma
       instancia de nivel inferior (por exemplo, o driver de disco). Ele faz
       uma copia da solicitac,ao bio (solicitac,oes bio SEMPRE precisam ser
       copiadas entre instancias, com g_clone_bio()!), modifica os campos de
       dados offset e de provedor de destino e executa a copia com
       g_io_request()

     * O driver de disco obtem a solicitac,ao bio tambem como uma chamada
       para .start() na thread g_down. Ela fala com o hardware, recupera os
       dados e chama g_io_deliver() na bio.

     * Agora, a notificac,ao de bio conclusao "borbulha" na thread g_up.
       Primeiro, o slicer de partic,ao obtem .done() chamado na thread g_up,
       ele usa as informac,oes armazenadas na bio para liberar a estrutura
       bio clonada (com g_destroy_bio()) e chama g_io_deliver() no pedido
       original.

     * O sistema de arquivos obtem os dados e os transfere para o usuario.

   Veja a pagina de manual para o g_bio(9) para obter informac,oes sobre como
   os dados sao passados para frente e para tras na estrutura bio (observe em
   particular os campos bio_parent e bio_children e como eles sao
   manipulados).

   Uma caracteristica importante: NAS THREADS G_UP E G_DOWN NAO SE PODE
   DORMIR (SELEEPING). Isso significa que nenhuma das seguintes coisas pode
   ser feita nessas threads (a lista nao e completa, mas apenas informativa):

     * Chamadas para msleep() e tsleep(), obviamente.

     * Chamadas para g_write_data() e g_read_data(), porque estes dormem
       entre passar os dados para os consumidores e retornar.

     * Esperando I/O.

     * Chamadas para malloc(9) e uma_zalloc() com o conjunto de flags
       M_WAITOK

     * sx e outros sleepable locks

   Esta restric,ao esta aqui para impedir que o codigo GEOM obstrua o caminho
   da solicitac,ao de I/O, ja que sleeping normalmente nao e limitado pelo
   tempo e nao pode haver garantias sobre quanto tempo levara (tambem existem
   algumas outras razoes mais tecnicas). Isso tambem significa que nao existe
   muito o que possa ser feito nessas threads; por exemplo, quase qualquer
   coisa complexa requer alocac,ao de memoria. Felizmente, existe uma saida:
   criar threads adicionais no kernel.

  4.9. Threads de kernel para uso no codigo GEOM

   As threads do kernel sao criadas com a func,ao kthread_create(9), e elas
   sao semelhantes aos threads do userland no comportamento, eles somente nao
   podem retornar ao chamador para exprimir a conclusao, mas deve chamar
   kthread_exit(9).

   No codigo GEOM, o uso usual de threads e para descarregar o processamento
   de requisic,oes da thread g_down (a func,ao .start). Estas threads se
   parecem com um "event handlers": elas tem uma lista encadeada de eventos
   associados a elas (nos quais eventos podem ser postados por varias
   func,oes em varias threads, portanto, devem ser protegidos por um mutex),
   pegam os eventos da lista, um por um, e processa-os em uma grande
   instruc,ao switch().

   A principal vantagem de usar uma thread para lidar com solicitac,oes de
   I/O e que ela pode dormir quando necessario. Agora, isso parece bom, mas
   deve ser cuidadosamente pensado. Dormir e bom e muito conveniente, mas
   pode ser muito efetivo em destruir o desempenho da transformac,ao geom. As
   classes extremamente sensiveis ao desempenho provavelmente devem fazer
   todo o trabalho na chamada de func,ao .start(), tomando muito cuidado para
   lidar com erros de falta de memoria e similares.

   O outro beneficio de ter uma thread de manipulac,ao de eventos como essa e
   serializar todas as solicitac,oes e respostas provenientes de diferentes
   threads geom em uma thread. Isso tambem e muito conveniente, mas pode ser
   lento. Na maioria dos casos, o tratamento de pedidos .done() pode ser
   deixado para a thread g_up.

   Mutexes no kernel do FreeBSD (veja mutex(9)) tem uma distinc,ao de seus
   primos mais comuns do userland - o codigo nao pode dormir enquanto estiver
   segurando um mutex). Se o codigo precisar dormir muito, os bloqueios sx(9)
   podem ser mais apropriados. Por outro lado, se voce faz quase tudo em um
   unico thread, voce pode se safar sem utilizar mutexes.
