Linux Kernel 5.0 - escrevendo Simple Block Device em blk-mq

Boas notícias, pessoal!

O kernel do Linux 5.0 já está aqui e aparece em distribuições experimentais como Arch, openSUSE Tumbleweed, Fedora.



E se você olhar as distribuições RC do Ubuntu Disko Dingo e Red Hat 8, fica claro: em breve o kernel 5.0 também será transferido dos desktops dos fãs para servidores sérios.
Alguém dirá - e daí? O próximo lançamento, nada de especial. Então o próprio Linus Torvalds disse:
Gostaria de salientar (mais uma vez) que não fazemos lançamentos baseados em recursos e que "5.0" não significa nada mais do que os números 4.x começaram a ficar grandes o suficiente para ficar sem dedos e dedos dos pés.

( Mais uma vez repito - nossos lançamentos não estão vinculados a nenhum recurso específico, portanto, o número da nova versão 5.0 significa apenas que, para numerar as versões 4.x, eu já não tenho dedos das mãos e pés )

No entanto, o módulo para disquetes (que não sabe - são discos do tamanho de uma camisa de bolso, com capacidade de 1,44 MB) - corrigido ...
E aqui está o porquê:

É tudo sobre a camada de blocos com várias filas (blk-mq). Existem muitos artigos introdutórios sobre ele na Internet, então vamos direto ao ponto. A transição para o blk-mq foi iniciada há muito tempo e avançava lentamente. Apareceu o scsi de várias filas (parâmetro do kernel scsi_mod.use_blk_mq), novos agendadores mq-deadline, bfq e assim por diante…

[root@fedora-29 sblkdev]# cat /sys/block/sda/queue/scheduler [mq-deadline] none 

A propósito, qual é a sua?

O número de drivers de dispositivo de bloco que funcionam à moda antiga foi reduzido. E no 5.0, a função blk_init_queue () foi removida como desnecessária. E agora o velho e glorioso código lwn.net/Articles/58720 de 2003 não está apenas indo, como também perdeu relevância. Além disso, as novas distribuições, que estão sendo preparadas para lançamento este ano, usam uma camada de blocos com várias filas na configuração padrão. Por exemplo, no 18º Manjaro, o kernel, apesar da versão 4.19, é blk-mq por padrão.

Portanto, podemos assumir que a transição para o blk-mq no kernel 5.0 foi concluída. E para mim, este é um evento importante que exigirá a reescrita do código e testes adicionais. O que por si só promete a aparência de bugs grandes e pequenos, além de vários servidores com falhas (é necessário, Fedya, é necessário! (C)).

A propósito, se alguém pensa que para rhel8 esse ponto de inflexão não chegou, já que o kernel foi "flashed" pela versão 4.18 lá, você está enganado. No RC recente no rhel8, novos produtos do 5.0 já haviam migrado e a função blk_init_queue () também foi cortada (provavelmente ao arrastar outro check-in do github.com/torvalds/linux para suas fontes).
Em geral, a versão "congelada" do kernel para distribuidores Linux, como SUSE e Red Hat, é um conceito de marketing há muito tempo. O sistema relata que a versão, por exemplo, é 4.4 e, de fato, a funcionalidade é de uma nova baunilha 4.8. Ao mesmo tempo, uma inscrição é exibida no site oficial como: "Na nova distribuição, mantivemos um kernel 4.4 estável para você".

Mas estávamos distraídos ...

Então aqui. Precisamos de um novo driver de dispositivo de bloco simples para deixar mais claro como isso funciona.
Portanto, a fonte em github.com/CodeImp/sblkdev . Proponho discutir, fazer solicitações pull, iniciar o problema - eu o corrigirei. O controle de qualidade ainda não foi testado.

Mais adiante neste artigo, tentarei descrever o porquê. Portanto, há muito código.
Peço desculpas imediatamente por o estilo de codificação do kernel do Linux não ser totalmente respeitado e sim - eu não gosto de ir.

Então, vamos começar pelos pontos de entrada.

 static int __init sblkdev_init(void) { int ret = SUCCESS; _sblkdev_major = register_blkdev(_sblkdev_major, _sblkdev_name); if (_sblkdev_major <= 0){ printk(KERN_WARNING "sblkdev: unable to get major number\n"); return -EBUSY; } ret = sblkdev_add_device(); if (ret) unregister_blkdev(_sblkdev_major, _sblkdev_name); return ret; } static void __exit sblkdev_exit(void) { sblkdev_remove_device(); if (_sblkdev_major > 0) unregister_blkdev(_sblkdev_major, _sblkdev_name); } module_init(sblkdev_init); module_exit(sblkdev_exit); 

Obviamente, quando o módulo é carregado, a função sblkdev_init () é iniciada, quando sblkdev_exit () é descarregado.
A função register_blkdev () registra um dispositivo de bloco. Ele recebe um número importante. unregister_blkdev () - libera esse número.

A estrutura de chaves do nosso módulo é sblkdev_device_t.

 // The internal representation of our device typedef struct sblkdev_device_s { sector_t capacity; // Device size in bytes u8* data; // The data aray. u8 - 8 bytes atomic_t open_counter; // How many openers struct blk_mq_tag_set tag_set; struct request_queue *queue; // For mutual exclusion struct gendisk *disk; // The gendisk structure } sblkdev_device_t; 

Ele contém todas as informações sobre o dispositivo necessárias para o módulo do kernel, em particular: a capacidade do dispositivo de bloco, os dados em si (isso é simples), ponteiros para o disco e a fila.

Toda a inicialização do dispositivo de bloco é realizada na função sblkdev_add_device ().

 static int sblkdev_add_device(void) { int ret = SUCCESS; sblkdev_device_t* dev = kzalloc(sizeof(sblkdev_device_t), GFP_KERNEL); if (dev == NULL) { printk(KERN_WARNING "sblkdev: unable to allocate %ld bytes\n", sizeof(sblkdev_device_t)); return -ENOMEM; } _sblkdev_device = dev; do{ ret = sblkdev_allocate_buffer(dev); if(ret) break; #if 0 //simply variant with helper function blk_mq_init_sq_queue. It`s available from kernel 4.20 (vanilla). {//configure tag_set struct request_queue *queue; dev->tag_set.cmd_size = sizeof(sblkdev_cmd_t); dev->tag_set.driver_data = dev; queue = blk_mq_init_sq_queue(&dev->tag_set, &_mq_ops, 128, BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_SG_MERGE); if (IS_ERR(queue)) { ret = PTR_ERR(queue); printk(KERN_WARNING "sblkdev: unable to allocate and initialize tag set\n"); break; } dev->queue = queue; } #else // more flexible variant {//configure tag_set dev->tag_set.ops = &_mq_ops; dev->tag_set.nr_hw_queues = 1; dev->tag_set.queue_depth = 128; dev->tag_set.numa_node = NUMA_NO_NODE; dev->tag_set.cmd_size = sizeof(sblkdev_cmd_t); dev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_SG_MERGE; dev->tag_set.driver_data = dev; ret = blk_mq_alloc_tag_set(&dev->tag_set); if (ret) { printk(KERN_WARNING "sblkdev: unable to allocate tag set\n"); break; } } {//configure queue struct request_queue *queue = blk_mq_init_queue(&dev->tag_set); if (IS_ERR(queue)) { ret = PTR_ERR(queue); printk(KERN_WARNING "sblkdev: Failed to allocate queue\n"); break; } dev->queue = queue; } #endif dev->queue->queuedata = dev; {// configure disk struct gendisk *disk = alloc_disk(1); //only one partition if (disk == NULL) { printk(KERN_WARNING "sblkdev: Failed to allocate disk\n"); ret = -ENOMEM; break; } disk->flags |= GENHD_FL_NO_PART_SCAN; //only one partition //disk->flags |= GENHD_FL_EXT_DEVT; disk->flags |= GENHD_FL_REMOVABLE; disk->major = _sblkdev_major; disk->first_minor = 0; disk->fops = &_fops; disk->private_data = dev; disk->queue = dev->queue; sprintf(disk->disk_name, "sblkdev%d", 0); set_capacity(disk, dev->capacity); dev->disk = disk; add_disk(disk); } printk(KERN_WARNING "sblkdev: simple block device was created\n"); }while(false); if (ret){ sblkdev_remove_device(); printk(KERN_WARNING "sblkdev: Failed add block device\n"); } return ret; } 

Alocamos memória para a estrutura, alocamos um buffer para armazenar dados. Nada de especial aqui.
Em seguida, inicializamos a fila de processamento de solicitações com uma função blk_mq_init_sq_queue () ou duas de uma vez: blk_mq_alloc_tag_set () + blk_mq_init_queue ().

A propósito, se você olhar o código fonte da função blk_mq_init_sq_queue (), verá que este é apenas um invólucro das funções blk_mq_alloc_tag_set () e blk_mq_init_queue (), que apareceram no kernel 4.20. Além disso, ele oculta muitos parâmetros da fila, mas parece muito mais fácil. Você tem que escolher qual opção é melhor, mas eu prefiro uma opção mais explícita.

A chave neste código é a variável global _mq_ops.

 static struct blk_mq_ops _mq_ops = { .queue_rq = queue_rq, }; 

É aqui que está localizada a função que fornece o processamento de solicitações, mas um pouco mais tarde. O principal é que designamos o ponto de entrada para o manipulador de solicitações.

Agora que criamos a fila, podemos criar uma instância do disco.

Não há grandes mudanças. O disco é alocado, os parâmetros são definidos e o disco é adicionado ao sistema. Eu quero explicar sobre o parâmetro disk-> flags. Ele permite que você informe ao sistema que o disco é removível ou, por exemplo, que ele não contém partições e que você não precisa procurá-las lá.

Existe uma estrutura _fops para gerenciamento de disco.

 static const struct block_device_operations _fops = { .owner = THIS_MODULE, .open = _open, .release = _release, .ioctl = _ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = _compat_ioctl, #endif }; 

Os pontos de entrada _open e _release para nós para um módulo de dispositivo de bloco simples ainda não são muito interessantes. Além do contador atômico e de incremento, não há nada lá. Também deixei compat_ioctl sem implementação, pois a versão dos sistemas com um kernel de 64 bits e um ambiente de espaço do usuário de 32 bits não me parece promissor.

Mas _ioctl permite processar solicitações de sistema para esta unidade. Quando um disco aparece, o sistema tenta aprender mais sobre ele. A seu critério, você pode responder a algumas perguntas (por exemplo, fingir ser um novo CD), mas a regra geral é a seguinte: se você não deseja responder a perguntas que não lhe interessam, basta retornar o código de erro -ENOTTY. A propósito, se necessário, aqui você pode adicionar seus manipuladores de solicitação em relação a essa unidade específica.

Então, adicionamos o dispositivo - precisamos cuidar da liberação de recursos. Ferrugem não está aqui para você.

 static void sblkdev_remove_device(void) { sblkdev_device_t* dev = _sblkdev_device; if (dev){ if (dev->disk) del_gendisk(dev->disk); if (dev->queue) { blk_cleanup_queue(dev->queue); dev->queue = NULL; } if (dev->tag_set.tags) blk_mq_free_tag_set(&dev->tag_set); if (dev->disk) { put_disk(dev->disk); dev->disk = NULL; } sblkdev_free_buffer(dev); kfree(dev); _sblkdev_device = NULL; printk(KERN_WARNING "sblkdev: simple block device was removed\n"); } } 

Em princípio, tudo é óbvio: excluímos o objeto de disco do sistema e liberamos a fila, após o que também liberamos nossos buffers (áreas de dados).

E agora o mais importante é o processamento de consultas na função queue_rq ().

 static blk_status_t queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data* bd) { blk_status_t status = BLK_STS_OK; struct request *rq = bd->rq; blk_mq_start_request(rq); //we cannot use any locks that make the thread sleep { unsigned int nr_bytes = 0; if (do_simple_request(rq, &nr_bytes) != SUCCESS) status = BLK_STS_IOERR; printk(KERN_WARNING "sblkdev: request process %d bytes\n", nr_bytes); #if 0 //simply and can be called from proprietary module blk_mq_end_request(rq, status); #else //can set real processed bytes count if (blk_update_request(rq, status, nr_bytes)) //GPL-only symbol BUG(); __blk_mq_end_request(rq, status); #endif } return BLK_STS_OK;//always return ok } 

Primeiro, considere os parâmetros. O primeiro é struct blk_mq_hw_ctx * hctx - o estado da fila de hardware. No nosso caso, fazemos sem a fila de hardware, portanto não utilizada.

O segundo parâmetro é const struct blk_mq_queue_data * bd - um parâmetro com uma estrutura muito concisa, que não tenho medo de apresentar à sua atenção em sua totalidade:

 struct blk_mq_queue_data { struct request *rq; bool last; }; 

Acontece que, em essência, esse é o mesmo pedido que nos chegou desde tempos em que o cronista elixir.bootlin.com não se lembra mais. Então, pegamos a solicitação e começamos a processá-la, sobre a qual notificamos o kernel chamando blk_mq_start_request (). Após a conclusão do processamento da solicitação, informaremos o kernel sobre isso chamando a função blk_mq_end_request ().

Aqui está uma pequena observação: a função blk_mq_end_request () é essencialmente um invólucro sobre as chamadas para blk_update_request () + __blk_mq_end_request (). Ao usar a função blk_mq_end_request (), você não pode especificar quantos bytes foram realmente processados. Acredita que tudo é processado.

A opção alternativa possui outro recurso: a função blk_update_request é exportada apenas para módulos somente GPL. Ou seja, se você deseja criar um módulo proprietário do kernel (deixe o PM salvá-lo desse caminho espinhoso), não poderá usar blk_update_request (). Então a escolha é sua.

Mudando diretamente os bytes da solicitação para o buffer e vice-versa, coloquei na função do_simple_request ().

 static int do_simple_request(struct request *rq, unsigned int *nr_bytes) { int ret = SUCCESS; struct bio_vec bvec; struct req_iterator iter; sblkdev_device_t *dev = rq->q->queuedata; loff_t pos = blk_rq_pos(rq) << SECTOR_SHIFT; loff_t dev_size = (loff_t)(dev->capacity << SECTOR_SHIFT); printk(KERN_WARNING "sblkdev: request start from sector %ld \n", blk_rq_pos(rq)); rq_for_each_segment(bvec, rq, iter) { unsigned long b_len = bvec.bv_len; void* b_buf = page_address(bvec.bv_page) + bvec.bv_offset; if ((pos + b_len) > dev_size) b_len = (unsigned long)(dev_size - pos); if (rq_data_dir(rq))//WRITE memcpy(dev->data + pos, b_buf, b_len); else//READ memcpy(b_buf, dev->data + pos, b_len); pos += b_len; *nr_bytes += b_len; } return ret; } 

Não há nada de novo: o rq_for_each_segment itera sobre tudo o que é bio, e todos eles têm estruturas de bio_vec, o que nos permite acessar as páginas com os dados da solicitação.

Quais são as suas impressões? Tudo parece simples? O processamento de solicitações em geral é simplesmente copiar dados entre as páginas da solicitação e o buffer interno. Bastante digno de um driver de dispositivo de bloco simples, certo?

Mas há um problema: isso não é para uso real!

A essência do problema é que a função de processamento de solicitações queue_rq () é chamada em um loop que processa solicitações da lista. Não sei qual bloqueio dessa lista é usado lá, Spin ou RCU (não quero mentir - quem sabe, me corrija), mas quando você tenta usar, por exemplo, mutex na função de processamento de solicitações, o kernel de depuração jura e avisa: cochile aqui é impossível. Ou seja, é impossível usar ferramentas de sincronização convencionais ou memória virtual contígua - que é alocada usando vmalloc e pode ser trocada com tudo o que implica - é impossível, pois o processo não pode entrar no estado de espera.

Portanto, apenas bloqueios Spin ou RCU e um buffer na forma de uma matriz de páginas, lista ou árvore, conforme implementado em .. \ linux \ drivers \ block \ brd.c, ou processamento atrasado em outro encadeamento, conforme implementado em .. \ linux \ drivers \ bloco \ loop.c.

Acho que não há necessidade de descrever como montar o módulo, como carregá-lo no sistema e como descarregar. Não há novos produtos nesta frente, e obrigado por isso :) Portanto, se alguém quiser experimentar, certamente descobrirá. Só não faça isso imediatamente no seu laptop favorito! Levante um virtualochka ou, pelo menos, faça um backup em uma bola.

A propósito, o Veeam Backup for Linux 3.0.1.1046 já está disponível. Apenas não tente executar o VAL 3.0.1.1046 em um kernel 5.0 ou posterior. O veeamsnap não será montado. E algumas inovações de várias filas ainda estão em fase de teste.

Source: https://habr.com/ru/post/pt446148/


All Articles