Você está na página 1de 232

www.it-ebooks.

info
Desenvolvi
mento de
kernel Linux

Terceira Edição
www.it-ebooks.info
Biblioteca do desenvolvedor
REFERÊNCIAS ESSENCIAIS PARA PROFISSIONAIS DE PROGRAMAÇÃO

Os livros da Biblioteca do Desenvolvedor são projetados para fornecer


aos programadores praticantes referências e tutoriais exclusivos e de
alta qualidade sobre as linguagens de programação e as tecnologias
que eles usam em seu trabalho diário.
Todos os livros da Biblioteca do Desenvolvedor são escritos por
profissionais especialistas em tecnologia que são especialmente
capacitados em organizar e apresentar informações de uma forma
útil para outros programadores.

Os principais títulos incluem alguns dos melhores e mais


aclamados livros em suas áreas temáticas:
Desenvolvimento Web de PHP e Referência essencial Python
MySQL
Luke Welling & Laura Thomson David Beazley
ISBN 978-0-672-32916-6 Portal ISBN-13: 978-0-672-32978-6
da arte Portal da arte
MySQL Programação no Objetivo-C 2.0
Paul DuBois Stephen G. Kochan
ISBN-13: 978-0-672-32938 -8 ISBN- 13: 978- 0- 321 -56615-7
Desenvolvimento de kernel Linux SQLPostgre
Robert Love Korry Douglas ISBN-
ISBN-13: 978-0-672-32946 -3 13: 978- 0- 672 -33015-5

Os livros da biblioteca do desenvolvedor estão disponíveis na maioria das


livrarias de varejo e on-line, bem como por assinatura da Safari Books Online
em safari.informit.com

Desenvolvedor
Biblioteca
informit.com/devlibrary
www.it-ebooks.info
Desenvolvi
mento de
kernel Linux

Terceira Edição

Robert Love

Upper Saddle River, NJ · Boston · Indianápolis · San Francisco


Nova Iorque · Toronto · Montreal ·Londres · · Boston ·
Indianápolis · Tóquio · Singapura · Cidade do México
www.it-ebooks.info
Desenvolvimento de kernel Linux Editor de
aquisições
Terceira Edição
Marcar
Copyright © 2010 Pearson Education, Inc. Tabulação
Todos os direitos reservados. Impresso nos Estados Unidos. Esta publicação está Editor de
protegida por direitos autorais e a permissão deve ser obtida do editor antes de Desenvolvim
ento
qualquer reprodução, armazenamento em um sistema de recuperação ou
transmissão proibida, de qualquer forma ou por qualquer meio, eletrônico, Michael Thurston
mecânico, fotocópia, gravação ou da mesma forma. Editor técnico
Robert P. J. Day
ISBN-13: 978-0-672-32946-3
ISBN- 10: 0- 672- 32946- 8 Gerenciando o
Editor
Dados de Catalogação na Publicação da Biblioteca do Congresso: Sandra Schroeder
Com amor, Robert. Editor de
Projeto
Desenvolvimento do núcleo Linux / Robert Love. — 3ª ed. Sênior
p. cm. Tonya Simpson
Copiar Editor
Inclui referências bibliográficas e índice.
Serviços de
ISBN 978-0-672-32946-3 (pbk. : alk. paper) 1. Linux 2) Sistemas operacionais Edição de
(computadores) Apóstrofos
I. Título. QA76.76.O63L674 2010 005.4’32—dc22 Indexador
2010018961 Brad Herriman
Revisor
Texto impresso nos Estados Unidos em papel reciclado em RR Donnelley,
Crawfordsville, Indiana. Primeira impressão junho de 2010 Debbie Williams
Coordenado
Muitas das designações utilizadas pelos fabricantes e vendedores para distinguir os r de
seus produtos são reivindicadas como marcas comerciais. Quando essas Publicação
designações aparecem neste livro, e o publicador tinha conhecimento de uma Vanessa Evans
reivindicação de marca, as designações foram impressas com letras maiúsculas
iniciais ou em todas as letras maiúsculas. Designer de
Livros
O autor e o editor têm tomado cuidado na preparação deste livro, mas não fazem nenhuma Gary Adair
garantia expressa ou implícita de qualquer tipo e não assumem nenhuma responsabilidade
por erros ou omissões. Nenhuma responsabilidade é assumida por danos incidentais ou
Compositor
consequenciais relacionados ou decorrentes da utilização das informações ou programas Mark Shirar
aqui contidos.

O editor oferece excelentes descontos neste livro quando encomendado em


quantidade para compras em massa ou vendas especiais, que podem incluir
versões eletrônicas e/ou capas personalizadas e conteúdo específico para o seu
negócio, objetivos de treinamento, foco de marketing e interesses de marca. Para
obter mais informações, entre em contato com:
Vendas corporativas e governamentais nos EUA
(800) 382-3419 corpsales@pearsontechgroup.com

Para vendas fora dos Estados Unidos, entre em contato com:


Vendas Internacionais
international@pearson.com
Visite-nos na Web: informit.com/aw
www.it-ebooks.info
Para Doris e Helen.

www.it-ebooks.info
Visão geral do conteúdo
1 Introdução ao Linux Kernel 1

2 Introdução ao Kernel 11.

3. Gerenciamento de processos 23º

4. Agendamento do Processo 41

5. Chamadas do Sistema 69

6. Estruturas de Dados do Kernel 85

7. º Interrupções e manipuladores de interrupção 113º

8. Metades Inferiores e Trabalho de Adiamento 133

9. Uma Introdução à Sincronização do Kernel 161

10. Métodos de Sincronização do Kernel 175º

11. Temporizadores e gerenciamento de tempo 207

12 Gerenciamento de memória 231

13º O sistema de arquivos virtual 261

14. A camada de I/O de bloco 289

15 O Espaço de Endereço do Processo 305

16º O Cache de Página e o Write-back de Página 323

17º Dispositivos e Módulos 337

18º Depuração 363

19. Portabilidade 379

20. Patches, hackers e a comunidade 395

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

3. Gerenciamento de processos 23º


O processo 23º
Descritor do Processo e a Estrutura da Tarefa 24º
Alocando o descritor do processo 25º
Armazenando o descritor do processo 26
Estado do Processo 27º
Manipulando o Estado do Processo Atual 29º
Contexto do Processo 29º
A árvore genealógica do processo 29
Criação de Processo 31
Copy-on-Write (cópia na gravação) 31
Bifurcação 32
vfork () 33º
A implementação Linux de threads 33º
Criando Threads 34º
Threads do Kernel 35º
Término do Processo 36º
Removendo o Descritor de Processo 37º
O dilema da tarefa sem pais 38
Conclusão 40º

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)

Seleção de Processo 52º


Escolhendo a próxima tarefa 53
Adicionando processos à árvore
Removendo processos da árvore 56
O Ponto de Entrada do Agendador 5
Dormindo e Acordando 58
Filas de Espera 58º
Acordando 61º
Preempção e Alternância de Contexto
Preempção do Usuário 62
Preempção de Kernel 63º
Políticas de agendamento em tempo real
Chamadas do Sistema Relacionadas ao Agendador
Política de programação e relacionada a prioridades
Chamadas do Sistema 66º
Chamadas do Sistema de Afinidade do Processador
Gerando tempo do processador 66º
Conclusão 67

5. Chamadas do Sistema 69º


Comunicação com o Kernel 69
APIs, POSIX e a biblioteca C 70
Chamadas do
sistema 71º
Números de Chamada do Sistema 72º
Desempenho da Chamada do Sistema 72º
Manipulador de Chamadas do Sistema 73º
Denotando a chamada de sistema correta 73
Passagem de Parâmetro 74º
Implementação de chamada do sistema 74º
Implementando Chamadas do Sistema 74
Verificando os Parâmetros 75º
Contexto de Chamada do Sistema 78º
Etapas finais na vinculação de uma chamada do sistema
Acessando a chamada do sistema do espaço do usuário 81
Por que não implementar uma chamada de sistema
Conclusão 83º
www.it-ebooks.info
x Sumário

6 Estruturas de dados do kernel 85º


Listas Vinculadas 85
Listas Vinculadas Única e Duplamente 85º
Listas Vinculadas Circulares 86º
Movendo-se por uma Lista Vinculada 87º
Implementação do Linux Kernel 88º
A Estrutura da Lista Vinculada 88º
Definindo uma Lista Vinculada 89º
Cabeçalhos de Lista 90
Manipulação de Listas Vinculadas 90º
Adicionando um nó a uma lista vinculada 90º

Percorrendo Listas Vinculadas 93º


A abordagem básica 93º
A abordagem utilizável 93º
Iterando através de uma lista para trás 94
Iterando ao remover 95º
Outros Métodos de Lista Vinculada 96º
Filas 96º
kfifo 97º
Criando uma Fila 97º
Enfileirando Dados 98º
Desenfileirando Dados 98º
Obtendo o Tamanho de uma Fila 98º
Redefinindo e Destruindo a Fila 99
Exemplo de Uso da Fila 99º
Mapas 100
Inicializando uma idr 101
Alocando um Novo UID 101
Procurando um UID 102
Removendo um UID 103
Destruindo um idr 103
Árvores Binárias 103
Árvores de Pesquisa Binárias 104
Árvores de pesquisa binárias com autoequilíbrio 105
Árvores Vermelho-Pretas 105
Árvores 116

www.it-ebooks.info
Sumário xi

Qual Estrutura de Dados Usar, Quando 108


Complexidade algorítmica
Algoritmos
Notação Big-O
Notação Teta Grande
Complexidade de tempo
Conclusão 111

7 Interrupções e manipuladores de interrupção


Interrupções 113
Manipuladores de interrupção 114
Metades Superiores Versus Metades Inferiores
Registrando um manipulador de interrupção
Sinalizadores de Manipulador de Interrupção 116º
Exemplo de interrupção 117
Liberando um manipulador de interrupção
Escrevendo um manipulador de interrupção
Manipuladores Compartilhados 119º
Um manipulador de interrupção real
Contexto de interrupção 122
Implementando manipuladores de interrupção
/proc/interrupções 126º
Controle de interrupção 127
Desativando e Ativando Interrupções
Desativando uma linha de interrupção específica
Status do sistema de interrupção
Conclusão 131

8. Metades Inferiores e Trabalho de Adiamento 133º


Metades Inferiores 134
Por que Metades Inferiores? 134
Um Mundo de Metades Inferiores 135
A "Metade Inferior" Original 135
Filas de Tarefas 135
Softirqs e Tasklets 136º
Dissipando a Confusão 137

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º

9. Uma introdução à sincronização do kernel


161 regiões críticas e condições de corrida 162

Por Que Precisamos De Proteção? 162º


A única variável 163º

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

10. Métodos de Sincronização do Kernel


Operações Atômicas 175º
Operações Atômicas com Números Inteiros
Operações Atômicas De 64 Bits 180º
Operações Atômicas Bit A Bit
Travas de Rotação 183º
Métodos de bloqueio de rotação 184º
Outros métodos de bloqueio de rotação
Travas de rotação e metades inferiores
Bloqueios de Rotação de Leitor-Gravador 188
Semáforos 190
Contagem e Semáforos Binários 191
Criando e inicializando semáforos 192
Uso de Semáforos 196
Semáforos Leitor-Escritor 194º
Mutexes 197
Semáforos Versus Mutexes
Travas De Rotação Versus Mutexes 197
Variáveis de conclusão 197
BKL: A Grande Fechadura do Kernel 198
Bloqueios Sequenciais 200
Desabilitação de Preempção 201
Pedidos e barreiras 203
Conclusão 206

11. Temporizadores e gerenciamento de tempo


Noção de Tempo do Kernel 208
A Taxa de Escala: HZ 208
O valor ideal de HZ 210
Vantagens com um HZ maior 210
Desvantagens com um HZ maior 211

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

12 Gerenciamento de Memória 231


Páginas 231
Zonas 233
Obtendo Páginas 235
Obtendo Páginas Anuladas 236
Liberação de Páginas 237
kmalloc () 238
Sinalizadores gfp_mask 238
Modificadores de Ação 239
Modificadores de zona 240
Sinalizadores de Tipo 241
kfree () 243º
vmalloc () 244
Camada de laje 245
Design da camada de laje246

www.it-ebooks.info
Sumário

Vx

Interface do alocador de laje 249


Alocando a partir do cache
Exemplo de uso do alocador de laje 251
Alocação Estática na Pilha
Pilhas de kernel de página única 252
Reproduzindo razoavelmente na pilha 253
Mapeamentos de Memória Alta 253
Mapeamentos Permanentes 254
Mapeamentos Temporários 254
Alocações por CPU 255
A nova interface percpu 256
Dados por CPU em tempo de compilação
Dados por CPU em Tempo de Execução
Razões para usar dados por CPU
Separando um Método de Alocação
Conclusão 260

13 O sistema virtual de arquivos 261


Interface comum do sistema de arquivos 26
Camada de Abstração do Sistema de Arquivos 262
Sistemas de arquivos Unix 263
Objetos VFS e suas estruturas de dados 265
O Objeto Superblock 266
Operações de superbloco 267
O Objeto Inode 270
Operações do Inode 271
O Objeto Dentry 275
Estado do Dentry 276
O Cache Dentry 276
Operações de Dentry 278
O objeto de arquivo 279
Operações de Arquivo 280
Estruturas de dados associadas a sistemas de arquivos
Estruturas de Dados Associadas a um Processo
Conclusão 288
www.it-ebooks.info
xvi Sumário

14 A camada de I/O de bloco 289


Anatomia de um dispositivo de bloco 290
Buffers e cabeçotes de buffer 291
A estrutura biológica 294
Vetores de E/S 295
O Velho Versus o Novo 296
Filas de Solicitação 297
Programadores de I/O 297
O Trabalho de um Programador de I/O 298
O elevador Linus 299
O Programador de E/S com Prazo Final 300
O programador de I/O antecipado 302
O Agendador de E/S 303 do Fair Queuing Completo
O programador de I/O Noop 303
Seleção do Agendador de E/S 304
Conclusão 304

15 O espaço de endereço do processo 305


Espaços de Endereços 305
O descritor de memória 306
Alocando um descritor de memória 308
Destruindo um descritor de memória 309
Os segmentos de mm_struct e Kernel 309
Áreas de memória virtual 309
Sinalizadores de VMA 311
Operações do VMA 312
Listas e Árvores de Áreas de Memória 313
Áreas de memória em vida real 314
Manipulação de Áreas de Memória 315
find_vma () 316
find_ vma_ prev () 317
find_vma_intersection() 317
mmap() e do_mmap(): Criando um
Intervalo de Endereço 318
munmap() and do_munmap(): Removendo um
Intervalo de Endereço 320
Tabelas de Página 320
Conclusão 322

www.it-ebooks.info
Sumário

xvii

16 O cache de páginas e o write-back de páginas


Abordagens ao cache 323
Cache de gravação 324
Remoção de Cache 324
Menos Usado Recentemente 325
A Estratégia De Duas Listas 325
O cache de páginas do Linux 326
O Objeto address_space 326
Operações address_space 328
Árvore Radix 330
A Tabela de Hash de Página Antiga 330
O Cache de Buffer 330
Os Threads do Flusher 331
Modo Laptop 333
Histórico: bdflush, kupdated e pdflush
Evitando congestionamento com vários segmentos 334
Conclusão 335

17º Dispositivos e Módulos 337


Tipos de Dispositivo 337
Módulos 338
Olá, Mundo! 338
Construindo Módulos 34
Vivendo na árvore de origem 340
Vivendo Externamente
Instalando Módulos 3
Gerando dependências de módulo
Carregando Módulos 34
Gerenciando Opções de Configuração
Parâmetros do módulo
Símbolos exportados 3
O modelo do dispositivo 348
Objetos do KName 349
Ktypes 350
Ksets 351
Interrelação de Kobjects, Ktypes e Ksets 351
Gerenciando e manipulando Kobjects 352
www.it-ebooks.info
xviii Sumário

Contagens de Referência 353


Incrementando e diminuindo
Contagens de Referência 354
Krefs 354
sysfs355
Adicionando e removendo kobjects do sysfs 357
Adicionando arquivos a sysfs 358
Atributos Padrão 358
Criando Novos Atributos 359
Destruindo atributos 360
Convenções sysfs 360
A Camada de Eventos do Kernel 361
Conclusão 362

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

Pesquisa Binária para Localizar a Alteração Culpada


Pesquisa Binária com o Git 376
Quando tudo mais falhar: a Comunidade
Conclusão 378

19. Portabilidade 379


Sistemas operacionais portáteis 379
História da portabilidade no Linux 380
Tamanho da Palavra e Tipos de Dados 381
Tipos Opacos 384
Tipos Especiais 384
Tipos de tamanho explícito 385
Assinatura de caracteres 386
Alinhamento de dados 386
Evitando problemas de alinhamento 387
Alinhamento de Tipos Não Padrão
Preenchimento da Estrutura 387
Ordem de Bytes 389
Hora 391
Tamanho da Página 391
Pedidos de processadores 392
SMP, Preempção de kernel e alta memória
Conclusão 393

20. Patches, hackers e a comunidade


A Comunidade 395
Estilo de codificação do Linux 396
Recuo 396
Instruções do Switch 396
Espaçamento 397
Chaves 398
Comprimento da Linha 399
Nomeação 400
Funções 400
Comentários 400
Definições de tipo 401
Usar Rotinas Existentes 402
www.it-ebooks.info
xx Sumário

Minimizar ifdefs na Origem 402


Inicializadores de Estrutura 402
Corrigindo O Código Ex Post Fato 403
Cadeia de comando 403
Enviando Relatórios de Erros 403
Correções 404
Gerando Patches 404
Gerando Patches com o Git 405
Enviando Patches 406
Conclusão 406

Bibliografia 407

Índice 411

www.it-ebooks.info
Prefácio

À medida que o kernel do Linux e os aplicativos que o usam se tornam mais


amplamente utilizados, estamos vendo um número crescente de desenvolvedores de
software de sistema que desejam se envolver no desenvolvimento e manutenção do
Linux. Alguns desses engenheiros são motivados puramente por interesse pessoal,
alguns trabalham para empresas Linux, alguns trabalham para fabricantes de hardware
e alguns estão envolvidos com projetos de desenvolvimento internos.
Mas todos enfrentam um problema comum:A curva de aprendizado para o kernel está
ficando mais longa e íngreme.O sistema está se tornando cada vez mais complexo e é muito
grande. E à medida que os anos passam, os atuais membros da equipe de desenvolvimento
do kernel ganham um conhecimento mais profundo e mais amplo dos recursos internos do
kernel, o que amplia a lacuna entre eles e os recém-chegados. Eu acredito que esta redução
na acessibilidade da base de fontes do Linux já é um problema
para a qualidade do kernel, e vai se tornar mais sério ao longo do
tempo.Aqueles que cuidam do Linux claramente têm um interesse em
aumentar o número de desenvolvedores que podem contribuir para o
kernel.
Uma abordagem para esse problema é manter o código limpo: interfaces sensatas,
layout consistente, "faça uma coisa, faça bem" e assim por diante. Essa é a solução de
Linus Torvalds.
A abordagem que eu aconselho é aplicar livremente o comentário ao código:
palavras que o leitor pode usar para entender o que o codificador pretendia alcançar
no momento. (O processo de identificação de divergências entre a intenção e a
implementação é conhecido como depuração. É difícil fazer isso se a intenção não for
conhecida.)
Mas mesmo o comentário de código não fornece uma visão abrangente do que
um subsistema principal deve fazer e de como seus desenvolvedores se propõem
a fazer isso.Isso, o ponto de partida do entendimento, é o que a palavra escrita
serve melhor.
A contribuição de Robert Love fornece um meio pelo qual desenvolvedores
experientes podem obter essa visão essencial de quais serviços os subsistemas
do kernel devem fornecer, e de como eles se propõem a fornecê-los.Isso será
conhecimento suficiente para muitas pessoas: os curiosos, os desenvolvedores
de aplicativos, aqueles que desejam avaliar o design do kernel e outros.
Mas o livro também é um trampolim para levar os aspirantes a desenvolvedores do
kernel para o próximo estágio, que é fazer alterações no kernel para alcançar algum objetivo
definido. Eu encorajaria aspirantes a desenvolvedores a sujar as mãos:A melhor maneira de
entender uma parte do kernel é fazer mudanças nele. Fazer uma mudança força o
desenvolvedor a um nível de compreensão que meramente ler o código não fornece.O
desenvolvedor kernel sério irá se juntar as listas de discussão de desenvolvimento e irá
interagir com outros desenvolvedores.Esta interação é o principal meio pelo qual os
contribuidores kernel aprender
www.it-ebooks.info
e fique a par. Robert aborda bem a mecânica e a cultura desta parte
importante da vida do núcleo.
Por favor, aproveite e aprenda com o livro de Robert. E se você decidir dar o
próximo passo e se tornar um membro da comunidade de desenvolvimento do
kernel, considere-se bem-vindo com antecedência. Nós valorizamos e medimos as
pessoas pela utilidade de suas contribuições, e quando você contribui para o
Linux, você faz isso sabendo que seu trabalho é de pequeno, mas imediato
benefício para dezenas ou mesmo centenas de milhões de seres humanos. Este é
um privilégio e uma responsabilidade muito agradáveis.

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.

Usando Este Livro


O desenvolvimento de código no kernel não requer gênio, magia, ou uma barba hacker
Unix-ardente.O kernel, embora tendo algumas regras interessantes por si só, não é
muito diferente de qualquer outro grande esforço de software.Você precisa dominar
muitos detalhes—como com qualquer grande projeto—mas as diferenças são
quantitativas, não qualitativas.
1

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.

Confirmações da Terceira Edição


Como a maioria dos autores, eu não escrevi esse livro numa caverna, o que é uma
coisa boa, porque há ursos em cavernas. Consequentemente, muitos corações e
mentes contribuíram para a conclusão deste manuscrito. Embora nenhuma lista
pudesse ser completa, é meu sincero prazer reconhecer a assistência de muitos
amigos e colegas que forneceram encorajamento, conhecimento e críticas
construtivas.
Primeiro, gostaria de agradecer à minha equipe na Addison-Wesley e
Pearson, que trabalharam muito e arduamente para tornar este livro
melhor, particularmente Mark Taber por liderar esta terceira edição da
concepção ao produto final; Michael Thurston, editor de desenvolvimento;
e Tonya Simpson, editora de projetos.
Um agradecimento especial ao meu editor técnico nesta edição, Robert P. J. Day.
Sua percepção, experiência e correções melhoraram este livro de forma imensurável.
No entanto, apesar do seu excelente esforço, quaisquer erros que restem continuam a
ser os meus. Tenho a mesma gratidão a Adam Belay, Zack Brown, Martin Pool e Chris
Rivera, cujos excelentes esforços de edição técnica na primeira e segunda edições
ainda brilham.
Muitos outros desenvolvedores do kernel responderam perguntas,
forneceram suporte ou simplesmente escreveram código interessante o
suficiente para escrever um livro.Eles incluem Andrea Arcangeli, Alan
Cox, Greg Kroah-Hartman, Dave Miller, Patrick Mochel, Andrew Morton,
Nick Piggin e Linus Torvalds.

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

Este capítulo apresenta o kernel Linux e o sistema operacional Linux, colocando-os no


contexto histórico do Unix.Hoje, o Unix é uma família de sistemas operacionais que
implementam uma interface de programação de aplicativo (API) semelhante e
construída em torno de deci-sões de design compartilhadas. Mas o Unix também é um
sistema operacional específico, criado há mais de 40 anos.Para entender o Linux,
primeiro devemos discutir o primeiro sistema Unix.

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

1 E o System IV? Era uma versão de desenvolvimento interno.


www.it-ebooks.info
2 Capítulo 1 Introdução ao Linux Kernel

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

Rede TCP/IP. Muitas variantes do Unix chegam a centenas de processadores,


enquanto outros sistemas Unix são executados em pequenos dispositivos
incorporados.Embora o Unix não seja mais um projeto de pesquisa, os sistemas
Unix continuam a se beneficiar dos avanços no design de sistemas operacionais,
permanecendo um sistema operacional prático e de uso geral.
Unix deve seu sucesso à simplicidade e elegância de seu design. Sua
força hoje deriva das decisões inaugurais que Dennis Ritchie, Ken
Thompson e outros desenvolvedores fizeram: escolhas que dotaram o
Unix com a capacidade de evoluir sem se comprometer.

Along Came Linus: Introdução ao Linux


Linus Torvalds desenvolveu a primeira versão do Linux em 1991 como um sistema
operacional para computadores alimentados pelo microprocessador Intel 80386, que na
época era um novo e avançado processador. Linus, então estudante da Universidade de
Helsinque, ficou perturbado pela falta de um sistema Unix poderoso e gratuito.O sistema
operacional de computador pessoal da época, o DOS da Microsoft, foi útil para Torvalds por
pouco mais do que jogar Prince of Persia. Linus usou o Minix, um Unix de baixo custo
criado como um auxílio didático, mas ele foi desencorajado pela incapacidade de fazer e
distribuir facilmente alterações no código-fonte do sistema (por causa da licença do Minix) e
por decisões de design feitas pelo autor do Minix.
Em resposta à sua situação, Linus fez o que qualquer estudante universitário
normal faria: ele decidiu escrever seu próprio sistema operacional. Linus
começou escrevendo um simples emulador de terminal, que ele usou para
conectar sistemas Unix maiores em sua escola. Ao longo do ano acadêmico,
seu emulador terminal evoluiu e melhorou. Em pouco tempo, Linus tinha um
Unix imaturo, mas de pleno direito em suas mãos. Ele postou um lançamento
antecipado na Internet no final de 1991.
O uso do Linux decolou, com as primeiras distribuições Linux rapidamente
ganhando muitos usuários. Mais importante para seu sucesso inicial, no entanto, é
que o Linux rapidamente atraiu muitos desenvolvedores—hackers adicionando,
mudando, melhorando o código. Por causa dos termos de sua licença, o Linux
evoluiu rapidamente para um projeto colaborativo desenvolvido por muitos.
Avançando rapidamente para o presente.Hoje, o Linux é um sistema operacional
completo que também executa Alpha, ARM, PowerPC, SPARC, x86-64 e muitas outras
arquiteturas. Ele é executado em sistemas tão pequenos quanto um relógio para máquinas
tão grandes quanto clusters de supercomputadores que preenchem salas.
O Linux alimenta os menores equipamentos eletrônicos de consumo e os maiores
data centers.Hoje, o interesse comercial no Linux é grande. Tanto as novas
corporações específicas do Linux, como a Red Hat, quanto as potências existentes,
como a IBM, estão fornecendo soluções baseadas no Linux para necessidades
incorporadas, móveis, de desktop e de servidor.
Linux é um sistema Unix-like, mas não é Unix.Isto é, embora Linux
empresta muitas ideias do Unix e implementa a API Unix (como definido
pelo POSIX e a Especificação Unix Single), não é um descendente direto do
código fonte Unix como outros sistemas Unix.Onde desejado, ele se desviou
do caminho tomado por outras implementações, mas não abandonou os
objetivos gerais de design do Unix ou interfaces de aplicativos padronizadas
quebradas.

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.

Visão geral de sistemas operacionais e kernels


Devido ao conjunto de características em constante crescimento e ao mau design de alguns
sistemas operacionais comerciais modernos, a noção do que define precisamente um
sistema operacional não é universal. Muitos usuários consideram o que veem na tela como
o sistema operacional.Tecnicamente falando, e neste livro, o sistema operacional é
considerado as partes do sistema responsáveis pelo uso e administração básicos.Isso
inclui o kernel e drivers de dispositivo, carregador de inicialização, shell de comando ou
outra interface de usuário, e utilitários básicos de arquivo e sistema. É o que você precisa,
não um navegador da Web ou players de música.O termo sistema, por sua vez, se refere ao
sistema operacional e a todos os aplicativos em execução nele.
É claro que o tópico deste livro é o kernel.Enquanto a interface de usuário é a parte mais
externa do sistema operacional, o kernel é a mais interna. É o núcleo interno; o software
que fornece serviços básicos para todas as outras partes do sistema, gerencia o hardware
e distribui recursos do sistema.O kernel é algumas vezes chamado de supervisor, núcleo
ou internos do sistema operacional.Os componentes típicos de um kernel são
manipuladores de interrupção para solicitações de interrupção de serviço, um agendador
para compartilhar o tempo do processador entre vários processos, um sistema de
gerenciamento de memória para gerenciar espaços de endereço de processo e serviços do
sistema, como rede e comunicação entre processos. Ligado

3 Deixo-vos o debate livre versus aberto. Consulte http://www.fsf.org e


http://www.opensource. org.
4 Você deve ler a GNU GPL versão 2.0. Há uma cópia no arquivo COPYING na árvore de origem do
kernel. Você também pode encontrá-lo online em http://www.fsf.org. Note que a última versão da
GNU GPL é a versão 3.0; os desenvolvedores do kernel decidiram permanecer com a versão 2.0.
www.it-ebooks.info
Visão geral de sistemas operacionais e kernels

sistemas modernos com unidades de gerenciamento de memória protegidas, o kernel


normalmente reside em um estado de sistema elevado em comparação com os
aplicativos normais do usuário.Isso inclui um espaço de memória protegido e acesso
total ao hardware.Esse estado do sistema e espaço de memória é coletivamente
chamado de espaço-kernel. Por outro lado, os aplicativos do usuário são executados no
espaço do usuário. Eles veem um subconjunto dos recursos disponíveis da máquina e
podem executar determinadas funções do sistema, acessar diretamente o hardware,
acessar a memória fora do alocado pelo kernel ou, de outra forma, comportar-se
incorretamente.Ao executar o código do kernel, o sistema está no modo kernel-space
executando no modo kernel.Ao executar um processo regular, o sistema está no espaço
do usuário executando no modo do usuário.
Aplicativos em execução no sistema se comunicam com o kernel por meio de
chamadas do sistema (veja a Figura 1.1).Um aplicativo normalmente chama funções em
uma biblioteca — por exemplo, a biblioteca C — que, por sua vez, depende da interface
de chamada do sistema para instruir o kernel a realizar tarefas em nome do aplicativo.
Algumas chamadas de biblioteca fornecem muitos recursos não encontrados na
chamada do sistema e, portanto, chamar o kernel é apenas um passo em uma função
grande. Por exemplo, considere a familiar função printf(). Ele fornece formatação e
armazenamento em buffer dos dados; apenas uma etapa em seu trabalho é chamar write()
para gravar os dados no console. Por outro lado, algumas chamadas de biblioteca têm
uma relação um-para-um com o kernel. Por exemplo, a função open() library faz pouco,
exceto chamar a chamada do sistema open(). Ainda outras funções da biblioteca C, como
strcpy(), devem (espera-se) não fazer uso direto do kernel. Quando um aplicativo executa
uma chamada do sistema, dizemos que o kernel está sendo executado em nome do
aplicativo. Além disso, diz-se que o aplicativo está executando uma chamada do
sistema no espaço do kernel e que o kernel está sendo executado no contexto do
processo. Essa relação — que os aplicativos chamam para o kernel através da interface
de chamada do sistema — é o principal elemento em que os aplicativos realizam o
trabalho.
O kernel também gerencia o hardware do sistema. Quase todas as arquiteturas,
incluindo todos os sistemas suportados pelo Linux, fornecem o conceito de
interrupções.Quando o hardware quer se comunicar com o sistema, ele emite uma
interrupção que literalmente interrompe o processador, o que, por sua vez, interrompe o
kernel.Um número identifica interrupções e o kernel usa esse número para executar um
manipulador de interrupções específico para processar e responder à interrupção. Por
exemplo, quando você digita, o controlador de teclado emite uma interrupção para permitir
que o sistema saiba que há novos dados no buffer do teclado.O kernel observa o número
da interrupção de entrada e executa o manipulador de interrupção correto.O manipulador
de interrupção processa os dados do teclado e permite que o controlador de teclado saiba
que está pronto para mais dados.Para fornecer sincronização, o kernel pode desativar
interrupções — todas as interrupções ou apenas um número de interrupção específico. Em
muitos sistemas operacionais, incluindo o Linux, os manipuladores de interrupção não são
executados em um contexto de processo. Em vez disso, eles são executados em um
contexto de interrupção especial que não está associado a nenhum processo. Esse
contexto especial existe apenas para permitir que um manipulador de interrupção responda
rapidamente a uma interrupção e saia.
Estes contextos representam a amplitude das atividades do núcleo. Na verdade, no
Linux, podemos generalizar que cada processador está fazendo exatamente uma das três
coisas a qualquer momento:
n No espaço do usuário, executando código do usuário em um processo
n No kernel-space, no contexto do processo, executando em nome de um
processo específico

www.it-ebooks.info
6 Capítulo 1 Introdução ao Linux Kernel

Aplicativo 1 Aplicativo 2

espaço do usuário

Interface de chamada do sistema

espaço do 'kernel'

Subsistemas do kernel

Drivers de dispositivo

hardware

Figura 1.1 Relação entre aplicativos, kernel e hardware.

n No espaço do kernel, no contexto de interrupção, não associado a um


processo, tratamento de uma interrupção
Esta lista é inclusiva. Mesmo os casos de canto se encaixam em uma
destas três atividades: Para exam-ple, quando ocioso, acontece que o
kernel está executando um processo ocioso no contexto do processo no
kernel.

Kernels Linux Versus Unix Clássico


Devido a sua ancestralidade comum e a mesma API, os núcleos modernos do Unix
compartilham várias características de design. (Veja a Bibliografia para meus livros
favoritos sobre o design dos núcleos clássicos do Unix.) Com poucas exceções, um
kernel Unix é tipicamente um binário estático monolítico. Isto é, ele existe como uma
única, grande, imagem executável que é executada em um único espaço de endereço.
Sistemas Unix normalmente requerem um sistema com uma unidade de gerenciamento
de memória paginada (MMU); este hardware permite que o sistema imponha a proteção
de memória e forneça um espaço de endereço virtual exclusivo para cada processo.
Historicamente, o Linux exigiu um MMU, mas
www.it-ebooks.info
Kernels Linux Versus Unix Clássico

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.

Designs de kernel monolítico versus microkernel


Podemos dividir os núcleos em duas escolas principais de design: o núcleo
monolítico e o micronúcleo. (Um terceiro campo, exokernel, é encontrado
principalmente em sistemas de pesquisa.)
Os núcleos monolíticos são o design mais simples dos dois, e todos os núcleos foram
projetados desta maneira até a década de 1980. Os kernels monolíticos são
implementados inteiramente como um único processo executado em um único espaço
de endereço. Consequentemente, tais kernels normalmente existem no disco como
binários estáticos sin-gle. Todos os serviços do kernel existem e são executados no
grande espaço de endereço do kernel. A comunicação dentro do kernel é trivial porque
tudo é executado no modo kernel no mesmo espaço de endereço: O kernel pode chamar
funções diretamente, como um aplicativo de espaço de usuário pode. Os proponentes
deste modelo citam a simplicidade e o desempenho da abordagem monolítica. A maioria
dos sistemas Unix tem design monolítico.
Microkernels, por outro lado, não são implementados como um único grande processo.
Em vez disso, a funcionalidade do kernel é dividida em processos separados, geralmente
chamados de servidores. Idealmente, somente os servidores que absolutamente
necessitam desses recursos são executados em um modo exe-cution privilegiado. O
restante dos servidores é executado no espaço do usuário. No entanto, todos os
servidores são classificados em espaços de endereço diferentes. Portanto, a invocação
de função direta como em núcleos monolíticos não é possível. Em vez disso, os
micronúcleos comunicam-se através da passagem de mensagens: Um mecanismo de
comunicação entre processos (IPC) é incorporado no sistema, e os vários servidores
comunicam-se uns com os outros e invocam "serviços" uns dos outros, enviando
mensagens através do mecanismo IPC. A separação dos vários servidores evita que uma
falha em um servidor derrube outro. Da mesma forma, a modularidade do sistema permite
que um servidor seja trocado por outro.
Como o mecanismo IPC envolve um pouco mais de sobrecarga do que uma chamada de
função trivial, e como uma mudança de contexto do espaço do kernel para o espaço do
usuário ou vice-versa é frequentemente envolvida, a passagem de mensagens inclui uma
latência e um acerto de taxa de transferência não vistos em kernels monolíticos com
invocação de função simples. Consequentemente, todos os sistemas práticos baseados
em microkernel agora colocam a maioria ou todos os servidores no espaço do kernel,
para remover a sobrecarga de switches de contexto fre-quent e potencialmente habilitar
a chamada de função direta. O kernel do Windows NT (no qual o Windows XP, Vista e 7
são baseados) e o Mach (no qual parte do Mac OS X é baseado) são exemplos de
microkernels. Nem o Windows NT nem o Mac OS X executam qualquer servidor de
micronúcleo no espaço do usuário em sua última iteração, derrotando o objetivo
principal do design de micronúcleo completamente.
O Linux é um kernel monolítico; isto é, o kernel do Linux é executado em um único espaço de
endereço inteiramente no modo kernel. O Linux, no entanto, empresta grande parte do bom
dos microkernels: o Linux possui um design modular, a capacidade de preempção (chamada
preempção do kernel), suporte para threads do kernel e a capacidade de carregar
dinamicamente binários separados (módulos do kernel) na imagem do kernel. Por outro lado,
o Linux não tem nenhum dos recursos de perda de desempenho que amaldiçoam o design do
microkernel: tudo é executado no modo kernel, com chamada de função direta - não
transmissão de mensagens - o modo de comunicação. No entanto, o Linux é modular,
segmentado e o próprio kernel é programável. O pragmatismo vence novamente.

www.it-ebooks.info
8 Capítulo 1 Introdução ao Linux Kernel

Como o Linus e outros desenvolvedores do kernel contribuem para o kernel


do Linux, eles decidem como avançar o Linux sem negligenciar suas raízes Unix
(e, mais importante, a API Unix). Consequentemente, como o Linux não é
baseado em nenhuma variante específica do Unix, Linus e a empresa podem
escolher a melhor solução para qualquer problema específico — ou, às vezes,
inventar novas soluções! Existem algumas diferenças notáveis entre o kernel do
Linux e os sistemas Unix clássicos:
n O Linux suporta o carregamento dinâmico de módulos do kernel. Embora o kernel
do Linux seja monolítico, ele pode carregar e descarregar dinamicamente o código do kernel sob
demanda.
n O Linux tem suporte para multiprocessador simétrico (SMP).Embora a maioria das
variantes comerciais do Unix agora suportem SMP, a maioria das implementações tradicionais do
Unix não suportam.
n O kernel do Linux é preventivo. Ao contrário das variantes
tradicionais do Unix, o kernel do Linux pode antecipar uma tarefa, mesmo quando
ela é executada no kernel. Das outras implementações comerciais do Unix, Solaris
e IRIX têm kernels preemptivos, mas a maioria dos kernels do Unix não são
preemptivos.
n O Linux adota uma abordagem interessante para o suporte de
processos: ele não diferencia entre processos e processos normais.Para o kernel,
todos os processos são iguais — alguns apenas compartilham recursos.
n O Linux fornece um modelo de dispositivo orientado a objetos com
classes de dispositivos, eventos de conexão automática e um sistema de arquivos
de dispositivo de espaço do usuário (sysfs).
n O Linux ignora alguns recursos comuns do Unix que os
desenvolvedores do kernel consideram mal projetados, como o STREAMS, ou
padrões que são impossíveis de implementar corretamente.
n O Linux é gratuito em todos os sentidos da palavra.O conjunto de recursos que
o Linux implementa é o resultado da liberdade do modelo de desenvolvimento aberto do
Linux. Se um recurso é sem mérito ou mal pensado, os desenvolvedores do Linux não têm
nenhuma obrigação de imple-lo.Ao contrário, o Linux adotou uma atitude elitista em relação
às mudanças: As modificações devem resolver um problema específico do mundo real,
derivar de um design limpo e ter uma implementação sólida. Consequentemente, as
características de algumas outras variantes modernas do Unix que são mais marcadores de
marketing ou pedidos únicos, como a memória paginável do núcleo, não receberam nenhuma
consideração.

Apesar dessas diferenças, no entanto, o Linux continua a ser um


sistema operacional com uma forte herança Unix.

Versões do kernel do Linux


Os kernels Linux vêm em dois sabores: estável e de desenvolvimento. Os kernels estáveis
são versões no nível de produção adequadas para implantação generalizada. Novas versões
estáveis do kernel são lançadas normalmente apenas para fornecer correções de bugs ou
novos drivers. Os kernels de desenvolvimento, por outro lado, sofrem mudanças rápidas
onde (quase) tudo acontece.À medida que os desenvolvedores experimentam novas
soluções, a base de código do kernel muda de maneiras muitas vezes drásticas.

www.it-ebooks.info
Versões do kernel do Linux

Os kernels Linux distinguem entre os kernels stable e development com um esquema de


nomenclatura simples (veja a Figura 1.2). Três ou quatro números, delineados com um
ponto, representam as versões do kernel do Linux. O primeiro valor é a versão principal, o
segundo é a versão secundária e o terceiro é a revisão. Um quarto valor opcional é a versão
estável. A versão secundária também determina se o kernel é estável ou de
desenvolvimento; um número par é estável, enquanto um número ímpar é desenvolvimento.
Por exemplo, a versão do kernel 2.6.30.1 designa um kernel estável. Este kernel tem uma
versão principal de dois, uma versão menor de seis, uma revisão de 30 e uma versão
estável de um. Os dois primeiros valores descrevem a "série ker-nel" — neste caso, a série
2.6 do kernel.

2.6.26

Figura 1.2 Convenção de nomenclatura da versão do kernel.

Os kernels de desenvolvimento têm uma série de fases. Inicialmente, os


desenvolvedores do kernel trabalham em novos recursos e o caos se segue. Com o tempo,
o núcleo amadurece e, eventualmente, um congelamento de recursos é declarado. Nesse
ponto, o Linus não aceitará novos recursos.Trabalhe nos recursos existentes, no entanto,
pode continuar.Depois que o Linus considerar o kernel quase estabilizado, um
congelamento de código é colocado em efeito.Quando isso ocorre, apenas correções de
bugs são aceitas. Pouco tempo depois (espero), Linus lança a primeira versão de uma nova
série estável. Por exemplo, a série de desenvolvimento 1.3 estabilizou em 2.0 e 2.5
estabilizou em 2.6.
Dentro de uma determinada série, Linus lança novos kernels regularmente, com
cada versão ganhando uma nova revisão. Por exemplo, a primeira versão da série
2.6 do kernel foi 2.6.0.A próxima foi 2.6.1.Estas revisões contêm correções de bugs,
novos drivers e novos recursos, mas a diferença entre duas revisões - digamos,
2.6.3 e 2.6.4 - é menor.
Foi assim que o desenvolvimento progrediu até 2004, quando no convite apenas Kernel
Developers Summit, os desenvolvedores de kernel reunidos decidiram prolongar a série 2.6
kernel e adiar a introdução de uma série 2.7 desenvolvimento.A lógica era que o kernel 2.6
foi bem recebido, estável e suficientemente maduro de tal forma que novos recursos de
desestabilização eram desnecessários.Este curso provou sábio, como os anos seguintes
mostraram que 2.6 é uma besta madura e capaz.A partir deste escrito, uma série de
desenvolvimento 2.7 não está na mesa e parece improvável. Em vez disso, o ciclo de
desenvolvimento de cada revisão 2.6 cresceu mais, cada versão incorporando uma
minissérie de desenvolvimento.Andrew Morton, o segundo em comando de Linus,
reaproveitou sua árvore de 2,6 mm — que já foi um campo de testes para mudanças
relacionadas ao gerenciamento de memória — em um ambiente de testes de uso geral.
Desestabilizador
www.it-ebooks.info
10 Capítulo 1 Introdução ao Linux Kernel

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.

Comunidade de desenvolvimento do kernel


Linux
Quando você começa a desenvolver o código para o kernel do Linux, você se torna
parte da comunidade global de desenvolvimento do kernel. O principal fórum para esta
comunidade é a Lista de discussão do kernel do Linux (muitas vezes encurtada para
lkml). As informações de assinatura estão disponíveis em http://vger. kernel.org.
Observe que esta é uma lista de alto tráfego com centenas de mensagens por dia e que
os outros leitores — que incluem todos os principais desenvolvedores do kernel,
incluindo o Linus — não estão abertos para lidar com bobagens.A lista é, no entanto,
uma ajuda inestimável durante o desenvolvimento porque é onde você pode encontrar
testadores, receber revisão por pares e fazer perguntas.
Os capítulos posteriores fornecem uma visão geral do processo de
desenvolvimento do kernel e uma descrição mais completa da participação bem-
sucedida na comunidade de desenvolvimento do kernel. Entretanto, no entanto, à
espreita (lendo silenciosamente) a lista de discussão do Linux Kernel é um
suplemento tão bom para este livro quanto você pode encontrar.

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.

Obtendo a origem do kernel


O código-fonte atual do Linux está sempre disponível em um pacote
completo (um arquivo criado com o comando tar) e em um patch
incremental a partir da casa oficial do kernel do Linux,
http://www.kernel.org.
A menos que você tenha uma razão específica para trabalhar com uma versão
mais antiga do código-fonte Linux, você sempre deseja o código mais recente.O
repositório em kernel.org é o lugar para obtê-lo, juntamente com patches adicionais
de vários desenvolvedores de kernel líderes.

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

Hacking e a Comunidade."Uma discussão completa do Git está fora do


escopo deste livro; muitos recursos on-line fornecem guias excelentes.

Instalando a origem do kernel


O tarball do kernel é distribuído nos formatos GNU zip (gzip) e bzip2. Bzip2 é o formato
padrão e preferido porque geralmente compacta um pouco melhor que gzip. O tarball
do kernel do Linux no formato bzip2 é chamado de linux- . . .tar.bz2, onde é a versão
dessa versão específica do código-fonte do kernel. Após o download da fonte,
descompactando e desfazendo o tarring é simples. Se o seu tarball estiver comprimido
com o bzip2, execute

Se estiver compactado com o GNU zip, execute

Isso descompacta e descompacta a origem para o diretório linux-x.y.z. Se você usa o


git para obter e gerenciar a origem do kernel, você não precisa baixar o tarball. Basta
executar o comando git clone conforme descrito e o git faz download e descompacta a
fonte mais recente.

Onde instalar e hackear na origem


A origem do kernel é normalmente instalada em /usr/src/linux. Você não deve usar esta árvore
de código-fonte para desenvolvimento porque a versão do kernel em relação à qual sua
biblioteca C é compilada é frequentemente vinculada a esta árvore. Além disso, você não
deve exigir root para fazer alterações no kernel — em vez disso, trabalhe fora de seu diretório
home e use root somente para instalar novos ker-nels. Mesmo ao instalar um novo kernel,
/usr/src/linux deve permanecer inalterado.

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

Geralmente, um patch para uma determinada versão do kernel é aplicado em relação à


versão anterior.
A geração e a aplicação de patches são discutidas com muito mais profundidade em
capítulos posteriores.

A árvore de origem do kernel


A árvore de origem do kernel é dividida em vários diretórios, a maioria
dos quais contém muito mais subdiretórios. Os diretórios na raiz da
árvore de origem, juntamente com suas descrições, estão listados na
Tabela 2.1.

www.it-ebooks.info
Construindo o kernel

Quadro 2.1 Diretórios na Raiz da Árvore de Origem do 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

Um número de arquivos na raiz da árvore de origem merece menção.O arquivo


COPYING é a licença do kernel (a GNU GPL v2). CREDITS é uma lista de desenvolvedores
com mais do que uma quantidade trivial de código no kernel. MAINTAINERS lista os nomes
dos indivíduos que mantêm subsistemas e drivers no kernel. Makefile é o Makefile do
kernel base.

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:

Ou um utilitário gráfico baseado em gtk+:

Esses três utilitários dividem as várias opções de configuração em


categorias, como "Tipo e recursos do processador". Você pode percorrer as
categorias, ver as opções do kernel e, é claro, alterar seus valores.
www.it-ebooks.info
Construindo o kernel

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:

Você deve sempre executar isto antes de construir um kernel.


A opção de configuração CONFIG_IKCONFIG_PROC coloca o arquivo completo de
configuração do kernel, compactado, em /proc/config.gz.Isso facilita a clonagem da
configuração atual ao criar um novo kernel. Se o seu kernel atual tem esta opção
habilitada, você pode copiar a configuração de /proc e usá-la para construir um novo
kernel:

Depois que a configuração do kernel estiver definida — no entanto,


você pode fazê-la — você pode construí-la com um comando sin-gle:

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.

Minimizando o ruído da compilação


Um truque para minimizar o ruído de compilação, mas ainda ver avisos e
erros, é redirecionar a saída do make:

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

Gerando Vários Trabalhos de Compilação


O programa make fornece um recurso para dividir o processo de construção em várias
tarefas paralelas. Cada um desses trabalhos é executado separadamente e
simultaneamente, acelerando significativamente o processo de criação em sistemas de
multiprocessamento. Ele também melhora a utilização do processador porque o tempo
para construir uma grande árvore de origem inclui um tempo significativo de espera de
E/S (tempo em que o processo fica ocioso aguardando a conclusão de uma solicitação
de E/S).
Por padrão, make gera apenas um único trabalho porque os Makefiles muitas
vezes têm informações de dependência incorretas.Com dependências incorretas,
vários trabalhos podem pisar nos dedos uns dos outros, resultando em erros no
processo de construção.Os Makefiles do kernel têm informações de dependência
corretas, portanto a geração de vários trabalhos não resulta em falhas.Para
construir o kernel com vários trabalhos make, use

Aqui está o número de empregos a serem criados. A prática comum é


criar um ou dois trabalhos por processador. Por exemplo, em uma máquina
de 16 núcleos, você pode

O uso de utilitários como o excelente distcc ou cache também pode


melhorar drasticamente o tempo de construção do kernel.

Instalando o novo kernel


Após a construção do kernel, é necessário instalá-lo. A forma como ele é
instalado depende da arquitetura e do carregador de inicialização — consulte as
instruções do seu carregador de inicialização sobre onde copiar a imagem do
ker-nel e como configurá-la para inicialização.Sempre mantenha um kernel
seguro ou dois ao redor no caso de seu novo kernel ter problemas!
Por exemplo, em um sistema x86 usando grub, você copiaria arch /i386/boot/bzImage
para /boot, nomeie-o como vmlinuz- e edite /boot/grub/grub.conf,
adicionando uma nova entrada para o novo kernel. Os sistemas que usam o LILO para
inicializar editariam
e depois execute o lilo novamente.
/etc/lilo.conf
A instalação de módulos, felizmente, é automatizada e
independente de arquitetura. Como root, basta executar

Isso instala todos os módulos compilados em seu home correto em /lib/modules.


O processo de construção também cria o arquivo System.map na raiz da árvore de origem
do kernel. Ele contém uma tabela de pesquisa de símbolo, mapeando símbolos de kernel
para seus endereços iniciais. Isso é usado durante a depuração para traduzir endereços de
memória para nomes de função e variáveis.

Uma Besta de Natureza Diferente


O kernel do Linux tem vários atributos exclusivos em comparação com uma aplicação
normal de espaço do usuário.Embora essas diferenças não necessariamente tornam o
desenvolvimento do código do kernel mais difícil do que o desenvolvimento do código
de espaço do usuário, eles certamente fazem isso diferente.

www.it-ebooks.info
Uma Besta de Natureza Diferente

Estas características fazem do miolo uma besta de natureza diferente.


Algumas das regras usuais são curvas; outras são inteiramente novas.
Embora algumas das diferenças sejam óbvias (todos sabemos que o kernel
pode fazer o que quiser), outras não são tão óbvias.As mais importantes
dessas diferenças são
n O kernel não tem acesso à biblioteca C nem aos cabeçalhos C padrão.
n O kernel é codificado no GNU C.
n O kernel não possui a proteção de memória oferecida ao espaço do usuário.
n O kernel não pode executar facilmente operações de ponto flutuante.
n O kernel tem uma pequena pilha de tamanho fixo por processo.
n Como o kernel tem interrupções assíncronas, é preemptivo e suporta SMP, a sincronização
e a simultaneidade são as principais preocupações dentro do kernel.
n A portabilidade é importante.
Vamos analisar brevemente cada um desses problemas porque todos
os desenvolvedores do kernel devem mantê-los em mente.

Sem cabeçalhos libc ou padrão


Ao contrário de um aplicativo de espaço de usuário, o kernel não é vinculado com a
biblioteca C padrão - ou qualquer outra biblioteca, para esse assunto. Há várias razões
para isso, incluindo uma situação de galinha e ovo, mas a principal razão é a velocidade
e tamanho. A biblioteca C completa - ou mesmo um subconjunto decente dela - é muito
grande e muito ineficiente para o kernel.
Não se preocupe: muitas das funções comuns da libc são
implementadas dentro do kernel. Por exemplo, as funções comuns de
manipulação de strings estão em lib/string.c. Basta incluir o arquivo de
cabeçalho <linux/string.h> e tê-los.

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

Uma diferença notável entre printf() e printk() é que printk() permite


especificar um flag de prioridade. Esse flag é usado por printk para decidir
onde exibir mensagens do kernel. Eis um exemplo dessas prioridades:

Observe que não há nenhuma vírgula entre KERN_ERR e a mensagem


impressa. Isso é intencional; o sinalizador de prioridade é definido pelo
pré-processador e representa uma string literal, que é concatenada na
mensagem impressa durante a compilação. Usamos printk() em todo este
livro.

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

Tornar grandes funções em linha, especialmente aquelas usadas


mais de uma vez ou que não são excessivamente críticas em termos
de tempo, é desaprovado.
Uma função embutida é declarada quando as palavras-chave static e
inline são usadas como parte da definição da função. Por exemplo

A declaração de função deve preceder qualquer uso, caso contrário, o compilador


não pode tornar a função embutida. A prática comum é colocar funções embutidas em
arquivos de cabeçalho. Como estão marcados como estáticos, uma função exportada
não é criada. Se uma função embutida é usada por apenas um arquivo, ela pode ser
colocada na parte superior desse arquivo.
No kernel, o uso de funções embutidas é preferível a macros
complicadas por razões de segurança e legibilidade do tipo.

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

O kernel do Linux é escrito em uma mistura de C e assembly, com


assembly relegado a arquitetura de baixo nível e código de caminho
rápido. A grande maioria do código do kernel é pro-grammed em C reto.

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:

Para marcar esta ramificação como muito improvável de tomada (isto é,


provavelmente não tomada):
www.it-ebooks.info
20 Capítulo 2 Introdução ao Kernel

Inversamente, para marcar uma ramificação como muito provavelmente


adotada:

Você só deve usar essas diretivas quando a direção da ramificação é


predominantemente conhecida a priori ou quando você deseja otimizar um caso
específico ao custo do outro caso. Este é um ponto importante:Essas diretivas
resultam em um aumento de desempenho quando a ramificação é marcada
corretamente, mas em uma perda de desempenho quando a ramificação é marcada
incorretamente.Um uso comum, como mostrado nesses exemplos, para improvável()
e provável() é con-dições de erro. Como você pode esperar, expected() encontra muito
mais uso no kernel porque as instruções if tendem a indicar um caso especial.

Sem proteção de memória


Quando um aplicativo de espaço de usuário tenta um acesso ilegal à memória, o kernel
pode interceptar o erro, enviar o sinal SIGSEGV e eliminar o processo. Entretanto, se o
kernel tentar um acesso de memória ilegal, os resultados serão menos controlados.
(Afinal, quem vai cuidar do kernel?) Violações de memória no kernel resultam em um
oops, que é um erro de kernel principal. Deve ser óbvio que você não deve acessar
ilegalmente a memória, como cancelar a referência de um ponteiro NULL — mas dentro
do kernel, os riscos são muito maiores!
Além disso, a memória do kernel não é paginável.Portanto, cada byte de
memória que você consome é um byte a menos de memória física
disponível. Tenha isso em mente na próxima vez que você precisar
adicionar mais um recurso ao kernel!

Sem (fácil) utilização de ponto flutuante


Quando um processo de espaço do usuário usa instruções de ponto flutuante, o
kernel gerencia a transição do modo inteiro para o modo de ponto flutuante. O que
o kernel tem a fazer ao usar instruções de ponto flutuante varia de acordo com a
arquitetura, mas o kernel normalmente captura uma armadilha e, em seguida, inicia
a transição do modo inteiro para o modo de ponto flutuante.
Ao contrário do espaço do usuário, o núcleo não tem o luxo de suporte sem costura para
ponto flutuante, porque não pode facilmente aprisionar-se. Usar um ponto flutuante dentro
do kernel requer salvar e restaurar manualmente os registros de ponto flutuante, entre
outras possíveis tarefas.
A resposta curta é: não faça isso! Exceto em casos raros, nenhuma
operação de ponto flutuante está no kernel.

Pilha pequena de tamanho fixo


O espaço do usuário pode se livrar da alocação estática de muitas variáveis na pilha,
incluindo grandes estruturas e matrizes de mil elementos.Esse comportamento é legal
porque o espaço do usuário tem uma grande pilha que pode crescer dinamicamente. (Os
desenvolvedores de sistemas operacionais mais antigos e menos avançados, digamos,
DOS, podem se lembrar de um momento em que até mesmo o espaço do usuário tinha uma
pilha de tamanho fixo.)

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

projeto ware.O passo mais importante no caminho para o desenvolvimento do Linux é a


realização-realização que o kernel não é algo a temer. Não é familiar, claro.
Insuperável? De jeito nenhum. Este e o capítulo anterior estabelecem a base para os
tópicos que abordamos nos capítulos restantes deste livro. Em cada capítulo
subsequente, abordamos um conceito ou subsistema de kernel específico.Ao longo do
caminho, é imperativo que você leia e modifique o código-fonte do kernel. Somente
através da leitura e experimentação do código você pode entender
it.The fonte está disponível gratuitamente—use-o!

www.it-ebooks.info
3
.
Gerenciamento de
processos

Este capítulo introduz o conceito do processo, uma das abstrações fundamentais em


sistemas operacionais Unix. Ele define o processo, bem como conceitos relacionados,
como threads, e discute como o kernel do Linux gerencia cada processo: como eles são
enumerados dentro do kernel, como eles são criados e como eles acabam morrendo.
Como a execução de aplicativos de usuário é a razão pela qual temos sistemas
operacionais, o gerenciamento de processos é uma parte crucial de qualquer kernel de
sistema operacional, incluindo o Linux.

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

Curiosamente, observe que os threads compartilham a abstração


de memória virtual, enquanto cada um recebe seu próprio
processador virtualizado.
Um programa em si não é um processo; um processo é um programa
ativo e recursos relacionados. Na verdade, podem existir dois ou mais
processos que estão executando o mesmo programa. Na verdade,
podem existir dois ou mais processos que compartilham vários
recursos, como arquivos abertos ou um espaço de endereço.
Um processo começa sua vida quando, não surpreendentemente, é criado. No Linux,
isso ocorre por meio da chamada do sistema fork(), que cria um novo processo
duplicando um já existente. O processo que chama fork() é o pai, enquanto o novo
processo é o filho. O pai retoma a execução e o filho inicia a execução no mesmo local:
onde a chamada para fork() retorna. A chamada do sistema retorna do kernel duas vezes:
uma vez no processo pai e novamente no recém-nascido.
Muitas vezes, imediatamente após uma bifurcação é desejável executar um
novo programa diferente.A família exec() de chamadas de função cria um novo
espaço de endereço e carrega um novo programa nele. Nos kernels Linux
contemporâneos, fork() é realmente implementado através da chamada de sistema clone(),
que é discutida em uma seção seguinte.
Finalmente, um programa é encerrado por meio da chamada do sistema exit() .Essa
função encerra o processo e libera todos os seus recursos.Um processo pai pode
consultar o status de um filho encerrado por meio da chamada do sistema wait4()1?, que permite que um
processo aguarde o término de um processo específico.Quando um processo é encerrado, ele é colocado em um estado zumbi especial que reenvia processos
terminados até o pai chamar wait() ou1 waitpid()s.

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.

Descritor do Processo e a Estrutura da Tarefa


O kernel armazena a lista de processos em uma lista circular duplamente
vinculada chamada lista de tarefas.2 Cada elemento na lista de tarefas é
um descritor de processo do tipo struct task_struct , que é definido em
<linux/sched.h>.O descritor de processo contém todas as informações
sobre um processo específico.
A task_struct é uma estrutura de dados relativamente grande, com cerca de 1,7 kilobytes em
uma máquina de 32 bits.Esse tamanho, no entanto, é bastante pequeno considerando que a
estrutura contém todas as informações que o kernel tem e precisa sobre um processo.O
descritor do processo contém

1 O kernel implementa a chamada do sistema. Os sistemas Linux, por meio da biblioteca C,


normalmente fornecem as funções , , e . Todas essas funções retornam status sobre um processo
terminado, embora com semânticas ligeiramente diferentes.
2 Alguns textos sobre o design do sistema operacional chamam isso de lista de tarefas. Como a
implementação do Linux é uma lista vinculada e não uma matriz estática, no Linux ela é chamada
de lista de tarefas.
www.it-ebooks.info
Descritor do Processo e a Estrutura da Tarefa

os dados que descrevem o programa em execução — arquivos abertos, o espaço


de endereço do processo, sinais pendentes, o estado do processo e muito mais
(veja a Figura 3.1).

struct_estrutura

struct_estrutura

struct_estrutura

struct_estrutura

Estado longo não assinado;


int prio;
política de longo prazo não assinada;
struct_struct *parent;
struct tarefas list_head;
descritor do processo pid_ t pid;
..

A estrutura_tarefa
a lista de tarefas

Figura 3.1 O descritor do processo e a lista de tarefas.

Alocando o descritor do processo


a estrutura é alocada por meio do alocador slab para fornecer reutilização de objetos e coloração de cache (consulte o Capítulo 12). Antes da série 2.6 do kernel, struct task_struct era armazenada no final da
pilha do kernel de cada processo.Isso permitia que arquiteturas com poucos registradores, como x86, calculassem a localização do descritor do processo através do ponteiro da pilha sem usar um registrador extra para armazenar a localização.Com o descritor do
processo agora criado dinamicamente através do alocador de slab, uma nova estrutura, struct thread_info, foi criada que vive novamente na parte inferior da pilha (para pilhas que crescem abaixo) e na parte superior da pilha (para pilhas que crescem acima).3 Ver
Figura 3.2.

informação_do <asm/thread_info.h>
_thread

O estrutura é definida em x86 in como


3 Arquiteturas deficientes de registro não foram o único motivo para criar . A nova estrutura também
torna bastante fácil calcular deslocamentos de seus valores para uso no código de montagem.

www.it-ebooks.info
26 Capítulo 3 Gerenciamento de processos

Processar Pilha de Kernel


- endereço de memória mais alto
Início da Pilha

- ponteiro de pilha

struct_estrutura_thread
- menor endereço de memória

a struct task_struct do processo

Figura 3.2 O descritor do processo e a pilha do kernel.

A estrutura thread_info de cada tarefa é alocada no final de sua pilha.O


elemento task da estrutura é um ponteiro para a estrutura atual
task_struct da tarefa.

Armazenando o descritor do processo


O sistema identifica processos por um valor de identificação de processo exclusivo
ou PID. O PID é um valor numérico representado pelo tipo opaco 4 pid_t, que é
normalmente um int. Devido à compatibilidade com versões anteriores do Unix e do
Linux, no entanto, o valor máximo padrão é apenas 32.768 (o de um int curto), embora
o valor opcionalmente possa ser aumentado até quatro milhões (isso é controlado
em <linux/threads.h>.O kernel armazena este valor como pid dentro de cada descritor de
processo.
Esse valor máximo é importante porque é essencialmente o número máximo de
processos que podem existir simultaneamente no sistema.Embora 32.768 possam ser
suficientes para um sistema de desktop, servidores grandes podem exigir muito mais
processos. Além disso, quanto mais baixo o valor, mais cedo os valores irão se envolver,
destruindo a noção útil de que maior

4 Um tipo opaco é um tipo de dados cuja representação física é desconhecida ou irrelevante.


www.it-ebooks.info
Descritor do Processo e a Estrutura da Tarefa

valores indicam processos de execução posterior aos valores inferiores. Se o sistema


estiver disposto a romper com a compatibilidade com aplicativos antigos, o administrador
poderá aumentar o valor máximo por meio de
/proc/sys/kernel/pid_max.
Dentro do kernel, as tarefas são normalmente referenciadas diretamente por um
ponteiro para suas
estrutura task_struct Na verdade, a maioria do código do kernel que lida com
processos funciona diretamente
com struct task_struct. Consequentemente, é útil poder consultar rapidamente o
descritor de processo da tarefa em execução no momento, que é feito por meio da
macro atual.
Essa macro deve ser implementada independentemente por cada arquitetura. Algumas
arquiteturas
salve um ponteiro para a estrutura task_struct do processo em execução no momento
em um registrador,
possibilitando um acesso eficiente. Outras arquiteturas, como x86 (que tem poucos
registros para
lixo), use o fato de que struct thread_info é armazenado na pilha do kernel para cal-
cule o local de thread_info e subsequentemente de task_struct.
Em x86, current é calculado mascarando os 13 bits menos significativos
do ponteiro da pilha para obter a estrutura thread_info. Isto é feito pela função
current_thread_info(). A montagem é mostrada aqui:

Isso pressupõe que o tamanho da pilha é de 8 KB.Quando pilhas de 4


KB estão habilitadas, 4096 é usado no lugar de 8192.
Finalmente, o membro task dedesreferenciamento atual de
estrutura_tarefa:

Compare essa abordagem com a adotada pelo PowerPC (o moderno


microprocessador baseado em RISC da IBM), que armazena a atual task_struct
em um registrador.Assim, current no PPC meramente retorna o valor
armazenado no registrador. A PPC pode adotar essa abordagem porque, ao
contrário da x86, ela tem muitos registros. Como o acesso ao descritor de
processo é um trabalho comum e importante, os desenvolvedores do kernel
PPC consideram usar um registro digno para a tarefa.

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

O agendador envia a tarefa a ser


executada:
schedule() chama context_switch().
Chamadas de tarefa existentes A tarefa foi encerrada.
fork() e cria
um novo processo.

Saída da tarefa via


do_sair.

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 foi substituída


por tarefa de prioridade mais
alta.

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.

Figura 3.3 Fluxograma de estados de processo.

n TASK_UNINTERRUPTIBLE—Este estado é idêntico a TASK_INTERRUPTIBLE exceto


que ele não acorda e se torna executável se receber um sinal.Isso é usado em
situações em que o processo deve aguardar sem interrupção ou quando o evento
deve ocorrer muito rapidamente. Como a tarefa não responde a sinais nesse
estado, TASK_UNINTERRUPTIBLE é usado com menos frequência do que
TASK_INTERRUPTIBLE.5

n — O processo está sendo rastreado por outro processo,


__TASK_TRACED
como um depurador, via ptrace.
n __TASK_STOPPED—A execução do processo foi interrompida; a tarefa não está em
execução nem está qualificada para ser executada.Isso ocorre se a tarefa receber o sinal SIGSTOP,
SIGTSTP, ou SIGTTIN, ou se receber qualquer sinal enquanto está sendo depurada.

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

Manipulando o Estado do Processo Atual


O código do kernel geralmente precisa alterar o estado de um processo.O mecanismo
preferido é usar

Esta função define a tarefa fornecida para o estado fornecido. Se


aplicável, ele também fornece uma barreira de memória para forçar o
pedido em outros processadores. (Isso só é necessário em sistemas
SMP.) Caso contrário, equivale a

O método set_current_state(state) é sinônimo de set_task_state(current,


Estado-Membro). Consulte <linux/sched.h> para obter informações sobre a implementação
dessas funções e das relacionadas.

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.

A árvore genealógica do processo


Existe uma hierarquia distinta entre os processos em sistemas Unix, e o Linux não é
exceção. Todos os processos são descendentes do processo init, cujo PID é one.O
kernel inicia o init na última etapa do processo de inicialização.O processo init, por sua vez,
lê o sistema e executa mais programas, eventualmente concluindo o processo de
inicialização.
Cada processo no sistema tem exatamente um pai. Da mesma forma, todo processo tem
zero ou mais filhos. Os processos que são todos filhos diretos do mesmo pai são chamados
de irmãos. O relacionamento entre processos é armazenado no descritor do processo. Cada
task_struct tem um ponteiro para a task_struct do pai, denominada task_struct, e uma lista de filhos,

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:

Da mesma forma, é possível iterar sobre os filhos de um processo com

O descritor do processo da tarefa init é estaticamente alocado como init_task. Um bom


exemplo da relação entre todos os processos é o fato de que esse código sempre terá
êxito:

Na verdade, você pode seguir a hierarquia do processo de qualquer processo


no sistema para qualquer outro. Muitas vezes, no entanto, é desejável
simplesmente iterar todos os processos no sistema. Isso é fácil porque a lista
de tarefas é circular e duplamente vinculada.Para obter a próxima tarefa na lista,
atribua qualquer tarefa válida, use

Obter a tarefa anterior funciona da mesma maneira:

Essas duas rotinas são fornecidas pelas macros next_task(task) e


prev_task(task), respectivamente. Finalmente, a macro for_each_process(task) é
fornecida,
que repete toda a lista de tarefas. Em cada iteração, a tarefa aponta para a próxima tarefa
em
a lista:

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.

2. Em seguida, ele verifica se o novo filho não excederá os limites de


recursos no número de processos para o usuário atual.
3. O filho precisa se diferenciar de seu pai.Vários membros do descritor de
processo são apagados ou definidos como valores iniciais. Os membros do descritor
de processo não herdados são principalmente informações estatísticas.A maior parte
dos valores em task_struct permanecem inalterados.
4. O estado do filho é definido como TASK_UNINTERRUPTIBLE para garantir que ele ainda
não seja executado.

5. copy_process() chama copy_flags() para atualizar os sinalizadores do


task_struct.O sinalizador PF_SUPERPRIV, que indica se uma tarefa usou
privilégios de superusuário, está desmarcado.O sinalizador
PF_FORKNOEXECy, que indica um processo que não chamou exec(), está definido.

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. Finalmente, copy_process() limpa e retorna ao chamador um ponteiro


para o novo filho.
De volta em do_fork(), se copy_process() retorna com sucesso, o novo filho é acordado e
executado. Deliberadamente, o kernel executa o processo filho primeiro. 8 No caso
comum do filho simplesmente chamando exec() imediatamente, isso elimina qualquer
sobrecarga de copy-on-write que ocorreria se o pai fosse executado primeiro e
começasse a gravar no espaço de endereço.

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.

Se tudo isso for como planejado, a criança agora estará sendo


executada em um novo espaço de endereço e o pai estará sendo
executado novamente em seu espaço de endereço original.A
sobrecarga é menor, mas a implementação não é muito bonita.

A implementação Linux de threads


Threads são uma abstração de programação moderna popular.Eles fornecem
vários threads de execução dentro do mesmo programa em um espaço de
endereço de memória compartilhada.Eles também podem compartilhar arquivos
abertos e outros recursos.Threads permitem programação simultânea e, em
sistemas de processador múltiplo, verdadeiro paralelismo.
O Linux tem uma implementação exclusiva de threads. Para o kernel do Linux, não há
conceito de um thread. O Linux implementa todos os threads como processos padrão. O
kernel do Linux

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

não fornece semântica de agendamento especial ou estruturas de dados para representar


threads. Em vez disso, um segmento é meramente um processo que compartilha
determinados recursos com outros processos. Cada thread tem uma task_struct exclusiva e
aparece para o kernel como um processo normal — os threads simplesmente
compartilham recursos, como um espaço de endereço, com outros processos.
Essa abordagem de threads contrasta muito com sistemas operacionais como
Microsoft Windows ou Sun Solaris, que têm suporte explícito de kernel para threads (e
às vezes chamam threads de processos leves).O nome "processo leve" resume a
diferença em filosofias entre Linux e outros sistemas.Para esses outros sistemas
operacionais, os threads são uma abstração para fornecer uma unidade de execução
mais leve e mais rápida do que o processo pesado.Para Linux, os threads são
simplesmente uma maneira de compartilhar recursos entre processos (que já são
bastante leves).10 Por exemplo, suponha que você tenha um processo que consiste em
quatro threads. Em sistemas com suporte a threads explícitos, pode existir um
descritor de processo que, por sua vez, aponta para os quatro threads diferentes.O
descritor de processo descreve os recursos compartilhados, como um espaço de
endereço ou arquivos abertos.Os threads descrevem os recursos que possuem
sozinhos. Por outro lado, no Linux, há simplesmente quatro processos e, portanto,
quatro estruturas normais de task_struct.Os quatro processos são configurados para
compartilhar determinados recursos. O resultado é bastante elegante.

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:

O código anterior resulta em um comportamento idêntico a um fork() normal, exceto


que o espaço de endereço, os recursos do sistema de arquivos, os descritores de
arquivos e os manipuladores de sinais são compartilhados. Em outras palavras, a nova
tarefa e seu pai são os que são popularmente chamados de threads.
Em contraste, uma fork() normal pode ser implementada como

And vfork() é implementado como

Os sinalizadores fornecidos para clone() ajudam a especificar o


comportamento do novo processo e detalham quais recursos o pai e o
filho compartilharão.A Tabela 3.1 lista os sinalizadores de clone, que são
definidos em <linux/sched.h>, e seus efeitos.
10 Por exemplo, compare o tempo de criação do processo no Linux com o tempo de criação do
processo (ou mesmo do thread!) nesses outros sistemas operacionais. Os resultados são favoráveis para
Linux.

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

kthreadd kernel process.The interface, declarada em <linux/kthread.h>,


para gerar um novo thread do kernel a partir de um existente é

A nova tarefa é criada através da chamada do sistema clone() pelo processo do


kernel do kthread. O novo processo executará a função threadfn, que recebe os dados
argumento.O processo será nomeado nome efmt, que recebe os argumentos de formatação do estilo printf na lista
de argumentos variável.O processo é criado em um estado inexecutável; ele não iniciará a execução até que seja
explicitamente acordado via wake_up_process(). Um processo pode ser criado e tornado
executável com uma única função,
kthread_ run():

Esta rotina, implementada como uma macro, simplesmente chama tanto


kthread_create() quanto
wake_up_process():

Quando iniciado, um thread do kernel continua a existir até que ele


chama do_exit() ou outra parte do kernel chama kthread_stop(), passando o
endereço da tarefa_struct_struct retornada por kthread_create():

Discutimos segmentos específicos do kernel com mais detalhes nos


capítulos posteriores.

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

processo recebe um sinal ou uma exceção que não pode manipular ou


ignorar. Independentemente de como um processo termina, a maior
parte do trabalho é manipulada por do_exit(), definida em kernel/exit.c, que
completa uma série de tarefas:
1. Define o indicador PF_EXITING no membro flags do parâmetrotask_struct.
2. Ele chama del_timer_sync() para remover qualquer temporizador do kernel. Ao retornar,
é garantido que nenhum temporizador esteja enfileirado e que nenhum manipulador de
temporizador esteja em execução.
3. Se a contabilidade do processo BSD está habilitada, do_exit() chama
acct_update_integrals() para escrever informações de contabilidade.

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.

Nesse ponto, todos os objetos associados à tarefa (assumindo que a tarefa


era o único usuário) são liberados.A tarefa não é executável (e não tem mais um
espaço de endereço no qual executar) e está no estado de saída EXIT_ZOMBIE.A
única memória que ela ocupa é sua pilha do kernel, a estrutura thread_info e a estrutura
thread_info.A tarefa existe apenas para fornecer informações ao seu pai.Depois que o
pai recuperar as informações ou notificar o kernel de que não está interessado, a
memória restante mantida pelo processo é liberada e retornados ao sistema para
uso.

Removendo o Descritor de Processo


Após do_exit() terminar, o descritor do processo finalizado ainda existe, mas o processo é um
zumbi e não pode ser executado. Conforme discutido, isso permite que o sistema obtenha
informações sobre um processo filho após seu encerramento. Por conseguinte, os atos
www.it-ebooks.info
38 Capítulo 3 Gerenciamento de processos

a limpeza após um processo e a remoção de seu descritor de processo são


separadas.Depois que o pai obtém informações sobre seu filho finalizado ou
indica ao kernel que não se importa, a task_struct do filho é desalocada.
A família wait() de funções é implementada por meio de uma única (e
complicada) chamada do sistema, wait4().O comportamento padrão é suspender a
execução da tarefa de chamada até que um de seus filhos saia, momento em
que a função retorna com o PID do filho saído. Além disso, um ponteiro é
fornecido para a função que, no retorno, mantém o código de saída do filho
finalizado.
Quando é hora de finalmente desalocar o descritor do processo,
release_task() é chamada. Ele faz o seguinte:

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.

Neste ponto, o descritor do processo e todos os recursos que


pertencem exclusivamente ao processo foram liberados.

O dilema da tarefa sem pais


Se um pai sair antes de seus filhos, algum mecanismo deverá existir para atribuir outro pai
a qualquer tarefa-filho em um novo processo, caso contrário, os processos terminados sem
pais permanecerão sempre como zumbis, desperdiçando memória do sistema.A solução é
atribuir outro pai aos filhos de uma tarefa ao sair de outro processo no grupo de threads
atual ou, se isso falhar, do processo de inicialização. do_exit()
calls exit_notify(), que chama Forget_original_parent(), que, por sua vez, chama
find_new_reaper() para executar a criação de novos pais:

www.it-ebooks.info
Término do Processo

Esse código tenta localizar e retornar outra tarefa no grupo de


threads do processo. Se outra tarefa não estiver no grupo de threads,
ela localizará e retornará o processo de inicialização. Agora que um novo
pai adequado para as crianças é encontrado, cada criança precisa ser
localizado e reparado para ceifa:

ptrace_exit_finish() é então chamado para fazer a mesma nova


atribuição de nova atribuição de nova atribuição, mas para uma lista de
filhos rastreados:
www.it-ebooks.info
40 Capítulo 3 Gerenciamento de processos

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

O capítulo anterior discutia os processos, a abstração do sistema operacional


do código do programa ativo. Este capítulo discute o agendador de processos,
o subsistema do kernel que coloca esses processos em funcionamento.
O agendador de processos decide qual processo é executado, quando e por quanto
tempo.O agendador de processos (ou simplesmente o agendador, para o qual é
geralmente encurtado) divide o recurso finito de tempo do processador entre os
processos executáveis em um sistema.O agendador é a base de um sistema
operacional multitarefa, como o Linux. Ao decidir qual processo é executado em
seguida, o programador é responsável pela melhor utilização do sistema e dá aos
usuários a impressão de que vários processos estão sendo executados
simultaneamente.
A ideia por trás do agendador é simples.Para melhor utilizar o tempo do processador,
supondo que existam processos executáveis, um processo deve estar sempre em
execução. Se houver mais processos executáveis do que processadores em um sistema,
alguns processos não serão executados em um determinado momento. Esses processos
estão aguardando para serem executados. Decidir qual processo é executado em seguida,
dado um conjunto de processos executáveis, é a decisão fundamental que o programador
deve tomar.

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

a suspensão involuntária de um processo em execução é chamada de preempção.O


tempo em que um processo é executado antes de ser preemptado geralmente é
predeterminado e é chamado de divisão de tempo do processo. A divisão de tempo, na
verdade, dá a cada processo executável uma fatia do tempo do processador. Gerenciar
a divisão de tempo permite que o agendador tome decisões de agendamento global
para o sistema. Ele também impede que qualquer processo monopolize o processador.
Em muitos sistemas operacionais modernos, a divisão de tempo é calculada
dinamicamente como uma função do comportamento do processo e da política de
sistema configurável. Como veremos, o programador "justo" exclusivo do Linux não
emprega intervalos de tempo per se, com um efeito interessante.
Por outro lado, na multitarefa cooperativa, um processo não para de rodar até que ele decida
voluntariamente fazê-lo.O ato de um processo se autosuspender voluntariamente é chamado de
cedência. Idealmente, os processos produzem frequentemente, dando a cada processo
executável uma parte decente do processador, mas o sistema operacional não pode impor
isso.As deficiências dessa abordagem são manifestas: o agendador não pode tomar decisões
globais em relação a quanto tempo os processos são executados; os processos podem
monopolizar o processador por mais tempo do que o usuário deseja; e um processo suspenso
que nunca produz pode potencialmente derrubar todo o sistema.Felizmente, a maioria dos
sistemas operacionais projetados nas últimas duas décadas empregam multitarefas preventivas,
com o Mac OS 9 (e anterior) e o Windows 3.1 (e anterior) sendo as exceções mais notáveis (e
embaraçosas). É claro, o Unix tem suportado multitarefa preemptiva desde o seu início.

Programador de processos do Linux


Desde a primeira versão do Linux em 1991 até a série 2.4 do kernel, o agendador
do Linux era simples, quase pedestre, em design. Era fácil de entender, mas mal
dimensionado à luz de muitos processos executáveis ou muitos processadores.
Em resposta, durante a série de desenvolvimento do kernel 2.5, o kernel do Linux
recebeu um
revisão do agendador. Um novo programador, comumente chamado de
programador por causa de seu comportamento algorítmico,1 resolveu as
deficiências do programador Linux anterior e introduziu novos recursos poderosos
e características de desempenho. Introduzindo um algoritmo de tempo constante
para cálculo de timeslice e runqueues por processador, ele corrigiu as limitações de
design do agendador anterior.
O O(1) agendador realizou admiravelmente e escalou sem esforço como Linux suportou
grande "ferro" com dezenas, se não centenas de processadores. Com o tempo, entretanto,
tornou-se evidente que o agendador O(1) tinha várias falhas patológicas relacionadas à
programação de aplicativos sensíveis à latência.Esses aplicativos, chamados de processos
interativos, incluem qualquer aplicativo com o qual o usuário interage.Assim, embora o
agendador O(1) fosse ideal para grandes cargas de trabalho de servidor—que não têm
processos interativos—ele se apresentava abaixo do normal em sistemas de desktop, onde
aplicativos interativos são a razão de ser. Começando no início do

1 é um exemplo de notação big-o. Em resumo, significa que o programador pode realizar


seu trabalho em tempo constante, independentemente do tamanho de qualquer entrada. Uma
explicação completa da notação big-o está no Capítulo 6, "Estruturas de dados do kernel".
www.it-ebooks.info
Política

43º

Série 2.6 do kernel, os desenvolvedores introduziram novos programadores de


processo com o objetivo de melhorar o desempenho interativo do programador
O(1). O mais notável deles foi o programador Rotating Staircase Deadline, que
introduziu o conceito de programação justa, emprestado da teoria de filas, para
o programador de processo do Linux.Este conceito foi a inspiração para o
programador O(1) eventual substituição do programador na versão 2.6.23 do kernel,
o Completely Fair Scheduler, ou CFS.
Este capítulo discute os fundamentos do design do agendador e como eles
se aplicam ao Completely Fair Scheduler e seus objetivos, design,
implementação, algoritmos e chamadas de sistema relacionadas.Nós também
discutimos o O(1) agendador porque sua implementação é um modelo de
agendador de processo Unix mais "clássico".

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.

Processos De E/S Associados Versus Processos Associados Ao


Processador
Os processos podem ser classificados como vinculados a I/O ou vinculados a
processadores.O primeiro é caracterizado como um processo que gasta grande
parte do seu tempo submetendo e aguardando solicitações de I/O.
Consequentemente, tal processo é executável por apenas pequenas durações,
porque acaba bloqueando a espera em mais I/O. (Aqui, por I/O, queremos dizer
qualquer tipo de recurso bloqueável, como entrada de teclado ou E/S de rede, e não
apenas E/S de disco.) A maioria dos aplicativos GUI (Graphical User Interface,
interface gráfica do usuário), por exemplo, é vinculada à E/S, mesmo que nunca leia
ou grave no disco, porque passa a maior parte do tempo aguardando a interação do
usuário através do teclado e do mouse.
Por outro lado, os processos vinculados a processadores gastam boa parte do
tempo executando código.Eles tendem a ser executados até que sejam superados,
pois não bloqueiam solicitações de E/S com muita frequência. Como não são
orientados por I/O, no entanto, a resposta do sistema não determina que o
agendador os execute com frequência.Uma política do agendador para processos
vinculados ao processador, portanto, tende a executar esses processos com
menos frequência, mas por períodos mais longos.O exemplo final de um processo
vinculado ao processador é a execução de um loop infinito. Exemplos mais
palatáveis incluem programas que executam muitos cálculos matemáticos, como
ssh-keygen ou MATLAB.
É claro que estas classificações não se excluem mutuamente. Os processos
podem apresentar ambos os comportamentos simultaneamente:O servidor X
Window, por exemplo, é intenso em processador e E/S. Outros processos
podem ser ligados à E/S, mas mergulham em períodos de intensa ação do
processador.Um bom exemplo disso é um processador de texto, que
normalmente fica esperando pressionamentos de teclas, mas a qualquer
momento pode colocar o processador em um encaixe rígido de verificação
ortográfica ou cálculo de macro.
A política de agendamento em um sistema deve tentar satisfazer duas metas conflitantes:
tempo de resposta rápido do processo (baixa latência) e utilização máxima do sistema (alta
taxa de transferência).Para

www.it-ebooks.info
44 Capítulo 4 Programação do processo

Para satisfazer esses requisitos em desacordo, os programadores muitas vezes


empregam algoritmos complexos para deter-minar o processo mais valioso a ser
executado, sem comprometer a justiça a outros processos de prioridade mais
baixa.A política do programador em sistemas Unix tende a favorecer explicitamente
processos vinculados a E/S, fornecendo assim um bom tempo de resposta do
processo. O Linux, com o objetivo de oferecer uma boa resposta interativa e
desempenho de desktop, otimiza a resposta do processo (baixa latência),
favorecendo os processos vinculados à E/S em relação aos processadores
vinculados ao processador.Como veremos, isso é feito de uma maneira criativa que
não negligencia os processos vinculados ao processador.

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.

A política de agendamento em ação


Considere um sistema com duas tarefas executáveis: um editor de texto e um codificador
de vídeo.O editor de texto é vinculado à E/S porque passa quase todo o tempo aguardando
pressionamentos de teclas do usuário. (Não importa quão rápido o usuário digita, não é tão
rápido.) Apesar disso, quando o editor de texto recebe um pressionamento de tecla, o
usuário espera que o editor responda imediatamente. Por outro lado, o codificador de vídeo
é vinculado ao processador.Além de ler o fluxo de dados brutos do disco
2 Timeslice às vezes é chamada de quantum ou processador slice em outros sistemas. O Linux o
chama de timeslice, assim como você deveria.

www.it-ebooks.info
46 Capítulo 4 Programação do processo

e depois de gravar o vídeo resultante, o codificador gasta todo o tempo


aplicando o codec de vídeo aos dados brutos, consumindo facilmente 100% do
processador.O codificador de vídeo não tem nenhuma restrição de tempo forte
sobre quando ele é executado—se ele começou a funcionar agora ou em meio
segundo, o usuário não poderia dizer e não se importaria. É claro, quanto mais
cedo terminar melhor, mas a latência não é uma preocupação primordial.
Neste cenário, idealmente o agendador dá ao editor de texto uma proporção maior
do processador disponível do que o codificador de vídeo, porque o editor de texto é
interativo.Temos dois objetivos para o editor de texto. Primeiro, queremos que ele
tenha uma grande quantidade de tempo do processador disponível; não porque ele
precise de muito processador (não precisa), mas porque queremos que ele sempre
tenha o tempo do processador disponível no momento em que ele precisar. Segundo,
queremos que o editor de texto antecipe o codificador de vídeo no momento em que ele
acorda (digamos, quando o usuário pressiona uma tecla).Isso pode garantir que o
editor de texto tenha bom desempenho interativo e seja responsivo à entrada do
usuário. Na maioria dos sistemas operacionais, essas metas são alcançadas (se é que
são alcançadas) dando ao editor de texto uma prioridade mais alta e maior timeslice do
que o codificador de vídeo.Os sistemas operacionais avançados fazem isso
automaticamente, detectando que o editor de texto é interativo. O Linux também
alcança esses objetivos, mas por meios diferentes. Em vez de atribuir ao editor de texto
uma prioridade e uma divisão de tempo específicas, ele garante ao editor de texto uma
proporção específica do processador. Se o codificador de vídeo e o editor de texto
forem os únicos processos em execução e ambos estiverem no mesmo nível
adequado, essa proporção seria de 50% — cada um teria a garantia de metade do
tempo do processador. Como o editor de texto passa a maior parte do tempo
bloqueado, aguardando pressionamentos de teclas do usuário, ele não usa perto de
50% do processador. Por outro lado, o codificador de vídeo é livre para usar mais do
que seus 50% alocados, permitindo-lhe terminar a codificação rapidamente.
O conceito crucial é o que acontece quando o editor de texto é ativado. Nosso objetivo
principal é garantir que ele seja executado imediatamente após a entrada do usuário. Nesse
caso, quando o editor é ativado, o CFS observa que é alocado 50% do processador, mas tem
usado consideravelmente menos. Especificamente, o CFS determina que o editor de texto
foi executado por menos tempo do que o codificador de vídeo. Tentando dar a todos os
processos uma parte justa do processador, ele então antecipa o codificador de vídeo e
permite que o editor de texto seja executado.O editor de texto é executado, processa
rapidamente o pressionamento de tecla do usuário e, novamente, dorme, aguardando mais
entrada.Como o editor de texto não consumiu seus 50% alocados, continuamos dessa
maneira, com o CFS sempre permitindo que o editor de texto seja executado quando desejar
e o codificador de vídeo seja executado o resto do tempo.

O algoritmo de agendamento do Linux


Nas seções anteriores, discutimos a programação de processos em abstrato, com
apenas uma menção ocasional de como o Linux aplica um determinado conceito à
realidade.Com a base da programação agora criada, podemos mergulhar no
programador de processos do Linux.

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

a classe tem uma prioridade.O código do agendador base, que é definido em


kernel/sched.c, itera sobre cada classe do agendador em ordem de prioridade.A
classe do agendador de prioridade mais alta que tem um processo
executável ganha, selecionando quem executa a próxima.
Completely Fair Scheduler (CFS) é a classe de agendador registrada para
processos normais, chamada SCHED_NORMAL no Linux (e SCHED_OTHER no
POSIX). O CFS é definido em kernel/sched_fair.c.O restante desta seção discute o
algoritmo do CFS e é mais comum a qualquer kernel do Linux desde a
versão 2.6.23.Discutimos a classe do agendador para processos em tempo
real em uma seção posterior.

Programação de processos em sistemas Unix


Para discutir o agendamento justo, primeiro devemos descrever como os sistemas
Unix tradicionais programam processos.Como mencionado na seção anterior, os
agendadores de processo modernos têm dois conceitos comuns: prioridade de
processo e timeslice.Timeslice é quanto tempo um processo é executado; processos
começam com algum timeslice padrão. Processos com prioridade mais alta são
executados com mais frequência e (em muitos sistemas) recebem uma divisão de
tempo mais alta. No Unix, a prioridade é exportada para o espaço do usuário na forma
de bons valores. Isso parece simples, mas na prática leva a vários problemas
patológicos, que agora discutimos.
Primeiro, o mapeamento de valores corretos em fatias de tempo requer uma decisão
sobre qual fatia de tempo absoluta deve alocar cada valor ideal. Isso leva a um
comportamento de alternância subideal. Para exam-ple, vamos supor que atribuamos
processos do valor de nice padrão (zero) a uma divisão de tempo de 100 mil-liseconds e
processos no valor de nice mais alto (+20, a prioridade mais baixa) a uma divisão de tempo
de 5 milissegundos. Além disso, vamos supor que um desses processos seja executável.
Nosso processo de prioridade padrão recebe, portanto, 20/21 (100 de 105 milissegundos) do
processador, enquanto nosso processo de baixa prioridade recebe 1/21 (5 de 105
milissegundos) do processador.Poderíamos ter usado qualquer número para este exemplo,
mas assumimos que essa alocação é ideal, pois o escolhemos. Agora, o que acontece se
executarmos exatamente dois processos de baixa prioridade? Esperamos que cada um
receba 50% do processador, o que eles fazem. Mas cada um deles aproveita o processador
por apenas 5 milissegundos de cada vez (5 em 10 milissegundos cada)! Ou seja, em vez de
alternar o contexto duas vezes a cada 105 milissegundos, agora alternamos o contexto duas
vezes a cada
10 milissegundos. Por outro lado, se tivermos dois processos de prioridade normal,
cada um receberá novamente os 50% corretos do processador, mas em incrementos de
100 milissegundos. Nenhuma dessas alocações de timeslice são necessariamente
ideais; cada uma é simplesmente um subproduto de um determinado valor para o
mapeamento de timeslice combinado com uma combinação específica de prioridades
de processo executáveis. De fato, dado que processos de alto valor agradável (baixa
prioridade) tendem a ser em segundo plano, tarefas de processador intensivo,
enquanto processos de prioridade normal tendem a ser tarefas de usuário em primeiro
plano, esta alocação de timeslice é exatamente para trás do ideal!
Um segundo problema diz respeito aos valores de nice relativos e novamente ao valor
de nice para o mapeamento de timeslice. Digamos que tenhamos dois processos, cada um
com um valor distinto. Primeiro, vamos supor que eles estejam em valores agradáveis 0 e 1.
Isso pode mapear (e de fato fez no agendador O(1))) para fatias de tempo de 100 e 95
milissegundos, respectivamente. Esses dois valores são quase idênticos e, portanto, a
diferença aqui entre um único valor bonito é pequena. Agora, em vez disso, vamos supor
que nossos dois processos estão com bons valores de 18 e 19. Isso agora mapeia para
períodos de tempo de

www.it-ebooks.info
48 Capítulo 4 Programação do processo

10 e 5 milissegundos, respectivamente — o primeiro recebendo o dobro do tempo do


processador do último! Como valores nice são mais comumente usados em termos
relativos (como a chamada do sistema aceita um incremento, não um valor absoluto), esse
comportamento significa que "nicing down a process by one" tem efeitos extremamente
diferentes dependendo do valor nice inicial.
Terceiro, se executar um bom valor para o mapeamento de timeslice, precisamos da
capacidade de atribuir um timeslice absoluto. Esse valor absoluto deve ser medido em
termos que o kernel pode medir. Na maioria dos sistemas operacionais, isso significa
que a divisão de tempo deve ser um múltiplo inteiro do tique de temporizador. (Consulte
o Capítulo 11,"Temporizadores e gerenciamento de tempo", para obter uma discussão
sobre o tempo.) Isso apresenta vários problemas. Primeiro, a piada de tempo mínima
tem um piso do período do tique-taque do temporizador, que pode ser tão alto quanto
10 milissegundos ou tão baixo quanto 1 milissegundo. Em segundo lugar, o
temporizador do sistema limita a diferença entre duas fatias de tempo; valores
agradáveis sucessivos podem ser mapeados para fatias de tempo em até 10
milissegundos ou até 1 milissegundo de intervalo. Finalmente, as fatias de tempo são
alteradas com diferentes tiques de temporizador. (Se a discussão de tiques de
temporizador deste parágrafo for estrangeira, leia-o novamente após ler o Capítulo
11.Esta é apenas uma motivação por trás do CFS.)
O quarto e último problema é lidar com a ativação do processo em um programador
com base em prioridades que deseja otimizar para tarefas interativas. Em tal sistema,
talvez você queira dar prioridade às tarefas recém-acordadas, permitindo que elas
sejam executadas imediatamente, mesmo que seus períodos de tempo tenham
expirado.Embora isso melhore o desempenho interativo em muitas, se não na maioria
das situações, também abre a porta para casos patológicos em que certos casos de uso
de sono/despertar podem forçar o programador a fornecer um processo por uma
quantidade injusta de tempo do processador, em detrimento do restante do sistema.
A maioria desses problemas são solucionáveis ao fazer mudanças substanciais,
mas não de mudança de paradigma, no programador Unix da velha escola. Por
exemplo, tornar os valores corretos geométricos em vez de aditivos resolve o
segundo problema.E mapear os valores corretos em fatias de tempo usando uma
medida dissociada do tique do temporizador resolve o terceiro problema. Mas tais
soluções desmentem o verdadeiro problema, que é que a atribuição de fatias de
tempo absolutas produz uma taxa de comutação constante, mas a justiça variável.A
abordagem adotada pela CFS é um radical (para programadores de processo)
repensando a alocação de fatias de tempo: Acabe com as fatias de tempo
completamente e atribua a cada processo uma proporção do processador. Assim, o
CFS produz uma equidade constante, mas uma taxa de mudança variável.

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

Obviamente, esse modelo também é impraticável, porque não é possível, em um


único processador, executar literalmente vários processos simultaneamente. Além
disso, não é eficiente executar processos por durações infinitamente pequenas.Ou seja,
há um custo de alternância para antecipar um processo a outro: a sobrecarga de trocar
um processo por outro e os efeitos sobre os caches, por exemplo.Assim, embora
gostaríamos de executar processos por durações muito pequenas, o CFS está atento à
sobrecarga e ao impacto no desempenho ao fazer isso. Em vez disso, o CFS executará
cada processo por algum tempo, alternadamente, selecionando em seguida o processo
que foi executado com menos frequência. Em vez de atribuir a cada processo uma
divisão de tempo, o CFS calcula quanto tempo um processo deve ser executado em
função do número total de processos executáveis. Em vez de usar o valor nice para
calcular um timeslice, o CFS usa o valor nice para ponderar a proposta de processador
que um processo deve receber: Processos de valor mais alto (prioridade mais baixa)
recebem um peso fracionário em relação ao valor nice padrão, enquanto processos de
valor mais baixo (prioridade mais alta) recebem um peso maior.
Cada processo é executado para um "timeslice" proporcional ao seu peso
dividido pelo peso total de todos os threads executáveis.Para calcular o timeslice
real, o CFS define um destino para sua aproximação da duração de agendamento
"infinitamente pequena" em multitarefa perfeita.Esse destino é chamado de latência
direcionada. Metas menores produzem melhor interatividade e uma aproximação
maior para multitarefa perfeita, à custa de custos de comutação mais altos e,
portanto, um throughput geral pior. Vamos supor que a latência almejada seja de 20
milissegundos e temos duas tarefas executáveis na mesma prioridade.
Independentemente da prioridade dessas tarefas, cada uma será executada por 10
milissegundos antes de se antecipar à outra. Se tivermos quatro tarefas com a
mesma prioridade, cada uma será executada por 5 milissegundos. Se houver 20
tarefas, cada uma será executada por 1 milissegundo.
Observe que, à medida que o número de tarefas executáveis se aproxima do
infinito, a proporção de processador alocado e o timeslice atribuído se aproxima de
zero.Como isso acabará resultando em custos de switching inaceitáveis, o CFS impõe
um piso no timeslice atribuído a cada processo.Esse piso é chamado de granularidade
mínima. Por padrão, é 1 milissegundo. Assim, mesmo que o número de processos
executáveis se aproxime do infinito, cada um deles será executado por pelo menos 1
milissegundo, para garantir que haja um teto para os custos de switching incorridos.
(Leitores astutos observarão que o CFS não é, portanto, perfeitamente justo quando o
número de processos cresce tão grande que a proporção calculada é inundada pela
granularidade mínima. Isso é verdade. Embora existam modificações no fair queuing
para melhorar essa equidade, o CFS foi explicitamente projetado para fazer esse trade-
off. No caso comum de apenas um punhado de processos executáveis, o CFS é
perfeitamente justo.)
Agora, vamos considerar novamente o caso de dois processos executáveis, exceto
com valores nice diferentes — digamos, um com o valor nice padrão (zero) e um com um
valor nice de 5. Esses bons valores têm pesos diferentes e, portanto, nossos dois
processos recebem proporções diferentes do tempo do processador. Neste caso, os
pesos resultam em cerca de 1/3 de penalidade para o processo nice-5. Se nossa latência
alvo for novamente 20 milissegundos, nossos dois processos receberão 15
milissegundos e 5 milissegundos cada de tempo do processador, respectivamente. Diga
que nossos dois processos executáveis em vez disso tinham bons valores de 10 e 15.Qual
seria o cronograma alocado? Novamente 15 e 5 milissegundos cada! Valores absolutos de
nice não

www.it-ebooks.info
50 Capítulo 4 Programação do processo

afetam mais as decisões de programação; somente os valores relativos


afetam a proporção do tempo do processador alocado.
Em termos gerais, a proporção de tempo do processador que qualquer processo
recebe é determinada apenas pela diferença relativa de gentileza entre ele e os outros
processos executáveis. Os valores nice, em vez de produzir aumentos aditivos para
timeslices, produzem diferenciais geométricos. A timeslice absoluta alocada qualquer
valor nice não é um número absoluto, mas uma determinada proporção do
processador. O CFS é chamado de fair scheduler porque dá a cada processo uma parte
justa — uma proporção — do tempo do processador.Conforme mencionado, observe
que o CFS não é perfeitamente justo, porque ele apenas se aproxima da multitarefa
perfeita, mas pode colocar um limite inferior na latência de n para n processos
executáveis na injustiça.

A implementação da programação do Linux


Com a discussão sobre a motivação e a lógica do CFS, podemos agora
explorar a implementação real do CFS, que está em kernel/sched_fair.c.
Especificamente, discutimos quatro componentes do CFS:
n Contabilização de Tempo
n Seleção de Processo
n O Ponto de Entrada do Agendador
n Dormindo e Acordando

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.

A Estrutura da Entidade do Agendador


O CFS não tem a noção de uma divisão de tempo, mas ainda deve ter em conta o
tempo em que cada processo é executado, porque precisa garantir que cada
processo seja executado somente para sua parte justa do processador. O CFS
usa a estrutura de entidade do agendador, struct sched_entity, definida em
<linux/sched.h>, para controlar a contabilidade do processo:
www.it-ebooks.info
A implementação da programação do Linux

A estrutura da entidade do agendador está incorporada no descritor do


processo, struct
task_stuct, como uma variável membro chamada se.Nós discutimos o descritor do
processo em
Capítulo 3, "Gerenciamento de processos".

O Tempo de Execução Virtual


A variável vruntime armazena o tempo de execução virtual de um processo, que é o
tempo de execução real (a quantidade de tempo gasto em execução) normalizado
(ou ponderado) pelo número de processos executáveis.As unidades do tempo de
execução virtual são nanossegundos e, portanto, vruntime é decou-pled do tique do
temporizador.O tempo de execução virtual é usado para nos ajudar a aproximar o
"processador ideal de multitarefas" que o CFS está modelando.Com um
processador tão ideal, não precisaríamos de vruntime, porque todos os processos
executáveis seriam perfeitamente multitarefas.Ou seja, processador ideal, o tempo
de execução virtual de todos os processos da mesma prioridade seria idêntico —
todas as tarefas teriam recebido uma parte igual e justa do processador. Como os
processadores não são capazes de realizar multitarefas perfeitas e precisamos
executar cada processo sucessivamente, o CFS usa o vruntime para considerar
quanto tempo um processo dura e, portanto, quanto tempo ele deve durar.
A função update_curr(), definida em kernel/sched_fair.c, gerencia esta
contabilidade:
www.it-ebooks.info
52 Capítulo 4 Programação do processo

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:

update_curr() é invocado periodicamente pelo temporizador do sistema e


também sempre que um processo se torna executável ou bloqueia,
tornando-se inexecutável. Dessa forma, vruntime é uma medida precisa do
tempo de execução de um determinado processo e um indicador de qual
processo deve ser executado em seguida.

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

está decidindo qual processo executar em seguida, ele seleciona o processo


com o menor vruntime.Este é, na verdade, o núcleo do algoritmo de
agendamento do CFS: Escolha a tarefa com o menor vruntime.É isso! O
restante desta subseção descreve como a seleção do processo com o menor
vruntime é implementada.
O CFS usa uma árvore vermelho-preta para gerenciar a lista de processos
executáveis e encontrar eficientemente o processo com o menor vruntime. Uma árvore
rubro-negra, chamada de árvore rubro no Linux, é um tipo de árvore binária de busca
com balanceamento automático. Discutimos árvores binárias de busca com
balanceamento automático em geral e árvores rubro-negras em particular no Capítulo 6.
Por enquanto, se você não estiver familiarizado, precisará saber apenas que árvores
rubro-negras são uma estrutura de dados que armazenam nós de dados arbitrários,
identificados por uma chave específica, e que permitem a pesquisa eficiente de uma
determinada chave. (Especificamente, a obtenção de um nó identificado por uma
determinada chave é logarítmica no tempo em função do total de nós na árvore.)

Escolhendo a próxima tarefa


Vamos começar com a suposição de que temos uma árvore rubro-negra preenchida com
todos os processos executáveis no sistema, em que a chave para cada nó é o tempo de
execução virtual do processo executável.Examinaremos como construímos essa árvore
daqui a pouco, mas, por enquanto, vamos supor que a temos. Dada esta árvore, o processo
que o CFS deseja executar em seguida, que é o processo com o menor vruntime, é o nó mais
à esquerda da árvore. Ou seja, se você seguir a árvore da raiz para baixo através do filho à
esquerda, e continuar movendo para a esquerda até chegar a um nó folha, você encontrará
o processo com o menor vruntime. (Novamente, se você não estiver familiarizado com
árvores de pesquisa binárias, não se preocupe. Apenas saiba que esse processo é
eficiente.) O algoritmo de seleção de processos do CFS é, portanto, resumido como
"executar o processo representado pelo nó mais à esquerda na árvore de erros". A função
que executa essa seleção é
__pick_next_entity(), definido em kernel/sched_fair.c:

Note que __pick_next_entity() não atravessa a árvore para encontrar o nó mais à


esquerda, porque o valor é armazenado em cache por rb_leftmost. Embora seja
eficiente percorrer a árvore para encontrar o nó mais à esquerda — O(altura da
árvore), que é O(log N) para nós O(log N) se a árvore estiver balanceada — é ainda mais
fácil armazenar em cache o nó mais à esquerda.O valor de retorno dessa função
é o processo que o CFS executará em seguida. Se a função retornar NULL, não
haverá nó mais à esquerda e, portanto, nenhum nó na árvore. Nesse caso, não
há processos executáveis e o CFS programa a tarefa ociosa.
www.it-ebooks.info
54 Capítulo 4 Programação do processo

Adicionando processos à árvore


Agora vamos ver como o CFS adiciona processos à árvore de erros e armazena em
cache o nó mais à esquerda. Isso ocorreria quando um processo se tornasse executável
(ativo) ou fosse criado pela primeira vez via fork(), conforme discutido no Capítulo 3.A
adição de processos à árvore é realizada por
enqueue_entity():

Esta função atualiza o tempo de execução e outras estatísticas e, em


seguida, chama
__enqueue_entity() para realizar o trabalho pesado real de inserir a entrada na
árvore vermelho-preta:
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

Removendo processos da árvore


Por fim, vejamos como o CFS remove processos da árvore vermelho-preto. Isso
acontece quando um processo é bloqueado (torna-se inexecutável) ou encerrado (deixa
de existir):

Assim como com a adição de um processo à árvore vermelho-preto, o trabalho real é


realizado por um auxiliar
função, __dequeue_entity():

Remover um processo da árvore é muito mais simples porque a implementação do


rbtree fornece a função rb_erase() que executa todo o trabalho. O resto desta função atualiza o
cache rb_leftmost. Se o processo de remoção for o nó mais à esquerda, o func-
www.it-ebooks.info
A implementação da programação do Linux

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.

O Ponto de Entrada do Agendador


O ponto de entrada principal no agendamento do processo é a função schedule(),
definida em kernel/sched.c.Esta é a função que o restante do kernel usa para chamar o
processoagendador, decidindo qual processo executar e, em seguida, executando-
o.schedule() é genérico com relação às classes do agendador.Ou seja, encontra a
classe do agendador de prioridade mais alta com um processo executável e
pergunta a ele o que executar em seguida. Desde que, não é surpresa que
schedule() seja simples.A única parte importante da função—que de outra forma é
muito desinteressante para ser reproduzida aqui—é a sua chamada de pick_next_task(),
também definida na funçãokernel/sched.cnext_task() passa por cada classe de
agendador, iniciando com a prioridade mais alta, e seleciona o processo de
prioridade mais alta na classe de prioridade mais alta:
www.it-ebooks.info
58 Capítulo 4 Programação do processo

Observe a otimização no início da função. Como o CFS é a classe do


agendador para processos normais, e a maioria dos sistemas executa
principalmente processos normais, há um pequeno hack para selecionar
rapidamente o próximo processo fornecido pelo CFS se o número de processos
executáveis for igual ao número de processos executáveis pelo CFS (o que
sugere que todos os processos executáveis são fornecidos pelo CFS).
O núcleo da função é o loop for(), que repete cada classe em prioridade
ordem, começando com a classe de prioridade mais alta. Cada classe
implementa o
pick_next_task(),
que retorna um ponteiro para seu próximo processo executável
ou, se
não há um, NULL.A primeira classe a retornar um valor não NULL selecionou o próximo
processo executável. Implementação do CFS de chamadas de pick_next_task()
pick_next_entity(), que por sua vez chama a função __pick_next_entity() que
discutido na seção anterior.

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

Algumas interfaces simples para dormir costumavam estar em uso amplo.


Estas interfaces, no entanto, têm corridas: É possível ir dormir depois que a
condição se torna verdadeira. Nesse caso, a tarefa pode dormir
indefinidamente.Portanto, o método recomendado para dormir no ker-nel é um
pouco mais complicado:

A tarefa executa as seguintes etapas para adicionar-se a uma fila de espera:

1. Cria uma fila de espera através da macro DEFINE_WAIT().

2. Adiciona-se a uma fila de espera via add_wait_queue().Esta fila de espera ativa o


processo quando ocorre a condição para a qual está esperando. É claro, precisa
haver código em outro lugar que chame wake_up() na fila quando o evento realmente
ocorre.

3. Chama prepare_to_wait() para mudar o estado do processo para


TASK_INTERRUPTIBLE ou TASK_UNINTERRUPTIBLE.Essa função também
adiciona a tarefa de volta à fila de espera, se necessário, que é
necessária nas iterações subsequentes do loop.

4. Se o estado for definido como TASK_INTERRUPTIBLE, um sinal ativará o


processo. Isso é chamado de ativação artificial (uma ativação não causada pela
ocorrência do evento). Então verifique e manipule sinais.

5. Quando a tarefa é ativada, ela verifica novamente se a condição é verdadeira. Se


estiver, ele sai do loop. Caso contrário, ele chama novamente schedule() e repete.

6. Agora que a condição é verdadeira, a tarefa define a si mesma como


TASK_RUNNING e remove a si mesma da fila de espera via finish_wait().
Se a condição ocorrer antes de a tarefa entrar em suspensão, o loop será
encerrado e a tarefa não entrará em suspensão por engano. Observe que o código do
kernel frequentemente precisa executar várias outras tarefas no corpo do loop. Por
exemplo, ele pode precisar liberar bloqueios antes de chamar schedule() e readquiri-los
após ou reagir a outros eventos.
www.it-ebooks.info
60 Capítulo 4 Programação do processo

A função inotify_read() em fs/notify/inotify/inotify_user.c, que lida com a


leitura do descritor de arquivos inotify, é um exemplo simples de uso
de filas de espera:

www.it-ebooks.info
A implementação da programação do Linux

Essa função segue o padrão estabelecido em nosso exemplo.A principal


diferença é que ela verifica a condição no corpo do loop while(), em vez da própria
instrução while().Isso ocorre porque verificar a condição é complicado e requer
bloqueios de captura.O loop é encerrado via break.

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

Figura 4.1 Dormindo e acordando.


www.it-ebooks.info
62 Capítulo 4 Programação do processo

Preempção e Alternância de Contexto


A alternância de contexto, a alternância de uma tarefa executável para outra, é tratada
pelo
context_switch()função
definida em kernel/sched.c. É chamada por schedule()
quando um novo processo tiver sido selecionado para execução. Ele faz
dois trabalhos básicos:
n Chama switch_mm(), que é declarado em <asm/mmu_context.h>, para alternar o
mapeamento da memória vir-tual do processo anterior para o do novo processo.
n Chama switch_to(), declarado em <asm/system.h>, para alternar o estado do
processador do processo anterior para o atual.Isso envolve salvar e restaurar
informações da pilha e os registros do processador e qualquer outro estado específico da
arquitetura que deva ser gerenciado e restaurado por processo.
O kernel, entretanto, deve saber quando chamar schedule(). Se chamasse schedule()
somente quando o código o fizesse explicitamente, os programas de espaço do usuário
poderiam ser executados indefinidamente. Em vez disso, o kernel fornece o flag
need_resched para indicar se um reagendamento deve ser executado (veja a Tabela 4.1).
Esse flag é definido por scheduler_tick() quando um processo deve ser preempted, e por
_to_wake_up() quando um processo que tem uma prioridade mais alta que o processo
em execução no momento é ativado.O kernel verifica o flag, vê que ele está definido e
chama schedule() para alternar para um novo processo.O flag é uma mensagem para o
kernel que o scheduler deve ser chamado assim que possível porque outro processo
merece ser executado.

Quadro 4.1 Funções para acessar e manipular need_resched


Função Finalidade
Define o sinalizador de need_resched no processo
set_tsk_need_resched() fornecido.

clear_tsk_need_resched() Limpar o sinalizador de need_resched no


processo.
need_resched() Teste o valor do flag de need_resched; return
true se definido e false caso contrário.

Ao retornar para o espaço do usuário ou de uma interrupção, o sinalizador


need_resched é verificado. Se estiver definido, o kernel chama o agendador antes
de continuar.
O sinalizador é por processo, e não simplesmente global, porque é mais rápido
acessar um valor no descritor de processo (devido à velocidade da corrente e alta
probabilidade de ser cache quente) do que uma variável global. Historicamente, a
bandeira era global antes do kernel 2.2. Em 2.2 e 2.4, o flag era um int dentro do
task_struct. No 2.6, ele foi movido para um bit de sin-gle de uma variável de flag
especial dentro da estrutura thread_info.

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

sabe que está em um estado seguro e silencioso. Em outras palavras, se é


seguro continuar executando a tarefa atual, também é seguro escolher uma
nova tarefa para executar. Consequentemente, sempre que o ker-nel estiver se
preparando para retornar ao espaço do usuário no retorno de uma interrupção
ou após uma chamada do sistema, o valor de need_resched é verificado. Se estiver
definido, o agendador será chamado para selecionar um novo processo (mais
adequado) a ser executado. Ambos os caminhos de retorno para retorno da
interrupção e retorno da chamada do sistema são dependentes da arquitetura e
normalmente implementados no assembly em entry.S (que, além do código de
entrada do kernel, também contém o código de saída do kernel).
Em resumo, a preempção do usuário pode ocorrer

n Ao retornar para o espaço do usuário de uma chamada do sistema


n Ao retornar ao espaço de usuário de um manipulador de interrupção

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

preempção. Presume-se que o código que chama explicitamente


schedule() sabe que é seguro reagendar.
A preempção do kernel pode ocorrer
n Quando um manipulador de interrupção sai, antes de retornar ao
espaço do kernel
n Quando o código do kernel se torna preempível novamente
n Se uma tarefa no kernel chamar explicitamente schedule()
n Se uma tarefa no kernel bloquear (o que resulta em uma chamada
para schedule())

Políticas de agendamento em tempo real


O Linux fornece duas políticas de agendamento em tempo real, SCHED_FIFO e
SCHED_RR.A política de agendamento não-mal, não em tempo real, é
SCHED_NORMAL.Através da estrutura de classes de agendamento, essas
políticas em tempo real são gerenciadas não pelo Completely Fair Scheduler,
mas por um agendador em tempo real especial, definido emkernel/sched_rt.c. O
restante desta seção discute as políticas e o algoritmo de agendamento em
tempo real.
SCHED_FIFO implementa um algoritmo simples de agendamento first-in, first-out sem
SCHED_ FIFO fatias de tempo.Uma tarefa executável SCHED_FIFO é sempre
agendada sobre qualquer SCHED_NORMAL tarefas. Quando uma tarefa SCHED_FIFO se torna
executável, ela continua a ser executada até que bloqueie ou explic-itly produza o
processador; ela não tem timeslice e pode ser executada indefinidamente. Somente
uma tarefa SCHED_FIFO ou SCHED_RR de prioridade mais alta pode antecipar a tarefa
SCHED_FIFO.Two ou mais

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

As prioridades em tempo real variam inclusive de zero a MAX_RT_PRIO menos


1. Por padrão, MAX_RT_PRIO é 100 — portanto, o intervalo de prioridade em tempo
real padrão é de zero a 99.Este espaço de prioridade é compartilhado com os
valores corretos de SCHED_NORMALogitasks:Eles usam o espaço de MAX_RT_PRIO para
(horizontalmente). Por padrão, isso significa que a faixa agradável -20 a +19 é mapeada
diretamente no espaço de prioridade de 100 a 139.

Chamadas do Sistema Relacionadas ao Agendador


O Linux fornece uma família de chamadas do sistema para o gerenciamento de parâmetros
do agendador. Essas chamadas do sistema permitem a manipulação da prioridade do
processo, da política de agendamento e da afinidade do processador, assim como fornecem
um mecanismo explícito para levar o processador a outras tarefas.
Vários livros—e suas páginas amigáveis do manual do sistema—fornecem referência a
essas chamadas do sistema (que são todas implementadas na biblioteca C sem muito
wrapper—eles apenas invocam a chamada do sistema).A Tabela 4.2 lista as chamadas do
sistema e fornece uma breve descrição.
Como as chamadas do sistema são implementadas no kernel é discutido no Capítulo 5,
"Chamadas do sistema".

Quadro 4.2 Chamadas do Sistema Relacionadas ao Agendador

Chamada do sistema Descrição


nice () Define o valor de nice de um processo
sched_ setscheduler () Define a política de agendamento de um processo
sched_ getscheduler () Obtém a política de agendamento de um processo

sched_ setparam () Define a prioridade em tempo real de um processo

sched_ getparam () Obtém a prioridade em tempo real de um processo


sched_get_priority_max() Obtém a prioridade máxima em tempo real

sched_get_priority_min() Obtém a prioridade mínima em tempo real

sched_rr_get_interval() Obtém o valor de timeslice de um processo


sched_ setaffinity () Define a afinidade do processador de um processo

sched_ getaffinity () Obtém a afinidade de um processador do processo

sched_ yield() Produz o processador temporariamente


www.it-ebooks.info
66 Capítulo 4 Programação do processo

Política de Agendamento e Chamadas do Sistema


Relacionadas a Prioridades
As chamadas do sistema sched_setscheduler() e sched_getscheduler() são
definidas e recebem a política de agendamento e a prioridade em tempo
real de um determinado processo, respectivamente.Sua implementação,
como a maioria das chamadas do sistema, envolve muita verificação de
argumentos, configuração e limpeza.O trabalho importante, no entanto, é
apenas ler ou gravar a política e os valores rt_priority no processo
As chamadas de sistema sched_setparam() e sched_getparam() são definidas e
recebem a prioridade em tempo real de um processo. Essas chamadas
meramente codificam
structure.The chamadas sched_get_priority_max() and sched_get_priority_min()
retornam as prioridades máxima e mínima, respectivamente, para uma
determinada política de agendamento. A prioridade máxima para as
políticas em tempo real éyXy_USER_RT_PRIO menos um; o mini-mum é
um.
Para tarefas normais, a função nice() incrementa a prioridade estática
do processo dado de acordo com o valor dado. Somente root pode
fornecer um valor negativo, diminuindo assim o valor nice e aumentando
a prioridade.A função nice() chama a função set_user_nice() do kernel, que define
o static_prio e prio valores na tarefa task_structnice como apropriado.

Chamadas do Sistema de Afinidade do Processador


O programador do Linux impõe a afinidade do processador rígido.Ou seja, embora ele
tente fornecer
afinidade suave ou natural, tentando manter os processos no mesmo processador, o sched-
uler também permite que um usuário diga: "Esta tarefa deve sched_ getaffinity ()
permanecer neste subconjunto do
processadores, não importa o que aconteça."Essa afinidade de hardware é
armazenada como uma máscara de bits na
task_struct como cpus_allowed.The bitmask contém um bit por possível processador
em
sistema. Por padrão, todos os bits são definidos e, portanto, um processo é potencialmente
executável em
qualquer processador.O usuário, entretanto, via sched_setaffinity(), pode fornecer um
bit-
máscara de qualquer combinação de um ou mais bits.
Da mesma forma, a chamada retorna o bitmask atual
cpus_allowed.
O kernel reforça a afinidade de hardware de uma maneira simples. Primeiro,
quando um processo é criado inicialmente, ele herda a máscara de afinidade de
seu pai. Como o pai está sendo executado em um processador permitido, o
filho é executado em um processador permitido. Segundo, quando a afinidade
de um processador é alterada, o kernel usa os threads de migração para enviar
a tarefa para um processador legal. Por fim, o balanceador de carga transfere
tarefas para apenas um processador permitido.Portanto, um processo só é
executado em um processador cujo bit é definido no campo cpus_allowed de
seu descritor de processo.
Gerando tempo do processador
O Linux fornece a sched_yield() chamada de sistema como um mecanismo para um processo
explicitamente ceder o processador a outros processos de espera. Ele funciona removendo
o processo do array ativo (onde ele está no momento, porque ele está em execução) e
inserindo-o no array expirado.Isso tem o efeito de não apenas antecipar o processo e
colocá-lo no final de sua lista de prioridades, mas também colocá-lo na lista expirada,
garantindo que ele não será executado por um

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

Em qualquer sistema operacional moderno, o kernel fornece um conjunto de interfaces


através das quais os processos em execução no espaço do usuário podem interagir com o
sistema.Essas interfaces dão aos aplicativos acesso controlado ao hardware, um
mecanismo com o qual criar novos processos e se comunicar com os existentes, e a
capacidade de solicitar outros recursos do sistema operacional.As interfaces agem como
mensageiros entre os aplicativos e o kernel, com os aplicativos emitindo várias solicitações
e o kernel atendendo-as (ou retornando um erro).A existência dessas interfaces e o fato de
que os aplicativos não são livres para fazer diretamente o que eles querem, é fundamental
para fornecer um sistema estável.

Comunicação com o Kernel


As chamadas do sistema fornecem uma camada entre o hardware e os processos do
espaço do usuário. Essa camada serve a três finalidades principais. Primeiro, ele fornece
uma interface de hardware abstrata para o espaço do usuário.Ao ler ou gravar de um
arquivo, por exemplo, os aplicativos não se preocupam com o tipo de disco, mídia ou
mesmo o tipo de sistema de arquivos no qual o arquivo reside. Segundo, as chamadas do
sistema garantem a segurança e a estabilidade do sistema.Com o kernel agindo como um
intermediário entre os recursos do sistema e o espaço do usuário, o kernel pode arbitrar o
acesso com base em permissões, usuários e outros critérios. Por exemplo, essa arbitragem
impede que os aplicativos usem incorretamente o hardware, roubem os recursos de outros
processos ou prejudiquem o sistema. Finalmente, uma única camada comum entre o
espaço do usuário e o restante do sistema permite o sistema virtualizado fornecido aos
processos, discutido no Capítulo 3, "Gerenciamento de processos". Se os aplicativos
fossem livres para acessar os recursos do sistema sem o conhecimento do kernel, seria
quase impossível implementar multitarefas e memória virtual, e certamente impossível fazê-
lo com estabilidade e segurança. No Linux, as chamadas do sistema são o único meio que o
espaço do usuário tem de fazer interface com o kernel; elas são o único ponto de entrada
legal no kernel, exceto exceções e armadilhas. De fato, outras interfaces, como arquivos de
dispositivo ou /proc, são finalmente acessadas através de chamadas do sistema.
Curiosamente, o Linux
www.it-ebooks.info
70 Capítulo 5 Chamadas do sistema

implementa muito menos chamadas do sistema do que a maioria dos


sistemas.1Este capítulo aborda o papel e a implementação das
chamadas do sistema no Linux.

APIs, POSIX e a biblioteca C


Normalmente, os aplicativos são programados em relação a uma API implementada no
espaço do usuário, não diretamente em chamadas do sistema.Isso é importante porque
não é necessária nenhuma correlação direta entre as interfaces que os aplicativos usam
e a interface real fornecida pelo kernel.Uma API define um conjunto de interfaces de
programação usadas pelos aplicativos.Essas interfaces podem ser implementadas
como uma chamada do sistema, implementadas por meio de várias chamadas do
sistema ou implementadas sem o uso de chamadas do sistema.A mesma API pode
existir em vários sistemas e fornecer a mesma interface para os aplicativos, enquanto a
implementação da própria API pode diferir muito de sistema para sistema. Consulte a
Figura 5.1 para obter um exemplo da relação entre uma API POSIX, a biblioteca C e as
chamadas do sistema.

Kernel de biblioteca do aplicativo C Comment

Figura 5.1 A relação entre aplicativos, a biblioteca C,


e o kernel com uma chamada para printf().

Uma das interfaces de programação de aplicativos mais comuns no mundo Unix é


baseada no padrão POSIX. Tecnicamente, POSIX é composto de uma série de padrões do
IEEE 2 que visam fornecer um padrão de sistema operacional portátil aproximadamente
baseado no Unix. O Linux se esforça para estar em conformidade com POSIX e SUSv3,
onde aplicável.
POSIX é um excelente exemplo da relação entre APIs e chamadas do sistema.
Na maioria dos sistemas Unix, as chamadas de API definidas pelo POSIX têm uma
forte correlação com as chamadas do sistema. De fato, o padrão POSIX foi criado
para se assemelhar às interfaces fornecidas pelos sistemas Unix anteriores. Por
outro lado, alguns sistemas que são bastante não-Unix, como o Microsoft
Windows, oferecem bibliotecas compatíveis com POSIX.

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º

A interface de chamada do sistema no Linux, como na maioria dos sistemas Unix, é


fornecida em parte pela biblioteca C.A biblioteca C implementa a API principal em
sistemas Unix, incluindo a biblioteca C padrão e a interface de chamada do sistema.A
biblioteca C é usada por todos os programas C e, por causa da natureza de C, é
facilmente empacotada por outras linguagens de programação para uso em seus
programas.A biblioteca C também fornece a maioria da API POSIX.
Do ponto de vista do programador de aplicativos, as chamadas do sistema são
irrelevantes; tudo o que o programador está preocupado é com a API. Por outro lado, o
núcleo está preocupado apenas com as chamadas do sistema; o que as chamadas da
biblioteca e as aplicações fazem uso das chamadas do sistema não é da preocupação do
núcleo. No entanto, é importante que o kernel acompanhe os usos potenciais de uma
chamada do sistema e mantenha a chamada do sistema o mais geral e flexível possível.
Um meme relacionado a interfaces no Unix é "Proporcionar mecanismo, não
política". Em outras palavras, as chamadas do sistema Unix existem para fornecer
uma função específica em um sentido abstrato. A maneira em que a função é usada
não é nenhum dos negócios do kernel.

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

4 O uso de texto longo

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:

Vejamos como as chamadas do sistema são definidas. Primeiro, observe o


modificador asmlinkage na definição da função. Esta é uma diretiva para dizer
ao compilador que procure apenas na pilha os argumentos desta função. Este é
um modificador necessário para todas as chamadas do sistema. Segundo, a
função retorna um longo. Para compatibilidade entre sistemas de 32 e 64 bits, as
chamadas do sistema definidas para retornar um int no espaço do usuário
retornam um long no kernel.Em terceiro lugar, observe que a chamada do
sistema getpid() é definida como sys_getpid() no kernel.Esta é a convenção de nomenclatura
obtida com todas as chamadas do sistema no Linux: System callBar() é implementada no ker-nel como
function sys_bar()Ajuda.

Números de Chamada do Sistema


No Linux, cada chamada do sistema é atribuída a um número syscall. Esse é
um número exclusivo usado para fazer referência a uma chamada do
sistema específica. Quando um processo do espaço do usuário executa
uma chamada do sistema, o número syscall identifica qual syscall foi
executado; o processo não faz referência ao syscall por nome.
O número syscall é importante; quando atribuído, ele não pode alterar nem compilar
appli-
catiões vão quebrar. Da mesma forma, se uma chamada do sistema for removida, seu número
de chamada do sistema não poderá ser
código previamente compilado teria por objetivo invocar uma chamada ao sistema,
realidade invocar outro. O Linux fornece uma chamada de sistema "não
implementada",
sys_ni_syscall(),
que não faz nada além de return -ENOSYS, o erro correspondente
a uma chamada de sistema inválida. Essa função é usada para "fechar o orifício" no caso
raro de um
syscall foi removido ou indisponível.
O kernel mantém uma lista de todas as chamadas do sistema
registradas na tabela de chamada do sistema, armazenada em
sys_call_table.Esta tabela é arquitetura; em x86-64, ela é definida em
arch/i386/kernel/syscall_64.c.Esta tabela atribui cada syscall válido a um número
exclusivo de syscall.

Desempenho da Chamada do Sistema


As chamadas de sistema no Linux são mais rápidas do que em muitos
outros sistemas operacionais.Isso se deve, em parte, aos tempos rápidos de
alternância de contexto do Linux; entrar e sair do kernel é um processo
simples e alinhado ao fluxo.O outro fator é a simplicidade do manipulador de
chamadas de sistema e das próprias chamadas de sistema individuais.
5 Você deve estar se perguntando por que getpid() retorna tgid, a ID do grupo de threads?

getpid ()

www.it-ebooks.info
Manipulador de Chamadas do Sis

Manipulador de Chamadas do Sistema


Não é possível para aplicativos do espaço do usuário executar o código do kernel
diretamente. Eles não podem simplesmente fazer uma chamada de função para um
método existente no espaço do kernel porque o kernel existe em um espaço de memória
protegido. Se os aplicativos pudessem ler e gravar diretamente no espaço de endereço
do kernel, a segurança e a estabilidade do sistema seriam inexistentes.
Em vez disso, aplicativos de espaço de usuário devem de alguma forma sinalizar
para o kernel que eles querem executar uma chamada do sistema e ter o sistema
alternado para o modo kernel, onde a chamada do sistema pode ser executada no
espaço kernel pelo kernel em nome do aplicativo.
O mecanismo para sinalizar o kernel é uma interrupção de software: gerar uma
exceção, e o sistema irá alternar para o modo kernel e executar o manipulador de
exceção. O manipulador de exceção, neste caso, é realmente o manipulador de
chamada do sistema. A interrupção de software definida em x86 é a interrupção número
128, que é incorrida através da instrução int $0x80. Ele dispara uma opção para o modo
kernel e a execução do vetor de exceção 128, que é o handler de chamada do sistema. O
handler de chamada do sistema é a função apropriadamente nomeada system_call(). É
dependente da arquitetura; em x86-64 é implementado em montagem em entry_64.S.6
Recentemente, os processadores x86 adicionaram um recurso conhecido como
sysenter.Este recurso fornece uma maneira mais rápida e especializada de capturar em
um kernel para executar uma chamada do sistema do que usando a instrução sysenter.
O suporte para este recurso foi rapidamente adicionado ao kernel. Independentemente
de como o manipulador de chamadas do sistema é chamado, no entanto, a noção
importante é que de alguma forma o espaço do usuário faz com que uma exceção ou
armadilha entre no kernel.

Denotando a chamada de sistema correta


Simplesmente digitar apenas o espaço do kernel não é suficiente porque existem
várias chamadas do sistema, todas as quais entram no kernel da mesma maneira.
Assim, o número da chamada do sistema deve ser passado para o kernel. Em x86,
o número syscall é alimentado para o kernel através do eax regis-ter. Antes de
causar a armadilha no kernel, o espaço do usuário mantém cada número
correspondente à chamada do sistema desejada.O manipulador de chamadas do
sistema lê o valor de cada. Outras arquiteturas fazem algo parecido.
A função system_call() verifica a validade do número de chamada do sistema
dado comparando-o com NR_syscalls. Se for maior ou igual a NR_syscalls, a função
retornará -ENOSYS. Caso contrário, a chamada do sistema especificada será
chamada:

Como cada elemento na tabela de chamada do sistema é de 64 bits (8 bytes), o


kernel multiplica o número de chamada do sistema fornecido por quatro para chegar
ao seu local na tabela de chamada do sistema. Em x86-32, o código é semelhante, com
o 8 substituído por 4. Ver Figura 5.2.
6 Grande parte da descrição a seguir do manipulador de chamadas do sistema é baseada na versão
x86. São todos semelhantes.

www.it-ebooks.info
74º Capítulo 5 Chamadas do sistema

chamar read() wrapper read()system_ call () sys_ read ()

Aplicativo Biblioteca C Manipulador Syscall sys_ read ()


wrapper read()

Espaço do Usuário Espaço do Kernel

Figura 5.2 Chamando o manipulador de chamadas do sistema e executando uma


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

Implementação de chamada do sistema


A implementação real de uma chamada do sistema no Linux não precisa se
preocupar com o comportamento do manipulador de chamadas do
sistema.Assim, adicionar uma nova chamada do sistema ao Linux é
realmente fácil.O trabalho árduo está em projetar e implementar a chamada
do sistema; registrá-la com o kernel é simples. Vejamos as etapas
envolvidas na criação de uma nova chamada de sistema para o Linux.

Implementando Chamadas do Sistema


A primeira etapa na implementação de uma chamada de sistema é definir seu
objetivo.O que ela fará? O syscall deve ter exatamente uma finalidade. A
multiplexação de syscalls (uma única chamada de sistema que faz coisas
extremamente diferentes dependendo de um argumento de flag) é desencorajada
no Linux. Olhe para ioctl() como um exemplo do que não fazer.
Quais são os argumentos, o valor de retorno e os códigos de erro da nova chamada do
sistema? A chamada do sistema deve ter uma interface limpa e simples com o menor
número possível de argumentos.A semântica e o comportamento de uma chamada do
sistema são importantes; eles não devem mudar, porque os aplicativos existentes passarão
a depender deles. Seja prospetivo; considere
www.it-ebooks.info
Implementação de chamada do sistema

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

n O ponteiro aponta para uma região da memória no espaço do usuário. Os processos


não devem ser capazes de enganar o kernel na leitura de dados no espaço do kernel em seu
nome.
n O ponteiro aponta para uma região da memória no espaço de endereço do processo.O processo
não deve ser capaz de enganar o kernel para que ele leia os dados de outra pessoa.
n Se estiver lendo, a memória está marcada como legível. Se estiver gravando, a
memória será marcada como gravável. Em execução, a memória é marcada como
executável. O processo não deve poder ignorar as restrições de acesso à memória.
O kernel fornece dois métodos para executar as verificações de requisitos e a
cópia desejada de e para o espaço do usuário. Note que o código do kernel nunca
deve seguir cegamente um ponteiro para o espaço do usuário! Um destes dois
métodos deve ser sempre utilizado.

www.it-ebooks.info
76 Capítulo 5 Chamadas do sistema

Para escrever no espaço do usuário, o método copy_to_user() é fornecido.


São necessários três parâmetros.O primeiro é o endereço da memória de
destino no espaço de endereço do processo.O segundo é o ponteiro de
origem no espaço de kernel. Finalmente, o terceiro argumento é o
tamanho em bytes dos dados a serem copiados.
Para ler do espaço do usuário, o método copy_from_user() é análogo a
copy_to_user().A função lê do segundo parâmetro para o primeiro
o número de bytes especificado no terceiro parâmetro.
Ambas as funções retornam o número de bytes que falharam ao copiar
em caso de erro. No sucesso, eles retornam zero. É padrão que o syscall
retorne -EFAULT no caso de tal erro.
Vamos considerar um exemplo de chamada do sistema que usa
copy_from_user() e
copy_to_user().This syscall, bobo_copy(), é totalmente inútil; ele copia dados de seu
primeiro parâmetro em seu segundo. Isso é subideal na medida em que envolve um
parâmetro intermediário e
cópia estranha no espaço do kernel para nenhum ganho. Mas ajuda a ilustrar o
ponto.

Tanto copy_to_user() quanto copy_from_user() podem bloquear. Isto ocorre, por


exemplo, se a página contendo os dados do usuário não estiver na memória
física, mas for trocada para o disco. Nesse caso, o processo é suspenso até
que o manipulador de falhas de página possa trazer a página do arquivo de
troca no disco para a memória física.
Uma verificação final possível é de permissão válida. Em versões mais antigas do
Linux, era padrão para syscalls que requerem privilégio de raiz para usar suser().Esta
função apenas foi verificada
www.it-ebooks.info
Implementação de chamada 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

Consulte <linux/capability.h> para obter uma lista de todos os recursos e quais


direitos eles envolvem.

Contexto de Chamada do Sistema


Conforme discutido no Capítulo 3, o kernel está no contexto do
processo durante a execução de uma chamada do sistema.O ponteiro
atual aponta para a tarefa atual, que é o processo que emitiu o syscall.
No contexto do processo, o kernel é capaz de dormir (por exemplo, se a chamada
do sistema for bloqueada em uma chamada ou chamar explicitamente schedule()) e for
totalmente imprimível.Esses dois pontos são importantes. Primeiro, a capacidade de
dormir significa que as chamadas do sistema podem fazer uso da maioria da
funcionalidade do kernel. Como veremos no Capítulo 7,"Interrupções e
www.it-ebooks.info
Contexto de Chamada do Sistema

Interrupt Handlers," a capacidade de dormir simplifica muito a programação do


kernel.7 O fato de que o contexto do processo é preemptível implica que, como o
espaço do usuário, a tarefa atual pode ser preemptada por outra tarefa. Como a nova
tarefa pode executar a mesma chamada do sistema, é necessário ter cuidado para
garantir que as chamadas do sistema sejam reentrantes. É claro que esta é a mesma
preocupação que o multiprocessamento simétrico introduz. A reentrada da
sincronização é abordada no Capítulo 9, "Uma introdução à sincronização do kernel",
e no Capítulo 10, "Métodos de sincronização do kernel".
Quando a chamada do sistema retorna, o controle continua em system_call(), que
finalmente alterna para o espaço do usuário e continua a execução do processo
do usuário.

Etapas finais na vinculação de uma chamada do sistema


Depois que a chamada do sistema é gravada, é trivial registrá-la como uma
chamada oficial do sistema:
1. Adicionar uma entrada ao final da tabela de chamadas do sistema.Isso precisa ser feito para
cada arquitetura que suporte a chamada do sistema (que, para a maioria das chamadas, são todas as
arquiteturas).A posição da syscall na tabela, começando em zero, é sua chamada do sistema num-ber.
Por exemplo, a décima entrada na lista é atribuída a syscall número nove.

2. Para cada arquitetura compatível, defina o número syscall em <asm/unistd.h>.


3. Compile o syscall na imagem do kernel (em vez de compilar como um módulo).
Isso pode ser tão simples quanto colocar a chamada do sistema em um arquivo
relevante no kernel/, como sys.c, que é o lar de várias chamadas do sistema.

Veja esses passos mais detalhadamente com uma chamada fictícia do


sistema, foo(). Primeiro, queremos adicionar sys_foo() à tabela de chamada
do sistema. Para a maioria das arquiteturas, a tabela está localizada em
entry.S e tem a seguinte aparência:

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

A nova chamada do sistema é então anexada à parte final desta lista:

Embora não seja especificado explicitamente, a chamada do sistema recebe o próximo


número syscall subsequente — nesse caso, 338. Para cada arquitetura à qual você deseja
oferecer suporte, a chamada do sistema deve ser adicionada à tabela de chamadas do
sistema da arquitetura. A chamada do sistema não precisa receber o mesmo número syscall
em cada arquitetura, pois o número da chamada do sistema faz parte da ABI exclusiva da
arquitetura. Normalmente, você desejaria tornar a chamada do sistema disponível para cada
arquitetura. Observe a convenção de colocar o número em um comentário a cada cinco
entradas; isso facilita descobrir qual syscall recebe qual número.
Em seguida, o número da chamada do sistema é adicionado a
<asm/unistd.h>,
que atualmente tem a seguinte aparência:

O seguinte é então adicionado ao final da lista:

www.it-ebooks.info
Contexto de Chamada do Sistema

Finalmente, a chamada real do sistema foo() é implementada. Como a chamada do


sistema deve ser compilada na imagem do núcleo do kernel em todas as
configurações, neste exemplo, ela é definida em kernel/sys.c.Você deve colocá-la onde
quer que a função seja mais relevante; por exemplo, se a função estiver relacionada ao
agendamento, você poderá defini-la em kernel/sched.c.

É isso! Inicialize este kernel e o espaço do usuário pode invocar a chamada do


sistema foo().

Acessando a chamada do sistema a partir do espaço do usuário


Geralmente, a biblioteca C fornece suporte para chamadas do sistema. Os
aplicativos do usuário podem extrair protótipos de função dos cabeçalhos padrão e
vinculá-los à biblioteca C para usar a chamada do sistema (ou a rotina da biblioteca
que, por sua vez, usa a chamada syscall). Se você apenas escreveu a chamada do
sistema, no entanto, é duvidoso que glibc já o suporte!
Felizmente, o Linux fornece um conjunto de macros para empacotar o acesso às
chamadas do sistema. Define
registrará o conteúdo e emitirá as instruções de interceptação.Essas macros são
nomeadas
_syscall (), onde está entre 0 e 6. O número corresponde ao número de
parâmetros passados para o syscall porque a macro precisa saber quantos parâmetros-
e, por conseguinte, a inscrição nos registros. Por exemplo, considere a chamada do sistema
open(), definido como

A macro syscall para usar esta chamada do sistema sem suporte explícito à
biblioteca seria

Em seguida, o aplicativo pode simplesmente chamar open().


Para cada macro, há 2 + 2 × n parâmetros. O primeiro parâmetro corresponde ao tipo
de retorno do syscall. O segundo é o nome da chamada do sistema. Em seguida, segue
o tipo e o nome de cada parâmetro na ordem da chamada do sistema.O __NR_open define
está em <asm/unistd.h>; é o número da chamada do sistema.A macro _syscall3 se expande
em uma função C com assembly embutido; o assembly executa as etapas discutidas na seção anterior para
empurrar o número da chamada do sistema e os parâmetros para os registros corretos e problema
www.it-ebooks.info
82 Capítulo 5 Chamadas do sistema

a interrupção do software para fazer trapping no kernel. Colocar esta


macro em um aplicativo é tudo o que é necessário para usar a
chamada do sistema open().
Vamos escrever a macro para usar nossa esplêndida chamada do
sistema new foo() e, em seguida, escrever algum código de teste para
mostrar nossos esforços.

Por que não implementar uma chamada de sistema


As seções anteriores mostraram que é fácil implementar uma nova chamada do
sistema, mas isso não deve incentivá-lo a fazer isso. Na verdade, você deve ter cautela
e restrição ao adicionar novas syscalls. Muitas vezes, alternativas muito mais viáveis
para fornecer uma nova chamada de sistema estão disponíveis. Vamos analisar os
prós, contras e alternativas.
Os prós da implementação de uma nova interface como um syscall são
os seguintes:
n As chamadas do sistema são simples de implementar e fáceis de
usar.
n O desempenho da chamada do sistema no Linux é rápido.
Os contras:
n Você precisa de um número syscall, que precisa ser atribuído oficialmente a
você.
n Depois que a chamada do sistema está em um kernel de série estável, ela é
escrita em stone.The interface não pode mudar sem quebrar aplicações de espaço do
usuário.
n Cada arquitetura precisa registrar separadamente a chamada do sistema e
oferecer suporte a ela.
n As chamadas do sistema não são facilmente usadas a partir de
scripts e não podem ser acessadas diretamente do sistema de arquivos.
n Como você precisa de um número syscall atribuído, é difícil manter e
usar uma chamada sys-tem fora da árvore mestre do kernel.
n Para simples trocas de informações, uma chamada do sistema é um
exagero.
As alternativas:
n Implementar um nó de dispositivo e ler() e gravar() nele. Use ioctl() para
manipular configurações específicas ou recuperar informações específicas.

www.it-ebooks.info
Conclusão

83º

n Certas interfaces, como semáforos, podem ser representadas como


descritores de arquivos e manipuladas como tal.
n Adicione as informações como um arquivo ao local apropriado no sysfs.
Para muitas interfaces, as chamadas do sistema são a resposta correta. O Linux, no
entanto, tentou evitar simplesmente adicionar uma chamada de sistema para suportar cada
nova abstração que vem junto.O resultado tem sido uma camada de chamada de sistema
incrivelmente limpa com alguns arrependimentos ou depreciações (interfaces não mais
usadas ou suportadas).A taxa lenta de adição de novas chamadas de sistema é um sinal de
que o Linux é um sistema operacional relativamente estável e com recursos completos.

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.

Listas Vinculadas Única e Duplamente


A estrutura de dados mais simples que representa essa lista
vinculada pode ser semelhante à seguinte:
www.it-ebooks.info
86 Capítulo 6 Estruturas de dados do kernel

A figura 6.1 é uma lista vinculada.

·· próxi nulo
mo • · próximo · · · próximo

Figura 6.1 Uma lista vinculada


individualmente.

Em algumas listas vinculadas, cada elemento também contém um


ponteiro para o elemento anterior.Essas listas são chamadas listas
duplamente vinculadas porque são vinculadas para frente e para trás. As
listas vinculadas, como a lista na Figura 6.1, que não têm um ponteiro para o
elemento anterior são chamadas listas vinculadas individualmente.
Uma estrutura de dados representando uma lista duplamente vinculada seria semelhante a
esta:

A figura 6.2 é uma lista duplamente vinculada.

próximo próximo nulo


nulo ·· anterior ·· próx ante ··
imo rior

Figura 6.2 Uma lista duplamente vinculada.

Listas Vinculadas Circulares


Normalmente, como o último elemento em uma lista vinculada não tem próximo elemento,
ele é definido para apontar para um valor especial, como NULL, para indicar que é o último
elemento na lista. Em algumas listas vinculadas, o último elemento não aponta para um
valor especial. Em vez disso, ele aponta de volta para o primeiro valor. Essa lista vinculada é
chamada de lista vinculada circular porque é cíclica. Listas circulares vinculadas podem vir
em versões duplamente vinculadas e individualmente. Numa lista circular duplamente
ligada,
www.it-ebooks.info
Listas Vinculadas

o ponteiro "anterior" do primeiro nó aponta para o último nó. As


figuras 6.3 e 6.4 são listas encadeadas, uma única vez e duas vezes
circulares, respectivamente.

·· próxi ·· próxi ·· próximo


mo mo

Figura 6.3 Uma lista circular com um único link.

ante ·· próx ante ·· próx ante ·· próximo


rior imo rior imo rior

Figura 6.4 Uma lista circular duplamente vinculada.

Embora a implementação da lista vinculada do kernel do Linux seja única, ela é


fundamentalmente uma lista circular duplamente vinculada. O uso desse tipo de lista
vinculada fornece a maior flexibilidade.

Movendo-se por uma Lista Vinculada


O movimento através de uma lista vinculada ocorre linearmente.Você visita um
elemento, segue o próximo ponteiro e visita o próximo elemento. Enxaguar e
repetir. Esse é o método mais fácil de percorrer uma lista vinculada e aquele
para o qual as listas vinculadas são mais adequadas. As listas vinculadas não
são adequadas para casos de uso em que o acesso aleatório é uma operação
importante. Em vez disso, você usa listas vinculadas ao iterar sobre a lista
inteira é importante e a adição e remoção dinâmicas de elementos é necessária.
Em implementações de listas vinculadas, o primeiro elemento é frequentemente
representado por um ponteiro especial — chamado de head — que permite fácil acesso ao
"início" da lista. Em uma lista não circu-lar, o último elemento é delineado por seu próximo
ponteiro sendo NULL. Em uma lista com vínculo circular, o último elemento é delineado
porque aponta para o elemento head.Percorrer a lista, portanto, ocorre linearmente através
de cada elemento, do primeiro ao último. Em uma lista de dobras vinculadas, o movimento
também pode ocorrer para trás, linearmente a partir do último elemento para o
www.it-ebooks.info
88 Capítulo 6 Estruturas de dados do kernel

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.

Implementação do Linux Kernel


Em comparação com a maioria das implementações de listas vinculadas — incluindo a
abordagem genérica descrita nas seções anteriores — a implementação do kernel do Linux é
exclusiva. Lembre-se da discussão anterior que os dados (ou um agrupamento de dados,
como uma estrutura) são mantidos em uma lista vinculada adicionando um ponteiro de nó
seguinte (e talvez um anterior) aos dados. Por exemplo, suponha que tínhamos uma
estrutura de raposa para descrever esse membro da família Canidae:

O padrão comum para armazenar essa estrutura em uma lista


vinculada é incorporar o ponteiro de lista na estrutura. Por exemplo:

A abordagem do kernel do Linux é diferente. Em vez de transformar a


estrutura em uma lista vinculada, a abordagem Linux é incorporar um nó de
lista vinculada na estrutura!

A Estrutura da Lista Vinculada


Antigamente, havia várias implementações de listas vinculadas no kernel.Uma única e
poderosa implementação de lista vinculada era necessária para remover o código
duplicado. Durante o
2.1 série de desenvolvimento do kernel, a implementação oficial da lista
vinculada do kernel foi introduzida. Todos os usos existentes de listas
vinculadas agora usam a implementação oficial; não rein-vent a roda!
O código de lista vinculada é declarado no arquivo de cabeçalho
<linux/list.h> e a estrutura de dados é simples:

O próximo ponteiro aponta para o próximo nó da lista, e o ponteiro anterior aponta


para o nó da lista anterior.Ainda assim, aparentemente, isso não é particularmente
útil.Que valor é uma lista vinculada gigante...de nós de lista vinculada? O utilitário
mostra como a estrutura list_head é usada:
www.it-ebooks.info
Listas Vinculadas

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.

Usando container_of(), podemos definir uma função simples para retornar


a estrutura pai contendo qualquer list_head:

Armado com list_entry(), o kernel fornece rotinas para criar, manipular


e gerenciar listas vinculadas - tudo sem saber nada sobre as estruturas
em que o list_head reside.

Definindo uma Lista Vinculada


Como mostrado, um list_head por si só não tem valor; ele normalmente é
incorporado dentro de sua própria estrutura:

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

Se a estrutura for criada estaticamente em tempo de compilação e


você tiver uma referência direta a ela, simplesmente faça o seguinte:

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:

Define e inicializa um list_head chamado fox_list.A maioria das rotinas de


lista vinculada aceita um ou dois parâmetros: o nó de cabeçalho ou o nó
de cabeçalho mais um nó de lista real. Vamos dar uma olhada nessas
rotinas.

Manipulação de Listas Vinculadas


O kernel fornece uma família de funções para manipular listas vinculadas.
Todas levam pontos a uma ou mais estruturas list_head.As funções são
implementadas como funções embutidas em C genérico e podem ser
encontradas em <linux/list.h>.
Curiosamente, todas essas funções são O(1).1 Isso significa que elas são
executadas em tempo constante, independentemente do tamanho da lista ou de
qualquer outra entrada. Por exemplo, leva o mesmo tempo para adicionar ou
remover uma entrada de ou para uma lista se essa lista tem 3 ou 3.000
entradas.Isso talvez não seja surpreendente, mas ainda é bom saber.

Adicionando um nó a uma lista vinculada


Para adicionar um nó a uma lista vinculada:

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 adiciona o novo nó à lista fornecida imediatamente após o


nó head.Como a lista é circular e geralmente não tem conceito de primeiro
ou último nós, você pode passar qualquer elemento para head. Entretanto,
se você passar o "último" elemento, esta função poderá ser usada para
implementar uma pilha.
Voltando ao nosso exemplo da fox, suponha que tivéssemos uma
nova struct fox que queríamos adicionar à fox_list list.Fazíamos o seguinte:

Para adicionar um nó ao final de uma lista vinculada:

Esta função adiciona o novo nó à lista fornecida imediatamente antes


do nó head. Como com list_add(), como as listas são circulares, você
geralmente pode passar qualquer elemento para head.Esta função pode ser
usada para implementar uma fila, no entanto, se você passar o "primeiro"
elemento.

Excluindo um nó de uma lista vinculada


Depois de adicionar um nó a uma lista vinculada, excluir um nó de uma
lista é a próxima operação mais importante.Para excluir um nó de uma
lista vinculada, use list_del():

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

Para excluir um nó de uma lista vinculada e reinicializá-lo, o kernel


fornece
list_del_init():

cabeçalho_d Esta função se comporta da mesma forma que list_del(),


a_lista
exceto que também reinicializa a função dada
com o motivo de que você não deseja mais a entrada na lista, mas pode
reutilize a estrutura de dados propriamente dita.

Movendo e dividindo nós de lista vinculada


Para mover um nó de uma lista para outra

Esta função remove a entrada de lista de sua lista vinculada e a adiciona


à lista fornecida após o elemento head.
Para mover um nó de uma lista para o final de outra

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

Retorna diferente de zero se a lista fornecida estiver vazia; caso contrário,


retorna zero.
Para unir duas listas desconectadas

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

Esta função funciona da mesma forma que a list_splice(), exceto que a


lista vazia apontada pela list é reinicializada.

Salvando um par de desreferências


Se você já tiver os próximos e anteriores ponteiros disponíveis, você pode salvar alguns
ciclos (especificamente, as desreferências para obter os ponteiros) chamando as
funções de lista interna diretamente. Cada função discutida anteriormente não faz nada,
exceto encontrar os próximos e os ponteiros anteriores e, em seguida, chamar as funções
internas. As funções internas geralmente têm o mesmo nome que seus wrappers, exceto
que eles são prefixados por sublinhados duplos. Para exam-ple, em vez de call
list_del(list), você pode chamar __list_del(prev, next). Isso é útil somente se os ponteiros
seguinte e anterior já tiverem a referência cancelada. Caso contrário, você está apenas
escrevendo código feio. Consulte o cabeçalho <linux/list.h> para obter as interfaces
exatas.
www.it-ebooks.info
Listas Vinculadas

Percorrendo Listas Vinculadas


Agora você sabe como declarar, inicializar e manipular uma lista vinculada no
kernel.Isso está tudo muito bem e bom, mas não tem sentido se você não tem como
acessar seus dados! As listas vinculadas são apenas contêineres que contêm seus
dados importantes; você precisa de uma maneira de usar listas para se mover e
acessar as estruturas reais que contêm os dados.O kernel (graças a Deus) fornece
um bom conjunto de interfaces para percorrer listas vinculadas e referenciar as
estruturas de dados que as incluem.
Note que, ao contrário das rotinas de manipulação de lista, iterar sobre
uma lista vinculada em sua totalidade é claramente uma operação O(n),
para entradas na lista.
cabeçalho_d
a_lista
A abordagem básica
A maneira mais básica de iterar sobre uma lista é com a macro list_for_each(). A macro
usa dois parâmetros, ambas as estruturas list_head. A primeira é um ponteiro
usado para apontar para a entrada atual; é uma variável temporária que você deve
fornecer. A segunda é a
atuando como o nó head da lista que você deseja percorrer (consulte a seção anterior, "Cabeçalhos de lista"). Em cada iteração do
loop, o primeiro parâmetro aponta para a próxima entrada na lista, até que cada entrada tenha sido visitada. O uso é o seguinte:

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

Aqui, pos é um ponteiro para o objeto que contém os nós


list_head.Pense nele como o valor de retorno de list_entry(). head é um ponteiro
para o nó list_head a partir do qual você deseja começar a iterar—em nosso exemplo anterior,
fox_list. member é o nome vari-able da list_head structure in list_head—isto soa como confus-
ing, mas é fácil de usar. Aqui está como nós reescreveríamos a list_for_each() para
iterar sobre cada nó fox:

Agora vamos ver um exemplo real, do inotify, o sistema de notificação do sistema de


arquivos do kernel:

Esta função percorre todas as entradas na lista inode->inotify_watches. Cada


é do tipo struct inotify_watch e o list_head nessa estrutura é nomeado
i_list.With
cada iteração do loop, watch aponta para um novo nó na lista.A finalidade
desta função simples é pesquisar a lista inotify_watches no inode struc-
Tente localizar uma entrada inotify_watch cuja inotify_handle corresponda ao
identificador fornecido.

Iterando através de uma lista para trás


A macro list_for_each_entry_reverse() funciona exatamente como
list_for_each_entry(), exceto que ela se move pela lista em reverse.Isto é, ao
invés de seguir os pointers adiante através da lista, ela segue os pointers anteriores.
Uso é o mesmo que com list_for_each_entry():

Há apenas um punhado de razões a favor de se mover através de uma lista em sentido


inverso. Um é o desempenho: se você sabe que o item que você está procurando está
provavelmente atrás do nó do qual você está iniciando sua pesquisa, você pode voltar atrás
na esperança de encontrá-lo mais cedo.Um segundo motivo é se a encomenda é importante.
Por exemplo, se você usar uma lista vinculada como uma pilha, poderá deslocar a lista da
cauda para trás para obter a ordenação last-in/first-out (LIFO). Se você fizer
www.it-ebooks.info
Listas Vinculadas

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:

Esta função percorre e remove todas as entradas na lista inotify_watches.


Se o padrão list_for_each_entry() fosse usado, este código introduziria um
bug use-after-free, já que mover para o próximo item da lista exigiria
acessar o watch, que foi destruído.
Se você precisar iterar sobre uma lista vinculada em elementos reversos e
potencialmente removidos, a
kernel fornece list_for_each_entry_safe_reverse():

Uso é o mesmo que com list_for_each_entry_safe().


www.it-ebooks.info
96 Capítulo 6 Estruturas de dados do kernel

Você Ainda Pode Precisar De Travamento!


As variantes "seguras" de list_for_each_entry() protegem você somente contra
remoções da lista dentro do corpo do loop. Se houver uma chance de remoções
simultâneas de outro código—ou qualquer outra forma de manipulação de lista
simultânea—você precisará bloquear o acesso à lista adequadamente.
Consulte os Capítulos 9, "An Introduction to Kernel Synchronization" (Introdução à
sincronização do kernel) e o Capítulo 10, "Kernel Syn-chronization Methods" (Métodos
de sincronização do kernel) para obter uma discussão sobre sincronização e bloqueio.

Outros Métodos de Lista Vinculada


O Linux fornece uma infinidade de outros métodos de lista, permitindo
aparentemente todas as formas concebíveis de acessar e manipular uma lista
vinculada. Todos esses métodos são definidos no arquivo de cabeçalho
<linux/list.h>.

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

Figura 6.5 Uma fila (FIFO).


www.it-ebooks.info
Filas

97º

A implementação de fila genérica do kernel do Linux é chamada kfifo e é


implementada em kernel/kfifo.c e declarada em <linux/kfifo.h>.Esta seção discute a
API após uma atualização deAPI na versão 2.6.33. O uso é um pouco diferente nas
versões do kernel anteriores à 2.6.33—verifique <linux/kfifo.h> antes de escrever o
código.

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.

Criando uma Fila


Para usar um kfifo, você deve primeiro defini-lo e inicializá-lo. Como na maioria
dos objetos do kernel, você pode fazer isso de forma dinâmica ou estática.O
método mais comum é o dinâmico:

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

Esta função cria e inicializa um kfifo que usará o tamanho em bytes


da memória apontada pelo buffer para sua fila.Com ambos kfifo_alloc() e
kfifo_init(), o tamanho deve ser a potência de dois.
Declarar estaticamente um kfifo é mais simples, mas menos comum:

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

Isto funciona da mesma forma que o kfifo_out(), exceto que o


deslocamento de saída não é incrementado, e assim os dados
desenfileirados estão disponíveis para ler em uma chamada subsequente
para kfifo_out().O parâmetro offsetout especifica um índice na fila; especifique
zero para ler do cabeçalho da fila, como kfifo_out() faz.

Obtendo o Tamanho de uma Fila


Para obter o tamanho total em bytes do buffer usado para armazenar a fila de um
kfifo, chame
kfifo_ size():
www.it-ebooks.info
Filas

99º

Em outro exemplo de nomenclatura de kernel horrível, use kfifo_len() para


obter o número de bytes enfileirados em um kfifo:

Para descobrir o número de bytes disponíveis para escrever em um kfifo,


chame kfifo_avail():

Finalmente, kfifo_is_empty() e kfifo_is_full() retorna nonzero se o kfifo


fornecido está vazio ou cheio, respectivamente, e zero se não estiver:

Redefinindo e Destruindo a Fila


Para reiniciar um kfifo, descartando todo o conteúdo da fila, chame kfifo_reset():

Para destruir um kfifo alocado com o kfifo_ alloc (), chame o kfifo_ free ():

Se você criou seu kfifo com kfifo_init(), é sua responsabilidade liberar o


buffer associado. Como você faz isso depende de como você o criou.
Consulte o Capítulo 12 para descompactar a alocação e a liberação de
memória dinâmica.

Exemplo de Uso da Fila


Com essas interfaces sob nosso controle, vamos dar uma olhada em um exemplo
simples de uso de um kfifo. Suponha que criamos um kfifo apontado por fifo com um
tamanho de fila de 8 KB.Agora podemos enfileirar dados na fila. Neste exemplo,
enfileiramos inteiros simples. Em seu próprio código, você provavelmente
enfileirará estruturas mais complicadas e específicas de tarefas. Usando inteiros
neste exemplo, vamos ver exatamente como o kfifo funciona:

O kfifo chamado fifo agora contém de 0 a 31, inclusive.Podemos dar


uma olhada no primeiro item da fila e verificar se é 0:
www.it-ebooks.info
100 Capítulo 6 Estruturas de dados do kernel

Para desenfileirar e imprimir todos os itens no kfifo, podemos usar


kfifo_out():

Imprime de 0 a 31, inclusive, e nessa ordem. (Se este trecho de código


imprimisse os números de trás para frente, de 31 para 0, teríamos uma pilha, não
uma fila.)

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

Você também pode gostar