Você está na página 1de 193

Mapas 101

número de identificação (UID) para um ponteiro. Além de fornecer as três


principais operações de mapa, a implementação do Linux também inclui
uma operação de alocação sobre a operação de adição. Essa operação
de alocação não apenas adiciona um par UID/valor ao mapa, mas também
gera o UID.
A estrutura de dados idr é usada para mapear UIDs de espaço de usuário,
como descritores de observação inotify ou IDs de temporizador POSIX, para sua
estrutura de dados do kernel associada, como as estruturas inotify_watch ou k_itimer,
respectivamente. Seguindo o esquema de nomes obscuros e confusos do kernel do
Linux, este mapa é chamado de idr.

Inicializando uma idr


Configurar um idr é fácil. Primeiro você define estaticamente ou aloca
dinamicamente uma estrutura idr. Em seguida, chama idr_init():

Por exemplo:

Alocando um Novo UID


Depois de configurar um idr, você pode alocar um novo UID, que é um
processo de duas etapas. Primeiro você diz ao idr que você deseja alocar um
novo UID, permitindo que ele redimensione a árvore de apoio conforme
necessário.Em seguida, com uma segunda chamada, você realmente solicita o
novo UID.Essa complicação existe para permitir que você execute o
redimensionamento inicial, que pode exigir uma alocação de memória, sem um
lock.We discutir alocações de memória no Capítulo 12 e travamento nos
Capítulos 9 e 10. Por enquanto, vamos nos concentrar no uso de idr sem nos
preocupar com a maneira como lidamos com o bloqueio.
A primeira função, para redimensionar a árvore de apoio, é idr_pre_get():

Essa função, se necessária para atender a uma nova alocação de UID,


redimensionará o idr apontado pelo idp. Se um redimensionamento for necessário,
a alocação de memória usará os sinalizadores gfp gfp_mask (os sinalizadores gfp
são discutidos no Capítulo 12). Você não precisa sincronizar o acesso simultâneo
a esta chamada. Invertido de quase todas as outras funções no kernel, idr_pre_get()
retorna um em sucesso e zero em erro - cuidado!
A segunda função, para obter realmente um novo UID e adicioná-lo ao idr, é
idr_get_new():

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

Vejamos um exemplo completo:

Se for bem-sucedido, este fragmento obterá um novo UID, que é


armazenado no id inteiro e mapeia esse UID para ptr (que não definimos
no fragmento).
A função idr_get_new_above() permite que o chamador especifique um
valor mínimo de UID para retornar:

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():

Uma chamada bem-sucedida para essa função retorna o ponteiro


associado ao id do UID no idr apontado pelo idp. Em caso de erro, a função
retorna NULL. Note se você mapeou NULL para um UID com idr_get_new() ou
idr_get_new_above(), esta função retorna com sucessoNULL, não dando
a você nenhuma maneira de distinguir sucesso de falha.
Consequentemente, você não deve mapear UIDs para NULL.
www.it-ebooks.info
Árvores Binárias

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():

Você chamaria idr_remove_all() no idr apontado por idp antes de


chamar a função ?

Á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

Árvores de Pesquisa Binárias


Uma árvore de pesquisa binária (normalmente abreviada como BST) é uma árvore
binária com uma ordenação específica imposta a seus nós. A ordenação é
frequentemente definida por meio da seguinte indução:

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.

n Todas as subárvores também são árvores de busca binárias.


Uma árvore de pesquisa binária é, portanto, uma árvore binária na qual todos os nós são
ordenados de tal forma que os filhos esquerdos são menores que seus pais em valor e os
filhos direitos são maiores que seus pais. Consequentemente, tanto a busca por um
determinado nó como a travessia em ordem são eficientes (loga-ritmico e linear,
respectivamente). Consulte a Figura 6.7 para ver um exemplo de árvore de pesquisa binária.

7. º

3.

1 5.

4. 6.

15

Figura 6.7 Uma árvore de pesquisa binária (BST).


www.it-ebooks.info
Árvores Binárias

Árvores de pesquisa binárias com autoequilíbrio


A profundidade de um nó é medida pela quantidade de nós pai na raiz.Os nós na "parte inferior"
da árvore — aqueles sem filhos — são chamados folhas.A altura de uma árvore é a profundidade
do nó mais profundo da árvore.Uma árvore de pesquisa binária balanceada é uma árvore de
pesquisa binária na qual a profundidade de todas as folhas difere em no máximo uma (veja a
Figura 6.8).Uma árvore de pesquisa binária de balanceamento automático é uma árvore de
pesquisa binária que tenta, como parte de suas operações normais, permanecer (semi)
balanceada.

11.

7. º 18º

3. 9. 16º 42º

8 10.

Figura 6.8 Uma árvore de pesquisa binária equilibrada.

Á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

Se as operações de inserção e remoção aplicarem essas seis propriedades, a árvore


permanecerá semibalanceada. Agora, pode parecer estranho exigir insert e remove
para manter essas propriedades específicas.Por que não implementar as operações de
forma que apliquem outras regras mais simples que resultem em uma árvore
balanceada? Acontece que essas propriedades são relativamente fáceis de aplicar
(embora complexas de implementar), permitindo inserir e remover para guaran-tee uma
árvore semibalanceada sem sobrecarga adicional onerosa.
Descrever como inserir e remover impõe essas regras está além do escopo
deste livro. Embora se trate de regras simples, a sua aplicação é complexa.
Qualquer bom livro de ensino de estruturas de dados de nível de graduação
deve dar um tratamento completo.

Á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:

Os nós individuais em uma rbtree são representados pela estrutura do rb_node.


Dado um
rb_node, podemos mover para seu filho esquerdo ou direito seguindo os ponteiros do
nó do
mesmo nome.
A implementação de rbtree não fornece rotinas de pesquisa e inserção. Usuários
de rbtrees são esperados para definir seus próprios. Isto é porque C não faz pro-
gramming genérico fácil, e os desenvolvedores do kernel Linux acreditavam que a
maneira mais eficiente para implantar a pesquisa e inserção era exigir que cada
usuário fizesse isso manualmente, usando as funções auxiliares do rbtree
fornecidas, mas seus próprios operadores de comparação.
A melhor maneira de demonstrar pesquisa e inserção é mostrar um exemplo
real. Primeiro, vamos analisar a pesquisa.A função a seguir implementa uma
pesquisa do cache de páginas do Linux por um bloco de um arquivo
(representado por um par de inode e offset). Cada inode tem seu próprio rbtree,
digitado fora dos deslocamentos de página em file.This função, assim, procura
o rbtree do inode dado para um valor de deslocamento correspondente:
www.it-ebooks.info
Árvores Binárias

Neste exemplo, o loop while percorre a rbtree, percorrendo, conforme necessário, o


filho à esquerda ou à direita na direção do deslocamento determinado.As instruções if
e else implementam a função de comparação da rbtree, aplicando, assim, a ordenação
da árvore. Se o loop encontrar um nó com um deslocamento correspondente, a
pesquisa será concluída e a função retornará a estrutura de página associada. Se o loop
atingir o final da rbtree sem encontrar uma correspondência, não existe uma na árvore
e a função retorna NULL.
A inserção é ainda mais complicada porque implementa a lógica de
pesquisa e de inserção. O seguinte não é uma função trivial, mas se você
precisa implementar seu próprio insert rou-tine, este é um bom guia:

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

insere o novo nó no local determinado. é então chamado para


executar o

dança de rebalanceamento complicada.A função retornará NULL se a


página tiver sido adicionada ao cache de páginas e o endereço de uma
estrutura de páginas existente se a página já estiver no cache.

Qual Estrutura de Dados Usar, Quando


Até agora, discutimos quatro das estruturas de dados mais importantes do
Linux: listas vinculadas, filas, mapas e árvores rubro-negras. Nesta seção,
abordamos algumas dicas para ajudá-lo a decidir qual estrutura de dados
usar em seu próprio código.
Se seu método de acesso principal estiver iterando todos os seus dados, use uma lista
vinculada. Intuitivamente, nenhuma estrutura de dados pode fornecer melhor do que a
complexidade linear ao visitar cada elemento, portanto, você deve favorecer a estrutura de
dados mais simples para esse trabalho simples.Além disso, considere listas vinculadas
quando o desempenho não é importante, quando você precisa armazenar um número
relativamente pequeno de itens ou quando você precisa fazer interface com outro código do
kernel que usa listas vinculadas.
Se o seu código segue o padrão produtor/consumidor, use uma fila,
particularmente se você quiser (ou puder lidar com) um buffer de tamanho fixo. As
filas tornam a adição e a remoção de itens simples e eficientes e fornecem semântica
first-in, first-out (FIFO), que é o que a maioria dos casos de uso de
produtores/consumidores exige. Por outro lado, se você precisar armazenar um
número desconhecido e potencialmente grande de itens, uma lista vinculada pode
fazer mais sentido, porque você pode adicionar dinamicamente qualquer número de
itens à lista.
Se você precisar mapear um UID para um objeto, use um mapa. Os mapas tornam
esses mapeamentos fáceis e eficientes e também mantêm e alocam o UID para você.
Entretanto, a interface de mapas do Linux, sendo específica para mapeamentos UID-
para-ponteiro, não é boa para muito mais. Se você estiver lidando com descritores
distribuídos para o espaço do usuário, considere esta opção.
Se você precisar armazenar uma grande quantidade de dados e pesquisá-los
eficientemente, considere uma árvore vermelho-preto. Árvores rubro-negras
permitem a pesquisa em tempo logarítmico, ao mesmo tempo em que fornecem um
tempo linear eficiente em ordem transversal.Embora a implementação seja mais
complicada do que as outras estruturas de dados, seu espaço na memória não é
significativamente pior. Se você não estiver realizando muitas operações de
pesquisa críticas ao tempo, uma árvore rubro-negra provavelmente não é sua
melhor aposta. Nesse caso, favoreça uma lista vinculada.
Nenhuma dessas estruturas de dados atende às suas necessidades? O kernel
implementa outras estruturas de dados raramente usadas que podem atender às suas
necessidades, como árvores radix (um tipo de trie) e bitmaps. Somente depois de esgotar
todas as soluções fornecidas pelo kernel você deve considerar "rolar sua própria"
estrutura de dados. Uma estrutura de dados comum frequentemente implementada em
arquivos de origem individuais é a tabela de hash. Como uma tabela de hash é pouco mais
do que alguns buckets e uma função de hash, e a função de hash é tão específica para
cada caso de uso, há pouco valor em fornecer uma solução em todo o kernel em uma
linguagem de programação não genérica como C.

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

onde y é o tempo necessário para contar as x pessoas.

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'

Em inglês, o tempo para completar f(x) é sempre menor ou igual ao


tempo para com-completar g(x) multiplicado por alguma constante
arbitrária, contanto que o input x seja maior que algum valor inicial.
Essencialmente, você está procurando por uma função cujo
comportamento é tão ruim quanto ou pior que o algoritmo. Você pode, então,
olhar para o resultado de grandes entradas para esta função e obter uma
compreensão do limite do seu algoritmo.

Notação Teta Grande


Quando a maioria das pessoas fala sobre a notação big-o, elas estão se referindo mais
precisamente ao que Donald Knuth descreve como notação big-theta. Tecnicamente, a
notação big-o se refere a uma notação
www.it-ebooks.info
110 Capítulo 6 Estruturas de dados do kernel

vinculado. Por exemplo, 7 é um limite superior de 6; assim como 9, 12 e 65.


Subsequentemente, quando a maioria das pessoas discute o crescimento
da função, elas falam sobre o limite mínimo superior, ou uma função que
modela os limites superior e inferior.2 O professor Knuth, o pai do campo
de análise algorítmica, descreve isso como notação big-theta e dá a
seguinte definição:

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.

Quadro 6.1 Tabela de Valores Comuns de Complexidade de Tempo


O(g(x)) Nome
1 Constante (escalabilidade perfeita)
log-n Logarítmica

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

Qual é a complexidade de apresentar a todos na sala? Qual é a possível função que


modela esse algoritmo? Se levasse 30 segundos para apresentar cada pessoa, quanto
tempo levaria para apresentar 10 pessoas uma à outra? Que tal 100 pessoas para cada
uma? Entender como um algoritmo funciona, já que tem cada vez mais trabalho a fazer,
é um componente crucial na determinação do melhor algoritmo para um determinado
trabalho.
É claro que é sábio evitar complexidades como O(n!) ou O(2 Da mesma forma, é
n).

usualmente uma melhoria para substituir um algoritmo O(n) por um O(log


n) algorítmo.Porém, nem sempre é esse o caso, e uma suposição cega não deve ser feita
com base apenas na notação big-o. Lembre-se que, dado O(g(x)), há uma constante, c,
multiplicada por (x).Portanto, é possível que um O(1)algoritmo leve 3 horas para ser
concluído. Claro, é sempre 3 horas, independentemente de quão grande a entrada, mas que
ainda pode ser um longo tempo comparado a um O(n) algoritmo com poucas entradas.O
tamanho de entrada típico deve sempre ser levado em conta ao comparar algoritmos.
Favoreça algoritmos menos complexos, mas tenha em mente a
sobrecarga do algoritmo em relação ao tamanho de entrada típico. Não
otimize cegamente para um nível de escalabilidade que você nunca
precisará suportar!

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

Uma responsabilidade central de qualquer kernel de sistema operacional é gerenciar o


hardware conectado à máquina — discos rígidos e discos Blu-ray, teclados e mouses,
processadores 3D e rádios sem fio.Para atender a essa responsabilidade, o kernel precisa
se comunicar com os dispositivos individuais da máquina. Dado que os processadores
podem ser pedidos de magnitude mais rápido do que o hardware com o qual eles se
comunicam, não é ideal para o kernel emitir uma solicitação e esperar por uma resposta do
hardware significativamente mais lento. Em vez disso, como o hardware é relativamente
lento para responder, o kernel deve ser livre para ir e lidar com outros trabalhos, lidando
com o hardware apenas depois que o hardware tiver realmente concluído seu trabalho.
Como o processador pode trabalhar com hardware sem afetar o desempenho
geral da máquina? Uma resposta a essa pergunta é a pesquisa. Periodicamente, o
kernel pode verificar o status do hardware no sistema e responder adequadamente.
Entretanto, a pesquisa incorre em sobrecarga porque ela deve ocorrer
repetidamente, independentemente de o hardware estar ativo ou pronto.Uma
solução melhor é fornecer um mecanismo para que o hardware sinalize para o
kernel quando a atenção for necessária.Esse mecanismo é chamado de interrupção.
Neste capítulo, discutimos as interrupções e como o kernel responde a elas, com
funções especiais chamadas 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

conecta várias linhas de interrupção em uma única linha ao processador. Ao


receber uma interrupção, o controlador de interrupção envia um sinal ao
processador.O processador detecta esse sinal e interrompe sua execução atual
para lidar com a interrupção.O processador pode, então, notificar o sistema
operacional de que ocorreu uma interrupção e o sistema operacional pode lidar
com a interrupção adequadamente.
Diferentes dispositivos podem ser associados a diferentes interrupções por meio de
um valor único associado a cada interrupção.Dessa forma, as interrupções do teclado
são distintas das interrupções do disco rígido.Isso permite que o sistema operacional
diferencie entre interrupções e saiba qual dispositivo de hardware causou cada
interrupção. Por sua vez, o sistema operacional pode atender cada interrupção com
seu manipulador correspondente.
Esses valores de interrupção são geralmente chamados de linhas de solicitação de
interrupção (IRQ). A cada linha de IRQ é atribuído um valor numérico; por exemplo, no PC
clássico, IRQ zero é a interrupção do temporizador e IRQ one é a interrupção do teclado.
Nem todos os números de interrupção, no entanto, são tão rigidamente definidos. As
interrupções associadas a dispositivos no barramento PCI, por exemplo, geralmente são
atribuídas dinamicamente. Outras arquiteturas não-PC têm atribuições dinâmicas
semelhantes para valores de interrupção.A noção importante é que uma interrupção
específica é associada a um dispositivo específico, e o kernel sabe disso.O hardware então
emite interrupções para chamar a atenção do kernel: Ei, tenho novas teclas pressionando!
Leia e processe esses meninos maus!

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

eles são funções comuns. O que diferencia os manipuladores de interrupção de outras


funções do kernel é que o kernel os chama em resposta a interrupções e que eles são
executados em um contexto especial (discutido mais adiante neste capítulo) chamado
contexto de interrupção. Este contexto especial é ocasionalmente chamado contexto
atômico porque, como veremos, o código em execução neste contexto é incapaz de
bloquear. Neste livro, usaremos o termo contexto de interrupção.
Como uma interrupção pode ocorrer a qualquer momento, um manipulador de
interrupção pode, por sua vez, ser cortado por exe a qualquer momento. É imperativo que
o manipulador seja executado rapidamente, para retomar a execução do código
interrompido o mais rápido possível.Portanto, embora seja importante para o hardware
que o sistema operacional atenda à interrupção sem demora, também é importante para o
resto do sistema que o manipulador de interrupção seja executado no menor período
possível.
No mínimo, o trabalho de um manipulador de interrupções é confirmar o recebimento
da interrupção para o hardware: Ei, hardware, eu ouço você; agora volte ao trabalho!
Muitas vezes, no entanto, os han-dlers de interrupção têm uma grande quantidade de
trabalho para executar. Por exemplo, considere o manipulador de interrupção para um
dispositivo de rede. Além de responder ao hardware, o manipulador de interrupção
precisa copiar pacotes de rede do hardware para a memória, processá-los e enviá-los
para a pilha de protocolos ou aplicativo apropriado. Obviamente, isso pode ser muito
trabalhoso, especialmente com as placas Gigabit e Ethernet de 10 gigabits atuais.

Metades Superiores Versus Metades Inferiores


Esses dois objetivos — que um manipulador de interrupções executa rapidamente e
executa uma grande quantidade de trabalho — claramente conflitam entre si. Por
causa desses objetivos concorrentes, o processamento de interrupções é dividido em
duas partes, ou metades.O manipulador de interrupções é a metade superior. A
metade superior é executada imediatamente após o recebimento da interrupção e
executa somente o trabalho crítico em termos de tempo, como confirmar o
recebimento da interrupção ou redefinir o hardware.O trabalho que pode ser executado
posteriormente é adiado até a metade inferior.A metade inferior é executada no futuro,
em um momento mais conveniente, com todas as interrupções habilitadas. O Linux
fornece vários mecanismos para implementar as metades inferiores, e todos eles são
discutidos no Capítulo 8, "Metades inferiores e adiamento do trabalho".
Vejamos um exemplo da dicotomia da metade superior/inferior, usando nosso
velho amigo, a placa de rede.Quando as placas de rede recebem pacotes da rede,
elas precisam alertar o kernel de sua disponibilidade.Elas querem e precisam fazer
isso imediatamente, para otimizar o throughput e a latência da rede e evitar
timeouts.Assim, elas imediatamente emitem uma interrupção: Ei, kernel, tenho
alguns pacotes novos aqui! O kernel responde executando a interrupção registrada
da placa de rede.
A interrupção é executada, reconhece o hardware, copia os novos pacotes de rede na
memória principal e prepara a placa de rede para mais pacotes.Esses trabalhos são o
trabalho importante, crítico em termos de tempo e específico de hardware.O kernel
geralmente precisa copiar rapidamente o pacote de rede na memória principal porque o
buffer de dados de rede na placa de rede é fixo e de tamanho mínimo, especialmente em
comparação à memória principal. Atrasos na cópia dos pacotes podem resultar em
saturação de buffer, com os pacotes recebidos sobrecarregando o buffer da placa de rede
e, portanto, os pacotes sendo descartados.Depois que os dados de rede estiverem com
segurança na memória principal, o trabalho da interrupção será realizado e poderá

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.

Registrando um manipulador de interrupção


Os manipuladores de interrupção são de responsabilidade do driver que
gerencia o hardware. Cada dispositivo tem um driver associado e, se esse
dispositivo usa interrupções (e a maioria usa), esse driver deve registrar um
manipulador de interrupções.
Os drivers podem registrar um manipulador de interrupção e habilitar uma
determinada linha de interrupção para manipulação com a função request_irq(),
que é declarada em <linux/interrupt.h>:

O primeiro parâmetro, irq, especifica o número de interrupção a ser


alocado. Para alguns dispositivos, por exemplo dispositivos de PC
herdados, como o temporizador do sistema ou o teclado, esse valor é
normalmente codificado. Para a maioria dos outros dispositivos, é
sondado ou determinado de outra forma programática e dinamicamente.
O segundo parâmetro, handler, é um ponteiro de função para o handler
de interrupção real que serve esta interrupção. Esta função é chamada
sempre que o sistema operacional recebe a interrupção.

Observe o protótipo específico da função do handler: Ela usa dois parâmetros e


tem um valor de retorno de irqreturn_t. Esta função é discutida posteriormente neste
capítulo.

Sinalizadores de Manipulador de Interrupção


O terceiro parâmetro, flags, pode ser zero ou uma máscara de bit de um ou mais
dos flags definidos em <linux/interrupt.h>.Entre esses flags, os mais importantes
são
n IRQF_DISABLED — Quando definido, esse flag instrui o kernel a desativar todas as
interrupções ao executar esse manipulador de interrupções. Quando desdefinido, os manipuladores de
interrupções são executados com todas as interrupções, exceto a própria habilitada. A maioria dos
manipuladores de interrupção não define esse sinalizador, pois desabilitar todas as interrupções é uma
forma incorreta. Seu uso é reservado para interrupções sensíveis ao desempenho que são executadas
rapidamente. Esse sinalizador é a manifestação atual do sinalizador SA_INTERRUPT, que no passado
distinguia as interrupções "rápidas" e "lentas".
n IRQF_SAMPLE_RANDOM — Este flag especifica que as interrupções geradas por este
dispositivo devem contribuir para o pool de entropia do kernel. O pool de entropia do kernel fornece
números verdadeiramente aleatórios derivados de vários eventos aleatórios. Se esse sinalizador for
especificado, a sincronização das interrupções desse dispositivo será alimentada no pool como
entropia. Não definir

www.it-ebooks.info
117ºRegistrando um manipulador de
interrupção

isso se o seu dispositivo apresentar interrupções a uma taxa previsível (por


n IRQF
exemplo, o temporizador do sistema) ou se puder ser influenciado por invasores
_TIMER—
externos (por exemplo, um dispositivo de rede). Por outro lado, a maioria dos
outros hardwares gera interrupções em momentos não-dissuasores e, portanto, é Este flag
uma boa fonte de entropia.
especifica que este handler processa interrupções para o timer do sistema.
n IRQF_SHARED—Este flag especifica que a linha de interrupção pode ser
compartilhada entre handlers de interrupção múltiplos. Cada manipulador registrado em
uma determinada linha deve especificar este sinalizador; caso contrário, somente um
manipulador pode existir por linha. Mais informações sobre manipuladores
compartilhados são fornecidas em uma seção a seguir.
O quarto parâmetro, name, é uma representação de texto ASCII do
dispositivo associado à interrupção. Por exemplo, esse valor para a
interrupção do teclado em um PC é o teclado. Esses nomes de texto são
usados por /proc/irq e /proc/interrupts para comunicação com o usuário, o que
será discutido em breve.
O quinto parâmetro, dev, é usado para linhas de interrupção compartilhadas.Quando
um manipulador de interrupção é liberado (discutido posteriormente), dev fornece um
cookie exclusivo para permitir a remoção apenas do manipulador de interrupção
desejado da linha de interrupção.Sem esse parâmetro, seria impossível para o kernel
saber qual manipulador remover em uma determinada linha de interrupção.Você pode
passar NULL aqui se a linha não for compartilhada, mas você deve passar um cookie
exclusivo se a linha de interrupção for compartilhada. (E a menos que o seu dispositivo
seja velho e crocante e viva no ônibus ISA, há uma boa chance de que ele deve suportar
o compartilhamento.) Esse ponteiro também é passado para o manipulador de
interrupção em cada chamada.Uma prática comum é passar a estrutura do dispositivo
do driver:Esse ponteiro é exclusivo e pode ser útil ter dentro dos manipuladores.
Em caso de sucesso, request_irq() retorna zero. Um valor diferente de zero
indica um erro e, nesse caso, o manipulador de interrupção especificado
não foi registrado.Um erro comum é -EBUSY, que indica que a linha de
interrupção fornecida já está em uso (e que o usuário atual ou você não
especificou IRQF_SHARED).
Note que request_irq() pode dormir e portanto não pode ser chamada do
contexto de interrupção ou outras situações onde o código não pode
bloquear. É um erro comum chamar request_irq() quando não é seguro dormir.
Isto é em parte por causa do porquê request_irq() pode bloquear: É realmente
incerto. No registro, uma entrada correspondente à interrupção é criada em
/proc/irq.A função proc_mkdir() cria novas entradas procfs.Esta função chama a
função proc_create() para configurar as novas entradas procfs, que, por sua vez,
chama kmalloc() para alocar memória. Como você verá no Capítulo
12,"Gerenciamento de memória" kmalloc() pode dormir. Então aí está!

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

Neste exemplo, irqn é a linha de interrupção solicitada; my_interrupt é o


manipulador; especificamos via sinalizadores que a linha pode ser
compartilhada; o dispositivo é nomeado my_device ; e passamos por my_dev para
devinicialização. Na falha, o código imprime um erro e retorna. Se a chamada
retornar zero, o manipulador foi instalado com êxito. A partir desse ponto, o
manipulador é chamado em resposta a uma interrupção. É importante inicializar
o hardware e registrar um manipulador de interrupção na ordem correta para
evitar que o manipulador de interrupção seja executado antes que o dispositivo
seja totalmente inicializado.

Liberando um manipulador de interrupção


Quando o driver for descarregado, você precisará cancelar o registro do
manipulador de interrupção e possivelmente desabilitar a linha de
interrupção.Para fazer isso, chame

Se a linha de interrupção especificada não for compartilhada, esta função removerá o


manipulador e
Ativa a linha. Se a linha de interrupção for compartilhada, o manipulador identificado via
dev será removido, mas
a linha de interrupção é desabilitada somente quando o último manipulador é removido.
Agora você pode ver por quê
um dispositivo exclusivo é importante.Com as linhas de interrupção compartilhadas, um
cookie exclusivo é necessário para
ferenciar entre os vários manipuladores que podem existir em uma única linha e
habilitar
free_irq() para remover apenas o manipulador correto. Em ambos os casos
(compartilhado ou não compartilhado), se dev
for non-NULL, ele deve corresponder ao handler desejado.Uma chamada para free_irq()
deve ser feita de
contexto do processo.
A Tabela 7.1 analisa as funções para registrar e cancelar o registro de um manipulador
de interrupção.

Quadro 7.1 Métodos de registro de interrupção


Função Descrição
Registra um determinado manipulador de interrupção em uma
request_irq () determinada linha de interrupção.
Cancele o registro de determinado manipulador de interrupção; se
free_ irq () nenhum manipulador permanecer na
linha, a linha de interrupção fornecida é desativada.

Escrevendo um manipulador de interrupção


O seguinte é uma declaração de um manipulador de interrupção:
Note que esta declaração corresponde ao protótipo do argumento do manipulador
dado a request_irq().O primeiro parâmetro,_irq, é o valor numérico da linha de
interrupção que o manipulador está servindo.Este valor é passado para o manipulador,
mas não é usado com muita frequência, exceto na impressão de mensagens de log.
Antes da versão 2.0 do kernel do Linux, não havia um parâmetro dev e, portanto, irq foi
usado para diferenciar entre vários dispositivos usando o

www.it-ebooks.info
Escrevendo um manipulador de interrup

mesmo driver e, portanto, o mesmo manipulador de interrupção.Como


exemplo disso, considere um computador com vários controladores de disco
rígido do mesmo tipo.
O segundo parâmetro, dev, é um ponteiro genérico para o mesmo dev que foi dado
para a função ? Se esse valor for exclusivo (que é necessário para suportar o
compartilhamento), ele poderá agir como um cookie para diferenciar entre vários
dispositivos que potencialmente usam o mesmo manipulador de interrupção. dev
também pode apontar para uma estrutura de uso para o manipulador de interrupção.
Como a estrutura do dispositivo é exclusiva de cada dispositivo e potencialmente útil para
ter dentro do manipulador, ela é normalmente passada para dev.
O valor de retorno de um manipulador de interrupção é o tipo especial irqreturn_t. Um
manipulador de interrupção pode retornar dois valores especiais, IRQ_NONE ou
IRQ_HANDLED.O primeiro é retornado quando o manipulador de interrupção detecta uma
interrupção para a qual seu dispositivo não era o origina-tor.O último é retornado se o
manipulador de interrupção foi chamado corretamente, e seu dispositivo de fato causou
a interrupção.Alternativamente, IRQ_RETVAL(val) pode ser usado. Se val for diferente de
zero, esta macro retornará IRQ_HANDLED. Caso contrário, a macro retornará
IRQ_NONE.Esses valores especiais são usados para permitir que o kernel saiba se os
dispositivos estão emitindo interrupções artificiais (ou seja, não solicitadas). Se todos
os manipuladores de interrupção em uma determinada linha de interrupção retornarem
IRQ_NONE, o kernel poderá detectar o problema. Observe o tipo de retorno curioso, irqreturn_t,
que é simplesmente um int.Este valor fornece compatibilidade com versões anteriores com
kernels anteriores, que não tinham esse recurso; antes da versão 2.6, os manipuladores de
interrupção retornavam Os drivers podem simplesmente digitar irqreturn_t para void e definir
os diferentes valores de retorno para no-ops e, em seguida, trabalhar em 2.4 sem mais
modificações.A interrupção han-dler é normalmente marcada como static porque nunca é
chamada diretamente de outro arquivo.
A função do manipulador de interrupção depende inteiramente do dispositivo e
de suas razões para emitir a interrupção.No mínimo, a maioria dos manipuladores
de interrupção precisa fornecer um segmento de confirmação ao dispositivo que
recebeu a interrupção. Os dispositivos mais complexos precisam enviar e receber
dados adicionalmente e executar trabalho estendido no manipulador de
interrupção. Como mencionado, o trabalho estendido é empurrado o máximo
possível para a metade inferior do manipulador, que é discutido no próximo
capítulo.

Manipuladores de reentrada e interrupção


Os manipuladores de interrupção no Linux não precisam ser reentrantes. Quando
um determinado manipulador de interrupção está em execução, a linha de
interrupção correspondente é mascarada em todos os processadores, impedindo
que outra interrupção na mesma linha seja recebida. Normalmente, todas as outras
interrupções são ativadas, de modo que outras interrupções são atendidas, mas a
linha atual está sempre desativada. Consequentemente, o mesmo manipulador de
interrupção nunca é chamado simultaneamente para atender a uma interrupção
aninhada. Isso simplifica muito a gravação do seu manipulador de interrupção.

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

exclusivo e potencialmente útil para o manipulador.Você não pode


passar NULL para um manipulador compartilhado!
n O manipulador de interrupção deve ser capaz de distinguir se seu dispositivo
realmente gerou uma interrupção. Isso requer suporte de hardware e lógica associada no
manipulador de interrupção. Se o hardware não oferecesse essa capacidade, não haveria
como o manipulador de interrupção saber se seu dispositivo associado ou algum outro
dispositivo compartilhando a linha causou a interrupção.
Todos os drivers que compartilham a linha de interrupção devem atender aos
requisitos anteriores. Se algum dispositivo não compartilha razoavelmente, nenhum
IRQF_COMPAR pode compartilhar a linha. Quando request_irq() é chamada com
TILHADO
especificado, a chamada será bem-sucedida somente se a linha de interrupção não estiver registrada no momento ou se todos os
manipuladores registrados na linha também tiverem especificado IRQF_SHARED. Os manipuladores compartilhados, entretanto, podem misturar o uso de
IRQF_DISABLED.

Quando o kernel recebe uma interrupção, ele chama sequencialmente cada


manipulador registrado na linha. Portanto, é importante que o manipulador
seja capaz de distinguir se gerou uma determinada interrupção. O manipulador
deve sair rapidamente se seu dispositivo associado não gerou a interrupção.
Isso requer que o dispositivo de hardware tenha um registro de status (ou
mecanismo semelhante) que o manipulador possa verificar. A maioria dos
hardwares realmente tem esse recurso.

Um manipulador de interrupção real


Vejamos um manipulador de interrupção real, do driver de relógio de tempo real
(RTC), encontrado em drivers/char/rtc.c.Um RTC é encontrado em muitas máquinas,
incluindo PCs. É um dispositivo, separado do temporizador do sistema, que ajusta o
relógio do sistema, fornece um alarme ou fornece um temporizador periódico. Na
maioria das arquiteturas, o relógio do sistema é ajustado gravando o tempo
desejado em um registro específico ou intervalo de E/S.Qualquer funcionalidade de
alarme ou temporizador periódico é normalmente implementada via interrupção.A
interrupção é equivalente a um alarme de relógio real:O recebimento da interrupção
é análogo a um alarme de zumbido.
Quando o driver RTC carrega, a função rtc_init() é chamada para
inicializar o driver. Uma de suas funções é registrar o operador de
interrupção:

Neste exemplo, a linha de interrupção é armazenada em rtc_irq. Essa variável é


definida como a interrupção RTC para uma determinada arquitetura. Em um PC,
o RTC está localizado em IRQ 8.O segundo parâmetro é o manipulador de
interrupção, rtc_interrupt, que está disposto a compartilhar a linha de interrupção
com outros manipuladores, graças ao sinalizador IRQF_SHARED. No quarto parâmetro,
você pode ver que o nome do driver é rtc. Como este dispositivo compartilha a
linha de interrupção, ele transmite um valor exclusivo por dispositivo para dev.
Finalmente, o próprio manipulador:

www.it-ebooks.info
Escrevendo um manipulador de
interrupção 121

Essa função é chamada sempre que a máquina recebe a interrupção do RTC.


Primeiro, observe as chamadas de bloqueio de rotação:O primeiro conjunto garante que
rtc_irq_data não seja acessado simultaneamente por outro processador em uma máquina
SMP, e o segundo conjunto protege rtc_callback do mesmo. Os bloqueios são discutidos
no Chapter 10,"Kernel Synchronization Methods."
A variável rtc_irq_data é uma longa não assinada que armazena informações sobre
o RTC e é atualizada em cada interrupção para refletir o status da
interrupção.
Em seguida, se um temporizador periódico RTC estiver definido, ele
será atualizado via mod_timer().Temporizadores são discutidos no
Chapter 11,"Timers and Time Management."
O pacote final de código, sob o comentário "agora faça o resto das ações",
executa uma possível função de retorno de chamada predefinida.O driver RTC
permite que uma função de retorno de chamada seja registrada e executada em
cada interrupção do RTC.
www.it-ebooks.info
122 Capítulo 7 Interrupções e manipuladores de interrupção

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çã

Seu manipulador de interrupção não deve se importar com a configuração da


pilha em uso ou com o tamanho da pilha do kernel. Use sempre uma quantidade
mínima absoluta de espaço da pilha.

Implementando manipuladores de interrupção


Talvez não seja surpreendente que a implementação do sistema de tratamento de
interrupções no Linux dependa da arquitetura.A implementação depende do
processador, do tipo de controlador de interrupção usado e do design da
arquitetura e da máquina.
A Figura 7.1 é um diagrama do caminho que uma interrupção toma pelo hardware e
pelo kernel.

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

Figura 7.1 O caminho que uma interrupção obtém do hardware e através do


kernel.

Um dispositivo emite uma interrupção enviando um sinal elétrico através de seu


barramento para o controlador de interrupção. Se a linha de interrupção estiver
habilitada (ela pode ser mascarada), o controlador de interrupção enviará a
interrupção ao processador. Na maioria das arquiteturas, isso é realizado por um
sinal elétrico enviado sobre um pino especial para o processador. A menos que as
interrupções sejam desativadas no processador (o que também pode acontecer), o
processador imediatamente interrompe o que está fazendo, desativa o sistema de
interrupção e salta para um local predefinido na memória e executa o código
localizado nele.Esse ponto predefinido é configurado pelo kernel e é o ponto de
entrada para os manipuladores de interrupção.
A jornada da interrupção no kernel começa nesse ponto de entrada
predefinido, assim como as chamadas do sistema entram no kernel por meio de
um manipulador de exceção predefinido. Para cada linha de interrupção, o
processador salta para um local exclusivo na memória e executa o código
localizado nele. Dessa maneira, o kernel conhece o número de IRQ da
interrupção de entrada. O ponto de entrada inicial simplesmente salva este valor
e armazena os valores de registro atuais (que pertencem à tarefa interrompida)
na pilha; então o kernel chama do_IRQ(). Daqui em diante, a maior parte do código
de manipulação de interrupções é escrito em C; no entanto, ele ainda depende
da arquitetura.

www.it-ebooks.info
124 Capítulo 7 Interrupções e manipuladores de interrupção

A função do_IRQ() é declarada como

Como a convenção de chamada C coloca argumentos de função na parte


superior da pilha, a estrutura pt_regs contém os valores de registro iniciais que
foram salvos anteriormente na rotina de entrada de assembly. Como o valor de
interrupção também foi salvo, do_IRQ() pode extraí-lo. Depois que a linha de
interrupção é calculada, do_IRQ() reconhece o recebimento da interrupção e desabilita
a entrega de interrupção na linha. Em computadores normais, estas operações são
manipuladas por mask_and_ack_8259A().
Em seguida, do_IRQ() garante que um handler válido seja registrado na
linha e que ele esteja habilitado e não sendo executado no momento. Se
sim, ele chama handle_IRQ_event(), definido em kernel/irq/handler.c, para executar os
manipuladores de interrupção instalados para a linha.
www.it-ebooks.info
Implementando manipuladores de interrupção

Primeiro, como o processador desativou as interrupções, elas são reativadas, a


IRQF_DESABILIT
ADO
menos que
IRQF_DESABILIT
ADO
foi especificado durante o registro do manipulador. Recorde-se que
especifica que o manipulador deve ser executado com interrupções desabilitadas. Em seguida, cada manipulador
potencial é executado em um loop. Se esta linha não for compartilhada, o loop termina após a primeira iteração. Caso contrário, todos os
manipuladores serão executados. Depois disso,

add_interrupt_randomness() é chamado se IRQF_SAMPLE_RANDOM foi especificado durante o


registro. Esta função usa a sincronização da interrupção para gerar entropia para o
gerador de números ran-dom. Finalmente, as interrupções são novamente desabilitadas
(do_IRQ() espera que elas ainda
www.it-ebooks.info
126 Capítulo 7 Interrupções e manipuladores de interrupção

estar desligado) e a função retorna. De volta em do_IRQ(), a função se


limpa e retorna ao ponto de entrada inicial, que então salta para
ret_from_intr().
A rotina ret_from_intr() é, como no código de entrada inicial, escrita em assembly. Essa
rotina verifica se há uma reprogramação pendente. (Lembre-se do Capítulo
4,"Programação do processo", que isso implica que need_resched está definido). Se uma
reprogramação estiver pendente e o kernel estiver retornando ao espaço do usuário (ou
seja, a interrupção interrompeu um processo do usuário), schedule() é chamado. Se o
kernel estiver retornando ao kernel-space (isto é, a interrupção interrompeu o próprio
kernel), schedule() é chamado somente se o preempt_count for zero. De outra forma, não é seguro
antecipar o kernel.After schedule() retorna, ou se não há trabalho pendente, os registros
iniciais são restaurados e o kernel retoma o que foi interrompido.
Em x86, as rotinas de montagem iniciais estão localizadas em
arch/x86/kernel/entry_64.S
(entry_32.S para x86 de 32 bits) e os métodos C estão localizados em
arch/x86/kernel/irq.c.
Outras arquiteturas compatíveis são semelhantes.

/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:

A primeira coluna é a linha de interrupção. Neste sistema, as interrupções numeradas 0-2,


4, 5, 12 e 15 estão presentes. Manipuladores não são instalados em linhas não exibidas.A
segunda coluna é um contador do número de interrupções recebidas.Uma coluna está
presente para cada processador no sistema, mas este computador tem apenas um
processador.Como você pode ver, a interrupção do temporizador recebeu 3.602.371
interrupções,2 enquanto a placa de som (EMU10K1) não recebeu nenhum (o que é uma
indicação de que não foi usada desde que o computador foi inicializado).A terceira coluna é
o controlador de interrupção que trata dessa interrupção. XT-PIC corresponde ao padrão

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

Controlador de interrupção programável do PC. Em sistemas com um APIC


de E/S, a maioria das interrupções listaria IO-APIC-level ou IO-APIC-edge como
seu controlador de interrupção. Finalmente, a última coluna é o dispositivo
associado a esta interrupção. Este nome é fornecido pelo parâmetro
devname para request_irq(), como discutido anteriormente. Se a interrupção
for compartilhada, como é o caso da interrupção número 4 neste exemplo,
todos os dispositivos registrados na linha de interrupção serão listados.
Para os curiosos, o código procfs está localizado principalmente em
fs/proc.A função que fornece /proc/interrupts é, sem surpresa, dependente da
arquitetura e nomeada
show_interrupts().

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.

Desativando e Ativando Interrupções


Para desativar as interrupções localmente para o processador atual (e
somente para o processador atual) e, em seguida, reativá-las mais tarde,
faça o seguinte:

Essas funções são normalmente implementadas como uma única operação


de montagem. (Claro, isso depende da arquitetura.) De fato, em x86,
local_irq_disable() é uma cli simples e local_irq_enable() é uma sti instrução simples.
sti . Em outras palavras, eles desativam e ativam a entrega de interrupção no
processador emissor.
www.it-ebooks.info
128 Capítulo 7 Interrupções e manipuladores de interrupção

A rotina local_irq_disable() é perigosa se as interrupções já estavam desabilitadas antes


de sua chamada. A chamada correspondente para local_irq_enable() habilita
incondicionalmente as interrupções, apesar do fato de que elas estavam desabilitadas
para começar. Em vez disso, é necessário um mecanismo para restaurar interrupções
para um estado anterior. Essa é uma preocupação comum porque um determinado
caminho de código no kernel pode ser alcançado com e sem interrupções habilitadas,
dependendo da cadeia de chamadas. Por exemplo, imagine que o trecho de código
anterior faça parte de uma função maior. Imagine que essa função é chamada por duas
outras funções, uma que desativa interrupções e outra que não. Como está ficando mais
difícil à medida que o kernel cresce em tamanho e complexidade para conhecer todos os
caminhos de código que levam a uma função, é muito mais seguro salvar o estado do
sistema de interrupção antes de desabilitá-lo.Em seguida, quando você estiver pronto
para reabilitar as interrupções, basta restaurá-las ao seu estado original:

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.

Não há mais cli() globais


O kernel anteriormente fornecia um método para desativar interrupções em todos os
processadores do sistema. Além disso, se outro processador chamasse esse
método, ele teria que esperar até que as interrupções fossem ativadas antes de
continuar. Esta função foi nomeada como cli() e a chamada enable correspondente
foi nomeada como sti() — muito centrada em x86, apesar de existir para todas as
arquiteturas. Essas interfaces foram preteridas durante o 2.5 e, consequentemente,
toda a sincronização de interrupções deve agora usar uma combinação de controle
de interrupção local e bloqueios de spin (discutidos no Capítulo 9, "An Introduction
to Kernel Synchronization"). Isso significa que o código que antes só precisava
desativar as interrupções globalmente para garantir o acesso mútuo exclusivo aos
dados compartilhados agora precisa trabalhar um pouco mais.
Anteriormente, os geradores de driver podiam assumir uma cli() usada em seus
manipuladores de interrupção e qualquer outro lugar onde os dados compartilhados
fossem acessados forneceria exclusão mútua. A chamada da cli() garantiria que
nenhum outro manipulador de interrupção (e, portanto, seu manipulador específico)
seria executado. Além disso, se outro processador entrasse em uma região protegida
por cli(), ele não continuaria até que o processador original saísse de sua região
protegida por cli() com uma chamada para cli().
Remover a cli() global tem algumas vantagens. Primeiro, força os roteiristas a implantar o
bloqueio real. Um bloqueio refinado com uma finalidade específica é mais rápido do que um
bloqueio global, que é efetivamente o que é cli(). Segundo, a remoção simplificou muito código
no sistema de interrupção e removeu um monte mais. O resultado é mais simples e mais fácil
de compreender.

www.it-ebooks.info
Controle de interrupção

Desativando uma linha de interrupção específica


Na seção anterior, analisamos as funções que desativam toda a entrega de
interrupções para um processador inteiro. Em alguns casos, é útil desabilitar
apenas uma linha de interrupção específica para todo o sistema. Isso é chamado
de mascaramento de uma linha de interrupção. Por exemplo, talvez você queira
desabilitar a entrega de interrupções de um dispositivo antes de manipular seu
estado. O Linux fornece quatro interfaces para essa tarefa:

As duas primeiras funções desabilitam uma determinada linha de interrupção no


controlador de interrupção. Isso desabilita a entrega da interrupção fornecida para
todos os processadores no sistema. Além disso, a função disable_irq()não retorna até
que qualquer manipulador em execução no momento seja concluído. Assim,
chamadores são assegurados não apenas que novas interrupções não serão entregues
na linha dada, mas também que quaisquer manipuladores já em execução foram
encerrados. A função disable_irq_nosync() não espera pela conclusão dos manipuladores
atuais.
A função synchronize_irq() espera a saída de um manipulador de
interrupção específico, se estiver sendo executado, antes de retornar.
Chamadas para estas funções aninham. Para cada chamada para disable_irq() ou
disable_irq_nosync() em uma determinada linha de interrupção, uma chamada
correspondente para enable_irq()
é necessário. Somente na última chamada para enable_irq() a linha de interrupção
está realmente habilitada.
Por exemplo, se disable_irq() é chamada duas vezes, a linha de interrupção não é
realmente reativada
até a segunda chamada para enable_irq().
Todas essas três funções podem ser chamadas do contexto de
interrupção ou processo e não dormem. Se chamar do contexto de
interrupção, tenha cuidado! Você não deseja, por exemplo, habilitar uma
linha de interrupção enquanto a manipula. (Lembre-se de que a linha de
interrupção de um manipulador é mascarada enquanto é atendida.)
Seria bastante rude desativar uma linha de interrupção compartilhada entre
vários manipuladores de interrupção. Desativar a linha desativa a interrupção da
entrega para todos os dispositivos na linha.Portanto, os drivers de dispositivos
mais recentes tendem a não usar essas interfaces.3 Como os dispositivos PCI têm
que suportar o compartilhamento de linha de interrupção por especificação, eles
não devem usar essas interfaces.Assim, disable_irq() e os amigos são encontrados
com mais frequência nos drivers de dispositivos antigos, como a porta paralela do
PC.
3 Muitos dispositivos mais antigos, particularmente os dispositivos ISA, não fornecem um método para
obter se geraram uma interrupção. Portanto, muitas vezes as linhas de interrupção para dispositivos ISA não
podem ser compartilhadas. Como a especificação PCI determina o compartilhamento de interrupções, os
dispositivos modernos baseados em PCI suportam o compartilhamento de interrupções. Em computadores
contemporâneos, quase todas as linhas de interrupção podem ser compartilhadas.

www.it-ebooks.info
130 Capítulo 7 Interrupções e manipuladores de interrupção

Status do sistema de interrupção


Geralmente, é útil saber o estado do sistema de interrupção (por exemplo, se as interrupções
estão ativadas ou desativadas) ou se você está executando no contexto de interrupção.
A macro irqs_disabled(), definida em <asm/system.h>, retornará diferente de zero se
o sistema de interrupção no processador local estiver desativado. Caso
contrário, retorna zero.
Duas macros, definidas em <linux/hardirq.h>, fornecem uma interface
para verificar o contexto atual do ker-nel.Elas são

O mais útil é o primeiro: Retorna diferente de zero se o kernel estiver


executando qualquer tipo de tratamento de interrupção. Isto inclui tanto
a execução de um manipulador de interrupção ou um manipulador da
metade inferior. A macro in_irq() retorna diferente de zero somente se o
kernel estiver executando especificamente um manipulador de
interrupção.
Com mais frequência, você deseja verificar se está no contexto do processo.Ou
seja, você deseja garantir que não está no contexto de interrupção.Esse geralmente
é o caso porque o código deseja fazer algo que só pode ser feito a partir do
contexto do processo, como dormir. Se in_interrupt() retorna zero, o kernel está no
contexto do processo.
Sim, os nomes são confusos e pouco fazem para transmitir seu
significado.A Tabela 7.2 é uma soma-mária dos métodos de controle de
interrupções e sua descrição.

Quadro 7.2 Métodos de controle de interrupção


Função Descrição
local_irq_disable() Desabilita a entrega de interrupção local

local_irq_enable() Habilita a entrega de interrupção local

local_irq_save() Salva o estado atual da entrega de interrupção local e


desabilita
Restaura a entrega de interrupção local para o estado
local_irq_restore () determinado
Desabilita a linha de interrupção fornecida e garante que nenhum
disable_irq() manipulador esteja ativado
a linha está sendo executada antes de retornar

disable_irq_nosync() Desabilita a linha de interrupção fornecida

enable_ irq () Habilita a linha de interrupção fornecida


Retorna diferente de zero se a entrega de interrupção local estiver
irqs_disabled() desabilitada; outro-
wise retorna zero
Retorna diferente de zero se estiver no contexto de interrupção e
in_ interrupt() zero se estiver no processo
contexto
Retorna diferente de zero se estiver executando atualmente um
in_ irq () manipulador de interrupção
e zero caso contrário
www.it-ebooks.info
Conclusã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

O capítulo anterior discutia os manipuladores de interrupção, o mecanismo do kernel


para lidar com interrupções de hardware. Os manipuladores de interrupção são uma
parte importante — na verdade, obrigatória — de qualquer sistema operacional. No
entanto, devido a várias limitações, os manipuladores de interrupção podem formar
apenas a primeira metade de qualquer solução de processamento de interrupção.
Essas limitações incluem

n Os manipuladores de interrupção são executados de forma assíncrona e, portanto,


interrompem outro código potencialmente importante, incluindo outros manipuladores de
interrupção. Portanto, para evitar a paralisação do código interrompido por muito tempo, os
manipuladores de interrupção precisam ser executados o mais rápido possível.
n Os manipuladores de interrupção são executados com o nível de
interrupção atual desabilitado na melhor das hipóteses (se
IRQF_DISABLED está desativado) e, na pior das hipóteses (se
IRQF_DISABLED estiver ativado), com todas as interrupções no
processador atual desativadas.Como a desativação das interrupções
impede que o hardware se comunique com os sistemas operacionais,
os manipuladores de interrupções precisam ser executados o mais
rápido possível.
n Os manipuladores de interrupção geralmente são críticos em relação à
temporização porque lidam com hardware.
n Os manipuladores de interrupção não são executados no contexto do
processo; portanto, não podem bloquear. Isso limita o que podem fazer.

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:

n Se o trabalho for sensível ao tempo, execute-o no manipulador de


interrupção.
n Se o trabalho estiver relacionado ao hardware, execute-o no manipulador de
interrupção.
n Se o trabalho precisar garantir que outra interrupção (particularmente a
mesma interrupção) não a interrompa, execute-a no manipulador de interrupção.
n Para todo o resto, considere realizar o trabalho na metade inferior.
Ao tentar escrever seu próprio driver de dispositivo, olhar para outros
manipuladores de interrupção e suas metades inferiores correspondentes
pode ajudar.Ao decidir como dividir seu trabalho de processamento de
interrupção entre a metade superior e inferior, pergunte-se o que deve estar
na metade superior e o que pode estar na metade inferior. Geralmente,
quanto mais rápido for executado o han-dler de interrupção, melhor.

Por que Metades Inferiores?


É crucial entender por que adiar o trabalho e quando adiá-lo exatamente.Você quer
limitar a quantidade de trabalho que você executa em um manipulador de interrupção
porque os manipuladores de interrupção são executados com a linha de interrupção
atual desabilitada em todos os processadores.Pior, os manipuladores que se registram
com IRQF_DISABLED são executados com todas as linhas de interrupção desabilitadas no
processador local, mais a linha de interrupção atual desabilitada em todos os
processadores. Minimizar o tempo gasto com interrupções desabilitadas é importante
para o desempenho e a resposta do sistema.Adicione a isso o fato de que os
manipuladores de interrupção são executados de forma assíncrona em relação a outro
código—até mesmo a outros manipuladores de interrupção—e está claro que você deve
trabalhar para minimizar por quanto tempo os manipuladores de interrupção são
executados. O processamento do tráfego de entrada da rede não deve impedir o
recebimento de pressionamentos de teclas pelo kernel. A solução é adiar parte do
trabalho para mais tarde.

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.

Um Mundo de Metades Inferiores


Ao contrário da metade superior, que é implementada inteiramente através do
manipulador de interrupção, vários mecanismos estão disponíveis para
implementar uma metade inferior.Esses mecanismos são interfaces e subsistemas
diferentes que permitem que você implemente as metades inferiores.Enquanto o
capítulo anterior examinava apenas uma única maneira de implementar
manipuladores de interrupção, este capítulo analisa vários métodos de
implementação das metades inferiores. Ao longo da história do Linux, houve muitos
mecanismos da metade inferior. Confusamente, alguns desses mecanismos têm
nomes parecidos ou mesmo idiotas. Ele requer um tipo especial de programador
para nomear as metades inferiores.
Este capítulo discute o design e a implementação dos mecanismos da
metade inferior que existem no 2.6. Também discutimos como usá-los no
código do kernel que você escreve. Os mecanismos antigos, mas há muito
removidos, da metade inferior são historicamente significativos e, portanto, são
mencionados quando relevantes.

A "Metade Inferior" Original


No início, o Linux fornecia apenas a "metade inferior" para implementar as metades
inferiores.Esse nome era lógico porque, na época, esse era o único meio disponível
para adiar o trabalho.A infraestrutura também era conhecida como BH, que é o que
chamaremos para evitar confusão com o termo genérico half inferior.A interface BH
era simples, como a maioria das coisas naqueles velhos tempos. Ele forneceu uma
lista estaticamente criada de 32 metades inferiores para todo o sistema.A metade
superior poderia marcar se a metade inferior seria executada definindo um bit em
um inteiro de 32 bits. Cada BH foi sincronizada globalmente. Não havia dois
processadores rodando ao mesmo tempo, nem mesmo em processadores
diferentes.Isso era fácil de usar, mas inflexível; uma abordagem simples, mas um
gargalo.

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.

Quadro 8.1 Metade Inferior do Status


Metade Inferior Estado
BH Removido em 2.5

Filas de tarefas Removido em 2.5

SoftirqName Disponível desde 2.3

Tarefa Disponível desde 2.3

Filas de trabalho Disponível desde 2.5

Com essa confusão de nomes resolvida, vamos olhar para os mecanismos


individuais.

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

Uma matriz de 32 entradas dessa estrutura é declarada em kernel/softirq.c:

Cada softriq registrado consome uma entrada no array. Por conseguinte,


NR_SOFTIRQS softirqs registrados.O número de softirqs registrados é determinado
estaticamente
em tempo de compilação e não pode ser alterado dinamicamente.O kernel impõe um
limite de 32
softirqs registrados; no kernel atual, no entanto, apenas nove existem. 3

O manipulador Softirq
O protótipo de um manipulador de softirq, ação, parece

Quando o kernel executa um manipulador softirq, ele executa esta


função de ação com um ponteiro para a estrutura softirq_action correspondente como
seu argumento único. Por exemplo, se my_softirq apontasse para uma
entrada na matriz softirq_vec, o kernel chamaria a função de manipulador softirq
como

Parece um pouco estranho que o kernel passe a estrutura inteira para


o manipulador softirq. Este truque permite adições futuras à estrutura
sem exigir uma mudança em cada manipulador softirq.
Um softirq nunca antecipa outro softirq.O único evento que pode
antecipar um softirq é um manipulador de interrupção.Outro softirq—
mesmo o mesmo—pode rodar em outro processador, no entanto.

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

softirqs, __do_softirq() repetirá cada um, invocando seu manipulador.


Vamos ver uma variante simplificada da parte importante de __do_softirq():

Este snippet é o coração do processamento do softirq. Ele verifica e


executa quaisquer softirqs pendentes. Especificamente
1. Ela define a variável local pendente para o valor retornado pelo
local_softirq_ending() macro.This is a 32-bit mask of ending softirqs—if bit
n is set, the nth softirq is ending.

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

última entrada.Uma nova entrada provavelmente


pertence ao entre a Tabela 8.2 contém uma lista dos
tipos de tasklet existentes.

Quadro 8.2 Tipos Softirq


Tarefa Prioridade Descrição do Softirq
HI_SOFTIRQ 0 Tarefas de alta prioridade

TIMER_SOFTIRQ 1 Temporizadores

NET_ TX_ SOFTIRQ 2 Enviar pacotes de rede

NET_ RX_ SOFTIRQ 3. Receber pacotes de rede

BLOCO_SOFTIRQ 4. Bloquear dispositivos

TASKLET_ SOFTIRQ 5. Tarefas de prioridade normal

SCHED_ SOFTIRQ 6. Agendador

TIMER_ SOFTIRQ 7. º Temporizadores de alta resolução

RCU_ SOFTIRQ 8. Bloqueio de RCU


www.it-ebooks.info
Softirqs

141º

Registrando seu manipulador


Em seguida, o manipulador softirq é registrado em tempo de execução via
open_softirq(), que usa dois parâmetros: o índice softirq e sua função de
manipulador.O subsistema de rede, por exemplo, registra seu softirq assim,
em net/core/dev.c:

Os manipuladores softirq são executados com interrupções ativadas e não podem


ser suspensos.Enquanto um manipulador é executado, os softirqs no processador atual
são desativados.Outro processador, entretanto, pode executar outros softirqs. Se o
mesmo softirq for acionado novamente enquanto estiver sendo executado, outro
processor poderá executá-lo simultaneamente. Isso significa que qualquer dado
compartilhado — mesmo dados globais usados somente dentro do manipulador softirq
— precisa de bloqueio adequado (conforme discutido nos próximos dois capítulos).
Este é um ponto importante, e é a razão pela qual os tasklets são geralmente preferidos.
Simplesmente impedir que seus softirqs funcionem simultaneamente não é ideal. Se um
softirq obtivesse uma trava para impedir que outra instância de si mesmo funcionasse
simultaneamente, não haveria razão para usar um softirq. Consequentemente, a maioria
dos manipuladores de software recorre a dados por processador (dados exclusivos de
cada processador e, portanto, não exigindo bloqueio) e outros truques para evitar o
bloqueio explícito e fornecer excelente escalabilidade.
A razão de ser do softirqs é a escalabilidade. Se você não precisar dimensionar para
um número infinito de processadores, use um tasklet.Tasklets são essencialmente
softirqs em que várias instâncias do mesmo manipulador não podem ser executadas
simultaneamente em vários processadores.

Levantando o seu Softirq


Depois que um manipulador é adicionado à lista enum e registrado via
open_softirq(), ele está pronto para ser executado. Para marcá-lo como
pendente, ele é executado na próxima chamada de open_softirq(), chame-o. Por
exemplo, o subsistema de rede chamaria,

Isso gera o softirq do NET_TX_SOFTIRQ. Seu manipulador, net_tx_action(), roda na próxima


vez que o kernel executa softirqs. Esta função desativa interrupções antes de
realmente elevar o softirq e, em seguida, restaura-as para seu estado anterior. Se as
interrupções já estão desligadas, a função raise_softirq_irqoff() pode ser usada como uma
pequena otimização. Por exemplo

Softirqs são mais frequentemente levantados de dentro de manipuladores de


interrupção. No caso de manipuladores de interrupção, o manipulador de
interrupção executa o trabalho básico relacionado ao hardware, eleva o softirq e,
em seguida, sai. Ao processar interrupções, o kernel chama do_softirq().O softirq, em
seguida, executa e pega onde o manipulador de interrupção parou. Neste exemplo,
o nome "metade superior" e "metade inferior" deve fazer sentido.

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>:

O membro func é o manipulador de tasklet (o equivalente de action a


um softirq) e recebe dados como seu único argumento.
O membro do estado é exatamente zero, TASKLET_STATE_SCHED ou
TASKLET_STATE_RUN.
TASKLET_STATE_SCHED denota uma tarefa programada para execução e
TASKLET_STATE_RUN denota uma tarefa que está em execução.Como uma
otimização,
TASKLET_STATE_RUN é usado somente em computadores com multiprocessador porque
um
o computador sempre sabe se o tasklet está em execução. (É o arquivo em execução no
momento
ou não.)
O campo de contagem é usado como uma contagem de referência para o tasklet. Se for
diferente de zero, a tarefa será desabilitada e não poderá ser executada; se for zero, a tarefa
será habilitada e poderá ser executada se estiver marcada como pendente.
www.it-ebooks.info
Tarefas

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).

Ambas as estruturas são listas de estruturas. Cada

a estrutura tasklet_struct na lista representa um tasklet diferente.


Tasklets são agendados através de tasklet_schedule() e tasklet_hi_schedule()
funções, que recebem um ponteiro para a tasklet_struct do tasklet como seu argumento
solitário. Cada função garante que a tarefa fornecida ainda não esteja agendada e, em
seguida, chama
e __tasklet_hi_schedule() conforme apropriado.Os dois func-
__tasklet_schedule()
As ações são semelhantes. (A diferença é que se usa TASKLET_SOFTIRQ e se usa
HI_SOFTIRQ.) Escrever e usar tasklets é abordado na próxima seção. Agora,
vamos ver as etapas que tasklet_schedule() realiza:
1. Verifique se o estado do tasklet é TASKLET_ESTADO_SCHED. Se estiver, a tarefa já está
programada para ser executada e a função pode retornar imediatamente.
2. Chame __tasklet_schedule().
3. Salve o estado do sistema de interrupção e desabilite as interrupções
locais.Isso garante que nada neste processador atrapalhará o código do tasklet
enquanto tasklet_schedule() estiver manipulando os tasklets.
4. Adicione o tasklet a ser agendado para o cabeçalho do tasklet_vec ou
tasklet_hi_vec lista vinculada, que é exclusiva para cada processador no sistema.

5. Levante o softirq TASKLET_SOFTIRQ ou HI_SOFTIRQ, portanto do_softirq()


executa isso
tarefa em um futuro próximo.
6. Restaurar interrupções para seu estado anterior e retornar.

Na próxima conveniência, do_softirq() é executado como discutido na seção


anterior. Como a maioria dos tasklets e softirqs são marcados como pendentes
em manipuladores de interrupção, do_softirq() provavelmente executa quando a
última interrupção retorna. Porque
TASKLET_SOFTIRQ ou HI_SOFTIRQ agora é acionado, do_softirq() executa os
manipuladores associados.Esses manipuladores, tasklet_action() edasklet_hi_action(), são
o coração do processamento de tasklet. Vejamos as etapas que esses
manipuladores executam:
1. Desabilite a entrega de interrupção local (não há necessidade de primeiro salvar seu
estado porque o código aqui é sempre chamado como um manipulador softirq e as
interrupções são sempre habilitadas) e recupere a lista tasklet_vec ou tasklet_hi_vec para este
processador.
2. Limpe a lista deste processador definindo-o como NULL.
5 Mais um exemplo dos esquemas confusos de nomenclatura que estão aqui em jogo. Por que softirqs
são levantados, mas tasklets são agendados? Quem sabe? Ambos os termos significam marcar essa
metade inferior pendente para que seja exe-cuted em breve.

www.it-ebooks.info
144 Capítulo 8 Metades inferiores e trabalho diferido

3. Habilitar entrega de interrupção local.Novamente, não há necessidade de restaurá-


los ao estado anterior porque essa função sabe que eles sempre foram originalmente habilitados.

4. Repetir cada tasklet pendente na lista recuperada.


5. Se esta for uma máquina de multiprocessamento, verifique se a tasklet está
sendo executada em outro processador, marcando o sinalizador TASKLET_STATE_RUN. Se ele
estiver em execução no momento, não o execute agora e vá para a próxima tarefa pendente.
(Lembre-se de que apenas uma tarefa de um determinado tipo pode ser executada
simultaneamente.)

6. Se a tarefa não estiver em execução no momento, defina o sinalizador


TASKLET_STATE_RUN, para que outro processador não a execute.
7. Verifique se há um valor de contagem zero, para garantir que o tasklet
não esteja desativado. Se a tarefa estiver desativada, ignore-a e vá para a próxima
tarefa pendente.
8. Agora sabemos que a tarefa não está sendo executada em outro lugar, está marcada
como em execução para que não comece a ser executada em outro lugar e tem um valor de contagem
zero. Execute o manipulador do tasklet.

9. Após a execução da tarefa, desmarque o sinalizador TASKLET_STATE_RUN no campo


estado da tarefa.

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.

Declarando seu Tasklet


Você pode criar tasklets estática ou dinamicamente.A opção escolhida
dependerá de você ter (ou desejar) uma referência direta ou indireta ao
tasklet. Se você for criar estaticamente o tasklet (e, portanto, tiver uma
referência direta a ele), use uma das duas macros em <linux/interrupt.h>:
Essas duas macros criam estaticamente uma struct tasklet_struct com o nome
fornecido. Quando o tasklet é agendado, o func de função fornecido é executado e
passado o argu-

www.it-ebooks.info
Tarefas

145º

Dados de ment.A diferença entre as duas macros é a contagem de referência


inicial.A primeira macro cria a tarefa com uma contagem de zero e ela é habilitada.A
segunda macro define count como um, e a tarefa é desabilitada. Aqui está um
exemplo:

Esta linha equivale a

Isso cria uma tarefa chamada my_tasklet habilitada com tasklet_handler


como seu han-dler.O valor de tasklet_handler é passado para o handler quando é
executado.
Para inicializar um tasklet que recebeu uma referência indireta (um ponteiro) a um
struct_struct, t, call_init():

Escrevendo o Manipulador do Tasklet


O manipulador de tasklet deve corresponder ao protótipo correto:

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").

Agendamento do seu Tasklet


Para agendar um tasklet para execução, tasklet_schedule() é chamado e recebe um
ponteiro para
a tasklet_struct relevante:

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:

TASK_ INTERRUPTIBLE Se algum softirqs estiver pendente (como relatado por


softirq_ending()), ksoftirqd
chama
do_softirq() para manipulá-los. Observe que ele faz isso repetidamente para lidar com
qualquer reativação
softirqs, too.After cada iteração, schedule() é chamado se necessário, para habilitar
mais import-
importantes processos a serem executados. Após a conclusão de todo o
processamento, o thread do kernel define a si mesmo
e chama o programador para selecionar um novo processo
executável.
Os threads do kernel softirq são despertados sempre que do_softirq()
detecta um thread do kernel executado reativando a si mesmo.

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.

Implementando filas de trabalho


Em sua forma mais básica, o subsistema da fila de trabalho é uma interface para criar
threads do kernel a fim de manipular o trabalho em fila de outro lugar.Esses threads
do kernel são chamados de threads do worker.As filas de trabalho permitem que o
driver crie um thread de trabalho especial para manipular o trabalho adiado.O
subsistema da fila de trabalho, no entanto, implementa e fornece um thread de
trabalho padrão para manipular o trabalho.Portanto, em sua forma mais comum, uma
fila de trabalho é uma interface simples para adiar o trabalho para um thread do kernel
genérico.
Os threads de trabalho padrão são chamados de events/n onde n é o número do
processador; há um por processador. Por exemplo, em um sistema uniprocessador, há
um thread, events/0.Um sistema de processador duplo também teria um events/1 thread.O
thread de trabalho padrão manipula o trabalho adiado de vários locais. Muitos drivers no
kernel adiam seu trabalho da metade inferior para o thread padrão. A menos que um driver
ou subsistema tenha um requisito forte para criar seu próprio thread, o thread padrão é
preferível.
No entanto, nada impede que o código crie seu próprio thread de trabalho. Isso pode ser
vantajoso se você executar grandes quantidades de processamento no thread de trabalho.
O trabalho que exige muito do processador e é crítico para o desempenho pode se
beneficiar de seu próprio processo.Isso também clareia a carga nos processos padrão, o
que evita que o restante do trabalho enfileirado fique sem trabalho.

Estruturas de dados que representam os threads


Os threads de trabalho são representados pela estrutura workqueue_struct:
www.it-ebooks.info
150 Capítulo 8 Metades inferiores e trabalho diferido

Essa estrutura, definida em kernel/workqueue.c, contém uma matriz de struct


cpu_workqueue_struct, um por possível processador no sistema. Porque o trabalhador
existem segmentos em cada processador no sistema, existe uma dessas estruturas por
operador
por processador, em uma determinada máquina.A cpu_workqueue_struct é o núcleo de
dados
estrutura e também é definido em kernel/workqueue.c:

Observe que cada tipo de thread de worker tem uma workqueue_struct


associada a ele. Dentro, há um cpu_workqueue_struct para cada segmento e,
portanto, cada processador, porque há um segmento de trabalho em cada
processador.

Estruturas de Dados que Representam o Trabalho


Todos os threads de trabalho são implementados como threads de kernel
normais executando o
worker_thread()function.After
initial setup, esta função entra em um loop infinito e
vai para sleep.When trabalho é enfileirado, o thread é despertado e processa o
trabalho. Quando não há mais trabalho a processar, ele volta a dormir.
O trabalho é representado pela estrutura work_struct, definida em
<linux/workqueue.h>:

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

trabalho, remove as entradas correspondentes de work_struct da lista


vinculada.Quando a lista está vazia, ela volta para o modo de
suspensão.
Vamos analisar o coração de worker_thread(), simplificado:

Esta função executa as seguintes funções, em um loop infinito:

1. O thread marca-se em repouso (o estado da tarefa é definido como


TASK_INTERRUPTIBLE) e se adiciona a uma fila de espera.

2. Se a lista vinculada de trabalho estiver vazia, o thread chamará schedule() e


entrará em suspensão.

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.

4. Se a lista não estiver vazia, a thread chama run_workqueue() para executar o


trabalho adiado.

A função run_workqueue(), por sua vez, na verdade executa o trabalho adiado:

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

4. Ele chama a função.


5. Repita.

Resumo da implementação da fila de trabalhos


A relação entre as diferentes estruturas de dados é reconhecidamente um pouco complicada.
A Figura 8.1 fornece um exemplo gráfico, que deve reunir tudo.

thread de trabalho cpu_workqueue_struct um por processador

uma por tipo de


estrutura workqueue_struct
linha

...
um por cada
função

estrutura_trabalho
...
.. . .estruturas
...
...

Figura 8.1 O relacionamento entre trabalho, filas de trabalho e o


threads de trabalho.

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

trabalho adiado.O trabalho é enviado a um thread de trabalho específico—neste caso, um


thread de falcon específico.O thread de trabalho acorda e executa o trabalho em fila.
A maioria dos drivers usa os threads de trabalho padrão existentes, chamados
events.They são fáceis e simples. No entanto, algumas situações mais sérias exigem
seus próprios threads de trabalho. O sistema de arquivos XFS, por exemplo, cria dois
novos tipos de threads de trabalho.

Usando filas de trabalho


O uso de filas de trabalho é fácil.Primeiro abordamos a fila de eventos
padrão e, em seguida, examinamos a criação de novos threads 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:

Isso cria estaticamente uma estrutura work_struct chamada name com


a função de manipulador
Como alternativa, você pode criar trabalho em tempo de execução através de
um ponteiro:

Isso inicializa dinamicamente a fila de trabalho apontada pelo trabalho


com a função de manipulador func e argumento data.

Seu manipulador de filas de trabalho


O protótipo do manipulador da fila de trabalho é

Um thread de trabalho executa essa função e, portanto, a função é executada no contexto


do processo. Por padrão, as interrupções são ativadas e nenhum bloqueio é mantido. Se
necessário, a função pode dormir. Observe que, apesar da execução no contexto do
processo, os manipuladores de trabalho não podem acessar a memória do espaço do
usuário porque não há nenhum mapa de memória do espaço do usuário associado para
threads do kernel. O kernel pode acessar a memória do usuário somente quando executado
em nome de um processo do espaço do usuário, como ao executar uma chamada do
sistema. Somente então a memória do usuário é mapeada em.
O bloqueio entre filas de trabalho ou outras partes do kernel é
manipulado da mesma forma que com qualquer outro código de contexto
de processo.Isso torna a escrita de manipuladores de trabalho muito
mais fácil.Os próximos dois capítulos abordam o bloqueio.

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:

Nesse caso, a estrutura_de_trabalho representada por &work não será executada


pelo menos no futuro. O uso de tiques como uma unidade de tempo é abordado no
Capítulo 10.

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:

Essa função aguarda até que todas as entradas na fila sejam


executadas antes de retornar.Enquanto espera pela execução de
qualquer trabalho pendente, a função é suspensa.Portanto, você pode
chamá-la somente do contexto do processo.
Note que esta função não cancela nenhum trabalho atrasado. Isto é,
qualquer trabalho agendado via schedule_delay_work(), e cujo atraso ainda
não está ativo, não é liberado via flush_schedul_work().Para cancelar o
trabalho atrasado, ligue

Esta função cancela o trabalho pendente, se houver, associado ao


estrutura_trabalho.

Criando novas filas de trabalho


Se a fila padrão for insuficiente para suas necessidades, você poderá
criar uma nova fila de trabalho e os threads de trabalho
correspondentes. Como isso cria um thread de trabalho por
processador, você deve criar filas de trabalho exclusivas somente se o
código precisar do desempenho de um conjunto exclusivo de threads.
Você cria uma nova fila de trabalho e os threads de trabalho associados por meio de uma
função simples:

O nome do parâmetro é usado para nomear os threads do kernel. Por


exemplo, a fila de eventos padrão é criada via

Essa função cria todos os threads de trabalho (um para cada


processador no sistema) e os prepara para trabalhar.
Criar trabalho é tratado da mesma maneira, independentemente do tipo de
fila.Depois que o trabalho é criado, as seguintes funções são análogas a schedule_work()
e

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

Como discutido anteriormente, essa função funciona de forma idêntica a


exceto que ele espera que a fila fornecida esvazie antes de retornar.

O antigo mecanismo da fila de tarefas


Assim como a interface BH, que deu lugar a softirqs e tasklets, a interface da fila de
trabalho cresceu a partir de falhas na interface da fila de tarefas.A interface da fila de
tarefas (muitas vezes chamada simplesmente de tq no kernel), como tasklets, também
não tem nada a ver com tarefas no sentido do processo. 7 Os usuários da interface da
fila de tarefas foram arrancados pela metade durante o kernel de desenvolvimento 2.5.
Metade dos usuários foram convertidos em tasklets, enquanto a outra metade
continuou usando a interface da fila de tarefas.O que restou da interface da fila de
tarefas tornou-se a interface da fila de trabalhos. Ver brevemente as filas de tarefas,
que estavam por aí por algum tempo, é um exercício histórico útil.
As filas de tarefas funcionam definindo um conjunto de filas.As filas têm nomes,
como a fila do agendador, a fila imediata ou a fila do temporizador. Cada fila é
executada em um ponto específico no kernel. Um thread do kernel, keventd, executou o
trabalho associado à fila do agendador. Este foi o precursor para a interface completa
da fila de trabalho. A fila do temporizador foi executada em cada tique do temporizador
do sistema, e a fila imediata foi executada em um punhado de locais diferentes para
garantir que ela fosse executada "imediatamente" (hack!). Havia outras filas também.
Além disso, você pode criar dinamicamente novas filas.
Tudo isso pode parecer útil, mas a realidade é que a interface da fila de tarefas era
uma bagunça. Todas as filas eram essencialmente abstrações arbitrárias, espalhadas
sobre o kernel como se jogadas no ar e mantidas onde elas pousaram. A única fila
significativa era a fila sched-uler, que forneceu a única maneira de adiar o trabalho para
processar o contexto.
A outra coisa boa das filas de tarefas era a interface simples sem cérebro. Apesar
da miríade de filas e das regras arbitrárias sobre quando elas eram executadas, a
interface era a mais simples possível. Mas isso é tudo — o restante das filas de
tarefas precisavam ser resolvidas.

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

Os vários usuários da fila de tarefas foram convertidos para outros mecanismos


da metade inferior. A maioria deles mudou para tasklets.The programador fila
usuários presos ao redor. Finalmente, o código do keventd foi generalizado no
excelente mecanismo de fila de trabalho que temos hoje, e as filas de tarefas foram
finalmente removidas do kernel.

Qual Metade Inferior Devo Usar?


A decisão sobre qual metade inferior usar é importante. No kernel 2.6 atual,
você tem três opções: softirqs, tasklets e workqueues.Tasklets são criados em
softirqs e, portanto, ambos são semelhantes.O mecanismo da fila de trabalho
é uma criatura totalmente diferente e é criado em threads do kernel.
Softirqs, por design, fornecem a menor serialização.Isso requer que os
manipuladores softirq passem por etapas extras para garantir que os dados
compartilhados sejam seguros porque dois ou mais softirqs do mesmo tipo podem
ser executados simultaneamente em processadores diferentes. Se o código em
questão já é altamente segmentado, como em um subsistema de rede que é
profundo em variáveis por processador, os softirqs fazem uma boa escolha.Eles
são certamente a alternativa mais rápida para usos críticos de temporização e de
alta frequência.
Os tasklets fazem mais sentido se o código não for finamente
segmentado.Eles têm uma interface mais simples e, como dois tasklets do
mesmo tipo podem não ser executados ao mesmo tempo, eles são mais fáceis
de implementar.Tasklets são efetivamente softirqs que não são executados ao
mesmo tempo.Um desenvolvedor de driver deve sempre escolher tasklets sobre
softirqs, a menos que preparado para utilizar variáveis por processador ou
magia semelhante para garantir que o softirq pode ser executado com
segurança simultaneamente em vários processadores.
Se o trabalho adiado precisar ser executado no contexto do processo, sua única
opção entre as três será filas de trabalho. Se o contexto do processo não for um
requisito — especificamente, se você não precisar dormir — softirqs ou tasklets talvez
sejam mais adequados.As filas de trabalho envolvem a sobrecarga mais alta porque
envolvem threads do kernel e, portanto, a alternância de contexto.Isso não quer dizer
que elas são ineficientes, mas à luz de milhares de interrupções que ocorrem por
segundo (como o subsistema de rede pode experimentar), outros métodos fazem mais
sentido. Entretanto, para a maioria das situações, as filas de trabalho são suficientes.
Em termos de facilidade de uso, as filas de trabalho assumem a coroa.
Usar a fila de eventos padrão é reprodução de uma criança. Em seguida
vêm as tasklets, que também têm uma interface simples. Por último,
temos os softirqs, que precisam ser criados estaticamente e exigem uma
reflexão cuidadosa com sua implementação.
O quadro 8.3 é uma comparação entre as três interfaces da metade
inferior.

Quadro 8.3 Comparação da metade inferior


Metade Inferior Contexto Serialização Inerente
SoftirqName Interrupção Nenhuma

Tarefa Interrupção Contra o mesmo tasklet


Filas de trabalho Processo Nenhum (agendado como contexto de processo)

www.it-ebooks.info
Desativação das metades inferiores

Em suma, os condutores de veículos normais têm duas opções. Primeiro, você


precisa de uma entidade programável para realizar seu trabalho adiado?
Basicamente, você precisa dormir para qualquer filho real? As filas de trabalho
são a única opção. Caso contrário, os tasklets são preferidos. Somente se a
escalabilidade se tornar uma preocupação é que você investiga softirqs.

Bloqueio entre as metades inferiores


Ainda não discutimos o bloqueio, que é um tema tão divertido e abrangente que
dedicamos os próximos dois capítulos a ele. No entanto, você precisa entender
que é crucial proteger os dados compartilhados do acesso simultâneo enquanto
usa as metades inferiores, mesmo em uma única máquina de processador.
Lembre-se, uma metade inferior pode correr em praticamente qualquer
momento.Você pode querer voltar a esta seção depois de ler os próximos dois
capítulos se o conceito de bloqueio é estranho para você.
Um dos benefícios dos tasklets é que eles são serializados em relação a si mesmos: O
mesmo tasklet não será executado simultaneamente, mesmo em dois processadores
diferentes.Isso significa que você não precisa se preocupar com problemas de
simultaneidade dentro do tasklet. A concordância de moedas entre tarefas (ou seja,
quando dois tasklets diferentes compartilham os mesmos dados) requer o bloqueio
adequado.
Como softirq não fornece serialização, (mesmo duas instâncias do mesmo
softirq podem ser executadas simultaneamente), todos os dados
compartilhados precisam de um bloqueio apropriado.
Se o código de contexto do processo e uma metade inferior compartilham
dados, você precisa desativar o processamento da metade inferior e obter
um bloqueio antes de acessar os dados. Fazer isso garante a proteção local
e SMP e evita um impasse.
Se o código de contexto de interrupção e uma metade inferior
compartilham dados, você precisa desabilitar interrupções e obter um
bloqueio antes de acessar os dados. Isso também garante proteção local
e SMP e evita um deadlock.
Qualquer dado compartilhado em uma fila de trabalho requer bloqueio também.Os
problemas de bloqueio não são diferentes do código normal do kernel porque as filas
de trabalho são executadas no contexto do processo.
O Capítulo 9, "An Introduction to Kernel Synchronization" (Uma introdução à
sincronização do kernel), fornece um plano de fundo sobre os problemas
relacionados à simultaneidade, e o Capítulo 10 aborda os primitivos de bloqueio do
kernel. Esses capítulos abordam como proteger os dados usados pela metade
inferior.

Desativação das metades inferiores


Normalmente, não é suficiente desativar apenas as metades inferiores. Com mais
frequência, para proteger dados compartilhados com segurança, você precisa obter
uma trava e desativar as metades inferiores. Esses métodos, que você pode usar em
um driver, são abordados no Capítulo 10. Entretanto, se você estiver escrevendo o
código núcleo do kernel, talvez precise desativar apenas as metades inferiores.
Para desativar todo o processamento da metade inferior (especificamente,
todos os softirqs e, portanto, todos os tasklets), chame local_bh_disable().Para ativar
o processamento da metade inferior, chame local_bh_enable().Sim, a função está errada;
ninguém se preocupou em alterar o nome quando a interface BH deu lugar a
softirqs.A Tabela 8.4 é um resumo dessas funções.

www.it-ebooks.info
158 Capítulo 8 Metades inferiores e trabalho diferido

Quadro 8.4 Métodos de controle da metade inferior


Método Descrição
void local_bh_disable() Desativa o processamento de softirq e tasklet
no processador local

void local_bh_enable() Habilita o processamento de softirq e tasklet


no processador local

As chamadas podem ser aninhadas - somente a chamada final para


local_bh_enable() realmente ativa local_bh_disable()
metades inferiores. Por exemplo, a primeira vez é chamado softirq local

o processamento está desabilitado. Se local_bh_disable() for chamado mais três vezes,


o processamento local permanecerá desativado. O processamento não é
reabilitado até a quarta chamada para local_bh_enable().
As funções realizam isso mantendo um contador por tarefa por meio do
preempt_count (o mesmo contador usado pela preempção do kernel).8 Quando a
o contador atinge zero, o processamento pela metade inferior é possível. Porque as metades
inferiores foram...
abled, local_bh_enable() também verifica se existem metades inferiores pendentes e as
executa.
As funções são exclusivas de cada arquitetura suportada e
geralmente são escritas como macros complicadas em <asm/softirq.h>.A
seguir estão representações C próximas para os curiosos:

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º

Essas chamadas não desativam a execução de filas de trabalho. Como as filas


de trabalho são executadas no contexto do processo, não há problemas com a
execução assíncrona e, portanto, não há necessidade de desativá-las. Como
softirqs e tasklets podem ocorrer de forma assíncrona (por exemplo, no retorno da
manipulação de uma interrupção), no entanto, o código do kernel pode precisar
desativá-los.Com filas de trabalho, por outro lado, proteger dados compartilhados é
o mesmo que em qualquer contexto de processo. Os capítulos 8 e 9 apresentam os
pormenores.

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

Em um aplicativo de memória compartilhada, os desenvolvedores devem garantir


que os recursos compartilhados estejam protegidos contra acesso simultâneo. O
kernel não é exceção. Os recursos compartilhados exigem proteção contra acesso
simultâneo porque se vários segmentos de acesso de execução1 e manipularem os
dados ao mesmo tempo, os segmentos poderão substituir as alterações uns dos
outros ou acessar os dados enquanto estiverem em um estado inconsistente. O
acesso simultâneo a dados compartilhados é uma receita para instabilidade que
muitas vezes se mostra difícil de rastrear e depurar — é importante que isso ocorra
logo no início.
Proteger adequadamente os recursos compartilhados pode ser difícil.Anos atrás, antes
de o Linux suportar multiprocessamento simétrico, era simples impedir o acesso
simultâneo aos dados. Como só havia suporte para um único processador, a única maneira
de acessar os dados simultaneamente era se ocorresse uma interrupção ou se o código do
kernel explicitamente reagendasse e ativasse outra tarefa para ser executada.Com os
kernels anteriores, o desenvolvimento era simples.
Esses dias de halcyon acabaram. O suporte a multiprocessamento simétrico foi
introduzido no kernel 2.0 e tem sido continuamente aprimorado desde então. O suporte a
multiprocessamento implica que o código do kernel pode ser executado simultaneamente
em dois ou mais processadores. Consequentemente, sem proteção, o código no kernel,
executado em dois processadores diferentes, pode acessar simultaneamente dados
compartilhados exatamente ao mesmo tempo.Com a introdução do
kernel 2.6, o kernel do Linux é preemptivo.Isso implica que (novamente, na
ausência de proteção) o agendador pode antecipar o código do kernel em
praticamente qualquer ponto e reagendar outra tarefa.Hoje, um número de
cenários permitem a simultaneidade dentro do kernel, e todos eles exigem
proteção.

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

Este capítulo discute as questões de concorrência e sincronização no


resumo, como elas existem em qualquer kernel de sistema operacional. O
próximo capítulo detalha os mecanismos específicos e as interfaces que o
kernel do Linux fornece para resolver problemas de sincronização e evitar
condições de corrida.

Regiões críticas e condições raciais


Os caminhos de código que acessam e manipulam dados compartilhados são
chamados de regiões críticas (também chamadas de seções críticas). Geralmente, não
é seguro que vários threads de execução acessem o mesmo recurso
simultaneamente.Para impedir o acesso simultâneo durante regiões críticas, o
programador deve garantir que o código seja executado atomicamente, ou seja,
operações concluídas sem interrupção, como se toda a região crítica fosse uma
instrução indivisível. É um bug se é possível que dois threads de execução sejam
executados simultaneamente dentro da mesma região crítica. Quando isso ocorre,
chamamos isso de condição de corrida, assim chamada porque os threads correram
para chegar lá primeiro. Observe como uma condição de corrida rara em seu código
pode ser mani-fest em si — a depuração de condições de corrida é frequentemente
difícil porque elas não são facilmente reproduzíveis. Assegurar que a simultaneidade
não segura seja evitada e que as condições de corrida não ocorram é chamado de
sincronização.

Por Que Precisamos De Proteção?


Para entender melhor a necessidade de sincronização, vejamos a onipresença das
condições de corrida. Para um primeiro exemplo, vamos considerar um caso real: um
caixa eletrônico (Automated Teller Machine, chamado de caixa eletrônico, ponto de
caixa ou ABM fora dos Estados Unidos).
Uma das funções mais comuns desempenhadas pelas caixas automáticas é
retirar dinheiro da conta bancária pessoal de um indivíduo.Uma pessoa caminha
até a máquina, insere um cartão de caixa eletrônico, digita um PIN, seleciona
Retirada, insere uma quantia pecuniária, pressiona OK, pega o dinheiro e envia-o
para mim.
Depois de o utilizador ter solicitado uma determinada quantia em dinheiro,
a caixa automática tem de assegurar que o dinheiro existe efetivamente na
conta desse utilizador. Se o dinheiro existe, então ele precisa deduzir o
levantamento do total de fundos disponíveis.O código para implementar isso
seria algo parecido com
www.it-ebooks.info
Regiões críticas e condições raciais

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:

Isso pode traduzir em instruções de máquina para o processador do


computador que se assemelham ao seguinte:
www.it-ebooks.info
164 Capítulo 9 Uma introdução à sincronização do kernel

Agora, suponha que haja dois segmentos de execução, ambos entre


nessa região crítica, e o valor inicial de i é 7. O resultado desejado é,
então, semelhante ao seguinte (com cada linha representando uma
unidade de tempo):

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)

Como esperado, 7 incrementado duas vezes é 9.Um resultado possível, no entanto, é


o seguinte:

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)

Se ambos os segmentos de execução leem o valor inicial de i antes de ser


incrementado, ambos os segmentos incrementam e salvam o mesmo valor. Como
resultado, a variável i contém o valor 8 quando, na verdade, agora deve conter
9.Este é um dos exemplos mais simples de uma região crítica.Felizmente, a
solução é igualmente simples:Precisamos apenas de uma maneira de executar
essas operações em uma etapa indivisível. A maioria dos processadores fornece
uma instrução para ler, incrementar e gravar automaticamente uma única variável.
Usando esta instrução atômica, o único resultado possível é

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º

Nunca seria possível as duas operações atômicas se intercalarem.O


processador fisicamente garantiria que isso fosse impossível. O uso de tal
instrução aliviaria o problema.O kernel fornece um conjunto de interfaces que
implementam essas instruções atômicas; elas são discutidas no próximo capítulo.

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

êxito: bloqueio adquirido falha: aguardando...

fila de acesso... aguardando...

desbloquear a fila aguardando...

.. êxito: bloqueio adquirido

fila de acesso...

desbloquear a fila

Observe que os bloqueios são consultivos e voluntários. Bloqueios são


inteiramente uma estrutura de programação que o programador deve aproveitar. Nada
impede que você escreva o código que manipula a fila fictícia sem o bloqueio
apropriado. Tal prática, é claro, acabaria por resultar em uma condição racial e
corrupção.
Os bloqueios têm várias formas e tamanhos — o Linux, por si só, implementa
vários mecanismos de bloqueio diferentes.A diferença mais significativa entre os
vários mecanismos é o comportamento quando o bloqueio não está disponível
porque outro thread já o mantém — algumas variantes de bloqueio aguardam, 2
enquanto outros bloqueios colocam a tarefa atual em espera até que o bloqueio se
torne disponível.O próximo capítulo discute o comportamento dos diferentes
bloqueios no Linux e suas interfaces.
Os leitores astutos estão agora gritando.O bloqueio não resolve o problema; ele
simplesmente encolhe a região crítica para apenas o código de bloqueio e desbloqueio:
provavelmente muito menor, claro, mas ainda uma corrida potencial! Felizmente, bloqueios
são implementados usando operações atômicas que asseguram que não existe raça. Uma
única instrução pode verificar se a chave foi usada e, caso contrário, capturá-la. Como isso
é feito é específico da arquitetura, mas quase todos os processadores implementam um
teste atômico e definem uma instrução que testa o valor de um inteiro e o define para um
novo valor apenas se for zero. Um valor de zero significa desbloqueado. Na popular
arquitetura x86, os bloqueios são implementados usando uma instrução semelhante
chamada compare e exchange.

2 Ou seja, gire em um loop fechado, verificando o status da trava repetidamente, aguardando


a trava ficar disponível.
www.it-ebooks.info
Bloqueio 167

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:

n Interrupções— Uma interrupção pode ocorrer de forma assíncrona em quase


qualquer momento, interrompendo o código atualmente em execução.
n Softirq e tasklets— O kernel pode acionar ou programar um softirq ou tasklet a
quase qualquer momento, interrompendo o código atualmente em execução.
n Preempção do kernel— Como o kernel é preventivo, uma tarefa no kernel
pode antecipar outra.
n Suspensão e sincronização com o espaço do usuário— Uma tarefa no kernel pode ser suspensa
e, assim, chamar o agendador, resultando na execução de um novo processo.
n Multiprocessamento simétrico— Dois ou mais processadores podem
executar código kernel exatamente ao mesmo tempo.
Os desenvolvedores do kernel precisam entender e se preparar para essas causas
de concorrência. É um bug maior se ocorrer uma interrupção no meio do código que
está manipulando um recurso e o manipulador de interrupção pode acessar o mesmo
recurso. Da mesma forma, é um bug se o código do kernel é preemptivo enquanto está
acessando um recurso compartilhado. Da mesma forma, é um bug se o código no
kernel é suspenso enquanto está no meio de uma seção crítica. Por fim, dois
processadores nunca devem acessar simultaneamente a mesma parte dos dados.Com
uma imagem clara de quais dados precisam de proteção, não é difícil fornecer o
bloqueio para manter o sistema estável. Em vez disso, a parte difícil é identificar essas
condições e perceber que, para evitar a concorrência, você precisa de alguma forma de
proteção.
Reiteremos este ponto, porque é importante. Implementar o bloqueio real no seu código
para proteger dados compartilhados não é difícil, especialmente quando feito logo no início
da fase de design do desenvolvimento.A parte complicada é identificar os dados
compartilhados reais e as seções críticas correspondentes.É por isso que projetar o
bloqueio no seu código a partir do início, e não como uma reflexão posterior, é de suma
importância. Pode ser difícil ir
www.it-ebooks.info
168 Capítulo 9 Uma introdução à sincronização do kernel

in, ex post, e identificar regiões críticas e bloqueio de retrofit no código


existente.O código resultante muitas vezes não é bonito, tampouco.A
consequência disso é sempre projetar o bloqueio adequado em seu código
desde o início.
O código que está a salvo do acesso simultâneo de um manipulador de
interrupção é dito estar a salvo de interrupção. O código que está a salvo da
concorrência em máquinas de multiprocessamento simétricas é seguro para SMP.
O código que está a salvo da simultaneidade com preempt-safe. 3 Os mecanismos
reais usados para fornecer sincronização e proteção contra condições de corrida
em todos esses casos são abordados no próximo capítulo.

Sabendo o que proteger


Identificar quais dados precisam de proteção especificamente é vital. Como qualquer
dado que possa ser acessado simultaneamente quase com certeza precisa de proteção,
muitas vezes é mais fácil identificar quais dados não precisam de proteção e trabalhar a
partir daí. Obviamente, todos os dados locais de um thread específico de execução não
precisam de proteção, pois somente esse thread pode acessar os dados. Por exemplo,
as variáveis locais automáticas (e as estruturas de dados alocadas dinamicamente cujo
endereço é armazenado apenas na pilha) não precisam de nenhum tipo de bloqueio
porque elas existem somente na pilha do thread em execução. Da mesma forma, os
dados que são acessados por apenas uma tarefa específica não exigem bloqueio
(porque um processo pode ser executado em apenas um processador por vez).
O que precisa de bloqueio? A maioria das estruturas de dados globais do kernel
faz.Uma boa regra prática é que se outro segmento de execução pode acessar os
dados, os dados precisam de algum tipo de bloqueio; se alguém pode vê-lo, bloqueie-
o. Lembre-se de bloquear dados, não códigos.

Opções de configuração: SMP versus UP


Como o kernel do Linux é configurável em tempo de compilação, faz sentido que você
possa personalizar o kernel especificamente para uma determinada máquina. Mais
importante, a opção CONFIG_SMP configure controla se o kernel suporta SMP. Muitos
problemas de travamento desaparecem em máquinas com um único processador;
consequentemente, quando CONFIG_SMP é desmarcado, o código desnecessário não é
compilado na imagem do kernel. Por exemplo, tal configuração permite que máquinas
uniprocessador renunciem à sobrecarga de travamentos de giro. O mesmo truque se
aplica a CONFIG_PREEMPT (a opção configure que habilita a preempção do kernel). Esta
foi uma excelente decisão de projeto— o kernel mantém uma base de fonte limpa, e os
vários mecanismos de bloqueio são usados conforme necessário. Diferentes
combinações de CONFIG_SMP e CONFIG_PREEMPT em diferentes arquiteturas são
compiladas em suporte de bloqueio variável.
Em seu código, forneça a proteção apropriada para o caso mais pessimista, SMP
com preempção do kernel e todos os cenários serão abordados.
3 Você também verá que, salvo algumas exceções, ser seguro para SMP implica ser seguro para
preempção.

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

Da mesma forma, considere n threads e n bloqueios. Se cada thread mantiver


um bloqueio desejado pelo outro, todos os threads serão bloqueados enquanto
aguardam que seus respectivos bloqueios se tornem disponíveis.O exemplo
mais comum é com dois threads e dois bloqueios, que geralmente é chamado
de abraço mortal ou o deadlock do ABBA:
Segmento 1 Segmento 2
adquirir bloqueio A adquirir bloqueio B

tentar adquirir bloqueio B tentar adquirir bloqueio A

aguardar bloqueio B aguardar a trava A

Cada thread está esperando o outro, e nenhum thread nunca


liberará seu bloqueio original; portanto, nenhum bloqueio ficará
disponível.
A prevenção de cenários de deadlock é importante.Embora seja difícil provar que o
código está livre de deadlocks, você pode escrever código livre de deadlock.Algumas regras
simples vão muito longe:

n Implemente a ordem de bloqueio. Bloqueios aninhados sempre


devem ser obtidos na mesma ordem. Isso evita o impasse mortal. Documente a
ordem de bloqueio para que outros a sigam.
n Evite a fome.Pergunte a si mesmo, esse código sempre termina? Se
foo não ocorrer, o bar vai esperar para sempre?
n Não adquira duas vezes o mesmo bloqueio.
n Design para simplificar. Complexidade em seu esquema de bloqueio
convida a impasses.

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

adquirir cão de bloqueio tentar adquirir lock dog

tentar adquirir a raposa de bloqueio esperar pelo cão de bloqueio

esperar pela raposa de bloqueio —


www.it-ebooks.info
Contenção e escalabilidade

O primeiro segmento está esperando pela fechadura da raposa, que o


segundo segmento segura, enquanto o segundo segmento está esperando pela
fechadura do cachorro, que o primeiro segmento segura. Nenhum deles nunca
libera sua fechadura e, portanto, ambos esperam para sempre - bam, impasse.
Se os bloqueios fossem sempre obtidos na mesma ordem, um impasse dessa
maneira não seria possível.
Sempre que bloqueios são aninhados dentro de outros bloqueios, uma
ordem específica deve ser obedecida. É uma boa prática colocar o pedido
em um comentário acima da fechadura. Algo como o seguinte é uma boa
ideia:

A ordem de desbloqueio não importa em relação ao impasse, embora seja prática


comum liberar as fechaduras em uma ordem inversa àquela em que foram adquiridas.
É importante evitar deadlocks.O kernel do Linux tem alguns recursos
básicos de depuração para detectar cenários de deadlock em um kernel
em execução.Esses recursos são discutidos no próximo capítulo.

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º

uma nova chamada do sistema ou um driver regravado, protegendo os


dados contra acesso simultâneo, precisa ser uma preocupação
principal.
Forneça proteção suficiente para cada cenário — SMP, preempção do
kernel etc. — e tenha certeza de que os dados estarão seguros em qualquer
máquina e configuração.O próximo capítulo discute como fazer isso.
Com os fundamentos e as teorias de sincronização, simultaneidade e
bloqueio atrás de nós, vamos agora mergulhar nas ferramentas reais que o
kernel do Linux fornece para garantir que seu código esteja livre de raça e
deadlock.
www.it-ebooks.info
Esta página foi deixada em branco intencionalmente

www.it-ebooks.info
10
.
Métodos de
Sincronização do
Kernel

O capítulo anterior discutia as origens e as soluções das condições de corrida.Graças a


isso, o kernel do Linux fornece uma família de métodos de sincronização.Os métodos de
sincronização do kernel do Linux permitem que os desenvolvedores criem um código
eficiente e sem corridas.Este capítulo discute esses métodos e suas interfaces,
comportamento e uso.

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

Com operadores atômicos, essa corrida não ocorre, na verdade, não


pode ocorrer. Em vez disso, o resultado é sempre um dos seguintes:
Segmento 1 Segmento 2
obter, incrementar e armazenar i (7 -> 8) —
— obter, incrementar e armazenar i (8 -> 9)

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.

Operações Atômicas com Números Inteiros


Os métodos de inteiros atômicos operam em um tipo de dados especial, atomic_t.Este
tipo especial é usado, ao contrário de ter as funções trabalhando diretamente no tipo C
int, para vários filhos reais. Primeiro, ter as funções atômicas aceitando apenas o tipo
atomic_t garante que as operações atômicas sejam usadas apenas com esses tipos
especiais. Da mesma forma, também garante que os tipos de dados não sejam
passados para funções não atômicas. Na verdade, de que serviriam as operações
atômicas se não fossem utilizadas de forma consistente nos dados? Em seguida, o uso
de atomic_t garante que o compilador não otimize (erroneamente, mas inteligentemente)
o acesso ao valor — é importante que as operações atômicas recebam o endereço de
memória correto e não um alias. linux/types.h Por fim, o uso de atomic_t pode ocultar
quaisquer diferenças específicas de arquitetura em sua implementação.O tipo atomic_t é
definido em

Apesar de ser um número inteiro e, portanto, 32 bits em todas as máquinas suportadas


pelo Linux, os desenvolvedores de desenvolvimento e seus códigos tinham que assumir
que um atomic_t não era maior do que 24 bits em tamanho.A porta SPARC no Linux tem uma
implementação ímpar de operações atômicas:Um bloqueio foi incorporado nos 8 bits
inferiores do int de 32 bits (parecia como a Figura 10.1).O bloqueio foi usado para proteger o acesso simultâneo ao
tipo atômico porque o archi-
www.it-ebooks.info
Operações Atômicas

a técnica carece de suporte apropriado no nível de instrução. Consequentemente,


apenas 24 bits utilizáveis estavam disponíveis em máquinas SPARC.Embora o código
que assumisse que o intervalo de 32 bits completo existia funcionaria em outras
máquinas; ele teria falhado de maneiras estranhas e sutis em máquinas SPARC—e
isso é apenas rude. Recentemente, espertos hacks permitiram que o SPARC
fornecesse um atomic_t de 32 bits totalmente utilizável, e essa limitação não existe mais.

atomic_t de 32 bits

inteiro de 24 bits assinado bloqueio

(bit) 31 7. º

Figura 10.1 Layout antigo de atomic_t de 32 bits em SPARC.

As declarações necessárias para usar as operações de inteiros atômicos


estão em <asm/atomic.h>. Algumas arquiteturas fornecem métodos adicionais que
são exclusivos para essa arquitetura, mas todas as arquiteturas fornecem pelo
menos um conjunto mínimo de operações que são usadas em todo o
kernel.Quando você escreve o código do kernel, você pode garantir que essas
operações sejam implementadas corretamente em todas as arquiteturas.
A definição de an atomic_t é feita da maneira usual. Opcionalmente, você
pode defini-lo como um valor inicial:

As operações são todas simples:

Se você precisar converter um atomic_t para um int, use atomic_read():

Um uso comum das operações de inteiros atômicos é implementar


contadores. Proteger um único contador com um esquema de bloqueio
complexo é um exagero, então os desenvolvedores usam atomic_inc() e atomic_dec(),
que são muito mais leves em peso.
Outro uso dos operadores de inteiros atômicos é executar atomicamente uma
operação e testar o resultado. Um exemplo comum é o decremento e teste atômicos:

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

Quadro 10.1 Métodos Atômicos Com Números


Inteiros
Operação Atômica de Número Inteiro Descrição
ATOMIC_ INIT( int i) Na declaração, inicialize para i.
int atomic_read( atomic_t * v) Ler atomicamente o valor inteiro
de v.
void atomic_set(atomic_t * v, int i) Atomicamente definir v igual a i.

void atomic_add(int i, atomic_t * v) Adicionar atomicamente i a v.

void atomic_sub(int i, atomic_t * v) Subtrair atomicamente i de v.

void atomic_ inc( atomic_ t * v) Adicionar atomicamente um a v.

void atomic_dec(atomic_t *v) Subtrair atomicamente um de v.

int atomic_sub_and_test(int i, atomic_t *v) Subtrair atomicamente i de v e


retorna true se o resultado for
zero;
caso contrário, falso.

int atômico_add_negativo(int i, atômico_t *v) Adicionar atomicamente i a v


e retornar true se o resultado
for negativo; caso contrário,
int atomic_add_return(int i, atomic_t *v) false.
Adicionar atomicamente i a v
int atomic_sub_return(int i, atomic_t *v) e retornar o resultado.
Subtrair atomicamente i de v e
int atomic_inc_return(int i, atomic_t *v) retornar o resultado.
Incrementa atomicamente v em
int atomic_dec_return(int i, atomic_t *v) um e retorna o resultado.
Diminuir atomicamente v por
int atomic_dec_and_test(atomic_t *v) um e retornar o resultado.
Diminui atomicamente v em um e
int atomic_inc_and_test(atomic_t *v) retorna verdadeiro se zero; caso
contrário, retorna falso.
Incrementa atomicamente v em
um e retorna verdadeiro se o
resultado é zero; caso
contrário, retorna falso.
www.it-ebooks.info
Operações Atômicas

As operações atômicas são tipicamente implementadas como funções em linha


com as-sembly em linha. No caso em que uma função específica é inerentemente
atômica, a função dada é geralmente apenas uma macro. Por exemplo, na maioria das
arquiteturas, uma leitura do tamanho de uma palavra é sempre atômica.Ou seja, uma
leitura de uma única palavra não pode ser concluída no meio de uma gravação nessa
palavra.A leitura sempre retorna a palavra em um estado consistente, antes ou depois
que a gravação é concluída, mas nunca no meio. Consequentemente, atomic_read() é
geralmente apenas uma macro retornando o valor inteiro de atomic_t:

Atomicidade Versus Ordenação


A discussão anterior sobre a leitura atômica suscita uma discussão sobre as diferenças entre
atomicidade e ordenação. Como discutido, uma leitura do tamanho de uma palavra sempre
ocorre atomicamente. Ele nunca termina com uma escrita para a mesma palavra; a leitura
sempre retorna a palavra em um estado consistente — talvez antes da escrita terminar, talvez
depois, mas nunca durante. Por exemplo, se um inteiro for inicialmente 42 e depois definido
como 365, uma leitura no inteiro sempre retornará 42 ou 365 e nunca alguma mistura dos dois
valores. Nós chamamos isso de atomicidade.
Seu código, no entanto, pode ter requisitos mais rigorosos do que este: Talvez você exija que
a leitura sempre ocorra antes da gravação pendente. Esse tipo de requisito não é de atomia,
mas de ordenação. A atomicidade garante que as instruções ocorram sem interrupção e que
elas sejam concluídas integralmente ou não sejam concluídas. A ordenação, por outro lado,
garante que a ordenação desejada e relativa de duas ou mais instruções — mesmo que elas
ocorram em segmentos separados de execução ou mesmo em processadores separados —
seja preservada.
As operações atômicas discutidas nesta seção garantem apenas a atomicidade. O
pedido é imposto por meio de operações de barreira, que serão discutidas
posteriormente neste capítulo.

Em seu código, geralmente é preferível escolher operações atômicas em


vez de mecanismos de bloqueio mais complexos. Na maioria das
arquiteturas, uma ou duas operações atômicas incorrem em menos
sobrecarga e menos sobrecarga da linha de cache do que um método de
sincronização mais complicado.Como acontece com qualquer código
sensível ao desempenho, no entanto, testar várias abordagens é sempre
inteligente.
www.it-ebooks.info
180 Capítulo 10 Métodos de sincronização do kernel

Operações Atômicas De 64 Bits


Com o aumento da prevalência de arquiteturas de 64 bits, não é surpresa que os
desenvolvedores do kernel do Linux tenham aumentado o tipo atomic_t de 32 bits com
uma variante de 64 bits, atomic64_t. Para portabilidade, o tamanho de atomic_t não pode
mudar entre arquiteturas, então atomic_t é 32 bits mesmo em arquiteturas de 64 bits. Em
vez disso, o tipo atomic64_t fornece um inteiro atômico de 64 bits que funciona de outra
forma idêntico ao seu irmão de 32 bits. O uso é exatamente o mesmo, exceto que o
intervalo utilizável do inteiro é de 64 bits, em vez de 32 bits. Quase todas as operações
atômicas clássicas de 32 bits são implementadas em variantes de 64 bits; elas são
prefixadas com atomic64 em vez de atomic.Table 10.2 é uma lista completa das
operações padrão; algumas arquetecnologias implementam mais, mas elas não são
portáteis.Como com atomic_t, o tipo atomic64_t é apenas um wrapper simples em torno de
um inteiro, este tipo a ogilong1:

Quadro 10.2 Métodos Atomic Integer


Operação Atômica de Número Inteiro Descrição
ATOMIC64_ INIT( long i) Na declaração, inicialize para i.
long atomic64_read(atomic64_t *v) Leia atomicamente o valor inteiro de v.
void atomic64_ set( atomic64_ t * v, int i) Atomicamente definir v igual a i.
void atomic64_ add( int i, atomic64_ t * v) Adicionar atomicamente i a v.
void atomic64_ sub( int i, atomic64_ t * v) Subtrair atomicamente i de v.
void atomic64_ inc( atomic64_ t * v) Adicionar atomicamente um a v.
void atomic64_ dec( atomic64_ t * v) Subtrair atomicamente um de v.
int atomic64_sub_and_test(int i, atomic64_t *v) Subtrair atomicamente i de v e
retorna true se o resultado for
zero;
caso contrário, falso.
int atomic64_add_deny(int i, atomic64_t *v) Adicionar atomicamente i a v e retornar true if
o resultado é negativo; caso contrário, falso.
long atomic64_add_return(int i, atomic64_t *v) Adicionar atomicamente i a v e retornar o
resultado.
long atomic64_sub_return(int i, atomic64_t *v) Subtrair atomicamente i de v e
retorna o resultado.
long atomic64_inc_return(int i, atomic64_t *v) Incrementar atomicamente v em um e
retorna o resultado.
long atomic64_dec_return(int i, atomic64_t *v) Diminui atomicamente v em um e
retorna o resultado.
int atomic64_dec_and_test(atomic64_t *v) Diminuir atomicamente v por um e
retorna true se zero; caso
contrário, false.
int atomic64_inc_and_test(atomic64_t *v) Incrementa atomicamente v em
um e retorna verdadeiro se o
resultado é zero; caso
contrário, retorna falso.
www.it-ebooks.info
Operações Atômicas

Todas as arquiteturas de 64 bits fornecem atomic64_t e uma família de funções


aritméticas para operar nele. A maioria das arquiteturas de 32 bits não suportam
atomic64_t—x86-32 é uma exceção notável. Para a portabilidade entre todas as
arquiteturas compatíveis com Linux, os desenvolvedores devem usar o tipo
atomic_t de 32 bits.O atomic64_t de 64 bits é reservado para o código específico da arquitetura e
que requer 64 bits.

Operações Atômicas Bit A Bit


Além das operações de inteiros atômicos, o núcleo também fornece uma família de funções
que operam no nível de bits. Não é de surpreender que sejam específicos da arquitetura e
definidos
<asm/bitops.h>.
O que pode ser surpreendente é que as funções bit a bit operam em ad-dresses de
memória genérica. Os argumentos são um ponteiro e um número de bit. O bit zero é o bit
menos significativo do endereço fornecido. Em máquinas de 32 bits, o bit 31 é o bit mais
significativo e o bit 32 é o bit menos significativo da seguinte palavra.Não há limitações no
número de bits fornecido; embora, a maioria dos usos das funções forneça uma palavra e,
consequentemente, um número de bits entre 0 e 31 em máquinas de 32 bits e 0 e 63 em
máquinas de 64 bits.
Como as funções operam em um ponteiro genérico, não há
equivalente do tipo atomic_t do inteiro atômico. Em vez disso, você
pode trabalhar com um ponteiro para os dados desejados. Considere
um exemplo:
www.it-ebooks.info
182 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.

Quadro 10.3 Métodos atômicos bit a bit


Operação Atômica Bit A Bit Descrição
void set_ bit( int nr, void *addr) Configurar atomicamente o n-ésimo bit iniciando
do endereço
void clear_bit( int nr, void *addr) Limpe atomicamente o n-ésimo
bit começando de addr.
void change_bit(int nr, void *addr) Inverta atomicamente o valor do
n-ésimo bit começando do addr.
int test_and_set_bit(int nr, void *addr) Defina atomicamente o n-ésimo
bit começando de addr e retorne
o valor anterior.
int test_and_clear_bit(int nr, void *addr) Limpe atomicamente o n-ésimo
bit começando de addr e retorne o
valor anterior.
int test_and_change_bit(int nr, void *addr) Inverta atomicamente o n-ésimo
bit começando de addr e retorne
o valor anterior.
int test_bit(int nr, void *addr) Retorna atomicamente o valor do
n-ésimo bit começando de addr.

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

O que é uma operação de bit não atômico?


À primeira vista, o conceito de uma operação de bit não atômico pode não fazer
nenhum sentido. Apenas um único bit está envolvido; portanto, não há
possibilidade de inconsistência. Se uma das operações for bem-sucedida, o que
mais poderá importar? Claro, ordenar pode ser importante, mas estamos falando de
atomicidade aqui. No final do dia, se o bit tiver um valor fornecido por qualquer uma
das in-structions, nós deveríamos estar prontos, certo?
Vamos voltar ao que a atomicidade significa. A atomicidade requer que as instruções
sejam totalmente bem-sucedidas, ininterruptas ou que as instruções não sejam
executadas. Portanto, se você emitir duas operações de bit atômico, você espera que
duas operações sejam bem-sucedidas. Após a conclusão de ambas as operações, o bit
precisa ter o valor especificado pela segunda operação. Mais acima, no entanto, em
algum momento antes da operação final, o bit precisa manter o valor especificado pela
primeira operação. Em termos mais gerais, a atomicidade real requer que todos os
estados intermediários sejam realizados corretamente.
Por exemplo, suponha que você emita duas operações de bit atômico: inicialmente
defina o bit e, em seguida, limpe-o. Sem operações atômicas, o bit pode acabar
limpo, mas pode nunca ter sido definido. A operação de definição pode ocorrer
simultaneamente com a operação de limpeza e falhar. A operação de limpeza seria
bem-sucedida, e o bit emergiria limpo como pretendido. Com operações atômicas,
no entanto, o conjunto realmente ocorreria - haveria um momento no tempo em que
uma leitura mostraria o bit como definido - e então o clear seria executado e o bit
seria zero.
Esse comportamento pode ser importante, especialmente quando o pedido entra
em jogo ou quando se lida com registros de hardware.

O kernel também fornece rotinas para encontrar o primeiro bit


definido (ou não definido) começando em um determinado endereço:

Ambas as funções têm um ponteiro como seu primeiro argumento e o número de


bits no total para pesquisar como seu segundo. Elas retornam o número de bits do
primeiro bit definido ou do primeiro bit não definido, respectivamente. Se o seu
código está pesquisando apenas uma palavra, as rotinas __ffs() e ffz(), que usam um
único parâmetro da palavra na qual pesquisar, são ideais.
Ao contrário das operações de inteiros atômicos, o código normalmente não
tem escolha se deve usar as operações bit a bit - elas são a única maneira
portátil de definir um bit específico.A única questão é se deve usar as variantes
atômicas ou não atômicas. Se seu código é inerentemente seguro de condições
de corrida, você pode usar as versões não atômicas, que podem ser mais
rápidas dependendo da arquitetura.

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.

Métodos de bloqueio de rotação


Os bloqueios de rotação dependem da arquitetura e são implementados
no assembly.O código dependente da arquitetura é definido em
<asm/spinlock.h>.As interfaces utilizáveis reais são definidas em
<linux/spinlock.h>.O uso básico de um bloqueio de rotação é
1 Isto é especialmente importante agora que o núcleo é preventivo. A duração dos bloqueios é
equivalente à latência de agendamento do sistema.

www.it-ebooks.info
Travas de Rotação

O bloqueio pode ser mantido simultaneamente por, no máximo, apenas um segmento de


execução. Consequentemente, somente um segmento é permitido na região crítica de cada
vez. Isso fornece a proteção necessária contra concorrência em máquinas de
multiprocessamento. Em uniprocessor ma-chines, os bloqueios são compilados e não
existem; eles simplesmente agem como marcadores para desativar e habilitar a preempção
do kernel. Se o kernel preempt estiver desativado, os bloqueios serão compilados
completamente.

Aviso: Os Bloqueios De Rotação Não São Recursivos!


Ao contrário das implementações de spin lock em outros sistemas operacionais e
bibliotecas de threading, os spin locks do kernel Linux não são recursivos. Isto significa
que se você tentar adquirir um bloqueio que você já possui, você irá girar, esperando
por si mesmo para liberar o bloqueio. Mas porque você está ocupado girando, você
nunca vai soltar a fechadura e você vai estagnar. Tenha cuidado!

Bloqueios de giro podem ser usados em manipuladores de interrupção, enquanto


semáforos não podem ser usados por causa do sono. Se um bloqueio for usado em um
manipulador de interrupção, você também deverá desativar as interrupções locais
(solicitações de interrupção no processador atual) antes de obter o bloqueio. Caso contrário,
é possível que um manipulador de interrupção interrompa o código do kernel enquanto o
bloqueio é mantido e tenta readquirir o bloqueio. O manipulador de interrupção gira,
aguardando que o bloqueio se torne disponível. O suporte de bloqueio, no entanto, não é
executado até que o manipulador de interrupção seja concluído. Este é um exemplo do
impasse de aquisição dupla discutido no capítulo anterior. Observe que você precisa
desativar as interrupções somente no processador atual. Se ocorrer uma interrupção em um
processador diferente e ele girar na mesma trava, isso não impedirá que o suporte da trava
(que está em um processador diferente), no final, libere a trava.
O kernel fornece uma interface que desativa convenientemente as
interrupções e adquire o bloqueio. O uso é

A rotina spin_lock_irqsave()salva o estado atual das interrupções, desativa-as


localmente e obtém o bloqueio fornecido. Inversamente,
spin_unlock_irqrestore()desbloqueia o bloqueio dado e retorna interrupções para seu
estado anterior. Desta forma, se as interrupções fossem inicialmente
desabilitadas, seu código não as habilitaria erroneamente, mas as manteria
desabilitadas. Observe que a variável flags é aparentemente passada por valor.
Isso ocorre porque as rotinas de bloqueio são implementadas parcialmente
como macros.
Em sistemas uniprocessador, o exemplo anterior ainda deve desativar interrupções
para impedir que um manipulador de interrupção acesse os dados compartilhados, mas o
mecanismo de bloqueio é compilado fora.O bloqueio e desbloqueio também desativam e
ativam a preempção do kernel, respectivamente.

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.

Se você sempre souber antes do fato de que as interrupções estão


inicialmente ativadas, não há necessidade de restaurar seu estado anterior.Você
pode ativá-las incondicionalmente ao desbloquear. Nesses casos, spin_lock_irq() e
spin_unlock_irq() são ideais:

À 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!

Depurando bloqueios de rotação


A opção de configuração CONFIG_DEBUG_SPINLOCK permite algumas verificações de
depuração no código de bloqueio de rotação. Por exemplo, com esta opção o código de
bloqueio de rotação verifica o uso de bloqueios de rotação não inicializados e
desbloqueia um bloqueio que ainda não está bloqueado. Ao testar seu código, você
deve sempre executar com a depuração de bloqueio de rotação habilitada. Para
depuração adicional de ciclos de vida de bloqueio, habilite
CONFIG_DEBUG_LOCK_ALLOC.

Outros métodos de bloqueio de rotação


Você pode usar o método spin_lock_init() para inicializar um bloqueio de giro
criado dinamicamente (um spinlock_t ao qual você não tem uma referência
direta, apenas um ponteiro).
O método spin_trylock() tenta obter o spin lock fornecido. Se o bloqueio é contendido, em
vez de girar e esperar que o bloqueio seja liberado, a função imediatamente retorna zero. Se
conseguir obter o bloqueio, ele retornará um valor diferente de zero. Do mesmo modo,
www.it-ebooks.info
Travas de Rotação

retorna um valor diferente de zero se o lock fornecido for adquirido


spin_is_locked()
atualmente. Caso contrário, retorna zero. Em nenhum dos casos, spin_is_locked()
realmente obtém o lock.2
O quadro 10.4 mostra uma lista completa dos métodos de bloqueio de rotação
normalizados.

Quadro 10.4 Métodos de bloqueio de rotação


Método Descrição
spin_lock () Adquire determinado bloqueio
spin_lock_irq () Desabilita interrupções locais e adquire determinado bloqueio
spin_lock_irqsave() Salva o estado atual de interrupções locais, desativa inter-
rompe, e adquire dado bloqueio
spin_unlock () Libera o bloqueio fornecido
spin_unlock_irq () Libera determinado bloqueio e habilita interrupções locais
Libera determinado bloqueio e restaura interrupções locais para
spin_unlock_irqrestore () determinado pré-
estado vívido
spin_lock_init() Inicializa dinamicamente determinado spinlock_t
Tenta adquirir determinado bloqueio; se não estiver disponível,
spin_trylock() retorna diferente de zero
Retorna diferente de zero se o bloqueio fornecido for adquirido no
spin_is_locked() momento, other-
sábio ele retorna zero

Travas de rotação e metades inferiores


Como discutido no Capítulo 8, "Metades inferiores e trabalho diferido",
certas precau-ções de bloqueio devem ser tomadas quando se trabalha
com metades inferiores.A função spin_lock_bh() obtém o bloqueio dado e
desativa todas as metades inferiores.A função spin_unlock_bh() realiza o
inverso.
Como uma metade inferior pode antecipar o código de contexto do processo, se os
dados forem compartilhados entre uma metade inferior do contexto do processo, você
deverá proteger os dados no contexto do processo com um bloqueio e a desativação
das metades inferiores. Da mesma forma, como um manipulador de interrupção pode
antecipar uma metade inferior, se os dados forem compartilhados entre um
manipulador de interrupção e uma metade inferior, você deverá obter o bloqueio
apropriado e desativar as interrupções.

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

Lembre-se de que dois tasklets do mesmo tipo nunca são executados


simultaneamente.Portanto, não há necessidade de proteger os dados usados somente
em um único tipo de tasklet. Entretanto, se os dados forem compartilhados interpolar
duas tarefas diferentes, você deverá obter um bloqueio de rotação normal antes de
acessar os dados na metade inferior.Não é necessário desativar as metades inferiores
porque uma tarefa nunca impede a execução de outra tarefa no mesmo processador.
Com softirqs, independentemente de ser do mesmo tipo de softirq, se os
dados forem compartilhados por softirqs, eles devem ser protegidos com
uma fechadura. Lembre-se de que softirqs, mesmo dois do mesmo tipo,
podem ser executados simultaneamente em vários processadores do
sistema. Um softirq nunca se antecipa a outro softirq rodando no mesmo
processador, no entanto, desabilitar as metades inferiores não é necessário.

Bloqueios de Rotação de Leitor-Gravador


Às vezes, o uso de bloqueio pode ser claramente dividido em caminhos de leitor e
gravador. Por exemplo, considere uma lista que seja atualizada e
pesquisada.Quando a lista é atualizada (gravada), é importante que nenhum outro
thread de execução simultaneamente grave ou leia na lista. Escrever exige exclusão
mútua. Por outro lado, quando a lista é pesquisada (lida de), é importante que nada
mais grave na lista. Vários leitores simultâneos estão seguros, desde que não haja
escritores.Os padrões de acesso da lista de tarefas (discutidos no Capítulo
3,"Gerenciamento do Processo") se encaixam nessa descrição. Não é de
surpreender que um bloqueio de rotação leitor-gravador proteja a lista de tarefas.
Quando uma estrutura de dados é dividida em padrões de uso de leitor/gravador
ou consumidor/produtor, faz sentido usar um mecanismo de bloqueio que forneça
semânticas semelhantes.Para satisfazer esse uso, o kernel do Linux fornece
bloqueios de rotação de leitor-gravador. Os bloqueios de rotação leitor-gravador
fornecem variantes separadas de leitor e gravador do bloqueio. Um ou mais leitores
podem manter o bloqueio do leitor simultaneamente.O bloqueio do gravador, por
outro lado, pode ser mantido por no máximo um gravador sem leitores simultâneos.
Os bloqueios de leitor/gravador às vezes são chamados de bloqueios
compartilhados/exclusivos ou simultâneos/exclusivos porque o bloqueio está
disponível em uma forma compartilhada (para leitores) e exclusiva (para
gravadores).
O uso é semelhante aos bloqueios de rotação. O bloqueio de rotação leitor-
gravador é inicializado via

Em seguida, no caminho de código do leitor:

Finalmente, no caminho de código do gravador:


Normalmente, os leitores e escritores estão em caminhos de código
totalmente separados, como neste exemplo.

www.it-ebooks.info
Bloqueios de Rotação de Leitor-Grava

Observe que você não pode "atualizar" um bloqueio de leitura para um


bloqueio de gravação. Por exemplo, considere este trecho de código:

A execução dessas duas funções, conforme mostrado, causará um impasse,


à medida que o bloqueio de gravação gira, aguardando que todos os leitores
liberem o bloqueio compartilhado, incluindo você mesmo. Se você precisar
gravar, obtenha o bloqueio de gravação desde o início. Se a linha entre seus
leitores e escritores for suja de lama, isso pode ser uma indicação de que você
não precisa usar bloqueios leitor-escritor. Nesse caso, um bloqueio de rotação
normal é ideal.
É seguro que vários leitores obtenham o mesmo bloqueio. Na verdade, é seguro
para o mesmo
a obter recursivamente o mesmo bloqueio de leitura.Isso se presta a uma
otimização. Se você tiver apenas leitores em manipuladores de interrupção, mas não
tiver escritores, poderá misturar
o uso dos bloqueios de "desativação de interrupção".Você pode usar read_lock() em
vez de
read_lock_irqsave() para proteção do leitor.Você ainda precisa desativar as interrupções
para gravação
access, à la write_lock_irqsave(), caso contrário um leitor em uma interrupção poderia
causar deadlock
no bloqueio de gravação mantido. Consulte a Tabela 10.5 para obter uma lista
completa do bloqueio de rotação do leitor-gravador
métodos.

Quadro 10.5 Métodos De Bloqueio De Rotação De Leitor-Gravador


Método Descrição
read_ lock () Adquire o bloqueio fornecido para leitura
read_ lock_ irq () Desabilita as interrupções locais e adquire o bloqueio fornecido para
leitura
read_lock_irqsave() Salva o estado atual de interrupções locais, desabilita a entrada local
interrompe e adquire o bloqueio dado para leitura
read_unlock() Libera o bloqueio fornecido para leitura
read_ unlock_ irq () Libera determinado bloqueio e habilita interrupções locais
read_unlock_ irqrestore() Libera o lock fornecido e restaura as interrupções locais
para o estado anterior dado
write_lock () Adquire determinado bloqueio para gravação
write_lock_irq () Desabilita interrupções locais e adquire o bloqueio fornecido para
gravação

write_lock_irqsave() Salva o estado atual das interrupções locais,


desabilita as interrupções locais e adquire o bloqueio
especificado para gravação
write_unlock() Libera o bloqueio fornecido
write_unlock_irq () Libera determinado bloqueio e habilita interrupções locais

www.it-ebooks.info
190 Capítulo 10 Métodos de sincronização do kernel

Quadro 10.5 Métodos de bloqueio de rotação do leitor-gravador (continuação)


Método Descrição
write_unlock_irqrestore() Libera o bloqueio fornecido e restaura as
interrupções locais ao estado anterior fornecido
write_trylock() Tenta adquirir determinado bloqueio para gravação; se
não estiver disponível, retorna diferente de zero
rwlock_ init () Inicializa determinado rwlock_t

Uma consideração final importante ao usar os bloqueios de spin de leitor-escritor


Linux é que eles favorecem os leitores em detrimento dos escritores. Se o bloqueio de
leitura for mantido e um gravador estiver aguardando o acesso exclusivo, os leitores
que tentarem adquirir o bloqueio continuarão a ter êxito.O gravador giratório não
adquire o bloqueio até que todos os leitores soltem o bloqueio.Portanto, um número
suficiente de leitores pode deixar os gravadores pendentes com fome.Isso é importante
ter em mente ao projetar o bloqueio. Às vezes, esse comportamento é benéfico; às
vezes, é catastrófico.
Os bloqueios de rotação fornecem um bloqueio rápido e simples.O
comportamento de rotação é ideal para tempos de espera curtos e código
que não consegue dormir (manipuladores de interrupção, por exemplo). Nos
casos em que o tempo de sono pode ser longo ou você potencialmente
precisa dormir enquanto segura a trava, o semáforo é uma solução.

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:

n Como as tarefas de contenção dormem enquanto aguardam o bloqueio se


tornar disponível, os semáforos são adequados para bloqueios que são mantidos por um
longo tempo.
www.it-ebooks.info
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.

Esses fatos destacam os usos de semáforos versus bloqueios de spin. Na


maioria dos usos de sema-foros, há pouca escolha sobre qual bloqueio usar. Se o
código precisa ser suspenso, o que geralmente acontece ao sincronizar com o
espaço do usuário, os semáforos são a única solução. Muitas vezes é mais fácil, se
não necessário, usar semáforos porque eles permitem a flexibilidade de
dormir.Quando você tem uma escolha, a decisão entre semáforo e bloqueio de giro
deve ser baseada no tempo de bloqueio de bloqueio. Idealmente, todos os seus
bloqueios devem ser mantidos tão brevemente quanto posível.Com os semáforos,
no entanto, tempos de bloqueio mais longos são mais aceitáveis.Além disso, ao
contrário dos bloqueios de rotação, os semáforos não desabilitam a preempção do
kernel e, consequentemente, o código que contém um semáforo pode ser
preempted.This significa que os semáforos não afetam adversamente a latência de
agendamento.

Contagem e Semáforos Binários


Um recurso útil final dos semáforos é que eles podem permitir um número arbitrário de
detentores de bloqueio si-multaneous.Enquanto os bloqueios de rotação permitem no
máximo uma tarefa para manter o bloqueio de cada vez, o número de detentores
simultâneos permitidos de semáforos pode ser definido no tempo de declaração-tion.Este
valor é chamado de contagem de uso ou simplesmente a contagem.O valor mais comum é
permitir, como bloqueios de rotação, apenas um detentor de bloqueio de cada vez. Nesse
caso, a contagem é igual a um, e o semáforo é chamado de semáforo binário (porque é
mantido por uma tarefa ou não é mantido) ou um mutex (porque impõe exclusão
mútua).Alternativamente, a contagem pode ser inicializada com um valor diferente de zero
maior que um. Neste caso, o semáforo é chamado de semáforo de contagem, e ele permite,
no máximo, contar os titulares da fechadura de cada vez. Os semáforos de contagem não
são usados para impor a exclusão mútua porque permitem vários threads de execução na
região crítica de uma vez. Em vez disso, eles são usados para impor limites em determinado
código. Eles não são muito usados no kernel. Se você usa um sema-forno, você quase
certamente quer usar um mutex (um semáforo com uma contagem de um).
www.it-ebooks.info
192 Capítulo 10 Métodos de sincronização do kernel

Os semáforos foram formalizados por Edsger Wybe Dijkstra3 em 1968 como um


mecanismo de bloqueio generalizado.Um semáforo suporta duas operações atômicas,
P() e V(), nomeado em homenagem à palavra holandesa Proberen, para testar
(literalmente, para sondar), e a palavra holandesa Verhogen, para em cremento.
Sistemas posteriores chamaram esses métodos de down() e up(), respectivamente, assim
como o Linux.O método de down() é usado para adquirir um semáforo, diminuindo a
contagem em um. Se a nova contagem for zero ou maior, o bloqueio será adquirido e a
tarefa poderá entrar na região crítica. Se a contagem for negativa, a tarefa será colocada
em uma fila de espera, e o processo-sor passará para outra coisa.Esses nomes são
usados como verbos:Você desce um semáforo para adquiri-lo.O método up() é usado
para liberar um semáforo após a conclusão de uma região crítica.Isso é chamado de
upping do semáforo.O método incrementa o valor de contagem; se a fila de espera do
semáforo não estiver vazia, uma das tarefas em espera será ativada e poderá adquirir o
semáforo.

Criando e inicializando semáforos


A implementação de semáforo depende da arquitetura e é definida em
<asm/semaphore.h>.O tipo de semáforo struct representa semáforos. Os semáforos
apagados estaticamente são criados através do seguinte, onde name é o
nome da variável e count é a contagem de uso do semáforo:

Como atalho para criar o mutex mais comum, use o seguinte, onde,
novamente, name é o nome da variável do semáforo binário:

Mais frequentemente, os semáforos são criados dinamicamente, muitas


vezes como parte de uma estrutura maior. Neste caso, para inicializar um
semáforo criado dinamicamente para o qual você tem apenas uma
referência de ponteiro indireto, basta chamar sema_init(), onde sem é um
ponteiro e contagem é a contagem us-age do semáforo:

Da mesma forma, para inicializar um mutex criado dinamicamente, você


pode usar

3 Dr. Dijkstra (1930-2002) é um dos mais talentosos cientistas da computação na


(reconhecidamente breve) história dos cientistas da computação. Suas inúmeras contribuições
incluem trabalho em design de SO, teoria de algoritmos e o conceito de semáforos. Ele nasceu em
Roterdã, Holanda, e lecionou na Universidade do Texas por 15 anos. Ele provavelmente não ficaria feliz
com o grande número de estados GOTO no kernel do Linux, no entanto.
www.it-ebooks.info
Semáforos 196

Eu não sei porque o "mutex" em init_MUTEX() está em maiúscula ou porque o


"init"
vem primeiro aqui, mas segundo em sema_init(). Suspeito que depois de ler o capítulo
8, o
inconsistência não é surpreendente.

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:

Uma listagem completa dos métodos de semáforo está na Tabela 10.6.

Quadro 10.6 Métodos de Semáforo


Método Descrição
sema_init(struct semáforo *, int) Inicializa o semáforo criado dinamicamente
para a contagem fornecida

init_MUTEX(semáforo struct *) Inicializa o semáforo criado


dinamicamente com a contagem de um
init_MUTEX_LOCKED(semáforo struct *) Inicializa o semáforo criado dinamicamente
com uma contagem de zero (então ele é
bloqueado inicialmente)
www.it-ebooks.info
Capítulo
194º 10 Métodos de Sincronização do Kernel

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

down(semáforo struct *) Tenta adquirir o semáforo dado e entrar em


sono ininterrupto se for contestado
down_trylock(semáforo struct *) Tenta adquirir o semáforo fornecido e
retornar imediatamente um valor diferente de
up(semáforo struct *) zero se for contestado
Libera o semáforo fornecido e ativa uma
tarefa em espera, se houver

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:

Os semáforos de leitura-gravação criados dinamicamente são inicializados


via

Todos os semáforos leitor-gravador são mutexes — ou seja, sua contagem de uso é um


— embora eles impõem exclusão mútua apenas para gravadores, não para leitores.Qualquer
número de leitores pode manter o bloqueio de leitura simultaneamente, desde que não haja
gravadores. Por outro lado, apenas um único gravador (sem leitores) pode adquirir a
variante de gravação do bloqueio. Todos os bloqueios de leitor-gravador usam sono
ininterrupto, de modo que há apenas uma versão de cada down(). Por exemplo:
www.it-ebooks.info
Mutexes 197

Como em semáforos, implementações de down_read_trylock() e


down_write_trylock() são fornecidos. Cada um tem um parâmetro: um ponteiro para um
leitor-
gravador semaphore.Ambos retornarão um valor diferente de zero se o bloqueio for
adquirido com êxito e zero
se for atualmente invocada. Cuidado: É verdade que não há uma boa razão para isso, é o
contrário
de comportamento semáforo normal!
Os semáforos leitor-gravador têm um método exclusivo que seus
primos de bloqueio de rotação leitor-gravador não têm:
downgrade_write().Essa função converte atomicamente um bloqueio de
gravação adquirido em um bloqueio de leitura.
Semáforos de leitura-gravação, como bloqueios de rotação da mesma
natureza, não devem ser usados, a menos que exista uma separação clara
entre caminhos de gravação e caminhos de leitura em seu código. Suportar
os mecanismos leitor-escritor tem um custo, e só vale a pena se seu código
naturalmente se divide ao longo de um limite leitor/escritor.

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:

Para inicializar dinamicamente um mutex, você chama


www.it-ebooks.info
196 Capítulo 10 Métodos de sincronização do kernel

Bloquear e desbloquear o mutex é fácil:

É isso! Mais simples que um semáforo e sem a necessidade de gerenciar o número


de utilizações.
O quadro 10.7 apresenta uma lista dos métodos de exclusão mútua de base.

Quadro 10.7 Métodos Mutex


Método Descrição
mutex_lock(struct mutex *) Bloqueia o mutex fornecido; dorme se o bloqueio for
indisponível
mutex_unlock(struct mutex *) Desbloqueia o mutex fornecido
mutex_trylock(struct mutex *) Tenta adquirir o mutex fornecido; retorna um se suc-
bem-sucedido e o bloqueio é adquirido; caso contrário,
zero
Retorna um se o bloqueio estiver bloqueado e zero, caso
mutex_is_locked (struct mutex *) contrário

A simplicidade e eficiência do mutex vem das restrições adicionais que ele


impõe aos seus usuários além do que o semáforo requer. Ao contrário de um
semáforo, que implementa o comportamento mais básico de acordo com o de-
sign original de Dijkstra, o mutex tem um caso de uso mais estrito e estreito:

n Somente uma tarefa pode manter o mutex de cada vez.Ou seja, a


contagem de uso em um mutex é sempre uma.
n Quem bloqueou um mutex deve desbloqueá-lo.Ou seja, você não pode bloquear um
mutex em um contexto e, em seguida, desbloqueá-lo em outro.Isso significa que o mutex não é
adequado para sincronizações mais complicadas entre o kernel e o espaço do usuário. A maioria
dos casos de uso, no entanto, bloqueie e desbloqueie perfeitamente do mesmo contexto.
n Bloqueios e desbloqueios recursivos não são permitidos. Ou seja, você não
pode adquirir recursivamente o mesmo mutex e não pode desbloquear um mutex
desbloqueado.
n Um processo não pode ser encerrado enquanto mantém um mutex.
n Um mutex não pode ser adquirido por um manipulador de interrupção ou
pela metade inferior, mesmo com
mutex_trylock().
n Um mutex só pode ser gerenciado através da API oficial: Ele deve ser inicializado através
dos métodos de metanfetamina descritos nesta seção e não pode ser copiado, inicializado manualmente
ou reinicializado.
Talvez o aspecto mais útil do novo mutex struct seja que, por meio de um modo de
depuração especial, o kernel pode programaticamente verificar e avisar sobre violações
dessas restrições.Quando a opção de configuração do kernel CONFIG_DEBUG_MUTEXES estiver
habilitada, um
www.it-ebooks.info
Variáveis de conclusão

inúmeras verificações de depuração garantem que essas (e outras)


restrições sejam sempre mantidas.Isso permite que você e outros
usuários do mutex garantam um padrão de uso regimentado e simples.

Semáforos Versus Mutexes


Mutexes e semáforos são semelhantes. Ter ambos no kernel é confuso.Felizmente,
a fórmula que dita qual usar é bastante simples: A menos que uma das restrições
adicionais do mutex impeçam você de usá-los, prefira o novo tipo de mutex para
semáforos.Ao escrever o novo código, apenas os usos específicos, muitas vezes
de baixo nível, precisam de um semáforo. Comece com um mu-tex e passe para um
semáforo somente se você encontrar uma de suas restrições e não tiver outra
alternativa.

Bloqueios de rotação versus mutexes


Saber quando usar um bloqueio de rotação em vez de um mutex (ou semáforo)
é importante para escrever o código ideal. Em muitos casos, no entanto, há
pouca escolha. Apenas um bloqueio de rotação pode ser usado no contexto de
interrupção, enquanto apenas um mutex pode ser mantido enquanto uma tarefa
está em suspensão.A Tabela 10.8 revê os requisitos que ditam qual bloqueio
deve ser usado.

Quadro 10.8 O que usar: Spin Locks Versus Semáforos


Requisito Bloqueio Recomendado
Bloqueio de sobrecarga baixa É preferível o bloqueio de rotação.
Tempo de espera de bloqueio curto É preferível o bloqueio de rotação.
Longo tempo de espera de bloqueio O mutex é preferido.
É necessário bloquear a partir do contexto de interrupção O bloqueio de rotação é necessário.
Necessidade de dormir ao manter o bloqueio O mutex é necessário.

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

Uma variável de conclusão criada dinamicamente é inicializada via


init_complete(). Em uma determinada variável de conclusão, as tarefas que
desejam aguardar
wait_for_completed().Após o evento ocorrer, chamar complete() sinaliza todas as tarefas
em espera para wake up.Table 10.9 tem uma listagem dos métodos da variável
complete.

Quadro 10.9 Métodos de variável de conclusão


Método Descrição
init_complete(conclusão de struct *) Inicializa o dado criado dinamicamente
variável de conclusão
wait_for_completed(conclusão de struct *) Aguarda a variável de conclusão fornecida
a sinalizar
Sinaliza qualquer tarefa em espera para
complete(conclusão de struct *) ativar

Para exemplos de uso de variáveis de conclusão, consulte kernel/sched.c e


kernel/fork.c.
Um uso comum é ter uma variável de conclusão criada dinamicamente como um membro
de um
estrutura de dados. Código do kernel aguardando a inicialização das chamadas
da estrutura de dados
wait_for_completed().Quando a inicialização é concluída, as tarefas em espera são
awak-
necessário por meio de uma chamada para complete().

BKL: A Grande Fechadura do Kernel


Bem-vindo ao enteado ruivo do kernel.O Big Kernel Lock (BKL) é um bloqueio de
spin global que foi criado para facilitar a transição da implementação SMP original
do Linux para o bloqueio de grão fino.O BKL tem algumas propriedades
interessantes:

n Você pode suspender enquanto mantém a BKL.O bloqueio é


automaticamente cancelado quando a tarefa é desagendada e readquirida quando a
tarefa é reagendada. Claro, isso não significa que é sempre seguro dormir enquanto
segura a BKL, apenas que você pode e você não vai deadlock.
n O BKL é um bloqueio recursivo.Um único processo pode adquirir o
bloqueio várias vezes e não um deadlock, como faria com um bloqueio de giro.
n Você pode usar o BKL somente no contexto do processo. Ao
contrário dos bloqueios de rotação, não é possível adquirir o BKL no contexto de
interrupção.
n Novos usuários do BKL são proibidos.Com cada versão do kernel,
cada vez menos drivers e subsistemas dependem do BKL.
Esses recursos ajudaram a facilitar a transição da versão 2.0 para a 2.2.Quando o
suporte a SMP foi introduzido na versão 2.0 do kernel, apenas uma tarefa poderia estar no
kernel de cada vez. É claro que agora o kernel está finamente roscado, percorremos um
longo caminho.O objetivo do 2.2 era permitir que vários processadores executassem no
kernel simultaneamente.O BKL

www.it-ebooks.info
BKL: A Grande Fechadura do Kernel

foi introduzido para ajudar a facilitar a transição para o bloqueio mais


fino. Era uma grande ajuda naquela época; agora é um fardo de
escalabilidade.
O uso do BKL é desencorajado. Na verdade, o novo código nunca deve introduzir o
bloqueio que usa o BKL. O bloqueio ainda é razoavelmente bem usado em partes do
kernel, entretanto. Portanto, compreender o BKL e suas interfaces é importante. O BKL
se comporta como um bloqueio de giro, com as adições discutidas anteriormente.A
função lock_kernel() adquire o bloqueio e a função unlock_kernel() libera o bloqueio.Um único
thread de execução pode adquirir o bloqueio recursivamente, mas deve então chamar
unlock_kernel() um número igual de vezes para liberar o bloqueio. Na última chamada
unlock, o lock será liberado. A função kernel_locked() retorna um valor diferente de zero se o
lock for mantido no momento; caso contrário, retorna zero. Essas interfaces são declaradas
em <linux/smp_lock.h>. Veja a seguir exemplos de uso:

O BKL também desabilita a preempção do kernel enquanto ele é


mantido. Nos kernels UP, o código BKL não executa nenhum bloqueio
físico.A Tabela 10.10 tem uma lista completa das funções BKL.

Quadro 10.10 Métodos BKL


Função Descrição
lock_ kernel () Adquire a BKL.

unlock_ kernel () Libera o BKL.


Retorna diferente de zero se o bloqueio for mantido e zero, caso contrário.
kernel_ locked () (UP sempre
retorna diferente de zero.)

Uma das principais questões relativas ao BKL é determinar o que o bloqueio


está protegendo-ing.Muitas vezes, o BKL é aparentemente associado com o
código (por exemplo,"ele sincroniza chamadores para foo()") em vez de dados ("ele
protege a estrutura foo").Isso torna a substituição de usos do BKL com um
bloqueio de rotação difícil porque não é fácil determinar apenas o que está sendo
bloqueado.A substituição é feita ainda mais difícil em que a relação entre todos os
usuários do BKL precisa ser determinada.
www.it-ebooks.info
200 Capítulo 10 Métodos de sincronização 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 é

Isso parece um código de bloqueio de rotação normal. A


estranheza vem com o caminho de leitura, que é um pouco diferente:

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:

n Seus dados têm muitos leitores.


n Seus dados têm poucos gravadores.
n Embora poucos em número, você quer favorecer escritores sobre
leitores e nunca permitir que leitores morram de fome escritores.
n Seus dados são simples, como uma estrutura simples ou mesmo um
único inteiro que, por alguma razão, não pode ser transformado em atômico.
Um usuário proeminente do seq lock é jiffies, a variável que armazena o tempo de
atividade de uma máquina Linux (consulte o Chapter 11,"Timers and Time
Management"). Jiffies mantém uma contagem de 64 bits de
www.it-ebooks.info

Você também pode gostar