Funções adiáveis, tasklets de kernel e filas de trabalho

Uma introdução às metades inferiores no Linux 2.6

Para operações encadeadas de alta frequência, o kernel Linux® fornece tasklets e filas de trabalho. As tasklets e as filas de trabalho implementam funcionalidade adiável e substituem o mecanismo antigo da metade inferior dos drivers. Este artigo explora o uso de tasklets e filas de trabalho no kernel e mostra como construir funções adiáveis com essas APIs.

M. Tim Jones, Consultant Engineer, Emulex Corp.

M. Tim JonesM. Tim Jones é arquiteto de firmware integrado e autor das obras Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming (agora, na segunda edição), AI Application Programming (na segunda edição) e BSD Sockets Programming from a Multilanguage Perspective. Sua experiência em engenharia vai desde o desenvolvimento de kernels para espaçonaves geossíncronas até a arquitetura de sistemas integrados e o desenvolvimento de protocolos de rede. Tim é engenheiro consultor da Emulex Corp. em Longmont, Colorado.



30/Mar/2010

Entre em contato com Tim

Tim é um de nossos autores mais conhecidos e produtivos. Procure todos os artigos de Tim no developerWorks. Confira o perfil de Tim e entre em contato com ele, com outros autores e leitores no My developerWorks.

Este artigo explora alguns métodos usados para adiar o processamento entre contextos de kernel (especificamente no kernel Linux 2.6.27.14). Embora esse métodos sejam específicos para o kernel Linux, as ideias por trás deles também são úteis a partir de uma perspectiva de arquitetura. Por exemplo, você pode implementar essas ideias em sistemas embarcadas tradicionais no lugar de um planejador tradicional para planejamento de trabalho.

Antes de mergulharmos nos métodos usados no kernel para adiar funções, no entanto, vamos começar com algumas informações sobre o problema que está sendo resolvido. Quando um sistema operacional é interrompido devido a um evento de hardware (como a presença de um pacote através de um adaptador de rede), o processamento começa em uma interrupção. Normalmente, a interrupção inicia uma quantidade substancial de trabalho. Uma parte desse trabalho é feita no contexto da interrupção e o trabalho é passado para a pilha de software para processamento adicional (consulte a Figura 1).

Figura 1. Processamento da metade superior e da metade inferior
Processamento da metade superior e da metade inferior

A questão é: quanto trabalho deve ser feito no contexto de interrupção? O problema com o contexto de interrupção é que algumas ou todas as interrupções podem ser desativadas durante esse tempo, o que aumenta a latência de manipulação de outros eventos de hardware (e introduz mudanças no comportamento do processamento). Assim, minimizar o trabalho feito na interrupção é desejável, empurrando uma parte do trabalho para o contexto do kernel (em que há uma maior probabilidade de que o processador possa ser compartilhado de forma vantajosa).

Como mostrado na Figura 1, o processamento feito no contexto de interrupção é chamado de metade superior, e o processamento que é empurrado para fora do contexto de interrupção é chamado de metade inferior (em que a metade superior programa o processamento subsequente pela parte inferior). O processamento da metade inferior é realizado no contexto do kernel, o que significa que interrupções estão ativadas. Isso leva a um melhor desempenho devido à habilidade de lidar rapidamente com eventos de interrupção de alta frequência, adiando o trabalho que não é urgente.

Breve histórico das metades inferiores

Versão do kernel Linux

Esta discussão de tasklets e filas de trabalho usa a versão 2.6.27.14 do kernel Linux.

O Linux tende a ser um canivete suíço de funcionalidade, e adiar funcionalidade não é diferente. Desde o kernel 2.3, as softirqs estão disponíveis e implementam um conjunto de 32 metades inferiores definidas estaticamente. Sendo elementos estáticos, eles são definidos no tempo de compilação (diferentemente de mecanismos novos, que são dinâmicos). As softirqs eram usadas para processamento crítico de tempo (interrupções de software) no contexto do encadeamento do kernel. É possível encontrar a origem da funcionalidade softirq em ./kernel/softirq.c. Também há tasklets introduzidas no kernel Linux 2.3 (consulte ./include/linux/interrupt.h). As tasklets são construídas sobre softirqs para permitir a criação dinâmica de funções adiáveis. Finalmente, no kernel Linux 2.5, as filas de trabalho foram introduzidas (consulte ./include/linux/workqueue.h). As filas de trabalho permitem que o trabalho seja adiado fora do contexto de interrupção no contexto do processo do kernel.

Agora vamos explorar os mecanismos dinâmicos para adiamento de trabalho, tasklets e filas de trabalho.


Apresentando as tasklets

As softirqs foram projetadas originalmente como um vetor de 32 entradas softirq suportando uma variedade de comportamentos de interrupção de software. Hoje, apenas nove vetores são usados em softirqs, sendo que um deles é o TASKLET_SOFTIRQ (consulte ./include/linux/interrupt.h). E embora as softirqs ainda existam no kernel, as tasklets e filas de trabalho são recomendadas em vez de alocar novos vetores softirq.

As tasklets são um esquema adiado que você pode programar para que uma função registrada seja executada mais tarde. A metade superior (o manipulador de interrupção) realiza uma pequena parte do trabalho e depois programa a tasklet para ser executada mais tarde na metade inferior.

Listagem 1. Declarando e programando uma tasklet
/* Declare a Tasklet (the Bottom-Half) */
void tasklet_function( unsigned long data );

DECLARE_TASKLET( tasklet_example, tasklet_function, tasklet_data );

...

/* Schedule the Bottom-Half */
tasklet_schedule( &tasklet_example );

Uma dada tasklet será executada em apenas uma CPU (a CPU em que a tasklet foi programada), e a mesma tasklet nunca será executada em mais de uma CPU de um dado processador simultaneamente. Mas tasklets diferentes podem ser executadas em CPUs diferentes ao mesmo tempo.

As tasklets são representadas pela estrutura tasklet_struct (consulte a Figura 2), que inclui os dados necessários para gerenciar e manter a tasklet (estado, ativar/desativar através de um atomic_t, ponteiro de função, dados e referência de lista vinculada).

Figura 2. A parte interna da estrutura tasklet_struct
A parte interna da estrutura tasklet_struct

As tasklets são programadas através do mecanismo softirq, às vezes através do ksoftirqd (um encadeamento de kernel por CPU), quando a máquina está sob forte carga de interrupção de software. A próxima seção explora as várias funções disponíveis na application programming interface (API) das tasklets.

Herança de sistemas embarcados

As ideias por trás das tasklets e filas de trabalho possuem alguma herança dos sistemas embarcados. Em muitos sistemas embarcados, não há um planejador tradicional, somente adiamento de trabalho (dirigido por entrada/saída [E/S] ou processamento interno). Em vez de um planejador, interrupções e aplicativos adiam trabalho como forma de programar o processamento para mais tarde por outros elementos do sistema. Dessa forma, o planejador se torna um processador de filas de trabalho (fornecendo trabalho para funções de manipulador) ou máscaras de bits (que indicam a capacidade de uma tasklet de fazer seu trabalho).

API de tasklets

As tasklets são definidas usando uma macro chamada DECLARE_TASKLET (consulte a Listagem 2). Por debaixo, essa macro fornece simplesmente uma inicialização tasklet_struct das informações que você fornece (nome da tasklet, função e dados específicos da tasklet). Por padrão, a tasklet é ativada, o que significa que ela pode ser programada. Uma tasklet também pode ser declarada com desativada por padrão usando a macro DECLARE_TASKLET_DISABLED. Isso requer que a função tasklet_enable seja invocada para tornar a tasklet programável. É possível ativar e desativar uma tasklet (de uma perspectiva de programação) usando as funções tasklet_enable e tasklet_disable, respectivamente. Também há uma função tasklet_init que inicializa uma tasklet_struct com os dados de tasklet fornecidos pelo usuário.

Listagem 2. Criação de tasklet e funções ativar/desativar
DECLARE_TASKLET( name, func, data );
DECLARE_TASKLET_DISABLED( name, func, data);
void tasklet_init( struct tasklet_struct *, void (*func)(unsigned long),
			unsigned long data );
void tasklet_disable_nosync( struct tasklet_struct * );
void tasklet_disable( struct tasklet_struct * );
void tasklet_enable( struct tasklet_struct * );
void tasklet_hi_enable( struct tasklet_struct * );

Há duas funções de desativação, sendo que cada uma delas solicita uma desativação da tasklet, mas somente a tasklet_disable retorna depois que a tasklet foi interrompida (sendo que a tasklet_disable_nosync pode retornar antes da interrupção). As funções de desativação permitem que a tasklet seja "mascarada" (ou seja, não executada) até que função de ativação seja chamada. Também há duas funções de ativação: uma para programação de prioridade normal (tasklet_enable) e uma para programação de prioridade alta (tasklet_hi_enable). A programação de prioridade normal é realizada através da softirq de nível TASKLET_SOFTIRQ, e a prioridade alta é realizada através da softirq de nível HI_SOFTIRQ.

Como acontece com as funções de ativação de prioridade normal e alta, há funções de programação de prioridade normal e alta (consulte a Listagem 3). Cada função enfileira a tasklet no vetor softirq em particular (tasklet_vec para prioridade normal e tasklet_hi_vec para alta prioridade). As tasklets do vetor de alta prioridade são atendidas primeira, seguidas por aquelas no vetor normal. Observe que cada CPU mantém seus próprios vetores softirq de prioridade normal e alta.

Listagem 3. Funções de programação de tasklet
void tasklet_schedule( struct tasklet_struct * );
void tasklet_hi_schedule( struct tasklet_struct * );

Finalmente, depois que uma tasklet é criada, é possível interrompê-la através das funções tasklet_kill (consulte a Listagem 4). A função tasklet_kill garante que a tasklet não será executada novamente e que, se a tasklet está atualmente programada para ser executada, aguardará sua conclusão e depois a cancelará. A tasklet_kill_immediate é usada somente quando uma dada CPU está no estado inativo.

Listagem 4. Funções de cancelamento de tasklet
void tasklet_kill( struct tasklet_struct * );
void tasklet_kill_immediate( struct tasklet_struct *, unsigned int cpu );

Pela API, é possível ver que a API da tasklet é simples, assim como sua implementação. É possível encontrar a implementação do mecanismo de tasklet em ./kernel/softirq.c e ./include/linux/interrupt.h.

Exemplo de tasklet simples

Vamos dar uma olhada em um uso simples da API de tasklets (consulte a Listagem 5). Como mostrado aqui, uma função de tasklet é criada com dados associados (my_tasklet_function e my_tasklet_data), que são então usados para declarar uma nova tasklet usando DECLARE_TASKLET. Quando o módulo é inserido, a tasklet é programada, o que a torna executável em algum momento futuro. Quando o módulo está descarregado, a função tasklet_kill é chamada para garantir que a tasklet não está em um estado programável.

Listagem 5. Exemplo simples de uma tasklet no contexto de um módulo do kernel
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>

MODULE_LICENSE("GPL");

char my_tasklet_data[]="my_tasklet_function was called";

/* Bottom Half Function */
void my_tasklet_function( unsigned long data )
{
  printk( "%s\n", (char *)data );
  return;
}

DECLARE_TASKLET( my_tasklet, my_tasklet_function, 
		 (unsigned long) &my_tasklet_data );

int init_module( void )
{
  /* Schedule the Bottom Half */
  tasklet_schedule( &my_tasklet );

  return 0;
}

void cleanup_module( void )
{
  /* Stop the tasklet before we exit */
  tasklet_kill( &my_tasklet );

  return;
}

Apresentando as filas de trabalho

As filas de trabalho são um mecanismo de adiamento mais recente, adicionado na versão do kernel Linux 2.5. Em vez de fornecer um esquema de adiamento de uma tentativa como é o caso das tasklets, as filas de trabalho são um mecanismo genérico de adiamento no qual a função de manipulador para a fila de trabalho pode dormir (o que não é possível no modelo tasklet). As filas de trabalho podem ter uma maior latência que as tasklets mas incluem uma API mais rica para adiamento de trabalho. O adiamento costumava ser gerenciado por filas de tarefa através de keventd, mas agora é gerenciado por encadeamentos do trabalhador do kernel chamado events/X.

As filas de trabalho fornecem um método genérico para adiar funcionalidade para as metades inferiores. No centro está a fila de trabalho (struct workqueue_struct), que é a estrutura na qual o trabalho é colocado. O trabalho é representado por uma estrutura work_struct, que define o trabalho a ser adiado e a função de adiamento a ser usada (consulte a Figura 3). Os encadeamentos de kernel events/X (um por CPU) extraem trabalho da fila de trabalho e ativam um dos manipuladores da metade inferior (como indicado pela função de manipulador em struct work_struct).

Figura 3. O processo por trás das filas de trabalho
O processo por trás das filas de trabalho

Como a work_struct indica a função de manipulador a ser usada, é possível usar a fila de trabalho para enfileirar trabalho para uma variedade de manipuladores. Agora, vamos dar uma olhada nas funções de API que podem ser encontradas para filas de trabalho.

API da fila de trabalho

A API da fila de trabalho é um pouco mais complicada que as tasklets, principalmente porque várias funções são suportadas. Primeiro vamos explorar as filas de trabalho e depois olharemos o trabalho e as variantes.

Lembre-se de que na Figura 3 a estrutura principal da fila de trabalho é a própria fila. Essa estrutura é usada para enfileirar trabalho da metade superior para ser adiado para execução posterior pela metade inferior. As filas de trabalho são criadas através de uma macro chamada create_workqueue, que retorna uma referência workqueue_struct. É possível controlar remotamente essa fila de trabalho mais tarde (se necessário) através de uma chamada para a função destroy_workqueue:

struct workqueue_struct *create_workqueue( name );
void destroy_workqueue( struct workqueue_struct * );

O trabalho a ser comunicado através da fila de trabalho é definido pela estrutura work_struct. Normalmente, essa estrutura é o primeiro elemento de uma estrutura do usuário de definição de trabalho (você verá um exemplo disso mais adiante). A API da fila de trabalha fornece três funções para inicializar trabalho (de um buffer alocado); consulte a Listagem 6. A INIT_WORK fornece a inicialização necessária e configuração da função de manipulador (passada pelo usuário). Nos casos em que o desenvolvedor precisa de um atraso antes que o trabalho seja enfileirado na fila de trabalho, é possível usar as macros INIT_DELAYED_WORK e INIT_DELAYED_WORK_DEFERRABLE.

Listagem 6. Macros de inicialização de trabalho
INIT_WORK( work, func );
INIT_DELAYED_WORK( work, func );
INIT_DELAYED_WORK_DEFERRABLE( work, func );

Com a estrutura de trabalho inicializada, a próxima etapa é enfileirar o trabalho em uma fila de trabalho. É possível fazer isso de algumas formas (consulte a Listagem 7). Primeiro, simplesmente enfileire o trabalho em uma fila de trabalho usando queue_work (que liga o trabalho à CPU atual). Ou você pode especificar a CPU na qual o manipulador deve ser executado usando queue_work_on. Duas funções adicionais fornecem a mesma funcionalidade para trabalho adiado (cuja estrutura encapsula a estrutura work_struct e um cronômetro de adiamento de trabalho).

Listagem 7. Funções da fila de trabalho
int queue_work( struct workqueue_struct *wq, struct work_struct *work );
int queue_work_on( int cpu, struct workqueue_struct *wq, struct work_struct *work );

int queue_delayed_work( struct workqueue_struct *wq,
			struct delayed_work *dwork, unsigned long delay );

int queue_delayed_work_on( int cpu, struct workqueue_struct *wq,
			struct delayed_work *dwork, unsigned long delay );

Você pode usar uma fila de trabalho de kernel global, com quatro funções que lidam com essa fila de trabalho. Essas funções (mostradas na Listagem 8) imitam as da Listagem 7, exceto que você não precisa definir a estrutura da fila de trabalho.

Listagem 8. Funções de fila de trabalho de kernel global
int schedule_work( struct work_struct *work );
int schedule_work_on( int cpu, struct work_struct *work );

int scheduled_delayed_work( struct delayed_work *dwork, unsigned long delay );
int scheduled_delayed_work_on( 
		int cpu, struct delayed_work *dwork, unsigned long delay );

Também há uma série de funções de ajuda que você pode usar para descarregar ou cancelar trabalho nas filas de trabalho. Para descarregar um item de trabalho em particular e bloqueá-lo até que o trabalho esteja completo, você pode fazer uma chamada para flush_work. Todo o trabalho em uma dada fila de trabalho pode ser concluído usando uma chamada para flush_workqueue. Em ambos os casos, o responsável pela chamada é bloqueado até que a operação seja concluída. Para descarregar a fila de trabalho do kernel global, chame flush_scheduled_work.

int flush_work( struct work_struct *work );
int flush_workqueue( struct workqueue_struct *wq );
void flush_scheduled_work( void );

Você pode cancelar trabalho se ele ainda não estiver sendo executado em um manipulador. Uma chamada cancel_work_sync cancelará o trabalho na fila ou o bloqueará até que o retorno de chamada tenha terminado (se o trabalho já estiver em andamento no manipulador). Se o trabalho for adiado, você pode usar uma chamada para cancel_delayed_work_sync.

int cancel_work_sync( struct work_struct *work );
int cancel_delayed_work_sync( struct delayed_work *dwork );

Finalmente, você pode descobrir se um item de trabalho está pendente (ainda não executado pelo manipulador) com uma chamada para work_pending ou delayed_work_pending.

work_pending( work );
delayed_work_pending( work );

Esse é o centro da API de fila de trabalho. É possível encontrar a implementação da API de fila de trabalho em ./kernel/workqueue.c, com definições de API em ./include/linux/workqueue.h. Agora vamos continuar com um exemplo simples de API de fila de trabalho.

Exemplo de fila de trabalho simples

O exemplo a seguir ilustra algumas das principais funções da API da fila de trabalho. Assim como aconteceu com o exemplo das tasklets, implemente este exemplo no contexto de um módulo do kernel para simplificar.

Primeiro, olhe sua estrutura de trabalho e a função de manipulador que usará para implementar a metade inferior (consulte a Listagem 9). A primeira coisa que você observará aqui é uma definição da sua referência de estrutura de fila de trabalho (my_wq) e a definição my_work_t. A typedef my_work_t inclui a estrutura work_struct no cabeçalho e um número inteiro que representa seu item de trabalho. Seu manipulador (uma função de retorno de chamada) dereferencia o ponteiro work_struct de volta ao tipo my_work_t. Depois de emitir o item de trabalho (número inteiro da estrutura), o ponteiro de trabalho é liberado.

Listagem 9. Estrutura de trabalho e manipulador da metade inferior
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/workqueue.h>

MODULE_LICENSE("GPL");

static struct workqueue_struct *my_wq;

typedef struct {
  struct work_struct my_work;
  int    x;
} my_work_t;

my_work_t *work, *work2;


static void my_wq_function( struct work_struct *work)
{
  my_work_t *my_work = (my_work_t *)work;

  printk( "my_work.x %d\n", my_work->x );

  kfree( (void *)work );

  return;
}

A Listagem 10 é a sua função init_module, que começa com a criação da fila de trabalho usando a função da API create_workqueue. Mediante a criação bem-sucedida da fila de trabalho, crie dois itens de trabalho (alocados através de kmalloc). Cada item de trabalho é então inicializado com INIT_WORK, o trabalho é definido e depois enfileirado na fila de trabalho com uma chamada para queue_work. O processo da metade superior (simulado aqui) agora está concluído. O trabalho será então, em algum momento posterior, processado pelo manipulador, como mostrado na Listagem 10.

Listagem 10. Fila de trabalho e criação de trabalho
int init_module( void )
{
  int ret;

  my_wq = create_workqueue("my_queue");
  if (my_wq) {

    /* Queue some work (item 1) */
    work = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);
    if (work) {

      INIT_WORK( (struct work_struct *)work, my_wq_function );

      work->x = 1;

      ret = queue_work( my_wq, (struct work_struct *)work );

    }

    /* Queue some additional work (item 2) */
    work2 = (my_work_t *)kmalloc(sizeof(my_work_t), GFP_KERNEL);
    if (work2) {

      INIT_WORK( (struct work_struct *)work2, my_wq_function );

      work2->x = 2;

      ret = queue_work( my_wq, (struct work_struct *)work2 );

    }

  }

  return 0;
}

Os elementos finais são mostrados na Listagem 11. Aqui, na limpeza do módulo, descarregue a fila de trabalho em particular (que é bloqueada até que o manipulador tenha concluído o processamento do trabalho), e depois destrua a fila de trabalho.

Listagem 11. Descarregamento e destruição da fila de trabalho
void cleanup_module( void )
{
  flush_workqueue( my_wq );

  destroy_workqueue( my_wq );

  return;
}

Diferenças entre tasklets e filas de trabalho

A partir desta breve introdução às tasklets e filas de trabalho, você pode ver dois esquemas diferentes de adiamento de trabalho das metades superior para as metades inferiores. As tasklets fornecem um mecanismo de baixa latência que é simples e objetivo, enquanto as filas de trabalho fornecem uma API flexível que permite o enfileiramento de múltiplos itens de trabalho. Cada um adia trabalho a partir do contexto de interrupção, mas somente as tasklets são executadas automaticamente de uma forma completa, de modo que as filas de trabalho permitam que os manipuladores durmam, se necessário. Qualquer método é útil para adiamento de trabalho, então o método é selecionado com base em suas necessidades particulares.


Indo além

Os métodos de adiamento de trabalho explorados aqui representam os métodos históricos e atuais usando no kernel Linux (excluindo os cronômetros, que serão abordados em um artigo futuro). Certamente eles não são novos—de fato, eles existiram de outras formas no passado—mas representam um padrão de arquitetura interessante que é útil no Linux e em outro lugar. Das softirqs às tasklets, filas de trabalho e filas de trabalho atrasadas, o Linux continua a evoluir em todos as áreas do kernel enquanto fornece uma experiência de espaço do usuário consistente e compatível.

Recursos

Aprender

Obter produtos e tecnologias

  • Avalie os produtos da IBM da forma que melhor lhe convém: Faça o download de uma versão de teste de produto, experimente um produto on-line, use um produto em um ambiente de nuvem ou passe algumas horas no SOA Sandbox aprendendo como implementar Arquitetura Orientada a Serviço de forma eficiente.

Discutir

  • Envolva-se na comunidade My developerWorks. Entre em contato com outros usuários do developerWorks enquanto explora os blogs, fóruns, grupos e wikis dos desenvolvedores.

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Linux
ArticleID=478578
ArticleTitle=Funções adiáveis, tasklets de kernel e filas de trabalho
publish-date=03302010