Linux Device Drivers – Trabalhando com Kernel Threads

- por Sergio Prado

Categorias: Linux Tags: , ,

Já faz um tempo que não escrevo sobre desenvolvimento de device drivers em Linux. Na parte 2 desta série desenvolvemos um device driver completo para acessar os leds do kit FriendlyARM mini2440. Que tal estudar um pouco mais sobre a API do kernel do Linux?

Neste artigo vamos aprender a trabalhar com threads em kernel space, e para exemplificar os conceitos, vamos fazer os leds do kit mini2440 piscarem.

THREADS?

Você já sabe que o Linux é um sistema operacional multi-tarefa, e que ele consegue executar mais de uma tarefa ao mesmo tempo. É claro que em sistemas com apenas 1 núcleo, apenas uma tarefa tem a atenção da CPU em determinado momento, mas temos a impressão de que várias coisas estão acontecendo simultaneamente. Isso ocorre devido ao fato do Linux ser um sistema operacional preemptivo. O escalonador aloca uma fatia do tempo disponível para cada tarefa em execução.

Se você trabalha a um tempo desenvolvendo aplicações para Linux, sabe que é possível ter mais de uma linha de execução em um processo. É o que chamamos de threads. E o conceito é simples: “dividir para conquistar”. Cada thread tem uma responsabilidade específica, e você acaba otimizando o uso dos recursos da máquina, além de estruturar melhor o código e facilitar a manutenção.

O que você talvez não saiba é que em kernel space também podemos ter mais de uma thread de execução.

KERNEL THREADS?

Kernel threads são threads dentro do kernel. Você pode por exemplo ter tarefas executando em background em um device driver, esperando por eventos assíncronos ou executando determinada atividade de tempos em tempos.

É fácil identificar as threads do kernel rodando no seu sistema. Ao listar os processos em execução com o comando “ps”, os nomes entre “[]” são kernel threads. Veja um trecho da saída deste comando na minha máquina de desenvolvimento:

$ ps auxf
PID   USER     COMMAND
    1 root     init
    2 root     [kthreadd]
    3 root     [ksoftirqd/0]
    4 root     [events/0]
    5 root     [khelper]
   81 root     [kblockd/0]
   92 root     [khubd]
...

Da listagem acima, com exceção do processo “init”, o restante dos processos são kernel threads.

Mas quando você vai precisar de kernel threads? Em algumas situações, na verdade. Se você precisa esperar por eventos assíncronos, é bem provável que a solução seja criar uma thread no kernel para aguardar por este eventos. Se você tem uma tarefa dentro de um device driver que leva bastante tempo para ser executada, você pode delegá-la para uma kernel thread. Se você precisa executar determinada tarefa periodicamente, pode também usar uma thread no kernel para fazer isso.

Mas como isso funciona na prática? É o que veremos agora!

NOSSO EXEMPLO

Mais uma vez, nosso exemplo será executado no kit mini2440. Neste artigo aqui desenvolvemos um device driver para acender e apagar os leds deste kit. Agora iremos extender as funcionalidades deste device driver para fazer os leds piscarem. Da mesma forma que “0” apaga e “1” acende, enviar “2” ao led irá colocá-lo em um estado de “blink“, fazendo-o piscar com uma frequência de 1 segundo.

Existem duas formas de trabalhar com threads no kernel: a antiga e ultrapassada kernel_thread() e a atual e melhor estruturada kthread. Vamos ver exemplos com as duas.

Iremos criar uma thread que a cada 1 segundo irá verificar se algum dos leds esta configurado para piscar, e então inverteremos seu estado. Simples, não? Então mãos à obra!

A ANTIGA INTERFACE KERNEL_THREADS

Este é o código responsável pelo gerenciamento da thread usando a antiga interface kernel_thread():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#define BLINK_TIME  1000
 
/* flag to enable thread */
unsigned int thread_enabled = 1;
struct completion leds_blink_exit;
 
/* kernel thread to blink the leds */
static int leds_blink(void *unused)
{
    int i;
 
    daemonize("leds_blink");
 
    while (thread_enabled) {
 
        for (i = 0; i < NUM_LEDS; i++) {
            if (leds_dev[i].status == LED_BLINK) {
                changeLedStatus(leds_dev[i].number, LED_BLINK);
            }
        }
 
        msleep(BLINK_TIME);
    }
 
    complete_and_exit(&leds_blink_exit, 0);
}
 
/* driver initialization */
int __init leds_init(void)
{
    ...
 
    /* init completion struct */
    init_completion(&leds_blink_exit);
 
    /* initialize leds_blink therad */
    kernel_thread(leds_blink, NULL, CLONE_FS | CLONE_FILES |
                  CLONE_SIGHAND | SIGCHLD);
 
    ....
}
 
/* driver exit */
void __exit leds_exit(void)
{
    ...
 
    /* stop thread */
    thread_enabled = 0;
    wait_for_completion(&leds_blink_exit);
 
    ...
}

A thread é criada na linha 37 com a função kernel_thread(). Esta função recebe basicamente um ponteiro para a função da thread e alguns parâmetros que indicam quais recursos serão compartilhados com o processo pai.

A função da thread leds_blink() na linha 8 é bem simples. Ela é basicamente um loop infinito que periodicamente faz piscar os leds com o estado configurado como LED_BLINK.

A complexidade aqui esta no controle de finalização da thread. Se removermos o módulo do device driver sem aguardar a finalização da thread, esta ficará no sistema com o status de zombie. Precisamos controlar manualmente este processo, e fazemos isso através da flag thread_enabled. Ao finalizar a execução do módulo, setamos esta flag na linha 49 e aguardamos a finalização da execução da thread na linha 50.

Para testar, basta enviar “2” ao arquivo de dispositivo do led que se deseja piscar. Por exemplo, para piscar o led 3:

$ echo 2 > /dev/leds3

A interface kernel_thread é antiga, propensa a bugs, e não deve ser usada em novos projetos. A frase abaixo é da própria documentação do kernel, disponível em “documentation/feature-removal-schedule.txt“:

“kernel_thread is a low-level implementation detail. Drivers should use the <linux/kthread.h> API instead which shields them from implementation details and provides a higherlevel interface that prevents bugs and code duplication”.

É por isso que você deve esquecer tudo que leu acima… brincadeira! É bem capaz que você encontre um ou outro device driver usando esta API, por isso é bom conhecê-la. Mas você verá que com a interface kthread as coisas são muito mais fáceis.

A INTERFACE KTHREAD

Os conceitos são os mesmos. A interface kthread na verdade utiliza a função kernel_thread internamente, mas abstraindo alguns conceitos, e tornando-a mais simples e segura de usar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define BLINK_TIME  1000
 
/* kernel thread to blink the leds */
static int leds_blink(void *unused)
{
    int i;
 
    while (!kthread_should_stop()) {
 
        for (i = 0; i < NUM_LEDS; i++) {
            if (leds_dev[i].status == LED_BLINK) {
                changeLedStatus(leds_dev[i].number, LED_BLINK);
            }
        }
 
        msleep(BLINK_TIME);
    }
 
    return(0);
}
 
/* driver initialization */
int __init leds_init(void)
{
    ...
 
    /* initialize leds_blink therad */
    leds_blink_task = kthread_run(leds_blink, NULL, "%s", "leds_blink");
 
    ....
}
 
/* driver exit */
void __exit leds_exit(void)
{
    ...
 
    /* stop thread */
    kthread_stop(leds_blink_task);
 
    ...
}

Veja como ficou simples. Usamos apenas 3 funções: kthread_run() para iniciar a thread (linha 28), kthread_should_stop() para que a thread saiba quando deve finalizar sua execução (linha 8) e kthread_stop() para finalizar a thread (linha 39).

Vocês viram como é fácil usar threads no kernel do Linux. Mas devemos ter sempre muito cuidado com o que fazemos em kernel space. Dentro do kernel, temos acesso completo a todos os recursos da máquina. E com o poder vem a responsabilidade. Portanto, use este recurso com sabedoria.

Um abraço,

Sergio Prado

Faça um Comentário

Navegue
Creative Commons Este trabalho de Sergio Prado é licenciado pelo
Creative Commons BY-NC-SA 3.0.