Escolar Documentos
Profissional Documentos
Cultura Documentos
01 PT SYSTRAN Generic
01 PT SYSTRAN Generic
info
Desenvolvi
mento de
kernel Linux
Terceira Edição
www.it-ebooks.info
Biblioteca do desenvolvedor
REFERÊNCIAS ESSENCIAIS PARA PROFISSIONAIS DE PROGRAMAÇÃO
Desenvolvedor
Biblioteca
informit.com/devlibrary
www.it-ebooks.info
Desenvolvi
mento de
kernel Linux
Terceira Edição
Robert Love
www.it-ebooks.info
Visão geral do conteúdo
1 Introdução ao Linux Kernel 1
4. Agendamento do Processo 41
5. Chamadas do Sistema 69
Bibliografia 407
Índice 411
www.it-ebooks.info
Sumário
1 Introdução ao Linux Kernel
História do Unix 1
Along Came Linus: Introdução ao Linux 3
Visão geral de sistemas operacionais e kernels
Kernels Linux Versus Unix Clássico 6
Kernel Linux versão 8
Comunidade de desenvolvimento do kernel Linux
Antes de Começarmos 10.
2 Introdução ao Kernel
Obtendo a origem do kernel 11.
Usando o Git 11.
Instalando a origem do kernel
Usando Patches 12
A árvore de origem do kernel 12
Construindo o kernel 13º
Configurando o kernel 14.
Minimizando o ruído da compilação 15
Gerando Vários Trabalhos de Compilação
Instalando o novo kernel 16º
Uma Besta de Natureza Diferente
Sem cabeçalhos libc ou padrão
C 18 GNU
Funções Embutidas 18º
Montagem in-line 19.
Anotação de Ramificação 19.
Sem proteção de memória 20.
Sem (fácil) utilização de ponto flutuante
Pilha pequena de tamanho fixo 20.
Sincronização e simultaneidade 21
Importância da portabilidade 21
Conclusão 21
www.it-ebooks.info
viii Sumário
4. Agendamento do Processo 41
Multitarefa 41
Programador de processos do Linux 42º
Política 43º
Processos De E/S Associados Versus Processos Associados Ao
Processador 43º
Prioridade do Processo 44º
Fatia do Tempo 45
A política de agendamento em ação 45º
O algoritmo de agendamento do Linux 46
Classes do Agendador 46
Programação de processos em sistemas Unix 47º
Regularização 48º
A implementação da programação do Linux 50º
Contabilização de Tempo 50º
A Estrutura da Entidade do Agendador 50º
O Tempo de Execução Virtual 51
www.it-ebooks.info
Sumário
ix)
www.it-ebooks.info
Sumário xi
www.it-ebooks.info
xii Sumário
Softirqs 137
Implementação do Softirqs 137
O manipulador Softirq 138
Executando Softirqs 138
Uso do Softirqs 140º
Atribuindo um Índice 140º
Registrando seu manipulador 141º
Levantando o seu Softirq 141º
Tarefas 142º
Implementando Tasklets 142º
A Estrutura do Tasklet 142º
Agendamento de Tasklets 143º
Uso de Tasklets 144º
Declarando seu Tasklet 144
Escrevendo o Manipulador do Tasklet 145º
Agendamento do seu Tasklet 145º
ksoftirqdComment 146º
O antigo mecanismo BH 148
Filas de trabalho 149
Implementando filas de trabalho 149
Estruturas de dados que representam os threads 149
Estruturas de Dados que Representam o Trabalho 150
Resumo da implementação da fila de trabalhos 152
Usando filas de trabalho 153º
Criando Trabalho 153º
Seu manipulador de filas de trabalho 153º
Trabalho de Agendamento 153º
Trabalho de descarga 154
Criando novas filas de trabalhos 154
O antigo mecanismo da fila de tarefas 155º
Qual Metade Inferior Devo Usar? 156º
Bloqueio entre as metades inferiores 157
Desativação das metades inferiores 157
Conclusão 159º
www.it-ebooks.info
Sumário xiii
Bloqueio 165
Causas da simultaneidade 167
Sabendo o que proteger 168
Deadlocks 169
Contenção e escalabilidade 171º
Conclusão 172
www.it-ebooks.info
xiv Sumário
Jiffies 212
Representação interna da Jiffies 213
Jiffies Envolvente 214
Espaço do Usuário e HZ 216
Relógios e cronômetros de hardware 216
Relógio em tempo real 217
Timer do sistema 217
O manipulador de interrupção do temporizador 217
A Hora do Dia 220
Temporizadores 222
Uso de Temporizadores 222
Condições de Corrida do Temporizador 224
Implementação do timer 224
Atrasando Execução 225
Loop Ocupado 225
Pequenos atrasos 226
schedule_timeout() 227
schedule_timeout() Implementação 228
Suspendendo em uma Fila de Espera, com um Timeout 229
Conclusão 230
www.it-ebooks.info
Sumário
Vx
www.it-ebooks.info
Sumário
xvii
18 Depuração 363
Primeiros passos 363
Erros no Kernel 364
Depuração por impressão 364
Robustez 365
Níveis de log 365
O Buffer de Log 366
syslogd e klogd 367
Transpondo printf() e printk() 367
Opa 367
ksymoops 369
kallsyms 369
Opções de Depuração do Kernel 370
Declarando Erros e Despejando Informações 370
Chave Magic SysRq 371
A saga de um depurador de kernel 372
gdbstar name 372
kgdb 373
Poking e sondagem do sistema 373
Uso do UID como Condicional 373
Uso de Variáveis de Condição 374
Uso de Estatísticas 374
Limitação de taxa e ocorrência da sua depuração 375
www.it-ebooks.info
Sumário
xix
Bibliografia 407
Índice 411
www.it-ebooks.info
Prefácio
Andrew Morton
www.it-ebooks.info
Prefáci
o
Quando fui abordado pela primeira vez sobre a conversão de minhas experiências com o
kernel Linux em um livro, eu prossegui com trepidation.What iria colocar o meu livro no
topo do seu assunto? Eu não estava interessado a não ser que pudesse fazer algo
especial, um trabalho do melhor da classe.
Percebi que poderia oferecer uma abordagem única para o tópico. Meu trabalho é
hackear o kernel. Meu passatempo é hackear o kernel. Meu amor está cortando o miolo. Ao
longo dos anos, eu tenho acu-mulado interessantes anedotas e dicas de informação
privilegiada. Com minhas experiências, eu poderia escrever um livro sobre como hackear o
kernel e - tão importante quanto - como não hackear o kernel. Em primeiro lugar, trata-se de
um livro sobre o design e a implementação do kernel do Linux.No entanto, a abordagem
deste livro difere dos concorrentes potenciais, na medida em que as informações são dadas
com uma inclinação para aprender o suficiente para realmente realizar o trabalho — e
realizá-lo corretamente. Sou um engenheiro pragmático e este é um livro prático. Deve ser
divertido, fácil de ler e útil.
Espero que os leitores possam sair deste trabalho com uma melhor
compreensão das regras (escritas e não escritas) do kernel do Linux. Eu pretendo
que você, fresco de ler este livro e o código-fonte do kernel, pode saltar dentro e
começar a escrever útil, correto, código kernel limpo. Claro, você também pode ler
este livro só por diversão.
Essa foi a primeira edição.O tempo passou, e agora voltamos mais uma vez à briga. Esta
terceira edição oferece bastante mais sobre a primeira e segunda: polimento intenso e
revisão, atualizações, e muitas seções novas e todos os novos capítulos.Esta edição
incorpora mudanças no kernel desde a segunda edição. Mais importante, no entanto, é a
decisão tomada pela comunidade do kernel Linux de não prosseguir com um kernel de
desenvolvimento 2.7 no curto a médio prazo.1 Em vez disso, os desenvolvedores do kernel
planejam continuar a desenvolver e estabilizar a série 2.6. Esta decisão tem muitas
implicações, mas o item de relevância para este livro é que há um pouco de poder de
permanência em um livro contemporâneo sobre o kernel 2.6 Linux. À medida que o kernel do
Linux amadurece, há uma maior chance de um instantâneo do kernel permanecer
representativo para o futuro. Este livro funciona como a documentação canônica para o
kernel, documentando-o com uma compreensão de sua história e um olho para o futuro.
www.it-ebooks.info
É imperativo que você utilize o código-fonte.A disponibilidade aberta do código-
fonte para o sistema Linux é um presente raro que você não deve tomar por garantido.
No entanto, não basta apenas ler a fonte.Você precisa escavar e alterar algum código.
Encontre um bug e corrija-o. Melhore os drivers para o seu hardware. Adicione alguns
novos recursos, mesmo que seja trivial. Encontre uma coceira e arranhe-a! Só quando
você escrever o código é que tudo se juntará.
Versão do Kernel
Este livro é baseado na série 2.6 do kernel Linux. Ele não cobre os kernels mais
antigos, exceto pela relevância histórica.Discutimos, por exemplo, como certos
subsistemas são implementados na série 2.4 do kernel Linux, já que suas
implementações mais simples são auxiliares de ensino úteis. Especificamente, este
livro está atualizado a partir do kernel Linux versão 2.6.34. Embora o ker-nel seja um
alvo comovente e nenhum esforço pode esperar capturar uma besta dinâmica de
uma maneira sem tempo, minha intenção é que este livro é relevante para
desenvolvedores e usuários de ambos os kernels mais antigos e mais novos.
Embora este livro discute o kernel 2.6.34, eu fiz um esforço para garantir que o
material é factualmente correto com respeito ao kernel 2.6.32 também. Esta última
versão é sancionada como o kernel "empresarial" pelas várias distribuições Linux,
garantindo que vamos continuar a vê-lo em sistemas de produção e sob
desenvolvimento ativo por muitos anos. (2.6.9, 2.6.18 e 2.6.27 foram versões
semelhantes de "longo prazo".)
Público
Este livro tem como alvo desenvolvedores e usuários do Linux que estão
interessados em entender o kernel do Linux. Não é um comentário linha a linha do
código do kernel. Também não é um guia para o desenvolvimento de drivers ou
uma referência na API do kernel. Em vez disso, o objetivo deste livro é fornecer
informações suficientes sobre o design e a implementação do kernel Linux que um
programador suficientemente realizado pode começar a desenvolver código no
kernel. O desenvolvimento do kernel pode ser divertido e gratificante, e eu quero
apresentar o leitor para esse mundo tão prontamente quanto possível. Este livro, no
entanto, ao discutir tanto a teoria quanto a aplicação, deve apelar para leitores de
ambas as convicções acadêmicas e práticas. Sempre fui da mente de que é preciso
entender a teoria para entender a aplicação, mas tento equilibrar as duas nesse
trabalho. Espero que, quaisquer que sejam suas motivações para entender o kernel
do Linux, este livro explique o design e a implementação de forma suficiente para
suas necessidades.
Assim, este livro abrange tanto o uso de sistemas núcleo núcleo e seu design e
implementação. Penso que isto é importante e merece um momento de discussão. Um
bom exemplo é o Capítulo 8, "Metades inferiores e trabalho diferido", que abrange um
componente de drivers de dispositivo chamado metades inferiores. Nesse capítulo,
discuto o design e a implementação dos mecanismos da metade inferior do kernel (que
um desenvolvedor de núcleo do kernel ou acadêmico pode achar interessante) e como
realmente usar as interfaces exportadas para implementar sua própria metade inferior
(que um desenvolvedor de driver de dispositivo ou hacker casual pode achar
pertinente). Creio que todos os grupos podem considerar ambas as discussões
relevantes.
www.it-ebooks.info
desenvolvedor, que certamente precisa entender o funcionamento
interno do kernel, deve ter um bom entendimento de como as interfaces
são realmente usadas. Ao mesmo tempo, um gerador de driver de
dispositivo pode se beneficiar de um bom entendimento da
implementação por trás da interface.
Isso é semelhante a aprender a API de alguma biblioteca versus estudar a
implementação real da biblioteca. À primeira vista, um programador de aplicativos
precisa entender apenas a API — muitas vezes é ensinado a tratar as interfaces como
uma caixa preta. Da mesma forma, um desenvolvedor de biblioteca só está preocupado
com o projeto e a implementação da biblioteca. Creio, no entanto, que ambas as partes
devem investir tempo na aprendizagem da outra metade. Um programador de
aplicativos que entende melhor o sistema operacional subjacente pode fazer muito
maior uso dele. Da mesma forma, o desenvolvedor da biblioteca não deve crescer fora
de contato com a realidade e a praticidade dos aplicativos que usam a biblioteca.
Consequentemente, discuto tanto o design quanto o uso dos subsistemas do kernel,
não apenas na esperança de que este livro seja útil para qualquer uma das partes, mas
também na esperança de que o livro inteiro seja útil para ambas as partes.
Presumo que o leitor conheça a linguagem de programação C e esteja familiarizado
com os sistemas Linux. Alguma experiência com design de sistema operacional e
tópicos relacionados de ciência de computadores é benéfica, mas tento explicar
conceitos o máximo possível — se não, a Bibliografia inclui alguns excelentes livros
sobre design de sistema operacional.
Este livro é apropriado para um curso de graduação introduzindo design de
sistema operacional como o texto aplicado se acompanhado por um livro
introdutório sobre teoria.Este livro deve ser bem sucedido em um curso de
graduação avançado ou em um curso de pós-graduação sem material auxiliar.
www.it-ebooks.info
Um grande obrigado aos meus colegas do Google, o grupo mais criativo e
inteligente com o qual já tive o prazer de trabalhar.Muitos nomes encheriam essas
páginas se eu listasse todos, mas destacarei Alan Blount, Jay Crim, Chris Danis, Chris
DiBona, Eric Flatt, Mike Lockwood, San Mehat, Brian Rogan, Brian Swetland, Jon
Trowbridge e Steve Vinter por sua amizade, conhecimento e apoio.
Respeito e amor a Paul Amici, Mikey Babbitt, Keith Barbag, Jacob Berkman,
Nat Friedman, Dustin Hall, Joyce Hawkins, Miguel de Icaza, Jimmy Krehl, Doris
Love, Linda Love, Brette Luck, Randy O’Dowd, Sal Ribaudo e mãe, Chris Rivera,
Carolyn Rodon, Joey Shaw, Sarah Stewart, Jeremy VanDoren e família, Luis
Villa, Steve Weisberg e família, e Helen Whisnant.
Finalmente, muito obrigado aos meus pais por tanto, particularmente
minhas orelhas bem proporcionadas. Feliz Hacking!
Robert Love
Boston
Sobre o autor
Robert Love é um programador de código aberto, palestrante e autor que tem usado
e contribuído para o Linux há mais de 15 anos. Robert é atualmente engenheiro de
software sênior na Google, onde foi membro da equipe que desenvolveu o kernel da
plataforma móvel Android. Antes da Google, ele foi arquiteto-chefe, Linux Desktop,
na Novell. Antes da Novell, ele era engenheiro de kernel na MontaVista Software e na
Ximian. Os projetos do núcleo de Robert incluem o núcleo preemptivo, o
programador de processos, o
camada de eventos do kernel, inotify, aprimoramentos da VM e vários
drivers de dispositivo.
Robert deu inúmeras palestras e escreveu vários artigos sobre o
núcleo Linux. Ele é um editor contribuinte para o Linux Journal. Seus
outros livros incluem Linux System Programming e Linux in a Nutshell.
Robert recebeu um grau de B.A. em matemática e um grau de B.S. em
ciência da computação da Universidade da Flórida. Ele mora em Boston.
www.it-ebooks.info
1
Introdução ao
Linux Kernel
História do Unix
Após quatro décadas de uso, os cientistas da computação continuam a considerar o sistema
operacional Unix como um dos sistemas mais poderosos e elegantes existentes. Desde a
criação do Unix em 1969, a criação de Dennis Ritchie e Ken Thompson tornou-se uma criatura
de pontas de perna, um sistema cujo design resistiu ao teste do tempo com poucos hematomas
em seu nome.
O Unix surgiu do Multics, um projeto de sistema operacional multiusuário falho no
qual os Laboratórios Bell estavam envolvidos.Com o projeto Multics encerrado, os
membros do Centro de Pesquisa em Ciências da Computação dos Laboratórios Bell
ficaram sem um sistema operacional interativo capaz. No verão de 1969, os
programadores do Bell Lab esboçaram um projeto de sistema de arquivos que acabou
evoluindo para Unix.Testando seu projeto, Thompson implementou o novo sistema em
um PDP-7 que estava ocioso. Em 1971, o Unix foi portado para o PDP-11, e em 1973, o
sistema operacional foi reescrito em C—um passo sem precedentes na época, mas que
abriu o caminho para a portabilidade futura.O primeiro Unix amplamente utilizado fora
Bell Labs foi Unix System, Sexta Edição, mais comumente chamado de V6.
Outras empresas portaram Unix para novas máquinas. Essas portas
foram acompanhadas de melhorias que resultaram em várias variantes do
sistema operacional. Em 1977, a Bell Labs lançou uma combinação dessas
variantes em um único sistema, o Unix System III; em 1982, a AT&T lançou
System V.1
A simplicidade do design do Unix, juntamente com o fato de que ele foi distribuído
com o código-fonte, levou a um maior desenvolvimento em organizações externas. O
mais influente desses colaboradores foi a Universidade da Califórnia em Berkeley.
Variantes do Unix de Berkeley são conhecidas como Berkeley Software Distributions,
ou BSD. O primeiro lançamento de Berkeley, o 1BSD em 1977, foi uma coleção de
patches e software adicional no topo do Unix da Bell Labs. O BSD 2 em 1978 continuou
esta tendência, adicionando os utilitários csh e vi, que persistem nos sistemas Unix até
hoje. O primeiro Unix autônomo de Berkeley foi o 3BSD em 1979. Ele adicionou
memória virtual (VM) a uma lista já impressionante de recursos.Uma série de versões
do 4BSD, 4.0BSD, 4.1BSD, 4.2BSD, 4.3BSD, seguido do 3BSD.Essas versões do Unix
adicionaram controle de trabalho, paginação por demanda e TCP/IP. Em 1994, a
universidade lançou o último oficial Berkeley Unix, com um subsistema VM reescrito,
como 4.4BSD.Hoje, graças à licença permissiva do BSD, o desenvolvimento do BSD
continua com os sistemas Darwin, FreeBSD, NetBSD e OpenBSD.
Nas décadas de 1980 e 1990, várias empresas de estações de trabalho e servidores
introduziram suas próprias versões comerciais do Unix.Esses sistemas eram baseados
em uma versão AT&T ou Berkeley e suportavam recursos high-end desenvolvidos para
sua arquitetura de hardware específica.Entre esses sistemas estavam o Tru64 da
Digital, HP-UX da Hewlett Packard, AIX da IBM, DYNIX/ptx da Sequent, IRIX da SGI e
Solaris e SunOS da Sun.
O design elegante original do sistema Unix, juntamente com os anos de inovação e
melhoria evolutiva que se seguiram, resultou em um sistema operacional poderoso,
robusto e estável.Um punhado de características do Unix estão no centro de sua força.
Primeiro, o Unix é simples: enquanto alguns sistemas operacionais implementam
milhares de chamadas de sistema e têm objetivos de design indefinidos, os sistemas
Unix implementam apenas centenas de chamadas de sistema e têm um design simples,
até mesmo básico. Em segundo lugar, no Unix, tudo é um arquivo.2 Isso simplifica a
manipulação de dados e dispositivos em um conjunto de chamadas do sistema central:
open(), read(), write(),h,y, e close().Third, o kernel do Unix e utilitários do sistema
relacionados são escritos em C—uma propriedade que dá ao Unix sua incrível portabilidade
para diversas arquiteturas de hardware e acessibilidade para uma ampla gama de
desenvolvedores. Quarto, o Unix tem tempo de criação de processo rápido e a chamada de
sistema única fork(). Finalmente, o Unix fornece primitivas de comunicação entre processos
(IPC) simples, mas robustas que, quando combinadas com o rápido tempo de criação de
processos, permitem a criação de programas simples que fazem uma coisa e fazem
bem.Esses programas de propósito único podem ser unidos para realizar tarefas de
aumentar a complexidade. Os sistemas Unix, portanto, exibem camadas limpas, com uma
forte separação entre política e mecanismo.
Atualmente, o Unix é um sistema operacional moderno que suporta multitarefas
preventivas, multi-threading, memória virtual, paginação por demanda, bibliotecas
compartilhadas com carga de demanda e
2 Bem, ok, nem tudo - mas muito é representado como um arquivo. Os soquetes são uma
exceção notável. Alguns esforços recentes, como o sucessor do Unix na Bell Labs, Plan9,
implementam quase todos os aspectos do sistema como um arquivo.
www.it-ebooks.info
Along Came Linus: Introdução ao Linux
www.it-ebooks.info
4 Capítulo 1 Introdução ao Linux Kernel
Uma das características mais interessantes do Linux é que ele não é um produto
comercial; em vez disso, é um projeto colaborativo desenvolvido pela Internet.Embora o
Linus continue a ser o criador do Linux e o mantenedor do kernel, o progresso continua
através de um grupo de desenvolvedores desconectados. Qualquer um pode contribuir
para o Linux.O kernel do Linux, como acontece com grande parte do sistema, é um
software livre ou de código aberto.3 Especificamente, o kernel do Linux é licenciado sob
a GNU General Public License (GPL) versão 2.0. Consequentemente, você está livre para
fazer o download do código-fonte e fazer as modificações que desejar.A única
advertência é que, se você distribuir suas alterações, deverá continuar a fornecer aos
destinatários os mesmos direitos de que você desfrutou, incluindo a disponibilidade do
código-fonte.4
O Linux é muito importante para muitas pessoas.Os fundamentos de um sistema Linux são o
kernel, a biblioteca C, a cadeia de ferramentas e os utilitários básicos do sistema, como um
processo de login e shell.Um sistema Linux também pode incluir uma implementação moderna
do X Window System, incluindo um ambiente de desktop completo, como o GNOME.Milhares de
aplicativos livres e comerciais existem para Linux. Neste livro, quando eu digo Linux, eu
normalmente me refiro ao kernel do Linux.Onde ele é ambíguo, eu tento explicitamente apontar
se estou referindo-me ao Linux como um sistema completo ou apenas o kernel próprio.
Estritamente falando, o termo Linux refere-se apenas ao kernel.
www.it-ebooks.info
6 Capítulo 1 Introdução ao Linux Kernel
Aplicativo 1 Aplicativo 2
espaço do usuário
espaço do 'kernel'
Subsistemas do kernel
Drivers de dispositivo
hardware
versões especiais podem realmente rodar sem um.Este é um recurso legal, permitindo
que o Linux seja executado em sistemas incorporados muito pequenos sem MMU, mas
de outra forma mais acadêmico do que prático—até mesmo sistemas incorporados
simples hoje em dia tendem a ter recursos avançados, como unidades de
gerenciamento de memória. Neste livro, focamos em sistemas baseados em MMU.
www.it-ebooks.info
8 Capítulo 1 Introdução ao Linux Kernel
www.it-ebooks.info
Versões do kernel do Linux
2.6.26
assim, as mudanças fluem para 2.6-mm e, quando maduro, para uma das séries de
minidesenvolvimento 2.6.Assim, ao longo dos últimos anos, cada versão 2.6 - por exemplo,
2.6.29 - levou sete meses, ostentando mudanças significativas sobre seu antecessor.Esta
"série de desenvolvimento em miniatura" tem se mostrado bastante bem sucedida,
mantendo altos níveis de estabilidade enquanto ainda introduz novos recursos e parece
improvável que mude no futuro próximo. De fato, o consenso entre os desenvolvedores do
kernel é que este novo processo de lançamento continuará indefinidamente.
Para compensar a frequência reduzida de versões, os desenvolvedores do kernel
introduziram a versão estável acima mencionada. Esta versão (o 8 em 2.6.32.8) contém
correções de bugs cruciais, muitas vezes com retroportagem do kernel em desenvolvimento
(neste exemplo, 2.6.33). Desta forma, a versão anterior continua a receber atenção focada na
estabilização.
Antes de Começarmos
Este livro é sobre o núcleo Linux: seus objetivos, o design que cumpre esses
objetivos, e a implementação que realiza esse design.A abordagem é prática,
tomando um meio-termo entre a teoria e a prática ao explicar como tudo funciona.
Meu objetivo é dar a você uma apreciação e compreensão de dentro para o design e
a implementação do kernel do Linux.Essa abordagem, juntamente com algumas
anedotas pessoais e dicas sobre hacking de kernel, deve garantir que este livro o
mantenha fora do jogo, quer você esteja procurando desenvolver o código núcleo
do kernel, um novo driver de dispositivo ou simplesmente entender melhor o
sistema operacional Linux.
Ao ler este livro, você deve ter acesso a um sistema Linux e ao código-fonte do
kernel. Idealmente, neste ponto, você é um usuário do Linux e cutucou e produziu
na fonte, mas requer alguma ajuda para que tudo se junte. Por outro lado, você
pode nunca ter usado o Linux, mas só quer aprender o design do kernel por
curiosidade. No entanto, se o seu desejo é escrever algum código próprio, não há
substituto para o source.The código fonte está disponível livremente; use-o!
Oh, e acima de tudo, divirta-se!
www.it-ebooks.info
2
Introdução ao Kernel
Neste capítulo, apresentamos algumas das noções básicas do kernel do Linux: onde
obter sua fonte, como compilá-la e como instalar o novo kernel.Em seguida,
examinamos as diferenças entre os programas kernel e user-space e construções de
programação comuns usadas no kernel. Embora o kernel certamente é único de muitas
maneiras, no final do dia, é pouco diferente de qualquer outro grande projeto de
software.
Usando o Git
Ao longo dos últimos dois anos, os hackers do kernel, liderados pelo próprio Linus,
começaram a usar um novo sistema de controle de versão para gerenciar o código-fonte do
kernel Linux. Linus criou esse sistema, chamado Git, pensando na velocidade. Ao contrário
dos sistemas tradicionais como o CVS, o Git é distribuído, e seu uso e fluxo de trabalho são
consequentemente desconhecidos para muitos desenvolvedores. Recomendo
enfaticamente o uso do Git para baixar e gerenciar a origem do kernel do Linux.
Você pode usar o Git para obter uma cópia da última versão "empurrada" da
árvore de Linus:
Quando estiver com check-out, você poderá atualizar sua árvore para a
mais recente de Linus:
Com esses dois comandos, você pode obter e, subsequentemente, manter-se atualizado
com a árvore oficial do kernel.Para confirmar e gerenciar suas próprias alterações, consulte
o Chapter 20,"Patches,
www.it-ebooks.info
12 Capítulo 2 Introdução ao Kernel
Usando Patches
Em toda a comunidade do kernel do Linux, os patches são a língua franca da comunicação.
Você distribuirá as alterações de código em patches e receberá o código de outros como
patches. Patches incrementais fornecem uma maneira fácil de mover de uma árvore do
kernel para a próxima. Em vez de baixar cada tarball grande da fonte do kernel, você pode
simplesmente aplicar um patch incremental-tal para ir de uma versão para a próxima.Isso
economiza toda a largura de banda e seu tempo. Para aplicar um patch incremental, de
dentro da árvore de origem do kernel, basta executar
www.it-ebooks.info
Construindo o kernel
Diretório Descrição
arco Origem específica da arquitetura
bloco Camada de I/O de bloco
criptografia API de criptografia
Documentação Documentação de origem do kernel
drivers Drivers de dispositivo
Firmware de dispositivo necessário para usar determinados
firmware drivers
fs O VFS e os sistemas de arquivos individuais
incluir Cabeçalhos do kernel
inicializar Inicialização e inicialização do kernel
ipc Código de comunicação entre processos
kernel Subsistemas centrais, como o agendador
biblioteca Rotinas auxiliares
mm Subsistema de gerenciamento de memória e a VM
líquido Subsistema "rede"
amostras Exemplo, código demonstrativo
scripts Scripts usados para criar o kernel
segurança Módulo de segurança do Linux
som Subsistema de som
usuário Código do espaço do usuário inicial (chamado initramfs)
ferramentas Ferramentas úteis para o desenvolvimento do Linux
virt Infraestrutura de virtualização
Construindo o kernel
Construir o núcleo é fácil. É surpreendentemente mais fácil do que
compilar e instalar outros componentes no nível do sistema, como glibc.A
série 2.6 do kernel introduziu uma nova configuração e sistema de
compilação, que tornou o trabalho ainda mais fácil e é uma melhoria bem-
vinda em relação às versões anteriores.
www.it-ebooks.info
14 Capítulo 2 Introdução ao Kernel
Configurando o kernel
Como o código-fonte do Linux está disponível, segue-se que você pode configurá-lo e
personalizá-lo antes da compilação. De fato, é possível compilar suporte no seu kernel
apenas para os recursos e drivers específicos que você deseja. A configuração do kernel é
uma etapa necessária antes de construí-lo. Como o kernel oferece uma variedade de
recursos e suporta uma cesta variada de hardware, há muito para configurar. A configuração
do kernel é controlada por opções de configuração, que são prefixadas por CONFIG no
formato CONFIG_FEATURE. Por exemplo, o SMP (sym-metrical multiprocessing) é controlado
pela opção de configuração CONFIG_SMP. Se esta opção estiver definida, o SMP será
habilitado; se não estiver definida, o SMP será desabilitado.As opções de configuração são
usadas tanto para decidir quais arquivos construir quanto para manipular o código através
de diretivas de pré-processador.
As opções de configuração que controlam o processo de compilação são
Booleanos ou triestados.Uma opção Booleana pode ser sim ou não. Os recursos do
kernel, como CONFIG_PREEMPT, geralmente são Booleanos.Uma opção triestado é uma
opção de sim, não ou módulo.A configuração do módulo representa uma opção de
configuração definida, mas deve ser compilada como um módulo (ou seja, um
objeto carregável dinamicamente separado). No caso de triestados, uma opção sim
significa explicitamente compilar o código na imagem principal do kernel e não
como um módulo. Os motoristas são geralmente representados por tristados.
As opções de configuração também podem ser cadeias de caracteres
ou inteiros. Essas opções não controlam o processo de compilação, mas
especificam valores que a origem do kernel pode acessar como uma
macro de pré-processador. Por exemplo, uma opção de configuração
pode especificar o tamanho de uma matriz alocada estaticamente.
Os kernels de fornecedores, como os fornecidos pela Canonical para o Ubuntu
ou Red Hat para o Fedora, são pré-compilados como parte da distribuição. Tais
kernels tipicamente permitem uma boa seção transversal dos recursos necessários
do kernel e compilam quase todos os drivers como módulos. Isso fornece um
grande kernel base com suporte para uma ampla gama de hardware como módulos
separados. Para melhor ou pior, como um hacker do kernel, você precisa compilar
seus próprios kernels e aprender quais módulos incluir por conta própria.
Felizmente, o kernel fornece várias ferramentas para facilitar a
configuração.A ferramenta mais simples é um utilitário de linha de
comando baseado em texto:
Este utilitário passa por cada opção, uma a uma, e pede ao usuário para
selecionar interativamente sim, não ou (para tristates) módulo. Como isso leva
muito tempo, a menos que você receba um pagamento por hora, use um utilitário
gráfico baseado em encurses:
Este comando cria uma configuração baseada nos padrões da sua arquitetura:
Embora esses padrões sejam um pouco arbitrários (em i386, há rumores de que
são a configuração de Linus!), eles fornecem um bom começo se você nunca
configurou o kernel. Para sair e entrar em execução rapidamente, execute este
comando e volte e verifique se as opções de configuração do hardware estão
habilitadas.
As opções de configuração são armazenadas na raiz da árvore de origem do kernel
em um arquivo chamado .config.Você pode achar mais fácil (como a maioria dos
desenvolvedores do kernel fazem) apenas editar este arquivo diretamente. É muito
fácil procurar e alterar o valor das opções de configuração.Após fazer alterações no
seu arquivo de configuração ou ao usar um arquivo de configuração existente em uma
nova árvore do kernel, você pode validar e atualizar a configuração:
Ao contrário dos kernels anteriores à versão 2.6, você não precisa mais executar
make dep antes de construir o kernel — a árvore de dependência é mantida
automaticamente.Você também não precisa especificar um tipo de construção
específico, como bzImage, ou módulos de construção separadamente, como fez em
versões antigas.A regra padrão Makefile tratará de tudo.
Se precisar ver a saída da construção, você pode ler o arquivo. No entanto, como os
avisos e erros são exibidos no erro padrão, normalmente não é necessário. Na verdade, eu
apenas
Isso redireciona toda a saída sem valor para aquele grande e sinistro ralo sem
retorno,
/dev/null.
www.it-ebooks.info
16 Capítulo 2 Introdução ao Kernel
www.it-ebooks.info
Uma Besta de Natureza Diferente
Arquivos de cabeçalho
Quando falo sobre arquivos de cabeçalho neste livro, estou me referindo aos
arquivos de cabeçalho do kernel que fazem parte da árvore de código-fonte do
kernel. Os arquivos de código-fonte do kernel não podem incluir cabeçalhos
externos, assim como não podem usar bibliotecas externas.
Os arquivos base estão localizados no diretório include/ na raiz da árvore de código-
fonte do kernel. Por exemplo, o arquivo de cabeçalho <linux/inotify.h> está localizado
em include/linux/inotify.h na árvore de origem do kernel.
Um conjunto de arquivos de cabeçalho específicos da arquitetura está localizado em
arch/<architecture>/include/asm na árvore de origem do kernel. Por exemplo, se estiver
compilando para a arquitetura x86, os cabeçalhos específicos da arquitetura estarão
em arch/x86/include/asm. O código-fonte inclui esses cabeçalhos apenas através do
asm/ prefixo, por exemplo <asm/ioctl.h>.
Das funções ausentes, a mais familiar é printf().O kernel não tem acesso a printf(),
mas fornece printk(), que funciona praticamente da mesma forma que seu primo
mais familiar.A printk()function copia a string formatada no buffer de log ker-nel, que
normalmente é lido pelo programa syslog. O uso é semelhante a
printf ():
www.it-ebooks.info
18 Capítulo 2 Introdução ao Kernel
C GNU
Como qualquer kernel Unix autorespeitável, o kernel Linux é programado em C.
Talvez de forma abrupta, o kernel não é programado em ANSI C estrito. Em vez
disso, onde aplicável, os desenvolvedores do kernel fazem uso de várias
extensões de linguagem disponíveis em gcc (o GNU Compiler Collection, que
contém o compilador C usado para compilar o kernel e a maioria de tudo o mais
escrito em C em um sistema Linux).
Os desenvolvedores do kernel usam as extensões ISO C991 e GNU C para a
linguagem C. Essas mudanças adaptaram o kernel do Linux ao gcc, embora
recentemente um outro compilador, o compilador Intel C, tenha suportado recursos
gcc suficientes para que ele também possa compilar o kernel do Linux.A versão gcc
mais antiga suportada é a 3.2; a versão 4.4 ou posterior do gcc é recomendada.As
extensões ISO C99 que o kernel usa não são nada especiais e, como C99 é uma
revisão oficial da linguagem C, estão aparecendo lentamente em vários outros
códigos.Os desvios mais desconhecidos do ANSI C padrão são os fornecidos pelo
GNU C. Vamos ver mais detalhes extensões interessantes que você verá no kernel;
essas alterações diferenciam o código do kernel de outros projetos com os quais
você pode estar familiarizado.
Funções Embutidas
Tanto o C99 quanto o GNU C suportam funções em linha. Uma função embutida é, como
seu nome sugere, inserida embutida em cada site de chamada de função. Isso elimina a
sobrecarga de chamada de função e retorno (registro de salvamento e restauração) e
permite uma otimização potencialmente maior, pois o compilador pode otimizar tanto o
chamador quanto a função chamada como um. Como uma desvantagem (nada na vida é
livre), o tamanho do código aumenta porque o conteúdo da função é copiado para todos os
chamadores, o que aumenta o consumo de memória e a pegada de cache da instrução. Os
desenvolvedores do kernel usam funções embutidas para pequenas funções críticas em
tempo.
1 A ISO C99 é a mais recente revisão importante da norma ISO C. O C99 adiciona inúmeros aprimoramentos à revisão
principal anterior, o ISO C90, incluindo inicializadores designados, matrizes de comprimento variável, comentários no estilo C++ e o
www.it-ebooks.info
Uma Besta de Natureza Diferente
Montagem in-line
O compilador C gcc permite a incorporação de instruções de montagem
em funções C normais. Este recurso, é claro, é usado apenas nas partes
do kernel que são exclusivas de uma determinada arquitetura de sistema.
A diretiva do compilador asm() é usada para embutir código assembly.
Por exemplo, esta diretiva de assembly embutida executa a instrução rdtsc
do processador x86, que retorna o valor do registro timestamp (tsc):
Anotação de Ramificação
O compilador C gcc tem uma diretiva incorporada que otimiza ramificações condicionais
como
muito provável ou muito improvável.O compilador usa a diretiva para apropriadamente
otimizar a ramificação.O kernel envolve a diretiva em macros fáceis de usar,
provavelmente() e
improvável().
Por exemplo, considere uma instrução if como a seguinte:
www.it-ebooks.info
Conclusão 21
A pilha do kernel não é grande nem dinâmica; ela é pequena e fixa em tamanho.O
tamanho exato da pilha do kernel varia de acordo com a arquitetura. Em x86, o
tamanho da pilha é configurável em tempo de compilação e pode ser de 4 KB ou 8 KB.
Historicamente, a pilha do kernel é de duas páginas, o que geralmente implica que é de
8 KB em arquiteturas de 32 bits e 16 KB em arquiteturas de 64 bits - este tamanho é
fixo e absoluto. Cada processo recebe sua própria pilha.
A pilha do kernel é discutida com muito mais detalhes nos capítulos
posteriores.
Sincronização e simultaneidade
O miolo é susceptível a condições de corrida. Ao contrário de um aplicativo de
espaço de usuário de thread único, um número de propriedades do kernel
permitem acesso simultâneo de recursos compartilhados e, portanto, exigem
sincronização para evitar corridas. Especificamente
n O Linux é um sistema operacional multitarefa preemptivo. Os processos são
agendados e reagendados por vontade do agendador de processos do kernel. O
kernel deve sincronizar essas tarefas.
n O Linux suporta multiprocessamento simétrico (SMP). Portanto, sem a proteção
adequada, o código do kernel executado simultaneamente em dois ou mais
processadores pode acessar simultaneamente o mesmo recurso.
n As interrupções ocorrem de forma assíncrona em relação ao código atualmente em execução.
Portanto, sem a proteção adequada, uma interrupção pode ocorrer no meio do acesso a um recurso, e
o manipulador de interrupção pode acessar o mesmo recurso.
n O kernel do Linux é preemptivo.Portanto, sem proteção, o código do kernel pode ser
preempcionado em favor de um código diferente que, em seguida, acessa o mesmo recurso.
Soluções típicas para as condições de corrida incluem spinlocks e semáforos.
Os capítulos posteriores fornecem uma discussão completa sobre sincronização
e concorrência.
Importância da portabilidade
Embora os aplicativos de espaço do usuário não precisem ser
direcionados para a portabilidade, o Linux é um sistema operacional
portátil e deve permanecer um.Isso significa que o código C independente
de arquitetura deve ser compilado e executado corretamente em uma
ampla variedade de sistemas, e que o código dependente de arquitetura
deve ser adequadamente segregado em diretórios específicos do sistema
na árvore de origem do kernel.
Um punhado de regras — como permanecer neutro com endian, ser
limpo com 64 bits, não assumir o tamanho da palavra ou da página e
assim por diante — vai longe. A portabilidade é discutida em
profundidade em um capítulo posterior.
Conclusão
Para ter certeza, o núcleo tem qualidades únicas. Ele impõe suas próprias regras e os
riscos, gerenciando todo o sistema como o kernel faz, são certamente maiores.Dito isso, a
complexidade e a barreira de entrada do kernel Linux não é qualitativamente diferente de
qualquer outro grande soft-
www.it-ebooks.info
22 Capítulo 2 Introdução ao Kernel
www.it-ebooks.info
3
.
Gerenciamento de
processos
O processo
Um processo é um programa (código objeto armazenado em alguma mídia) no meio da
execução. Os processos são, no entanto, mais do que apenas o código do programa em
execução (muitas vezes chamado de seção de texto no Unix).Eles também incluem um
conjunto de recursos como arquivos abertos e sinais pendentes, dados internos do
kernel, estado do processador, um espaço de endereço de memória com um ou mais
mapeamentos de memória, um ou mais threads de execução e uma seção de dados
contendo variáveis globais. Os processos, na verdade, são o resultado vivo da
execução do código do programa.O kernel precisa gerenciar todos esses detalhes de
forma eficiente e transparente.
Os segmentos de execução, muitas vezes encurtados para segmentos, são os objetos
de atividade dentro do processo. Cada thread inclui um contador de programa exclusivo,
uma pilha de processos e um conjunto de registros de processo-ordenador. O kernel
agenda segmentos individuais, não processos. Em sistemas Unix tradicionais, cada
processo consiste em um segmento. Em sistemas modernos, no entanto, os programas
multithreaded — aqueles que consistem em mais de um thread — são comuns.Como você
verá mais adiante, o Linux tem uma implementação exclusiva de threads: ele não
diferencia entre threads e processos.Para o Linux, um thread é apenas um tipo especial de
processo.
Em sistemas operacionais modernos, os processos fornecem duas virtualizações: um
processador virtualizado e uma memória virtual.O processador virtual dá ao processo a
ilusão de que ele sozinho monopoliza o sistema, apesar de possivelmente compartilhar o
processador entre centenas de outros processos. O Capítulo 4,"Programação de
processos", discute essa virtualização.A memória virtual permite que o processo aloque e
gerencie a memória como se ela sozinha possuísse toda a memória no sistema.A memória
virtual é abordada no Capítulo 12,"Gerenciamento de memória".
www.it-ebooks.info
24 Capítulo 3 Gerenciamento de processos
Nota
Outro nome para um processo é uma tarefa. O kernel do Linux refere-se
internamente aos processos como tarefas. Neste livro, utilizo os termos
indistintamente, embora quando digo tarefa me refira geralmente a um processo do
ponto de vista do núcleo.
struct_estrutura
struct_estrutura
struct_estrutura
struct_estrutura
A estrutura_tarefa
a lista de tarefas
informação_do <asm/thread_info.h>
_thread
www.it-ebooks.info
26 Capítulo 3 Gerenciamento de processos
- ponteiro de pilha
struct_estrutura_thread
- menor endereço de memória
Estado do Processo
O campo de estado do descritor de processo descreve a condição atual do
processo (consulte a Figura 3.3). Cada processo no sistema está em
exatamente um dos cinco estados diferentes. Esse valor é representado
por um dos cinco sinalizadores:
n TASK_RUNNING — O processo é executável; ele está em execução no momento
ou em uma fila de execução aguardando para ser executado (as filas de execução são
discutidas no Capítulo 4). Esse é o único estado possível para um processo em
execução no espaço do usuário; ele também pode ser aplicado a um processo no
espaço do kernel que esteja em execução ativa.
n TASK_INTERRUPTIBLE — O processo está em espera (ou seja, está bloqueado),
aguardando que alguma condição exista.Quando essa condição existe, o kernel
define o estado do processo como TASK_RUNNING.O processo também acorda
prematuramente e se torna executável se receber um sinal.
www.it-ebooks.info
28º Capítulo 3 Gerenciamento de processos
Bifurcações de tarefas.
TAREFA_EM_EXECUÇÃO TAREFA_EM_EX
ECUÇÃO
(pronto, mas
não está em execução) (em execução)
A tarefa é colocada em
espera
para um evento
específico.
TASK_ INTERRUPTIBLE
ou
TASK_ UNINTERRUPTIBLE
O evento ocorre e a tarefa é ativada (aguardando)
e colocada de volta na fila de execução.
5 É por isso que temos esses temidos processos inabáveis com estado em . Porque a tarefa
não responder a sinais, você não pode enviar um sinal a ele. Além disso, mesmo se você
pudesse terminar a tarefa, não seria sábio, porque a tarefa está supostamente no meio de
uma operação importante e pode segurar um semáforo.
www.it-ebooks.info
Descritor do Processo e a Estrutura da Tarefa
Contexto do Processo
Uma das partes mais importantes de um processo é o código do programa em
execução.Esse código é lido em um arquivo executável e executado no espaço de endereço
do programa. A execução normal do programa ocorre no espaço do usuário.Quando um
programa executa uma chamada do sistema (consulte o Chapter 5,"System Calls") ou
dispara uma exceção, ele entra no espaço do kernel. Neste ponto, diz-se que o kernel está
"executando em nome do processo" e está no contexto do processo.Quando no contexto do
processo, a macro atual é válida.6 Ao sair do kernel, o processo retoma a execução no espaço
do usuário, a menos que um processo de prioridade mais alta tenha se tornado executável
nesse ínterim, nesse caso o agendador é chamado para selecionar o processo de prioridade
mais alta.
Chamadas do sistema e manipuladores de exceção são interfaces bem
definidas no kernel. Um processo pode começar a ser executado no espaço
do kernel somente por meio de uma dessas interfaces — todo o acesso ao
kernel é feito por meio dessas interfaces.
denominada
6 Além do contexto de processo, há o contexto de interrupção, que discutimos no Capítulo 7,
"Interrupts and Interrupt Handlers" (Interrupções e manipuladores de interrupção). No contexto de
interrupção, o sistema não está sendo executado em nome de um processo, mas está executando um
manipulador de interrupção. Nenhum processo está vinculado a manipuladores de interrupção.
www.it-ebooks.info
30 Capítulo 3 Gerenciamento de processos
crianças.
Consequentemente, dado o processo atual, é possível obter o
descritor de processo de seu pai com o seguinte código:
Cuidado
É caro repetir cada tarefa em um sistema com muitos processos; o código deve ter
uma boa razão (e nenhuma alternativa) antes de fazer isso.
www.it-ebooks.info
Criação de Processo
Criação de Processo
A criação de processos no Unix é exclusiva. A maioria dos sistemas
operacionais implementa um mecanismo spawn para criar um novo processo
em um novo espaço de endereço, ler em um executável, e começar a executá-lo.
O Unix adota a abordagem incomum de separar esses passos em duas funções
distintas: fork() e exec(). Ele difere do pai apenas em seu PID (que é exclusivo), seu
PPID (PID do pai, que é definido para o processo original) e determinados
recursos e estatísticas, como sinais pendentes, que não são herdados.A
segunda função, exec(), carrega um novo executável no espaço de endereço e
começa a executá-lo.A combinação de fork() seguida por fork()exec()é semelhante
à função única que a maioria dos sistemas operacionais fornece.
Cópia na gravação
Tradicionalmente, em fork(), todos os recursos de propriedade do pai são duplicados e
a cópia é dada ao filho. Essa abordagem é ingênua e ineficiente, pois copia muitos
dados que poderiam ser compartilhados.Pior ainda, se o novo processo executasse
imediatamente uma nova imagem, toda essa cópia seria desperdiçada. No Linux, fork()
é implementado através do uso de páginas copy-on-write. Copy-on-write (ou COW) é
uma técnica para atrasar ou impedir completamente a cópia dos dados. Em vez de
duplicar o espaço de endereço do processo, o pai e o filho podem compartilhar uma
única cópia.
Os dados, no entanto, são marcados de forma que, se forem gravados, uma
duplicata será feita e cada processo receberá uma cópia exclusiva.
Consequentemente, a duplicação de recursos ocorre somente quando eles são
gravados; até então, eles são compartilhados como somente leitura.Essa técnica
atrasa a cópia de cada página no espaço de endereço até que ela seja realmente
gravada. No caso de as páginas nunca serem escritas — por exemplo, se exec() for
chamada imediatamente após fork() — elas nunca precisarão ser copiadas.
A única sobrecarga incorrida por fork() é a duplicação das tabelas de página
do pai e a criação de um descritor de processo exclusivo para o filho. No caso
comum de um processo executar uma nova imagem executável imediatamente
após a bifurcação, esta otimização evita a cópia desperdiçada de grandes
quantidades de dados (com o espaço de endereço, facilmente dezenas de
megabytes).Esta é uma otimização importante porque a filosofia Unix incentiva
a execução rápida do processo.
7 Por exemplo, qualquer membro da família de funções. O kernel implementa a chamada do sistema
sobre a qual , , , e são implementados.
www.it-ebooks.info
32 Capítulo 3 Gerenciamento de processos
Bifurcação
O Linux implementa fork() através da chamada do sistema clone(). Essa chamada recebe
uma série de sinalizadores que especificam quais recursos, se houver, o
processo pai e filho devem compartilhar. (Consulte a seção "Implementação
Linux de Threads", mais adiante neste capítulo, para obter mais informações
sobre os flags.) A biblioteca fork(), vfork() e __clone() chama todas para chamar a
chamada do sistema clone() com os flags.TheRequired().
A maior parte do trabalho em bifurcação é manipulada por do_fork(), que
é definido em kernel/fork.c.Esta função chamaogicopy_process() e então inicia o
processo em execução. O trabalho interessante é feito por copy_process():
1. Ele chama dup_task_struct(), que cria uma nova pilha de kernel, thread_info struc-ture,
eogic_task_struct para o novo processo.Os novos valores são idênticos aos da tarefa atual.Neste
ponto, os descritores de processo filho e pai são idênticos.
6. Ele chama alloc_pid() para atribuir um PID disponível para a nova tarefa.
7. Dependendo dos sinalizadores passados para clone(), copy_process() duplica ou
compartilha arquivos abertos, informações do sistema de arquivos, manipuladores de sinais,
espaço de endereço de processo e namespace.Esses recursos são normalmente
compartilhados entre threads em um determinado processo; caso contrário, eles são
exclusivos e, portanto, copiados aqui.
8 Atualmente, isso não funciona corretamente, embora o objetivo seja que a criança corra
primeiro.
www.it-ebooks.info
A implementação Linux de threads
vfork ()
A chamada do sistema vfork() tem o mesmo efeito que fork(), exceto que as entradas da
tabela de páginas do processo pai não são copiadas. Em vez disso, o filho é
executado como o único thread no espaço de endereço do pai, e o pai é bloqueado
até que o filho chame exec() ou saia. A criança não tem permissão para escrever no
espaço de endereço. Esta foi uma otimização bem-vinda nos velhos tempos do
3BSD quando a chamada foi introduzida porque no momento as páginas copy-on-
write não eram usadas para implementar fork().Hoje, com as semânticas copy-on-
write e child-runs-first, o único benefício para vfork() não é copiar as entradas das
tabelas de páginas pai. Se o Linux um dia ganhar entradas de tabela de página de
cópia em gravação, não haverá mais nenhum ben-efit.9 Como a semântica de vfork() é
complicada (o que, por exemplo, acontece se o bexec() falhar?), idealmente os
sistemas não precisariam de vfork() e o kernel não o implificaria. É totalmente possível
implementar vfork() como um fork() normal — isto é o que o Linux fez até a versão 2.2.
A chamada do sistema vfork() é implementada através de um sinalizador especial
para a chamada do sistema clone():
1. Em copy_process(), o membro task_struct é definido como
2. Em do_fork(), se o sinalizador especial foi dado, vfork_done é apontado para um
endereço.
3. Depois que o filho é executado pela primeira vez, o pai — em vez de retornar
— espera que o filho o sinalize através do ponteiro vfork_done.
4. Na função mm_release(), que é usada quando uma tarefa sai de um espaço de
endereçamento de memória, vfork_done é verificado para ver se é NULL. Se não
estiver, o pai é sinalizado.
5. De volta em do_fork(), o pai acorda e retorna.
9Patches estão disponíveis para adicionar essa funcionalidade ao Linux. Com o tempo, este recurso
muito provavelmente encontrará seu caminho para o kernel Linux de linha principal.
www.it-ebooks.info
34 Capítulo 3 Gerenciamento de processos
Criando Threads
Os threads são criados da mesma forma que as tarefas normais, com a exceção de
que a chamada do sistema clone() recebe sinalizadores correspondentes aos
recursos específicos a serem compartilhados:
www.it-ebooks.info
A implementação Linux de threads
Bandeira Significado
CLONAR_ARQUIVOS Compartilhamento pai e filho de arquivos abertos.
Informações do sistema de arquivos compartilhados pelos pais e
CLONAR_FS filhos.
CLONAR_TAREFA Defina PID como zero (usado somente pelas tarefas ociosas).
CLONAR_NOVOS Criar um novo namespace para o filho.
CLONAR_PAI O filho deve ter o mesmo pai como seu pai.
CLONAR_RASTREAR Continuar rastreando filho.
CLONE_ SETTID Gravar a TID de volta no espaço do usuário.
CLONAR_CONFIGURAÇÕES Criar um novo TLS para o filho.
Manipuladores de sinal de compartilhamento pai e filho e sinais
CLONAR_MÃO bloqueados.
CLONAR_SYSVSEM Compartilhamento pai e filho de semânticas System V SEM_UNDO.
CLONAR_THREAD Pai e filho estão no mesmo grupo de threads.
CLONAR_VFORK vfork() foi usado e o pai irá dormir até que o filho
acorda.
Não permita que o processo de rastreamento force
CLONAR_NÃO RASTREADO CLONE_PTRACE no
criança.
PARAR_CLONAGEM Inicie o processo no estado TASK_STOPPED.
Crie um novo TLS (armazenamento de segmento local) para o
CLONAR_CONFIGURAÇÕES filho.
CLONE_CHILD_CLEARTID Limpe a TID na criança.
CLONE_CHILD_SETTID Defina a TID na criança.
CLONE_PARENT_SETTID Defina a TID no pai.
CLONAR_VM Espaço de endereço de compartilhamento pai e filho.
Threads do Kernel
Geralmente é útil para o kernel realizar algumas operações em segundo plano.O
ker-nel realiza isso através de threads do kernel — processos padrão que existem
somente no espaço do kernel.A diferença significativa entre threads do kernel e
processos normais é que os threads do kernel não têm um espaço de endereço. (O
ponteiro mm, que aponta para o espaço de endereço, é NULL.) Eles operam apenas
no espaço do kernel e não alteram o contexto para o espaço do usuário.
Entretanto, os segmentos do kernel são programáveis e podem ser evitados, da
mesma forma que os processos normais.
O Linux delega várias tarefas para os threads do kernel, mais notavelmente as tarefas
de descarga e a tarefa ksoftirqd. Você pode ver os threads do kernel no seu sistema Linux
executando o com-mand ps -ef. Os threads do kernel são criados na inicialização do
sistema por outros threads do kernel. Na verdade, um thread do kernel pode ser criado
apenas por outro thread do kernel. O kernel o manipula automaticamente, bifurcando
todos os novos threads do kernel do
www.it-ebooks.info
36 Capítulo 3 Gerenciamento de processos
Término do Processo
É triste, mas, eventualmente, os processos devem morrer.Quando um processo é
encerrado, o kernel libera os recursos de propriedade do processo e notifica o pai da
criança de seu desaparecimento.
Geralmente, a destruição do processo é autoinduzida. Ocorre quando o processo
chama a chamada exit() do sistema, seja explicitamente quando está pronto para
terminar ou implicitamente no retorno da sub-rotina principal de qualquer programa.
(Ou seja, o compilador C faz uma chamada para exit() após main() retornar.) Um processo
também pode terminar involuntariamente. Isso ocorre quando a
www.it-ebooks.info
Término do Processo
4. Ele chama exit_mm() para liberar a mm_struct mantida por este processo. Se
nenhum outro processo estiver usando esse espaço de endereço — que ele, se o
espaço de endereço não for compartilhado — o kernel o destruirá.
5. Chama exit_sem(). Se o processo for enfileirado aguardando um sinal IPC, ele
será desenfileirado aqui.
6. Ele então chama exit_files() e exit_fs() para diminuir a contagem de uso de objetos relacionados
a descritores de arquivos e dados do sistema de arquivos, respectivamente. Se as contagens de
uso atingirem zero, o objeto não estará mais em uso por nenhum processo e será destruído.
7. Ele define o código de saída da tarefa, armazenado no membro exit_code do task_struct,
para o código fornecido por exit() ou qualquer mecanismo do kernel que tenha forçado o
terminal-tion.O código de saída é armazenado aqui para recuperação opcional pelo pai.
8. Ele chama exit_notify() para enviar sinais ao pai da tarefa, reorganiza qualquer um dos filhos
da tarefa em outro thread em seu grupo de threads ou no processo init e define o
estado exit da tarefa, armazenado em exit_state na estrutura task_struct, para
EXIT_ ZOMBIE.
9. do_exit() chama schedule() para alternar para um novo processo (veja o Capítulo 4).
Como o processo não é agendável agora, este é o último código que a tarefa jamais
executará. do_exit() nunca retorna.
1. Ele chama __exit_signal(), que chama __unhash_process(), que por sua vez
chama __detach_pid() para remover o processo do pidhash e remover o processo
da lista de tarefas.
2. __exit_signal() libera todos os recursos restantes usados pelo processo
agora inativo e finaliza estatísticas e bookkeeping.
3. Se a tarefa era o último membro de um grupo de threads, e o líder é
um zumbi, então release_task() notifica o pai do líder zumbi.
4. release_task() chama put_task_struct() para liberar as páginas que contêm a
pilha do kernel do processo e thread_info desaloque o cache slab contendo a pilha task_struct.
www.it-ebooks.info
Término do Processo
A lógica por trás de ter uma lista de filhos e uma lista rastreada é interessante; é
um novo recurso no kernel 2.6.Quando uma tarefa é rastreada, ela é
temporariamente reparada para o processo de depuração.Quando o pai da tarefa
sai, no entanto, ela deve ser reparada junto com seus outros irmãos. Nos kernels
anteriores, isso resultou em um loop sobre cada processo no sistema que procura
filhos.A solução é simplesmente manter uma lista separada dos filhos de um
processo sendo rastreada — reduzindo a pesquisa pelos filhos de cada processo
para apenas duas listas realmente pequenas.
Com o processo reparado com sucesso, não há risco de processos zumbis perdidos.O
processo init rotineiramente chama wait() em seus filhos, limpando todos os zumbis atribuídos a
ele.
Conclusão
Neste capítulo, analisamos a abstração central do sistema operacional do
processo. Discutimos as generalidades do processo, por que ele é importante e a
relação entre processos e threads. Em seguida, discutimos como o Linux
armazena e representa processos (com task_struct e thread_info), como os
processos são criados (via fork() e, finalmente, clone()), como as novas imagens
executáveis são carregadas nos espaços de endereço (via exec()Família de chamadas
do sistema), a hierarquia de processos, como os pais obtêm informações sobre seus
filhos falecidos (via afamília wait()) chamadas do sistema), e como os processos
acabam morrendo (forçada ou intencionalmente viaexit().O processo é uma abstração
fundamental e crucial, no centro de cada sistema operacional moderno e, por fim, a
razão pela qual temos sistemas operacionais (para executar programas).
O próximo capítulo discute a programação do processo, que é a
maneira delicada e interessante na qual o kernel decide quais
processos executar, em que horário e em que ordem.
www.it-ebooks.info
4
.
Agendamento do
Processo
Multitarefa
Um sistema operacional multitarefa é aquele que pode simultaneamente intercalar a
execução de mais de um processo. Em máquinas com um único processador, isso dá a
ilusão de vários processos sendo executados simultaneamente. Em máquinas com vários
processadores, essa funcionalidade permite que os processos sejam realmente
executados simultaneamente, em paralelo, em diferentes processadores. Em qualquer tipo
de máquina, ele também permite que muitos processos bloqueiem ou suspendam, não
sendo executados até que o trabalho esteja disponível.Esses processos, embora na
memória, não são executáveis. Em vez disso, esses processos utilizam o kernel para
esperar até que algum evento (entrada de teclado, dados de rede, passagem de tempo,
etc.) ocorra. Consequentemente, um sistema Linux moderno pode ter muitos processos
na memória, mas, digamos, apenas um em um estado executável.
Os sistemas operacionais de multitarefa têm duas opções: multitarefa cooperativa e
multitarefa preventiva. O Linux, como todas as variantes Unix e a maioria dos sistemas
operacionais modernos, implementa multitarefa preventiva. Em multitarefa preventiva, o
programador decide quando um processo deve parar de ser executado e um novo
processo deve começar a ser executado.
www.it-ebooks.info
42 Capítulo 4 Programação do processo
43º
Política
A política é o comportamento do agendador que determina o que é executado
quando.A política de um agendador geralmente determina a sensação geral de um
sistema e é responsável pela utilização ideal do tempo do processador.Portanto,
ela é muito importante.
www.it-ebooks.info
44 Capítulo 4 Programação do processo
Prioridade do Processo
Um tipo comum de algoritmo de agendamento é o agendamento baseado em
prioridade.O objetivo é classificar os processos com base em seu valor e na
necessidade de tempo do processador.A ideia geral, que não é exatamente
implementada no Linux, é que os processos com prioridade mais alta sejam
executados antes daqueles com prioridade mais baixa, enquanto os processos com a
mesma prioridade são agendados de forma alternada (um após o outro, repetindo). Em
alguns sistemas, os processos com uma prioridade mais alta também recebem um
timeslice mais longo.O processo executável com timeslice restante e a prioridade mais
alta sempre é executado. Tanto o usuário quanto o sistema podem definir a prioridade
de um processo para influenciar o comportamento de agendamento do sistema.
O kernel do Linux implementa dois intervalos de prioridade separados.O primeiro é o
valor agradável, um número de -20 a +19 com um padrão de 0. Valores nice maiores
correspondem a uma prioridade mais baixa — você está sendo "nice" para os outros
processos no sistema. Processos com um valor de prioridade mais baixo (prioridade
mais alta) recebem uma proporção maior do processador do sistema em comparação a
processos com um valor de prioridade mais alto (prioridade mais baixa). Os valores de
Nice são o intervalo de prioridade padrão usado em todos os sistemas Unix, embora
diferentes sistemas Unix os apliquem de maneiras diferentes, refletindo seus
algoritmos de agendamento individuais. Em outros sistemas baseados em Unix, como o
Mac OS X, o valor nice é um controle sobre o timeslice absoluto atribuído a um
processo; no Linux, é um controle sobre a proporção de timeslice.Você pode ver uma
lista dos processos em seu sistema e seus respectivos valores nice (sob a coluna
marcada NI) com o comando ps -el.
O segundo intervalo é a prioridade em tempo real. Os valores são configuráveis,
mas por padrão variam de 0 a 99, inclusive. Ao contrário dos valores corretos, os
valores de prioridade em tempo real mais altos correspondem a uma prioridade
maior.Todos os processos em tempo real têm uma prioridade mais alta do que os
processos não-mal; ou seja, a prioridade em tempo real e o valor preciso estão em
espaços de valores separados. O Linux implementa prioridades em tempo real de
acordo com os padrões relevantes do Unix, especificamente POSIX.1b.Todos os
sistemas modernos do Unix implementam um esquema semelhante.Você pode ver uma
lista dos processos em seu sistema e suas respectivas prioridades em tempo real (sob
a coluna marcada RTPRIO) com o comando
ps -eo estado,uid,pid,ppid,rtprio,tempo,comm.
Um valor "-" significa que o processo não é em tempo real.
www.it-ebooks.info
Política
45º
Fatia de tempo
O timeslice2 é o valor numérico que representa quanto tempo uma tarefa pode ser
executada até ser previamente executada.A política do agendador deve ditar um
timeslice padrão, que não é um serviço de tempo trivial.Um serviço de tempo muito
longo faz com que o sistema tenha um desempenho interativo ruim; o sistema não
sentirá mais como se os aplicativos fossem executados simultaneamente.Um serviço
de tempo muito curto faz com que quantidades significativas de tempo do processador
sejam desperdiçadas na sobrecarga de processos de comutação porque uma
porcentagem significativa do tempo do sistema é gasto alternando de um processo
com um serviço de tempo curto para o próximo. Além disso, os objetivos conflitantes
dos processos vinculados à E/S versus vinculados ao processador novamente surgem:
os processos vinculados à E/S não precisam de intervalos de tempo mais longos
(embora gostem de ser executados com frequência), enquanto os processos
vinculados ao processador precisam de intervalos de tempo longos (para manter seus
caches quentes).
Com este argumento, parece que qualquer intervalo de tempo longo resultaria em um
desempenho interativo ruim. Em muitos sistemas operacionais, essa observação é levada a
sério, e a divisão de tempo padrão é bastante baixa — por exemplo, 10 milissegundos.
Entretanto, o programador CFS do Linux não atribui diretamente fatias de tempo a
processos. Em vez disso, em uma nova abordagem, o CFS atribui aos processos uma
proporção do processador. No Linux, portanto, a quantidade de tempo de processo-
classificação que um processo recebe é uma função da carga do sistema.Essa porção
atribuída é afetada ainda mais pelo valor de cada processo.O valor de nice atua como um
peso, alterando a proporção do tempo de processador que cada processo recebe.
Processos com valores nice mais altos (uma prioridade mais baixa) recebem um peso
deflacionário, produzindo uma proporção menor do processador; processos com valores
nice menores (uma prioridade mais alta) recebem um peso inflacionário, compensando-os
com uma proporção maior do processador.
Como mencionado, o sistema operacional Linux é preventivo.Quando um processo
entra no estado executável, ele se torna elegível para execução. Na maioria dos
sistemas operacionais, se o processo é executado imediatamente, antecipando-se ao
processo em execução no momento, é uma função da prioridade do processo e da
divisão de tempo disponível. No Linux, sob o novo programador CFS, a decisão é uma
função de quanto de uma proporção do processador o novo processador utilizou. Se
consumiu uma proporção menor do processador do que o processo em execução no
momento, ele é executado imediatamente, antecipando o processo atual. Caso
contrário, sua execução será programada para mais tarde.
www.it-ebooks.info
46 Capítulo 4 Programação do processo
Classes do Agendador
O agendador do Linux é modular, permitindo que diferentes algoritmos programem
diferentes tipos de processos. Essa modularidade é chamada de classes do agendador. As
classes do programador permitem a coexistência de diferentes algoritmos conectáveis,
programando seus próprios tipos de processos. Cada programador
www.it-ebooks.info
O algoritmo de agendamento do Linux
www.it-ebooks.info
48 Capítulo 4 Programação do processo
Regularização
CFS é baseado em um conceito simples: programação de processo de modelo como se o
sistema tivesse um processador ideal, perfeitamente multitarefa. Em tal sistema, cada
processo receberia 1/n do tempo do processador, onde n é o número de processos
executáveis, e os agendaríamos para durações infinitamente pequenas, de modo que, em
qualquer período mensurável, teríamos executado todos os n processos pela mesma
quantidade de tempo.Como exemplo, suponha que temos dois processos. No modelo
padrão Unix, podemos executar um processo por 5 milissegundos e, em seguida, outro
processo por 5 milissegundos.Durante a execução, cada processo receberia 100% do
processador. Em um processador ideal, perfeitamente multitarefa, executaríamos ambos os
processos simultaneamente por 10 milissegundos, cada um com 50% de energia.Este último
modelo é chamado de multitarefa perfeita.
www.it-ebooks.info
O algoritmo de agendamento do Linux
www.it-ebooks.info
50 Capítulo 4 Programação do processo
Contabilização de Tempo
Todos os programadores de processo devem considerar o tempo de execução de um
processo. A maioria dos sistemas Unix fazem isso, como discutido anteriormente,
atribuindo cada processo a um timeslice. Em cada tique do relógio do sistema, o timeslice é
diminuído pelo período de tique.Quando o timeslice atinge zero, o processo é antecipado em
favor de outro processo executável com um timeslice diferente de zero.
update_ curr()
calcula o tempo de execução do processo atual e armazena esse valor em delta_exec. Ele então passa o tempo de execução para __update_curr(),
que pesa o tempo pelo número de processos executáveis.O tempo de execução do processo atual é então aumentado pelo valor ponderado:
Seleção de Processo
Na última seção, discutimos como o vruntime em um processo-sor ideal, perfeitamente
multitarefa seria idêntico entre todos os processos executáveis. Na realidade, não é possível
realizar várias tarefas perfeitamente, de modo que o CFS tenta equilibrar o tempo de
execução virtual de um processo com uma regra simples:Quando o CFS
www.it-ebooks.info
A implementação da programação do Linux
Vamos examinar esta função. O corpo do loop while() atravessa a árvore em busca de
uma chave correspondente, que é o vruntime do processo inserido. Segundo as regras
do mercado
árvore, ele se move para a esquerda se a chave for menor que a chave do nó atual e para
a
filho direito se a chave for maior. Se alguma vez se mover para a direita, mesmo
uma vez, ele sabe o
o processo inserido não pode ser o novo nó mais à esquerda e ele define o mais à esquerda
como zero. Se ele se mover
somente à esquerda, na extremidade esquerda permanece como um, e temos um novo nó
na extremidade esquerda e podemos atualizar
o cache definindo rb_leftmost para o processo inserido. O loop termina quando
comparar-se com um nó que não tem filho na direção que nós movemos; link é então
NULL e o loop termina. Quando fora do loop, a função chama rb_link_node()
no nó pai, tornando o processo inserido o novo filho. A função
rb_insert_color() atualiza as propriedades de balanceamento automático da árvore; nós
discutimos o col-
No capítulo 6.
www.it-ebooks.info
56 Capítulo 4 Programação do processo
tion invoca rb_next() para encontrar qual seria o próximo nó em uma travessia em
ordem.
Este será o nó mais à esquerda quando o nó mais à esquerda atual for removido.
Dormindo e Acordando
As tarefas que estão em espera (bloqueadas) estão em um estado especial não
executável.Isso é importante porque, sem esse estado especial, o agendador selecionaria
tarefas que não queriam ser executadas ou, pior, que deveriam ser implementadas como
looping ocupado.Uma tarefa fica em espera por vários motivos, mas sempre enquanto ela
está aguardando algum evento.O evento pode ser uma quantidade de tempo especificada,
mais dados de uma E/S de arquivo ou outro evento de hardware.Uma tarefa também pode
entrar em espera involuntariamente quando tenta obter um semáforo contestado no kernel
(isso é abordado no Capítulo 9, Uma Introdução à Sincronização do Kernel").Um motivo
comum para suspender é a E/S de arquivo—por exemplo, a tarefa emitiu uma solicitação
read() em um arquivo, que precisa ser lido do disco.Como outro exemplo, a tarefa poderia
estar aguardando a entrada do teclado.Seja qual for o caso, o comportamento do kernel é o
mesmo:A tarefa marca a si mesma como suspensa, coloca-se em uma fila de espera,
remove-se da árvore vermelho-preto de executável, e chama schedule() para selecionar um novo
processo a ser executado.Ativar novamente é o inverso:A tarefa é redefinida como executável,
removida fila de espera e adicionado de volta à árvore vermelho-preto.
Como discutido no capítulo anterior, dois estados estão associados ao sono,
TASK_INTERRUPTIBLE e TASK_UNINTERRUPTIBLE.Elas diferem apenas nas tarefas do
TASK_UNINTERRUPTIBLE estado ignorar sinais, enquanto as tarefas do TASK_INTERRUPTIBLE
despertar prematuramente e responder a um sinal se um for emitido. Os dois tipos de
tarefas em repouso ficam em uma fila de espera, aguardando a ocorrência de um
evento, e não são executáveis.
Filas de Espera
A suspensão é tratada por meio de filas de espera. Uma fila de espera é uma lista simples de
processos aguardando
um evento a ocorrer.As filas de espera são representadas no kernel por
wake_queue_head_t.Wait
as filas são criadas estaticamente via DECLARE_WAITQUEUE() ou
dinamicamente via
init_waitqueue_head(). Os processos se colocam em uma fila de espera e se marcam
não executável.Quando o evento associado à fila de espera ocorre, os processos na
são ativadas. É importante implementar dormir e acordar corretamente, para evitar
condições de corrida.
www.it-ebooks.info
A implementação da programação do Linux
www.it-ebooks.info
A implementação da programação do Linux
Acordando
A vigília é feita via wake_up(), que ativa todas as tarefas que estão esperando
na fila de espera fornecida. Ele chama try_to_wake_up(), que define o estado
da tarefa como TASK_RUNNING, chama enqueue_task() para adicionar a tarefa à
árvore vermelho-preto e defineManage_resched se a prioridade da tarefa
despertada for maior que a prioridade da tarefa atual.O código que causa o
evento normalmente chama wake_up() ele mesmo. Por exemplo, quando os
dados chegam do disco rígido, o VFS chama wake_up() na fila de espera que
mantém os processos esperando pelos dados.
Uma nota importante sobre dormir é que há despertares espúrios. O
simples fato de uma tarefa ser despertada não significa que o evento pelo
qual a tarefa está aguardando ocorreu; o repouso deve ser sempre tratado
em um loop que assegure que a condição para a qual a tarefa está
aguardando realmente ocorreu. A Figura 4.1 descreve a relação entre cada
estado do agendador.
TAREFA_EM_EXECUÇÃO
Preempção do Usuário
A preempção do usuário ocorre quando o kernel está prestes a retornar ao espaço do
usuário, need_resched é definido e, portanto, o agendador é chamado. Se o kernel estiver
retornando ao espaço do usuário, ele
www.it-ebooks.info
Preempção e Alternância de Contexto
Preempção de Kernel
O kernel Linux, ao contrário da maioria das outras variantes Unix e muitos outros
sistemas operacionais, é um kernel totalmente preemptivo. Em kernels sem
preempção, o código do kernel é executado até a conclusão. Isto é, o agendador
não pode reagendar uma tarefa enquanto ela está no kernel — o código do kernel é
agendado de forma cooperativa, não preventiva. O código do kernel é executado até
terminar (retornar ao espaço do usuário) ou bloquear explicitamente. No kernel 2.6,
no entanto, o kernel do Linux tornou-se preemptivo: Agora é possível antecipar uma
tarefa em qualquer ponto, desde que o kernel esteja em um estado no qual é seguro
reprogramar.
Então, quando é seguro reagendar? O kernel pode antecipar uma tarefa em
execução no kernel, desde que ela não contenha um lock.Ou seja, os bloqueios
são usados como marcadores de regiões de não-preemptibilidade. Como o
kernel é seguro para SMP, se um bloqueio não for mantido, o código atual é
verde-trant e capaz de ser preemptado.
A primeira alteração no suporte à preempção do kernel foi a adição de um contador
de preempção, preempt_count, ao thread_info de cada processo.Esse contador começa em
zero e aumenta uma vez para cada bloqueio adquirido e diminui uma vez para cada
bloqueio liberado.Quando o contador é zero, o kernel é preemptível. Ao retornar da
interrupção, se retornar ao espaço do kernel, o kernel verifica os valores de need_resched
e
contagem_antecipada. Se need_resched estiver definido e preempt_count for zero, então
uma tarefa mais importante será executável e será segura para preempt.Assim,
o agendador será chamado. Se preempt_count for diferente de zero, um bloqueio
será mantido e não será seguro reagendar. Nesse caso, a interrupção retorna
normalmente à tarefa em execução no momento.Quando todos os bloqueios
que a tarefa cur-rent está mantendo são liberados, preempt_count retorna a
zero.Nesse momento, o código de desbloqueio verifica se need_resched está
definido. Em caso afirmativo, o programador será chamado. Ativar e desativar a
preempção do kernel às vezes é necessário no código do kernel e é discutido
no Capítulo 9.
A preempção do kernel também pode ocorrer explicitamente, quando uma tarefa nos
blocos do kernel ou chama explicitamente schedule(). Essa forma de preempção do kernel
sempre foi suportada porque nenhuma lógica adicional é necessária para garantir que o
kernel esteja em um estado seguro para
www.it-ebooks.info
64 Capítulo 4 Programação do processo
as tarefas com a mesma prioridade são executadas em rodízio, mas, novamente, só produzindo o processador quando eles
decidirem explicitamente fazê-lo. Se uma tarefa SCHED_FIFO for executável, todas as tarefas em uma prioridade mais baixa não poderão ser executadas até
que se torne inexecutável.
SCHED_RR é idêntico a SCHED_FIFO exceto que cada processo pode ser executado
somente até esgotar um intervalo de tempo predeterminado.Ou seja, SCHED_RR é
SCHED_FIFO com intervalos de tempo—ele é
a algoritmo de agendamento round-robin em tempo real.Quando uma tarefa SCHED_RR esgota
seus times-lice, todos os outros processos em tempo real em sua prioridade são agendados round-
robin.O timeslice é usado para permitir apenas o reagendamento de processos de mesma prioridade.
Assim como com SCHED_FIFO, um processo de prioridade mais alta sempre se antecipa imediatamente a
um processo de prioridade mais baixa, e um processo de prioridade mais baixa nunca pode se
antecipar a uma tarefa SCHED_RR, mesmo se sua divisão de tempo estiver esgotada.
Ambas as políticas de agendamento em tempo real implementam prioridades
estáticas.O kernel não calcula valores de prioridade dinâmicos para tarefas em
tempo real.Isso garante que um processo em tempo real em uma determinada
prioridade sempre se sobreponha a um processo em uma prioridade mais baixa.
As políticas de agendamento em tempo real no Linux oferecem um
comportamento flexível em tempo real. Soft em tempo real refere-se à noção de que
o kernel tenta agendar aplicações dentro de prazos de tempo, mas o kernel não
promete sempre alcançar esses objetivos. Por outro lado, os sistemas rígidos em
tempo real têm a garantia de atender a quaisquer requisitos de programação dentro
de certos limites. O Linux não oferece garantias sobre a capacidade de programar
tarefas em tempo real. Apesar de não ter um design que garanta um
comportamento duro em tempo real, a programação em tempo real por
desempenho no Linux é muito bom.O kernel 2.6 Linux é capaz de atender aos
requisitos de tempo rigorosos.
www.it-ebooks.info
Chamadas do Sistema Relacionadas ao Agen
www.it-ebooks.info
Conclusão
67º
enquanto. Como as tarefas em tempo real nunca expiram, elas são um caso
especial.Portanto, elas são simplesmente movidas para o final de sua lista de
prioridades (e não inseridas no array expirado). Em versões anteriores do Linux, a
semântica dasched_yield()chamada era bem diferente; na melhor das hipóteses, a
tarefa foi movida apenas para o final de sua lista de prioridades. Hoje em dia,
aplicativos e até mesmo o código do kernel devem ter certeza de que realmente
querem desistir do processador antes de chamar sched_yield().
O código do kernel, por conveniência, pode chamar yield(), que garante que
o estado da tarefa seja TASK_RUNNING e, em seguida, callogic_yield(). Os
aplicativos de espaço do usuário usam o
sched_yield() chamada do sistema.
Conclusão
O agendador de processos é uma parte importante de qualquer kernel porque a
execução de processos é (para a maioria de nós, pelo menos) o ponto de usar o
computador em primeiro lugar. Fazer malabarismo com as demandas de
agendamento de processos não é trivial, no entanto:Um grande número de
processos executáveis, preocupações com escalabilidade, compensações entre
latência e throughput, e as demandas de várias cargas de trabalho tornam difícil
alcançar um algoritmo de tamanho único.O novo agendador de processos CFS do
kernel do Linux, no entanto, está perto de apaziguar todas as partes e fornecer
uma solução ideal para a maioria dos casos de uso com boa escalabilidade por
meio de uma abordagem inovadora e interessante.
O capítulo anterior abordava o gerenciamento de processos.Este capítulo se
concentrava na teoria por trás da programação de processos e da implementação
específica, algoritmos e interfaces usados pelo kernel atual do Linux.O próximo
capítulo aborda a interface primária que o kernel fornece aos processos em
execução: chamadas do sistema.
www.it-ebooks.info
Esta página foi deixada em branco intencionalmente
www.it-ebooks.info
5.
Chamadas do Sistema
1 Há cerca de 335 chamadas de sistema em x86. (Cada arquitetura tem permissão para definir
chamadas de sistema exclusivas.) Embora nem todos os sistemas operacionais publiquem suas
chamadas de sistema exatas, estima-se que alguns sistemas operacionais tenham mais de mil. Na
edição anterior deste livro, x86 tinha apenas 250 chamadas de sistema.
2 IEEE (eye-triple-E) é o Instituto de Engenheiros Elétricos e Eletrônicos. É uma associação
profissional sem fins lucrativos envolvida em inúmeras áreas técnicas e responsável por muitos
padrões importantes, como a POSIX. Para obter mais informações, visite http://www.ieee.org.
www.it-ebooks.info
Chamadas do
sistema
71º
Chamadas do sistema
Chamadas de sistema (geralmente chamadas de syscalls no Linux) são normalmente
acessadas por chamadas de função definidas na biblioteca C. Elas podem definir zero, um
ou mais argumentos (entradas) e podem resultar em um ou mais efeitos colaterais,3 por
exemplo, gravar em um arquivo ou copiar alguns dados em um ponteiro fornecido. As
chamadas do sistema também fornecem um valor de retorno do tipo long4 que significa
sucesso ou erro — geralmente, embora nem sempre, um valor de retorno negativo denota
um erro. Um valor de retorno zero é geralmente (mas nem sempre) um sinal de sucesso. A
biblioteca C, quando uma chamada do sistema retorna um erro, grava um código de erro
especial na variável global errno. Essa variável pode ser traduzida em erros legíveis por meio
de funções de biblioteca, como
erro().
Finalmente, as chamadas do sistema têm um comportamento
definido. Por exemplo, a chamada do sistema getpid() é definida para
retornar um inteiro que é o PID do processo atual. A implementação
deste syscall no kernel é simples:
Observe que a definição não diz nada sobre a implementação. O kernel deve fornecer o
comportamento pretendido da chamada do sistema, mas é livre para fazê-lo com qualquer
implementação
3 Observe o "pode" aqui. Embora quase todas as chamadas do sistema tenham um efeito colateral (ou seja, elas
resultam em alguma alteração do estado do sistema), algumas syscalls, como getpid(),
www.it-ebooks.info
72 Capítulo 5 Chamadas do sistema
quer, desde que o resultado esteja correto. É claro que esta chamada do
sistema é tão simples quanto ela é feita, e não há muitas outras maneiras de
implementá-la.5
SYSCALL_DEFINE0é simplesmente uma macro que define uma chamada
do sistema sem parâmetros (daí o 0). O código expandido se parece com
isto:
getpid ()
www.it-ebooks.info
Manipulador de Chamadas do Sis
www.it-ebooks.info
74º Capítulo 5 Chamadas do sistema
Passagem de Parâmetro
Além do número de chamada do sistema, a maioria das syscalls requer que um
ou mais parâmetros sejam passados para eles. De alguma forma, o espaço do
usuário deve retransmitir os parâmetros para o kernel durante o trap.A maneira
mais fácil de fazer isso é através do mesmo meio que o número syscall é
passado: Os parâmetros são armazenados em registradores. Em x86-32, os
registros ebx, ecx, edx, esi , e os registros contêm, em ordem, os cinco primeiros
argumentos. No caso improvável de seis ou mais argumentos, um único
registrador é usado para manter um ponteiro para o espaço do usuário onde todos
os parâmetros são armazenados.
O valor de retorno é enviado para o espaço do usuário também via
register. Em x86, é escrito no registro eax.
como a função pode mudar ao longo do tempo. Uma nova funcionalidade pode ser
adicionada à sua chamada do sistema ou qualquer alteração exigirá uma função totalmente
nova? Você pode corrigir facilmente bugs sem quebrar a compatibilidade com versões
anteriores? Muitas chamadas do sistema fornecem um argumento de sinalizador para
abordar a compatibilidade direta.O sinalizador não é usado para multiplexar
comportamentos diferentes em uma única chamada do sistema — como mencionado, isso
não é aceitável —, mas para habilitar novas funcionalidades e opções sem interromper a
compatibilidade com versões anteriores ou precisar adicionar uma nova chamada do
sistema.
Projetar a interface com um olho em direção ao futuro é importante.Você está limitando
desnecessariamente a função? Projete a chamada do sistema para ser o mais geral
possível. Não suponha que seu uso hoje seja o mesmo que seu uso amanhã. A finalidade da
chamada do sistema permanecerá constante, mas seus usos poderão mudar. A chamada do
sistema é portátil? Não faça suposições sobre o tamanho da palavra ou a endianidade de
uma arquitetura. O Capítulo 19, "Portabilidade", discute essas questões. Certifique-se de
não estar fazendo suposições ruins que interromperão a chamada do sistema no futuro.
Lembre-se do lema do Unix: "Fornecer mecanismo, não política."
Quando você escreve uma chamada de sistema, você precisa perceber a
necessidade de portabilidade e robustez, não apenas hoje, mas no futuro.As chamadas
de sistema básicas do Unix sobreviveram a esse teste de tempo; a maioria delas é tão
útil e aplicável hoje como eram 30 anos atrás!
Verificando os Parâmetros
As chamadas do sistema devem verificar cuidadosamente todos os seus parâmetros
para garantir que são válidos e legais.A chamada do sistema é executada no espaço
do kernel, e se o usuário pode passar entrada inválida para o kernel sem restrição, a
segurança e a estabilidade do sistema podem sofrer.
Por exemplo, as syscalls de E/S de arquivo devem verificar se o descritor de arquivo
é válido. As funções relacionadas ao processo devem verificar se o PID fornecido é
válido. Todos os parâmetros devem ser verificados para garantir que não sejam apenas
válidos e legais, mas corretos. Os processos não devem pedir ao kernel para acessar
recursos aos quais o processo não tem acesso.
Uma das verificações mais importantes é a validade de todos os ponteiros
fornecidos pelo usuário. Imagine se um processo pudesse passar qualquer ponteiro
para o kernel, desmarcado, com verrugas e tudo, mesmo passando um ponteiro para o
qual ele não tinha acesso de leitura! Os processos poderiam enganar o kernel para
copiar dados para os quais eles não tinham permissão de acesso, como dados
pertencentes a outro processo ou dados mapeados ilegíveis. Antes de seguir um
ponteiro para o espaço do usuário, o sistema deve garantir que
www.it-ebooks.info
76 Capítulo 5 Chamadas do sistema
se um usuário era root; isto agora é removido e um sistema de "recursos" mais refinado
é
novo sistema permite verificações de acesso específicas em recursos específicos.Um
convite à
capabilities() com um sinalizador de recursos válidos retornará diferente de zero se o
chamador contiver o
capacidade e zero, caso contrário. Por exemplo, a opção capacitado(CAP_SYS_NICE) verifica
se o
chamador tem a capacidade de modificar valores nice de outros processos. Por padrão,
o superusuário
possui todos os recursos e não possui nenhum. Por exemplo, aqui está a função reboot()
chamada do sistema. Observe como sua primeira etapa é garantir que o processo
de chamada tenha o
CAP_SYS_REBOOT. Se essa instrução condicional fosse removida, qualquer processo
poderia
reinicialize o sistema.
www.it-ebooks.info
78 Capítulo 5 Chamadas do sistema
7 Os manipuladores de interrupção não podem ser suspensos e, portanto, são muito mais limitados no
que podem fazer do que as chamadas do sistema em execução no contexto do processo.
www.it-ebooks.info
80 Capítulo 5 Chamadas do sistema
www.it-ebooks.info
Contexto de Chamada do Sistema
A macro syscall para usar esta chamada do sistema sem suporte explícito à
biblioteca seria
www.it-ebooks.info
Conclusão
83º
Conclusão
Neste capítulo, discutimos quais são as chamadas do sistema e como elas se relacionam
com as chamadas de biblioteca e a API (interface de programação de aplicativo). Em
seguida, examinamos como o kernel do Linux implementa as chamadas do sistema e a
cadeia de eventos necessária para executar uma chamada do sistema: interceptar no kernel,
transmitir o número de syscall e quaisquer argumentos, executar a função correta de
chamada do sistema e retornar ao espaço do usuário com o valor de retorno do syscall.
Em seguida, analisamos como adicionar chamadas do sistema e fornecemos
um exemplo simples do uso de uma nova chamada do sistema do espaço do
usuário.Todo o processo foi bastante fácil! Como a simplicidade de adicionar
uma nova chamada do sistema demonstra, o trabalho está na implementação do
syscall.O restante deste livro discute conceitos e interfaces do kernel
necessárias para gravar chamadas do sistema bem comportadas, ideais e
seguras.
Finalmente, encerramos o capítulo com uma discussão sobre os prós e
contras da implementação de chamadas ao sistema e uma breve lista de
alternativas à adição de novas.
www.it-ebooks.info
Esta página foi deixada em branco intencionalmente
www.it-ebooks.info
6.
Estruturas de Dados do
Kernel
Este capítulo apresenta várias estruturas de dados incorporadas para uso no código do
kernel Linux. Como acontece com qualquer grande projeto de software, o kernel do Linux
fornece essas estruturas de dados genéricos e primitivos para incentivar a reutilização do
código. Os desenvolvedores do kernel devem usar essas estruturas de dados sempre que
possível, e não "enrolar suas próprias" soluções. Nas seções a seguir, abordamos as
estruturas de dados genéricos mais úteis, que são as seguintes:
n Listas vinculadas
n Filas
n Mapas
n Árvores binárias
Concluímos o capítulo com uma discussão sobre a complexidade algorítmica,
a facilidade com que os algoritmos e as estruturas de dados são dimensionados
para suportar entradas cada vez maiores.
Listas Vinculadas
A lista vinculada é a estrutura de dados mais simples e comum no kernel do Linux.Uma
lista vinculada é uma estrutura de dados que permite o armazenamento e a manipulação
de um número variável de elementos, chamados de nós da lista. Ao contrário de uma
matriz estática, os elementos em uma lista vinculada são criados dinamicamente e
inseridos na lista.Isso permite o gerenciamento de um número variável de elementos
desconhecidos em tempo de compilação. Como os elementos são criados em
momentos diferentes, eles não ocupam necessariamente regiões contíguas na
memória.Portanto, os elementos precisam ser vinculados; portanto, cada elemento na
lista contém um ponteiro para o próximo elemento. À medida que os elementos são
adicionados ou removidos da lista, o ponteiro para o próximo nó é simplesmente
ajustado.
·· próxi nulo
mo • · próximo · · · próximo
primeiro. Claro, dado um elemento específico na lista, você pode iterar para
trás e para frente qualquer número de elementos, também.Você não precisa
atravessar a lista inteira.
Com isso, list.next em fox aponta para o próximo elemento, eogilist.prev em fox
aponta para o anterior. Agora isso está se tornando útil, mas fica melhor.O kernel
fornece uma família de rotinas para manipular listas vinculadas. Por exemplo, o
método list_add() adiciona um novo nó a uma lista vinculada existente. Esses métodos,
no entanto, são genéricos: eles aceitam apenas estruturas list_head. Usando a macro
container_of(), podemos facilmente encontrar a estrutura par-ente contendo qualquer
variável membro dada. Isso ocorre porque em C, o deslocamento de uma dada
variável em uma estrutura é fixado pela ABI em tempo de compilação.
A lista precisa ser inicializada antes de ser usada. Como a maioria dos
elementos é criada dinamicamente (provavelmente porque você precisa
de uma lista vinculada), a maneira mais comum de inicializar a lista
vinculada é em tempo de execução:
www.it-ebooks.info
90 Capítulo 6 Estruturas de dados do kernel
Cabeçalhos de Lista
A seção anterior mostra como é fácil pegar uma estrutura existente — como o
nosso exemplo de struct fox — e transformá-la em uma lista vinculada.Com as simples
alterações de código, nossa estrutura agora é gerenciável pelas rotinas de lista
vinculada do kernel. Mas antes de podermos usar essas rotinas, precisamos de um
ponteiro canônico para nos referir à lista como um todo - um ponteiro de cabeça.
Um aspecto interessante da implementação da lista vinculada do kernel é que
nossos nós fox são indistinguíveis. Cada um contém um list_head, e podemos iterar
de qualquer nó para o próximo, até que tenhamos visto cada node.This abordagem
é elegante, mas você geralmente vai querer um ponteiro especial que se refere à
sua lista vinculada, sem ser um nó de lista em si. Interessantemente, este nó
especial é na verdade um list_head normal:
1 Veja a seção "Complexidade Algorítmica", mais adiante neste capítulo, para uma discussão
sobre .
www.it-ebooks.info
Listas Vinculadas
Esta função remove a entrada de elemento da lista. Observe que ela não libera
nenhuma memória pertencente à entrada ou à estrutura de dados na qual ela está
incorporada; essa função simplesmente remove o elemento da lista.Depois de
chamar isso, você normalmente destruiria sua estrutura de dados e o list_head
dentro dela.
Por exemplo, para excluir o nó do fox adicionado anteriormente à fox_list:
Observe que a função não recebe como input fox_list. Ele simplesmente recebe
um nó específico e modifica os ponteiros dos nós anteriores e subsequentes de
forma que o nó fornecido não faça mais parte da lista. A implementação é
instrutiva:
www.it-ebooks.info
92 Capítulo 6 Estruturas de dados do kernel
Esta função faz o mesmo que a list_move(), mas insere o elemento list
antes da entrada list.
Para verificar se uma lista está vazia
Essa função divide duas listas inserindo a lista apontada por lista para
a lista fornecida após o cabeçalho do elemento.
Para unir duas listas desconectadas e reinicializar a lista antiga
cabeçalho_d
a_lista
Bem, isso ainda é inútil! Um ponteiro para a estrutura de lista geralmente não é
bom; o que precisamos é de um ponteiro para a estrutura que contém o list_head. Por
exemplo, com o exemplo de estrutura anterior de raposa, queremos um ponteiro para
cada raposa, e não um ponteiro para o membro foxlist na estrutura. Podemos usar a
macro list_entry(), que discutimos ear-
lider, para recuperar a estrutura que contém um dado . Por exemplo:
A abordagem utilizável
A abordagem anterior não faz para o código particularmente intuitivo ou
elegante, embora ilustre como os nós list_head funcionam. Consequentemente, a
maioria do código do kernel usa a macro list_for_each_entry() para iterar sobre uma
lista vinculada. Esta macro manipula o trabalho realizado por list_entry()ogiva, tornando a
iteração de lista simples:
www.it-ebooks.info
94 Capítulo 6 Estruturas de dados do kernel
não tenha um motivo explícito para percorrer a lista ao contrário, não — apenas
use
list_for_each_entry().
Iterando ao remover
Os métodos de iteração de lista padrão não são apropriados se você estiver removendo
entradas da lista à medida que você iterar.Os métodos padrão dependem do fato de que
as entradas da lista não estão mudando abaixo delas e, portanto, se a entrada atual for
removida no corpo do loop, a iteração subsequente não poderá avançar para o próximo
ponteiro (ou anterior).Este é um padrão comum em loops, e os programadores
resolvem-no armazenando o próximo ponteiro (ou anterior) em uma variável temporária
antes de uma operação de remoção potencial.O kernel do Linux fornece uma rotina para
lidar com esta situação para você:
Você usa esta versão da mesma maneira que list_for_each_entry(), exceto que
você fornece o próximo ponteiro, que é do mesmo tipo que
list_for_each_entry_safe(), tornando seguro remover a próxima entrada da lista. Vamos
considerar um exemplo, novamente em inotify:
Filas
Um padrão de programação comum em qualquer kernel de sistema operacional é
produtor e consumidor. Nesse padrão, um produtor cria dados — digamos,
mensagens de erro a serem lidas ou pacotes de rede a serem processados —
enquanto um consumidor, por sua vez, lê, processa ou consome os dados. Em geral, a
maneira mais fácil de implementar esse padrão é com uma fila.O produtor envia dados
para a fila e o consumidor retira os dados da fila.O consumidor recupera os dados na
ordem em que foram enfileirados.Ou seja, os primeiros dados na fila são os primeiros
dados fora da fila. Por essa razão, as filas também são chamadas de FIFOs, abreviação
de first-in, first-out. Veja na Figura 6.5 um exemplo de fila padrão.
Desenfileirar
Enfileirar
Uma Fila
97º
kfifo
O kfifo do Linux funciona como a maioria das outras abstrações de fila, fornecendo
duas operações principais: enfileirar (infelizmente nomeado como in) e desenfileirar
(out).O objeto kfifo mantém dois off-sets na fila: um deslocamento de entrada e um
deslocamento de saída.O deslocamento de entrada é o local na fila para o qual o
próximo enfileiramento ocorrerá.O deslocamento de saída é o local na fila a partir
do qual o próximo desenfileiramento ocorrerá.O deslocamento de saída é sempre
menor ou igual ao deslocamento de entrada. Não faria sentido se fosse maior; caso
contrário, você poderia desenfileirar dados que ainda não tinham sido enfileirados.
A operação de enfileiramento (entrada) copia dados na fila, iniciando no
deslocamento interno.Quando concluída, o deslocamento interno é incrementado
pela quantidade de dados enfileirados.A operação de desenfileiramento (saída)
copia dados para fora da fila, iniciando no deslocamento externo.Quando concluída,
o deslocamento externo é incrementado pela quantidade de dados
enfileirados.Quando o deslocamento externo é igual ao deslocamento interno, a fila
fica vazia: Não é possível desenfileirar mais dados até que mais dados sejam
enfileirados.Quando o deslocamento interno é igual ao comprimento da fila, não é
possível enfileirar mais dados até que A.
Esta função cria e inicializa um kfifo com uma fila de tamanho de bytes.O kernel
usa a máscara gfp gfp_mask para alocar a fila. (Discutimos alocações de memória no
Capítulo 12, "Gerenciamento de memória"). Com sucesso, kfifo_ alloc () retorna zero;
em caso de erro retorna um código de erro negativo. Veja a seguir um exemplo
simples:
Se você mesmo quiser alocar o buffer, poderá:
www.it-ebooks.info
98 Capítulo 6 Estruturas de dados do kernel
Isto cria um kfifo estático chamado name com uma fila de tamanho
bytes. Como antes, o tamanho deve ser uma potência de 2.
Enfileirando Dados
Quando o seu kfifo é criado e inicializado, o enfileiramento de dados na
fila é executado através da função kfifo_ in ():
Esta função copia os bytes de len iniciando em de para a fila representada por ? Em
caso de sucesso, retorna o número de bytes enfileirados. Se menos de len bytes
estiverem livres na fila, a função copiará somente até a quantidade de bytes
disponíveis. Assim, o valor de retorno pode ser menor que len ou mesmo zero, se nada
tiver sido copiado.
Desenfileirando Dados
Quando você adiciona dados a uma fila com kfifo_in(), você pode removê-la com
kfifo_out():
Esta função copia no máximo len bytes da fila apontada por fifo para o buffer
apontado por fifo. Em caso de sucesso, a função retorna o número de bytes copiados.
Se menos de len bytes estiverem na fila, a função copiará menos do que o solicitado.
Quando desenfileirados, os dados não estão mais acessíveis da fila.
Este é o uso normal de uma fila, mas se você quiser "espiar" os dados
dentro da fila sem removê-la, você pode usar kfifo_out_peek():
99º
Para destruir um kfifo alocado com o kfifo_ alloc (), chame o kfifo_ free ():
Mapas
Um mapa, também conhecido como matriz associativa, é uma coleção de
chaves exclusivas, onde cada chave é associada a um valor específico. A
relação entre uma chave e seu valor é chamada de mapeamento. Os mapas
suportam pelo menos três operações:
n Adicionar (chave, valor)
n Remover (chave)
n valor = Lookup (chave)
Embora uma tabela de hash seja um tipo de mapa, nem todos os mapas são
implementados via hashes. Em vez de uma tabela de hash, os mapas também
podem usar uma árvore de pesquisa binária com balanceamento automático para
armazenar seus dados.Embora um hash ofereça melhor complexidade assintótica
de caso médio (consulte a seção "Complexidade Algorítmica", mais adiante neste
capítulo), uma árvore de pesquisa binária tem melhor comportamento de pior caso
(logarítmico versus linear).Uma árvore de pesquisa binária também permite a
preservação da ordem, permitindo que os usuários iterem eficientemente sobre
toda a coleção em uma ordem classificada. Finalmente, uma árvore de busca
binária não requer uma função hash; em vez disso, qualquer tipo de chave é
adequado, desde que possa definir o operador <=.
Embora o termo geral para todas as coleções mapeiem uma chave para um
valor, os mapas de nome muitas vezes se referem especificamente a uma matriz
associada implementada usando uma árvore de pesquisa binária em oposição a
uma tabela hash. Por exemplo, o contêiner STL C++ std::map é implementado usando
uma árvore de pesquisa binária de balanceamento automático (ou estrutura de
dados semelhante), porque fornece a capacidade de percorrer a coleção em ordem.
O kernel Linux fornece uma estrutura de dados de mapa simples e eficiente, mas não é
um mapa de propósito geral. Em vez disso, ele é projetado para um caso de uso
específico: mapear um único
www.it-ebooks.info