Escolar Documentos
Profissional Documentos
Cultura Documentos
Por exemplo:
Essa função usa o idr apontado pelo idp para alocar um novo UID e associá-lo ao
ponteiro ptr. Em caso de sucesso, a função retorna zero e armazena o novo UID no id.
Em caso de erro, retorna um código de erro diferente de zero: -EAGAIN se você precisar
(novamente) chamar
idr_pre_get() e -ENOSPC se o idr estiver cheio.
www.it-ebooks.info
102 Capítulo 6 Estruturas de dados do kernel
Isto funciona da mesma forma que idr_get_new(), exceto que o novo UID é garantido ser
igual ou maior que starting_id. O uso dessa variante da função permite que os usuários idr
assegurem que um UID nunca seja reutilizado, permitindo que o valor seja exclusivo não
apenas entre os IDs atualmente alocados, mas em toda a vida útil de um sistema.Este
trecho de código é o mesmo do nosso exemplo anterior, exceto que solicitamos o aumento
estrito dos valores de UID:
Procurando um UID
Quando alocamos alguns números de UIDs em um idr, podemos pesquisá-los:O
chamador fornece o UID, e o idr retorna o ponteiro associado.Isso é feito, de uma
maneira muito mais simples do que alocar um novo UID, com a função idr_find():
O uso é simples:
Removendo um UID
Para remover um UID de um idr, use idr_remove():
Uma chamada bem sucedida para idr_remove() remove o id do UID do idr apontado por
idp.
Infelizmente, idr_remove() não tem como significar erro (por exemplo, se id não está
em id).
Destruindo um idr
Destruir um idr é uma operação simples, realizada com a função idr_destroy():
Uma chamada bem sucedida para idr_destroy() desaloca somente a memória não
utilizada associada com o idr apontado por idp. Ele não libera nenhuma memória
atualmente em uso pelos UIDs alocados. Geralmente, o código do kernel não
destruiria seu recurso idr até que fosse desligado ou descarregado, e ele não seria
descarregado até que não tivesse mais usuários (e, portanto, não tivesse mais
UIDs), mas para forçar a remoção de todos os UIDs, você pode chamar
idr_remove_all():
Árvores Binárias
Uma árvore é uma estrutura de dados que fornece uma estrutura hierárquica de dados
em forma de árvore. Matematicamente, é um grafo acíclico, conectado e direcionado em
que cada vértice (chamado de nó) tem zero ou mais arestas de saída e zero ou uma
aresta de entrada. Uma árvore binária é uma árvore em que os nós têm no máximo duas
arestas de saída - isto é, uma árvore em que os nós têm zero, um ou dois filhos. Ver
figura 6.6 para um exemplo de árvore binária.
www.it-ebooks.info
104 Capítulo 6 Estruturas de dados do kernel
7. º
4. 5.
9. 5.
Figura 6.6 Um
n A subárvore esquerda da raiz contém apenas nós com valores menores que a
raiz.
n A subárvore direita da raiz contém somente nós com valores maiores que a raiz.
7. º
3.
1 5.
4. 6.
15
11.
7. º 18º
3. 9. 16º 42º
8 10.
Árvores Vermelho-Pretas
Uma árvore vermelho-preta é um tipo de árvore binária de busca
autobalanceada. A principal estrutura de dados da árvore binária do Linux é
a árvore vermelho-preta. Árvores vermelho-preto têm um atributo de cor
especial, que é vermelho ou preto. As árvores rubro-negras permanecem
semiequilibradas, impondo que as seis propriedades a seguir permaneçam
verdadeiras:
1. Todos os nós são vermelhos ou pretos.
2. Os nós de folha são pretos.
3. Os nós folha não contêm dados.
4. Todos os nós não-folha têm dois filhos.
5. Se um nó for vermelho, ambos os filhos serão pretos.
6. O caminho de um nó para uma de suas folhas contém o mesmo número de
nós pretos que o caminho mais curto para qualquer uma de suas outras folhas.
Juntas, essas propriedades garantem que a folha mais profunda tenha uma
profundidade não superior ao dobro da da folha mais rasa. Consequentemente, a árvore é
sempre semiequilibrada. A razão pela qual isto é verdade é surpreendentemente simples.
Primeiro, por propriedade cinco, um nó vermelho não pode ser filho ou pai de outro nó
vermelho. Pela propriedade seis, todos os caminhos pela árvore até suas folhas têm o
mesmo número de nós pretos. O caminho mais longo pela árvore alterna os nós vermelhos
e pretos. Assim, o caminho mais curto, que deve ter o mesmo número de nós pretos,
contém apenas nós pretos. Portanto, o caminho mais longo da raiz até uma folha não é
mais do que o dobro do caminho mais curto da raiz para qualquer outra folha.
www.it-ebooks.info
106 Capítulo 6 Estruturas de dados do kernel
Árvores
A implementação Linux de árvores rubro-negras é chamada de
rbtrees.They são definidas em lib/rbtree.c e declaradas em
<linux/rbtree.h>.Além de otimizações, as árvores rubro-negras do Linux se
assemelham à árvore rubro-negra "clássica", conforme descrito na seção anterior.Elas
permanecem equilibradas de modo que as inserções são sempre logarítmicas em relação ao
número de nós na árvore.
A raiz de um rbtree é representada pela estrutura rb_root.Para criar uma
nova árvore, alocamos um novo rb_root e o inicializamos para o valor
especialRb_ROOT:
www.it-ebooks.info
108 Capítulo 6 Estruturas de dados do kernel
Assim como em nossa função de busca, o loop while está iterando sobre a árvore,
movendo-se na direção do deslocamento fornecido. Ao contrário de search, no
entanto, a função não espera encontrar um offset correspondente, mas, em vez disso,
alcançar o nó folha que é o ponto de inserção correto para o novo offset.When o ponto
de inserção é encontrado, rb_link_node() rb_insert_color()
é chamado para
www.it-ebooks.info
Complexidade algorítmica
Complexidade algorítmica
Muitas vezes, em ciência da computação e disciplinas relacionadas, é útil expressar
a complexidade algorítmica — ou escalabilidade — dos algoritmos
quantitativamente.Existem vários métodos para reenviar a escalabilidade. Uma
técnica comum é estudar o comportamento assintótico do algo-ritmo. Este é o
comportamento do algoritmo porque suas entradas crescem excessivamente
grandes e se aproximam do infinito. O comportamento assintótico mostra quão bem
um algoritmo é dimensionado à medida que sua entrada cresce cada vez mais.
Estudar a escalabilidade de um algoritmo — como ele se comporta à medida que o
tamanho de sua entrada aumenta — permite modelar o algoritmo com base em um
benchmark e entender melhor seu comportamento.
Algoritmos
Um algoritmo é uma série de instruções, possivelmente uma ou mais entradas,
e, finalmente, um resultado ou saída. Por exemplo, os passos realizados para
contar o número de pessoas em uma sala são um algoritmo, com as pessoas
sendo a entrada e a contagem sendo a saída. No kernel do Linux, tanto a
remoção de página quanto o agendador de processo são exemplos de
algoritmos. Matematicamente, um algoritmo é como uma função. (Ou, pelo
menos, você pode modelá-lo como um.) Por exemplo, se você chamar o
algoritmo de contagem de pessoas f e o número de pessoas para contar x,
poderá escrever
Notação Big-O
Uma notação assintótica útil é o limite superior, que é uma função cujo valor, após um ponto
inicial, é sempre maior do que o valor da função que você está estudando. Diz-se que o
limite superior cresce tão rápido ou mais rápido do que a função em questão.Uma notação
especial, notação big-o (pronuncia-se big oh), é usada para descrever esse crescimento.
Está escrito f(x) é O(g(x)) e é lido como f é big-oh de g.A definição matemática formal é
Se f(x) é O(g(x)), então
c, x' tal que f(x) c g(x), x >x'
Então, você pode dizer que f(x) é da ordem g(x).A ordem, ou big-theta, de um algoritmo
é uma das mais importantes ferramentas matemáticas para entender algoritmos no
kernel.
Consequentemente, quando as pessoas se referem à notação big-o,
elas estão mais frequentemente falando sobre o menos tão grande-o, o
big-theta.You realmente não tem que se preocupar com isso, a menos
que você queira fazer Professor Knuth realmente feliz.
Complexidade de tempo
Considere o exemplo original de ter que contar o número de pessoas em uma sala.
Pré-tende você pode contar uma pessoa por segundo. Então, se houver 7 pessoas
na sala, levará 7 segundos para contá-las. De forma mais geral, considerando n
pessoas, será necessário n segundos para contar todo mundo.Assim, você pode
dizer que este algoritmo é O(n). Porque levaria a mesma quantidade de tempo para
dançar se havia 5 ou 5.000 pessoas na sala, essa tarefa é O(1). Ver quadro 6.1 para
outras complexidades comuns.
n Linear
n2 Quadrático
n3. Cúbico
2n Exponencial
Não! Fatorial
2 Se você estiver curioso, o limite inferior é modelado pela notação megaomega. A definição é a mesma que
big-
o, exceto é sempre menor ou igual a , não maior ou igual a. A notação megaomega é
menos útil do que a notação big-o porque encontrar funções menores do que sua função
raramente indica comportamento.
www.it-ebooks.info
Conclusão 111
Conclusão
Neste capítulo, discutimos muitas das estruturas de dados genéricos que os
desenvolvedores do kernel do Linux usam para implementar tudo, desde o
agendador de processo até os drivers de dispositivo.Você achará essas
estruturas de dados úteis à medida que continuamos nosso estudo do
kernel do Linux.Ao escrever seu próprio código do kernel, sempre reutilize a
infraestrutura do kernel existente e não reinvente a roda.
Também abordamos a complexidade algorítmica e ferramentas para medi-
la e expressá-la, sendo a notação mais notável a notação big-o.Ao longo
deste livro e do kernel do Linux, a notação big-o é uma noção importante de
como os algoritmos e os componentes do kernel são dimensionados à luz
de muitos usuários, processos, processadores, conexões de rede e outras
entradas em constante expansão.
www.it-ebooks.info
Esta página foi deixada em branco intencionalmente
www.it-ebooks.info
7. º
Interrupções e
Manipuladores de
interrupção
Interrupções
As interrupções permitem que o hardware sinalize para o processador. Por exemplo, à
medida que você digita, o controlador do teclado (o dispositivo de hardware que gerencia o
teclado) emite um sinal elétrico ao processador para alertar o sistema operacional sobre as
novas teclas pressionadas.Esses sinais elétricos são interrupções.O processador recebe a
interrupção e sinaliza ao sistema operacional para permitir que o sistema operacional
responda aos novos dados. Os dispositivos de hardware geram interrupções de forma
assíncrona em relação ao relógio do processador — elas podem ocorrer a qualquer
momento. Consequentemente, o kernel pode ser interrompido a qualquer momento para
processar interrupções.
Uma interrupção é produzida fisicamente por sinais eletrônicos provenientes de
dispositivos de hardware e direcionados para pinos de entrada em um controlador de
interrupção, um chip simples que
www.it-ebooks.info
114 Capítulo 7 Interrupções e manipuladores de interrupção
Exceções
Nos textos do SO, as exceções são frequentemente discutidas ao mesmo tempo que as
interrupções. Diferentemente das interrupções, as exceções ocorrem de forma síncrona
em relação ao relógio do processador. De fato, eles são muitas vezes chamados de
interrupções síncronas. As exceções são produzidas pelo processador durante a
execução de instruções, seja em resposta a um erro de programação (por exemplo,
dividir por zero) ou condições anormais que devem ser manipuladas pelo kernel (por
exemplo, uma falha de página). Como muitas arquiteturas de processador lidam com
exceções de maneira similar às interrupções, a infraestrutura do kernel para lidar com as
duas é similar. Grande parte da discussão sobre interrupções (interrupções assíncronas
geradas pelo hardware) neste capítulo também se refere a exceções (interrupções
síncronas geradas pelo processador).
Você já está familiarizado com uma exceção: no capítulo anterior, você viu como as
chamadas do sistema na arquitetura x86 são implementadas pela emissão de uma
interrupção de software, que trapping no kernel e causa a execução de um manipulador de
chamada de sistema especial. As interrupções funcionam de maneira semelhante, você verá,
exceto problemas de hardware — e não de software — interrupções.
Manipuladores de interrupção
A função que o kernel executa em resposta a uma interrupção específica é chamada de
manipulador de interrupção ou rotina de serviço de interrupção (ISR). Cada dispositivo que
gera interrupções tem um manipulador de interrupções associado. Por exemplo, uma função
manipula interrupções do temporizador do sistema, enquanto outra função manipula
interrupções geradas pelo teclado.O manipulador de interrupções de um dispositivo faz parte
do driver do dispositivo — o código do kernel que gerencia o dispositivo.
No Linux, os manipuladores de interrupção são funções C normais. Eles correspondem a
um protótipo específico, o que permite que o kernel transmita as informações do
manipulador de uma maneira padrão, mas caso contrário
www.it-ebooks.info
Metades Superiores Versus Metades Inferiores
www.it-ebooks.info
116 Capítulo 7 Interrupções e manipuladores de interrupção
retorne o controle do sistema ao código que foi interrompido quando a interrupção foi
gerada.O restante do processamento e manuseio dos pacotes ocorre posteriormente, na
metade bot-tom. Neste capítulo, olhamos para a metade superior; no próximo capítulo,
estudamos a parte inferior.
www.it-ebooks.info
117ºRegistrando um manipulador de
interrupção
Exemplo de interrupção
Em um driver, solicitar uma linha de interrupção e instalar um manipulador é feito
via
request_irq():
www.it-ebooks.info
118 Capítulo 7 Interrupções e manipuladores de interrupção
www.it-ebooks.info
Escrevendo um manipulador de interrup
Manipuladores Compartilhados
Um manipulador compartilhado é registrado e executado de forma muito
semelhante a um manipulador não compartilhado. Estas são três
diferenças principais:
n O sinalizador IRQF_SHARED deve ser definido no argumento flags como_irq().
n O argumento dev deve ser exclusivo para cada manipulador registrado. Um ponteiro para
qualquer estrutura por dispositivo é suficiente; uma escolha comum é a estrutura do dispositivo
como ela é
www.it-ebooks.info
120 Capítulo 7 Interrupções e manipuladores de interrupção
www.it-ebooks.info
Escrevendo um manipulador de
interrupção 121
Finalmente, esta função retorna IRQ_HANDLED para significar que ele manipulou
corretamente este dispositivo. Como o manipulador de interrupção não suporta
compartilhamento e não há nenhum mecanismo para que o RTC detecte uma
interrupção artificial, esse manipulador sempre retorna IRQ_HANDLED.
Contexto de interrupção
Ao executar um manipulador de interrupção, o kernel está no contexto de
interrupção. Lembre-se de que o contexto do processo é o modo de operação
em que o kernel está sendo executado em nome de um processo, por exemplo,
executando uma chamada do sistema ou um thread do kernel. No contexto do
processo, a macro atual aponta para a tarefa associada. Além disso, como um
processo é acoplado ao kernel no contexto do processo, o contexto do
processo pode ser suspenso ou, de outra forma, chamar o agendador.
O contexto de interrupção, por outro lado, não está associado a um
processo.A macro atual não é relevante (embora aponte para o processo
interrompido).Sem um processo de backup, o contexto de interrupção não
pode ser suspenso — como ele reagendaria? Portanto, não é possível
chamar certas funções do contexto de interrupção. Se uma função for
suspensa, você não poderá usá-la a partir do manipulador de interrupção —
isso limita as funções que podem ser chamadas a partir de um manipulador
de interrupção.
O contexto de interrupção é crítico em termos de tempo porque o manipulador de
interrupção interrompe outro código. O código deve ser rápido e simples. O looping
ocupado é possível, mas discouraged.This é um ponto importante; sempre tenha em
mente que seu manipulador de interrupção interrompeu outro código (possivelmente
até mesmo outro manipulador de interrupção em uma linha diferente!). Devido a essa
natureza assíncrona, é imperativo que todos os manipuladores de interrupção sejam o
mais rápido e simples possível.Tanto quanto possível, o trabalho deve ser empurrado
para fora do manipulador de interrupção e executado em uma metade inferior, que é
executada em um momento mais conveniente.
A configuração das pilhas de um manipulador de interrupção é uma opção
de configuração. Historicamente, os manipuladores de interrupção não
recebiam suas próprias pilhas. Em vez disso, eles compartilhariam a pilha do
processo que interromperam.1 A pilha do kernel tem duas páginas de tamanho;
normalmente, são 8 KB em arquiteturas de 32 bits e 16 KB em arquiteturas de
64 bits. Como nessa configuração os manipuladores de interrupção
compartilham a pilha, eles devem ser excepcionalmente frugais com quais
dados alocam lá. Obviamente, a pilha do kernel é limitada para começar,
portanto, todo o código do kernel deve ser cauteloso.
No início do processo do kernel 2.6, foi adicionada uma opção para reduzir o
tamanho da pilha de duas páginas para uma, fornecendo apenas uma pilha de 4 KB em
sistemas de 32 bits.Isso reduziu a pressão da memória porque cada processo no
sistema precisava anteriormente de duas páginas de memória do kernel contígua e não
intercambiável.Para lidar com o tamanho reduzido da pilha, os manipuladores de
interrupção receberam sua própria pilha, uma pilha por processador, uma página em
tamanho.Essa pilha é referida como a pilha de interrupção. Embora o tamanho total da
pilha de interrupção seja a metade do tamanho da pilha compartilhada original, o
espaço médio da pilha disponível é maior porque os manipuladores de interrupção
obtêm a página inteira da memória para si mesmos.
1 Um processo está sempre em execução. Quando nada mais é programável, a tarefa ociosa é
executada.
www.it-ebooks.info
Implementando manipuladores de interrupçã
handle_IRQ_event()
Hardware
gera uma interrupção
sim
executar todas as
interrupções do processador Há uma interrupção interrupções
o núcleo
manipulador nesta linha? manipuladores nesta linh
controlador de
interrupção
não
do_IRQ () retornar ao
ret_from_intr() código do kernel
que foi
interrompido
Processador
www.it-ebooks.info
124 Capítulo 7 Interrupções e manipuladores de interrupção
/proc/interrupções
Procfs é um sistema de arquivos virtual que existe apenas na memória do
kernel e é tipicamente montado em /proc. Ler ou gravar arquivos em procfs
invoca funções do kernel que simulam ler ou gravar a partir de um arquivo
real. Um exemplo relevante é o arquivo / proc/interrupts, que é pop-up com
estatísticas relacionadas a interrupções no sistema. Este é um exemplo de
saída de um PC uniprocessador:
2 Como um exercício, após ler o Capítulo 11, você pode dizer quanto tempo o sistema esteve
ativo (em termos de ), sabendo o número de interrupções do temporizador que ocorreram?
www.it-ebooks.info
Controle de interrupção
Controle de interrupção
O kernel do Linux implementa uma família de interfaces para manipular o estado de
interrupções em uma máquina.Essas interfaces permitem desabilitar o sistema de
interrupção do processador atual ou mascarar uma linha de interrupção para a
máquina inteira.Essas rotinas dependem da arquitetura e podem ser encontradas em
<asm/system.h> e <asm/irq.h>. Consulte a Tabela 7.2, mais adiante neste capítulo, para obter
uma lista completa das interfaces.
As razões para controlar o sistema de interrupção geralmente se resumem à
necessidade de fornecer sincronização. Ao desativar as interrupções, você pode garantir
que um manipulador de interrupções não irá antecipar seu código atual. Além disso,
desativar as interrupções também desativa a preempção do kernel. Entretanto, nem a
desabilitação da entrega de interrupção nem a desabilitação da preempção do kernel
fornecem proteção contra acesso simultâneo de outro processador. Como o Linux suporta
vários processadores, o código do kernel geralmente precisa obter algum tipo de bloqueio
para impedir que outro processador acesse dados compartilhados simultaneamente.Esses
bloqueios geralmente são obtidos junto com a desativação de interrupções locais.O
bloqueio fornece proteção contra acesso simultâneo de outro processador, enquanto a
desativação de interrupções fornece proteção contra acesso simultâneo de um possível
manipulador de interrupções. Os Capítulos 9 e 10 discutem os vários problemas de
sincronização e suas soluções. No entanto, é importante entender as interfaces de controle
de interrupção do kernel.
Observe que esses métodos são implementados pelo menos em parte como
macros, de modo que o parâmetro flags (que deve ser definido como um long não
assinado) é aparentemente transmitido por valor. Este parâmetro contém dados
específicos da arquitetura que contêm o estado dos sistemas de interrupção.
Como pelo menos uma arquitetura com suporte incorpora informações de pilha
no valor (ahem, SPARC), os sinalizadores não podem ser passados para outra função
(especificamente, ela deve permanecer no mesmo quadro de pilha). Por esse
motivo, a chamada para salvar e a chamada para restaurar interrupções devem
ocorrer na mesma função.
Todas as funções anteriores podem ser chamadas a partir do contexto de
interrupção e de processo.
www.it-ebooks.info
Controle de interrupção
www.it-ebooks.info
130 Capítulo 7 Interrupções e manipuladores de interrupção
131º
Conclusão
Este capítulo tratou das interrupções, um recurso de hardware usado
pelos dispositivos para sinalizar o processador de forma assíncrona.
Interrupções, na verdade, são usadas pelo hardware para interromper
o sistema operacional.
O hardware mais moderno usa interrupções para se comunicar com os
sistemas operacionais.O driver de dispositivo que gerencia um determinado
hardware registra um manipulador de interrupções para responder e processar
interrupções emitidas de seu hardware associado.O trabalho realizado em
interrupções inclui confirmar e redefinir o hardware, copiar dados do
dispositivo para a memória principal e vice-versa, processar solicitações de
hardware e enviar novas solicitações de hardware.
O kernel fornece interfaces para registrar e cancelar o registro de manipuladores
de interrupção, desativar interrupções, mascarar linhas de interrupção e verificar o
status do sistema de interrupção.A Tabela 7.2 fornece uma visão geral de muitas
dessas funções.
Como as interrupções interrompem outros códigos em execução (processos, o próprio
kernel e até mesmo outros manipuladores de interrupção), elas devem ser executadas
rapidamente. Muitas vezes, no entanto, há muito trabalho a fazer.Para equilibrar a grande
quantidade de trabalho com a necessidade de execução rápida, o kernel divide o trabalho de
processamento de interrupções em duas metades.O manipulador de interrupções, a metade
superior, foi discutido neste capítulo.O próximo capítulo olha para a metade inferior.
www.it-ebooks.info
Esta página foi deixada em branco intencionalmente
www.it-ebooks.info
8.
Metades Inferiores
e Trabalho de
Adiamento
Agora deve ser evidente que os manipuladores de interrupção são apenas uma parte da
solução para gerenciar interrupções de hardware. Os sistemas operacionais certamente
precisam de um mecanismo simples, assíncrono e rápido para responder imediatamente ao
hardware e executar qualquer ação crítica em termos de tempo. Os manipuladores de
interrupção servem bem esta função; mas outros trabalhos menos críticos podem e devem
ser adiados para um ponto posterior quando as interrupções são habilitadas.
Consequentemente, o gerenciamento de interrupções é dividido em duas partes,
ou metades.A primeira parte, os manipuladores de interrupção (metades superiores),
são executados pelo kernel de forma assíncrona em resposta imediata a uma
interrupção de hardware, conforme discutido no capítulo anterior.Este capítulo analisa
a segunda parte da solução de interrupção, metades inferiores.
www.it-ebooks.info
134 Capítulo 8 Metades inferiores e trabalho diferido
Metades Inferiores
O trabalho das metades inferiores é executar qualquer trabalho relacionado à
interrupção não executado pelo manipulador de interrupção. Em um mundo ideal,
isso é quase todo o trabalho porque você deseja que o manipulador de
interrupções execute o mínimo de trabalho (e, por sua vez, seja o mais rápido
possível). Ao descarregar o máximo de trabalho possível para a metade inferior, o
manipulador de interrupções pode retornar o controle do sistema para o que ele
interrompeu o mais rápido possível.
No entanto, o manipulador de interrupções deve executar parte do trabalho. Por
exemplo, o manipulador de interrupção quase seguramente precisa confirmar ao
hardware o recebimento da interrupção. Talvez seja necessário copiar dados de ou
para o hardware.Esse trabalho é sensível à temporização, portanto, faz sentido
executá-lo no manipulador de interrupção.
Quase tudo é um jogo justo para se apresentar na metade inferior. Por exemplo, se
você copiar dados do hardware para a memória na metade superior, certamente faz
sentido processá-los na metade inferior. Infelizmente, não existem regras rígidas e
rápidas sobre o trabalho a ser executado onde—a decisão é deixada inteiramente para o
autor do driver do dispositivo.Embora nenhum arranjo seja ilegal, um arranjo pode
certamente ser subideal. Lembre-se de que os manipuladores de interrupção são
executados de forma assíncrona, com pelo menos a linha de interrupção atual
desabilitada. Minimizar sua duração é importante.Embora nem sempre esteja claro
como dividir o trabalho entre a metade superior e inferior, algumas dicas úteis ajudam:
www.it-ebooks.info
Metades Inferiores
Mas quando é "mais tarde?" A coisa importante a perceber é que mais tarde é
simplesmente não agora. O ponto de uma metade inferior não é fazer o trabalho em
algum ponto específico no futuro, mas sim-ply para adiar o trabalho até qualquer
ponto no futuro, quando o sistema está menos ocupado e as interrupções são
novamente habilitadas. Muitas vezes, as metades inferiores são executadas
imediatamente após o retorno da interrupção. O principal é que elas são executadas
com todas as interrupções ativadas.
O Linux não é o único a separar o processamento de interrupções de
hardware em duas partes; a maioria dos sistemas operacionais faz isso.A
metade superior é rápida e simples e é executada com algumas ou todas as
interrupções desabilitadas.A metade inferior (entretanto, ela é implementada) é
executada posteriormente com todas as interrupções habilitadas.Esse design
mantém a latência do sistema baixa, sendo executada com interrupções
desabilitadas pelo menor tempo necessário.
Filas de Tarefas
Mais tarde, os desenvolvedores do kernel introduziram filas de tarefas tanto como um
método de adiamento de trabalho e como um substituto para o mecanismo BH. O kernel
definiu uma família de filas. Cada fila continha uma lista vinculada de funções a serem
chamadas. As funções enfileiradas eram executadas em determinados momentos,
dependendo da fila em que estavam. Os condutores podiam registrar as suas metades
bot-tom na fila apropriada. Isto funcionou bastante bem, mas ainda era demasiado
inflexível
www.it-ebooks.info
136 Capítulo 8 Metades inferiores e trabalho diferido
para substituir completamente a interface BH. Ele também não era leve o
suficiente para subsistemas de desempenho crítico, como redes.
Softirqs e Tasklets
Durante a série de desenvolvimento 2.3, os desenvolvedores do kernel introduziram softirqs
e tasklets. Com exceção da compatibilidade com drivers existentes, softirqs e tasklets
podem substituir completamente a interface BH.1 Softirqs são um conjunto de metades
inferiores estaticamente definidas que podem ser executadas simultaneamente em qualquer
processador; até mesmo dois do mesmo tipo podem ser executados
simultaneamente.Tasklets, que têm um nome terrível e confuso,2 são metades inferiores
flexíveis e dinamicamente criadas sobre softirqs.Dois tasklets diferentes podem ser
executados simultaneamente em processadores diferentes, mas dois do mesmo tipo de
tasklet não podem ser executados simultaneamente.Assim, os tasklets são uma boa escolha
entre desempenho e facilidade de uso. Para a maioria do processamento da metade inferior,
o tasklet é suficiente. Softirqs são úteis quando o desempenho é crítico, como em redes. O
uso de softirq requer mais cuidado, no entanto, porque dois dos mesmos softirq podem
funcionar ao mesmo tempo. Além disso, os softirqs devem ser registrados estaticamente no
tempo com-pile. Por outro lado, o código pode registrar dinamicamente os tasklets.
Para confundir ainda mais o problema, algumas pessoas se referem a
todas as metades inferiores como interrupções de software ou softirqs. Em
outras palavras, eles chamam tanto o mecanismo softirq quanto as metades
inferiores em geral softirq. Ignore essas pessoas. Eles correm com a mesma
multidão que nomeou os mecanismos BH e tasklet.
Ao desenvolver o kernel 2.5, a interface BH foi finalmente lançada para
o meio-fio porque todos os usuários BH foram convertidos para as outras
interfaces da metade inferior.Além disso, a interface da fila de tarefas foi
substituída pela interface da fila de trabalhos.As filas de trabalhos são
um método simples, mas útil, de enfileirar o trabalho para ser executado
posteriormente no contexto do processo.Chegamos a elas mais tarde.
Consequentemente, hoje o 2.6 tem três mecanismos da metade inferior no kernel:
softirqs, tasklets e filas de trabalho.As antigas interfaces BH e fila de tarefas são apenas
memórias.
Timers do Kernel
Outro mecanismo para adiar o trabalho são os temporizadores do kernel. Ao
contrário dos mecanismos discutidos no capítulo até agora, os temporizadores
adiam o trabalho por um período de tempo especificado. Ou seja, embora as
ferramentas discutidas neste capítulo sejam úteis para adiar o trabalho para qualquer
momento, mas agora você usa cronômetros para adiar o trabalho até que pelo
menos um tempo específico tenha decorrido.
Portanto, os temporizadores têm usos diferentes dos mecanismos gerais discutidos neste
chap-ter. Uma discussão completa sobre temporizadores é apresentada no Capítulo 11,
"Temporizadores e gerenciamento de tempo".
1 Não é trivial converter BHs para softirqs ou tasklets porque BHs são globalmente
sincronizados e, portanto, assumir que nenhum outro BH está em execução durante sua execução. A
conversão acabou por acontecer, no entanto, em 2,5.
2Eles não têm nada a ver com tarefas. Pense em um tasklet como um software simples e fácil de
usar.
www.it-ebooks.info
Softirqs 137
Dissipando a Confusão
Isso é algo realmente confuso, mas na verdade envolve apenas nomear
problemas. Vamos repassar isso.
Metade inferior é um termo genérico do sistema operacional que se refere à
parte adiada do processamento de interrupção, assim chamado porque
representa a segunda, ou metade inferior, da solução de processamento de
interrupção. No Linux, o termo também tem esse significado atualmente.Todos os
mecanismos do kernel para adiar o trabalho são "metades inferiores". Algumas
pessoas também confundem chamar todas as metades inferiores de "softirqs".
A metade inferior também se refere ao mecanismo de trabalho adiado
original no Linux.This mech-anism também é conhecido como um BH, então
nós o chamamos por esse nome agora e deixamos o primeiro como uma
descrição genérica.O mecanismo BH foi preterido um tempo atrás e totalmente
removido na série 2.5 do kernel de desenvolvimento.
Atualmente, existem três métodos para adiar o trabalho: softirqs,
tasklets e filas de trabalho. Os tasklets são construídos em softirqs e as
filas de trabalho são seu próprio subsistema.A Tabela 8.1 apresenta um
histórico das metades inferiores.
Softirqs
O lugar para começar esta discussão dos métodos reais da metade inferior é com
softirqs. Softirqs raramente são usados diretamente; tasklets são uma forma
muito mais comum da metade inferior. No entanto, como os tasklets são
construídos em softirqs, nós os cobrimos primeiro.O código softirq reside no
arquivo kernel/softirq.c na árvore de origem do kernel.
Implementação do Softirqs
Softirqs são alocados estaticamente em tempo de compilação. Ao
contrário do tasklets, não é possível registrar e destruir softirqs
dinamicamente. Softirqs são representados pela estrutura softirq_action, que
é definida em <linux/interrupt.h>:
www.it-ebooks.info
138 Capítulo 8 Metades inferiores e trabalho diferido
O manipulador Softirq
O protótipo de um manipulador de softirq, ação, parece
Executando Softirqs
Um softirq registrado deve ser marcado antes de ser executado. Isso é
chamado de elevar o softirq. Normalmente, um manipulador de
interrupção marca seu softirq para execução antes de retornar. Então,
em um momento adequado, o softirq é executado. Os softirqs
pendentes são verificados e executados nos seguintes locais:
n No retorno do caminho de código de interrupção de hardware
n No tópico do kernel ksoftirqd
n Em qualquer código que explicitamente verifica e executa softirqs
pendentes, como o subsistema de rede
Independentemente do método de invocação, a execução de softirq ocorre em
__do_softirq(), que é invocado por do_softirq().A função é bastante simples. Se houver
3 A maioria dos drivers usa tasklets ou filas de trabalho para sua metade inferior. As tarefas
são construídas a partir de softirqs, como a próxima seção explica.
www.it-ebooks.info
Softirqs 139
2. Agora que o bitmask pendente de softirqs é salvo, ele limpa o bitmask real. 4
3. O ponteiro h é definido para a primeira entrada no softirq_vec.
4. Se o primeiro bit em pendente estiver definido, h->action(h) é chamado.
5. O ponteiro h é incrementado em uma unidade para que agora aponte para a
segunda entrada em
a matriz softirq_vec.
6. O bitmask pendente é deslocado à direita por one.This joga o primeiro bit para
longe e move todos os outros bits um lugar para a direita. Consequentemente, o
segundo bit é agora o primeiro (e assim por diante).
7. O ponteiro h agora aponta para a segunda entrada na matriz, e o bit-
a máscara agora tem o segundo bit como o primeiro. Repita as etapas
anteriores.
4 Na verdade, isso ocorre com interrupções locais desabilitadas, mas isso é omitido neste exemplo simplificado. Se as
interrupções não fossem desabilitadas, um softirq poderia ter sido levantado (e, portanto, estar pendente) no intervalo de
tempo entre salvar a máscara e limpá-la. Isso resultaria na limpeza incorreta de um bit pendente.
www.it-ebooks.info
140 Capítulo 8 Metades inferiores e trabalho diferido
8. Continue repetindo até que o pendente seja zero, momento em que não há
mais softirqs pendentes e o trabalho é feito. Note que esta verificação é suficiente
para garantir que h sempre aponta para uma entrada válida no softirq_vec porque esse
loop tem no máximo 32 bits definidos e, portanto, é executado no máximo 32 vezes.
Uso do Softirqs
Softirqs são reservados para o processamento mais crítico de temporização e importante da
metade inferior no sistema. Atualmente, apenas dois subsistemas — rede e dispositivos de
bloco — usam diretamente softirqs.Além disso, temporizadores e tasklets do kernel são
construídos sobre softirqs. Se você adicionar um novo softirq, você normalmente quer
perguntar a si mesmo por que usar um tasklet é insuficiente.Tasklets são criados
dinamicamente e são mais simples de usar por causa de seus requisitos de bloqueio mais
fraco, e eles ainda funcionam muito bem. No entanto, para aplicativos de cronometragem
crítica que podem fazer seu próprio travamento de forma eficiente, softirqs podem ser a
solução correta.
Atribuindo um Índice
Você declara softirqs estaticamente em tempo de compilação por meio de um enum
em <linux/interrupt.h>.O kernel usa esse índice, que começa em zero, como uma
prioridade relativa. Softirqs com prioridade numérica mais baixa são executados
antes daqueles com prioridade numérica mais alta.
Criar um novo softirq inclui adicionar uma nova entrada a este enum.Ao adicionar um
novo softirq, você pode não querer simplesmente BLOCK_SOFTIRQ e TASKLET_SOFTIRQ.
adicionar sua entrada ao final da lista, como você faria em outro lugar. Em vez disso, você
precisa inserir a nova entrada dependendo da prioridade que deseja atribuir a ela. Por
convenção, HI_SOFTIRQ é sempre o primeiro e RCU_SOFTIRQ é sempre o
TIMER_SOFTIRQ 1 Temporizadores
141º
www.it-ebooks.info
142 Capítulo 8 Metades inferiores e trabalho diferido
Tarefas
Tasklets são um mecanismo de metade inferior construído sobre
softirqs.Como mencionado, eles não têm nada a ver com tasks.Tasklets são
semelhantes em natureza e comportamento a softirqs; no entanto, eles têm
uma interface mais simples e regras de bloqueio relaxado.
Como um autor de driver de dispositivo, a decisão de usar softirqs versus tasklets é
simples: Você quase sempre quer usar tasklets. Como vimos na seção anterior, você
pode (quase) contar em uma mão os usuários de softirqs. Softirqs são necessários
apenas para usos de alta frequência e altamente segmentados.Tasklets, por outro lado,
ver uso muito maior. As tarefas funcionam perfeitamente para a grande maioria dos
casos e são muito fáceis de usar.
Implementando Tasklets
Como as tasklets são implementadas sobre softirqs, elas são
softirqs.Conforme discutido, as tasklets são representadas por dois
softirqs: HI_SOFTIRQ e TASKLET_SOFTIRQ.A única diferença nesses tipos é
que as tasklets baseadas em HI_SOFTIRQ são executadas antes das tasklets
baseadas em TASKLET_SOFTIRQ.
A Estrutura do Tasklet
Os tasklets são representados pela estrutura tasklet_struct. Cada
estrutura representa um tasklet exclusivo. A estrutura é declarada em
<linux/interrupt.h>:
143º
Agendamento de Tasklets
Tasklets agendados (o equivalente de estrutura_do_tasklet
softirqs elevados)5 são armazenados em
duas estruturas por processador: tasklet_vec (para tasklets regulares)
eogivtasklet_hi_vec (para tasklets de alta prioridade).
www.it-ebooks.info
144 Capítulo 8 Metades inferiores e trabalho diferido
10. Repita para a próxima tarefa pendente, até que não haja mais tarefas
agendadas aguardando execução.
A implementação de tasklets é simples, mas bastante inteligente.Como você viu,
todos os tasklets são multiplexados em cima de dois softirqs, HI_SOFTIRQ e
TASKLET_SOFTIRQ.Quando um tasklet é agendado, o kernel levanta um destes
softirqs.Estes softirqs, por sua vez, são manipulados por funções especiais que, em
seguida, executam quaisquer tasklets agendados.As funções especiais garantem que
apenas um tasklet de um determinado tipo é executado ao mesmo tempo. (Mas outras
tarefas podem ser executadas simultaneamente.) Toda essa complexidade é então
ocultada por trás de uma interface simples e limpa.
Uso de Tasklets
Na maioria dos casos, os tasklets são o mecanismo preferido com o
qual implementar sua metade bot-tom para um dispositivo de hardware
normal.Tasklets são criados dinamicamente, fáceis de usar e rápidos.
Além disso, embora o nome deles seja inacreditavelmente confuso,
cresce em você: É bonito.
www.it-ebooks.info
Tarefas
145º
Como acontece com softirqs, os tasklets não podem dormir.Isso significa que
você não pode usar semáforos ou outras funções de bloqueio em um
tasklet.Tasklets também são executados com todas as interrupções ativadas,
portanto, você deve tomar precauções (por exemplo, desativar interrupções e obter
um bloqueio) se o seu tasklet compartilhar dados com um manipulador de
interrupções. Ao contrário do softirqs, no entanto, dois dos mesmos tasklets nunca
são executados simultaneamente — embora dois tasklets diferentes possam ser
executados ao mesmo tempo em dois processadores diferentes. Se o seu tasklet
compartilha dados com outro tasklet ou softirq, você precisa usar o bloqueio
adequado (consulte o Capítulo 9,"Uma introdução à sincronização do kernel" e o
Capítulo 10,"Métodos de sincronização do kernel").
Depois que uma tarefa é agendada, ela é executada uma vez em algum momento no
futuro próximo. Se o mesmo tasklet estiver programado novamente, antes de ter tido a
chance de ser executado, ele ainda será executado apenas uma vez. Se ele já estiver
sendo executado, por exemplo, em outro processador, o tasklet será reagendado e
executado novamente.Como otimização, um tasklet sempre será executado no
processador que o programou — fazendo um melhor uso do cache do processador,
você espera.
Você pode desativar uma tarefa através de uma chamada a tasklet_disable(), que
desativa a tarefa fornecida. Se o tasklet estiver em execução no momento, a função
não retornará até que ele conclua o corte do exe.Alternativamente, você pode usar
tasklet_disable_nosync(), que desabilita o tasklet fornecido, mas não espera a conclusão
do tasklet antes de retornar.Isso geralmente não é seguro porque você não pode
supor que o tasklet ainda não esteja em execução. Uma chamada para
www.it-ebooks.info
146 Capítulo 8 Metades inferiores e trabalho diferido
tasklet_enable()habilita
o tasklet.Esta função também deve ser chamada
antes que um tasklet criado com DECLARE_TASKLET_DISABLED() seja utilizável.
Por exemplo:
Você pode remover um tasklet da fila pendente via tasklet_kill().Essa função recebe um
ponteiro como um argumento solitário para o tasklet_struct do tasklet. A remoção de uma
tarefa agendada da fila é útil ao lidar com uma tarefa que frequentemente redimensiona-
se a si mesma.Essa função primeiro aguarda que a tarefa termine a execução e, em
seguida, remove a tarefa da fila. É claro que nada impede que algum outro código
reprograme a tarefa.Essa função não deve ser usada no contexto de interrupção porque
ela é suspensa.
ksoftirqdComment
O processamento Softirq (e, portanto, tasklet) é auxiliado por um conjunto de threads do
kernel por processador.Esses threads do kernel ajudam no processamento de softirqs
quando o sistema está sobrecarregado com softirqs. Como os tasklets são implementados
com o uso de softirqs, a discussão a seguir se aplica igualmente a softirqs e tasklets. Para
abreviar, nos referiremos principalmente a softirqs.
Como já descrito, o kernel processa softirqs em vários lugares, a maioria com-
monly no retorno do tratamento de uma interrupção. Softirqs podem ser aumentados
em taxas altas (como durante tráfego intenso de rede). Além disso, as funções softirq
podem se reativar.Ou seja, durante a execução, um softirq pode se elevar para que ele
seja executado novamente (por exemplo, o softirq do subsistema network-ing se
eleva).A possibilidade de uma alta frequência de softirq em conjunto com sua
capacidade de se remarcar como ativo pode resultar em programas do espaço do
usuário ficando sem tempo do processador. Não processar os softirqs reativados em
tempo hábil, no entanto, é inaceitável.Quando os softirqs foram projetados pela
primeira vez, isso causou um dilema que precisava ser consertado, e nenhuma solução
óbvia era boa. Primeiro, vamos analisar cada uma das duas soluções óbvias.
A primeira solução é simplesmente manter os softirqs de processamento à medida
que entram e reverificar e reprocessar quaisquer softirqs pendentes antes de retornar.
Isso garante que o kernel processe os softirqs em tempo hábil e, o mais importante,
que quaisquer softirqs reativados também sejam imediatamente processados. O
problema está em ambientes de carga elevada, nos quais muitos softirqs ocorrem, que
continuamente se reativam. O kernel pode continuamente servir softirqs sem realizar
muito mais. O espaço do usuário é negligenciado — na verdade, nada além de softirqs
e manipuladores de interrupção são executados e, por sua vez, os usuários do sistema
ficam loucos.Essa abordagem pode funcionar bem se o sistema nunca estiver sob
carga intensa; se o sistema experimentar níveis de interrupção moderados, essa
solução não é aceitável. O espaço do usuário não pode ficar sem espaço por períodos
significativos.
A segunda solução não é lidar com softwares reativados. No retorno da interrupção, o
kernel simplesmente olha para todos os softirqs pendentes e os executa normalmente. Se
algum softirqs
www.it-ebooks.info
Tarefas 147
reativam-se, no entanto, eles não serão executados até a próxima vez que o kernel
manipular softirqs pendentes.Isto é mais provável que não até que a próxima
interrupção ocorra, o que pode equivaler a uma quantidade longa de tempo antes que
qualquer softirqs novos (ou reativados) sejam executados.Pior, em um sistema de
outra forma ocioso, é benéfico processar os softirqs imediatamente. Infelizmente, essa
abordagem é indiferente a quais processos são executáveis.Portanto, embora esse
método evite que o espaço do usuário fique com fome, ele não deixa os softirqs sem
energia e não tira bom proveito de um sistema ocioso.
Ao projetar softirqs, os desenvolvedores do kernel perceberam que algum tipo de
comprometimento era necessário.A solução finalmente implementada no kernel é não
processar imediatamente softirqs reativados. Em vez disso, se o número de softirqs crescer
excessivamente, o kernel acorda uma família de threads do kernel para lidar com a carga.Os
threads do kernel são executados com a prioridade mais baixa possível (bom valor de 19), o
que garante que eles não funcionam no lugar de qualquer impor-tant.This concessão impede
que a atividade pesada do softirq de esfomear completamente o espaço do usuário do
tempo do processador. Por outro lado, ele também garante que softriqs "em excesso" sejam
executados eventualmente. Finalmente, esta solução tem a propriedade adicionada de que
em um sistema ocioso os softirqs são manipulados rapidamente porque os threads do
kernel serão agendados imediatamente.
Existe um processo por processador.Os processos são chamados ksoftirqd/n, onde n é
o número do processador. Em um sistema de dois processadores, você teria ksoftirqd/0 e
ksoftirqd/1. Ter um processo em cada processador garante que um processador ocioso, se
disponível, sempre pode atender softirqs.Após os processos serem inicializados, eles
executam um loop rígido semelhante a este:
www.it-ebooks.info
148 Capítulo 8 Metades inferiores e trabalho diferido
O antigo mecanismo BH
Embora a antiga interface BH, felizmente, não esteja mais presente no 2.6, ela esteve por aí
por um longo tempo — desde as primeiras versões do kernel. Porque tinha imenso poder
de permanência, certamente carrega algum significado histórico que requer mais do que
um olhar passageiro. Nada nesta breve seção se refere ao ponto 2.6, mas a história é
importante.
A interface BH é antiga, e ela mostrou. Cada BH deve ser estaticamente
definido, e há um máximo de 32. Como todos os manipuladores devem ser
definidos em tempo de compilação, os módulos não podiam usar diretamente a
interface BH. No entanto, eles podiam retirar um BH existente. Ao longo do
tempo, esta exigência estática e o máximo de 32 metades inferiores tornaram-se
um grande obstáculo para o seu uso.
Todos os manipuladores BH são estritamente serializados — não há
dois manipuladores BH, mesmo de tipos diferentes, que possam ser
executados simultaneamente.Isso facilitou a sincronização, mas não foi
benéfico para a escalabilidade do multiprocessador. O desempenho em
grandes máquinas com SMP estava abaixo do normal.Um driver usando a
interface BH não foi bem dimensionado para vários processadores.A
camada de rede, em particular, sofreu.
Além desses atributos, o mecanismo BH é semelhante ao tasklets. Na verdade, a
interface BH foi implementada no topo de tasklets em 2.4.As 32 metades inferiores
possíveis foram representadas por constantes definidas em <linux/interrupt.h>.Para
marcar um BH como pendente, a função mark_bh() foi chamada e passou o número do
BH. No 2.4, isto por sua vez agendou a tarefa BH, bh_action(), para ser executada.
Antes do kernel 2.4, o mecanismo BH era implementado independentemente e não
dependia de nenhum mecha-nismo inferior de nível inferior, tanto quanto softirqs
são implementados hoje.
Por causa das deficiências desta forma de metade inferior, os desenvolvedores do kernel
introduziram filas de tarefas para substituir as metades inferiores.As filas de tarefas nunca
alcançaram este objetivo, embora tenham ganhado muitos novos usuários. Em 2.3, os
mecanismos softirq e tasklet foram introduzidos para colocar um fim ao BH.O mecanismo
BH foi reimplementado em cima de tasklets.
Infelizmente, foi complicado portar as metades inferiores da interface BH para
tasklets ou softirqs por causa da serialização inerente mais fraca das novas
interfaces.6 Durante o 2.5, no entanto, a conversão ocorreu quando os
temporizadores e SCSI—os usuários remanescentes de BH— finalmente
passaram para softirqs.Os desenvolvedores do kernel removeram
sumariamente a interface BH. Boa viagem, BH!
6 Ou seja, a serialização mais fraca foi benéfica para o desempenho, mas também mais difícil de
programar. A conversão de um BH em um tasklet, por exemplo, exigiu uma reflexão cuidadosa: esse código é
seguro ao ser executado ao mesmo tempo que qualquer outro tasklet? Quando finalmente convertido, no entanto,
o desempenho valeu a pena.
www.it-ebooks.info
Filas de trabalho
Filas de trabalho
As filas de trabalho são uma forma diferente de adiar o trabalho do que vimos até agora.
As filas de trabalho adiam o trabalho em um thread do kernel — essa metade inferior
sempre é executada no contexto do processo. Portanto, o código adiado para uma fila
de trabalho tem todos os benefícios usuais do contexto do processo. Mais importante,
as filas de trabalho são programáveis e, portanto, podem ser suspensas.
Normalmente, é fácil decidir entre usar filas de trabalho e softirqs/tasklets. Se o
trabalho adiado precisar ser suspenso, as filas de trabalho serão usadas. Se o
trabalho adiado não precisar dormir, softirqs ou tasklets são usados. De fato, a
alternativa usual para filas de trabalho são os threads do kernel. Como os
desenvolvedores do kernel desaprovam a criação de um novo segmento do kernel
(e, em alguns locais, é uma ofensa punível), as filas de trabalho são fortemente
preferidas.Elas também são muito fáceis de usar.
Se você precisar de uma entidade programável para executar seu processamento da
metade inferior, precisará de filas de trabalho.Eles são os únicos mecanismos da metade
inferior que são executados no contexto do processo e, portanto, os únicos que podem ser
suspensos.Isso significa que eles são úteis para situações em que você precisa alocar
muita memória, obter um semáforo ou executar E/S de bloco. Se você não precisa de um
thread do kernel para manipular seu trabalho adiado, considere um tasklet.
Essas estruturas são colocadas em uma lista vinculada, uma para cada tipo de fila
em cada processador. Por exemplo, há uma lista de trabalhos adiados para o thread
genérico, por processador.Quando um thread de trabalho é ativado, ele executa
qualquer trabalho em sua lista.Quando ele é concluído
www.it-ebooks.info
Filas de trabalho
3. Se a lista não estiver vazia, o thread não entrará em suspensão. Em vez disso,
ele marca a si mesmo TASK_RUNNING e se remove da fila de espera.
Esta função efetua loop sobre cada entrada na lista vinculada de trabalhos
pendentes e executa o membro func da workqueue_struct para cada entrada na
lista vinculada:
1. Embora a lista não esteja vazia, ela obtém a próxima entrada na lista.
2. Ele recupera a função que deve ser chamada, func, e seu argumento, data.
3. Ele remove essa entrada da lista e limpa o bit pendente na própria estrutura.
www.it-ebooks.info
152 Capítulo 8 Metades inferiores e trabalho diferido
...
um por cada
função
estrutura_trabalho
...
.. . .estruturas
...
...
No nível mais alto, há threads de trabalho. Pode haver vários tipos de threads de
trabalho; há um thread de trabalho por processador de um determinado tipo. Partes do
kernel podem criar threads de trabalho conforme necessário. Por padrão, há o thread de
trabalho de eventos. Cada thread de trabalho é representado pela estrutura
cpu_workqueue_struct.A estrutura workqueue_struct representa todos os threads de trabalho de
um determinado tipo.
Por exemplo, suponha que, além do tipo de worker de eventos genéricos, você
também crie um tipo de worker de falcon. Além disso, suponha que você tenha um
computador com quatro processadores. Em seguida, há quatro threads de eventos
(e, portanto, quatro estruturas cpu_workqueue_struct) e quatro threads de falcon (e,
portanto, outras quatro estruturas cpu_workqueue_struct). Há um tipo de evento e um para
o tipo de falcon.
Agora, vamos abordar a partir do nível mais baixo, que começa com work.Your driver
cria trabalho, que ele quer adiar para mais tarde.The work_struct structure representa este
trabalho. Entre outras coisas, esta estrutura contém um ponteiro para a função que
manipula o
www.it-ebooks.info
Filas de trabalho
Criando Trabalho
A primeira etapa é realmente criar algum trabalho a ser adiado.Para
criar a estrutura estaticamente em tempo de execução, use
DECLARE_WORK:
Trabalho de Agendamento
Agora que o trabalho foi criado, podemos programá-lo.Para enfileirar uma
determinada função de manipulador de trabalho com os threads de
trabalho de eventos padrão, basta chamar
O trabalho é agendado imediatamente e é executado assim que o
thread de trabalho de eventos no processador atual é ativado.
www.it-ebooks.info
154 Capítulo 8 Metades inferiores e trabalho diferido
Às vezes, você não deseja que o trabalho seja executado imediatamente, mas depois
de algum atraso. Nesses casos, você pode programar o trabalho a ser executado em um
determinado momento no futuro:
Trabalho de descarga
O trabalho enfileirado é executado quando o thread de trabalho é ativado em
seguida. Às vezes, você precisa garantir que um determinado lote de trabalho
foi concluído antes de continuar.Isso é especialmente importante para
módulos, que quase certamente querem chamar esta função antes de
descarregar. Outros lugares no núcleo também podem precisar ter certeza de
que nenhum trabalho está pendente, para evitar condições de corrida.
Para essas necessidades, há uma função para liberar uma determinada
fila de trabalho:
www.it-ebooks.info
Filas de trabalho
schedule_delay_work(),
exceto que eles trabalham na fila de trabalho fornecida
e não na fila de eventos padrão.
flush_schedule_work(),
Finalmente, você pode liberar uma fila de espera por meio de uma chamada
para a função
7 Os nomes da metade de baixo são aparentemente uma conspiração para confundir novos
desenvolvedores do kernel. Sério, esses nomes são horríveis.
www.it-ebooks.info
156 Capítulo 8 Metades inferiores e trabalho diferido
www.it-ebooks.info
Desativação das metades inferiores
www.it-ebooks.info
158 Capítulo 8 Metades inferiores e trabalho diferido
8 Esse contador é usado pelos subsistemas de interrupção e pela metade inferior. Assim, no
Linux, um único contador por tarefa representa a atomicidade de uma tarefa. Isso se mostrou útil para
trabalhos como depuração de bugs atômicos enquanto dormem.
www.it-ebooks.info
Conclusão
159º
Conclusão
Neste capítulo, abordamos os três mecanismos usados para adiar o trabalho no kernel do
Linux: softirqs, tasklets e filas de trabalho.Examinamos seu design e
implementação.Discutimos como usá-los em seu próprio código e insultamos seus nomes
mal concebidos. Para uma completa história, também observamos os mecanismos da
metade inferior que existiam em versões anteriores do kernel do Linux: BHs e filas de
tarefas.
Falamos muito neste capítulo sobre sincronização e simultaneidade porque esses
tópicos se aplicam um pouco às metades inferiores.Inclusive, concluímos o capítulo com
uma discussão sobre como desativar as metades inferiores por razões de proteção da
simultaneidade. Chegou a altura de nos debruçarmos primeiro sobre estes temas. O
Capítulo 9 discute a sincronização do kernel e a concordância de moedas em abstrato,
fornecendo uma base para a compreensão dos problemas no centro do problema. O
Capítulo 10 discute as interfaces específicas fornecidas pelo nosso amado núcleo para
resolver estes problemas. Armado com os próximos dois capítulos, o mundo é a sua ostra.
www.it-ebooks.info
Esta página foi deixada em branco intencionalmente
www.it-ebooks.info
9.
Uma introdução à
sincronização do
kernel
1 O termo threads de execução implica qualquer instância de código em execução. Isso inclui, por exemplo,
uma tarefa no kernel, um manipulador de interrupção, uma metade inferior ou um thread do kernel. Este
capítulo pode encurtar os segmentos de execução para simples segmentos. Lembre-se de que este termo
descreve qualquer código em execução.
www.it-ebooks.info
162 Capítulo 9 Uma introdução à sincronização do kernel
Agora, vamos supor que outra dedução nos fundos do usuário esteja ocorrendo ao
mesmo tempo. Não importa como a dedução simultânea está acontecendo:Suponha que o
cônjuge do usuário esteja iniciando outro levantamento em outro caixa eletrônico, que um
beneficiário esteja transferindo fundos eletronicamente para fora da conta ou que o banco
esteja deduzindo uma taxa da conta (como os bancos atualmente são tão habituais a
fazer).Qualquer um desses cenários se encaixa em nosso exemplo.
Ambos os sistemas que realizam a retirada teriam um código semelhante ao que
acabamos de ver: Primeiro verifique se a dedução é possível, depois calcule os novos
fundos totais e, finalmente, execute a dedução física. Agora vamos fazer alguns
números. Suponha que a primeira dedução seja um levantamento de um caixa
eletrônico por US$ 100 e que a segunda dedução seja o banco aplicando uma taxa de
US$ 10 porque o cliente entrou no banco. Suponha que o cliente tenha um total de US$
105 no banco. Obviamente, uma dessas transações não pode ser concluída
corretamente sem enviar a conta para o vermelho.
O que você esperaria é algo como isto:A transação da taxa acontece
primeiro.Dez dólares é menos que $105, então 10 é subtraído de 105
para obter um novo total de 95, e $10 é embolsado pelo banco.Então o
saque do ATM vem junto e falha porque $95 é menos que $100.
Com as condições de corrida, a vida pode ser muito mais interessante.
Suponha que as duas transações sejam iniciadas aproximadamente ao mesmo
tempo. Ambas as transações verificam se existem fundos suficientes: US$ 105
é mais do que US$ 100 e US$ 10, então tudo é bom.Em seguida, o processo de
retirada subtrai US$ 100 de US$ 105, gerando US$ 5.A transação a taxa faz o
mesmo, subtraindo US$ 10 de US$ 105 e obtendo US$ 95.O processo de
retirada atualiza o novo total de fundos disponíveis do usuário para US$ 5.
Agora, a transação de taxa também atualiza o novo total, resultando em US$ 95.
Dinheiro grátis!
Claramente, as instituições financeiras devem garantir que isso nunca
aconteça.Elas devem bloquear a conta durante certas operações, tornando
cada transação atômica em relação a qualquer outra transação. Essas
transações devem ocorrer na íntegra, sem interrupção, ou não ocorrer de
todo.
A única variável
Agora, vamos ver um exemplo específico de computação. Considere um recurso
compartilhado simples, um inteiro global de segmento único e uma região crítica simples, a
operação de simplesmente incrementá-lo:
Segmento 1 Segmento 2
obter i (7) —
incremento i (7 -> 8) —
write-back i (8) —
— obter i (8)
— incremento i (8 -> 9)
— write-back i (9)
Segmento 1 Segmento 2
obter i (7) obter i (7)
incremento i (7 -> 8) —
— incremento i (7 -> 8)
write-back i (8) —
— write-back i (8)
Segmento 1 Segmento 2
incrementar e armazenar i (7 -> 8) —
— incrementar e armazenar i (8 -> 9)
Ou inversamente
Segmento 1 Segmento 2
— incrementar e armazenar (7 -> 8)
incrementar e armazenar (8 -> 9) —
www.it-ebooks.info
Bloqueio
165º
Bloqueio
Agora, vamos considerar uma condição de corrida mais complicada que requer uma
solução mais complicada. Suponha que você tenha uma fila de solicitações que precisam
ser atendidas. Para este exercício, vamos supor que a implementação seja uma lista
vinculada, na qual cada nó representa uma solicitação. Duas funções manipulam a fila. Uma
função adiciona uma nova solicitação à parte final da fila. Outra função remove uma
solicitação do cabeçalho da fila e faz algo útil com a solicitação. Várias partes do kernel
invocam essas duas funções; portanto, as solicitações são continuamente adicionadas,
removidas e atendidas. Manipular as filas de solicitação certamente requer várias
instruções. Se um thread tentar ler a partir da fila enquanto outro estiver no meio de
manipulá-lo, o thread de leitura encontrará a fila em um estado inconsistente. Deve ser
aparente o tipo de dano que poderia ocorrer se o acesso à fila pudesse ocorrer
simultaneamente. Muitas vezes, quando o recurso compartilhado é uma estrutura de dados
complexa, o resultado de uma condição de corrida é a corrupção da estrutura de dados.
O cenário anterior, no início, pode não ter uma solução clara. Como você pode
impedir que um processador leia da fila enquanto outro processador o está
atualizando? Embora seja viável para uma arquitetura particular implementar
instruções simples, como aritmética e comparação, atomicamente é ridículo para
arquiteturas fornecer instruções para suportar as regiões críticas de tamanho
indefinido que existiriam no exemplo anterior.O que é necessário é uma maneira de
garantir que apenas um segmento manipule a estrutura de dados de cada vez - um
mecanismo para impedir o acesso a um recurso enquanto outro segmento de execução
está na região marcada.
Uma fechadura fornece tal mecanismo; funciona muito como uma fechadura
em uma porta. Imagine a sala do lado de fora da porta como a região crítica.
Dentro da sala, apenas um segmento de execução pode estar presente em um
determinado momento.Quando um segmento entra na sala, ele bloqueia a porta
atrás dele.Quando o segmento termina de manipular os dados compartilhados,
ele sai da sala e desbloqueia a porta. Se outro fio alcançar a porta enquanto
estiver travado, ele deve esperar que o fio dentro saia da sala e destrave a porta
antes que ela possa entrar.Os fios seguram as fechaduras; as fechaduras
protegem os dados.
No exemplo da fila de solicitações anterior, um único bloqueio poderia ter sido usado
para proteger a fila.Sempre que houvesse uma nova solicitação para adicionar à fila, o
thread primeiro obteria o bloqueio.Em seguida, ele poderia adicionar com segurança a
solicitação à fila e, por fim, liberar o bloqueio.Quando um thread desejasse remover uma
solicitação da fila, ele também obteria o bloqueio.Em seguida, ele poderia ler a solicitação e
removê-la da fila. Finalmente, ele liberaria o bloqueio.Qualquer outro acesso à fila também
precisaria obter o bloqueio. Como o bloqueio pode ser mantido por apenas um thread por
vez, apenas um thread pode manipular a fila por vez. Se um thread aparecer enquanto outro
thread já estiver
www.it-ebooks.info
166 Capítulo 9 Uma introdução à sincronização do kernel
atualizando-o, o segundo segmento tem que esperar que o primeiro libere o bloqueio
antes que ele possa continuar. O bloqueio evita a simultaneidade e protege a fila das
condições de corrida. Qualquer código que acesse a fila primeiro precisa obter o
bloqueio relevante. Se outro
o segmento de execução vem junto, o bloqueio evita a simultaneidade:
Segmento 1 Segmento 2
tentar bloquear a fila tentar bloquear a fila
fila de acesso...
desbloquear a fila
Causas da simultaneidade
No espaço do usuário, a necessidade de sincronização deriva do fato de que os programas
são programados preventivamente à vontade do programador. Como um processo pode ser
antecipado a qualquer momento e outro processo pode ser programado para o processador,
um processo pode ser involuntariamente antecipado no meio do acesso a uma região
crítica. Se o processo recém-agendado entrar na mesma região crítica (digamos, se os dois
processos manipularem a mesma memória compartilhada ou gravarem no mesmo descritor
de arquivo), poderá ocorrer uma corrida.O mesmo problema pode ocorrer com vários
processos de segmento único compartilhando arquivos, ou em um único programa com
sinais, porque os sinais podem ocorrer de forma assíncrona.Esse tipo de simultaneidade —
na qual duas coisas não acontecem de fato ao mesmo tempo, mas interagem entre si de
forma que elas também possam ocorrer — é chamado de pseudo-simultaneidade.
Se você tiver uma máquina de multiprocessamento simétrica, dois processos podem
realmente ser recortados por exe em uma região crítica ao mesmo tempo.Isso é chamado
de simultaneidade verdadeira.Embora as causas e semânticas da simultaneidade
verdadeira versus pseudosimultaneidade sejam diferentes, ambos resultam nas mesmas
condições de corrida e exigem o mesmo tipo de proteção.
O kernel tem causas semelhantes de simultaneidade:
www.it-ebooks.info
Deadlocks 169
Sempre que você escrever o código do kernel, você deve fazer a si mesmo
estas perguntas:
n Os dados são globais? Um thread de execução diferente do atual pode acessá-lo?
n Os dados são compartilhados entre o contexto do processo e o contexto de
interrupção? Ele é compartilhado entre dois manipuladores de interrupção
diferentes?
n Se um processo for substituído durante o acesso a esses dados, o processo
recém-agendado poderá acessar os mesmos dados?
n O processo atual pode ser suspenso (bloqueado) em algo? Em caso
afirmativo, em que estado se encontram os dados partilhados?
n O que impede que os dados sejam liberados de dentro de mim?
n O que acontece se esta função for chamada novamente em outro
processador?
n Considerando os pontos de procedimento, como posso garantir que meu
código esteja livre de concorrência?
Em resumo, quase todos os dados globais e compartilhados no
kernel requerem alguma forma dos métodos de sincronização,
discutidos no próximo capítulo.
Deadlocks
Um deadlock é uma condição que envolve um ou mais threads de execução e
um ou mais recursos, de forma que cada thread espere por um dos recursos,
mas todos os recursos já são mantidos.Todos os threads aguardam um pelo
outro, mas nunca fazem qualquer progresso em direção à liberação dos
recursos que já mantêm.Portanto, nenhum dos threads pode continuar, o que
resulta em um deadlock.
Uma boa analogia é uma parada de trânsito de quatro vias. Se cada carro na parada
decide esperar pelos outros carros antes de ir, nenhum carro vai continuar, e nós
temos um impasse no trânsito.
O exemplo mais simples de um impasse é o autoimpasse:4 Se um
segmento de execução tenta adquirir um bloqueio que já possui, ele tem
que esperar que o bloqueio seja liberado. Mas ele nunca vai liberar a
fechadura, porque está ocupado esperando a fechadura, e o resultado é
um impasse:
4 Alguns kernels evitam esse tipo de deadlock fornecendo bloqueios recursivos. Esses são bloqueios que
um único thread de execução pode adquirir várias vezes. O Linux, felizmente, não fornece bloqueios
recursivos. Isso é amplamente considerado uma coisa boa. Embora bloqueios recursivos possam aliviar o
problema de autobloqueio, eles levam muito prontamente à semântica de bloqueio desleixada.
www.it-ebooks.info
170 Capítulo 9 Uma introdução à sincronização do kernel
O primeiro ponto é muito importante e merece ser salientado. Se dois ou mais bloqueios
forem adquiridos ao mesmo tempo, eles devem ser sempre adquiridos na mesma ordem.
Vamos supor que você tenha as fechaduras de gato, cachorro e raposa que protegem
estruturas de dados de mesmo nome. Agora, suponha que você tenha uma função que
precise trabalhar em todas essas três estruturas de dados simultaneamente — talvez para
copiar dados entre elas.Seja qual for o caso, as estruturas de dados exigem bloqueio para
garantir o acesso seguro. Se uma função adquire as fechaduras na ordem gato, cão e, em
seguida, raposa, então todas as outras funções devem obter essas fechaduras (ou um
subconjunto delas) nesta mesma ordem. Por exemplo, é um possível impasse (e, portanto,
um bug) para primeiro obter a fechadura de raposa e, em seguida, obter a fechadura de
cachorro, porque a fechadura de cachorro deve sempre ser adquirida antes da fechadura de
raposa. Aqui está um exemplo no qual isso causaria um impasse:
Segmento 1 Segmento 2
adquirir gato de bloqueio adquirir bloqueio de raposa
Contenção e escalabilidade
O termo contenção de bloqueio, ou simplesmente contenção, descreve um
bloqueio atualmente em uso, mas que outro thread está tentando adquirir.Um
bloqueio altamente contestado geralmente tem threads aguardando para adquiri-lo.
A alta contenção pode ocorrer porque uma trava é frequentemente obtida, mantida
por um longo período após ser obtida, ou ambos. Como o trabalho de um bloqueio
é serializar o acesso a um recurso, não é surpresa que os bloqueios possam
reduzir o desempenho de um sistema.Um bloqueio altamente disputado pode se
tornar um gargalo no sistema, limitando rapidamente seu desempenho. É claro que
as travas também são necessárias para evitar que o sistema se desmonte em
pedaços, de modo que uma solução para alta disputa deve continuar a fornecer a
proteção de simultaneidade necessária.
A escalabilidade é uma medida de como um sistema pode ser expandido. Em
sistemas operacionais, falamos da escalabilidade com um grande número de
processos, um grande número de processadores ou grandes quantidades de
memória.Podemos discutir a escalabilidade em relação a praticamente qualquer
componente de um computador ao qual podemos anexar uma quantidade. O ideal é
que o dobro do número de processadores resulte na duplicação do desempenho do
processador do sistema. É claro que isso nunca acontece.
A escalabilidade do Linux em um grande número de processadores aumentou
drasticamente no tempo desde que o suporte ao multiprocessamento foi introduzido no
kernel 2.0. No início do suporte ao multiprocessamento do Linux, apenas uma tarefa podia
ser executada no kernel por vez.
Durante a versão 2.2, essa limitação foi removida à medida que os mecanismos de
bloqueio cresciam mais finos.Através da versão 2.4 e adiante, o bloqueio do kernel
ficou ainda mais fino.Hoje, no kernel 2.6 do Linux, o bloqueio do kernel é muito fino
e a escalabilidade é boa.
A granularidade do bloqueio é uma descrição do tamanho ou da quantidade de dados
protegidos por um bloqueio.Um bloqueio muito grosseiro protege uma grande quantidade
de dados, por exemplo, um sub-
www.it-ebooks.info
172 Capítulo 9 Uma introdução à sincronização do kernel
conjunto de estruturas de dados do sistema. Por outro lado, uma trava muito fina
protege uma pequena quantidade de dados — por exemplo, apenas um único elemento
em uma estrutura maior. Na realidade, a maioria das fechaduras situam-se algures
entre estes dois extremos, não protegendo nem um subsistema inteiro nem um
elemento individual, mas talvez uma única estrutura ou lista de estruturas. A maioria
das travas começa de forma bastante grosseira e é mais refinada, já que a contenção
de bloqueio se mostra um problema. Um exemplo de evolução para o bloqueio mais
refinado são as runqueues do agendador, dis-
discutido no Capítulo 4, "Programação do processo". No 2.4 e nos kernels
anteriores, o scheduler tinha uma única fila de execução. (Lembre-se de que uma
fila de execução é a lista de processos executáveis.) No início da série 2.6, o
programador O(1) introduziu runqueues por processador, cada um com um bloqueio
exclusivo. O bloqueio evoluiu de um único bloqueio global para bloqueios
separados para cada processador.Esta foi uma otimização importante, porque o
bloqueio de runqueue era altamente disputado em máquinas grandes,
essencialmente serializando todo o processo de agendamento para um único
processador executando no agendador de cada vez. Mais tarde na série 2.6, o CFS
Scheduler melhorou ainda mais a escalabilidade.
Geralmente, essa melhoria de escalabilidade é uma coisa boa, porque melhora o
desempenho do Linux em sistemas maiores e mais potentes. "Melhorias" desenfreadas
de escalabilidade podem levar a uma diminuição no desempenho em máquinas SMP e
UP menores, entretanto, porque máquinas menores podem não precisar de travamento
fino, mas, mesmo assim, precisarão lidar com o aumento da complexidade e da
sobrecarga. Considere uma lista vinculada.Um esquema de bloqueio inicial forneceria
um único bloqueio para a lista inteira. Com o tempo, essa trava única pode vir a ser um
gargalo de escalabilidade em grandes máquinas com vários processadores que
acessam essa lista vinculada de forma livre. Em resposta, o bloqueio único poderia ser
dividido em um bloqueio por nó na lista vinculada. Para cada nó que você deseja ler ou
gravar, você obteve o bloqueio exclusivo do nó. Agora, só há contenção de bloqueio
quando vários processadores estão acessando o mesmo nó exato.E se ainda houver
contenção de bloqueio, como fazer? Você fornece um bloqueio para cada elemento em
cada nó? Cada bit de cada elemento? A resposta é não. Embora esse travamento
refinado possa garantir excelente escalabilidade em máquinas SMP grandes, como ele
funciona em máquinas com processador duplo? A sobrecarga de todas essas travas
extras é desperdiçada se uma máquina com processador duplo não detectar,
inicialmente, uma disputa significativa por travas.
No entanto, a escalabilidade é uma consideração importante. Projetar seu
bloqueio desde o início para dimensionar bem é importante. O bloqueio grosseiro
dos principais recursos pode facilmente se tornar um gargalo até mesmo em
máquinas pequenas.Há uma linha fina entre o bloqueio muito grosseiro e o bloqueio
muito fino. O travamento muito grosseiro resulta em baixa escalabilidade se houver
alta contenção de bloqueio, enquanto o travamento muito fino resulta em
sobrecarga desperdiçadora se houver pouca contenção de bloqueio. Ambos os
cenários equivalem a um desempenho insatisfatório. Comece simples e aumente a
complexidade somente conforme necessário. Simplicidade é a chave.
Conclusão
Tornar seu código seguro para SMP não é algo que possa ser adicionado posteriormente. A
sincronização adequada — bloqueio livre de bloqueios, escalável e limpo — requer
decisões de design do início ao fim.Sempre que você escreve código kernel, se é
www.it-ebooks.info
Conclusão
173º
www.it-ebooks.info
10
.
Métodos de
Sincronização do
Kernel
Operações Atômicas
Começamos nossa discussão sobre métodos de sincronização com operações atômicas
porque eles são a base sobre a qual outros métodos de sincronização são construídos. As
operações Atomic fornecem instruções que são executadas atomicamente — sem
interrupção. Assim como o átomo foi originalmente pensado para ser uma partícula
indivisível, operadores atômicos são indivisíveis instruções. Por exemplo, como discutido
no capítulo anterior, um incremento atômico pode ler e incrementar uma variável por um em
uma única etapa indivisível e ininterrupta. Lembre-se da corrida simples ao incrementar um
inteiro que discutimos no capítulo anterior:
Segmento 1 Segmento 2
obter i (7) obter i (7)
incremento i (7 -> 8)
— incremento i (7 -> 8)
write-back i (8) —
— write-back i (8)
www.it-ebooks.info
176 Capítulo 10 Métodos de sincronização do kernel
Ou
Segmento 1 Segmento 2
— obter, incrementar e armazenar i (7 -> 8)
obter, incrementar e armazenar i (8 -> 9) —
O valor final, sempre nove, está correto. Nunca é possível que as duas
operações atômicas ocorram na mesma variável simultaneamente.
Portanto, não é possível que os incrementos corram.
O kernel fornece dois conjuntos de interfaces para operações atômicas—uma que opera
em inteiros e outra que opera em bits individuais.Essas interfaces são implementadas em
todas as arquiteturas suportadas pelo Linux. A maioria das arquiteturas contém instruções
que fornecem versões atômicas de operações aritméticas simples. Outras arquiteturas, sem
operações atômicas diretas, fornecem uma operação para bloquear o barramento de
memória para uma única operação, garantindo assim que outra operação que afeta a
memória não pode ocorrer simultaneamente.
atomic_t de 32 bits
(bit) 31 7. º
Esta função diminui em um valor atômico dado. Se o resultado for zero, ele
retornará true; caso contrário, ele retornará false.Uma lista completa das
operações de inteiros atômicos padrão (aquelas encontradas em todas as
arquiteturas) está na Tabela 10.1.Todas as operações implementadas em uma
arquitetura específica podem ser encontradas em <asm/atomic.h>.
www.it-ebooks.info
178 Capítulo 10 Métodos de sincronização do kernel
Uma listagem das operações de bit atômico padrão está na Tabela 10.3.
Convenientemente, versões não atômicas de todas as funções bit a bit também são
fornecidas. Eles se comportam de forma idêntica a seus irmãos atômicos, exceto que
eles não garantem a atomicidade, e seus nomes são prefixados com sublinhados
duplos. Por exemplo, a forma não atômica de test_bit() é __test_bit(). Se você não precisar de
atomicidade (por exemplo, porque uma trava já protege seus dados), essas variantes das
funções bit a bit podem ser mais rápidas.
www.it-ebooks.info
Travas de Rotação
Travas de Rotação
Embora seria bom se cada região crítica consistisse em código que não fizesse nada
mais complicado do que incrementar uma variável, a realidade é muito mais cruel. Na
vida real, regiões críticas podem abranger várias funções. Por exemplo, muitas vezes os
dados precisam ser movidos novamente de uma estrutura, formatados e analisados e
adicionados a outra estrutura. Isso
www.it-ebooks.info
184 Capítulo 10 Métodos de sincronização do kernel
toda a operação deve ocorrer atomicamente; não deve ser possível para outro
código ler ou gravar em qualquer estrutura antes que a atualização seja concluída.
Uma vez que operações atômicas simples são claramente incapazes de fornecer a
proteção necessária em um sce-nario tão complexo, um método mais geral de
sincronização é necessário: bloqueios.
O bloqueio mais comum no kernel do Linux é o spin lock.A spin lock é um
bloqueio que pode ser mantido por no máximo um thread de execução. Se um
segmento de execução tenta adquirir um bloqueio de giro enquanto ele já está
suspenso, o que é chamado de contended (contestado), o segmento busy loops—
gira — aguardando que o bloqueio se torne disponível. Se o bloqueio não for
contestado, o segmento poderá adquirir imediatamente o bloqueio e continuar.A
rotação impede que mais de um segmento de execução entre na região crítica a
qualquer momento.O mesmo bloqueio pode ser usado em vários locais, de modo
que todo o acesso a uma determinada estrutura de dados, por exemplo, possa ser
protegido e sincronizado.
Voltando à analogia da porta e da chave do último capítulo, fechaduras de giro são
como sentar do lado de fora da porta, esperando o sujeito de dentro sair e entregar-lhe a
chave. Se você chegar à porta e ninguém estiver lá dentro, você pode pegar a chave e
entrar no quarto. Se você chegar à porta e alguém está atualmente dentro, você deve
esperar fora para a chave, efetivamente verificando a sua presença
repetidamente.Quando o quarto está desocupado, você pode pegar a chave e ir para
dentro.Graças à chave (leia: bloqueio de rotação), apenas uma pessoa (leia: linha de ex-
execução) é permitida dentro do quarto (leia: região crítica) ao mesmo tempo.
O fato de que um bloqueio de giro contestado faz com que os threads girem
(essencialmente perdendo tempo do processador) enquanto aguardam que o bloqueio
se torne disponível é importante.Esse comportamento é o ponto do bloqueio de giro.
Não é aconselhável manter um bloqueio de giro por muito tempo.Essa é a natureza do
bloqueio de giro: um bloqueio leve de um único titular que deve ser mantido por curtos
períodos.Um comportamento alternativo quando o bloqueio é disputado é colocar o
thread atual para dormir e despertá-lo quando ele se tornar disponível.Então o
processador pode desligar e executar outro código.Isso incorre um pouco de
sobrecarga—mais notavelmente os dois switches de contexto necessários para sair e
voltar para o thread de bloqueio, que é certamente muito mais código do que o punhado
de linhas usadas para implementar um bloqueio de giro.Portanto, é aconselhável manter
bloqueios de giro de duração por menos do que o de duas alternâncias de contexto.
Como a maioria de nós tem coisas melhores para fazer do que medir chaves de
contexto, apenas tente segurar o bloqueio pelo menor tempo possível. 1 Mais tarde neste
capítulo, discutimos semáforos, que fornecem um bloqueio que faz o segmento de
espera dormir, em vez de girar, quando contestado.
www.it-ebooks.info
Travas de Rotação
www.it-ebooks.info
186 Capítulo 10 Métodos de sincronização do kernel
O Que Eu Bloqueo?
É importante que cada eclusa esteja claramente associada ao que está bloqueando.
Mais importante, você deve proteger os dados e não codificar. Apesar dos
exemplos neste capítulo que explicam a importância de proteger as seções críticas,
são os dados reais que precisam de proteção, e não o código.
Big Fat Rule: Bloqueios que simplesmente envolvam regiões de código são difíceis
de entender e propensos a condições de corrida. Bloquear dados, não códigos.
Em vez de bloquear código, sempre associe seus dados compartilhados a um
bloqueio específico. Por exemplo, "a struct foo está bloqueada por foo_lock." Sempre
que acessar dados compartilhados, verifique se estão seguros. Provavelmente, isso
significa obter o bloqueio apropriado antes de manipular os dados e liberar o
bloqueio quando concluído.
À medida que o kernel cresce em tamanho e complexidade, é cada vez mais difícil
garantir que
as interrupções são sempre habilitadas em qualquer caminho de código no
kernel. Utilização de
spin_lock_irq(),
portanto, não é recomendado. Se você usá-lo, é melhor ser posi-
ativa que as interrupções estavam originalmente ativadas ou as pessoas ficarão
aborrecidas quando esperarem interrupções
para estar fora, mas encontrá-los!
2 O uso dessas duas funções pode levar a um código complicado. Você não deve ter que verificar frequentemente os
valores de bloqueios de rotação — seu código deve sempre adquirir o próprio bloqueio ou ser sempre chamado enquanto
o bloqueio já estiver suspenso. No entanto, existem alguns usos legítimos, de modo que essas interfaces são fornecidas.
www.it-ebooks.info
188 Capítulo 10 Métodos de sincronização do kernel
www.it-ebooks.info
Bloqueios de Rotação de Leitor-Grava
www.it-ebooks.info
190 Capítulo 10 Métodos de sincronização do kernel
Semáforos
Os semáforos no Linux são bloqueios em espera.Quando uma tarefa tenta
adquirir um semáforo que não está disponível, o semáforo coloca a tarefa
em uma fila de espera e a coloca em espera.O processador fica livre para
executar outro código.Quando o semáforo fica disponível, uma das tarefas
na fila de espera é ativada para que ela possa adquirir o semáforo.
Vamos voltar para a porta e analogia da chave.Quando uma pessoa chega à
porta, ela pode pegar a chave e entrar no quarto.A grande diferença está no que
acontece quando outro cara chega à porta e a chave não está disponível. Neste
caso, em vez de girar, o fel-low coloca seu nome em uma lista e leva um
número.Quando a pessoa dentro da sala sai, ele verifica a lista na porta. Se o nome
de alguém está na lista, ele vai até o primeiro nome e lhe dá um brincalhão no peito,
acordando-o e permitindo-lhe entrar na sala. Desta forma, a chave (leia: semáforo)
continua a garantir que há apenas uma pessoa (leia: linha de execução) dentro da
sala (leia: região crítica) de cada vez. Isso fornece melhor utilização do processador
do que bloqueios de rotação porque não há tempo gasto em looping ocupado, mas
os semáforos têm uma sobrecarga muito maior do que os bloqueios de rotação. A
vida é sempre uma troca.
Você pode tirar algumas conclusões interessantes do comportamento de dormir dos
semáforos:
191.
n Por outro lado, os semáforos não são ideais para bloqueios que são mantidos
por períodos curtos, porque a sobrecarga de dormir, manter a fila de espera e acordar
pode facilmente superar o tempo total de espera de bloqueio.
n Como um thread de execução é suspenso na contenção de bloqueio, os semáforos devem
ser mantidos apenas no contexto do processo, pois o contexto de interrupção não pode ser
agendado.
n Você pode (embora você não queira) dormir enquanto segura um semáforo be-
cause você não vai travar quando outro processo adquire o mesmo semáforo. (Ele vai
apenas ir dormir e, eventualmente, deixá-lo continuar.)
n Você não pode manter um bloqueio de giro enquanto adquire um semáforo,
porque talvez tenha que dormir enquanto espera pelo semáforo, e você não pode
dormir enquanto segura um bloqueio de giro.
Como atalho para criar o mutex mais comum, use o seguinte, onde,
novamente, name é o nome da variável do semáforo binário:
Uso de Semáforos
A função down_interruptible() tenta adquirir o semáforo dado. Se o semáforo não estiver
disponível, ele colocará o processo de chamada para dormir no estado
TASK_INTERRUPTIBLE. Recorde-se do capítulo 3 que este estado de processo implica que
uma tarefa pode ser despertada com um sinal, o que é geralmente uma coisa boa. Se a
tarefa recebe um sinal enquanto espera pelo semáforo, ela é despertada e
down_interruptible() retorna -EINTR. Como alternativa, a função down() coloca a tarefa no estado
TASK_UNINTERRUPTIBLE quando ela é colocada em espera.Provavelmente você não deseja
isso porque o processo que está aguardando o semáforo não responde aos
sinais.Portanto, o uso de TASK_UNINTERRUPTIBLE() é muito mais comum (e correto) do
queTASK_UNINTERRUPTIBLE().
Você pode usar down_trylock() para tentar adquirir o semáforo fornecido sem
bloquear. Se o semáforo já estiver retido, a função imediatamente retornará
um valor diferente de zero. Caso contrário, retorna zero e você mantém o
bloqueio com êxito.
Para liberar um determinado semáforo, chame up(). Considere um exemplo:
Quadro
10.6 Métodos de semáforo (continuação)
Método Descrição
down_interruptible (struct semaphore *) Tenta adquirir o semáforo dado e entrar em
sono interruptível se for contestado
Semáforos Leitor-Escritor
Semáforos, como bloqueios de spin, também vêm em um sabor leitor-
escritor.As situações em que os semáforos leitor-escritor são preferidos em
relação aos semáforos padrão são os mesmos que com bloqueios de spin leitor-
escritor versus bloqueios de spin padrão.
Os semáforos leitor-gravador são representados pelo tipo struct rw_semaphore, que
é declarado em <linux/rwsem.h>. Os semáforos leitor-gravador declarados
estaticamente são criados através do seguinte, onde name é o nome declarado do
novo semáforo:
Mutexes
Até recentemente, a única trava dormente no núcleo era o semáforo. A maioria dos usuários
de sem-áforos instanciaram um semáforo com uma contagem de um e os trataram como um
bloqueio de exclusão mútua - uma versão adormecida do bloqueio de rotação. Infelizmente,
os semáforos são bastante genéricos e não impõem muitas restrições de uso.Isso os torna
úteis para gerenciar o acesso exclusivo em situações obscuras, como danças complicadas
entre o kernel e o espaço do usuário. Mas isso também significa que um bloqueio mais
simples é mais difícil de fazer, e a falta de regras impostas torna impossível qualquer tipo de
depuração automatizada ou imposição de restrições. Buscando uma trava de dormir mais
simples, os desenvolvedores do kernel introduziram o mutex.Yes, como você está
acostumado agora, que é um nome confuso. Vamos esclarecer.O termo "mutex" é um nome
genérico para se referir a qualquer cadeado para dormir que imponha exclusão mútua, como
um semáforo com uma contagem de um us-age. Nos kernels Linux recentes, o substantivo
adequado "mutex" é agora também um tipo específico de bloqueio de sono que implementa
exclusão mútua. Ou seja, um mutex é um mutex.
O mutex é representado pelo mutex struct. Ele se comporta de forma
semelhante a um semáforo com uma contagem de um, mas tem uma interface
mais simples, desempenho mais eficiente e restrições adicionais sobre seu
uso.Para definir estaticamente um mutex, você faz o seguinte:
Variáveis de conclusão
O uso de variáveis de conclusão é uma maneira fácil de sincronizar entre duas tarefas no
kernel quando uma tarefa precisa sinalizar à outra que um evento ocorreu. Uma tarefa
aguarda a variável de conclusão enquanto outra tarefa executa algum trabalho.Quando a
outra tarefa tiver concluído o trabalho, ela usará a variável de conclusão para ativar
qualquer tarefa em espera. Se você acha que isso soa como um semáforo, você está certo -
a ideia é muito a mesma. Na verdade, as variáveis de conclusão simplesmente fornecem
uma solução simples para um problema cuja resposta é ambos os semáforos. Por exemplo,
a chamada do sistema vfork() usa variáveis de conclusão para ativar o processo pai quando
o processo filho é executado ou encerrado.
Variáveis de conclusão são representadas pelo tipo de conclusão de
struct, que é definido em <linux/completion.h>.Uma variável de conclusão
criada estaticamente é criada e inicializada via
www.it-ebooks.info
198 Capítulo 10 Métodos de sincronização do kernel
www.it-ebooks.info
BKL: A Grande Fechadura do Kernel
Bloqueios Sequenciais
O bloqueio sequencial, geralmente abreviado para seq lock, é um tipo mais novo
de bloqueio introduzido no kernel 2.6. Ele fornece um mecanismo simples para ler
e gravar dados compartilhados. Ele funciona mantendo um contador de
sequências.Sempre que os dados em questão são gravados, um bloqueio é obtido
e um número de sequência é incrementado. Antes e depois de ler os dados, o
número de sequência é lido. Se os valores forem os mesmos, uma gravação não
terá início no meio da leitura. Além disso, se os valores forem pares, uma gravação
não está em andamento. (Agarrar o bloqueio de gravação torna o valor ímpar,
enquanto liberá-lo o torna par porque o bloqueio começa em zero.)
Para definir um bloqueio de seq:
O caminho de gravação é
Bloqueios de seq são úteis para fornecer um bloqueio leve e escalável para uso
com muitos leitores e alguns escritores. Bloqueios de seq, no entanto, favorecem
os escritores em detrimento dos leitores.Uma aquisição do bloqueio de write
sempre tem êxito, desde que não haja outros escritores. Os leitores não afetam o
bloqueio de gravação, como é o caso de bloqueios de rotação de leitor-gravador e
semáforos. Além disso, os escritores pendentes continuamente fazem com que o
loop de leitura (o exemplo anterior) se repita, até que não haja mais escritores
segurando o bloqueio.
Bloqueios de seq são ideais quando suas necessidades de bloqueio atendem a
maioria ou todos estes requisitos: