Usando arquivos de sequência do kernel do Linux

Um recurso característico da programação moderna é o uso da rede global como fonte de informações de referência, em particular uma fonte de padrões para resolver problemas desconhecidos ou pouco conhecidos para um programador específico. Essa abordagem economiza muito tempo e geralmente gera resultados bastante qualitativos. No entanto, as soluções apresentadas na rede, embora geralmente corretas, nem sempre levam em consideração todas as sutilezas da solução de um problema, o que leva ao aparecimento no código-fonte das seções que normalmente funcionam corretamente, mas sob circunstâncias não muito comuns. fontes de surpresas desagradáveis.

Considere o tópico do uso de arquivos de sequência no kernel do Linux, esses arquivos são considerados o mecanismo mais conveniente para imprimir no modo kernel. Mas, na prática, usá-los corretamente é muito mais difícil do que você imagina.

Muitos materiais sobre esse tópico estão disponíveis online. O melhor é o código fonte do próprio kernel, que possui comentários bastante detalhados. O problema com essa fonte de informação é seu volume. Se você não sabe exatamente o que procurar, é melhor se você tiver tempo limitado, para não tentar. Para mim, quando me interessei pelo assunto, o Google forneceu várias fontes aparentemente excelentes de informações relacionadas à minha pesquisa: o famoso livro The Linux Kernel Module Programming Guide e uma série de artigos de Rob Day . Essas fontes não são novas, mas são muito sólidas.

Vamos primeiro considerar com mais detalhes quando é natural usar arquivos de sequência. A situação mais comum é criar seu próprio arquivo no sistema de arquivos / proc. Ao ler os arquivos deste sistema, você pode obter uma variedade de informações sobre o equipamento usado, seus drivers, RAM, processos, etc.

Parece que a impressão de qualquer coisa é a tarefa mais simples da programação. Mas, trabalhando no modo kernel, o sistema operacional impõe muitas restrições que podem parecer completamente inimagináveis ​​para o desenvolvedor do aplicativo. No modo kernel, o tamanho do buffer de impressão é limitado pelo tamanho de uma página de memória virtual. Para a arquitetura x86, são quatro kilobytes. Portanto, um bom programa ao imprimir grandes quantidades de dados deve primeiro preencher o buffer ao máximo, depois imprimi-lo e repita essa iteração até que os dados para impressão estejam completamente esgotados. É claro que você pode imprimir caractere por caractere, o que simplificaria bastante tudo, mas estamos falando de bons programas.

As fontes acima mencionadas foram um pouco piores que o esperado. No livro, por exemplo, algumas das informações acabaram sendo geralmente incorretas e foi isso que me motivou a escrever esta nota. É comum considerar que as informações fornecidas na forma de uma imagem de esquema são as mais fáceis de entender e usar. Contudo, neste livro, a imagem relacionada ao assunto está incorreta. O uso desse esquema pode levar a erros graves, embora o exemplo no livro funcione corretamente e siga esse mesmo esquema. Isso se deve ao fato de que, neste exemplo, apenas alguns bytes são impressos por vez quando o / proc / iter é acessado. Se você o usar como modelo para imprimir textos maiores que uma página de memória, haverá surpresas. A série de artigos acima mencionada não contém erros óbvios, mas não relata alguns detalhes importantes para a compreensão do tópico.

Portanto, vamos primeiro considerar o esquema correto de como trabalhar com um arquivo de sequência.



Para trabalhar com esse arquivo, você precisa criar as funções start (), stop (), next () e show (). Os nomes dessas funções podem ser quaisquer, eu escolhi as palavras mais curtas que correspondem em significado às ações das funções. Quando essas funções estão presentes e conectadas adequadamente aos sistemas do kernel, elas começam a funcionar automaticamente ao acessar o arquivo associado a elas no diretório / proc. O mais confuso é o uso da função stop (), que pode ser chamada em três contextos diferentes. Chamar depois de start () significa encerrar o trabalho de impressão. A chamada após show () significa que a última operação de impressão no buffer (geralmente a função seq_printf é usada para isso) excedeu o buffer da página e essa operação de impressão foi cancelada. Sua chamada após next () é o caso mais interessante que ocorre ao imprimir alguns dados no buffer e é necessário concluir o trabalho ou usar novos dados. Por exemplo, suponha que nosso arquivo no diretório / proc, ao acessá-lo, primeiro produz algumas informações sobre dispositivos de bloco e depois sobre os caracteres. Primeiramente, a função start () inicializa a impressão para dispositivos de bloco, e as funções next () e, possivelmente, show () usam esses dados de inicialização para imprimir informações passo a passo sobre os dispositivos de bloco. Quando tudo estiver pronto, após a última chamada para next (), será feita a chamada para parar (), após a qual start () é chamada, que desta vez já deve iniciar uma impressão adicional para dispositivos de caracteres.

Dou um exemplo ligeiramente modificado (o conteúdo do arquivo evens.c) do artigo de Rob Day. Eu tive que substituir a chamada de uma função, que está ausente nos kernels modernos, pelo seu equivalente real. Os comentários também são ligeiramente alterados.

#include <linux/module.h> #include <linux/moduleparam.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/proc_fs.h> #include <linux/fs.h> #include <linux/seq_file.h> #include <linux/slab.h> static int limit = 10; //default value, it can be changed here or module_param(limit, int, S_IRUGO); //transfered as a module parameter static int* even_ptr; //we will work with dynamic memory /** * start */ static void *ct_seq_start(struct seq_file *s, loff_t *pos) { printk(KERN_INFO "Entering start(), pos = %Ld, seq-file pos = %lu.\n", *pos, s->count); if (*pos >= limit) { // are we done? printk(KERN_INFO "Apparently, we're done.\n"); return NULL; } //Allocate an integer to hold our increasing even value even_ptr = kmalloc(sizeof(int), GFP_KERNEL); if (!even_ptr) // fatal kernel allocation error return NULL; printk(KERN_INFO "In start(), even_ptr = %pX.\n", even_ptr); *even_ptr = (*pos)*2; return even_ptr; } /** * show */ static int ct_seq_show(struct seq_file *s, void *v) { printk(KERN_INFO "In show(), even = %d.\n", *(int*)v); seq_printf(s, "The current value of the even number is %d\n", *(int*)v); return 0; } /** * next */ static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos) { printk(KERN_INFO "In next(), v = %pX, pos = %Ld, seq-file pos = %lu.\n", v, *pos, s->count); (*pos)++; //increase my position counter if (*pos >= limit) //are we done? return NULL; *(int*)v += 2; //to the next even value return v; } /** * stop */ static void ct_seq_stop(struct seq_file *s, void *v) { printk(KERN_INFO "Entering stop().\n"); if (v) printk(KERN_INFO "v is %pX.\n", v); else printk(KERN_INFO "v is null.\n"); printk(KERN_INFO "In stop(), even_ptr = %pX.\n", even_ptr); if (even_ptr) { printk(KERN_INFO "Freeing and clearing even_ptr.\n"); kfree(even_ptr); even_ptr = NULL; } else printk(KERN_INFO "even_ptr is already null.\n"); } /** * This structure gathers functions which control the sequential reading */ static struct seq_operations ct_seq_ops = { .start = ct_seq_start, .next = ct_seq_next, .stop = ct_seq_stop, .show = ct_seq_show }; /** * This function is called when a file from /proc is opened */ static int ct_open(struct inode *inode, struct file *file) { return seq_open(file, &ct_seq_ops); }; /** * This structure gathers functions for a /proc-file operations */ static struct file_operations ct_file_ops = { .owner = THIS_MODULE, .open = ct_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release }; /** * This function is called when this module is loaded into the kernel */ static int __init ct_init(void) { proc_create("evens", 0, NULL, &ct_file_ops); return 0; } /** * This function is called when this module is removed from the kernel */ static void __exit ct_exit(void) { remove_proc_entry("evens", NULL); } module_init(ct_init); module_exit(ct_exit); MODULE_LICENSE("GPL"); 


As funções para trabalhar com um arquivo de sequência usam dois ponteiros com funcionalidade sobreposta (isso também é um pouco confuso). Um deles deve apontar para o objeto atual a ser impresso no buffer por show () - é o ponteiro `v 'no programa. O outro ponteiro `pos 'é geralmente usado para apontar para o contador.

Para aqueles que desejam pela primeira vez executar seu programa no modo kernel, dou um exemplo de Makefile para uma compilação bem-sucedida. Obviamente, para uma construção bem-sucedida, você deve ter cabeçalhos de origem do kernel Linux no sistema.

 obj-m += evens.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 


A conexão com o kernel é feita com o comando sudo insmod evens.ko , verificando a funcionalidade do / proc / evens-file, que apareceu depois disso, com o comando cat /proc/evens , lendo o log de eventos que explica as operações do sistema com o comando sudo cat /var/log/messages .

Para estourar o buffer da página, configure o parâmetro limit para um valor maior, por exemplo, 200. Esse valor pode ser inserido no texto do programa ou usado ao carregar o módulo com um comando sudo insmod events.ko limit=200 .

A análise de log pode explicar os pontos obscuros restantes. Por exemplo, você pode notar que, antes de chamar stop () após next () ou start (), o sistema zera a variável `v '. Você também pode observar que, antes de chamar start () após stop (), o sistema imprime o conteúdo do buffer.

Ficaria muito grato se alguém denunciasse imprecisões encontradas em minha nota ou qualquer outra coisa que devesse ser mencionada.

O código fonte também está disponível aqui .

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


All Articles