Explicação detalhada do kernel do Linux e do esquema de otimização do kernel

1. História do Linux

1, Unix

A relação entre UNIX e Linux é um tópico interessante. Entre os atuais sistemas operacionais do lado do servidor, o UNIX nasceu no final da década de 1960, o Windows nasceu em meados da década de 1980 e o Linux nasceu no início da década de 1990. Pode-se dizer que o UNIX é o "irmão mais velho" no O Windows e o Linux referenciaram posteriormente o UNIX.

O sistema operacional UNIX foi inventado por Ken Thompson e Dennis Ritchie. Parte de suas origens técnicas pode ser rastreada até o programa de engenharia Multics iniciado em 1965, iniciado pela Bell Labs, o Massachusetts Institute of Technology e a General Electric Company com o objetivo de desenvolver um programa interativo e multiprogramado. sistema operacional de compartilhamento capaz de substituir o sistema operacional em lote que era amplamente utilizado na época.

Descrição: O sistema operacional de compartilhamento de tempo permite que um computador atenda vários usuários ao mesmo tempo, e os usuários do terminal conectado ao computador emitem comandos de forma interativa. O tempo de CPU é dividido em vários segmentos, chamados de fatias de tempo). O sistema operacional usa fatias de tempo como uma unidade e atende a cada usuário final, uma fatia de cada vez.

Infelizmente, o projeto Multics era tão grande e complexo que seus desenvolvedores não tinham ideia de como seria, e acabou fracassando.

Os pesquisadores do Bell Labs liderados por Ken Thompson aprenderam as lições do fracasso do projeto Multics e realizaram o protótipo de um sistema operacional de compartilhamento de tempo em 1969, que foi oficialmente chamado de UNIX em 1970.

Curiosamente, a intenção original de Ken Thompson para desenvolver o UNIX era rodar um jogo de computador que ele escreveu, Space Travel, que simula o movimento de corpos celestes no sistema solar, com jogadores pilotando naves espaciais, vendo o cenário e tentando pousar em vários planetas e luas. . Ele testou sucessivamente em vários sistemas, mas os resultados não foram satisfatórios, então decidiu desenvolver seu próprio sistema operacional, e assim nasceu o UNIX.

Desde 1970, o sistema UNIX tornou-se gradualmente popular entre os programadores dentro do Bell Labs. Em 1971-1972, o colega de Ken Thompson, Dennis Ritchie, inventou a lendária linguagem C, uma linguagem de alto nível adequada para escrever software de sistema. Seu nascimento foi um marco importante no desenvolvimento de sistemas UNIX. No desenvolvimento de sistemas operacionais, a linguagem assembly é não é mais a língua dominante.

Em 1973, a maior parte do código-fonte do sistema UNIX foi reescrito em linguagem C, o que lançou as bases para melhorar a portabilidade do sistema UNIX (os sistemas operacionais anteriores usavam principalmente a linguagem assembly, que era altamente dependente do hardware). condições para melhorar a eficiência do desenvolvimento do software do sistema. Pode-se dizer que o sistema UNIX e a linguagem C são irmãos gêmeos e têm uma relação inseparável.

No início da década de 1970, houve outra grande invenção no mundo da computação - o protocolo TCP/IP, que era um protocolo de rede desenvolvido depois que o Departamento de Defesa dos EUA assumiu o controle da ARPAnet. O Departamento de Defesa dos EUA juntou o protocolo TCP/IP com o sistema UNIX e a linguagem C, e recebeu uma licença não comercial da AT&T para várias universidades nos Estados Unidos, que deu início ao desenvolvimento do sistema UNIX, o C linguagem e o protocolo TCP/IP, que influenciaram os três campos do sistema operacional, linguagem de programação e protocolo de rede, respectivamente. Ken Thompson e Dennis Ritchie receberam o Turing Award, o maior prêmio em ciência da computação, em 1983 por suas contribuições extraordinárias para o campo da computação.

Posteriormente, surgiram várias versões de sistemas UNIX, como Sun Solaris, FreeBSD, IBM AIX, HP-UX e assim por diante.

2. Solaris e FreeBSD

Solaris é um ramo importante do sistema UNIX. O Solaris pode ser executado em plataformas de CPU x86 além das plataformas de CPU SPARC. No mercado de servidores, a plataforma de hardware da Sun tem alta disponibilidade e alta confiabilidade e é o sistema UNIX dominante no mercado. Solaris x86 é um servidor usado para aplicativos de produção reais.O Solaris x86 pode ser usado gratuitamente para estudo, pesquisa ou aplicativos comerciais, sujeito aos termos de licença relevantes da Sun.

O FreeBSD originado da versão UNIX desenvolvida pela Universidade da Califórnia, Berkeley, é desenvolvido e mantido por voluntários de todo o mundo, oferecendo diferentes níveis de suporte para sistemas computacionais com diferentes arquiteturas. O FreeBSD é lançado sob o contrato de licença BSD, que permite que qualquer pessoa use e distribua livremente com a premissa de reter informações de direitos autorais e contrato de licença, e não restringe a distribuição do código FreeBSD sob outro contrato, para que empresas comerciais possam integrar livremente o código FreeBSD em em seus produtos. O OS X da Apple é um sistema operacional baseado no FreeBSD.

Uma parte considerável dos grupos de usuários do FreeBSD e do Linux se sobrepõem, os ambientes de hardware suportados pelos dois também são relativamente consistentes, e o software utilizado é relativamente semelhante. A maior característica do FreeBSD é a estabilidade e eficiência, e é uma boa escolha como sistema operacional de servidor; porém, seu suporte de hardware não é tão completo quanto o do Linux, por isso não é adequado como sistema desktop.

3. O nascimento do Linux

O kernel Linux foi originalmente escrito por Linus Torvalds como um hobby quando ele era estudante na Universidade de Helsinque. Naquela época, ele achava que o Minix, uma mini versão do sistema operacional UNIX para ensino, era muito difícil de usar, então ele decidiu desenvolver seu próprio sistema operacional. A versão 1 foi lançada em setembro de 1991 com apenas 10.000 linhas de código.

Linus Torvalds não manteve os direitos autorais do código-fonte do Linux, tornou o código público e convidou outros a melhorarem o Linux juntos. Ao contrário do Windows e de outros sistemas operacionais proprietários, o Linux é de código aberto e gratuito para qualquer pessoa usar.

Estima-se que apenas 2% do código do kernel Linux seja agora escrito pelo próprio Linus Torvalds, embora ele ainda possua o kernel Linux (a parte central do sistema operacional) e retenha novas formas de selecionar novo código e precisar mesclar o kernel final direito de governar. O Linux que todo mundo usa agora, prefiro dizer que foi desenvolvido em conjunto por Linus Torvalds e muitos especialistas em Linux que se juntaram depois.

O software de código aberto é um modelo diferente do software comercial. Literalmente, significa código de código aberto. Você não precisa se preocupar com nenhum truque nele, o que trará inovação e segurança de software.

 O Linux é muito popular entre os entusiastas de computadores por dois motivos principais:

  1. Pertence ao software de código aberto, os usuários podem obtê-lo e seu código-fonte sem pagar uma taxa, e podem fazer as modificações necessárias de acordo com suas próprias necessidades, usá-lo gratuitamente e continuar a difundi-lo sem restrições;
  2. Ele tem todos os recursos do UNIX, e qualquer pessoa que use o sistema operacional UNIX ou queira aprender o sistema operacional UNIX pode se beneficiar do Linux.

Além disso, o código aberto não é realmente equivalente ao gratuito, mas um novo modelo de lucro de software. Atualmente, muitos softwares são softwares de código aberto, que têm um impacto profundo na indústria de computadores e na Internet.

2. Introdução ao kernel do Linux

1. A composição do sistema de computador

Um sistema de computador é uma simbiose de hardware e software, que são interdependentes e inseparáveis.

O hardware do computador, incluindo periféricos, processadores, memória, discos rígidos e outros dispositivos eletrônicos, constitui o motor do computador, mas sem software para operá-lo e controlá-lo, ele não pode funcionar sozinho.

O software que completa esse trabalho de controle é chamado de sistema operacional. O sistema operacional é o software do sistema que gerencia os recursos de hardware e software do computador e também é o núcleo e a pedra angular do sistema de computador. O sistema operacional precisa lidar com tarefas básicas, como gerenciar e configurar a memória, priorizar o fornecimento e a demanda de recursos do sistema, controlar dispositivos de entrada e saída, operar a rede e gerenciar o sistema de arquivos. O sistema operacional também fornece uma interface para o usuário interagir com o sistema.

A composição do sistema operacional:

Bootloader: É o principal responsável pelo processo de boot do dispositivo.

Shell: Shell é uma linguagem de programação que pode controlar outros arquivos, processos e todos os outros programas.

Kernel: É o principal componente do sistema operacional, gerenciando memória, CPU e outros componentes relacionados.

Ambiente de Trabalho: Este é o ambiente com o qual os usuários normalmente interagem.

Servidor gráfico: É um subsistema do sistema operacional que exibe gráficos na tela

Aplicativos: São assemblies que executam diferentes tarefas do usuário, como word, excel, etc.

· Daemons: provedores de serviços de backend.

2. O que é o Kernel?

O kernel é uma parte fundamental do sistema operacional porque controla todos os programas do sistema. Ele atua como uma ponte entre aplicativos e processamento de dados no nível de hardware com a ajuda de comunicação entre processos e chamadas de sistema.

Quando o dispositivo é inicializado, o sistema operacional é carregado na memória, momento em que o kernel passa por um processo de inicialização que cuida da parte de alocação de memória e a mantém lá até que o sistema operacional seja desligado. E cria um ambiente para executar aplicativos, onde o kernel cuida de tarefas de baixo nível, como gerenciamento de tarefas, gerenciamento de memória, gerenciamento de riscos, etc.

O kernel atua como um provedor de serviços, de modo que um programa pode solicitar ao kernel que execute várias tarefas, como solicitar o uso de um disco, placa de rede ou outro hardware, e o kernel define interrupções para a CPU habilitar a multitarefa. Ele protege o ambiente de computação impedindo que programas incorretos entrem nas funções operacionais de outros programas. Bloqueia a entrada de programas não autorizados, não permitindo espaço de armazenamento e limita a quantidade de dinheiro que consomem

tempo de CPU.

resumidamente:

1. Do ponto de vista técnico, o kernel é uma camada intermediária entre hardware e software. A função é passar a solicitação da camada de aplicação para o hardware e atuar como o driver subjacente para endereçar vários dispositivos e componentes no sistema.

2. No nível do aplicativo, o aplicativo não tem conexão com o hardware, mas apenas com o kernel, que é a camada mais baixa na hierarquia que o aplicativo conhece. Na prática, o kernel abstrai os detalhes relevantes.

3. O kernel é um gerenciador de recursos. Responsável por alocar recursos compartilhados disponíveis (tempo de CPU, espaço em disco, conexão de rede, etc.) para vários processos do sistema.

4. O kernel é como uma biblioteca que fornece um conjunto de comandos orientados ao sistema. Chamadas de sistema são como chamar funções comuns para um aplicativo.

3. Classificação de kernels

Existem geralmente três categorias de kernels:

1. Kernel monolítico: contém vários drivers de dispositivo que criam uma interface de comunicação entre o hardware e o software de um dispositivo.

É o kernel amplamente utilizado pelos sistemas operacionais. Em uma arquitetura monolítica, o kernel consiste em vários módulos que podem ser carregados e descarregados dinamicamente. Essa arquitetura estenderá a funcionalidade do sistema operacional e permitirá uma fácil extensão do kernel.

Com uma arquitetura monolítica, a manutenção do kernel é facilitada porque permite que módulos relacionados sejam carregados e descarregados quando um bug em um módulo específico precisa ser corrigido. Assim, ele remove o trabalho tedioso de baixar e recompilar todo o kernel para pequenas alterações. Em um kernel monolítico, é mais fácil descarregar módulos que não são mais usados.

2. Micro kernel: Só pode executar funções básicas.

Os microkernels foram desenvolvidos como uma alternativa aos kernels monolíticos para resolver o crescente problema do código do kernel que os kernels monolíticos não conseguem. Essa arquitetura permite que determinados serviços básicos (como pilhas de protocolo, gerenciamento de driver de dispositivo, sistemas de arquivos etc.) sejam executados no espaço do usuário. Isso aprimora a funcionalidade do sistema operacional com código mínimo, melhora a segurança e garante estabilidade.

Limita os danos às áreas afetadas, permitindo que o resto do sistema funcione sem qualquer interrupção. Em uma arquitetura de microkernel, todos os serviços essenciais do sistema operacional estão disponíveis para programas por meio de comunicação entre processos (IPC). Microkernels permitem interação direta entre drivers de dispositivo e hardware.

3. Kernel híbrido: Combina vários aspectos do kernel monolítico e do microkernel.

Um kernel híbrido pode decidir o que executar no modo de usuário e no modo de supervisor. Normalmente, em um ambiente de kernel misto, coisas como drivers de dispositivo, E/S do sistema de arquivos serão executadas no modo de usuário, enquanto as chamadas de servidor e IPC permanecem no modo de supervisor.

4. Gênero de design do kernel

1. Micronúcleo. As funções mais básicas são implementadas por um kernel central (microkernel). O Windows NT usa a arquitetura microkernel. Para as características da arquitetura do microkernel, a parte central do sistema operacional é um pequeno kernel que implementa alguns serviços básicos, como criar e excluir processos, gerenciamento de memória, gerenciamento de interrupções e assim por diante. As outras partes do sistema de arquivos, protocolos de rede, etc., todos são executados no espaço do usuário fora do microkernel. Essas funções são delegadas a alguns processos independentes que se comunicam com o kernel central por meio de interfaces de comunicação bem definidas.

2. Núcleo de macro (único). Todo o código para o kernel, incluindo subsistemas (como gerenciamento de memória, gerenciamento de arquivos, drivers de dispositivo) é empacotado em um único arquivo. Todas as funções são feitas juntas, todas no kernel, ou seja, todo o kernel é um único programa muito grande. Cada função no kernel tem acesso a todas as outras partes do kernel. Atualmente, o carregamento e descarregamento dinâmico (corte) de módulos é suportado, e o kernel Linux é implementado com base nessa estratégia.

Um sistema operacional que usa um microkernel tem boa escalabilidade e o kernel é muito pequeno, mas esse sistema operacional é ineficiente devido ao custo da passagem de mensagens entre diferentes camadas. Para um sistema operacional de arquitetura única, todos os módulos são integrados juntos, a velocidade e o desempenho do sistema são bons, mas a escalabilidade e a manutenção são relativamente ruins.

Logicamente falando, a estrutura de microkernel do Linux é implementada, mas não é, o Linux é uma estrutura de kernel único (monolítica). Isso significa que, embora o Linux seja dividido em vários subsistemas que controlam vários componentes do sistema (como gerenciamento de memória e gerenciamento de processos), todos os subsistemas são totalmente integrados para formar o kernel inteiro.

Em contraste, um sistema operacional de microkernel fornece um conjunto mínimo de funções, e todas as outras camadas do sistema operacional são executadas por processo no topo do microkernel. Os sistemas operacionais de microkernel são geralmente menos eficientes devido à passagem de mensagens entre as camadas, mas esses sistemas operacionais são muito escaláveis.

Fundamentalmente falando, é uma das filosofias de design do linux decompor uma coisa em pequenos problemas, e então cada pequeno problema é responsável apenas por uma tarefa.O kernel do Linux pode ser estendido através de módulos.

Um módulo é um programa que é executado no espaço do kernel, que na verdade é um tipo de arquivo objeto-objeto. Ele não está vinculado e não pode ser executado independentemente, mas seu código pode ser vinculado ao sistema em tempo de execução para ser executado como parte do kernel ou retirado do kernel, para que possa ser estendido dinamicamente a funcionalidade do kernel.

Esse código de objeto geralmente consiste em um conjunto de funções e estruturas de dados usadas para implementar um sistema de arquivos, um driver ou outra funcionalidade do kernel superior. O nome completo do mecanismo do módulo deve ser um módulo de kernel carregável dinamicamente (Loadable Kernel Module) ou LKM, geralmente chamado de módulo. Ao contrário dos processos mencionados acima em execução no espaço de usuário externo do sistema operacional do sistema microkernel, o módulo não é executado como um processo, mas como outras funções do kernel vinculadas estaticamente, ele é executado em nome do processo atual no modo kernel. Devido à introdução do mecanismo do módulo, o kernel do Linux pode ser minimizado, ou seja, algumas funções básicas são implementadas no kernel, como a interface do módulo para o kernel, a forma como o kernel gerencia todos os módulos, etc., e a escalabilidade do sistema é deixada para o módulo completar.

Os módulos têm recursos de kernel que fornecem os benefícios de um microkernel sem a sobrecarga extra.

5. Funções do kernel

O kernel Linux implementa muitas propriedades arquiteturais importantes. Em um nível superior ou inferior, o kernel é dividido em subsistemas.

O Linux também pode ser visto como um todo, pois integra todos esses serviços básicos ao kernel. Isso é diferente da arquitetura de microkernel, que fornece alguns serviços básicos, como comunicação, E/S, memória e gerenciamento de processos, e serviços mais específicos são conectados à camada de microkernel.

As principais tarefas do kernel são:

· Gestão de processos para execução de aplicações.

· Gerenciamento de memória e E/S (entrada/saída).

· Controle de chamadas do sistema (comportamento central do kernel).

· Gerenciamento de dispositivos com drivers de dispositivos.

· Fornecer um ambiente de execução para o aplicativo.

6. As principais funções do kernel

As principais funções do kernel Linux são: gerenciamento de armazenamento, gerenciamento de CPU e processos , sistema de arquivos, gerenciamento de dispositivos e driver, comunicação de rede e inicialização do sistema (boot), chamadas de sistema, etc. 

As principais funções são as seguintes:

  • Gerenciamento de memória do sistema
  • gerenciamento de programas de software
  • gerenciamento de dispositivos de hardware
  • gerenciamento de sistema de arquivos

1) Gerenciamento de memória do sistema

Uma das principais funções do kernel do sistema operacional é o gerenciamento de memória. O kernel gerencia não apenas a memória física disponível no servidor, mas também cria e gerencia a memória virtual (ou seja, memória que na verdade não existe).

O kernel implementa a memória virtual através do espaço de armazenamento no disco rígido, que é chamado de espaço de troca. O kernel constantemente troca o conteúdo da memória virtual entre o espaço de troca e a memória física real. Isso faz com que o sistema pense que tem mais memória disponível do que a memória física.

Uma unidade de armazenamento de memória é dividida em grupos em blocos chamados páginas. O kernel coloca cada página de memória na memória física ou espaço de troca. O kernel então mantém uma tabela de páginas de memória indicando quais páginas estão na memória física e quais páginas são trocadas para o disco.

2) Gerenciamento de programas de software

O sistema operacional Linux refere-se à execução de programas como processos. Os processos podem ser executados em primeiro plano, exibindo a saída na tela, ou em segundo plano, escondendo-se nos bastidores. O kernel controla como o sistema Linux gerencia todos os processos em execução no sistema.

O kernel cria o primeiro processo (chamado de processo init) para iniciar todos os outros processos no sistema. Quando o kernel é iniciado, ele
carrega o processo init na memória virtual. Quando o kernel inicia qualquer outro processo, ele aloca uma área dedicada na memória virtual para o novo processo armazenar os dados e o código usado por esse processo.

Algumas distribuições Linux usam uma tabela para gerenciar processos para iniciar automaticamente quando o sistema é ligado. Em sistemas Linux, esta tabela geralmente está localizada no arquivo especial /etc/inittab.

Outros sistemas (como a popular distribuição Ubuntu Linux) usam o diretório /etc/init.d, onde
são colocados os scripts que iniciam ou param um aplicativo na inicialização. Esses scripts são iniciados por meio de entradas no diretório /etc/rcX.d, onde X significa nível de execução.

O sistema init do sistema operacional Linux adota o nível de execução. O nível de execução determina que o processo init execute determinados tipos de processos definidos no arquivo /etc/inittab ou no diretório /etc/rcX.d. O sistema operacional Linux possui 5 níveis de execução de inicialização.

  • No nível de execução 1, apenas os processos básicos do sistema e um processo do terminal do console são iniciados. Nós o chamamos de modo de usuário único. O modo de usuário único é normalmente usado para realizar manutenção urgente do sistema de arquivos quando há um problema com o sistema. Obviamente, neste modo, apenas uma pessoa (geralmente o administrador do sistema) pode fazer logon no sistema para manipular os dados.
  • O nível de execução de inicialização padrão é 3. Nesse nível de execução, a maioria dos aplicativos, como programas de suporte de rede, é iniciada. Outro runlevel comum no Linux é o 5. Nesse nível de execução, o sistema inicia o X Window System gráfico, permitindo que os usuários efetuem login no sistema por meio de uma janela gráfica da área de trabalho.

Você pode usar o comando ps para visualizar os processos atualmente em execução em um sistema Linux.

3) Gerenciamento de dispositivos de hardware

Outra responsabilidade do kernel é gerenciar dispositivos de hardware. Qualquer dispositivo com o qual um sistema Linux precise se comunicar precisa
incluir seu código de driver no código do kernel. O código do driver atua como um intermediário entre o aplicativo e o dispositivo de hardware, permitindo que os dados sejam trocados entre o kernel e o dispositivo. Existem dois métodos para inserir o código do driver de dispositivo no kernel do Linux:

  • Código do driver de dispositivo compilado no kernel
  • módulo de driver de dispositivo conectável ao kernel

Anteriormente, a única maneira de inserir o código do driver de dispositivo era recompilar o kernel. Cada vez que um novo dispositivo é adicionado ao sistema, o código do kernel deve ser recompilado. Como o kernel Linux suporta cada vez mais dispositivos de hardware, esse processo se torna cada vez mais ineficiente. Felizmente, os desenvolvedores do Linux criaram uma maneira melhor de inserir o código do driver em um kernel em execução.

Os desenvolvedores criaram o conceito de módulos do kernel. Ele permite que o código do driver seja inserido em um kernel em execução sem recompilar o
kernel. Ao mesmo tempo, os módulos do kernel também podem ser removidos do kernel quando o dispositivo não estiver mais em uso. Essa abordagem simplifica muito e expande o uso de dispositivos de hardware no Linux.

O sistema Linux trata os dispositivos de hardware como arquivos especiais chamados de arquivos de dispositivos. Existem 3 categorias de arquivos de dispositivo:

  • Arquivo de dispositivo de caractere: Refere-se a dispositivos que processam dados um caractere por vez, como a maioria dos tipos de modems e
    terminais.
  • Arquivo de dispositivo de bloco: Refere-se a um dispositivo que pode processar grandes blocos de dados sempre que processa dados, como um disco rígido.
  • Arquivo de dispositivo de rede: refere-se ao dispositivo que usa pacotes de dados para enviar e receber dados, incluindo várias placas de rede e um dispositivo de loopback especial.

4) Gerenciamento do sistema de arquivos

Ao contrário de alguns outros sistemas operacionais, o kernel do Linux suporta leitura e gravação de dados do disco rígido por meio de diferentes tipos de sistemas de arquivos. Além
de seus próprios sistemas de arquivos, o Linux também suporta
leitura e gravação de dados dos sistemas de arquivos de outros sistemas operacionais (como o Microsoft Windows). O kernel deve ser compilado com suporte para todos os sistemas de arquivos possíveis. A tabela a seguir lista os sistemas de arquivos padrão que os sistemas Linux usam para ler e gravar dados.

Todos os discos rígidos acessados ​​pelo servidor Linux devem ser formatados com um dos tipos de sistema de arquivos listados na tabela acima.

O kernel Linux é altamente eficiente no uso de memória e CPU e é muito estável ao longo do tempo. Mas o que é mais interessante no Linux é a portabilidade desse tamanho e complexidade. O Linux é compilado para rodar em um grande número de processadores e plataformas com diferentes restrições e requisitos de arquitetura. Um exemplo é que o Linux pode ser executado em um processador com uma unidade de gerenciamento de memória (MMU), ou naqueles processadores que não fornecem uma MMU. A porta uClinux do kernel Linux fornece suporte para não-MMUs.

Terceiro, a arquitetura geral do kernel Linux

1. Arquitetura do Kernel Linux 

O sistema UNIX/Linux pode ser resumido em três níveis, a camada inferior é o kernel do sistema (Kernel); a camada intermediária é a camada Shell, ou seja, a camada de interpretação de comandos; a camada superior é a camada de aplicação.

(1) Camada do kernel

A camada kernel é o núcleo e a base do sistema UNIX/Linux. Ela está diretamente ligada à plataforma de hardware, controla e gerencia vários recursos (recursos de hardware e recursos de software) no sistema e organiza efetivamente a operação do processo, expandir as funções do hardware.Melhorar a eficiência de utilização dos recursos e fornecer aos usuários um ambiente de aplicação conveniente, eficiente, seguro e confiável.

(2) Camada de casca

A camada Shell é a interface que interage diretamente com o usuário. O usuário pode inserir a linha de comando no prompt, e o Shell a interpreta e gera os resultados correspondentes ou informações relacionadas, então também chamamos o Shell de interpretador de comandos, que pode concluir várias tarefas de maneira rápida e fácil usando os comandos avançados fornecidos por o sistema.

(3) Camada de aplicação

A camada de aplicação fornece um ambiente gráfico baseado no protocolo X Window. O X Window Protocol define as funções que um sistema deve ter.

O kernel Linux é apenas uma parte do sistema operacional Linux. Certo, ele gerencia todos os dispositivos de hardware do sistema; certo, ele fornece interfaces para Library Routine (como biblioteca C) ou outras aplicações através de chamadas de sistema.

1) Espaço do kernel:

O espaço do kernel inclui chamadas de sistema, o kernel e o código relacionado à arquitetura da plataforma. O kernel está em um estado de sistema elevado que inclui um espaço de memória protegido e acesso total ao hardware do dispositivo. Esse estado do sistema e espaço de memória são chamados coletivamente de espaço do kernel. No espaço do kernel, o acesso central ao hardware e aos serviços do sistema é gerenciado e fornecido como serviços para o restante do sistema.

2) Espaço do usuário:

O espaço do usuário também contém, o aplicativo do usuário, a biblioteca C. O espaço do usuário ou domínio do usuário é o código que é executado fora do ambiente do kernel do sistema operacional, o espaço do usuário é definido como os vários aplicativos ou programas ou bibliotecas usados ​​pelo sistema operacional para fazer interface com o kernel.

Os aplicativos do usuário são executados no espaço do usuário e podem acessar uma parte dos recursos disponíveis do computador por meio de chamadas de sistema do kernel. Usando os serviços principais fornecidos pelo kernel, aplicativos de nível de usuário, como jogos ou software de escritório, podem ser criados.

O kernel fornece um conjunto de interfaces para aplicativos executados no modo de usuário para interagir com o sistema. Também conhecidas como chamadas de sistema, essas interfaces permitem que aplicativos acessem hardware e outros recursos do kernel. As chamadas de sistema não apenas fornecem um nível de hardware abstrato para aplicativos, mas também garantem a segurança e a estabilidade do sistema.

A maioria dos aplicativos não usa chamadas de sistema diretamente. Em vez disso, uma interface de programação de aplicativos (API) é usada para programação. Observe que não há correlação entre a API e as chamadas do sistema. As APIs são fornecidas aos aplicativos como parte de um arquivo de biblioteca e essas APIs geralmente são implementadas por meio de uma ou mais chamadas do sistema.

2. Arquitetura do kernel do Linux

Para gerenciar os vários recursos e dispositivos acima, o kernel Linux propõe a seguinte arquitetura: 

De acordo com as funções centrais do kernel, o kernel Linux propõe 5 subsistemas:

1. Process Scheduler, também conhecido como gerenciamento de processos, escalonamento de processos. Responsável por gerenciar os recursos da CPU para que cada processo possa acessar a CPU da maneira mais justa possível.

2. Gerenciador de Memória, gerenciamento de memória. Responsável por gerenciar os recursos de Memória para que os processos possam compartilhar com segurança os recursos de memória da máquina. Além disso, o gerenciamento de memória fornecerá um mecanismo de memória virtual, que permite que o processo use mais memória do que o sistema pode usar. A memória não utilizada será armazenada na memória não volátil externa através do sistema de arquivos, e quando for necessário usado, ele será recuperado na memória.

3. VFS (Virtual File System), sistema de arquivos virtual. O kernel Linux abstrai dispositivos externos com diferentes funções, como dispositivos de disco (discos rígidos, discos, NAND Flash, Nor Flash, etc.), dispositivos de entrada e saída, dispositivos de exibição, etc., em uma interface de operação de arquivo unificada (aberta, fechar, ler, etc.) escrever, etc.) para acessar. Esta é a personificação de "tudo é um arquivo" no sistema Linux (na verdade, o Linux não faz isso completamente, porque CPU, memória, rede, etc. ainda não são arquivos. Se você realmente precisa que tudo seja um arquivo, depende do que a Bell Labs está desenvolvendo.” “ Plano 9 ”).

4. Rede, subsistema de rede. Responsável por gerenciar os equipamentos de rede do sistema e implementar uma variedade de padrões de rede.

5. IPC (Inter-Process Communication), comunicação entre processos. O IPC não gerencia nenhum hardware, é o principal responsável pela comunicação entre os processos no sistema Linux.

Agendador de processos

O escalonamento de processos é o subsistema mais importante no kernel do Linux, que fornece principalmente controle de acesso à CPU. Como no computador, os recursos da CPU são limitados e muitos aplicativos usam os recursos da CPU, portanto, o "subsistema de agendamento de processos" é necessário para agendar e gerenciar a CPU.

O subsistema de escalonamento de processos inclui 4 submódulos (veja a figura abaixo), e suas funções são as seguintes:

  1. Scheduling Policy, a estratégia de implementação do escalonamento de processos, que determina quais (ou quais) processos terão a CPU.
  2. Os Schedulers específicos da arquitetura, a parte relacionada à arquitetura, são usados ​​para abstrair o controle de diferentes CPUs em uma interface unificada. Esses controles são usados ​​principalmente em processos de suspensão e retomada, envolvendo acesso a registradores de CPU, operações de instruções de montagem e assim por diante.
  3. Agendador independente de arquitetura, a parte independente de arquitetura. Ele se comunicará com o "módulo de política de agendamento" para decidir qual processo executar em seguida e, em seguida, retomará o processo especificado por meio do "módulo Agendadores específicos da arquitetura".
  4. Interface de chamada do sistema, a interface de chamada do sistema. O subsistema de agendamento de processo abre a interface que precisa ser fornecida ao espaço do usuário por meio da interface de chamada do sistema e, ao mesmo tempo, protege os detalhes que não precisam ser considerados pelo programa de espaço do usuário.

Gerenciador de Memória (MM)

O gerenciamento de memória também é o subsistema mais importante no kernel do Linux, que fornece principalmente controle de acesso aos recursos de memória. O sistema Linux irá estabelecer uma relação de mapeamento entre a memória física do hardware e a memória utilizada pelo processo (chamada de memória virtual). podem ser mapeados para diferentes memórias físicas.

O subsistema de gerenciamento de memória inclui 3 submódulos (veja a figura abaixo), e suas funções são as seguintes:

  1. Gerentes Específicos de Arquitetura, partes relacionadas à arquitetura. Fornece uma interface virtual para acessar a memória do hardware.
  2. Gerente Independente de Arquitetura, a parte independente de arquitetura. Fornece todos os mecanismos de gerenciamento de memória, incluindo: mapeamento de memória baseado em processo; troca de memória virtual.
  3. Interface de chamada do sistema, a interface de chamada do sistema. Por meio dessa interface, funções como alocação de memória, liberação e mapeamento de arquivos são fornecidas aos programas e aplicativos do espaço do usuário.

Sistema de arquivos virtual (VFS)

Um sistema de arquivos no sentido tradicional é um método de armazenamento e organização de dados de computador. Ele abstrai blocos de dados frios em discos de computador, discos rígidos e outros dispositivos de maneira fácil de entender e amigável (estrutura de arquivos e diretórios), tornando-os fáceis de encontrar e acessar. Portanto, a essência do sistema de arquivos é "o método de armazenamento e organização de dados", e a manifestação do sistema de arquivos é "ler dados de um determinado dispositivo e gravar dados em um determinado dispositivo".

À medida que a tecnologia dos computadores avança, também avançam os métodos de armazenamento e organização de dados, resultando em vários tipos de sistemas de arquivos, como FAT, FAT32, NTFS, EXT2, EXT3 e muito mais. Para ser compatível, o sistema operacional ou kernel deve suportar vários tipos de sistemas de arquivos da mesma forma, o que estende o conceito de sistema de arquivos virtual (VFS). A função do VFS é gerenciar vários sistemas de arquivos, blindar suas diferenças e fornecer aos programas do usuário uma interface para acessar os arquivos de maneira unificada.

Podemos ler ou gravar dados de discos, discos rígidos, NAND Flash e outros dispositivos, de modo que os sistemas de arquivos originais foram construídos nesses dispositivos. Este conceito também pode ser estendido a outros dispositivos de hardware, como memória, display (LCD), teclado, porta serial e assim por diante. Nosso controle de acesso a dispositivos de hardware também pode ser resumido como leitura ou gravação de dados, para que possa ser acessado com uma interface unificada de operação de arquivos. O kernel do Linux faz exatamente isso, abstraindo sistemas de arquivos de dispositivos, sistemas de arquivos na memória e muito mais, além dos sistemas de arquivos de disco tradicionais. Essas lógicas são implementadas pelo subsistema VFS.

O subsistema VFS inclui 6 submódulos (veja a figura abaixo), e suas funções são as seguintes:

  1. Drivers de dispositivo, drivers de dispositivo, são usados ​​para controlar todos os dispositivos e controladores externos. Como há um grande número de dispositivos de hardware (especialmente produtos incorporados) que não são compatíveis entre si, também há muitos drivers de dispositivo. Portanto, quase metade dos códigos-fonte no kernel do Linux são drivers de dispositivo, e a maioria dos engenheiros de nível inferior do Linux (especialmente empresas domésticas) estão escrevendo ou mantendo drivers de dispositivo e não têm tempo para estimar outro conteúdo (eles são precisamente os essência do kernel Linux). onde).
  2. Device Independent Interface, este módulo define uma forma unificada de descrever dispositivos de hardware (modelo de dispositivo unificado), todos os drivers de dispositivo obedecem a esta definição, o que pode reduzir a dificuldade de desenvolvimento. Ao mesmo tempo, a interface pode ser fornecida para cima com uma situação consistente.
  3. Sistemas Lógicos, cada sistema de arquivos corresponde a um Sistema Lógico (sistema de arquivos lógicos), que implementa uma lógica específica do sistema de arquivos.
  4. Interface Independente do Sistema, este módulo é responsável por representar dispositivos de hardware e sistemas de arquivos lógicos com uma interface unificada (dispositivo rápido e dispositivo de caractere), de modo que o software da camada superior não se preocupe mais com a forma específica do hardware.
  5. Interface de chamada do sistema, a interface de chamada do sistema, fornece ao espaço do usuário uma interface unificada para acessar o sistema de arquivos e os dispositivos de hardware.

Subsistema de Rede (Rede)

O subsistema de rede no kernel Linux é responsável principalmente por gerenciar vários dispositivos de rede, implementar várias pilhas de protocolos de rede e, finalmente, realizar a função de conectar outros sistemas através da rede. No kernel Linux, o subsistema de rede é quase autocontido, inclui 5 submódulos (veja a figura abaixo), e suas funções são as seguintes:

  1. Drivers de dispositivo de rede, os drivers de dispositivos de rede, são os mesmos que os drivers de dispositivo no subsistema VFS.
  2. Interface independente de dispositivo, que é a mesma do subsistema VFS.
  3. Protocolos de rede, que implementa vários protocolos de transmissão de rede, como IP, TCP, UDP, etc.
  4. Interface Independente de Protocolo, protege diferentes dispositivos de hardware e protocolos de rede, e fornece interfaces (sockets) no mesmo formato.
  5. A interface de chamada do sistema, a interface de chamada do sistema, fornece espaço de usuário com uma interface unificada para acessar dispositivos de rede.

subsistema IPC, consulte: 

Gerenciamento de processos Linux e tarefas agendadas e backup e recuperação do sistema - Blog do Wespten - Blog CSDN

3. Estrutura de diretórios do código-fonte do kernel Linux

O código-fonte do kernel do Linux consiste em três partes principais:

1. Código do núcleo do kernel, incluindo vários subsistemas e submódulos, e outros subsistemas de suporte, como gerenciamento de energia, inicialização do Linux, etc.

2. Outros códigos não essenciais, como arquivos de biblioteca (pois o kernel Linux é um kernel autocontido, ou seja, o kernel não depende de nenhum outro software e pode ser compilado por si mesmo), coleções de firmware, KVM (virtual tecnologia da máquina), etc.

3. Scripts de compilação, arquivos de configuração, documentos de ajuda, instruções de direitos autorais e outros arquivos auxiliares

O seguinte diretório do kernel usa o kernel linux-3.14 como explicação:

1. documentação:

Prestar assistência documental. Para algumas informações explicativas sobre o kernel, haverá um manual de ajuda neste diretório.

比如linux-3.14-fs4412/Documentation/devicetree/bindings/interrupt-controller/interrupts.txt

Este arquivo explica a descrição detalhada da célula da propriedade de interrupção do dispositivo do nó do número do dispositivo.

Apenas de acordo com o nome da pasta, podemos encontrar a documentação que precisamos.

2. arco:

arco é uma abreviação de arquitetura. Todo o código relacionado à arquitetura está neste diretório começando com

include/asm-*/ diretórios. Cada arquitetura suportada pelo Linux tem um diretório correspondente no diretório arch, e ainda

Os passos são decompostos em subdiretórios como boot, mm, kernel, etc.:

 |--arm arm e subdiretórios para arquiteturas compatíveis

    |--boot Bootloader e implementação do gerenciador de memória usado para iniciar o kernel nesta plataforma de hardware.

         |--descompressão do kernel compactado

 |--tools programa para gerar imagem de kernel compactada

         | --kernel: contém implementações que oferecem suporte a recursos específicos de arquitetura, como manipulação de semáforos e SMP.

         | --lib: contém implementações específicas de arquitetura de funções comuns, como strlen e memcpy.

         | --mm: contém a implementação do gerenciador de memória específico da arquitetura.

Além desses três subdiretórios, a maioria das arquiteturas também possui um subdiretório de inicialização, se necessário, que contém a implementação do gerenciador de memória usado para iniciar o kernel nessas plataformas de hardware.

3. motoristas:

Código de driver, um driver é um software que controla o hardware. Este diretório é o maior diretório do kernel, e drivers para placas gráficas, placas de rede, adaptadores SCSI, barramentos PCI, barramentos USB e quaisquer outros periféricos ou barramentos suportados pelo Linux podem ser encontrados aqui.

4. fs:

O código para o sistema de arquivos virtual (VFS) e o código para cada um dos diferentes sistemas de arquivos estão nesse diretório. Todos os sistemas de arquivos suportados pelo Linux têm um subdiretório correspondente no diretório fs. Por exemplo, o sistema de arquivos ext2 corresponde ao diretório fs/ext2.

Um sistema de arquivos é o meio entre o dispositivo de armazenamento e os processos que precisam acessar o dispositivo de armazenamento. Os dispositivos de armazenamento podem ser fisicamente acessíveis localmente, como discos rígidos ou unidades de CD-ROM, que usam os sistemas de arquivos ext2/ext3 e isofs do sistema, respectivamente.

Existem também alguns sistemas de arquivos virtuais (procs), que são um sistema de arquivos padrão presente. No entanto, os arquivos nele existem apenas na memória e não ocupam espaço em disco.

5. incluem:

Este diretório contém a maioria dos arquivos de cabeçalho no kernel e é agrupado nos seguintes subdiretórios. Para modificar a arquitetura do processador simplesmente edite o makefile do kernel e execute novamente o programa de configuração do kernel Linux.

       | include/asm-*/ Cada subdiretório correspondente a um arco, como include/asm-alpha,

Incluir/asm-braço etc. Os arquivos em cada subdiretório definem as funções de pré-processamento e embutidas necessárias para dar suporte a uma determinada arquitetura, a maioria das quais são implementações de linguagem assembly completas ou parciais.

| include/linux Os arquivos de cabeçalho independentes de plataforma estão todos neste diretório, geralmente está vinculado ao diretório                                             

 /usr/include/linux (ou todos os arquivos dentro dele serão copiados paradiretório /usrinclude/linux)      

6. calor:    

     Código de inicialização do kernel. Inclui main.c, código para criar espaço de usuário inicial e outro código de inicialização.

7. IP:

IPC (Comunicação entre Processos). Ele contém memória compartilhada, semáforos e outras formas de código IPC.

8. núcleo:

A parte central do kernel, incluindo o escalonamento do processo (sched.c), bem como a criação e cancelamento do processo (fork.c e exit.c), e outra parte do código central relacionado à plataforma estão no diretório arch/*/kernel.

9. milímetros

Este diretório contém parte do código de gerenciamento de memória que é independente da arquitetura. O código de gerenciamento de memória dependente de arquitetura está localizado em arch/*/mm.

10. rede

    O código de rede principal implementa vários protocolos de rede comuns, como TCP/IP, IPX, etc.

11. lib

    Este diretório contém o código da biblioteca principal. Implementa um subconjunto genérico da biblioteca C padrão, incluindo funções para manipulação de string e memória (strlen, mmcpy, etc.) e funções relacionadas às séries sprintf e atoi. Diferente do código sob arch/lib, o código da biblioteca aqui é escrito em C e pode ser usado diretamente na nova versão portada do kernel. O código da biblioteca relacionado à arquitetura do processador é colocado em arch/mm.

12. bloco:

    Drivers de dispositivo de bloco incluem drivers IDE (em ide.c). Um dispositivo de bloco é um dispositivo que recebe e envia dados em blocos. O código da camada de bloco inicial está parcialmente no diretório drivers e parcialmente no diretório fs. Desde a versão 2.6.15, o código central da camada de bloco foi extraído e colocado no diretório de bloco de nível superior. Se você quiser procurar o processo de inicialização desses dispositivos que podem conter sistemas de arquivos, deve ser device_setup() em drivers/block/genhd.c. Ao instalar um sistema de arquivos nfs, não apenas o disco rígido, mas também a rede devem ser inicializados. Dispositivos de bloco incluem dispositivos IDE e SCSI.

13. firmware

Fireware contém código que permite que os computadores leiam e compreendam os sinais enviados pelos dispositivos. Por exemplo, uma câmera gerencia seu próprio hardware, mas o computador deve entender os sinais que a câmera envia ao computador. Os sistemas Linux usam firmware vicam para entender a comunicação da câmera. Caso contrário, sem o firmware, o sistema Linux não saberia o que fazer com as informações da câmera. Além disso, o firmware também ajuda a enviar mensagens do sistema Linux para o dispositivo. Desta forma, o sistema Linux pode dizer à câmera para reajustar ou desligar a câmera.

13. usr:

Implemente cpio etc para compactação e compactação. O código nesta pasta cria esses arquivos após a compilação do kernel.

14. segurança:

Este diretório contém código para diferentes modelos de segurança do Linux. É importante manter seu computador protegido contra vírus e hackers. Caso contrário, o sistema Linux pode ser danificado.

15. criptografia:

A API de criptografia usada pelo próprio kernel implementa algoritmos de criptografia e hash comumente usados, bem como alguns algoritmos de compactação e verificação de CRC. Exemplo: "sha1_generic.c" Este arquivo contém o código para o algoritmo de criptografia SHA1.

16. roteiros:

Não há código do kernel neste diretório, apenas os arquivos de script usados ​​para configurar o kernel. Ao executar comandos como make menuconfig ou make xconfig para configurar o kernel, o usuário interage com os scripts localizados neste diretório.

17. som:

Driver da placa de som e outro código-fonte relacionado ao som.

18. amostras

Alguns exemplos de programação do kernel

19. vir

Essa pasta contém o código de virtualização, que permite que os usuários executem vários sistemas operacionais ao mesmo tempo. Com a virtualização, o sistema operacional convidado é executado como qualquer outro aplicativo executado no host Linux.

20. ferramentas

Esta pasta contém ferramentas para interagir com o kernel.

COPIANDO: Informações de licença e autorização. O kernel Linux é licenciado sob a licença GPLv2. Esta licença concede a qualquer pessoa o direito de usar, modificar, distribuir e compartilhar código fonte e compilado gratuitamente. No entanto, ninguém pode vender o código-fonte.

CRÉDITOS: lista de colaboradores

Kbuild : Este é um script que define algumas configurações do kernel. Por exemplo, este script define uma variável ARCH, que é o tipo de processador que o desenvolvedor deseja gerar para o kernel suportar.

Kconfig: Este script será usado pelos desenvolvedores ao configurar o kernel

MAINTAINERS : Esta é uma lista de mantenedores atuais, seus endereços de e-mail, páginas iniciais e partes ou arquivos específicos do kernel que eles são responsáveis ​​por desenvolver e manter. Isso é útil quando um desenvolvedor encontra um problema no kernel e deseja reportá-lo a um mantenedor que possa lidar com isso.

Makefile : Este script é o arquivo principal para compilar o kernel. Este arquivo passa os parâmetros e arquivos de compilação e as informações necessárias para a compilação ao compilador.

README : Este documento fornece informações para desenvolvedores que desejam saber como compilar o kernel.

REPORTING-BUGS : Este documento fornece informações sobre como relatar bugs.

O código do kernel é um arquivo com a extensão ".c" ou ".h". A extensão ".c" indica que o kernel é escrito em C, uma das muitas linguagens de programação, e os arquivos "h" são arquivos de cabeçalho e também são escritos em C. Os arquivos de cabeçalho contêm muito código que os arquivos ".c" precisam usar porque podem importar o código existente em vez de reescrevê-lo, o que economiza tempo dos programadores. Caso contrário, um conjunto de código que executa a mesma ação existirá em muitos ou em todos os arquivos "c". Isso também consome e desperdiça espaço no disco rígido. (Anotação: os arquivos de cabeçalho podem não apenas salvar codificação repetida, mas também a reutilização de código reduzirá a chance de erros de código)

Resumo da arquitetura geral do kernel Linux:

Arquitetura do Kernel Linux:

(1) Interface de chamada do sistema

A camada SCI fornece alguns mecanismos para realizar chamadas de função do espaço do usuário para o kernel. Conforme discutido anteriormente, essa interface depende da arquitetura, mesmo dentro da mesma família de processadores. SCI é realmente um serviço de multiplexação e demultiplexação de chamada de função muito útil. Você pode encontrar a implementação do SCI em ./linux/kernel e as partes dependentes da arquitetura em ./linux/arch.

(2) Gestão de processos

O foco do gerenciamento de processos é a execução do processo. No kernel, esses processos são chamados de threads e representam virtualização de processador separada (código de thread, dados, pilha e registros de CPU). No espaço do usuário, o termo processo é frequentemente usado, mas a implementação do Linux não distingue entre os dois conceitos (processo e encadeamento). O kernel fornece uma interface de programação de aplicativos (API) por meio de SCI para criar um novo processo (funções fork, exec ou Portable Operating System Interface [POSIX]), parar processos (kill, exit) e comunicar e sincronizar entre eles (sinal ou mecanismo POSIX).

O gerenciamento de processos também inclui lidar com a necessidade de compartilhar CPU entre processos ativos. O kernel implementa um novo tipo de algoritmo de escalonamento que opera em tempo constante independente de quantos threads estão competindo pela CPU. Esse algoritmo é chamado de escalonador O(1), e o nome indica que leva a mesma quantidade de tempo para agendar vários threads quanto para agendar um thread. O escalonador O(1) também pode suportar vários processadores (chamados de multiprocessamento simétrico ou SMP). Você pode encontrar o código-fonte para gerenciamento de processos em ./linux/kernel e a fonte dependente de arquitetura em ./linux/arch.

(3) Gerenciamento de memória

Outro importante recurso gerenciado pelo kernel é a memória. Para maior eficiência, se a memória virtual for gerenciada por hardware, a memória será gerenciada nas chamadas páginas de memória (4 KB para a maioria das arquiteturas). O Linux inclui maneiras de gerenciar a memória disponível, bem como mecanismos de hardware usados ​​para mapeamento físico e virtual. Mas o gerenciamento de memória pode gerenciar buffers de mais de 4 KB. O Linux fornece abstrações para buffers de 4 KB, como o alocador de slab. Esse modo de gerenciamento de memória usa um buffer de 4 KB como base, aloca estruturas a partir dele e acompanha o uso da página de memória, como quais páginas de memória estão cheias, quais não são totalmente usadas e quais estão vazias. Isso permite que o modo ajuste dinamicamente o uso da memória com base nas necessidades do sistema. Para dar suporte a vários usuários usando memória, às vezes a memória disponível se esgota. Por esse motivo, as páginas podem ser removidas da memória e colocadas no disco. Esse processo é chamado de troca porque as páginas são trocadas da memória para o disco. O código fonte para gerenciamento de memória pode ser encontrado em ./linux/mm.

(4) Sistema de arquivos virtuais

O Virtual File System (VFS) é um aspecto muito útil do kernel Linux porque fornece uma abstração de interface comum para o sistema de arquivos. O VFS fornece uma camada de troca entre o SCI e os sistemas de arquivos suportados pelo kernel.

Hierarquia do sistema de arquivos:

Além do VFS, há uma abstração de API genérica para funções como abrir, fechar, ler e gravar. Abaixo do VFS está a abstração do sistema de arquivos, que define como as funções de nível superior são implementadas. São plugins para um determinado sistema de arquivos (mais de 50). O código fonte do sistema de arquivos pode ser encontrado em ./linux/fs. Abaixo da camada do sistema de arquivos está o cache de buffer, que fornece um conjunto geral de funções para a camada do sistema de arquivos (independente do sistema de arquivos específico). Essa camada de cache otimiza o acesso a dispositivos físicos retendo os dados por um período de tempo (ou lendo-os antecipadamente para que estejam disponíveis quando necessário). Abaixo do cache de buffer está o driver de dispositivo, que implementa uma interface para um dispositivo físico específico.

(5) Pilha de rede

A pilha de rede é projetada para seguir uma arquitetura em camadas que imita o próprio protocolo. Lembre-se de que o Internet Protocol (IP) é o protocolo principal da camada de rede subjacente ao protocolo de transporte (geralmente chamado de Transmission Control Protocol ou TCP). Acima do TCP está a camada de soquete, que é chamada através do SCI. A camada de soquete é a API padrão do subsistema de rede, que fornece uma interface de usuário para vários protocolos de rede. Do acesso ao quadro bruto às Unidades de Dados do Protocolo IP (PDUs) ao TCP e ao Protocolo de Datagramas do Usuário (UDP), a camada de soquete fornece uma maneira padronizada de gerenciar conexões e mover dados entre terminais. O código fonte da rede no kernel pode ser encontrado em ./linux/net.

(6) Driver de dispositivo

Muito do código no kernel do Linux está em drivers de dispositivo, que são capazes de executar dispositivos de hardware específicos. A árvore de origem do Linux fornece um subdiretório de driver, que é dividido em vários dispositivos de suporte, como Bluetooth, I2C, serial, etc. O código para os drivers de dispositivo pode ser encontrado em ./linux/drivers.

(7) Código dependente da arquitetura

Embora o Linux seja amplamente independente da arquitetura em que é executado, alguns elementos devem ser considerados na arquitetura para operar adequadamente e obter maior eficiência. O subdiretório ./linux/arch define a parte dependente da arquitetura do código-fonte do kernel e contém vários subdiretórios específicos da arquitetura (que juntos formam o BSP). Para um sistema desktop típico, o diretório x86 é usado. Cada subdiretório de arquitetura contém muitos outros subdiretórios, cada um focando em um aspecto específico do kernel, como inicialização, kernel, gerenciamento de memória, etc. Esses códigos dependentes da arquitetura podem ser encontrados em ./linux/arch.

Se a portabilidade e a eficiência do kernel do Linux não forem boas o suficiente, o Linux também oferece alguns outros recursos que não se encaixam nas categorias acima. Como sistema operacional de produção e software de código aberto, o Linux é uma boa plataforma para testar novos protocolos e seus aprimoramentos. O Linux suporta vários protocolos de rede, incluindo TCP/IP típico e extensões para redes de alta velocidade (superiores a 1 Gigabit Ethernet [GbE] e 10 GbE). O Linux também pode suportar protocolos como o Stream Control Transmission Protocol (SCTP), que fornece muitos recursos mais avançados do que o TCP (o sucessor do protocolo da camada de transporte).

O Linux também é um kernel dinâmico que suporta a adição ou remoção dinâmica de componentes de software. Conhecidos como módulos de kernel carregáveis ​​dinamicamente, eles podem ser inseridos pelo usuário no momento da inicialização conforme necessário (atualmente o dispositivo específico requer este módulo) ou a qualquer momento.

Um dos aprimoramentos mais recentes do Linux é um sistema operacional (chamado de hypervisor) que pode ser usado como sistema operacional para outros sistemas operacionais. Mais recentemente, houve modificações no kernel chamadas Kernel-Based Virtual Machines (KVM). Essa modificação habilita uma nova interface para o espaço do usuário que permite que outros sistemas operacionais sejam executados no kernel habilitado para KVM. Além de executar outras instâncias do Linux, o Microsoft Windows também pode ser virtualizado. A única limitação é que o processador subjacente deve suportar as novas instruções de virtualização.

Quarto, o design geral da arquitetura do kernel

1. Mecanismo do kernel

Cada função é feita em diferentes subsistemas do kernel.Se os subsistemas quiserem se comunicar, um mecanismo deve ser projetado para permitir que os subsistemas se comuniquem entre si de maneira segura, confiável e eficiente.

O Linux absorveu a experiência de design do microkernel no desenvolvimento passo a passo, embora seja um único kernel, possui as características do microkernel.

O Linux usa um design de kernel modular para ter ambas as características de microkernel, mas esse design modular não é um subsistema como um microkernel, mas um kernel composto por um núcleo e módulos funcionais periféricos. Todos os subsistemas de microkernel são executados independentemente e podem funcionar sem depender de outras partes.Os módulos Linux devem depender do núcleo, mas podem ser carregados quando estão em uso e descarregados dinamicamente quando não estão em uso. O módulo no linux é representado externamente como um arquivo de biblioteca do tipo programa, mas o arquivo de biblioteca do programa é .so, e o módulo do kernel é .ko (objeto do kernel), que é chamado pelo kernel.  

Supondo que se o driver for fornecido pelo kernel, imagine compilar um kernel e instalá-lo no host, caso seja descoberto posteriormente que ele não pode conduzir o novo dispositivo de hardware que adicionamos posteriormente. Como todos os tipos de hardware são controlados pelo kernel, e o kernel não fornece esse programa, é muito problemático para usuários e fabricantes recompilar o kernel.

O design modular pode evitar esta situação, pois cada fabricante desenvolve seu próprio driver de forma modular e precisa apenas desenvolver seu próprio programa de driver para um dispositivo específico.

Uma das coisas que os desenvolvedores do kernel do Linux fizeram é tornar os módulos do kernel carregáveis ​​e descarregáveis ​​em tempo de execução, o que significa que você pode adicionar ou remover recursos do kernel dinamicamente. Isso não apenas adiciona recursos de hardware ao kernel, mas também pode incluir módulos para executar processos de servidor, como virtualização de baixo nível, mas também pode substituir todo o kernel sem exigir a reinicialização do computador em alguns casos.

Podemos compilar esses módulos quando necessário.Quando uma função não é necessária, ela pode ser desmontada por si mesma sem afetar o funcionamento do núcleo. Imagine se você pudesse atualizar para um service pack do Windows sem reinicializar, esse é um dos benefícios e vantagens da modularidade.

1) mecanismo de trabalho da CPU

Modo de trabalho da CPU

As CPUs modernas geralmente implementam diferentes modos de trabalho. Tome o ARM como exemplo: ARM implementa 7 modos de trabalho. Em diferentes modos, as instruções que a CPU pode executar ou os registradores que podem ser acessados ​​são diferentes:

(1) Modo de usuário usr

(2) Sistema de modo de sistema

(3) Modo de gerenciamento svc

(4) Fiq de interrupção rápida

(5) Irq de interrupção externa

(6) Abt terminação de acesso a dados

(7) Exceção de instrução indefinida

Tome (2) X86 como exemplo: X86 implementa 4 níveis diferentes de permissões, Ring0—Ring3; Ring0 pode executar instruções privilegiadas e acessar dispositivos de E/S; Ring3 tem muitas restrições

Portanto, do ponto de vista da CPU, para proteger a segurança do kernel, o Linux divide o sistema em espaço do usuário e espaço do kernel.

Espaço do usuário e espaço do kernel são dois estados diferentes de execução do programa.Podemos concluir a transferência do espaço do usuário para o espaço do kernel por meio de "chamadas de sistema" e "interrupções de hardware".

Os aplicativos são executados no kernel, é apenas o caso lógico. No entanto, na verdade ele funciona diretamente no hardware.Qualquer dado do aplicativo está na memória, e o processamento de dados é todo CPU, mas eles não podem ser usados ​​à vontade, e devem ser gerenciados pelo kernel.

Mas há apenas uma CPU. Quando o programa aplicativo está funcionando, o kernel é suspenso e o programa aplicativo também está no espaço de memória. Uma vez que o programa aplicativo deseja acessar outros recursos de hardware, ou seja, quando deseja executar I /O instruções, ele não pode ser executado. Como o programa aplicativo não pode ver o hardware, o programa aplicativo é um programa baseado em chamadas de sistema. Quando o programa aplicativo precisa acessar recursos de hardware, ele inicia uma solicitação de privilégio à CPU. Uma vez que a CPU recebe a solicitação de privilégio, a CPU acorda o kernel e executa a operação no kernel.Um pedaço de código (não um programa de kernel completo) retorna o resultado para o aplicativo, então o código do kernel é encerrado e o programa do kernel é suspenso.

Durante esse tempo, a CPU muda do modo usuário para o modo kernel, que parece ser um modo privilegiado.

Todas as aplicações são executadas diretamente no hardware, e somente são gerenciadas e monitoradas pelo kernel quando necessário, então o kernel também é um monitor, um programa de monitoramento e um programa de monitoramento de recursos e processos.

O kernel não tem produtividade, e a produtividade é gerada por uma aplicação chamada, então devemos tentar deixar o sistema rodar em modo de aplicação, então quanto menos tempo o kernel ocupar, melhor. O kernel ocupa principalmente tempo em funções relacionadas, como comutação de processo e processamento de interrupção. O objetivo da alternância de modo é concluir a produção, mas a comutação de processo e a produção não têm significado. O processamento de interrupção pode ser considerado relacionado à própria produção, porque o aplicativo precisa executar E/S.

O principal objetivo do kernel é completar o gerenciamento de hardware, e há uma ideia no Linux de que cada processo é derivado de seu processo pai e fork() do processo pai, então quem fará o fork() e gerenciará esses processos, então Com o init do grande programa de governanta, ele gerencia todos os processos no espaço do usuário como um todo.

O gerenciamento do espaço do usuário não será realizado pelo kernel, portanto, após iniciarmos o kernel, precisamos iniciar o init primeiro se quisermos iniciar o espaço do usuário, portanto, o número PID do init é sempre 1. O init também é derivado de seu processo pai fork(), que é um mecanismo no espaço do kernel para orientar especificamente os processos do espaço do usuário. init é um aplicativo, em /sbin/init, um arquivo executável.

Tempo de CPU

Como cada processo na memória monopoliza diretamente a CPU, o kernel virtualiza a CPU e a fornece ao processo. alocando poder de computação entre processos, a CPU fornece seu poder de computação em termos de tempo.

Quanto maior o poder de computação que pode ser fornecido por unidade de tempo, mais rápida deve ser a velocidade, caso contrário, o tempo só pode ser estendido. É por isso que precisamos de uma CPU mais rápida para economizar tempo.

Características computacionais da CPU

 A E/S é o dispositivo mais lento. Nossa CPU gasta muito tempo esperando a conclusão da E/S. Para evitar espera ociosa e sem sentido, quando precisamos esperar, deixe a CPU executar outros processos ou threads.

Devemos espremer o poder de computação da CPU ao máximo, porque o poder de computação da CPU está oscilando com o oscilador da frequência do relógio ao longo do tempo e está funcionando, quer você o use ou não.

Se você deixar a CPU ociosa, ela ainda consumirá energia e, com o tempo, a capacidade de computação será perdida, para que você possa fazer a CPU funcionar com 80-90% de utilização, o que significa que sua capacidade de produção está totalmente ativa. A CPU não é ruim, não há desgaste, é um equipamento elétrico, exceto que a potência é grande, o calor é grande e a dissipação de calor é suficiente. Para equipamentos elétricos, ele será danificado se não for usado.

9 Mecanismos de Sincronização no Kernel Linux

O Linux geralmente usa uma tabela de hash para implementar um cache (Cache), que é uma informação que precisa ser acessada rapidamente.

Depois que o sistema operacional introduz o conceito de processo, depois que o processo se torna a entidade de agendamento, o sistema tem a capacidade de executar vários processos simultaneamente, mas também leva à competição e compartilhamento de recursos entre os vários processos do sistema.

Além disso, devido à introdução de interrupções, mecanismos de exceção e preempção de estado do kernel, esses caminhos de execução do kernel (processos) são executados de maneira intercalada. Para os caminhos do kernel executados por esses caminhos intercalados, se as medidas de sincronização necessárias não forem tomadas, algumas estruturas de dados chave serão acessadas e modificadas intercaladas, resultando em estados inconsistentes dessas estruturas de dados, o que, por sua vez, leva a falhas no sistema. Portanto, para garantir a operação eficiente, estável e ordenada do sistema, o linux deve adotar um mecanismo de sincronização.

No sistema Linux, chamamos de seção crítica o segmento de código que acessa os recursos compartilhados. O que faz com que vários processos acessem o mesmo recurso compartilhado é chamado de fonte simultânea.

As principais fontes de simultaneidade em sistemas Linux são:

Interromper o processamento: Por exemplo, quando um processo é interrompido ao acessar um recurso crítico, e então entra no manipulador de interrupção, se no manipulador de interrupção, o recurso crítico também é acessado. Embora não seja estritamente simultaneidade, também causará uma condição de corrida para o recurso.

Preempção do estado do kernel: Por exemplo, quando um processo acessa um recurso crítico, ocorre a preempção do estado do kernel e, em seguida, entra em um processo de alta prioridade. Se o processo também acessar o mesmo recurso crítico, causará um conflito processo a processo. simultaneidade.

Simultaneidade de vários processadores: há uma concorrência estrita entre os processos em um sistema multiprocessador. Cada processador pode agendar e executar um processo de forma independente, e vários processos estão sendo executados ao mesmo tempo.

Conforme mencionado acima, pode-se observar que o objetivo do uso do mecanismo de sincronização é evitar que vários processos acessem simultaneamente o mesmo recurso crítico.

9 mecanismos de sincronização:

1) Por variável de CPU

A forma principal é uma matriz de estruturas de dados, um elemento da matriz para cada CPU no sistema.

Caso de uso: Os dados devem ser logicamente independentes

Diretrizes de uso: As variáveis ​​por CPU devem ser acessadas com a preempção desabilitada no caminho de controle do kernel.

2) Operações atômicas

Princípio: Realiza-se por meio das instruções de montagem que são atômicas para "ler-modificar-escrever" em instruções em linguagem de montagem.

3) Barreiras de memória

Justificativa: Use uma primitiva de barreira de memória para assegurar que uma operação anterior à primitiva tenha sido concluída antes do início da operação seguinte à primitiva.

4) Bloqueio de giro

Usado principalmente em ambientes multiprocessadores.

Justificativa: Se um caminho de controle do kernel descobrir que o spinlock solicitado já está "bloqueado" por um caminho de controle do kernel em execução em outra CPU, ele executa uma instrução de loop repetidamente até que o bloqueio seja liberado.

Descrição: Spinlocks geralmente são usados ​​para proteger seções críticas que são proibidas de serem antecipadas pelo kernel.

Em um único processador, os spinlocks apenas desabilitam ou habilitam a preempção do kernel.

5) Bloqueio de sequência

Um bloqueio sequencial é muito semelhante a um bloqueio de rotação, exceto que o gravador em um bloqueio sequencial tem uma prioridade mais alta que o leitor, o que significa que o gravador pode continuar executando mesmo quando o leitor estiver lendo.

6)RCU

Usado principalmente para proteger estruturas de dados que são lidas por várias CPUs.

Vários leitores e gravadores podem ser executados ao mesmo tempo, e o RCU é livre de bloqueios.

Restrições de uso:

1) RCU protege apenas estruturas de dados que são alocadas dinamicamente e referenciadas por ponteiros

2) Em uma seção crítica protegida por RCU, nenhum caminho de controle do kernel pode dormir.

princípio:

Quando o gravador deseja atualizar os dados, ele faz uma cópia de toda a estrutura de dados fazendo referência ao ponteiro e, em seguida, faz modificações nessa cópia. Após a modificação, o gravador altera o ponteiro para a estrutura de dados original para que aponte para a cópia modificada (a modificação do ponteiro é atômica).

7) Semáforo:

Princípio: Quando o caminho de controle do kernel tenta adquirir o recurso ocupado protegido pelo semáforo do kernel, o processo correspondente é suspenso; somente quando o recurso é liberado, o processo torna-se executável novamente.

Restrições de uso: Somente funções que podem dormir podem adquirir semáforos do kernel;

Nem os manipuladores de interrupção nem as funções adiáveis ​​podem usar semáforos do kernel.

8) Desativação de interrupção local

Princípio: A proibição de interrupção local pode garantir que, mesmo que o dispositivo de hardware gere um sinal IRQ, o caminho de controle do kernel continue a ser executado, de modo que a estrutura de dados acessada pela rotina de processamento de interrupção seja protegida.

Desvantagem: Desabilitar interrupções locais não limita o acesso simultâneo a estruturas de dados compartilhadas por manipuladores de interrupção executados em outra CPU,

Portanto, em um ambiente multiprocessador, a desativação de interrupções locais precisa ser usada em conjunto com spinlocks.

9) Proibição de interrupção suave local

Método 1:

Como o softirq começa a ser executado no final do manipulador de interrupção de hardware, a maneira mais fácil é desabilitar as interrupções nessa CPU.

Como nenhuma rotina de tratamento de interrupção é ativada, o softirq não tem chance de ser executado.

Método 2:

Softirq pode ser ativado ou desativado na CPU local manipulando o contador softirq armazenado no campo preempt_count do descritor thread_info atual. Porque o kernel às vezes só precisa desabilitar o softirq sem desabilitar as interrupções.

2) Mecanismo de memória

O mecanismo de memória do Linux inclui espaço de endereçamento, memória física, mapeamento de memória, mecanismo de paginação e mecanismo de comutação.

espaço de endereçamento

Uma das vantagens da memória virtual é que cada processo pensa que tem todo o espaço de endereçamento de que precisa. O tamanho da memória virtual pode ser muitas vezes o tamanho da memória física do sistema. Cada processo no sistema tem seu próprio espaço de endereço virtual, que é completamente independente um do outro. Um processo executando um aplicativo não afetará outros processos e os aplicativos também são protegidos uns dos outros. O espaço de endereço virtual é mapeado para a memória física pelo sistema operacional. Do ponto de vista do aplicativo, esse espaço de endereço é um espaço de endereço plano linear; no entanto, o kernel lida com o espaço de endereço virtual do usuário de maneira muito diferente.

O espaço de endereço linear é dividido em duas partes: o espaço de endereço do usuário e o espaço de endereço do kernel. O espaço de endereço do usuário não muda toda vez que ocorre uma troca de contexto, enquanto o espaço de endereço do kernel permanece sempre o mesmo. A quantidade de espaço alocado para espaço do usuário e espaço do kernel depende principalmente se o sistema é uma arquitetura de 32 bits ou 64 bits. Por exemplo, x86 é uma arquitetura de 32 bits, que suporta apenas 4 GB de espaço de endereço, dos quais 3 GB são reservados para o espaço do usuário e 1 GB é alocado para o espaço de endereço do kernel. O tamanho específico da partição é determinado pela variável de configuração do kernel PAGE_OFFSET.

memória física

Para oferecer suporte a várias arquiteturas, o Linux usa uma maneira independente de arquitetura para descrever a memória física.

A memória física pode ser organizada em bancos de memória (bancos), cada um a uma distância específica do processador. Esse tipo de layout de memória tornou-se muito comum à medida que mais e mais máquinas adotam a tecnologia Nonuniform Memory Access (NUMA). As VMs do Linux representam esse arranjo como nós. Cada nó é dividido em vários blocos de memória chamados zonas de gerenciamento, que representam um intervalo de endereços na memória. Existem três zonas de gerenciamento diferentes: ZONE_DMA, ZONE_NORMAL e ZONE_HIGHMEM. Por exemplo, x86 tem as seguintes áreas de gerenciamento de memória:

ZONE_DMA Os primeiros 16MB do endereço de memória
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB~o final do endereço de memória
Cada área de gerenciamento tem seu próprio propósito. Alguns dispositivos ISA anteriores tinham restrições sobre quais endereços poderiam executar operações de E/S, e ZONE_DMA remove essas restrições.
ZONE_NORMAL é usado para todas as operações e alocações do kernel. É extremamente importante para o desempenho do sistema.
ZONE_HIGHMEM é o restante da memória do sistema. Deve-se notar que ZONE_HIGHMEM não pode ser usado para alocação de kernel e estruturas de dados, e só pode ser usado para salvar dados do usuário.

mapa de memória

Para entender melhor o mecanismo de mapeamento da memória do kernel, veja a seguir um exemplo de x86. Como mencionado anteriormente, o kernel tem apenas 1 GB de espaço de endereço virtual disponível e os outros 3 GB são reservados para o espaço do usuário. O kernel mapeia a memória física em ZONE_DMA e ZONE_NORMAL diretamente em seu espaço de endereço. Isso significa que os primeiros 896 MB de memória física no sistema são mapeados no espaço de endereço virtual do kernel, deixando apenas 128 MB de espaço de endereço virtual. Esses 128 MB de espaço de endereço virtual são usados ​​para operações como vmalloc e kmap.

Esse mecanismo de mapeamento funciona bem se a capacidade de memória física for pequena (menos de 1 GB). No entanto, todos os servidores atuais suportam dezenas de gigabytes de memória. A Intel introduziu um mecanismo de extensão de endereço físico (PAE) em seu processador Pentium, que pode suportar até 64 GB de memória física. O mecanismo de mapeamento de memória mencionado acima torna o manuseio de memória física de até dezenas de gigabytes uma fonte importante de problemas para o Linux x86. O kernel Linux lida com memória high-end (toda a memória acima de 896 MB) da seguinte forma: Quando o kernel Linux precisa endereçar uma página na memória high-end, ele mapeia a página para uma pequena janela de espaço de endereço virtual através da operação kmap. uma operação na página e, em seguida, desmapear a página. O espaço de endereçamento de uma arquitetura de 64 bits é enorme, portanto, esse tipo de sistema não apresenta esse problema.

mecanismo de paginação

A memória virtual pode ser implementada de várias maneiras, sendo a mais eficiente uma solução baseada em hardware. O espaço de endereço virtual é dividido em blocos de memória de tamanho fixo, chamados de páginas. Os acessos à memória virtual são traduzidos em endereços de memória física por meio de tabelas de páginas. Para suportar várias arquiteturas e tamanhos de página, o Linux usa um mecanismo de paginação de três níveis. Ele fornece os três tipos de tabela de página a seguir:

Page Global Directory (PGD)
, Page Middle Directory (PMD) e a tradução de endereços da
Tabela de Páginas (PTE)
fornecem um método para separar o espaço de endereço virtual de um processo do espaço de endereço físico. Cada página de memória virtual pode ser marcada como "presente" ou "ausente" na memória principal. Se um processo acessar um endereço de memória virtual que não existe, o hardware gerará uma falha de página e o kernel o tratará. A página é colocada na memória principal quando o kernel trata o erro. Durante esse processo, o sistema pode precisar substituir uma página existente para liberar espaço para a nova página.

A estratégia de substituição é um dos aspectos mais críticos do sistema de paginação. A versão Linux 2.6 corrige problemas com versões anteriores do Linux em relação a várias seleções e substituições de páginas.

mecanismo de troca

A troca é o processo de mover um processo inteiro para dentro ou para fora do armazenamento secundário quando a memória principal está ficando sem capacidade. Devido à alta sobrecarga de troca de contexto, muitos sistemas operacionais modernos, incluindo o Linux, não usam essa abordagem, mas usam um mecanismo de paginação. No Linux, a troca é executada no nível da página e não no nível do processo. A principal vantagem da troca é que ela expande o espaço de endereço disponível para um processo. Quando o kernel precisa liberar memória para abrir espaço para novas páginas, algumas páginas menos usadas ou não utilizadas podem precisar ser descartadas. Algumas páginas não são facilmente liberadas porque não são armazenadas em disco e precisam ser copiadas para o armazenamento de backup (swap) e lidas novamente do armazenamento de apoio quando necessário. A principal desvantagem do mecanismo de troca é que ele é lento. As leituras e gravações de disco geralmente são muito lentas, portanto, a troca deve ser eliminada o máximo possível.

3) Mecanismo de processo

Processos, tarefas e threads do kernel

Uma tarefa é apenas uma "descrição geral do trabalho que precisa ser feito", que pode ser um segmento leve ou um processo completo.

Um thread é a instância de tarefa mais leve. O custo de criação de uma thread no kernel pode ser alto ou baixo, dependendo das características que a thread precisa ter. No caso mais simples, um encadeamento compartilha todos os recursos com seu encadeamento pai, incluindo código, dados e muitas estruturas de dados internas, com apenas uma pequena diferença na distinção desse encadeamento de outros encadeamentos.

Um processo no Linux é uma estrutura de dados "pesada". Se necessário, vários threads podem ser executados em um único processo (e compartilhar alguns recursos desse processo). No Linux, um processo é apenas uma thread com características de peso total. Threads e processos são escalonados da mesma forma pelo escalonador.

Um thread de kernel é um thread que sempre é executado no modo kernel e não possui contexto de usuário. Os threads do kernel geralmente existem para uma função específica e é fácil manipulá-los no kernel. Os threads do kernel geralmente têm a função desejada: poder ser agendado como qualquer outro processo; quando outros processos precisam dessa funcionalidade para funcionar, forneça a esses processos um thread de destino que implemente essa funcionalidade (enviando um sinal).

Agendamento e mudança de contexto

O escalonamento de processos é a ciência (alguns chamam de arte) de garantir que cada processo receba uma parte justa da CPU. Sempre há discordância sobre a definição de "justiça" porque os programadores geralmente fazem escolhas com base em informações que não são óbvias e visíveis.

Deve-se notar que muitos usuários de Linux acreditam que um escalonador que está correto o tempo todo é mais importante do que um escalonador que está completamente correto na maior parte do tempo, ou seja, um processo de execução lenta é preferível a um escalonador cuidadosamente escolhido. processo que para de ser executado devido a política ou erro. O programa agendador atual do Linux segue este princípio.

Quando um processo para de ser executado e é substituído por outro processo, ele é chamado de troca de contexto. Normalmente, essa operação é cara e os programadores de kernel e programadores de aplicativos sempre tentam minimizar o número de trocas de contexto que o sistema executa. Um processo pode parar de executar ativamente porque está esperando por algum evento ou recurso, ou desistir passivamente porque o sistema decide que a CPU deve ser alocada para outro processo. Para o primeiro caso, a CPU pode realmente entrar em um estado ocioso se não houver outros processos esperando para serem executados. No segundo caso, o processo é substituído por outro processo em espera ou atribuído a uma nova fatia de tempo de execução ou período de tempo para continuar a execução.

Mesmo enquanto um processo está sendo agendado e executado de forma ordenada, ele pode ser interrompido por outras tarefas de maior prioridade. Por exemplo, se o disco estiver pronto com dados para uma leitura de disco, ele envia um sinal para a CPU e espera que a CPU busque os dados do disco. O kernel deve lidar com essa situação em tempo hábil, caso contrário, reduzirá a taxa de transferência do disco. Sinais, interrupções e exceções são eventos assíncronos diferentes, mas semelhantes em muitos aspectos, e todos devem ser tratados rapidamente, mesmo que a CPU já esteja ocupada.

Por exemplo, um disco pronto para dados pode causar uma interrupção. O kernel chama o manipulador de interrupção para esse dispositivo específico, interrompendo o processo em execução no momento e usando seus muitos recursos. Quando a execução do manipulador de interrupção termina, o processo atualmente em execução retoma a execução. Isso efetivamente rouba o tempo de CPU do processo atualmente em execução, porque as versões atuais do kernel apenas medem o tempo decorrido desde que o processo entrou na CPU, ignorando o fato de que as interrupções consomem um tempo precioso para o processo.

Os manipuladores de interrupção geralmente são muito rápidos e compactos e, portanto, podem ser processados ​​e limpos rapidamente para permitir a entrada de dados subsequentes. Mas, às vezes, uma interrupção pode precisar lidar com mais trabalho do que o esperado no manipulador de interrupção em um curto período de tempo. As interrupções também precisam de um ambiente bem definido para fazer seu trabalho (lembre-se, as interrupções utilizam os recursos de algum processo aleatório). Nesse caso, reúna informações suficientes para atrasar o envio do trabalho ao manipulador da metade inferior para processamento. O manipulador da metade inferior está programado para ser executado de tempos em tempos. Embora o mecanismo da metade inferior fosse comumente usado em versões anteriores do Linux, seu uso é desencorajado nas versões atuais do Linux.

4) Mecanismo de plataforma baseado em Linux

Comparado com o mecanismo device_driver tradicional, o mecanismo de driver de plataforma do Linux tem uma vantagem muito óbvia, pois o mecanismo de plataforma registra seus próprios recursos no kernel, que é gerenciado pelo kernel de maneira uniforme. é usada a interface fornecida por platform_device. Aplicar e usar. Isso melhora a independência dos drivers e do gerenciamento de recursos e tem melhor portabilidade e segurança. A seguir está um diagrama esquemático da hierarquia do driver SPI. O barramento SPI no Linux pode ser entendido como o barramento derivado do controlador SPI:

Assim como os drivers tradicionais, o mecanismo da plataforma também é dividido em três etapas:

1. Etapa de registro de ônibus:

Kernel_init()→do_basic_setup()→driver_init()→platform_bus_init()→bus_register(&platform_bus_type) no arquivo main.c durante a inicialização do kernel registra um barramento de plataforma (barramento virtual, platform_bus).

2. Adicione o estágio do equipamento:

Quando o dispositivo estiver registrado, Platform_device_register()→platform_device_add()→ (pdev→dev.bus = &platform_bus_type)→device_add(), basta travar o dispositivo no barramento virtual.

3. Etapa de registro do motorista:

Platform_driver_register() →driver_register() →bus_add_driver() →driver_attach()→bus_for_each_dev(), faça __driver_attach()→driver_probe_device() para cada dispositivo pendurado no barramento da plataforma virtual, julgue drv→bus→match() Se a execução é bem sucedido, execute platform_match→strncmp(pdev→name , drv→name , BUS_ID_SIZE ) através do ponteiro neste momento, se corresponder, chame really_probe (realmente execute o platform_driver→probe(platform_device) do dispositivo correspondente.) Inicie a detecção real , se o teste for bem-sucedido, o dispositivo será vinculado ao driver.

Como pode ser visto acima, o mecanismo da plataforma finalmente chama as três funções principais de bus_register(), device_add() e driver_register().

Aqui estão algumas estruturas:

struct platform_device 
(/include/linux/Platform_device.h)
{ 
const char * name; 
int id; 
struct device dev; 
u32 num_resources; 
struct resource * resource;
};

A estrutura Platform_device descreve um dispositivo de uma estrutura de plataforma, que contém a estrutura geral do dispositivo struct device dev; a estrutura de recurso do dispositivo struct resource * resource; e o nome do dispositivo const char * name. (Observe que este nome deve ser o mesmo que platform_driver.driver àname posteriormente, o motivo será explicado posteriormente.)

A coisa mais importante nesta estrutura é a estrutura de recursos, razão pela qual o mecanismo de plataforma é introduzido.

struct resource 
( /include/linux/ioport.h)
{ 
resource_size_t start; 
resource_size_t end; 
const char *name; 
unsigned long flags; 
struct resource *parent, *sibling, *child;
};

O bit de flags indica o tipo do recurso, e start e end indicam o endereço inicial e final do recurso respectivamente (/include/linux/Platform_device.h):

struct platform_driver 
{ 
int (*probe)(struct platform_device *); 
int (*remove)(struct platform_device *); 
void (*shutdown)(struct platform_device *); 
int (*suspend)(struct platform_device *, pm_message_t state); 
int (*suspend_late)(struct platform_device *, pm_message_t state); 
int (*resume_early)(struct platform_device *); 
int (*resume)(struct platform_device *); 
struct device_driver driver;
};
Platform_driver

A estrutura Platform_driver descreve o driver de uma estrutura de plataforma. Além de alguns ponteiros de função, há também uma estrutura geral de driver device_driver.

Razões para ter o mesmo nome:

O driver mencionado acima chamará a função bus_for_each_dev() quando estiver registrado, e fará __driver_attach()→driver_probe_device() para cada dispositivo pendurado no barramento da plataforma virtual, nesta função, dev e drv serão correspondidos inicialmente. , que chama a função apontada por drv->bus->match. Na função platform_driver_register, drv->driver.bus = &platform_bus_type, então drv->bus->matc é platform_bus_type→match, que é a função platform_match. A função é a seguinte:

static int platform_match(struct device * dev, struct device_driver * drv) 
{ 
struct platform_device *pdev = container_of(dev, struct platform_device, dev);
return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0);
}

É para comparar os nomes de dev e drv. Se eles forem iguais, eles entrarão na função really_probe(), de modo a entrar na função de teste escrita por eles mesmos para mais correspondência. Portanto, dev→name e driver→drv→name devem ser preenchidos da mesma forma durante a inicialização.

Diferentes tipos de drivers têm diferentes funções de correspondência. O driver da plataforma compara os nomes de dev e drv. Lembra da correspondência no driver usb? Ele compara o ID do produto e o ID do fornecedor.

Os benefícios do mecanismo da plataforma:

1. Forneça um barramento do tipo platform_bus_type e inclua os dispositivos soc que não são tipos de barramento nesse barramento virtual. Assim, o modelo acionado por dispositivo de barramento pode ser popularizado.

2. Forneça estruturas de dados do tipo platform_device e platform_driver, incorpore estruturas de dados de driver e dispositivo tradicionais nelas e adicione membros de recursos para facilitar a integração com o novo tipo de bootloader e kernel que transfere dinamicamente recursos de dispositivo como Open Firmware.

2. Escreva seu próprio sistema operacional

Como vários aplicativos, o kernel também é um aplicativo, exceto que esse aplicativo opera diretamente o hardware. O kernel enfrenta diretamente o hardware, chama a interface de hardware e é desenvolvido através do conjunto de instruções fornecido por um fabricante de hardware e um fabricante de CPU. É muito mais simples desenvolver aplicativos em face do kernel, chamadas de sistema ou chamadas de biblioteca.

Para escrever aplicativos no nível do kernel, e para evitar um nível muito baixo, existem muitos arquivos de biblioteca inerentes que podem ser usados ​​quando o kernel é compilado.

O kernel é orientado diretamente ao hardware, então os recursos disponíveis têm grande autoridade, mas o kernel funciona em um espaço de endereço limitado. No que diz respeito ao linux, em um sistema de 32 bits, no espaço de endereço linear, o kernel que ele tem 1G, embora possa Master 4G, mas você só pode usar 1G para sua própria operação, e o 3G restante é para outros aplicativos. vitória é cada 2G. Portanto, o espaço de memória disponível quando desenvolvemos o kernel é muito limitado, principalmente quando desenvolvemos drivers, devemos entender que nosso espaço disponível é muito limitado, por isso precisa ser eficiente.

A arquitetura do kernel também é muito clara, desde a camada de hardware, camada de abstração de hardware, módulos básicos do kernel (escalonamento de processos, gerenciamento de memória, pilha de protocolos de rede, etc.) arquitetura que combina vários softwares e hardwares. Por exemplo, sistemas IoT (desde pequenos sistemas embarcados, como microcomputadores de chip único, MCUs, até casas inteligentes, comunidades inteligentes e até cidades inteligentes) podem ser referenciados nos dispositivos finais de acesso.

O Linux originalmente rodava em um PC, e o processador de arquitetura x86 usado era relativamente poderoso, e todos os tipos de instruções e modos eram relativamente completos. Por exemplo, o modo de usuário e o modo de kernel que vemos não estão disponíveis em pequenos processadores embarcados em geral. Sua vantagem é proteger o código e os dados no modo kernel, dando diferentes permissões ao código e aos segmentos de dados (incluindo recursos de hardware) devem ser acessado por meio de uma chamada de sistema semelhante (SysCall) para garantir a estabilidade do kernel.

O processo de escrever um sistema operacional:

Imagine se você fosse obrigado a escrever um sistema operacional, quais fatores você precisaria considerar?

Gerenciamento de processos: como alocar fatias de tempo de CPU de acordo com o algoritmo de agendamento em um sistema multitarefa.

Gerenciamento de memória: como mapear memória virtual e memória física, alocar e recuperar memória.

Sistema de arquivos: como organizar os setores do disco rígido em um sistema de arquivos para realizar a operação de leitura e gravação de arquivos.

Gerenciamento de dispositivos: como endereçar, acessar, ler e gravar informações e dados de configuração do dispositivo.

Gerenciamento de processos

Alguns processos são chamados de processos e alguns são chamados de tarefas em diferentes sistemas operacionais. A estrutura de dados do processo no sistema operacional contém muitos elementos, muitas vezes conectados por uma lista encadeada.

O conteúdo relacionado ao processo inclui principalmente: espaço de endereço virtual, prioridade, ciclo de vida (bloqueio, pronto, execução etc.), recursos ocupados (como semáforos, arquivos etc.).

A CPU verifica os processos na fila de prontos (percorre a estrutura do processo na lista encadeada) quando cada interrupção de tick do sistema é gerada. do processo atualmente em execução (incluindo as informações da pilha, etc.), suspenda o processo atual e selecione um novo processo a ser executado, que é o escalonamento de processos.

A diferença na prioridade dos processos é a base básica para o escalonamento da CPU.O objetivo final do escalonamento é permitir que as atividades de alta prioridade obtenham imediatamente os recursos de computação da CPU (resposta instantânea) e que as tarefas de baixa prioridade sejam alocadas de forma justa aos recursos da CPU. Como é necessário salvar o contexto do processo, etc., a própria comutação do processo tem um custo, e o algoritmo de escalonamento também precisa considerar a eficiência da frequência de comutação do processo.

No início do sistema operacional Linux, o algoritmo round-robin de fatia de tempo (Round-Robin) é usado principalmente, e o kernel seleciona o processo de alta prioridade na fila de processos prontos para execução, e cada um é executado por um tempo igual. O algoritmo é simples e intuitivo, mas ainda faz com que alguns processos de baixa prioridade fiquem desprogramados por muito tempo. A fim de melhorar a justiça do agendamento, após o Linux 2.6.23, um agendador completamente justo chamado CFS (Completely Fair Scheduler) foi introduzido.

A CPU só pode executar um programa por vez. Quando o usuário está usando o aplicativo Youku para assistir a vídeos, ele está digitando e conversando no WeChat ao mesmo tempo. Youku e WeChat são dois programas diferentes. Por que eles parecem estar em execução ao mesmo tempo? O objetivo do CFS é fazer com que todos os programas pareçam estar rodando na mesma velocidade em múltiplas CPUs paralelas, ou seja, processos nr_running, cada um rodando concorrentemente a uma velocidade de 1/nr_running, por exemplo, se houver 2 tarefas possíveis em execução, então cada executa simultaneamente em 50% da potência física da CPU.

O CFS introduz o conceito de "tempo de execução virtual", que é representado por p->se.vruntime (nanosec-unit), pelo qual registra e mede o "tempo de CPU" que uma tarefa deve obter. Em uma situação de escalonamento ideal, todas as tarefas devem ter o mesmo valor de p->se.vruntime em todos os momentos (as mencionadas acima são executadas na mesma velocidade). Como cada tarefa é executada simultaneamente, nenhuma tarefa excederá o tempo de CPU ideal que deveria ocupar. A lógica do CFS selecionando tarefas a serem executadas é baseada no valor de p->se.vruntime, que é muito simples: ele sempre escolhe a tarefa com o menor valor de p->se.vruntime a ser executada (a tarefa menos agendada) .

O CFS usa uma árvore vermelho-preta baseada em tempo para agendar a execução de tarefas futuras. Todas as tarefas são classificadas pela palavra-chave p->se.vruntime. O CFS seleciona a tarefa mais à esquerda a ser executada na árvore. À medida que o sistema é executado, as tarefas executadas são colocadas no lado direito da árvore, dando progressivamente a cada tarefa a chance de ser a tarefa mais à esquerda, ganhando assim recursos de CPU por um determinado período de tempo.

Resumindo, o CFS primeiro executa uma tarefa. Quando a tarefa é comutada (ou quando ocorre a interrupção Tick), o tempo de CPU usado pela tarefa será adicionado a p->se.vruntime, quando o valor de p->se .vruntime gradualmente Quando outra tarefa se torna a tarefa mais à esquerda da árvore vermelho-preta (ao mesmo tempo, uma pequena distância de granularidade é adicionada entre a tarefa e a tarefa mais à esquerda para evitar troca excessiva de tarefas e afetar o desempenho), a tarefa mais à esquerda é selecionado para execução , a tarefa atual é preemptada.

CFS árvore vermelho-preta

Em geral, o escalonador lida com tarefas individuais e tenta dar a cada tarefa uma quantidade razoável de tempo de CPU. Em algum momento, pode ser desejável agrupar tarefas e dar a cada grupo uma parte justa do tempo de CPU. Por exemplo, o sistema pode alocar um tempo médio de CPU para cada usuário e, em seguida, alocar um tempo médio de CPU para cada tarefa para cada usuário.

gerenciamento de memória

A memória em si é um dispositivo de armazenamento externo.O sistema precisa endereçar a área de memória, encontrar a célula de memória correspondente e ler e gravar os dados nela.

A área de memória é endereçada por ponteiros e o comprimento de bytes da CPU (máquina de 32 bits, máquina de 64 bits) determina o maior espaço de endereço endereçável. O espaço máximo de endereçamento em uma máquina de 32 bits é de 4 GBtyes. Em uma máquina de 64 bits existem teoricamente 2^64Bytes.

O maior espaço de endereço não tem nada a ver com a quantidade de memória física que o sistema real possui, por isso é chamado de espaço de endereço virtual. Para todos os processos no sistema, parece que cada processo ocupa esse espaço de endereço de forma independente e não pode perceber o espaço de memória de outros processos. O fato de o sistema operacional permitir que os aplicativos não precisem prestar atenção em outros aplicativos, parece que cada tarefa é o único processo em execução neste computador.

O Linux divide o espaço de endereço virtual em espaço do kernel e espaço do usuário. O espaço virtual para cada processo do usuário varia de 0 a TASK_SIZE. A área de TASK_SIZE a 2^32 ou 2^64 é reservada para o kernel e não pode ser acessada pelos processos do usuário. TASK_SIZE pode ser configurado, a configuração padrão do sistema Linux é 3:1, o aplicativo usa 3 GB de espaço e o kernel usa 1 GB de espaço. Esta divisão não depende do tamanho real da RAM. Em uma máquina de 64 bits, o espaço de endereço virtual pode ser muito grande, mas apenas 42 ou 47 bits (2^42 ou 2^47) são realmente usados.

espaço de endereço virtual

Na grande maioria dos casos, o espaço de endereço virtual é maior que a memória física (RAM) disponível para o sistema real, e o kernel e a CPU devem considerar como mapear a memória física real disponível no espaço de endereço virtual.

Uma maneira é mapear endereços virtuais para endereços físicos por meio da Tabela de Páginas. Os endereços virtuais estão relacionados aos endereços do usuário e do kernel usados ​​pelo processo, e os endereços físicos são usados ​​para endereçar a RAM real usada.

Conforme mostrado na figura abaixo, os espaços de endereços virtuais dos processos A e B são divididos em partes de tamanhos iguais chamadas páginas. A memória física também é dividida em páginas (quadros de página) de tamanho igual.

Mapeamento de espaço de endereço virtual e físico

A primeira página de memória do processo A é mapeada para a quarta página da memória física (RAM); a primeira página de memória do processo B é mapeada para a quinta página da memória física. A quinta página de memória do processo A e a primeira página de memória do processo B são mapeadas para a quinta página da memória física (o kernel pode decidir qual espaço de memória é compartilhado por diferentes processos).

Conforme mostrado na figura, nem todas as páginas no espaço de endereço virtual estão associadas a um quadro de página. A página não pode ser usada ou os dados não foram carregados na memória física (não necessários no momento) ou podem ser porque a página de memória física é substituída no disco rígido e, em seguida, recolocada na memória quando for realmente necessária mais tarde.

A tabela de páginas mapeia o espaço de endereço virtual para o espaço de endereço físico. A maneira mais fácil de fazer isso é usar uma matriz para mapear páginas virtuais para páginas físicas um a um, mas isso pode exigir o consumo de toda a RAM para manter essa tabela de páginas, supondo que cada página tenha 4 KB de tamanho e o tamanho espaço de endereço virtual tem 4 GB de tamanho, você precisa de uma matriz de 1 milhão de elementos para conter a tabela de páginas.

Como a maioria das áreas do espaço de endereço virtual não é realmente usada e essas páginas não estão realmente associadas ao quadro de página, a introdução da paginação multinível pode reduzir bastante a memória usada pela tabela de página e melhorar a eficiência da consulta. Para a descrição detalhada da tabela multinível, consulte xxx.

O mapeamento de memória é uma abstração importante que é usada em muitos lugares, como o kernel e os aplicativos do usuário. Mapear é transferir dados de uma fonte de dados (também pode ser uma porta de E/S de um dispositivo, etc.) para o espaço de memória virtual de um processo. As operações no espaço de endereço mapeado podem usar o método de lidar com a memória comum (leitura e escrita direta do conteúdo do endereço). Quaisquer alterações na memória serão transferidas automaticamente para a fonte de dados original, como mapear o conteúdo de um arquivo na memória, você só precisa ler a memória para obter o conteúdo do arquivo e gravar as alterações na memória para modificar o conteúdo do arquivo, o kernel garante que quaisquer alterações sejam refletidas automaticamente no arquivo.

Além disso, no kernel, ao implementar drivers de dispositivos, as áreas de entrada e saída dos periféricos (dispositivos externos) podem ser mapeadas para espaços de endereçamento virtuais, e a leitura e escrita desses espaços serão redirecionadas para o dispositivo pelo sistema, de forma que o dispositivo pode ser operado, implementação de driver muito simplificada.

O kernel precisa acompanhar quais páginas físicas foram alocadas e quais ainda estão livres para evitar dois processos usando a mesma área de RAM. A alocação e liberação de memória são tarefas muito frequentes. O kernel deve garantir que a velocidade de conclusão seja a mais rápida possível. O kernel só pode alocar o quadro de página inteiro. Ele atribui ao usuário a tarefa de dividir a memória em partes menores space, a biblioteca do programa de espaço do usuário O quadro de página recebido do kernel pode ser dividido em regiões menores e alocado ao processo.

sistema de arquivos virtuais

Os sistemas Unix são construídos em algumas ideias perspicazes, uma metáfora muito importante é:

Tudo é um arquivo.

Ou seja, quase todos os recursos do sistema podem ser considerados como arquivos. Para suportar diferentes sistemas de arquivos locais, o kernel inclui uma camada de sistema de arquivos virtual (Virtual File System) entre o processo do usuário e a implementação do sistema de arquivos. A maioria das funções fornecidas pelo kernel podem ser acessadas através da interface de arquivos definida pelo VFS (Virtual File System). Por exemplo, subsistemas do kernel: dispositivos de caracteres e blocos, pipes, soquetes de rede, terminais interativos de entrada e saída, etc.

Além disso, os arquivos de dispositivo usados ​​para operar dispositivos de caracteres e blocos são arquivos reais no diretório /dev. Quando as operações de leitura e gravação são realizadas, seu conteúdo será criado dinamicamente pelo driver de dispositivo correspondente.

Sistema VFS

Em um sistema de arquivos virtual, os inodes são usados ​​para representar arquivos e diretórios de arquivos (para o sistema, um diretório é um tipo especial de arquivo). Os elementos do inode incluem duas categorias: 1. Os metadados são usados ​​para descrever o estado do arquivo, como permissões de leitura e gravação. 2. O segmento de dados usado para salvar o conteúdo do arquivo.

Cada inode possui um número especial para identificação exclusiva, e a associação entre o nome do arquivo e o inode é baseada no número. Pegue o kernel para encontrar /usr/bin/emacs como um exemplo para explicar como os inodes formam a estrutura de diretórios do sistema de arquivos. A pesquisa começa a partir do inode raiz (ou seja, o diretório raiz '/'), que é representado por um inode. O segmento de dados do inode não possui dados comuns, mas contém apenas alguns itens de arquivo/diretório armazenados no diretório raiz . Esses itens podem representar arquivos ou outro diretório, cada item contém duas partes: 1. O número do inode onde o próximo item de dados está localizado 2. O nome do arquivo ou diretório

Primeiro escaneie a área de dados do inode raiz até que uma entrada chamada 'usr' seja encontrada, procurando por inodes no subdiretório usr. Encontre o inode associado pelo número do inode 'usr'. Repita as etapas acima para encontrar o item de dados chamado 'bin', então procure o item de dados chamado 'emacs' no inode correspondente a 'bin' de seu item de dados, e o inode retornado representa um arquivo em vez de um diretório. O conteúdo do arquivo do último inode é diferente dos anteriores.Os três primeiros representam um diretório, contendo seus subdiretórios e listagens de arquivos.O inode associado ao arquivo emacs contém o conteúdo real do arquivo em seu segmento de dados.

Embora as etapas para localizar um arquivo no VFS sejam as mesmas descritas acima, existem algumas diferenças nos detalhes. Por exemplo, como abrir arquivos com frequência é uma operação lenta, a introdução de um cache pode acelerar as pesquisas.

Encontre um arquivo através do mecanismo inode:

 

driver do dispositivo

A comunicação com periféricos geralmente se refere a operações de entrada e saída, ou E/S para abreviar. Um núcleo de E/S que implementa um periférico deve lidar com três tarefas: Primeiro, o hardware deve ser endereçado de maneiras diferentes para diferentes tipos de dispositivos. Em segundo lugar, o kernel deve fornecer métodos para aplicativos de usuário e ferramentas de sistema para operar diferentes dispositivos, e um mecanismo uniforme é necessário para garantir o menor esforço de programação possível e para garantir que os aplicativos possam interagir uns com os outros mesmo com diferentes métodos de hardware. Terceiro, o espaço do usuário precisa saber quais dispositivos estão no kernel.

A relação hierárquica com os periféricos é a seguinte:

Diagrama de hierarquia de comunicação do dispositivo

A maioria dos dispositivos externos é conectada à CPU através do barramento, e o sistema geralmente tem mais de um barramento, mas um conjunto de barramentos. Muitos projetos de PC incluem dois barramentos PCI conectados por uma ponte. Alguns barramentos, como o USB, não podem ser usados ​​como barramento principal e precisam passar dados para o processador por meio de um barramento do sistema. O diagrama abaixo mostra como os diferentes barramentos estão conectados ao sistema.

 

Topologia de barramento do sistema

O sistema interage com os periféricos principalmente das seguintes maneiras:

Porta de E/S: No caso de usar comunicação de porta de E/S, o kernel envia os dados através de um controlador de E/S, cada dispositivo receptor possui um número de porta exclusivo e encaminha os dados para o hardware conectado ao sistema. Há um espaço de endereço virtual separado gerenciado pelo processador para gerenciar todos os endereços de E/S.

O espaço de endereço de E/S nem sempre está associado à memória normal do sistema, o que geralmente é difícil de entender, considerando que as portas podem ser mapeadas na memória.

Existem diferentes tipos de portos. Alguns são somente leitura, outros somente gravação e, em geral, podem operar em ambas as direções, e os dados podem ser trocados em ambas as direções entre o processador e os periféricos.

Na arquitetura IA-32, o espaço de endereço da porta contém 2^16 endereços diferentes de 8 bits, que podem ser identificados exclusivamente por números de 0x0 a 0xFFFFH. Cada porta tem um dispositivo atribuído a ela, ou está ociosa e não utilizada, e vários periféricos não podem compartilhar uma porta. Em muitos casos, a troca de dados de 8 bits é insuficiente e, por esse motivo, duas portas consecutivas de 8 bits podem ser vinculadas a uma única porta de 16 bits. Duas portas consecutivas de 16 bits podem ser tratadas como uma porta de 32 bits, e o processador pode realizar operações de entrada e saída montando instruções.

Diferentes tipos de processadores implementam as portas de operação de forma diferente, o kernel deve fornecer uma camada de abstração adequada, como outb (escrever um byte), outw (escrever uma palavra) e inb (ler um byte).Esses comandos podem ser usados ​​para a porta de operação.

Mapeamento de memória de E/S: Muitos dispositivos devem poder ser endereçados como memória RAM. Portanto, o processador fornece a porta de E/S correspondente ao dispositivo periférico a ser mapeado na memória, para que o dispositivo possa ser operado como uma memória comum. Por exemplo, as placas gráficas usam esse mecanismo e o PCI geralmente é endereçado por endereços de E/S mapeados.

Para implementar o mapeamento de memória, as portas de E/S devem primeiro ser mapeadas na memória normal do sistema (usando funções específicas do processador). Como as implementações variam muito entre as plataformas, o kernel fornece uma camada de abstração para mapear e desmapear regiões de E/S.

Além de como acessar o periférico, quando o sistema saberá se o periférico tem dados para acessar? Existem duas formas principais: polling e interrupções.

O polling acessa periodicamente o dispositivo de consulta para ver se ele tem dados prontos e, em caso afirmativo, busca os dados. Esse método exige que o processador acesse continuamente o dispositivo mesmo quando o dispositivo não tiver dados, desperdiçando fatias de tempo da CPU.

Outra forma é interromper, a ideia é que após o periférico terminar algo, ele notificará ativamente a CPU, a interrupção tem a maior prioridade e interromperá o processo atual da CPU. Cada CPU fornece uma linha de interrupção (que pode ser compartilhada por diferentes dispositivos), cada interrupção é identificada por um número de interrupção único e o kernel fornece um método de serviço para cada interrupção utilizada (ISR, Interrupt Service Routine, ou seja, após a interrupção ocorre, a função de processamento chamada pela CPU), a própria interrupção também pode definir a prioridade.

As interrupções suspendem o trabalho normal do sistema. O periférico dispara uma interrupção quando os dados estão prontos para serem usados ​​pelo kernel ou indiretamente por um aplicativo. O uso de interrupções garante que o sistema notifique o processador apenas quando um periférico precisar que o processador intervenha, melhorando efetivamente a eficiência.

Controle de aparelhos via bus: Nem todos os aparelhos são endereçados e operados diretamente por meio de instruções I/O, mas em muitos casos por meio de um sistema de bus.

Nem todos os tipos de dispositivos podem ser conectados diretamente a todos os sistemas de barramento, como discos rígidos conectados a interfaces SCSI, mas não placas gráficas (as placas gráficas podem ser conectadas ao barramento PCI). O disco rígido deve ser conectado ao barramento PCI indiretamente por meio de IDE.

Os tipos de barramento podem ser divididos em barramento de sistema e barramento de expansão. As diferenças de implementação no hardware não são importantes para o núcleo, apenas como o barramento e seus periféricos conectados são endereçados. Para um barramento do sistema, como o barramento PCI, as instruções de E/S e os mapas de memória são usados ​​para se comunicar com o barramento, bem como com os dispositivos aos quais ele está conectado. O kernel também fornece alguns comandos para drivers de dispositivo chamarem funções de barramento, como acessar a lista de dispositivos disponíveis e ler e gravar informações de configuração em um formato uniforme.

Os barramentos de expansão, como USB, SCSI, trocam dados e comandos com dispositivos conectados por meio de um protocolo de barramento claramente definido. O kernel se comunica com o barramento por meio de instruções de E/S ou mapas de memória, e o barramento se comunica com dispositivos conectados por meio de funções independentes de plataforma.

A comunicação com um dispositivo conectado ao barramento não precisa necessariamente ser feita por meio de um driver no espaço do kernel, mas em alguns casos também pode ser feita no espaço do usuário. Um excelente exemplo é o SCSI Writer, endereçado pela ferramenta cdrecord. Essa ferramenta gera os comandos SCSI necessários, envia os comandos para o dispositivo correspondente através do barramento SCSI com a ajuda do kernel e processa e responde às informações geradas ou retornadas pelo dispositivo.

Dispositivos de bloco (bloco) e dispositivos de caractere (personagem) diferem significativamente de 3 maneiras:

Os dados em um dispositivo de bloco podem ser manipulados a qualquer momento, enquanto um dispositivo de caractere não.

As transferências de dados do dispositivo de bloco sempre usam blocos de tamanho fixo. O driver de dispositivo sempre obtém um bloco completo do dispositivo, mesmo quando apenas um byte é solicitado. Em contraste, os dispositivos de caracteres são capazes de retornar um único byte.

Ler e gravar em um dispositivo de bloco usa o cache. Para operações de leitura, os dados são armazenados em cache na memória e podem ser revisitados quando necessário. Em termos de operações de gravação, ele também será armazenado em cache para atrasar a gravação no dispositivo. Usar um cache não é razoável para dispositivos de caracteres (por exemplo, teclados), cada solicitação de leitura deve ser interagida de forma confiável com o dispositivo.

O conceito de blocos e setores: Um bloco é uma seqüência de bytes de um tamanho especificado que é usada para salvar os dados transferidos entre o kernel e o dispositivo.O tamanho do bloco pode ser definido. Um setor é um tamanho fixo, a menor quantidade de dados que pode ser transferida por um dispositivo. Um bloco é um segmento contíguo de setores e o tamanho do bloco é um múltiplo inteiro do setor.

rede

O subsistema de rede do Linux fornece uma base sólida para o desenvolvimento da Internet. O modelo de rede é baseado no modelo OSI da ISO, conforme mostrado na metade direita da figura abaixo. No entanto, em aplicações específicas, as camadas correspondentes são frequentemente combinadas para simplificar o modelo.A metade esquerda da figura abaixo é o modelo de referência TCP/IP usado pelo Linux. (Como há muitas informações sobre a parte da rede Linux, neste artigo é fornecida apenas uma breve introdução ao nível grande e nenhuma explicação é fornecida.)

A camada host-to-host (camada física e camada de enlace de dados, ou seja, a camada física e a camada de enlace de dados) é responsável pela transferência de dados de um computador para outro. Essa camada lida com as propriedades elétricas e de codec do meio de transmissão físico e também divide o fluxo de dados em quadros de dados de tamanho fixo para transmissão. Se vários computadores compartilham uma rota de transmissão, o adaptador de rede (placa de rede, etc.) deve ter um ID exclusivo (ou seja, endereço MAC) para distingui-lo. Do ponto de vista do kernel, esta camada é implementada através do driver de dispositivo da placa de rede.

A camada de rede do modelo OSI é chamada de camada de rede no modelo TCP/IP. A camada de rede permite que os computadores da rede troquem dados, e esses computadores não estão necessariamente conectados diretamente.

Se não houver conexão direta fisicamente, não haverá troca direta de dados. A tarefa da camada de rede é encontrar rotas para comunicação entre máquinas na rede.

computador conectado em rede

A camada de rede também é responsável por dividir o pacote a ser transmitido no tamanho especificado, pois o tamanho máximo do pacote suportado por cada computador no caminho de transmissão pode ser diferente. a extremidade receptora são combinados.

A camada de rede atribui endereços de rede exclusivos aos computadores na rede para que eles possam se comunicar uns com os outros (em oposição aos endereços MAC de hardware, pois as redes geralmente são compostas de sub-redes). Na Internet, a camada de rede consiste em redes IP e existem versões V4 e V6.

A tarefa da camada de transporte é regular a transferência de dados entre aplicativos executados em dois computadores conectados. Por exemplo, programas cliente e servidor em dois computadores, incluindo conexões TCP ou UDP, identificam aplicativos de comunicação por números de porta. Por exemplo, a porta número 80 é usada para o servidor web e o cliente do navegador deve enviar solicitações para essa porta para obter os dados necessários. O cliente também precisa ter um número de porta exclusivo para que o servidor da Web possa enviar respostas a ele.

Essa camada também é responsável por fornecer uma conexão confiável (no caso do TCP) para transmissão de dados.

A camada de aplicação no modelo TCP/IP está incluída no modelo OSI (camada de sessão, camada de apresentação, camada de aplicação). Quando uma conexão de comunicação é estabelecida entre duas aplicações, esta camada é responsável pela transmissão real do conteúdo. Por exemplo, o protocolo e os dados transmitidos entre o servidor web e seu cliente são diferentes daqueles entre o servidor de correio e seu cliente.

A maioria dos protocolos de rede são definidos em RFC (Request for Comments).

Modelo em camadas de implementação de rede: A implementação da camada de rede pelo kernel é semelhante ao modelo de referência TCP/IP. Ele é implementado em código C e cada camada só pode se comunicar com suas camadas superior e inferior, com a vantagem de que diferentes protocolos e mecanismos de transmissão podem ser combinados. Como mostrado abaixo:

4. Explicação detalhada dos módulos do kernel Linux

O kernel não é mágico, mas é essencial para qualquer computador em funcionamento. O kernel do Linux difere do OS X e do Windows, pois contém drivers no nível do kernel e faz muitas coisas funcionarem "fora da caixa".

E se o Windows já instalou todos os drivers disponíveis e você só precisa abrir o driver que você precisa?Isso é essencialmente o que os módulos do kernel fazem para o Linux. Os módulos do kernel, também conhecidos como módulos de kernel carregáveis ​​(LKMs), são essenciais para manter o kernel funcionando com todo o hardware sem consumir toda a memória disponível. 

Os módulos normalmente adicionam funcionalidades como dispositivos, sistemas de arquivos e chamadas de sistema ao kernel base. A extensão do arquivo lkm é .ko e geralmente é armazenada no diretório /lib/modules. Devido à natureza dos módulos, você pode personalizar facilmente o kernel configurando o módulo para carregar ou não carregar no momento da inicialização usando o comando menuconfig ou editando o arquivo /boot/config ou carregando e descarregando módulos dinamicamente usando o comando modprobe .

Módulos de terceiros e de código fechado estão disponíveis em algumas distribuições, como Ubuntu, e podem não ser instalados por padrão porque o código-fonte desses módulos não está disponível. Os desenvolvedores deste software (ou seja, nVidia, ATI, etc.) não fornecem o código-fonte, mas constroem seus próprios módulos e compilam os arquivos .ko necessários para distribuição. Embora esses módulos sejam livres como a cerveja, eles não são livres como a fala e, portanto, não são incluídos em algumas distribuições porque os mantenedores acreditam que "polui" o kernel fornecendo software não-livre.

Vantagens de usar módulos:

1. Tornar o kernel mais compacto e flexível
2. Ao modificar o kernel, não é necessário recompilar o kernel inteiro, o que pode economizar muito tempo e evitar erros manuais. Se você precisar usar um novo módulo no sistema, basta compilar o módulo correspondente e inserir o módulo usando um programa de espaço do usuário específico.
3. Os módulos podem não depender de uma plataforma de hardware fixa.
4. Uma vez que o código-objeto do módulo é vinculado ao kernel, sua função é exatamente a mesma do código-objeto do kernel vinculado estaticamente. Portanto, nenhuma passagem de mensagem explícita é necessária ao chamar uma função de um módulo.

No entanto, a introdução de módulos do kernel também traz alguns problemas:

1. Como a memória ocupada pelo kernel não será trocada, os módulos vinculados ao kernel trarão certas perdas de desempenho e utilização de memória para todo o sistema.
2. O módulo carregado no kernel torna-se parte do kernel e pode modificar outras partes do kernel, portanto, o uso indevido do módulo causará o travamento do sistema.
3. Para que um módulo do kernel acesse todos os recursos do kernel, o kernel deve manter uma tabela de símbolos e modificar a tabela de símbolos quando os módulos são carregados e descarregados.
4. Os módulos exigirão o uso de funções de outros módulos, então o kernel precisa manter dependências entre os módulos.

Os módulos são executados no mesmo espaço de endereço que o kernel, e a programação do módulo é a programação do kernel em certo sentido. Mas os módulos não estão disponíveis em todo o kernel. Módulos são geralmente usados ​​em drivers de dispositivos, sistemas de arquivos, etc., mas para lugares extremamente importantes no kernel do Linux, como gerenciamento de processos e gerenciamento de memória, ainda é difícil conseguir através de módulos, e geralmente o kernel deve ser modificado diretamente.

No programa de origem do kernel Linux, as funções que são frequentemente implementadas pelos módulos do kernel incluem sistemas de arquivos, drivers avançados SCSI, a maioria dos drivers SCSI, a maioria dos drivers de CD-ROM, drivers Ethernet e assim por diante.

1. Compile e instale o kernel do Linux

Componentes do kernel Linux:

  • kernel: núcleo do kernel, geralmente bzImage, geralmente no diretório /boot

    vmlinuz-VERSION-RELEASE
  • objeto kernel: objeto kernel, geralmente colocado em

    /lib/modules/VERSION-RELEASE/
  • Arquivo auxiliar: ramdisk

    initrd-VERSION-RELEASE.img:从CentOS 5 版本以前
    initramfs-VERSION-RELEASE.img:从CentOS6 版本以后

Verifique a versão do kernel:

uname -r
-r 显示VERSION-RELEASE
-n  打印网络节点主机名
-a  打印所有信息

comandos do módulo do kernel

As chamadas de sistema são, obviamente, uma maneira viável de inserir módulos do kernel no kernel. Mas é um nível muito baixo. Além disso, existem duas maneiras de conseguir isso no ambiente Linux. Um método é um pouco mais automático, que pode ser carregado automaticamente quando necessário e descarregado quando não for necessário. Este método requer a execução do programa modprobe.

A outra é usar o comando insmod para carregar manualmente o módulo do kernel. Na análise anterior do exemplo helloworld, mencionamos que o papel do insmod é inserir o módulo que precisa ser inserido no kernel na forma de código objeto. Observe que apenas superusuários podem usar esse comando.

A maioria das chamadas de sistema fornecidas pelo mecanismo do módulo do kernel Linux são usadas pelo programa modutils. Pode-se dizer que a combinação do mecanismo do módulo do kernel Linux e modutils fornece a interface de programação do módulo. modutils (modutils-xyztar.gz) pode ser obtido onde quer que o código-fonte do kernel seja obtido. Selecione o nível de patch de nível mais alto xyz igual ou menor que a versão atual do kernel. Após a instalação, haverá insmod, rmmod, ksyms, lsmod no /sbin diretório , modprobe e outros utilitários. Claro, geralmente quando carregamos o kernel do Linux, o modutils já foi carregado.

comando lsmod:

  • mostre os módulos do kernel que foram carregados pelo kernel
  • O conteúdo exibido vem de: arquivo /proc/modules

Na verdade, a função deste programa é ler as informações do arquivo /proc/modules no sistema de arquivos /proc. Portanto, este comando é equivalente a cat /proc/modules. Seu formato é:

[root@centos8 ~]#lsmod 
Module                 Size Used by
uas                    28672  0
usb_storage            73728  1 uas
nls_utf8               16384  0
isofs                  45056  0 #显示:名称、大小,使用次数,被哪些模块依赖

comando ksyms:

Exibe informações sobre símbolos do kernel e tabelas de símbolos do módulo e pode ler o arquivo /proc/kallsyms.

comando modinfo:

Função: gerenciar módulos do kernel

Arquivo de configuração:

/etc/modprobe.conf, /etc/modprobe.d/*.conf
  • Exibe informações detalhadas da descrição do módulo
modinfo [ -k kernel ]  [ modulename|filename... ]

Opções comuns:

-n:只显示模块文件路径
-p:显示模块参数
-a:作者
-d:描述

Caso:

lsmod |grep xfs 
modinfo  xfs

comando insmod:

Especifique o arquivo de módulo, não resolva módulos dependentes automaticamente. Um programa simples para inserir um módulo no kernel do Linux.

gramática:

insmod [ filename ]  [ module options... ]

Caso:

insmod 
modinfo –n exportfs

lnsmod 
modinfo –n xfs

insmod é na verdade um utilitário do módulo modutils. Quando usamos este comando como superusuário, este programa completa a seguinte série de tarefas:

1. Leia o nome do módulo a ser vinculado a partir da linha de comando, geralmente um arquivo objeto com a extensão ".ko" e formato elf.
2. Determine a localização do arquivo onde o código de objeto do módulo está localizado. Normalmente este arquivo está em um subdiretório de lib/modules.
3. Calcule o tamanho da memória necessária para armazenar o código do módulo, o nome do módulo e o objeto do módulo.
4. Aloque uma área de memória no espaço do usuário, copie o objeto do módulo, o nome do módulo e o código do módulo realocado para o kernel em execução nesta memória. Entre eles, o campo init no objeto módulo aponta para o endereço reatribuído pela função de entrada deste módulo; o campo de saída aponta para o endereço reatribuído pela função de saída.
5. Chame init_module() e passe o endereço da área de memória user-mode criada acima.Analisamos o processo de implementação em detalhes.
6. Libere a memória do modo de usuário e todo o processo termina.

comando modprobe:

  • Adicionando e removendo módulos no kernel Linux
modprobe [ -C config-file ] [ modulename ] [ module parame-ters... ] modprobe [ -r ] modulename…

Opções comuns:

-C:使用文件
-r:删除模块

uso: 

装载:modprobe 模块名 
卸载: modprobe -r 模块名 # rmmod命令:卸载模块

modprobe é um programa fornecido pelo modutils que insere módulos automaticamente com base nas dependências entre os módulos. O método de carregamento de módulo de carregamento sob demanda mencionado anteriormente chamará este programa para realizar a função de carregamento sob demanda. Por exemplo, se o módulo A depender do módulo B e o módulo B não for carregado no kernel, quando o sistema solicitar o carregamento do módulo A, o programa modprobe carregará automaticamente o módulo B no kernel.

Semelhante ao insmod, o programa modprobe também vincula um módulo especificado na linha de comando, mas também pode vincular recursivamente outros módulos referenciados pelo módulo especificado. Em termos de implementação, o modprobe apenas verifica as dependências do módulo, e o trabalho real de carregamento ainda é implementado pelo insmod. Então, como ele conhece as dependências entre os módulos? Simplificando, o modprobe aprende sobre essa dependência através de outro programa modutils, o depmod. E depmod encontra todos os módulos no kernel e escreve as dependências entre eles em um arquivo chamado modules.dep no diretório /lib/modules/2.6.15-1.2054_FC5.

comando kmod:

Nas versões anteriores do kernel, o mecanismo de carregamento automático do módulo era implementado por um processo de usuário kerneld. o programa modprobe para carregar o módulo. Mas em versões recentes do kernel, outro método kmod é usado para atingir essa função. Comparado com o kerneld, a maior diferença entre o kmod é que ele é um processo rodando no espaço do kernel, podendo chamar diretamente o modprobe no espaço do kernel, o que simplifica bastante todo o processo.

comando depmod:

Uma ferramenta para gerar arquivos de dependência do módulo do kernel e arquivos de mapeamento de informações do sistema, gerando módulos .dep e arquivos de mapa.

comando rmmod:

Uninstall module, um programa simples para remover um módulo do kernel Linux.

O programa rmmod removerá do kernel o módulo que foi inserido no kernel, e o rmmod executará automaticamente a função exit definida pelo próprio módulo do kernel. Seu formato é:

rmmod xfs
rmmod exportfs

Claro, ele é implementado por meio da chamada de sistema delete_module(). 

compilar o núcleo

Compile e instale a preparação do kernel:

(1) Preparar o ambiente de desenvolvimento; 

(2) Obter as informações relevantes do dispositivo de hardware no host de destino;

(3) Obter informações relevantes sobre a função do sistema host de destino, por exemplo: o sistema de arquivos correspondente precisa ser ativado;

(4) Obtenha o pacote de código fonte do kernel, www.kernel.org;

Compilar preparação

Informações sobre o dispositivo de hardware do host de destino

CPU:

cat /proc/cpuinfo
x86info -a
lscpu

Dispositivos PCI: lspci -v , -vv:

[root@centos8 ~]#lspci
00:00.0 Host bridge: Intel Corporation 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)
00:01.0 PCI bridge: Intel Corporation 440BX/ZX/DX - 82443BX/ZX/DX AGP bridge (rev 01)
00:07.0 ISA bridge: Intel Corporation 82371AB/EB/MB PIIX4 ISA (rev 08)
00:07.1 IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)
00:07.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 08)
00:07.7 System peripheral: VMware Virtual Machine Communication Interface (rev 10)
00:0f.0 VGA compatible controller: VMware SVGA II Adapter
00:10.0 SCSI storage controller: Broadcom / LSI 53c1030 PCI-X Fusion-MPT Dual Ultra320 SCSI (rev 01)
00:11.0 PCI bridge: VMware PCI bridge (rev 02)
00:15.0 PCI bridge: VMware PCI Express Root Port (rev 01)
00:15.1 PCI bridge: VMware PCI Express Root Port (rev 01)

Dispositivo USB: lsusb -v, -vv:

[root@centos8 ~]#dnf install usbutils -y
[root@centos8 ~]#lsusb
Bus 001 Device 004: ID 0951:1666 Kingston Technology DataTraveler 100 G3/G4/SE9 G2
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 003: ID 0e0f:0002 VMware, Inc. Virtual USB Hub
Bus 002 Device 002: ID 0e0f:0003 VMware, Inc. Virtual Mouse
Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
[root@centos8 ~]#lsmod |grep usb
usb_storage            73728  1 uas

dispositivo de bloco lsblk

Todas as informações do dispositivo de hardware: hal-device: CentOS 6

Pacotes relacionados ao ambiente de desenvolvimento

gcc make ncurses-devel flex bison openssl-devel elfutils-libelf-devel
  • Baixar arquivos de origem
  • Prepare o arquivo de configuração de texto /boot/.config
  • make menuconfig: configura as opções do kernel
  • make [-j #] ou faça em duas etapas:
    make -j # bzImage
    make -j # modules
  • Instalar módulos: make modules_install
  • Instale os arquivos relacionados ao kernel: make install
    • Instale bzImage como /boot/vmlinuz-VERSION-RELEASE
    • Gerar arquivo initramfs
    • Edite o arquivo de configuração do grub

Compile e instale o caso de verificação interna:

[root@centos7 ~]#yum -y install gcc gcc-c++ make ncurses-devel flex bison openssl-devel elfutils-libelf-devel
[root@centos7 ~]#tar xvf linux-5.15.51.tar.xz -C /usr/local/src
[root@centos7 ~]#cd /usr/local/src
[root@centos7 src]#ls
linux-5.15.51
[root@centos7 src]#du -sh *
1.2G	linux-5.15.51
[root@centos7 src]#cd linux-5.15.51/
[root@centos7 linux-5.15.51]#ls
arch   COPYING  Documentation  include  Kbuild   lib          Makefile  README   security  usr
block  CREDITS  drivers        init     Kconfig  LICENSES     mm        samples  sound     virt
certs  crypto   fs             ipc      kernel   MAINTAINERS  net       scripts  tools
[root@centos7 linux-5.15.51]#cp /boot/config-3.10.0-1160.el7.x86_64 .config
[root@centos7 linux-5.15.51]#vim .config
#修改下面三行
#CONFIG_MODULE_SIG=y    #注释此行
CONFIG_SYSTEM_TRUSTED_KEYRING=""    #修改此行
#CONFIG_DEBUG_INFO=y    #linux-5.8.5版本后需要注释此行

#升级gcc版本,可以到清华的镜像站上下载相关的依赖包
#https://mirrors.tuna.tsinghua.edu.cn/gnu/gcc/gcc-9.1.0/
#https://mirrors.tuna.tsinghua.edu.cn/gnu/gmp/
#https://mirrors.tuna.tsinghua.edu.cn/gnu/mpc/
#https://mirrors.tuna.tsinghua.edu.cn/gnu/mpfr/

[root@centos7 linux-5.15.51]#cd ..
[root@centos7 src]#tar xvf gcc-9.1.0.tar.gz
[root@centos7 src]#tar xvf gmp-6.1.2.tar.bz2 -C gcc-9.1.0/
[root@centos7 src]#cd gcc-9.1.0/
[root@centos7 gcc-9.1.0]#mv gmp-6.1.2 gmp
[root@centos7 gcc-9.1.0]#cd ..
[root@centos7 src]#tar xvf mpc-1.1.0.tar.gz -C gcc-9.1.0/
[root@centos7 src]#cd gcc-9.1.0/
[root@centos7 gcc-9.1.0]#mv mpc-1.1.0 mpc
[root@centos7 gcc-9.1.0]#cd ..
[root@centos7 src]#tar xvf mpfr-4.0.2.tar.gz -C gcc-9.1.0/
[root@centos7 src]#cd gcc-9.1.0/
[root@centos7 gcc-9.1.0]#mv mpfr-4.0.2 mpfr

#编译安装gcc
[root@centos7 gcc-9.1.0]#./configure --prefix=/usr/local/ --enable-checking=release --disable-multilib --enable-languages=c,c++ --enable-bootstrap
[root@centos7 gcc-9.1.0]#make -j 2  #CPU核数要多加,不然编译会很慢
[root@centos7 gcc-9.1.0]#make install

[root@centos7 gcc-9.1.0]#cd ..
[root@centos7 src]#cd linux-5.15.51/
[root@centos7 linux-5.15.51]#make help
[root@centos7 linux-5.15.51]#make menuconfig

Entre em [Configurações gerais] e pressione Enter:

Adicione a versão do kernel e pressione Enter:

Insira a versão personalizada do kernel e pressione Enter:

Pressione【Tab】, selecione【Sair】para sair:

Selecione [Sistema de Arquivos] e pressione Enter: 

Selecione [NT File System] e pressione Enter: 

Selecione [sistema de arquivos NTFS], pressione [Barra de espaço], M significa modo modular: 

Selecione [Suporte a depuração e escrita], pressione [Barra de espaço] para selecionar, pressione [Tab], selecione [Sair] e pressione Enter para sair:

Pressione [Tab], selecione [Sair] e pressione Enter para sair:

Pressione [Tab], selecione [Sair] e pressione Enter para sair:

Salve a configuração e pressione Enter: 

[root@centos7 linux-5.15.51]#grep -i ntfs .config
CONFIG_NTFS_FS=m
CONFIG_NTFS_DEBUG=y
CONFIG_NTFS_RW=y
# CONFIG_NTFS3_FS is not set

[root@centos7 linux-5.15.51]#make -j 2  #CPU核数要多加,不然编译会很慢
[root@centos7 linux-5.15.51]#pwd
/usr/local/src/linux-5.15.51
[root@centos7 linux-5.15.51]#du -sh .
3.0G	.
[root@centos7 linux-5.15.51]#make modules_install
[root@centos7 linux-5.15.51]#ls /lib/modules
3.10.0-1160.el7.x86_64  5.15.51-150.el7.x86_64
[root@centos7 linux-5.15.51]#du -sh /lib/modules/*
45M	/lib/modules/3.10.0-1160.el7.x86_64
224M	/lib/modules/5.15.51-150.el7.x86_64
[root@centos7 linux-5.15.51]#make install
[root@centos7 linux-5.15.51]#ls /boot/
config-3.10.0-1160.el7.x86_64                            System.map
efi                                                      System.map-3.10.0-1160.el7.x86_64
grub                                                     System.map-5.15.51-150.el7.x86_64
grub2                                                    vmlinuz
initramfs-0-rescue-afe373e8a26e45c681032325645782c8.img  vmlinuz-0-rescue-afe373e8a26e45c681032325645782c8
initramfs-3.10.0-1160.el7.x86_64.img                     vmlinuz-3.10.0-1160.el7.x86_64
initramfs-5.15.51-150.el7.x86_64.img                     vmlinuz-5.15.51-150.el7.x86_64
symvers-3.10.0-1160.el7.x86_64.gz

Selecione o kernel Linux5.15 para iniciar:

[root@centos7 linux-5.15.51]#reboot  
[root@centos7 ~]#uname -r
5.15.51-150.el7.x86_64

Instruções de compilação do kernel

Configure as opções do kernel:

Suporte ao modo "atualização" para configuração, faça a ajuda:

(a) make config:基于命令行以遍历的方式配置内核中可配置的每个选项
(b) make menuconfig:基于curses的文本窗口界面
(c) make gconfig:基于GTK (GNOME)环境窗口界面
(d) make xconfig:基于QT(KDE)环境的窗口界面

Suporta o modo "fresh configuration" para configuração:

(a) make defconfig:基于内核为目标平台提供的“默认”配置进行配置
(b) make allyesconfig: 所有选项均回答为“yes“
(c) make allnoconfig: 所有选项均回答为“no“

compilar o núcleo

  • Compilação completa:
make [-j #]
  • Compile parte da função do kernel:
    (a) Compile apenas o código relevante em um subdiretório
cd /usr/src/linux
make dir/

(b) compilar apenas um módulo específico

cd /usr/src/linux
make dir/file.ko

 Compile o driver apenas para e1000:

make drivers/net/ethernet/intel/e1000/e1000.ko

compilação cruzada do kernel

A plataforma de destino para compilação não é a mesma que a plataforma atual:

make ARCH=arch_name

Para obter ajuda usando uma plataforma de destino específica:

make ARCH=arch_name help

A recompilação requer operações de limpeza prévias:

make clean:清理大多数编译生成的文件,但会保留.config文件等
make mrproper: 清理所有编译生成的文件、config及某些备份文件
make distclean:包含 make mrproper,并清理patches以及编辑器备份文件

Desinstale o kernel:

1. Exclua o código-fonte do kernel desnecessário no diretório /usr/src/linux/;

2. Exclua os arquivos desnecessários da biblioteca do kernel no diretório /lib/modules/;

3. Exclua o kernel e os arquivos de imagem do kernel iniciados no diretório /boot;

4. Altere o arquivo de configuração do grub e exclua a lista de inicialização desnecessária do kernel grub2-mkconfig -o /boot/grub2/grub.cfg O
CentOS 8 também precisa excluir /boot/loader/entries/5b85fc7444b240a992c42ce2a9f65db5-new kernel version.conf;

2. Mecanismo de implementação do módulo do kernel Linux

Antes de mergulhar nos módulos, vale a pena revisar as diferenças entre os módulos do kernel e nossos aplicativos familiares.

O ponto principal, devemos ser claros, os módulos do kernel são executados no "espaço do kernel" e os aplicativos são executados no "espaço do usuário". Espaço do kernel e espaço do usuário são os dois conceitos mais básicos em sistemas operacionais. Talvez você não saiba a diferença entre eles, então vamos revisá-los juntos.

Uma das funções do sistema operacional é fornecer gerenciamento de recursos para aplicativos, para que todos os aplicativos possam usar os recursos de hardware de que precisam. No entanto, a norma atual é que os hosts geralmente têm apenas um conjunto de recursos de hardware; os sistemas operacionais modernos podem aproveitar esse conjunto de hardware para oferecer suporte a sistemas multiusuário. Para garantir que o kernel não seja perturbado pelo programa aplicativo, o sistema operacional multiusuário implementa o acesso autorizado aos recursos de hardware, e a realização desse mecanismo de acesso autorizado se beneficia da realização de diferentes níveis de proteção operacional dentro da CPU. Tomando como exemplo a CPU da INTEL, ela sempre roda em um dos quatro níveis de privilégio a qualquer momento.Se precisar acessar o espaço de armazenamento de alto nível de privilégio, deve passar por um número limitado de portas de privilégio. O sistema Linux foi projetado para aproveitar ao máximo esse recurso de hardware e usa apenas dois níveis de proteção (embora os microprocessadores da série i386 forneçam um modo de quatro níveis).

Em um sistema Linux, o kernel é executado no nível mais alto. Nesse nível, o acesso a qualquer dispositivo é possível. E os aplicativos são executados no nível mais baixo. Nesse nível, o processador proíbe programas de acesso direto ao hardware e acesso não autorizado ao espaço do kernel. Portanto, correspondendo ao programa do kernel em execução no nível mais alto, o espaço de memória onde ele está localizado é o espaço do kernel. E correspondendo ao aplicativo executado no nível mais baixo, o espaço de memória onde ele está localizado é o espaço do usuário. O Linux completa a conversão do espaço do usuário para o espaço do kernel por meio de chamadas ou interrupções do sistema. O código do kernel que executa a chamada do sistema é executado no contexto do processo, que conclui a operação no espaço do kernel em nome do processo de chamada e também pode acessar os dados no espaço de endereço do usuário do processo. Mas para interrupções, ele não existe em nenhum contexto de processo, mas é executado pelo kernel.

Bem, agora podemos analisar mais especificamente as semelhanças e diferenças entre os módulos do kernel e os aplicativos. Vejamos a Tabela 6-1.

Uma comparação da maneira como os aplicativos e os módulos do kernel são programados:

Nesta tabela, vemos que o módulo do kernel deve informar ao sistema: "Estou indo" através da função init_module(); "Estou saindo" através da função cleanup_module(). Esta é a maior característica dos módulos, que podem ser carregados e descarregados dinamicamente. insmod é um comando para carregar módulos no kernel no conjunto de ferramentas de manipulação de módulos do kernel modutils, que apresentaremos em detalhes posteriormente. Por causa do espaço de endereço, os módulos do kernel não podem usar livremente as bibliotecas de funções definidas no espaço do usuário, como libc, como printf(), como os aplicativos podem; os módulos só podem usar as funções com restrição de recursos definidas no espaço do kernel, como printk() . O código-fonte do aplicativo pode chamar funções que não são definidas por si mesmo e só precisa resolver essas referências externas com a biblioteca de funções correspondente durante o processo de conexão. A função que pode ser chamada pelo aplicativo printf() é declarada em stdio.h, e há um código de destino vinculável em libc. No entanto, para o módulo do kernel, ele não pode usar esta função de impressão, mas pode usar apenas a função printk() definida no espaço do kernel. A função printk() não suporta a saída de números de ponto flutuante e a quantidade de dados de saída é limitada pelo espaço de memória disponível do kernel.

Outra dificuldade com os módulos do kernel é que as falhas do kernel geralmente são fatais para todo o sistema ou para o processo atual. Durante o desenvolvimento de aplicativos, falhas de segmento não causam nenhum dano. Podemos usar depuradores para rastrear facilmente o local errado. Portanto, cuidados especiais devem ser tomados no processo de programação do módulo do kernel.

Vamos dar uma olhada em como o mecanismo do módulo do kernel é implementado em detalhes.

tabela de símbolos do kernel

Primeiro, vamos entender o conceito da tabela de símbolos do kernel. A tabela de símbolos do kernel é uma tabela especial usada para armazenar esses símbolos e seus endereços correspondentes que todos os módulos podem acessar. A vinculação de módulos é o processo de inserção de módulos no kernel. Quaisquer símbolos globais declarados por um módulo tornam-se parte da tabela de símbolos do kernel. Os módulos do kernel obtêm os endereços dos símbolos do espaço do kernel de acordo com a tabela de símbolos do sistema para garantir a operação correta no espaço do kernel.

Esta é uma tabela de símbolos pública que podemos ler textualmente do arquivo /proc/kallsyms. O formato para armazenar dados neste arquivo é o seguinte:

Atributo de endereço de memória Nome do símbolo [Módulo ao qual pertence]

Na programação do módulo, você pode usar o nome do símbolo para recuperar o endereço do símbolo na memória desse arquivo e, em seguida, acessar diretamente a memória para obter os dados do kernel. Para os símbolos exportados pelo módulo do kernel, será incluída a quarta coluna "módulo pertencendo", que é usada para marcar o nome do módulo ao qual o símbolo pertence; e para os símbolos liberados do kernel, não há dados em esta coluna.

A tabela de símbolos do kernel está localizada na parte _ksymtab do segmento de código do kernel, e seu endereço inicial e final são especificados por dois símbolos gerados pelo compilador C: __start___ksymtab e __stop___ksymtab.

dependências do módulo

A tabela de símbolos do kernel registra os símbolos e endereços correspondentes que todos os módulos podem acessar. Depois que um módulo do kernel é carregado, os símbolos que ele declara serão registrados nesta tabela, e esses símbolos podem, é claro, ser referenciados por outros módulos. Isso leva ao problema das dependências do módulo.

Quando um módulo A referencia um símbolo exportado por outro módulo B, dizemos que o módulo B é referenciado pelo módulo A, ou que o módulo A é carregado no topo do módulo B. Se você deseja vincular o módulo A, deve vincular o módulo B primeiro. Caso contrário, as referências a esses símbolos exportados pelo módulo B não podem ser vinculadas ao módulo A. Essa inter-relação entre módulos é chamada de dependências de módulo.

Análise de código do kernel

Implementação do código-fonte do mecanismo do módulo do kernel, contribuição de Richard Henderson. Depois de 2002, reescrito por Rusty Russell. Versões mais recentes do kernel Linux, adotam o último.

1) Estrutura de dados

A estrutura de dados relacionada ao módulo é armazenada em include/linux/module.h. É claro que o primeiro módulo struct é recomendado:

include/linux/module.h
 
232  struct module
 
233  {
 
234        enum module_state state;
 
235
 
236        /* Member of list of modules */
 
237        struct list_head list;
 
238
 
239        /* Unique handle for this module */
 
240        char name[MODULE_NAME_LEN];
 
241
 
242        /* Sysfs stuff. */
 
243        struct module_kobject mkobj;
 
244        struct module_param_attrs *param_attrs;
 
245        const char *version;
 
246        const char *srcversion;
 
247
 
248        /* Exported symbols */
 
249        const struct kernel_symbol *syms;
 
250        unsigned int num_syms;
 
251        const unsigned long *crcs;
 
252
 
253        /* GPL-only exported symbols. */
 
254        const struct kernel_symbol *gpl_syms;
 
255        unsigned int num_gpl_syms;
 
256        const unsigned long *gpl_crcs;
 
257
 
258        /* Exception table */
 
259        unsigned int num_exentries;
 
260        const struct exception_table_entry *extable;
 
261
 
262        /* Startup function. */
 
263        int (*init)(void);
 
264
 
265        /* If this is non-NULL, vfree after init() returns */
 
266        void *module_init;
 
267
 
268        /* Here is the actual code + data, vfree'd on unload. */
 
269        void *module_core;
 
270
 
271        /* Here are the sizes of the init and core sections */
 
272        unsigned long init_size, core_size;
 
273
 
274        /* The size of the executable code in each section.  */
 
275        unsigned long init_text_size, core_text_size;
 
276
 
277        /* Arch-specific module values */
 
278        struct mod_arch_specific arch;
 
279
 
280        /* Am I unsafe to unload? */
 
281        int unsafe;
 
282
 
283        /* Am I GPL-compatible */
 
284        int license_gplok;
 
285       
 
286        /* Am I gpg signed */
 
287        int gpgsig_ok;
 
288
 
289  #ifdef CONFIG_MODULE_UNLOAD
 
290        /* Reference counts */
 
291        struct module_ref ref[NR_CPUS];
 
292
 
293        /* What modules depend on me? */
 
294        struct list_head modules_which_use_me;
 
295
 
296        /* Who is waiting for us to be unloaded */
 
297        struct task_struct *waiter;
 
298
 
299        /* Destruction function. */
 
300        void (*exit)(void);
 
301  #endif
 
302
 
303  #ifdef CONFIG_KALLSYMS
 
304        /* We keep the symbol and string tables for kallsyms. */
 
305        Elf_Sym *symtab;
 
306        unsigned long num_symtab;
 
307        char *strtab;
 
308
 
309        /* Section attributes */
 
310        struct module_sect_attrs *sect_attrs;
 
311  #endif
 
312
 
313        /* Per-cpu data. */
 
314        void *percpu;
 
315
 
316        /* The command line arguments (may be mangled).  People like
317          keeping pointers to this stuff */
 
318        char *args;
 
319  };
 

No kernel, cada informação de módulo do kernel é descrita por tal objeto de módulo. Todos os objetos do módulo são vinculados por uma lista. O primeiro elemento da lista encadeada é estabelecido pelo static LIST_HEAD(modules), veja a linha 65 de kernel/module.c. Se você ler a definição da macro LIST_HEAD em include/linux/list.h, você entenderá rapidamente que a variável modules é uma estrutura do tipo struct list_head, e o próximo ponteiro e o ponteiro prev dentro da estrutura apontam para os próprios módulos quando inicializados. As operações na lista encadeada de módulos são protegidas por module_mutex e modlist_lock.

Aqui estão algumas descrições de alguns campos importantes na estrutura do módulo:

234 state表示module当前的状态,可使用的宏定义有:

MODULE_STATE_LIVE

MODULE_STATE_COMING

MODULE_STATE_GOING

240 name数组保存module对象的名称。

244 param_attrs指向module可传递的参数名称,及其属性

248-251 module中可供内核或其它模块引用的符号表。num_syms表示该模块定义的内核模块符号的个数,syms就指向符号表。

300  init和exit 是两个函数指针,其中init函数在初始化模块的时候调用;exit是在删除模块的时候调用的。
294 struct list_head modules_which_use_me,指向一个链表,链表中的模块均依靠当前模块。

Depois de apresentar a estrutura de dados{} do módulo, você ainda pode achar que não a entendeu, porque há muitos conceitos e estruturas de dados relacionadas que você ainda não entende.

Por exemplo, kernel_symbol{} (consulte include/linux/module.h):

struct kernel_symbol

{

       unsigned long value;

       const char *name;

};

Essa estrutura é usada para armazenar símbolos do kernel no código objeto. Ao compilar, o compilador grava os símbolos do kernel definidos no módulo em um arquivo e lê as informações de símbolo contidas nele por meio dessa estrutura de dados ao ler o arquivo e carregar o módulo.

value define o endereço de entrada do símbolo do kernel;

name aponta para o nome do símbolo do kernel;

função de implementação

Em seguida, temos que estudar várias funções importantes no código-fonte. Conforme mencionado no parágrafo anterior, quando o sistema operacional é inicializado, static LIST_HEAD(modules) estabeleceu uma lista vinculada vazia. Depois disso, cada vez que um módulo do kernel é carregado, uma estrutura de módulo é criada e vinculada à lista de módulos.

Sabemos que, do ponto de vista do kernel do sistema operacional, ele fornece serviços ao usuário por meio da única interface chamada chamadas de sistema. Então, e os serviços relacionados aos módulos do kernel? Consulte arch/i386/kernel/syscall_table.S, a versão 2.6.15 do kernel, carregue o módulo do kernel através da chamada do sistema init_module, descarregue o módulo do kernel através da chamada do sistema delete_module, não há outra maneira. Agora, a leitura de código ficou mais fácil.

kernel/module.c:

1931 asmlinkage long
 
1932 sys_init_module(void __user *umod,
 
1933              unsigned long len,
 
1934              const char __user *uargs)
 
1935 {
 
1936       struct module *mod;
 
1937       int ret = 0;
 
1938
 
1939       /* Must have permission */
 
1940       if (!capable(CAP_SYS_MODULE))
 
1941             return -EPERM;
 
1942
 
1943       /* Only one module load at a time, please */
 
1944       if (down_interruptible(&module_mutex) != 0)
 
1945              return -EINTR;
 
1946
 
1947       /* Do all the hard work */
 
1948       mod = load_module(umod, len, uargs);
 
1949       if (IS_ERR(mod)) {
 
1950             up(&module_mutex);
 
1951             return PTR_ERR(mod);
 
1952       }
 
1953
 
1954       /* Now sew it into the lists.  They won't access us, since
1955         strong_try_module_get() will fail. */
 
1956       stop_machine_run(__link_module, mod, NR_CPUS);
 
1957
 
1958       /* Drop lock so they can recurse */
 
1959       up(&module_mutex);
 
1960
 
1961       down(&notify_mutex);
 
1962       notifier_call_chain(&module_notify_list, MODULE_STATE_COMING, mod);
 
1963       up(&notify_mutex);
 
1964
 
1965       /* Start the module */
 
1966       if (mod->init != NULL)
 
1967             ret = mod->init();
 
1968       if (ret < 0) {
 
1969             /* Init routine failed: abort.  Try to protect us from
1970               buggy refcounters. */
 
1971             mod->state = MODULE_STATE_GOING;
 
1972             synchronize_sched();
 
1973             if (mod->unsafe)
 
1974                   printk(KERN_ERR "%s: module is now stuck!\n",
 
1975                         mod->name);
 
1976             else {
 
1977                   module_put(mod);
 
1978                   down(&module_mutex);
 
1979                   free_module(mod);
 
1980                   up(&module_mutex);
 
1981             }
 
1982             return ret;
 
1983       }
 
1984
 
1985       /* Now it's a first class citizen! */
 
1986       down(&module_mutex);
 
1987       mod->state = MODULE_STATE_LIVE;
 
1988       /* Drop initial reference. */
 
1989       module_put(mod);
 
1990       module_free(mod, mod->module_init);
 
1991       mod->module_init = NULL;
 
1992       mod->init_size = 0;
 
1993       mod->init_text_size = 0;
 
1994       up(&module_mutex);
 
1995
 
1996       return 0;
 
1997 }
 

A função sys_init_module() é a implementação da chamada de sistema init_module(). O parâmetro de entrada umod aponta para a localização da imagem do módulo do kernel no espaço do usuário. A imagem é salva no formato de arquivo executável do ELF. A primeira parte da imagem é a estrutura do tipo elf_ehdr, e o comprimento é indicado por len. uargs apontam para argumentos do espaço do usuário. O protótipo de sintaxe da chamada de sistema init_module() é:

long sys_init_module(void *umod, unsigned long len, const char *uargs);

ilustrar:

1940-1941 调用capable( )函数验证是否有权限装入内核模块。

1944-1945 在并发运行环境里,仍然需保证,每次最多只有一个module准备装入。这通过down_interruptible(&module_mutex)实现。

1948-1952 调用load_module()函数,将指定的内核模块读入内核空间。这包括申请内核空间,装配全程量符号表,赋值__ksymtab、__ksymtab_gpl、__param等变量,检验内核模块版本号,复制用户参数,确认modules链表中没有重复的模块,模块状态设置为MODULE_STATE_COMING,设置license信息,等等。

1956      将这个内核模块插入至modules链表的前部,也即将modules指向这个内核模块的module结构。

1966-1983 执行内核模块的初始化函数,也就是表6-1所述的入口函数。

1987      将内核模块的状态设为MODULE_STATE_LIVE。从此,内核模块装入成功。

/kernel/module.c: 

573  asmlinkage long
 
574  sys_delete_module(const char __user *name_user, unsigned int flags)
 
575  {
 
576        struct module *mod;
 
577        char name[MODULE_NAME_LEN];
 
578        int ret, forced = 0;
 
579
 
580        if (!capable(CAP_SYS_MODULE))
 
581              return -EPERM;
 
582
 
583        if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
 
584              return -EFAULT;
 
585        name[MODULE_NAME_LEN-1] = '\0';
 
586
 
587        if (down_interruptible(&module_mutex) != 0)
 
588               return -EINTR;
 
589
 
590        mod = find_module(name);
 
591        if (!mod) {
 
592              ret = -ENOENT;
 
593              goto out;
 
594        }
 
595
 
596        if (!list_empty(&mod->modules_which_use_me)) {
 
597              /* Other modules depend on us: get rid of them first. */
 
598              ret = -EWOULDBLOCK;
 
599              goto out;
 
600        }
 
601
 
602        /* Doing init or already dying? */
 
603        if (mod->state != MODULE_STATE_LIVE) {
 
604               /* FIXME: if (force), slam module count and wake up
605                 waiter --RR */
 
606              DEBUGP("%s already dying\n", mod->name);
 
607              ret = -EBUSY;
 
608              goto out;
 
609        }
 
610
 
611        /* If it has an init func, it must have an exit func to unload */
 
612        if ((mod->init != NULL && mod->exit == NULL)
 
613            || mod->unsafe) {
 
614                forced = try_force_unload(flags);
 
615                if (!forced) {
 
616                    /* This module can't be removed */
 
617                    ret = -EBUSY;
 
618                    goto out;
 
619              }
 
620        }
 
621
 
622        /* Set this up before setting mod->state */
 
623        mod->waiter = current;
 
624
 
625        /* Stop the machine so refcounts can't move and disable module. */
 
626        ret = try_stop_module(mod, flags, &forced);
 
627        if (ret != 0)
 
628             goto out;
 
629
 
630        /* Never wait if forced. */
 
631        if (!forced && module_refcount(mod) != 0)
 
632             wait_for_zero_refcount(mod);
 
633
 
634        /* Final destruction now noone is using it. */
 
635        if (mod->exit != NULL) {
 
636              up(&module_mutex);
 
637              mod->exit();
 
638              down(&module_mutex);
 
639        }
 
640        free_module(mod);
 
641
 
642  out:
 
643        up(&module_mutex);
 
644        return ret;
 
645  }

A função sys_delete_module() é a implementação da chamada de sistema delete_module(). O efeito de chamar essa função é excluir um módulo do kernel que foi carregado pelo sistema. O parâmetro de entrada name_user é o nome do módulo a ser excluído.

ilustrar:

580-581 调用capable( )函数,验证是否有权限操作内核模块。

583-585 取得该模块的名称

590-594 从modules链表中,找到该模块

597-599 如果存在其它内核模块,它们依赖该模块,那么,不能删除。

635-638 执行内核模块的exit函数,也就是表6-1所述的出口函数。

640     释放module结构占用的内核空间。

O conteúdo do código-fonte pode ser visto aqui. Existem algumas outras funções no arquivo kernel/module.c.

Tente analisar, o nome do processo exibido pelo comando top contém o significado dos colchetes "[]"

 Ao executar top/ pscommand, em COMMANDuma coluna, encontraremos alguns nomes de processos entre eles [], por exemplo:

  PID  PPID USER     STAT   VSZ %VSZ %CPU COMMAND
 1542   928 root     R     1064   2%   5% top
    1     0 root     S     1348   2%   0% /sbin/procd
  928     1 root     S     1060   2%   0% /bin/ash --login
  115     2 root     SW       0   0%   0% [kworker/u4:2]
    6     2 root     SW       0   0%   0% [kworker/u4:0]
    4     2 root     SW       0   0%   0% [kworker/0:0]
  697     2 root     SW       0   0%   0% [kworker/1:3]
  703     2 root     SW       0   0%   0% [kworker/0:3]
   15     2 root     SW       0   0%   0% [kworker/1:0]
   27     2 root     SW       0   0%   0% [kworker/1:1]

Análise lógica do código do aplicativo

palavra-chave: COMANDO

Depois de obter o código-fonte do busybox, tente palavras-chave de pesquisa simples e grosseiras:

[GMPY@12:22 busybox-1.27.2]$grep "COMMAND" -rnw *

Acontece que muitos dados correspondentes:

applets/usage_pod.c:79: printf("=head1 COMMAND DESCRIPTIONS\n\n");
archival/cpio.c:100:      --rsh-command=COMMAND  Use remote COMMAND instead of rsh
docs/BusyBox.html:1655:<p>which [COMMAND]...</p>
docs/BusyBox.html:1657:<p>Locate a COMMAND</p>
docs/BusyBox.txt:93:COMMAND DESCRIPTIONS
docs/BusyBox.txt:112:        brctl COMMAND [BRIDGE [INTERFACE]]
docs/BusyBox.txt:612:    ip  ip [OPTIONS] address|route|link|neigh|rule [COMMAND]
docs/BusyBox.txt:614:        OPTIONS := -f[amily] inet|inet6|link | -o[neline] COMMAND := ip addr
docs/BusyBox.txt:1354:        which [COMMAND]...
docs/BusyBox.txt:1356:        Locate a COMMAND
......

Neste ponto, descobri que há muitos arquivos não-fonte na primeira partida, então há muitos, então só posso recuperar arquivos C?

[GMPY@12:25 busybox-1.27.2]$find -name "*.c" -exec grep -Hn --color=auto "COMMAND" {} \;

Desta vez, o resultado é de apenas 71 linhas. Depois de simplesmente escanear os arquivos correspondentes, há uma descoberta interessante:

......
./shell/ash.c:9707:         if (cmdentry.u.cmd == COMMANDCMD) {
./editors/vi.c:1109:    // get the COMMAND into cmd[]
./procps/lsof.c:31: * COMMAND    PID USER   FD   TYPE             DEVICE     SIZE       NODE NAME
./procps/top.c:626:     " COMMAND");
./procps/top.c:701:     /* PID PPID USER STAT VSZ %VSZ [%CPU] COMMAND */
./procps/top.c:841: strcpy(line_buf, HDR_STR " COMMAND");
./procps/top.c:854:     /* PID VSZ VSZRW RSS (SHR) DIRTY (SHR) COMMAND */
./procps/ps.c:441:  { 16                 , "comm"  ,"COMMAND",func_comm  ,PSSCAN_COMM    },
......

No busybox, cada comando é um arquivo separado, esse código tem uma boa estrutura lógica. Entramos diretamente na linha 626 do arquivo procps/top.c.

Função: display_process_list

A linha 626 do procps/top.c pertence à função display_process_list. Basta olhar a lógica do código:

static NOINLINE void display_process_list(int lines_rem, int scr_width)
{
    ......
    /* 打印表头 */
    printf(OPT_BATCH_MODE ? "%.*s" : "\033[7m%.*s\033[0m", scr_width,
        "  PID  PPID USER     STAT   VSZ %VSZ"
        IF_FEATURE_TOP_SMP_PROCESS(" CPU")
        IF_FEATURE_TOP_CPU_USAGE_PERCENTAGE(" %CPU")
        " COMMAND");
 
    ......
    /* 遍历每一个进程对应的描述 */
    while (--lines_rem >= 0) {
        if (s->vsz >= 100000)
            sprintf(vsz_str_buf, "%6ldm", s->vsz/1024);
        else
            sprintf(vsz_str_buf, "%7lu", s->vsz);
        /*打印每一行中除了COMMAND之外的信息,例如PID,USER,STAT等 */
        col = snprintf(line_buf, scr_width,
                "\n" "%5u%6u %-8.8s %s%s" FMT
                IF_FEATURE_TOP_SMP_PROCESS(" %3d")
                IF_FEATURE_TOP_CPU_USAGE_PERCENTAGE(FMT)
                " ",
                s->pid, s->ppid, get_cached_username(s->uid),
                s->state, vsz_str_buf,
                SHOW_STAT(pmem)
                IF_FEATURE_TOP_SMP_PROCESS(, s->last_seen_on_cpu)
                IF_FEATURE_TOP_CPU_USAGE_PERCENTAGE(, SHOW_STAT(pcpu))
        );
        /* 关键在这,读取cmdline */
        if ((int)(col + 1) < scr_width)
            read_cmdline(line_buf + col, scr_width - col, s->pid, s->comm);
        ......
    }
}

Depois de remover o código irrelevante, a lógica da função é clara

  1. Todos os processos foram percorridos no código antes desta função e a estrutura de descrição foi construída
  2. Percorra a estrutura de descrição em display_process_list e imprima as informações na ordem especificada
  3. Obtenha e imprima o nome do processo por meio de read_cmdline

Entramos na função read_cmdline

Função: read_cmdline

void FAST_FUNC read_cmdline(char *buf, int col, unsigned pid, const char *comm)
{
    ......
    sprintf(filename, "/proc/%u/cmdline", pid);
    sz = open_read_close(filename, buf, col - 1);
    if (sz > 0) {
        ......
        while (sz >= 0) {
            if ((unsigned char)(buf[sz]) < ' ')
                buf[sz] = ' ';
            sz--;
        }
        ......
        if (strncmp(base, comm, comm_len) != 0) {
            ......
            snprintf(buf, col, "{%s}", comm);
            ......
    } else {
        snprintf(buf, col, "[%s]", comm ? comm : "?");
    }
}

Depois de remover o código estranho, encontrei

  1. /proc/<PID>/cmdlineobtendo o nome do processo
  2. Se /proc/<PID>/cmdlineestiver vazio, é usado comm, neste caso está entre []colchetes
  3. Se cmdlineo nome base for comminconsistente, {}coloque-o entre

Para facilitar a leitura, não há mais análises cmdlinee análises comm.

Nós nos concentramos na questão, em que circunstâncias, /proc/<PID>/cmdlineestá vazio?

Análise lógica do código do kernel

palavra-chave: cmdline

/proc monta proc, um sistema de arquivos especial, e cmdline é definitivamente sua função exclusiva.

Assumindo que somos novos no kernel, tudo o que podemos fazer neste momento é recuperar a palavra-chave cmdline no código-fonte do proc do kernel.

[GMPY@09:54 proc]$cd fs/proc && grep "cmdline" -rnw *

Encontrados dois arquivos de correspondência de chave base.ce cmdline.c

array.c:11: * Pauline Middelink :  Made cmdline,envline only break at '\0's, to
base.c:224: /* Check if process spawned far enough to have cmdline. */
base.c:708: * May current process learn task's sched/cmdline info (for hide_pid_min=1)
base.c:2902:    REG("cmdline",    S_IRUGO, proc_pid_cmdline_ops),
base.c:3294:    REG("cmdline",   S_IRUGO, proc_pid_cmdline_ops),
cmdline.c:26:   proc_create("cmdline", 0, NULL, &cmdline_proc_fops);
Makefile:16:proc-y  += cmdline.o
vmcore.c:1158:   * If elfcorehdr= has been passed in cmdline or created in 2nd kernel,

A lógica de código do cmdline.c é muito simples, é fácil descobrir que é a implementação de /proc/cmdline, não nossas necessidades

Vamos nos concentrar em base.c, o código relevante

REG("cmdline",   S_IRUGO, proc_pid_cmdline_ops),

A intuição da experiência me diz,

  1. cmdline: é o nome do arquivo
  2. S_IRUGO: é a permissão do arquivo
  3. proc_pid_cmdline_ops: é a estrutura de operação correspondente ao arquivo

Com certeza, entrando proc_pid_cmdline_ops, descobrimos que é definido como:

static const struct file_operations proc_pid_cmdline_ops = {
    .read   = proc_pid_cmdline_read,
    .llseek = generic_file_llseek,
}

Função: proc_pid_cmdline_read

static ssize_t proc_pid_cmdline_read(struct file *file, char __user *buf,
                size_t _count, loff_t *pos)
{
    ......
    /* 获取进程对应的虚拟地址空间描述符 */
    mm = get_task_mm(tsk);
    ......
    /* 获取argv的地址和env的地址 */
    arg_start = mm->arg_start;
    arg_end = mm->arg_end;
    env_start = mm->env_start;
    env_end = mm->env_end;
    ......
    while (count > 0 && len > 0) {
        ......
        /* 计算地址偏移 */
        p = arg_start + *pos;
        while (count > 0 && len > 0) {
            ......
            /* 获取进程地址空间的数据 */
            nr_read = access_remote_vm(mm, p, page, _count, FOLL_ANON);
            ......
        }
    }
}

Xiaobai pode estar confuso neste momento, como você sabe o que access_remote_vmé?

Muito simples, pule para a access_remote_vmfunção, você pode ver que esta função está comentada

/**
 * access_remote_vm - access another process' address space
 * @mm:         the mm_struct of the target address space
 * @addr:       start address to access
 * @buf:        source or destination buffer
 * @len:        number of bytes to transfer
 * @gup_flags:  flags modifying lookup behaviour
 *
 * The caller must hold a reference on @mm.
 */
int access_remote_vm(struct mm_struct *mm, unsigned long addr,
        void *buf, int len, unsigned int gup_flags)
{
    return __access_remote_vm(NULL, mm, addr, buf, len, gup_flags);
}

No código-fonte do kernel Linux, muitas funções têm descrições de funções muito padronizadas, descrições de parâmetros, precauções, etc. Devemos fazer uso total desses recursos para aprender o código.

Dito isso, voltemos ao assunto.

A partirproc_pid_cmdline_read disso, descobrimos que ler /proc/<PID>/cmdlineé, na verdade, ler arg_startos dados do espaço de endereço no início. Portanto, quando os dados do espaço de endereçamento estão vazios, é claro, nenhum dado pode ser lido. Portanto, a questão é: quando os dados do espaço de endereço identificados por arg_start estão vazios?

palavra-chave: arg_start

Relacionado ao espaço de endereço, definitivamente não apenas proc, tentamos recuperar palavras-chave globalmente no código-fonte do kernel:

[GMPY@09:55 proc]$find -name "*.c" -exec grep --color=auto -Hnw "arg_start" {} \;

Há muitas correspondências, não quero vê-las uma a uma e não consigo encontrar a direção do código recuperado:

./mm/util.c:635:    unsigned long arg_start, arg_end, env_start, env_end;
......
./kernel/sys.c:1747:        offsetof(struct prctl_mm_map, arg_start),
......
./fs/exec.c:709:    mm->arg_start = bprm->p - stack_shift;
./fs/exec.c:722:    mm->arg_start = bprm->p;
......
./fs/binfmt_elf.c:301:  p = current->mm->arg_end = current->mm->arg_start;
./fs/binfmt_elf.c:1495: len = mm->arg_end - mm->arg_start;
./fs/binfmt_elf.c:1499:                (const char __user *)mm->arg_start, len))
......
./fs/proc/base.c:246:   len1 = arg_end - arg_start;
......

Mas a partir do nome do arquivo correspondente me deu inspiração:

/proc/<PID>/cmdline é o atributo de cada processo. De task_struct a mm_struct , eles descrevem o processo e os recursos relacionados. Quando será modificado para mm_struct onde arg_start está localizado? Quando o processo é inicializado!

Pensa-se ainda que criar um processo no espaço do usuário nada mais é do que duas etapas:

  1. garfo
  2. executivo

Quando fork, apenas novos são criados task_struct, e os processos pai e filho compartilham um compartilhamento , mm_structsomente execquando forem, eles serão independentes mm_struct, então arg_start deve execser modificado quando eles forem! Nos arg_startarquivos correspondentes, existem apenas exec.c.

Depois de verificar a fs/exec.cfunção onde a palavra-chave está localizada setup_arg_pages, o código da chave não foi encontrado, então continuei verificando o nome do arquivo correspondente e ocorreram outras associações:

exec executa um novo programa, que na verdade carrega o arquivo bin do novo programa, e os arquivos correspondentes à palavra-chave estão lá binfmt_elf.c!

O problema de posicionamento não é apenas entender o código, a Lenovo também é muito eficaz às vezes

Função: create_elf_tables

A função create_elf_tables corresponde à palavra-chave arg_start em binfmt_elf.c. A função é bastante longa. Vamos simplificar:

static int
create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec,
        unsigned long load_addr, unsigned long interp_load_addr)
{
    ......
    /* Populate argv and envp */
    p = current->mm->arg_end = current->mm->arg_start;
    while (argc-- > 0) {
        ......
        if (__put_user((elf_addr_t)p, argv++))
            return -EFAULT;
        ......
    }
    ......
    current->mm->arg_end = current->mm->env_start = p;
    while (envc-- > 0) {
        ......
        if (__put_user((elf_addr_t)p, envp++))
            return -EFAULT;
        ......
    }
    ......
}

Nesta função, argv e envp são armazenados no espaço de endereço de arg_start e env_start .

Em seguida, vamos tentar rastrear a origem por origem e rastrear create_elf_tablesa chamada da função juntos

Em primeiro lugar, ele create_elf_tablesé declarado como static , o que significa que seu escopo efetivo não pode exceder o arquivo onde está localizado. Recuperando no arquivo, verifica-se que a função superior é:

static int load_elf_binary(struct linux_binprm *bprm)

Acabou sendo static , então continuei pesquisando neste arquivo load_elf_binarye encontrei o seguinte código:

static struct linux_binfmt elf_format = {
    .module         = THIS_MODULE,
    .load_binary    = load_elf_binary,
    .load_shlib     = load_elf_library
    .core_dump      = elf_core_dump,
    .min_coredump   = ELF_EXEC_PAGESIZE,
};
 
static int __init init_elf_binfmt(void)
{
    register_binfmt(&elf_format);
    return 0;
}
 
core_initcall(init_elf_binfmt);

Recuperado aqui, a estrutura do código é muito clara, a load_elf_binaryfunção é atribuída a struct linux_binfmt, e registrada na camada superior por meio de ` register_binfmt, fornecendo o retorno de chamada da camada superior.

Palavra-chave: load_binary

Por que bloquear a palavra-chave load_binary? Como .load_binary = load_elf_binary,, isso significa que a chamada da camada superior deve ser XXX->load_binary(...), portanto, bloqueie a palavra-chave load_binary para localizar onde esse retorno de chamada é chamado.

[GMPY@09:55 proc]$ grep "\->load_binary" -rn *

Felizmente, este retorno de chamada apenas fs/exec.cchama:


fs/exec.c:78:   if (WARN_ON(!fmt->load_binary))
fs/exec.c:1621:     retval = fmt->load_binary(bprm);

Digite a linha 1621 de fs/exex.c , que pertence à função search_binary_handler. Infelizmente, EXPORT_SYMBOL(search_binary_handler);sua existência significa que essa função provavelmente será chamada em vários lugares. Obviamente, é muito difícil continuar a análise avançada neste momento. Por que não tentar reverter análise? ?

Quando a estrada falhar, olhe para o problema de um ângulo diferente, e a resposta está na sua frente

Como não é fácil continuar a análise do search_binary_handler, vamos ver se execvea chamada do sistema pode ser alcançada passo a passo search_binary_handler?

palavra-chave: exec

No Linux-4.9, a definição da chamada do sistema é geralmente SYSCALL_DEFILNE<参数数量>(<函数名>..., então pesquisamos a palavra-chave globalmente, primeiro determinamos onde a chamada do sistema é definida?

[GMPY@09:55 proc]$ grep "SYSCALL_DEFINE.*exec" -rn *

Navegue até o arquivofs/exec.c:

fs/exec.c:1905:SYSCALL_DEFINE3(execve,
fs/exec.c:1913:SYSCALL_DEFINE5(execveat,
fs/exec.c:1927:COMPAT_SYSCALL_DEFINE3(execve, const char __user *, filename,
fs/exec.c:1934:COMPAT_SYSCALL_DEFINE5(execveat, int, fd,
kernel/kexec.c:187:SYSCALL_DEFINE4(kexec_load, unsigned long, entry, unsigned long, nr_segments,
kernel/kexec.c:233:COMPAT_SYSCALL_DEFINE4(kexec_load, compat_ulong_t, entry,
kernel/kexec_file.c:256:SYSCALL_DEFINE5(kexec_file_load, int, kernel_fd, int, initrd_fd,

A chamada da função de acompanhamento não é mais complicada e a relação de chamada é resumida da seguinte forma:

execve -> do_execveat -> do_execveat_common -> exec_binprm -> search_binary_handler

Afinal, ele retorna para search_binary_handler

Depois de analisar isso, determinamos a lógica de atribuição:

  1. Quando execveum novo programa é executado, ele é inicializadomm_struct

  2. Salve o execveargv e o envp passados ​​no endereço especificado por arg_start e env_start

  3. Quando cat /proc/<PID>/cmdlineos dados são obtidos do endereço virtual de arg_start

Portanto, enquanto o processo criado pelo espaço do usuário passar a chamada do sistema execve, haverá /proc/<PID>/cmdline, mas ainda não está esclarecido, quando o cmdline estará vazio?

Sabemos que no Linux, os processos podem ser divididos em processos de espaço de usuário e processos de espaço de kernel.Como o cmdline de processo de espaço de usuário não está vazio, vejamos o processo de kernel.

Função: kthread_run

No driver do kernel, geralmente kthread_runcriamos um processo do kernel. Usamos essa função como ponto de entrada para analisar se o cmdline será atribuído ao criar um processo do kernel?

Comece diretamente de kthread_run, rastreie o relacionamento de chamada e descubra que o trabalho real é a função__kthread_create_on_node:

kthread_run -> kthread_create -> kthread_create_on_node -> __kthread_create_on_node

Remova o código redundante e concentre-se no que a função faz:

static struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
                void *data, int node, const char namefmt[], va_list args)
{
    /* 把新进程相关的属性存于 kthread_create_info 的结构体中 */
    struct kthread_create_info *create = kmalloc(sizeof(*create), GFP_KERNEL);
    create->threadfn = threadfn;
    create->data = data;
    create->node = node;
    create->done = &done;
    
    /* 把初始化后的create加入到链表,并唤醒kthreadd_task进程来完成创建工作 */
    list_add_tail(&create->list, &kthread_create_list);
    wake_up_process(kthreadd_task);
    /* 等待创建完成 */
    wait_for_completion_killable(&done)
    
    ......
 
    task = create->result;
    if (!IS_ERR(task)) {
        ......
        /* 创建后,设置进程名,此处的进程名属性为comm,不同于cmdline */
        vsnprintf(name, sizeof(name), namefmt, args);
        set_task_comm(task, name);
        ......
    }
}

O método de análise é semelhante ao anterior e não é repetido aqui. Em resumo, a função faz duas coisas

  1. Ativar um processo kthread_taskpara criar um novo processo

  2. Defina as propriedades do processo, onde as propriedades incluem comm, mas não cmdline

Revise a análise do código do usuário , se /proc/<PID>/cmdlineestiver vazio, use comm e coloque-o entre []**

Portanto, o /proc/<PID>/cmdlineconteúdo do processo do kernel criado por kthread_run/ktrhread_create está vazio.

Nesta análise, os seguintes métodos de análise foram usados ​​principalmente

  1. Recuperação de palavra-chave - do COMMAND do programa principal para arg_start, load_binary, exec do código-fonte do kernel
  2. anotação de função - descrição da função da função access_remote_vm
  3. Associar - associe o atributo do processo ao espaço do usuário para criar um processo e, em seguida, localize a função de processamento da palavra-chave arg_start
  4. Pensamento reverso - É difícil deduzir a relação de chamada de search_binary_handler. A chamada de sistema de execve pode ser analisada passo a passo para search_binary_handler?

Com base nesta análise, tiramos as seguintes conclusões:

  1. Os processos criados pelo espaço do usuário não precisam de [] no display top/ps;

  2. O processo criado pelo espaço do kernel terá [] no display top/ps;

Como as unidades de módulo interagem com o kernel do sistema?

O arquivo make do módulo do kernel:

Primeiro, vamos dar uma olhada em como o arquivo make do programa do módulo deve ser escrito. Desde a versão 2.6, as especificações do Linux para módulos do kernel mudaram muito. Por exemplo, o nome da extensão de todos os módulos é alterado de ".o" para ".ko". Para obter detalhes, consulte Documentation/kbuild/makefiles.txt. Para editar o Makefile para módulos do kernel, veja Documentation/kbuild/modules.txt.

Quando praticamos "helloworld.ko", usamos um Makefile simples:

TARGET = helloworld
 
KDIR = /usr/src/linux
 
PWD = $(shell pwd)
 
obj-m += $(TARGET).o
 
default:
 
       make -C $(KDIR) M=$(PWD) modules

$(KDIR) indica a localização do diretório de nível superior do código-fonte.

"obj-m += $(TARGET).o" diz ao kbuild que ele quer compilar $(TARGET), que é helloworld, em um módulo do kernel.

"M=$(PWD)" significa que os arquivos de módulo gerados estarão no diretório atual.

make file para o módulo do kernel multifile

Agora, vamos estender a pergunta, como compilar um módulo de kernel com vários arquivos? Também tomando "Hello, world" como exemplo, precisamos fazer o seguinte:

Entre todos os arquivos de origem, apenas um arquivo adiciona a linha #define __NO_VERSION__. Isso ocorre porque module.h geralmente inclui a definição da variável global kernel_version, que contém as informações da versão do kernel para a qual o módulo foi compilado. Se você precisar de version.h, você precisa incluí-lo você mesmo, porque module.h não incluirá version.h depois que __NO_VERSION__ for definido.

Um exemplo de um módulo de kernel multi-arquivo é dado abaixo.  

Início.c:

/* start.c
*
 * "Hello, world" –内核模块版本
* 这个文件仅包括启动模块例程
 */
 
   
 
/* 必要的头文件 */
 
 
 
/* 内核模块中的标准 */
 
#include <linux/kernel.h>   /*我们在做内核的工作 */
 
#include <linux/module.h> 
 
 
 
/*初始化模块 */
 
int init_module()
 
{
 
  printk("Hello, world!\n");
 
 
 
/* 如果我们返回一个非零值, 那就意味着
* init_module 初始化失败并且内核模块
* 不能加载 */
 
  return 0;
 
}

pare.c:

/* stop.c
*"Hello, world" -内核模块版本
*这个文件仅包括关闭模块例程
 */
   
 
/*必要的头文件 */
/*内核模块中的标准 */
#include <linux/kernel.h>   /*我们在做内核的工作 */
#define __NO_VERSION__     
#include <linux/module.h>  
#include <linux/version.h>   /* 不被module.h包括,因为__NO_VERSION__ */
 
/* Cleanup - 撤消 init_module 所做的任何事情*/
 
void cleanup_module()
 
{
 
  printk("Bye!\n");
 
}
 
/*结束*/

Desta vez, o módulo do kernel helloworld contém dois arquivos fonte, "start.c" e "stop.c". Vamos dar uma olhada em como escrever um Makefile para um módulo de kernel com vários arquivos

Makefile:

TARGET = helloworld
 
KDIR = /usr/src/linux
 
PWD = $(shell pwd)
 
obj-m += $(TARGET).o
 
$(TARGET)-y := start.o stop.o
 
default:
 
    make -C $(KDIR) M=$(PWD) modules

Comparado com o anterior, apenas uma linha é adicionada:

$(TARGET)-y := start.o stop.o

Escreva um módulo do kernel

Vamos tentar escrever um programa de módulo muito simples, que pode ser implementado na versão 2.6.15, e pode precisar de alguns ajustes para versões de kernel inferiores a 2.4.

helloworld.c

#include <linux/module.h>        /* Needed by all modules */
#include <linux/kernel.h>          /* Needed for KERN_INFO */
 
int init_module(void)
{
    printk(KERN_INFO “Hello World!\n”);
    return 0;
}
 
void cleanup_module(void)
{
    printk(KERN_INFO “Goodbye!\n”);
}
 
MODULE_LICENSE(“GPL”);

ilustrar:

1. A escrita de qualquer programa de módulo precisa incluir o arquivo de cabeçalho linux/module.h, que contém a definição da estrutura do módulo e o controle de versão do módulo. As principais estruturas de dados no arquivo serão descritas em detalhes posteriormente.
2. A função init_module() e a função cleanup_module() são as funções mais básicas e necessárias na programação de módulos. init_module() registra novas funções fornecidas pelo módulo ao kernel; cleanup_module() é responsável por cancelar o registro de todas as funções registradas pelo módulo.
3. Observe que estamos usando a função printk() aqui (não a escreva habitualmente como printf), a função printk() é definida pelo kernel do Linux e sua função é semelhante a printf. A string KERN_INFO representa a prioridade da mensagem.Uma das características de printk() é que ele processa mensagens de diferentes prioridades de forma diferente.

Em seguida, precisamos compilar e carregar este módulo. Mais uma coisa a ser observada: certifique-se de ser um superusuário agora. Porque apenas o superusuário pode carregar e descarregar módulos. Antes de compilar o módulo do kernel, prepare um Makefile:

TARGET = helloworld
 
KDIR = /usr/src/linux
 
PWD = $(shell pwd)
 
obj-m += $(TARGET).o
 
default:
       make -C $(KDIR) M=$(PWD) modules

Em seguida, basta digitar o comando make:

#make

Como resultado, obtemos o arquivo "helloworld.ko". Em seguida, execute o comando load do módulo do kernel:

#insmod  helloworld.ko

Hello World!

Neste momento, a string "Hello World!" é gerada, que é definida em init_module(). Isso mostra que o módulo helloworld foi carregado no kernel. Podemos usar o comando lsmod para ver. O que o comando lsmod faz é nos informar informações sobre todos os módulos em execução no kernel, incluindo o nome do módulo, o tamanho de sua área de cobertura, contagem de uso e seu estado atual e dependências.

root# lsmod

Module    Size    Used  by

helloworld  464    0   (unused)

Finalmente, queremos desinstalar este módulo.

# rmmod helloworld

Goodbye!

Viu "Adeus!" impresso na tela, que é definido em cleanup_module(). Isso mostra que o módulo helloworld foi excluído. Se usarmos lsmod para verificar novamente neste momento, descobriremos que o módulo helloworld não está mais lá.

Em relação aos dois comandos insmod e rmmod, posso apenas dizer brevemente que são dois utilitários para inserir e remover módulos do kernel. O insmod, rmmod e lsmod usados ​​anteriormente são todos utilitários do módulo modutils.

Implementamos com sucesso um programa de módulo mais simples na máquina.

3. Gerenciamento de memória

O subsistema de gerenciamento de memória é uma parte importante do sistema operacional. Desde os primórdios do desenvolvimento do computador, houve uma necessidade de memória maior do que as capacidades físicas do sistema. Para superar essa limitação, muitas estratégias foram desenvolvidas, sendo a memória virtual a mais bem-sucedida. A memória virtual faz com que o sistema pareça ter mais memória do que realmente tem, compartilhando memória entre processos concorrentes.

Não só a memória virtual faz seu computador parecer mais memória, o subsistema de gerenciamento de memória também fornece:

Espaços de Endereço Grandes O sistema operacional faz com que o sistema pareça ter uma quantidade maior de memória do que realmente tem. A memória virtual pode ser muitas vezes maior que a memória física do sistema.

Cada processo no sistema de proteção tem seu próprio espaço de endereço virtual. Esses espaços de endereço virtual são completamente separados uns dos outros, portanto, um processo executando um aplicativo não afetará outro processo. Além disso, o mecanismo de memória virtual do hardware permite a proteção contra gravação de áreas de memória. Isso evita que o código e os dados sejam substituídos por programas maliciosos.

Mapeamento de memória O mapeamento de memória é usado para mapear imagens e dados no espaço de endereço de um processo. Com o mapeamento de memória, o conteúdo do arquivo é vinculado diretamente ao espaço de endereço virtual do processo.

Alocação de Memória Física Justa O subsistema de gerenciamento de memória permite que cada processo em execução no sistema compartilhe a memória física do sistema de forma justa

Memória virtual compartilhada Embora a memória virtual permita que os processos tenham espaços de endereçamento (virtuais) separados, às vezes você também precisa compartilhar memória entre os processos. Por exemplo, pode haver vários processos no sistema executando o interpretador de comandos bash. Embora seja possível ter uma cópia do bash no espaço de endereço virtual de cada processo, é melhor ter apenas uma cópia na memória física e todos os processos executando o código de compartilhamento do bash. Bibliotecas vinculadas dinamicamente são outro exemplo comum de vários processos compartilhando código em execução. A memória compartilhada também pode ser usada para mecanismos de comunicação entre processos (IPC), onde dois ou mais processos podem trocar informações por meio de memória de propriedade conjunta. O sistema Linux suporta o mecanismo IPC de memória compartilhada do System V.

3.1 Um Modelo Abstrato de Memória Virtual

Antes de pensar na abordagem do Linux para suportar memória virtual, é melhor pensar em um modelo abstrato para não ficar confuso com muitos detalhes.

Quando um processo executa um programa, ele lê instruções da memória e as decodifica. A decodificação de uma instrução pode exigir a leitura ou armazenamento do conteúdo de um local específico na memória e, em seguida, o processo executa a instrução e passa para a próxima instrução no programa. Um processo acessa a memória, seja lendo instruções ou acessando dados.

Em um sistema de memória virtual, todos os endereços são endereços virtuais em vez de endereços físicos. O processador converte endereços virtuais em endereços físicos por meio de um conjunto de informações mantidas pelo sistema operacional.

Para facilitar essa conversão, a memória virtual e a memória física são divididas em blocos de tamanho adequado, chamados de páginas. As páginas são do mesmo tamanho. (Claro que pode ser diferente, mas isso dificulta o gerenciamento do sistema). O Linux usa páginas de 8K bytes em sistemas Alpha AXP e páginas de 4K bytes em sistemas Intel x86. Cada página recebe um número exclusivo: número do quadro da página (número da página PFN). Nesse modelo de paginação, um endereço virtual consiste em duas partes: o número da página virtual e o deslocamento dentro da página. Se o tamanho da página for 4K, os bits 11 a 0 do endereço virtual incluem o deslocamento dentro da página e os bits 12 e acima são o número da página. Toda vez que o processador encontra um endereço virtual, ele deve extrair o deslocamento e o número da página virtual. O processador deve traduzir o número da página virtual para a página física e acessar o deslocamento correto da página física. Para isso, o processador usa tabelas de páginas.

A Figura 3.1 mostra os espaços de endereços virtuais de dois processos, Processo X e Processo Y, cada um com sua própria tabela de páginas. Essas tabelas de páginas mapeiam as páginas virtuais de cada processo para as páginas físicas da memória. A figura mostra que a página virtual número 0 do processo X mapeia para a página física número 1, enquanto a página virtual número 1 do processo Y mapeia para a página física número 4. Em teoria, cada entrada na tabela de páginas inclui as seguintes informações:

Sinalizador válido Indica se esta entrada na tabela de páginas é válida

O número da página física descrito por esta entrada da tabela de páginas

Informações de controle de acesso Descreva como esta página é usada: É gravável? Inclui a execução de código?

As tabelas de páginas são acessadas usando números de página virtuais como deslocamentos. A página virtual número 5 é o 6º elemento na tabela (0 é o primeiro elemento)

Para traduzir um endereço virtual para um endereço físico, o processador primeiro encontra o número da página e o deslocamento dentro da página do endereço virtual. O uso de tamanhos de página em potência de 2 pode ser tratado simplesmente com máscaras ou deslocamentos. Olhando novamente para a Figura 3.1, supondo que o tamanho da página seja 0x2000 (8192 decimal) e o endereço no espaço de endereço virtual do processo Y seja 0x2194, o processador traduzirá o endereço para o deslocamento 0x194 dentro da página virtual número 1.

O processador usa o número da página virtual como um índice para encontrar sua entrada na tabela de páginas na tabela de páginas do processo. Se a entrada for válida, o processador buscará o número da página física da entrada. Se esta entrada for inválida, o processo acessou uma área de sua memória virtual que não existe. Nesse caso, o processador não pode interpretar o endereço e deve passar o controle para o sistema operacional para tratá-lo.

A forma como o processador informa especificamente ao processo do sistema operacional que está acessando um endereço virtual inválido que não pode ser traduzido depende do processador. O processador comunica essas informações (falha de página) e o sistema operacional é notificado de que o endereço virtual está com defeito e o motivo da falha.

Assumindo que esta é uma entrada válida na tabela de páginas, o processador busca o número da página física e multiplica o tamanho da página para obter o endereço base da página na memória física. Finalmente, o processador adiciona o deslocamento da instrução ou dados de que necessita.

Usando o exemplo acima novamente, a página virtual número 1 do processo Y é mapeada para a página física número 4 (começando em 0x8000, 4x 0x2000), e o deslocamento 0x194 é adicionado para obter o endereço físico final 0x8194.

Ao mapear endereços virtuais para endereços físicos dessa maneira, a memória virtual pode ser mapeada na memória física do sistema em qualquer ordem. Por exemplo, na Figura 3.1, o número da página virtual da memória virtual X é mapeado para a página física número 1 e a página virtual número 7 é mapeada para a página física número 0, embora seja maior na memória virtual do que na página virtual 0. Isso também demonstra um subproduto interessante da memória virtual: as páginas de memória virtual não precisam ser mapeadas na memória física em uma ordem especificada.

3.1.1 Paginação de Demanda

Como a memória física é muito menor que a memória virtual, o sistema operacional deve evitar o uso ineficiente da memória física. Uma maneira de economizar memória física é carregar apenas as páginas virtuais que o executor está usando. Por exemplo: um programa de banco de dados pode estar executando uma consulta no banco de dados. Neste caso, nem todos os dados devem ser colocados na memória, mas apenas o registro de dados que está sendo examinado. Se esta for uma consulta de pesquisa, o código no carregador para incrementar o registro não faz muito sentido. Essa técnica de carregar páginas virtuais apenas quando acessadas é chamada de paginação por demanda.

Quando um processo tenta acessar um endereço virtual que não está atualmente na memória, o processador não consegue encontrar a entrada da tabela de páginas para a página virtual referenciada. Por exemplo: O processo X na Figura 3.1 não tem uma entrada para a página virtual 2 em sua tabela de páginas, portanto, se o processo X tentar ler de um endereço na página virtual 2, o processador não poderá traduzir o endereço para um endereço físico. Neste momento, o processador notifica o sistema operacional sobre uma falha de página.

Se o endereço virtual errado for inválido, significa que o processo está tentando acessar um endereço virtual que não deveria. Talvez um erro de programa, como escrever em um endereço arbitrário na memória. Nesse caso, o sistema operacional o interromperá, protegendo outros processos do sistema.

Se o endereço virtual defeituoso for válido, mas a página em que ele estiver não estiver na memória, o sistema operacional deverá carregar a página correspondente na memória a partir da imagem de disco. Os acessos ao disco levam um tempo relativamente longo, portanto, o processo deve esperar até que a página seja buscada na memória. Se houver outros sistemas em execução, o sistema operacional escolherá um para ser executado. A página buscada é gravada em uma página livre e uma entrada de página virtual válida é adicionada à tabela de páginas do processo. O processo então executa novamente as instruções de máquina onde ocorreu o erro de memória. Durante esse acesso à memória virtual, o processador pode converter o endereço virtual em um endereço físico, para que o processo possa continuar em execução.

O Linux usa a tecnologia de paginação por demanda para carregar a imagem executável na memória virtual do processo. Quando um comando é executado, o arquivo que o contém é aberto e seu conteúdo é mapeado na memória virtual do processo. Esse processo é realizado modificando as estruturas de dados que descrevem o mapeamento de memória do processo, também conhecido como mapeamento de memória. No entanto, apenas a primeira parte da imagem é realmente colocada na memória física. O restante da imagem permanece no disco. Quando a imagem é executada, ela gera uma falha de página e o Linux usa a tabela de mapa de memória do processo para determinar qual parte da imagem precisa ser carregada na memória para execução.

3.1.2 Trocando

Se um processo precisar colocar uma página virtual na memória física e não houver páginas físicas livres, o sistema operacional deverá descartar outra página no espaço físico para liberar espaço para essa página.

Se a página na memória física que precisa ser descartada vier de uma imagem ou arquivo de dados em disco e não tiver sido gravada para não precisar ser armazenada, a página será descartada. Se o processo precisar da página novamente, ela poderá ser carregada novamente na memória a partir do arquivo de imagem ou de dados.

No entanto, se a página foi alterada, o sistema operacional deve preservar seu conteúdo para acesso posterior. Isso também é chamado de página suja e, quando é descartado da memória física, é armazenado em um arquivo especial chamado arquivo de troca. Como o acesso ao arquivo de troca é lento em comparação ao acesso ao processador e à memória física, o sistema operacional deve decidir se deseja gravar as páginas de dados no disco ou mantê-las na memória para o próximo acesso.

Thrashing ocorre se o algoritmo para decidir quais páginas precisam ser descartadas ou troca não for eficiente. Neste ponto, as páginas estão constantemente sendo gravadas no disco e lidas de volta, e o sistema operacional está muito ocupado para fazer o trabalho real. Por exemplo, na Figura 3.1, se a página física número 1 for acessada com frequência, não a troque para o disco. O que um processo está usando também é chamado de conjunto de trabalho. Um esquema de troca eficiente deve garantir que os conjuntos de trabalho de todos os processos estejam na memória física.

O Linux usa a técnica de página LRU (Least Recentemente Used) para selecionar de forma justa as páginas que precisam ser descartadas do sistema. Esse esquema atribui a cada página do sistema uma idade que muda quando a página é acessada. Quanto mais páginas visitadas, mais novas, menos visitadas, mais antigas, mais obsoletas. Páginas obsoletas são boas candidatas para troca.

3.1.3 Memória Virtual Compartilhada

A memória virtual permite que vários processos compartilhem memória facilmente. Todos os acessos à memória são por meio de tabelas de páginas, e cada processo possui sua própria tabela de páginas. Para que dois processos compartilhem uma página de memória física, o número da página física deve aparecer nas tabelas de página de ambos os processos.

A Figura 3.1 mostra dois processos compartilhando a página física número 4. O número da página virtual é 4 para o processo X e 6 para o processo Y. Isso também mostra um aspecto interessante das páginas compartilhadas: uma página física compartilhada não precisa existir no mesmo lugar no espaço de memória virtual do processo que a compartilha.

3.1.4 Modos de endereçamento físico e virtual

Para o próprio sistema operacional, rodar na memória virtual faz pouco sentido. Isso seria um pesadelo se o sistema operacional tivesse que manter suas próprias tabelas de páginas. A maioria dos processadores multiuso oferece suporte aos modos de endereço físico e virtual. O modo de endereçamento físico não requer tabelas de páginas e o processador não precisa fazer nenhuma tradução de endereço neste modo. O kernel do Linux é executado no modo de endereço físico.

Os processadores Alpha AXP não possuem modos de endereçamento físico especiais. Ele divide o espaço de memória em várias áreas, duas das quais são designadas como áreas de endereço físico mapeado. O espaço de endereço do kernel é chamado de espaço de endereço KSEG e inclui todos os endereços de 0xfffffc0000000000 para cima. Para executar o código (código núcleo) anexado ao KSEG ou acessar os dados nele, o código deve ser executado no estado núcleo. O kernel Linux no Alpha se conecta para executar a partir do endereço 0xfffffc0000310000.

3.1.5 Controle de Acesso

As entradas da tabela de páginas também incluem informações de controle de acesso. Quando o processador usa as entradas da tabela de páginas para mapear o endereço virtual de um processo para um endereço físico, ele pode facilmente usar as informações de controle de acesso para impedir que o processo o acesse de uma maneira não permitida.

Há muitas razões pelas quais você pode querer restringir o acesso às regiões de memória. Alguma memória, como a que contém código executável, é essencialmente código somente leitura, e o sistema operacional deve impedir que um processo grave seu código executável. Por sua vez, as páginas que contêm dados podem ser gravadas, mas as tentativas de executar essa memória devem falhar. A maioria dos processadores tem dois estados de execução: modo kernel e modo usuário. Você não quer que os usuários executem diretamente o código do modo kernel ou acessem as estruturas de dados do kernel, a menos que o processador esteja sendo executado no modo kernel.

As informações de controle de acesso são colocadas no PTE (entrada da tabela de páginas) e estão relacionadas ao processador específico. A Figura 3.2 mostra o PTE de Alpha AXP. O significado de cada bit é o seguinte:

V 有效,这个PTE是否有效
FOE “Fault on Execute” 试图执行本页代码时,处理器是否要报告page fault,并将控制权传递给操作系统。
FOW “Fault on Write” 如上,在试图写本页时产生page fault
FOR “fault on read” 如上,在试图读本页时产生page fault
ASM 地址空间匹配。用于操作系统清除转换缓冲区中的部分条目
KRE 核心态的代码可以读本页
URE 用户态的代码可以读本页
GII 间隔因子,用于将一整块映射到一个转换缓冲条目而非多个。
KWE 核心态的代码可以写本页
UWE 用户态的代码可以写本页
Page frame number 对于V位有效的PTE,包括了本PTE的物理页编号;对于无效的PTE,如果不是0,包括了本页是否在交换文件的信息。

Os dois bits a seguir são definidos e usados ​​pelo Linux:

_PAGE_DIRTY 如果设置,本页需要写到交换文件中
_PAGE_ACCESSED Linux 使用,标志一页已经访问过

3.2 Caches

Se você implementar um sistema com o modelo teórico acima, ele funcionará, mas não será muito eficiente. Os projetistas de sistemas operacionais e processadores se esforçam para tornar os sistemas mais eficientes. Além de usar processadores mais rápidos, memória etc., a melhor abordagem é manter um cache de informações e dados úteis, o que tornará algumas operações mais rápidas.

O Linux usa uma série de técnicas de gerenciamento de memória relacionadas ao cache:

Cache de buffer: O cache de buffer contém buffers de dados para drivers de dispositivo de bloco. Esses buffers são de tamanho fixo (por exemplo, 512 bytes) e contêm dados lidos ou gravados em um dispositivo de bloco. Um dispositivo de bloco é um dispositivo que só pode ser acessado lendo e escrevendo blocos de dados de tamanho fixo. Todos os discos rígidos são dispositivos de bloco. O dispositivo de bloco usa o identificador do dispositivo e o número do bloco de dados a ser acessado como um índice para localizar rapidamente o bloco de dados. Os dispositivos de bloco só podem ser acessados ​​por meio do cache de buffer. Se os dados puderem ser encontrados no cache de buffer, não será necessário ler de um dispositivo de bloco físico, como um disco rígido, acelerando assim o acesso.

Veja fs/buffer.c

O Page Cache é usado para acelerar o acesso a imagens e dados em disco. Ele é usado para armazenar em cache o conteúdo lógico de um arquivo, uma página por vez, e acessado por meio do arquivo e dos deslocamentos dentro do arquivo. Quando uma página de dados é lida do disco para a memória, ela é armazenada em cache no cache da página.

Veja mm/filemap.c

Cache de troca Somente páginas alteradas (ou sujas) existem no arquivo de troca. Contanto que elas não sejam modificadas novamente após serem gravadas no arquivo de troca, na próxima vez que essas páginas precisarem ser trocadas, elas não precisarão ser gravadas no arquivo de troca, porque a página já está no arquivo de troca, e a página pode ser descartada diretamente. Em um sistema fortemente trocado, isso economiza muitas operações de disco desnecessárias e caras.

Consulte mm/swap_state.c mm/swapfile.c

Cache de hardware: Uma implementação comum de cache de hardware está dentro do processador: o cache PTE. Nesse caso, o processador nem sempre precisa ler a tabela de páginas diretamente, mas coloca a tabela de tradução de páginas no cache quando necessário. Existem buffers da tabela de tradução (TLB Translation Look-aside Buffers) na CPU, que colocam uma cópia em cache das entradas da tabela de páginas de um ou mais processos no sistema.

Ao referenciar um endereço virtual, a área de processamento tenta procurar no TLB. Se encontrado, ele traduz diretamente o endereço virtual para o endereço físico e executa a operação correta nos dados. Se não conseguir encontrá-lo, ele precisa da ajuda do sistema operacional. Ele sinaliza ao sistema operacional que ocorreu uma falta de TLB. Um mecanismo dependente do sistema encaminha a exceção para o código correspondente no sistema operacional para processamento. O sistema operacional gera novas entradas TLB para esse mapeamento de endereço. Quando a exceção é eliminada, o processador tenta novamente traduzir o endereço virtual, desta vez será bem-sucedido porque há uma entrada válida para esse endereço no TLB.

Um efeito colateral dos caches (hardware ou não) é que o Linux precisa gastar muito tempo e espaço mantendo esses caches e, se esses caches travarem, o sistema também.

3.3 Tabelas de Páginas do Linux

O Linux assume uma tabela de páginas de três níveis. Cada tabela de páginas acessada inclui o número da página da tabela de páginas do próximo nível. A Figura 3.3 mostra como um endereço virtual é dividido em uma série de campos: cada campo fornece um deslocamento em uma tabela de páginas. Para traduzir um endereço virtual para um endereço físico, o processador deve pegar o conteúdo de cada campo de nível, traduzir para um deslocamento dentro da página física que inclui essa tabela de página e, em seguida, ler o número da página da tabela de página do próximo nível. Isso é repetido três vezes até que o número da página do endereço físico, incluindo o endereço virtual, seja encontrado. Em seguida, use o último campo no endereço virtual: o deslocamento de byte para pesquisar os dados na página.

Cada plataforma na qual o Linux é executado deve fornecer macros de tradução que permitem que o núcleo lide com as tabelas de páginas de um processo específico. Dessa forma, o kernel não precisa conhecer a estrutura exata das entradas da tabela de páginas ou como elas são organizadas. Dessa forma, o Linux usa com sucesso o mesmo manipulador de tabela de páginas para processadores Alpha e Intel x86, onde Alpha usa tabelas de páginas de três níveis e a Intel usa tabelas de páginas de dois níveis.

Veja include/asm/pgtable.h

3.4 Alocação e desalocação de páginas

Há uma grande demanda por páginas físicas no sistema. Por exemplo, o sistema operacional precisa alocar páginas quando uma imagem de programa é carregada na memória. Essas páginas precisam ser liberadas quando o programa terminar a execução e descarregar. Além disso, para armazenar estruturas de dados relacionadas ao núcleo, como a própria tabela de páginas, também são necessárias páginas físicas. Esse mecanismo e estrutura de dados para alocar e recuperar páginas talvez seja o mais importante para manter a eficiência do subsistema de memória virtual.

Todas as páginas físicas do sistema são descritas usando a estrutura de dados mem_map. Esta é uma lista vinculada de estruturas mem_map_t, inicializada na inicialização. Cada estrutura mem_map_t (confusamente essa estrutura também é chamada de estrutura de página) descreve uma página física no sistema. Os campos importantes (pelo menos para gerenciamento de memória) são:

veja include/linux/mm.h

contagem O número de usuários nesta página. Se esta página for compartilhada por vários processos, o contador será maior que 1.

Idade Descreve a idade desta página. Usado para decidir se esta página pode ser descartada ou trocada.

O número da página física descrito por Map_nr mem_map_t.

O código de alocação de página usa o vetor free_area para localizar páginas livres. Todo o esquema de gerenciamento de buffer é suportado por esse mecanismo. Enquanto esse código for usado, o tamanho da página usada pelo processador e o mecanismo da página física podem ser irrelevantes.

Cada unidade free_area inclui informações de bloco de página. A primeira célula na matriz descreve uma única página, a próxima é um bloco de 2 páginas, a próxima é um bloco de 4 páginas e assim por diante, todos os múltiplos de 2 para cima. Essa célula de lista vinculada é usada como o início da fila, com ponteiros para as estruturas de dados das páginas no array mem_map. Os blocos de página gratuitos são enfileirados aqui. Map é um bitmap que acompanha os grupos de alocação para páginas desse tamanho. Se o enésimo bloco no bloco de página estiver livre, o enésimo bit no bitmap é definido.

A Figura 3.4 mostra a estrutura free_area. A unidade 0 tem uma página livre (página número 0), e a unidade 2 tem 2 blocos livres de 4 páginas, o primeiro começando na página número 4 e o segundo começando na página número 56.

3.4.1 Alocação de Página

Veja mm/page_alloc.c get_free_pages()

O Linux usa o algoritmo Buddy para alocar e recuperar blocos de página com eficiência. O código de alocação de página tenta alocar um bloco de uma ou mais páginas físicas. As alocações de página usam blocos de potência de dois tamanhos. Isso significa que blocos de 1 página, 2 páginas, 4 páginas podem ser alocados e assim por diante. Desde que o sistema tenha páginas livres suficientes para atender às necessidades (nr_free_pages > min_free_pages), o código de alocação procurará na free_area um bloco de páginas do tamanho necessário. Cada célula na Free_area possui um bitmap que descreve a ocupação e a desocupação de seu próprio bloco de página de tamanho. Por exemplo, a célula 2 na matriz possui um mapa de alocação que descreve os blocos livres e ocupados de 4 páginas de tamanho.

O algoritmo primeiro encontra um bloco de páginas de memória do tamanho solicitado. Ele mantém o controle da lista vinculada de páginas livres na fila da unidade de lista na estrutura de dados free_area. Se um bloco de página do tamanho solicitado não estiver livre, encontre um bloco do próximo tamanho (2 vezes o tamanho solicitado). Continue este processo até que todas as áreas_livres sejam percorridas ou um bloco de página livre seja encontrado. Se o bloco de página encontrado for maior que o bloco de página solicitado, o bloco será dividido em blocos de tamanho apropriado. Como todos os blocos são compostos de páginas em potência de 2, o processo de divisão é relativamente simples, você só precisa dividi-lo igualmente. Os blocos livres são colocados na fila apropriada e os blocos de página alocados são retornados ao chamador.

Por exemplo, na Figura 3.4, se um bloco de dados de 2 páginas for solicitado, o primeiro bloco de 4 páginas (começando na página número 4) será dividido em dois blocos de 2 páginas. O primeiro bloco de 2 páginas começando na página número 4 será retornado ao chamador, e o segundo bloco de 2 páginas (começando na página número 6) será enfileirado no local 1 no array free_area com 2 páginas livres na fila de blocos .

3.4.2 Desalocação de Página

No processo de alocação de blocos de página, a divisão de blocos de página grandes em blocos de página pequenos tornará a memória mais fragmentada. O código para recuperação de página une páginas em grandes blocos sempre que possível. Na verdade, o tamanho do bloco de página é muito importante (potência de 2), porque isso facilita o agrupamento de blocos de página em grandes blocos de página.

Sempre que um bloco de página é recuperado, é verificado se os blocos de página adjacentes ou juntos do mesmo tamanho estão livres. Nesse caso, ele é combinado com o bloco de página recém-liberado para formar um novo bloco de página livre do próximo tamanho. Sempre que dois blocos de página de memória são combinados em um bloco de página maior, o código de recuperação de página tenta mesclar os blocos de página em um bloco maior. Dessa forma, os blocos de páginas gratuitas serão os maiores possíveis.

Por exemplo, na Figura 3.4, se a página número 1 for liberada, ela será combinada com a página já livre número 0 e colocada na fila de blocos de 2 páginas livres na unidade 1 da área_livre.

3.5 Mapeamento de Memória

Quando uma imagem é executada, o conteúdo da imagem em execução deve ser colocado no espaço de endereço virtual do processo. O mesmo vale para qualquer biblioteca compartilhada à qual a imagem de execução se conecte. O arquivo executável não é realmente colocado na memória física, mas apenas anexado à memória virtual do processo. Dessa forma, sempre que um programa em execução fizer referência a uma parte da imagem, essa parte da imagem será carregada na memória a partir do executável. Essa conexão da imagem com o espaço de endereço virtual do processo é chamada de mapa de memória.

A memória virtual de cada processo é representada por uma estrutura de dados mm_struct. Isso inclui informações sobre a imagem em execução no momento (por exemplo, bash) e um ponteiro para um conjunto de estruturas vm_area_struct. Cada estrutura de dados vm_area_struct descreve o início da área de memória, os direitos de acesso do processo à área de memória e as operações nessa memória. Essas operações são um conjunto de rotinas que o Linux usa para gerenciar essa memória virtual. Por exemplo, uma das operações de memória virtual é a operação correta que deve ser executada quando um processo tenta acessar essa memória virtual e descobre (através de uma falha de página) que a memória não está na memória física. Essa operação é chamada de operação nopage . A operação nopage é usada quando o Linux solicita que as páginas da imagem de execução sejam carregadas na memória.

Quando uma imagem de execução é mapeada no espaço de endereço virtual do processo, um conjunto de estruturas de dados vm_area_struct é gerado. Cada estrutura vm_area_struct representa uma parte da imagem de execução: código de execução, dados inicializados (variáveis), dados não inicializados e assim por diante. O Linux suporta um conjunto padrão de operações de memória virtual e um conjunto correto de operações de memória virtual é associado a elas quando a estrutura de dados vm_area_struct é criada.

3.6 Paginação de Demanda

Desde que a imagem de execução esteja mapeada na memória virtual do processo, ela pode começar a ser executada. Porque apenas a primeira parte da imagem

Os pontos são colocados na memória física, e em breve a área do espaço virtual que não foi colocada na memória física será acessada. Quando um processo acessa um endereço virtual que não possui uma entrada válida na tabela de páginas, o processador relata uma falha de página ao Linux. A falha de página descreve o endereço virtual e o tipo de acesso à memória em que ocorre a falha de página.

O Linux deve encontrar a estrutura de dados vm_area_struct (conectada junto com a estrutura de árvore Adelson-Velskii e Landis AVL) correspondente à área do espaço onde ocorreu a falha de página. Se a estrutura vm_area_struct correspondente a este endereço virtual não puder ser encontrada, significa que o processo acessou um endereço virtual ilegal. O Linux sinalizará o processo, enviando um sinal SIGSEGV, e se o processo não tratar o sinal, ele sairá.

Veja handle_mm_fault() em mm/memory.c

O Linux então verifica o tipo de falha de página e o tipo de acesso permitido para aquela área de memória virtual. Um erro de memória também é sinalizado se um processo acessar a memória de maneira ilegal, como escrever em uma área que ele só pode ler.

Agora que o Linux determina que a falha de página é legítima, ele deve tratá-la. O Linux deve distinguir entre as páginas no arquivo de troca e a imagem de disco, que ele usa para determinar a entrada da tabela de páginas para o endereço virtual onde ocorreu a falha de página.

Veja do_no_page() em mm/memory.c

Se a entrada da tabela de páginas para a página for inválida, mas não estiver vazia, a página estará no arquivo de troca. Para entradas da tabela de página Alpha AXP, o bit válido é definido, mas o campo PFN não está vazio. Nesse caso, o campo PFN mantém a posição da página no arquivo de troca (e nesse arquivo de troca). Como as páginas são tratadas no arquivo de troca é discutida posteriormente.

Nem todas as estruturas de dados vm_area_struct possuem um conjunto completo de operações de memória virtual, e aquelas que possuem operações de memória especiais também podem não ter operações nopang. Porque, por padrão, para operações nopage, o Linux aloca uma nova página física e cria uma entrada de tabela de página válida. Se esta seção de memória virtual tiver uma operação especial nopage, o Linux chamará esse código especial.

A operação usual nopage do Linux é usada para mapear a memória da imagem de execução e usar o cache de página para carregar a página de imagem solicitada na memória física. Embora a tabela de páginas do processo seja atualizada após a página solicitada ser trazida para a memória física, a ação de hardware necessária pode ser necessária para atualizar essas entradas, especialmente se o processador usar o TLB. Agora que a falha de página foi tratada, ela pode ser descartada e o processo reiniciado na instrução que causou o erro de acesso à memória virtual.

Veja filemap_nopage() em mm/filemap.c

 

3.7 O Cache de Página do Linux

O papel do cache de página do Linux é acelerar o acesso aos arquivos do disco. Um arquivo mapeado em memória é lido uma página por vez e essas páginas são armazenadas no cache de página. A Figura 3.6 mostra o cache da página, incluindo um vetor de ponteiros para a estrutura de dados mem_map_t: page_hash_table. Cada arquivo no Linux é identificado por uma estrutura de dados de inode VFS (descrita na Seção 9), e cada inode VFS é único e pode identificar totalmente um arquivo único. O índice da tabela de páginas é obtido do número do inode do VFS e do deslocamento no arquivo.

Veja linux/pagemap.h

Quando uma página de dados é lida de um arquivo mapeado na memória, como quando a paginação por demanda precisa ser colocada na memória, a página é lida do cache de página. Se a página estiver no cache, um ponteiro para a estrutura de dados mem_map_t será retornado ao código de tratamento de falhas de página. Caso contrário, a página deve ser carregada na memória do sistema de arquivos que contém o arquivo. O Linux aloca memória física e lê a página de um arquivo em disco. Se possível, o Linux inicia a leitura da próxima página do arquivo. Essa antecipação de página única significa que, se o processo ler os dados sequencialmente do arquivo, a próxima página de dados estará aguardando na memória.

O cache da página continua crescendo à medida que a imagem do programa é lida e executada. Se a página não for mais necessária, ela será removida do cache. Como uma imagem que não é mais usada por nenhum processo. Quando o Linux usa memória, as páginas físicas podem continuar a diminuir.Neste momento, o Linux pode reduzir o cache de página.

3.8 Trocando e Descartando Páginas

Quando a memória física é escassa, o subsistema de gerenciamento de memória do Linux deve tentar liberar páginas físicas. Esta tarefa recai sobre o processo de troca do núcleo (kswapd). Um daemon de troca de núcleo é um tipo especial de processo, um thread de núcleo. Um thread de núcleo é um processo sem memória virtual e é executado no espaço de endereço físico no estado de núcleo. O daemon de troca do núcleo tem um nome um pouco errado porque faz mais do que apenas trocar páginas para o arquivo de troca do sistema. Seu trabalho é garantir que o sistema tenha páginas livres suficientes para que o sistema de gerenciamento de memória opere com eficiência.

O daemon de troca do kernel (kswapd) é iniciado pelo processo init do kernel na inicialização e aguarda a expiração do temporizador de troca do kernel. Cada vez que o temporizador expira, o processo de troca verifica se há poucas páginas livres no sistema. Ele usa duas variáveis: free_pages_high e free_pages_low para decidir se libera algumas páginas. Enquanto o número de páginas livres no sistema permanecer acima de free_pages_high, o processo de troca não faz nada. Ele dorme novamente até a próxima vez que seu cronômetro expirar. Para fazer essa verificação, o processo de troca leva em consideração o número de páginas que estão sendo gravadas no arquivo de troca, contado por nr_async_pages: incrementado cada vez que uma página é enfileirada para gravação no arquivo de troca e decrementada quando termina. Free_page_low e free_page_high são definidos na inicialização do sistema e estão relacionados ao número de páginas físicas no sistema. Se o número de páginas livres no sistema for menor que free_pages_high ou menor que free_page_low, o processo de troca do kernel tentará três métodos para reduzir o número de páginas físicas usadas pelo sistema:

Veja kswapd() em mm/vmscan.c

Reduza o tamanho do cache de buffer e do cache de página

Troque as páginas de memória compartilhada do System V

Trocar e descartar páginas

Se o número de páginas livres no sistema ficar abaixo de free_pages_low, o processo de troca do núcleo tentará liberar 6 páginas antes da próxima execução. Caso contrário, tente liberar 3 páginas. Cada um dos métodos acima é tentado até que páginas suficientes sejam liberadas. O processo de troca principal registra o último método usado para liberar páginas físicas. Cada vez que for executado, ele tentará primeiro o último método bem-sucedido para liberar a página.

Depois de liberar páginas suficientes, o processo de troca entra em suspensão novamente até que seu cronômetro expire novamente. Se o motivo pelo qual o processo de troca do kernel libera páginas é que o número de páginas livres do sistema é menor que free_pages_low, ele dorme apenas metade do tempo que normalmente faria. Desde que o número de páginas livres seja maior que free_pages_low, o processo de troca retoma a verificação no intervalo original.

3.8.1 Reduzindo o tamanho dos caches de página e buffer

Páginas e páginas no cache de buffer são bons candidatos para o vetor free_area. O Cache de Página, que contém páginas de arquivos mapeados na memória, pode conter dados desnecessários que ocupam a memória do sistema. Da mesma forma, o Buffer Cache, que inclui dados lidos ou gravados em dispositivos físicos, também pode conter buffers inúteis. Quando as páginas físicas do sistema estão prestes a se esgotar, é relativamente fácil descartar as páginas nesses caches porque não requer gravação no dispositivo físico (ao contrário da troca de páginas da memória). Abandonar essas páginas não tem muitos efeitos colaterais prejudiciais, exceto que o acesso a dispositivos físicos e arquivos mapeados na memória é um pouco mais lento. No entanto, se as páginas nesses caches forem razoavelmente descartadas, todos os processos serão igualmente afetados.

Cada vez que o processo de troca do kernel deseja reduzir esses buffers, ele verifica os blocos de página no vetor de página mem_map para ver se eles podem ser descartados da memória física. Se as páginas livres do sistema estiverem muito baixas (perigosas) e o processo de troca do núcleo estiver trocando muito, o tamanho do bloco de página para essa verificação será maior. O tamanho do bloco de página é verificado de forma round-robin: cada tentativa de reduzir o mapa de memória usa um tamanho de bloco de página diferente. Isso é chamado de algoritmo do relógio, como os ponteiros do relógio. Todo o vetor de página mem_map é verificado, algumas páginas por vez.

Veja mm/filemap.c shrink_map()

Cada página verificada deve ser julgada para ser armazenada em cache no cache de página ou no cache de buffer. Observe que o descarte de páginas compartilhadas não é considerado neste momento e uma página não estará nos dois caches ao mesmo tempo. Se a página não estiver em nenhum dos buffers, a próxima página da tabela de vetores de página mem_map será verificada.

As páginas armazenadas em cache de buffer ch (ou buffers nas páginas são armazenadas em cache) tornam a alocação e a desalocação de buffer mais eficientes. O código que reduz o mapa de memória tenta liberar o buffer que contém a página marcada. Se o buffer for liberado, a página que contém o buffer também será liberada. Se a página marcada estiver no cache de página do Linux, ela será removida do cache de página e liberada.

veja fs/buffer.c free_buffer()

Se essa tentativa liberar páginas suficientes, o processo de troca de núcleo continuará aguardando até a próxima ativação periódica. Como as páginas liberadas não pertencem à memória virtual de nenhum processo (apenas páginas armazenadas em cache), não há necessidade de atualizar a tabela de páginas do processo. Se ainda não houver páginas de cache descartadas suficientes, o processo de troca tentará trocar algumas páginas compartilhadas.

3.8.2 Trocando as Páginas de Memória Compartilhada do System V

A memória compartilhada no System V é um mecanismo de comunicação entre processos que troca informações compartilhando memória virtual entre dois ou mais processos. Como a memória é compartilhada entre os processos é discutida em detalhes na Seção 5. Por enquanto, basta dizer que cada parte da memória compartilhada do System V é descrita por uma estrutura de dados shmid_ds. Ele inclui um ponteiro para a estrutura de dados da lista vinculada vm_area_struct para cada processo que compartilha essa memória. A estrutura de dados Vm_area_struct descreve a localização dessa memória compartilhada em cada processo. Cada estrutura vm_area_struct nesta memória do System V está vinculada aos ponteiros vm_next_shared e vm_prev_shared. Cada estrutura de dados shmid_ds tem uma lista vinculada de entradas da tabela de páginas, cada uma das quais descreve a correspondência entre uma página virtual compartilhada e uma página física.

O processo de troca de kernel também usa o algoritmo de clock ao trocar as páginas de memória compartilhada do System V. Toda vez que é executado, ele registra a página da memória compartilhada que foi trocada da última vez. Ele é registrado com dois índices: o primeiro é o índice na matriz de estrutura de dados shmid_ds e o segundo é o índice na cadeia da tabela de páginas dessa área de memória compartilhada. Dessa forma, o sacrifício da área de memória compartilhada é mais justo.

Veja ipc/shm.c shm_swap()

Como o número da página física correspondente a uma página virtual de uma memória compartilhada do System V especificada está incluído na tabela de páginas de cada processo que compartilha a memória virtual, o processo de troca do kernel deve modificar as tabelas de páginas de todos os processos para refletir que a página é não está mais disponível.memória e no arquivo de troca. Para cada página compartilhada trocada, o processo de troca deve encontrar a entrada correspondente para esta página na tabela de páginas de cada processo compartilhado (procurando cada ponteiro vm_area_struct) se a entrada para esta página de memória compartilhada na tabela de páginas de um processo for válida, a processo de troca irá invalidá-lo, marcá-lo como uma página de troca e diminuir o número em uso desta página compartilhada em 1. O formato da tabela de página compartilhada do System V trocada consiste em um índice no grupo de estrutura de dados shmid_ds e um índice na entrada da tabela de página nesta área de memória compartilhada.

Se toda a memória compartilhada tiver sido modificada e o número de páginas em uso se tornar 0, a página compartilhada poderá ser gravada no arquivo de troca. A entrada para esta página na tabela de páginas apontada pela estrutura de dados shmid_ds desta área de memória compartilhada do System V será substituída pela entrada da tabela de páginas trocadas. A entrada da tabela de páginas trocadas é inválida, mas contém um índice para o arquivo de troca aberto e o deslocamento desta página dentro deste arquivo. Essas informações são usadas para recuperar a página de volta na memória física.

3.3 Trocando e Descartando Páginas

O processo de troca se reveza para verificar se todos os processos no sistema estão disponíveis para troca. Bons candidatos são processos que podem ser trocados (alguns não) e têm uma ou mais páginas que podem ser trocadas ou descartadas. As páginas são trocadas da memória física para o arquivo de troca do sistema somente quando nada mais funciona.

Veja mm/vmscan.c swap_out()

A maior parte do conteúdo da imagem de execução do arquivo de imagem pode ser relido a partir do arquivo. Por exemplo: as instruções de execução de uma imagem não serão alteradas por si só, portanto, não precisa ser gravada no arquivo de troca. Essas páginas são simplesmente descartadas. Se referenciado pelo processo novamente, ele pode ser carregado na memória novamente a partir da imagem de execução.

Uma vez que o processo a ser trocado é determinado, o processo de troca examina todas as suas regiões de memória virtual, procurando regiões que não são compartilhadas ou bloqueadas. O Linux não troca todas as páginas do processo selecionado que podem ser trocadas, mas apenas remove um pequeno número de páginas. Se uma página estiver bloqueada na memória, ela não poderá ser trocada ou descartada.

Consulte mm/vmscan.c swap_out_vme() Rastreia o ponteiro vm_next vm_nex na estrutura vm_area_struct organizada no processo mm_struct.

O algoritmo de troca do Linux usa a idade da página. Cada página tem um contador (na estrutura de dados mem_map_t) que informa ao processo de troca do kernel se vale a pena trocar a página. As páginas envelhecem quando não estão em uso e são atualizadas quando acessadas. O processo de troca apenas troca as páginas antigas. Por padrão, a idade recebe o valor 3 quando a página é alocada pela primeira vez. A cada visita, sua idade aumenta em 3 até chegar aos 20. Cada vez que o processo de troca do sistema é executado, ele diminui a idade da página em 1 para torná-la antiga. Esse comportamento padrão pode ser alterado, portanto, essas informações (e outras informações relacionadas) são armazenadas na estrutura de dados swap_control.

Se a página for muito antiga (idade = 0), o processo de troca será processado. Páginas sujas podem ser trocadas, e o Linux descreve tais páginas com um bit dependente da arquitetura no PTE que descreve a página (veja a Figura 3.2). No entanto, nem todas as páginas sujas precisam ser gravadas no arquivo de troca. A área de memória virtual de cada processo pode ter sua própria operação de troca (indicada pelo ponteiro vm_ops em vm_area_struct), se sim, o processo de troca a utilizará desta forma. Caso contrário, o processo de troca aloca uma página do arquivo de troca e grava a página no arquivo.

A entrada da tabela de páginas para esta página é substituída por uma entrada inválida, mas contém informações sobre a página no arquivo de troca: o deslocamento dentro do arquivo onde a página está localizada e o arquivo de troca usado. Independentemente da troca, a página física original é colocada de volta na free_area para relançamento. Páginas limpas (ou não sujas) podem ser descartadas e colocadas de volta na free_area para reutilização.

Se páginas suficientes para o processo de troca forem trocadas ou descartadas, o processo de troca será suspenso novamente. Na próxima vez que acordar, considerará o próximo processo no sistema. Dessa forma, o processo de troca devora as páginas físicas de cada processo até que o sistema seja reequilibrado. Isso é mais justo do que trocar todo o processo.

3.9 O Cache de Troca

Ao trocar páginas para o arquivo de troca, o Linux evita escrever páginas desnecessárias. Às vezes, uma página pode existir tanto no arquivo de troca quanto na memória física. Isso acontece quando uma página é trocada da memória e trazida de volta à memória quando o processo deseja acessá-la. Enquanto a página na memória não tiver sido gravada, a cópia no arquivo de troca continua válida.

O Linux usa o cache de troca para acompanhar essas páginas. O cache de troca é uma entrada da tabela de páginas ou uma lista vinculada de páginas físicas do sistema. Uma página de troca tem uma entrada de tabela de páginas que descreve o arquivo de troca usado e sua localização no arquivo de troca. Se a entrada do cache de troca for diferente de zero, significa que uma página no arquivo de troca não foi alterada. Se a página for posteriormente modificada (escrita), sua entrada será removida do cache de troca)

Quando o Linux precisa trocar uma página física para o arquivo de troca, ele procura no cache de troca e, se houver uma entrada válida para a página, ele não precisa gravar a página no arquivo de troca. Porque esta página na memória não foi modificada desde a última vez que o arquivo de troca foi lido.

As entradas no cache de troca são entradas da tabela de páginas que já foram trocadas. Eles são marcados como inválidos, mas contêm informações que permitem que o Linux encontre o arquivo de troca correto e as páginas corretas no arquivo de troca.

3.10 Troca de Página

Páginas sujas armazenadas no arquivo de troca podem precisar ser acessadas novamente. Por exemplo: quando a aplicação deseja gravar dados na memória virtual, e a página física correspondente a esta página é trocada para o arquivo de troca. Acessar páginas de memória virtual que não estão na memória física causará uma falha de página. Uma falha de página é um sinal do processador para informar ao sistema operacional que ele não pode converter memória virtual em memória física. Porque a entrada da tabela de páginas na memória virtual que descreve esta página é marcada como inválida após a troca. O processador não pode manipular a tradução de endereço virtual para físico, transferindo o controle de volta para o sistema operacional, informando o endereço virtual do erro e o motivo do erro. O formato dessas informações e como o processador transfere o controle de volta para o sistema operacional depende do tipo de processador. O código de manipulação de falha de página dependente do processador deve localizar a estrutura de dados vm_area_struct que descreve a área de memória virtual que contém o endereço virtual da falha. Ele funciona examinando a estrutura de dados vm_area_struct do processo até encontrar aquela que contém o endereço virtual incorreto. Este é um código muito crítico em termos de tempo, portanto, a estrutura de dados vm_area_struct de um processo é organizada de uma maneira específica para fazer essa pesquisa levar o menor tempo possível.

veja arch/i386/mm/fault.c do_page_fault()

Tendo executado as ações dependentes do processador apropriadas e encontrado uma memória virtual válida, incluindo o endereço virtual onde a falha ocorreu (ocorreu), o processo de tratamento de falhas de página é novamente genérico e pode ser usado para todos os processadores nos quais o Linux pode ser executado. O código genérico de tratamento de falhas de página pesquisa a entrada da tabela de páginas para o endereço virtual incorreto. Se a entrada da tabela de páginas encontrada for uma página que foi trocada, o Linux deve trocar a página de volta para a memória física. O formato das entradas da tabela de páginas para páginas trocadas depende do processador, mas todos os processadores marcam essas páginas como inválidas e colocam as informações necessárias na entrada da tabela de páginas para localizar a página no arquivo de troca. O Linux usa essas informações para paginar a página de volta na memória física.

Veja mm/memory.c do_no_page()

Neste ponto, o Linux sabe o endereço virtual onde o erro (ocorreu) e a entrada da tabela de páginas sobre onde esta página é trocada. A estrutura de dados Vm_area_struct pode conter um ponteiro para uma rotina para trocar páginas nessa memória virtual de volta para a memória física. Esta é a operação de swapin. Se houver uma operação de swapin nesta memória, o Linux a utilizará. Na verdade, a razão pela qual a memória compartilhada trocada do System V precisa de processamento especial é porque o formato das páginas de memória compartilhada trocada do System V é diferente daquele das páginas de troca comuns. Se não houver operação de swapin, o Linux assume que esta é uma página normal e não requer tratamento especial. Ele aloca uma página física livre e lê as páginas trocadas do arquivo de troca. As informações sobre onde (e qual arquivo de troca) do arquivo de troca são obtidas de uma entrada de tabela de página inválida.

Veja mm/page_alloc.c swap_in()

Se o acesso que causou a falha de página não foi um acesso de gravação, a página é deixada no cache de troca e sua entrada na tabela de páginas é marcada como não gravável. Se a página for gravada posteriormente, ocorre outra falha de página, momento em que a página é marcada como suja e sua entrada é removida do cache de troca. Se a página não foi modificada e precisa ser trocada, o Linux pode evitar gravar a página no arquivo de troca porque a página já está no arquivo de troca.

Se o acesso para trazer a página de volta do arquivo de troca for um acesso de gravação, a página será removida do cache de troca e a página de entrada da tabela de páginas para esta página será marcada como suja e gravável.

4. Processos

Os processos executam tarefas no sistema operacional. Um programa é uma imagem executável de uma série de instruções de código de máquina e dados armazenados em disco e, portanto, é uma entidade passiva. Um processo pode ser pensado como um programa de computador em execução. É uma entidade dinâmica que está em constante mudança à medida que o processador executa instruções de código de máquina. Processando as instruções e dados do programa, o processo também inclui o contador do programa e outros registradores da CPU, bem como a pilha, que inclui dados temporários, como parâmetros de rotina, endereços de retorno e variáveis ​​salvas. O programa ou processo atualmente em execução inclui toda a atividade atual no microprocessador. Linux é um sistema operacional multiprocesso. Os processos são tarefas separadas com seus próprios direitos e responsabilidades. Se um processo travar, não deve travar outro processo no sistema. Cada processo independente é executado em seu próprio espaço de endereço virtual e não pode afetar outros processos, exceto por meio de um mecanismo gerenciado pelo núcleo seguro.

Durante o tempo de vida de um processo, ele usa muitos recursos do sistema. Ele usa a CPU do sistema para executar suas instruções e a memória física do sistema para armazená-lo e seus dados. Ele abre e usa arquivos no sistema de arquivos e, direta ou indiretamente, usa os dispositivos físicos do sistema. O Linux deve acompanhar o próprio processo e os recursos do sistema que ele usa para gerenciar de maneira justa esse processo e outros processos no sistema. Se um processo monopoliza a maior parte da memória física e da CPU do sistema, é injusto com outros processos.

O recurso mais precioso do sistema é a CPU. Normalmente existe apenas um sistema. Linux é um sistema operacional multiprocesso. Seu objetivo é manter o processo rodando em todas as CPUs do sistema, fazendo pleno uso da CPU. Se houver mais processos do que a CPU (o que é mais comum), os processos restantes devem esperar até que a CPU seja liberada antes de poderem ser executados. O multiprocessamento é uma ideia simples: um processo continua rodando até que tenha que esperar, normalmente por algum recurso do sistema, antes de continuar rodando. Em um sistema de processo único, como o DOS, a CPU é simplesmente configurada para ocioso, de modo que o tempo de espera é desperdiçado. Em um sistema multiprocesso, muitos processos estão na memória ao mesmo tempo. Quando um processo precisa esperar, o sistema operacional retira a CPU do processo e a entrega a outro processo que precisa mais dela. é o agendador selecionado

O próximo processo mais apropriado. O Linux usa uma série de esquemas de agendamento para garantir a justiça.

Linux suporta muitos formatos de arquivos executáveis ​​diferentes, ELF é um deles, Java é outro. O Linux deve gerenciar esses arquivos de forma transparente porque os processos usam as bibliotecas compartilhadas do sistema.

4.1 Processos Linux

No Linux, cada processo é representado por uma estrutura de dados task_struct (interoperabilidade de tarefas e processos no Linux), que é usada para gerenciar os processos no sistema. A tabela de vetores de tarefas é uma matriz de ponteiros para cada estrutura de dados task_struct no sistema. Isso significa que o número máximo de processos no sistema é limitado pela tabela de vetores de tarefas, que é 512 por padrão. Quando um novo processo é criado, uma nova task_struct é alocada da memória do sistema e adicionada à tabela de vetores de tarefas. Para facilitar a localização, use o ponteiro atual para apontar para o processo em execução no momento.

veja include/linux/sched.h

Além dos processos normais, o Linux também suporta processos em tempo real. Esses processos devem reagir rapidamente a eventos externos (daí o nome "tempo real") e o escalonador deve ser tratado de forma diferente dos processos normais do usuário. Embora a estrutura de dados task_struct seja muito grande e complexa, seus campos podem ser divididos nas seguintes funções:

Quando o processo State é executado, ele muda de estado de acordo com a situação. Os processos do Linux usam os seguintes estados: (SWAPPING é omitido aqui porque não parece ser usado)

Processo em execução está em execução (é o processo atual do sistema) ou pronto para execução (aguardando agendamento em uma CPU do sistema)

Um processo Aguardando está aguardando um evento ou recurso. O Linux distingue entre dois tipos de processos de espera: interrompíveis e ininterruptos. Um processo de espera ininterrupta pode ser interrompido por um sinal, enquanto um processo de espera ininterrupta espera diretamente por uma condição de hardware e não pode ser interrompido por nenhuma situação.

Parado Processo parado, geralmente ao receber um sinal. O processo que está sendo depurado pode estar em um estado parado.

O processo finalizado pelo Zombie, por algum motivo, tem uma entrada na estrutura de dados task_struct na tabela de vetores de tarefas. Assim como parece, é um processo morto.

Informações de Agendamento O agendador precisa dessas informações para decidir com justiça quais dos processos do sistema devem ser executados.

Cada processo no sistema de Identificadores tem um identificador de processo. O identificador do processo não é um índice na tabela de vetores de tarefas, mas apenas um número. Cada processo também possui identificadores de usuário e grupo. Usado para controlar o acesso do processo a arquivos e dispositivos no sistema.

Inter-Process Communication Linux suporta mecanismos UNIX-IPC tradicionais, nomeadamente sinais, pipes e semáforos, bem como mecanismos IPC do System V, nomeadamente memória partilhada, semáforos e filas de mensagens. Os mecanismos IPC suportados pelo Linux são descritos na Seção 5.

Links Em um sistema Linux, nenhum processo é completamente independente de outros processos. Todo processo no sistema, exceto o processo inicial, tem um processo pai. O novo processo não é criado, mas copiado ou clonado do processo anterior. task_struct de cada processo tem ponteiros para seus processos pai e irmão (processos com o mesmo pai) e seus processos filho.

Em sistemas Linux, você pode ver os relacionamentos familiares de processos em execução com o comando pstree:

init(1)-+-crond(98)
|-emacs(387)
|-gpm(146)
|-inetd(110)
|-kerneld(18)
|-kflushd(2)
|-klogd(87)
|-kswapd(3)
|-login(160)---bash(192)---emacs(225)
|-lpd(121)
|-mingetty(161)
|-mingetty(162)
|-mingetty(163)
|-mingetty(164)
|-login(403)---bash(404)---pstree(594)
|-sendmail(134)
|-syslogd(78)
`-update(166)

Além disso, todas as informações do processo no sistema também são armazenadas em uma lista duplamente vinculada de estrutura de dados task_struct, e a raiz é o processo init. Esta tabela permite que o Linux encontre todos os processos no sistema. Ele precisa dessa tabela para fornecer suporte a comandos como ps ou kill.

Tempos e Temporizadores Durante o ciclo de vida de um processo, o núcleo mantém o controle de seus outros tempos além do tempo de CPU que ele usa. A cada fatia de tempo (tique do relógio), o kernel atualiza o tempo gasto pelo processo atual em instantes no sistema e no modo de usuário. O Linux também oferece suporte a contadores para intervalos de tempo especificados pelo processo. Um processo pode usar uma chamada de sistema para configurar um cronômetro e enviar um sinal para si mesmo quando o cronômetro expirar. Este temporizador pode ser único ou periódico.

O processo do sistema de arquivos pode abrir ou fechar arquivos conforme necessário.A estrutura task_struct do processo armazena um ponteiro para cada descritor de arquivo aberto e um ponteiro para dois inodes VFS. Cada inode VFS descreve exclusivamente um arquivo ou diretório em um sistema de arquivos e também fornece uma interface comum para o sistema de arquivos subjacente. Como os sistemas de arquivos são suportados no Linux é descrito na Seção 9. O primeiro inode é a raiz do processo (seu diretório inicial) e o segundo é seu diretório atual ou pwd. Pwd é obtido do comando Unix: print out working directory. Os dois nós VFS possuem campos de contagem que crescem à medida que um ou mais processos os referenciam. É por isso que você não pode excluir um diretório que um processo definiu como seu diretório de trabalho.

Memória virtual A maioria dos processos tem alguma memória virtual (os threads do kernel e os daemons do kernel não têm), e o kernel do Linux deve saber como essa memória virtual é mapeada para a memória física do sistema.

O processo Processor Specific Context pode ser visto como a soma do estado atual do sistema. Enquanto o processo for executado, ele usará os registradores, a pilha e assim por diante do processador. Quando um processo é suspenso, o contexto desses processos e o contexto relacionado à CPU devem ser salvos na estrutura task_struct do processo. Quando o agendador reinicia o processo, seu contexto é restaurado a partir daqui.

4.2 Identificadores

O Linux, como todos os Unixes, usa identificadores de usuário e grupo para verificar direitos de acesso a arquivos e imagens no sistema. Todos os arquivos em um sistema Linux têm propriedade e permissões, e essas permissões descrevem quais permissões o sistema possui nesse arquivo ou diretório. As permissões básicas são ler, escrever e executar, e 3 grupos de usuários são atribuídos: o proprietário do arquivo, processos pertencentes a um grupo específico e outros processos no sistema. Cada grupo de usuários pode ter permissões diferentes, por exemplo, um arquivo pode ser lido e gravado por seu proprietário e lido por seu grupo, mas outros processos do sistema não podem acessá-lo.

O Linux usa grupos para conceder permissões a um grupo de usuários para um arquivo ou diretório, em vez de para um único usuário ou processo no sistema. Por exemplo, você pode criar um grupo para todos os usuários em um projeto de software, para que somente eles possam ler e escrever o código-fonte do projeto. Um processo pode pertencer a vários grupos (32 por padrão), e esses grupos são colocados na tabela de vetores de grupos na estrutura task_struct de cada processo. Desde que um dos grupos aos quais um processo pertence tenha acesso a um arquivo, o processo terá as permissões de grupo apropriadas para o arquivo.

Existem 4 pares de identificadores de processo e grupo na task_struct de um processo.

Uid, gid  o identificador do usuário e o identificador do grupo usado pelo processo a ser executado

uid e gid efetivos  Alguns programas alteram o uid e o gid do processo de execução para seus próprios (nas propriedades da imagem de execução do inode VFS). Esses programas são chamados de programas setuid. Isso é útil porque pode restringir o acesso a serviços, especialmente aqueles executados no caminho de outra pessoa, como um daemon de rede. O uid e o gid efetivos vêm do programa setuid, e o uid e o gid permanecem os mesmos. O kernel verifica o uid e o gid efetivos ao verificar os privilégios.

O uid e o gid do sistema de arquivos  geralmente são iguais ao uid e gid efetivos, verifique os direitos de acesso ao sistema de arquivos. Sistema de arquivos para montagem via NFS. Neste momento, o servidor NFS de modo de usuário precisa acessar o arquivo como um processo especial. Apenas o uid e o gid do sistema de arquivos mudam (não o uid e o gid efetivos). Isso impede que usuários mal-intencionados enviem sinais Kill para servidores NFS. Kill é enviado para o processo com um uid e gid efetivos especiais.

Uid e gid salvos  Este é um requisito do padrão POSIX, permitindo que os programas alterem o uid e o gid de um processo por meio de chamadas do sistema. Usado para armazenar o uid e o gid reais depois que o uid e o gid originais forem alterados.

4.3 Agendamento

Todos os processos são executados parcialmente no modo de usuário e parcialmente no modo de sistema. Como o hardware subjacente suporta esses estados varia, mas geralmente há um mecanismo de segurança para ir do modo de usuário para o modo de sistema e vice-versa. O modo de usuário tem permissões muito menores do que o modo de sistema. Toda vez que um processo executa uma chamada de sistema, ele alterna do modo de usuário para o modo de sistema e continua a execução. Neste ponto, deixe o núcleo executar o processo. No Linux, os processos não estão competindo entre si para serem o processo em execução no momento, eles não podem parar outros processos em execução e executar a si mesmos. Cada processo abandona a CPU quando precisa aguardar algum evento do sistema. Por exemplo, um processo pode ter que esperar para ler um caractere de um arquivo. Essa espera acontece em uma chamada de sistema no modo de sistema. O processo usa uma função de biblioteca para abrir e ler o arquivo, que por sua vez executa uma chamada de sistema para ler bytes do arquivo aberto. Nesse ponto, o processo em espera será suspenso e outro processo mais valioso será selecionado para execução. Os processos geralmente chamam chamadas de sistema, portanto, geralmente precisam esperar. Mesmo que o processo precise esperar, ele pode usar eventos de CPU desbalanceados, então o Linux usa agendamento preemptivo. Com esse esquema, cada processo pode ser executado por um pequeno período de tempo, 200 milissegundos. Quando esse tempo se esgota, outro processo é selecionado para ser executado e o processo original aguarda um pouco até ser executado novamente. Esse período de tempo é chamado de fatia de tempo.

O escalonador deve selecionar o processo mais merecedor de todos os processos que podem ser executados no sistema. Um processo executável é aquele que apenas espera pela CPU. O Linux usa um algoritmo de escalonamento baseado em prioridade simples e razoável para escolher entre os processos atuais no sistema. Quando ele seleciona um novo processo para ser executado, ele salva o estado do processo atual, registros relacionados ao processador e outras informações contextuais que precisam ser salvas na estrutura de dados task_struct do processo. Em seguida, ele restaura o estado do novo processo a ser executado (novamente, associado ao processador) e transfere o controle do sistema para esse processo. Para distribuir o tempo de CPU de maneira justa entre todos os processos executáveis ​​no sistema, o escalonador armazena informações na estrutura task_struct de cada processo:

Veja também kernel/sched.c schedule()

política A política de agendamento do processo. O Linux tem dois tipos de processos: normal e em tempo real. Os processos em tempo real têm prioridade mais alta do que todos os outros processos. Se houver um processo ativo pronto para ser executado, ele sempre será executado primeiro. Existem duas estratégias para processos em tempo real: round robin e first in first out. Na estratégia de escalonamento do anel, cada processo em tempo real é executado em sequência, enquanto na estratégia primeiro a entrar, primeiro a sair, cada processo executável é executado de acordo com sua ordem na fila de escalonamento, e essa ordem não será alterada.

Priority A prioridade de agendamento do processo. Também a quantidade de tempo (jiffies) que ele pode usar quando é permitido executar. Você pode alterar a prioridade de um processo por meio de chamadas do sistema ou do comando renice.

Rt_priority Linux suporta processos em tempo real. Esses processos têm prioridade mais alta do que outros processos não em tempo real no sistema. Este campo permite que o escalonador atribua uma prioridade relativa a cada processo em tempo real. A prioridade de um processo em tempo real pode ser modificada com uma chamada de sistema

Coutner A quantidade de tempo (jiffies) que o processo pode ser executado neste momento. Quando o processo inicia, é igual à prioridade (priority), que é decrementada a cada ciclo de clock.

O agendador é executado a partir de vários locais no núcleo. Ele pode ser executado após o processo atual ser colocado na fila de espera ou pode ser executado após a chamada do sistema antes que o processo retorne do estado do sistema para o estado do processo. Outra razão para executar o escalonador é que o relógio do sistema simplesmente coloca o contador do processo atual em zero. Toda vez que o agendador é executado, ele faz o seguinte:

Veja também kernel/sched.c schedule()

O agendador de trabalho do kernel executa o manipulador da metade inferior e lida com a fila de tarefas agendadas do sistema. Esses threads principais leves são descritos em detalhes na Seção 11

O processo atual deve descartar o processo atual antes de selecionar outro processo.

Se a política de agendamento do processo atual for anel, ela será colocada no final da fila de execução.

Se a tarefa for interrompível e recebeu um sinal na última vez em que foi agendada, seu estado muda para RUNNING

Se o processo atual expirar, seu estado se tornará RUNNING

Se o estado do processo atual for RUNNING, mantenha este estado

Os processos que não estão em EXECUÇÃO ou INTERRUPTÍVEIS são removidos da fila de execução. Isso significa que tais processos não são considerados quando o escalonador procura os processos mais valiosos para serem executados.

O agendador de Seleção de Processos examina os processos na fila de execução para encontrar os processos mais valiosos a serem executados. Se houver um processo em tempo real (com uma política de agendamento em tempo real), ele será mais pesado que um processo normal. O peso de um processo normal é seu contador, mas para um processo em tempo real é contador mais 1000. Isso significa que, se houver um processo executável em tempo real no sistema, ele sempre será executado antes de qualquer processo executável normal. O processo atual, por consumir algumas fatias de tempo (seu contador é reduzido), ficará em desvantagem se outros processos de mesma prioridade estiverem no sistema: como deveria estar. Se vários processos tiverem a mesma prioridade, o que estiver mais próximo da frente da fila de execução será selecionado. O processo atual é colocado na parte de trás da fila de execução. Se um sistema balanceado tiver um grande número de processos de mesma prioridade, execute esses processos em ordem. Isso é chamado de política de agendamento em anel. No entanto, como os processos precisam aguardar recursos, a ordem em que são executados pode mudar.

Trocar processos Se o processo mais valioso não for o processo atual, o processo atual deve ser suspenso e o novo processo será executado. Quando um processo é executado, ele usa a CPU e os registradores do sistema e a memória física. Cada vez que ele chama uma rotina, ele passa parâmetros através de registradores ou pilhas, salva valores como o endereço de retorno da rotina que está chamando, e assim por diante. Portanto, quando o agendador é executado, ele é executado no contexto do processo atual. Pode estar no modo privilegiado: modo kernel, mas ainda é o processo em execução no momento. Quando o processo deve ser suspenso, todo o estado da máquina, incluindo o contador de programa (PC) e todos os registradores do processador, devem ser armazenados na estrutura de dados task_struct do processo. Em seguida, todo o estado da máquina do novo processo deve ser carregado. Essa operação depende do sistema, não exatamente a mesma em CPUs diferentes, mas geralmente com a ajuda de algum hardware.

A troca do contexto do processo acontece no final do cronograma. O contexto armazenado pelo processo anterior é um instantâneo do contexto de hardware do sistema quando o processo está programado para terminar. Da mesma forma, quando um novo contexto de processo é carregado, ainda há um instantâneo no final do agendamento, incluindo o conteúdo do contador de programa do processo e dos registradores.

Se o processo anterior ou o novo processo atual usar memória virtual, as tabelas de páginas do sistema precisam ser atualizadas. Novamente, essa ação depende da arquitetura. Os processadores Alpha AXP, usando TLT (Translation Look-aside Table) ou entradas da tabela de páginas armazenadas em cache, devem limpar as entradas da tabela de páginas armazenadas em cache pertencentes ao processo anterior.

4.3.1 Agendamento em Sistemas Multiprocessadores

No mundo Linux, existem relativamente poucos sistemas multi-CPU, mas muito trabalho foi feito para tornar o Linux um sistema operacional SMP (multiprocessamento simétrico). Ou seja, a capacidade de balancear a carga entre as CPUs do sistema. O balanceamento de carga não é mais importante do que no agendador.

Em um sistema multiprocessador, a situação desejada é que todos os processadores estejam ocupados executando processos. Cada processo executa o escalonador independentemente até que seu processo atual fique sem intervalo de tempo ou tenha que esperar por recursos do sistema. A primeira coisa que deve ser observada em sistemas SMP é que pode haver mais de um processo ocioso no sistema. Em um sistema de processador único, o processo ocioso é a primeira tarefa na tabela de vetores de tarefas.Em um sistema SMP, cada CPU tem um processo ocioso e você pode ter mais de uma CPU ociosa. Além disso, cada CPU possui um processo atual, portanto, o sistema SMP deve registrar os processos atuais e inativos de cada processador.

Em um sistema SMP, a task_struct de cada processo contém o número do processador atualmente em execução (processador) e o último número do processador em execução (last_processor). Não faz sentido porque um processo não deve ser executado em uma CPU diferente cada vez que é escolhido para ser executado, mas o Linux pode usar processor_mask para restringir um processo a uma ou mais CPUs. Se o bit N estiver definido, o processo pode ser executado no processador N. Quando o escalonador escolhe um processo para ser executado, ele não considera os processos cujos bits correspondentes de processor_mask não estão definidos. O escalonador também aproveita o último processo em execução no processador atual, pois geralmente há um custo de desempenho na movimentação de um processo para outro processador.

4.4 Arquivos

A Figura 4.1 mostra as duas estruturas de dados usadas para descrever as informações relacionadas ao sistema de arquivos em cada processo do sistema. O primeiro fs_struct contém o inode VFS do processo e seu umask. Umask é o modo padrão quando novos arquivos são criados e podem ser alterados por meio de chamadas do sistema.

veja include/linux/sched.h

A segunda estrutura de dados, files_struct, contém informações sobre todos os arquivos atualmente em uso pelo processo. Os programas lêem da entrada padrão, gravam na saída padrão e enviam mensagens de erro para o erro padrão. Podem ser arquivos, terminais de entrada/saída ou dispositivos do século, mas todos são considerados arquivos do ponto de vista de um programa. Cada arquivo tem seu descritor e files_struct contém ponteiros para 256 resultados de dados de arquivo, cada um descrevendo um arquivo em forma de processo. O campo F_mode descreve o modo no qual o arquivo foi criado: somente leitura, leitura-gravação ou somente gravação. F_pos registra a posição da próxima operação de leitura e escrita no arquivo. F_inode aponta para o inode que descreve o arquivo e f_ops é um ponteiro para um conjunto de endereços de rotina, cada um dos quais é uma função para processar o arquivo. Por exemplo, funções que gravam dados. Essa interface abstrata é muito poderosa, permitindo que o Linux suporte um grande número de tipos de arquivos. Podemos ver que o pipe também é implementado com esse mecanismo no Linux.

Cada vez que um arquivo é aberto, um ponteiro de arquivo livre em files_struct é usado para apontar para a nova estrutura de arquivo. Existem 3 descritores de arquivo já abertos quando o processo do Linux é iniciado. Estes são entrada padrão, saída padrão e erro padrão, todos herdados do processo pai que os criou. O acesso aos arquivos se dá por meio de chamadas de sistema padrão, que precisam passar ou retornar um descritor de arquivo. Esses descritores são índices na tabela de vetores fd do processo, portanto, os descritores de arquivo para entrada padrão, saída padrão e erro padrão são 0, 1 e 2, respectivamente. Todo o acesso ao arquivo é realizado usando as rotinas de operação do arquivo na estrutura de dados do arquivo junto com seu inode VFS.

4.5 Memória Virtual

A memória virtual de um processo inclui a execução de código e dados de várias fontes. A primeira é uma imagem de programa carregada, como o comando ls. Esse comando, como todas as imagens de execução, consiste em código e dados de execução. O arquivo de imagem contém todas as informações necessárias para carregar o código de execução e os dados do programa associado na memória virtual do processo. Segundo, um processo pode alocar memória (virtual) durante o processamento, como para manter o conteúdo de um arquivo que ele lê. A memória virtual recém-alocada precisa ser conectada à memória virtual existente do processo antes de poder ser usada. No terceiro, os processos do Linux usam bibliotecas de código comum, como processamento de arquivos. Não faz sentido que cada processo inclua uma cópia da biblioteca, o Linux usa bibliotecas compartilhadas, que podem ser compartilhadas por vários processos em execução concorrente. O código e os dados nessas bibliotecas compartilhadas devem estar vinculados ao espaço de endereço virtual do processo e ao espaço de endereço virtual de outros processos que compartilham a biblioteca.

Em um determinado momento, um processo não utiliza todo o código e dados contidos em sua memória virtual. Pode incluir código destinado a ser usado em situações específicas, como inicialização ou manipulação de eventos específicos. Ele pode estar usando apenas algumas das rotinas em sua biblioteca compartilhada. Seria um desperdício carregar todo esse código na memória física e não usá-lo. Multiplique esse desperdício pelo número de processos no sistema e a eficiência operacional do sistema será muito baixa. Em vez disso, o Linux usa a tecnologia de paginação por demanda, onde a memória virtual de um processo é trazida para a memória física somente quando o processo tenta usá-la. Portanto, o Linux não carrega código e dados diretamente na memória, mas modifica a tabela de páginas do processo para marcar essas áreas virtuais como existentes, mas não na memória. Quando um processo tenta acessar esse código ou dados, o hardware do sistema gera uma falha de página, passando o controle para o kernel Linux para processamento. Portanto, para cada região de memória virtual do espaço de endereçamento do processo, o Linux precisa saber de onde veio e como colocá-la na memória para que possa lidar com essas falhas de página.

O kernel do Linux precisa gerenciar todas essas áreas de memória virtual, e o conteúdo da memória virtual de cada processo é descrito por uma estrutura de dados mm_struct mm_struc apontada por sua task_struct. A estrutura de dados mm_struct do processo também inclui informações sobre a imagem de execução carregada e um ponteiro para a tabela de páginas do processo. Ele contém ponteiros para um conjunto de estruturas de dados vm_area_struct, cada uma representando uma área de memória virtual no processo.

Essa lista vinculada é classificada na ordem da memória virtual. A Figura 4.2 mostra a distribuição da memória virtual de um processo simples e as principais estruturas de dados que o gerenciam. Como essas áreas de memória virtual vêm de fontes diferentes, o Linux abstrai a interface por vm_area_struct apontando para um conjunto de rotinas de processamento de memória virtual (via vm_ops). Dessa forma, toda a memória virtual de um processo pode ser tratada de maneira consistente, independentemente do serviço subjacente que gerencia essa memória. Por exemplo, haveria uma rotina genérica que é chamada quando um processo tenta acessar uma memória inexistente, que é como as falhas de página são tratadas.

Quando o Linux cria novas áreas de memória virtual para um processo e manipula referências à memória virtual que não estão na memória física do sistema, a lista de estrutura de dados vm_area_struct do processo é referenciada repetidamente. Isso significa que o tempo necessário para encontrar a estrutura de dados vm_area_struct correta é importante para o desempenho do sistema. Para acelerar o acesso, o Linux também coloca a estrutura de dados vm_area_struct em uma árvore AVL (Adelson-Velskii e Landis). A árvore é organizada de forma que cada vm_area_struct (ou nó) tenha um ponteiro esquerdo e um direito para a estrutura vm_area_struct adjacente. O ponteiro esquerdo aponta para um nó com um endereço virtual inicial mais baixo e o ponteiro direito aponta para um nó com um endereço virtual inicial mais alto. Para encontrar o nó correto, o Linux inicia na raiz da árvore e segue os ponteiros esquerdo e direito de cada nó até que o vm_area_struct correto seja encontrado. É claro que liberar no meio dessa árvore não leva tempo e inserir um novo vm_area_struct leva tempo extra de processamento.

Quando um processo aloca memória virtual, o Linux não reserva memória física para o processo. Ele descreve essa memória virtual por meio de uma nova estrutura de dados vm_area_struct, que é conectada à lista de memória virtual do processo. Quando o processo tenta gravar nessa nova área de memória virtual, o sistema falhará na página. O processador tenta decodificar este endereço virtual, mas não há entrada na tabela de páginas para essa memória, ele vai desistir e gerar uma exceção de falha de página, que o kernel do Linux irá manipular. O Linux verifica se o endereço virtual referenciado está no espaço de endereço virtual do processo e, em caso afirmativo, o Linux cria o PTE apropriado e aloca uma página de memória física para o processo. Talvez o código ou os dados correspondentes precisem ser carregados do sistema de arquivos ou do disco de troca e, em seguida, o processo é executado novamente a partir da instrução que causou a falha de página, porque desta vez a memória realmente existe e pode continuar.

4.6 Criando um Processo

Quando o sistema é iniciado, ele é executado no estado kernel, neste momento existe apenas um processo: o processo de inicialização. Como todos os outros processos, o processo inicial tem um conjunto de estados de máquina representados por pilhas, registradores e assim por diante. Essas informações são armazenadas na estrutura de dados task_struct do processo inicial quando outros processos no sistema são criados e executados. No final da inicialização do sistema, o processo inicial inicia um thread principal (chamado init) e executa um loop inativo, sem fazer nada. O escalonador executa esse processo ocioso quando não há nada a fazer. O task_struct deste processo ocioso é o único que não é alocado dinamicamente, mas definido estaticamente quando o núcleo é conectado, para evitar confusão, ele é chamado de init_task.

A thread ou processo do núcleo Init tem o identificador de processo 1 e é o primeiro processo real do sistema. Ele executa alguma configuração inicial do sistema (como ligar o sistema para controlá-lo, montar o sistema de arquivos raiz) e, em seguida, executa a rotina de inicialização do sistema. Dependendo do seu sistema, pode ser um dos /etc/init, /bin/init ou /sbin/init. O programa Init usa /etc/inittab como um arquivo de script para criar novos processos no sistema. Esses novos processos podem criar novos processos. Por exemplo, o processo getty pode criar um processo de login quando o usuário tenta fazer login. Todos os processos no sistema são descendentes do thread principal init.

A criação de um novo processo é conseguida clonando o processo antigo ou clonando o processo atual. Uma nova tarefa é criada por meio de uma chamada de sistema (fork ou clone) e a clonagem ocorre no estado do kernel do kernel. Ao final da chamada do sistema, um novo processo é gerado, aguardando que o escalonador o escolha para execução. Aloque uma ou mais páginas físicas da memória física do sistema para a pilha do processo clonado (usuário e núcleo) para a nova estrutura de dados task_struct. Será criado um identificador de processo exclusivo dentro do grupo de identificadores de processo do sistema. No entanto, também é possível que o processo clonado retenha o identificador de processo de seu pai. O novo task_struct entra na tabela de vetores de tarefas e o conteúdo do task_struct do processo antigo (atual) é copiado para o task_struct clonado.

Veja kernel/fork.c do_fork()

Ao clonar processos, o Linux permite que dois processos compartilhem recursos em vez de ter cópias separadas. Incluindo arquivos de processo, manipulação de sinal e memória virtual. Ao compartilhar esses recursos, seus campos de contagem correspondentes aumentam ou diminuem de acordo, para que o Linux não libere esses recursos até que ambos os processos parem de usá-los. Por exemplo, se o processo clonado quiser compartilhar memória virtual, seu task_struct incluirá um ponteiro para o mm_struct do processo original e o campo de contagem do mm_struct será incrementado para indicar o número de processos que o compartilham atualmente.

A clonagem da memória virtual de um processo requer uma habilidade considerável. Um conjunto de estruturas de dados vm_area_struct, as estruturas de dados mm_struct correspondentes e a tabela de páginas do processo clonado devem ser geradas sem copiar a memória virtual do processo. Essa pode ser uma tarefa difícil e demorada, pois parte da memória virtual pode estar na memória física e outra parte pode estar no arquivo de troca. Em vez disso, o Linux usa uma técnica chamada "copiar na gravação", que copia a memória virtual apenas quando um dos dois processos tenta gravar. Qualquer memória virtual que não seja escrita, e possivelmente até escrita, pode ser compartilhada entre dois processos. A memória somente leitura, como código de execução, pode ser compartilhada. Para implementar "copiar na gravação", a entrada da tabela de páginas de uma área gravável é marcada como somente leitura e a estrutura de dados vm_area_struct que a descreve é ​​marcada como "cópia na gravação". Uma falha de página ocorre quando um processo tenta gravar nessa memória virtual. Neste ponto, o Linux fará uma cópia dessa memória e processará as tabelas de páginas e as estruturas de dados da memória virtual dos dois processos.

4.7 Tempos e Temporizador

O núcleo rastreia o tempo de CPU do processo e algum outro tempo. A cada ciclo de clock, o kernel atualiza os instantes do processo atual para representar a soma do tempo gasto no sistema e no modo de usuário.

Além desses cronômetros de contabilidade, o Linux também oferece suporte a cronômetros de intervalo específicos do processo. Um processo pode usar esses temporizadores para sinalizar a si mesmo quando esses temporizadores expirarem. Três tipos de temporizadores de intervalo são suportados:

Veja kernel/timer.c

Real Este timer utiliza temporização em tempo real, e quando o timer expira, um sinal SIGALRM é enviado ao processo.

Virtual Este temporizador conta apenas quando o processo está em execução, quando expira, envia um sinal SIGVTALARM ao processo.

O perfil é oportuno tanto quando o processo está em execução quanto quando o sistema executa em nome do processo. Quando expirar, será enviado um sinal SIGPROF.

Um ou todos os temporizadores de intervalo podem ser executados e o Linux registra todas as informações necessárias na estrutura de dados task_struct do processo. Esses temporizadores de intervalo podem ser criados, iniciados, parados e o valor atual lido usando chamadas do sistema. Os temporizadores virtuais e de perfil são tratados da mesma maneira: a cada ciclo de clock, o temporizador do processo atual é decrementado e, se expirar, o sinal apropriado é emitido

Veja kernel/sched.c do_it_virtual(), do_it_prof()

Os temporizadores de intervalo em tempo real são ligeiramente diferentes. O mecanismo pelo qual o Linux usa temporizadores é descrito na Seção 11. Cada processo possui sua própria estrutura de dados timer_list. Ao usar um timer de tempo real, a tabela de timer do sistema é usada. Quando ele expira, a segunda metade do processo do temporizador o remove da fila e chama o manipulador do temporizador de intervalo. Ele gera o sinal SIGALRM e reinicia o cronômetro de intervalo, adicionando-o de volta à fila do cronômetro do sistema.

Veja também: kernel/iterm.c it_real_fn()

4.8 Executando Programas

No Linux, como no Unix, os programas e comandos geralmente são executados por meio de um interpretador de comandos. O interpretador de comandos é um processo do usuário como qualquer outro processo, chamado de shell (imagine uma noz com o kernel como a parte comestível do meio, e o shell o cerca, fornecendo uma interface). Existem muitos shells no Linux, os mais usados ​​são sh, bash e tcsh. Exceto por alguns comandos internos, como cd e pwd, os comandos são binários executáveis. Para cada comando inserido, o shell procura um nome correspondente no diretório especificado pelo caminho de pesquisa do processo atual (na variável de ambiente PATH). Se o arquivo for encontrado, carregue-o e execute-o. O shell se clona usando o mecanismo de fork mencionado acima e substitui a imagem binária que está executando (o shell) pelo conteúdo do arquivo de imagem de execução encontrado no processo filho. Normalmente, o shell espera que o comando termine ou o subprocesso saia. Você pode enviar um sinal SIGSTOP para o processo filho digitando control-Z, que interrompe o processo filho e o coloca em segundo plano, permitindo que o shell seja executado novamente. Você pode usar o comando shell bg para que o shell envie um sinal SIGCONT para o processo filho, coloque o processo filho em segundo plano e execute-o novamente, ele continuará sendo executado até terminar ou precisar de entrada ou saída do terminal.

O arquivo executável pode estar em vários formatos ou até mesmo em um arquivo de script. Os arquivos de script devem ser reconhecidos e executados com um interpretador adequado. Por exemplo, /bin/sh interpreta scripts de shell. Arquivos de objetos executáveis ​​contêm código e dados executáveis ​​e outras informações suficientes para que o sistema operacional possa carregá-los na memória e executá-los. O tipo de arquivo objeto mais comumente usado no Linux é o ELF e, em teoria, o Linux é flexível o suficiente para lidar com praticamente qualquer formato de arquivo objeto.

Assim como os sistemas de arquivos, os formatos binários que o Linux pode suportar são construídos diretamente no kernel quando o kernel está vinculado ou podem ser carregados como módulos. O kernel mantém uma lista de formatos binários suportados (veja a Figura 4.3), e ao tentar executar um arquivo, cada formato binário é testado até que funcione. Em geral, os binários suportados pelo Linux são a.out e ELF. O executável não precisa ser lido completamente na memória, mas usa uma técnica chamada carregamento de demanda. Parte da imagem de execução é carregada na memória quando o processo a utiliza, e imagens não utilizadas podem ser descartadas da memória.

veja fs/exec.c do_execve()

 

4.9 ELF

Os arquivos de objeto ELF (Executable and Linkable Format), projetados pela Unix Systems Labs, agora são o formato mais usado para Linux. Embora haja uma pequena sobrecarga de desempenho em comparação com outros formatos de arquivo de objeto, como ECOFF e a.out, o ELF parece mais flexível. Os arquivos executáveis ​​ELF incluem código executável (às vezes chamado de texto) e dados. A tabela na imagem de execução descreve como o programa deve ser colocado na memória virtual do processo. Imagens vinculadas estaticamente são criadas com o vinculador (ld) ou editor de links, e uma única imagem contém todo o código e dados necessários para executar a imagem. Esta imagem também descreve o layout da imagem na memória e o endereço na imagem da primeira parte do código a ser executado.

A Figura 4.4 mostra o layout de uma imagem executável ELF vinculada estaticamente. Este é um programa C simples que imprime "hello world" e sai. O arquivo de cabeçalho descreve que é uma imagem ELF, com dois cabeçalhos físicos (e_phnum é 2), a partir do 52º byte no início do arquivo de imagem (e_phoff). O primeiro cabeçalho físico descreve o código de execução na imagem, no endereço virtual 0x8048000, com 65532 bytes. Por estar vinculado estaticamente, ele inclui todo o código da biblioteca que chama printf() que gera "hello world". A entrada da imagem, ou seja, a primeira instrução do programa, não está localizada no início da imagem, mas no endereço virtual 0x8048090 (e_entry). O código começa imediatamente após o segundo cabeçalho físico. Este cabeçalho físico descreve os dados do programa e será carregado na memória virtual no endereço 0x8059BB8. Este pedaço de dados pode ser lido e escrito. Você notará que o tamanho dos dados no arquivo é de 2200 bytes (p_filesz) e o tamanho da memória é de 4248 bytes. Porque os primeiros 2.200 bytes contêm dados pré-inicializados e os próximos 2.048 bytes contêm dados que serão inicializados pelo código em execução.

veja include/linux/elf.h

Quando o Linux carrega a imagem executável ELF no espaço de endereço virtual do processo, não é a imagem real carregada. Ele configura as estruturas de dados da memória virtual, vm_area_struct do processo e suas tabelas de página. Quando o programa executa uma falta de página, o código e os dados do programa serão colocados na memória física. As partes do programa não utilizadas não serão colocadas na memória. Uma vez que o carregador de formato binário ELF satisfaça as condições, a imagem é uma imagem executável ELF válida, que limpa a imagem executável atual do processo de sua memória virtual. Como este processo é uma imagem clonada (como todos os processos são), a imagem antiga é a imagem do programa executado pelo processo pai (por exemplo, o interpretador de comandos shell bash). A limpeza de imagens executáveis ​​antigas descarta estruturas de dados de memória virtual antigas e redefine as tabelas de páginas do processo. Ele também limpa o conjunto de outros manipuladores de sinal, fechando arquivos abertos. No final do processo de limpeza, o processo está pronto para executar a nova imagem executável. Independentemente do formato da imagem executável, as mesmas informações são definidas no mm_struct do processo. Inclui ponteiros para o início do código e dados na imagem. Esses valores são lidos do cabeçalho físico da imagem executável ELF, e a parte que eles descrevem também é mapeada no espaço de endereço virtual do processo. Isso também acontece quando a estrutura de dados vm_area_struct do processo é criada e a tabela de páginas é modificada. A estrutura de dados mm_struct também inclui ponteiros para os parâmetros passados ​​ao programa e as variáveis ​​de ambiente do processo.

Bibliotecas compartilhadas ELF

As imagens vinculadas dinamicamente, por sua vez, não contêm todo o código e os dados necessários para serem executados. Alguns deles são colocados em bibliotecas compartilhadas e vinculados à imagem em tempo de execução. Quando a biblioteca dinâmica de tempo de execução está vinculada à imagem, o vinculador dinâmico também usa a tabela de biblioteca compartilhada ELF. O Linux usa vários vinculadores dinâmicos, ld.so.1, libc.so.1 e ld-linux.so.1, todos no diretório /lib. Essas bibliotecas incluem código comum, como sub-rotinas de linguagem. Sem vinculação dinâmica, todos os programas teriam que ter cópias independentes dessas bibliotecas, exigindo mais espaço em disco e memória virtual. No caso de vinculação dinâmica, a tabela da imagem ELF inclui informações sobre todas as rotinas da biblioteca referenciadas. Essas informações instruem o vinculador dinâmico como localizar rotinas de biblioteca e como vincular ao espaço de endereço do programa.

4.10 Arquivos de Scripts

Arquivos de script são arquivos executáveis ​​que requerem um interpretador para serem executados. Há um grande número de interpretadores no Linux, como wish, perl e interpretadores de comandos, como tcsh. O Linux usa a convenção padrão do Unix de incluir o nome do interpretador na primeira linha de um arquivo de script. Portanto, um arquivo de script típico pode começar com:

#!/usr/bin/desejo

O carregador de arquivos de script tenta descobrir qual interpretador o arquivo está usando. Ele tenta abrir o executável especificado na primeira linha do arquivo de script. Se ele puder ser aberto, obtenha um ponteiro para o inode VFS do arquivo e execute-o para interpretar o arquivo de script. O nome do arquivo de script se torna o parâmetro 0 (o primeiro parâmetro), e todos os outros parâmetros são deslocados para cima (o primeiro parâmetro original se tornou o segundo parâmetro, etc.). Carregar um interpretador é o mesmo que carregar outros programas executáveis ​​no Linux. O Linux tenta vários formatos binários sucessivamente até funcionar. Isso significa que, em teoria, você pode empilhar vários interpretadores e formatos binários para tornar os manipuladores de formatos binários do Linux mais flexíveis.

Veja fs/binfmt_script.c do_load_script()

5. Mecanismos de comunicação entre processos

Os processos se comunicam entre si e com o núcleo, coordenando seu comportamento. O Linux suporta alguns mecanismos para comunicação entre processos (IPC). Sinais e pipes são dois deles, e o Linux também suporta o mecanismo System V IPC (nomeado após a versão do Unix que apareceu pela primeira vez).

5.1 Sinais

Os sinais são um dos métodos mais antigos de comunicação entre processos usados ​​em sistemas Unix. Sinais usados ​​para enviar eventos assíncronos para um ou mais processos. O sinal pode ser gerado usando o terminal do teclado ou por uma condição de erro, como um processo tentando acessar um local em sua memória virtual que não existe. O shell também usa sinais para enviar sinais de controle de trabalho para seus processos filhos.

Alguns sinais são gerados pelo núcleo e outros podem ser gerados por outros processos privilegiados no sistema. Você pode usar o comando kill (kill -l) para listar o conjunto de sinais do seu sistema, gerado no meu sistema Linux Intel:

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SELO

5) SIGTRAP 6) SIGIOT 7) SIGBUS 8) SIGFPE

9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2

13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD

18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN

22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO

30) CINGAPURA

Os números são diferentes nos sistemas Alpha AXP Linux. Um processo pode optar por ignorar a maioria dos sinais gerados, com duas exceções: SIGSTOP (para interromper a execução do processo) e SIGKILL (para fazer com que o processo saia) não podem ser ignorados, embora um processo possa escolher como tratar os sinais. Um processo pode bloquear um sinal e, se não bloquear um sinal, pode optar por tratá-lo sozinho ou deixar que o núcleo o trate. Se manuseado pelo núcleo, o comportamento padrão para este sinal será executado. Por exemplo, a ação padrão de um processo que recebe um SIGFPE (exceção de ponto flutuante) é gerar núcleo e sair. Os sinais não têm prioridade inerente, se um processo gerar dois sinais ao mesmo tempo, eles aparecerão no processo em qualquer ordem e serão processados ​​em qualquer ordem. Além disso, não há mecanismo para lidar com vários sinais do mesmo tipo. Não há como um processo saber se recebeu 1 ou 42 sinais SIGCONT.

O Linux usa as informações armazenadas no task_struct do processo para implementar o mecanismo de sinal. Os sinais suportados são limitados pelo tamanho da palavra do processador. Um processador de comprimento de palavra de 32 bits pode ter 32 sinais, enquanto um processador de 64 bits, como o Alpha AXP, pode ter até 64 sinais. O sinal atualmente pendente é colocado no campo de sinal e o campo bloqueado é colocado na máscara de sinal a ser bloqueado. Todos os sinais podem ser bloqueados, exceto SIGSTOP e SIGKILL. Se for gerado um sinal bloqueado, ele permanece pendente até ser desbloqueado. O Linux também salva informações sobre como cada processo trata cada sinal possível.Esta informação é colocada em um array de estruturas de dados sigaction, e a task_struct de cada processo tem um ponteiro para o array correspondente. Esta matriz contém o endereço da rotina que trata o sinal, ou um sinalizador que informa ao Linux se o processo deseja ignorar o sinal ou deixar que o núcleo o trate. Os processos alteram o tratamento de sinal padrão executando chamadas de sistema que alteram a orientação de sinal apropriada e a máscara de bloqueio.

Nem todos os processos no sistema podem enviar sinais para todos os outros processos, apenas o núcleo e o superusuário podem. Processos normais só podem enviar sinais para processos que tenham o mesmo uid e gid ou estejam no mesmo grupo de processos. Os sinais são gerados definindo os bits apropriados no sinal de task-struct. Se o processo não está bloqueando o sinal, e está esperando, mas pode ser interrompido (estado é Interruptível), então seu estado é alterado para Em execução e é confirmado que está na fila de execução, e desta forma é acordado. Dessa forma, o escalonador o tratará como um candidato em execução quando o sistema o agendar novamente. Se o tratamento padrão for necessário, o Linux pode otimizar o tratamento de sinais. Por exemplo, se o sinal SIGWINCH (janela X muda de foco) ocorrer e o manipulador padrão for usado, nada precisa ser feito.

Os sinais não aparecem no processo imediatamente quando são gerados, eles devem esperar até a próxima execução do processo. Toda vez que um processo sai de um sistema chame seu sinal e os campos bloqueados são verificados, e se houver algum sinal desbloqueado, ele pode ser enviado. Isso pode parecer pouco confiável, mas todo processo no sistema está invocando uma chamada de sistema, como no processo de escrever um caractere no terminal. Os processos podem optar por esperar por um sinal, se desejarem, e ficam no estado Interruptível até que um sinal seja dado. O código de manipulação de sinal do Linux verifica a estrutura sigaction para cada sinal atualmente desbloqueado.

Se um manipulador de sinal for definido como a ação padrão, o núcleo o tratará. O processamento padrão do sinal SIGSTOP é alterar o estado do processo atual para Parado, depois executar o agendador e selecionar um novo processo a ser executado. A ação padrão do sinal SIGFPE é fazer com que o processo atual gere um núcleo (core dump) e deixe-o sair. Alternativamente, um processo pode especificar seu próprio manipulador de sinal. Esta é uma rotina que é chamada quando o sinal é gerado e a estrutura sigaction contém o endereço desta rotina. O Linux deve chamar a rotina de tratamento de sinal do processo, e como isso acontece depende do processador. No entanto, todas as CPUs têm que lidar com processos que estão sendo executados no modo kernel e estão se preparando para retornar ao modo de usuário que chamou rotinas do kernel ou do sistema. A solução para este problema é lidar com a pilha e os registradores do processo. O contador do programa de processo é definido para o endereço de seu manipulador de sinal e os argumentos da rotina são adicionados à estrutura de chamada ou passados ​​por registradores. Quando o processo recomeça, parece que o manipulador de sinal é uma chamada normal.

O Linux é compatível com POSIX, portanto, um processo pode especificar um sinal a ser bloqueado ao chamar um manipulador de sinal específico. Isso significa alterar a máscara bloqueada ao chamar o manipulador de sinais do processo. Quando o manipulador de sinal termina, a máscara bloqueada deve ser restaurada ao seu valor original. Portanto, o Linux adiciona uma chamada a uma rotina de limpeza à pilha do processo que recebeu o sinal, restaurando a máscara bloqueada ao seu valor original. O Linux também otimiza esta situação: se várias rotinas de processamento de sinais precisam ser chamadas ao mesmo tempo, elas são empilhadas juntas e cada vez que um manipulador é encerrado, o próximo é chamado e a rotina de arrumação não é chamada até o final.

5.2 Tubos

Shells normais do Linux permitem redirecionamento. Por exemplo:

$ l | pr | lpr

Paginar canalizando a saída do comando ls, que lista os arquivos de diretório, para a entrada padrão do comando pr. Finalmente, a saída padrão do comando pr é canalizada para a entrada padrão do comando lpr e o resultado é impresso na impressora padrão. Um pipe é um fluxo unidirecional de bytes que conecta a saída padrão de um processo com a entrada padrão de outro processo. Nenhum dos processos está ciente desse redirecionamento e funciona normalmente. É o shell que estabelece o pipe temporário entre os processos. No Linux, os pipes são implementados usando duas estruturas de dados de arquivo apontando para o mesmo inode VFS temporário (que aponta para uma página física na memória). A Figura 5.1 mostra que cada estrutura de dados de arquivo contém ponteiros para uma tabela de vetores de diferentes rotinas de manipulação de arquivos: uma para escrita e outra para leitura do pipe. Isso mascara a diferença das chamadas usuais do sistema para leitura e gravação de arquivos comuns. Quando o processo de gravação grava no pipe, os bytes são copiados para a página de dados compartilhada e, ao ler do pipe, os bytes são copiados da página compartilhada. O Linux deve sincronizar o acesso aos pipes. As gravações e leituras do pipeline devem ser mantidas em sincronia, usando bloqueios, filas de espera e sinais.

Veja include/linux/inode_fs.h

Quando o processo de gravação grava no pipe, ele usa as funções de biblioteca de gravação padrão. O descritor de arquivo passado por essas funções de biblioteca é um índice no grupo de estruturas de dados de arquivo do processo, cada uma representando um arquivo aberto, neste caso, um canal aberto. A chamada do sistema Linux usa a rotina de gravação apontada pela estrutura de dados do arquivo que descreve esse pipe. A rotina de gravação usa as informações armazenadas no inode VFS que representa o canal para gerenciar solicitações de gravação. Se houver espaço suficiente para gravar todos os bytes no pipe, desde que o pipe não esteja bloqueado pelo processo de leitura, o Linux bloqueia o processo de gravação e copia os bytes do espaço de endereço do processo para a página de dados compartilhada. Se o pipe estiver bloqueado pelo processo de leitura ou não houver espaço suficiente, o processo atual dorme e é colocado na fila de espera do nó do pipe I e chama o escalonador para executar outro processo. É interrompível, por isso pode receber sinais. Quando houver espaço suficiente no pipe para escrever os dados ou o bloqueio for liberado, o processo de escrita será despertado pelo processo de leitura. Depois que os dados são gravados, o bloqueio do inode VFS do pipeline é liberado e todos os processos de leitura na fila de espera do inode do pipeline serão ativados.

Veja fs/pipe.c pipe_write()

Ler dados de um pipe é muito semelhante a escrever dados. Os processos têm permissão para fazer leituras não bloqueantes (dependendo do modo em que abriram o arquivo ou pipe), caso em que um erro é retornado se não houver dados para ler ou o pipe estiver bloqueado. Isso significa que o processo continuará a ser executado. Outra maneira é esperar na fila de espera do inode do pipe até que o processo de escrita seja concluído. Se o processo do pipeline tiver concluído a operação, o inode do pipeline e a página de dados compartilhada correspondente serão descartados.

Veja fs/pipe.c pipe_read()

O Linux também pode suportar pipes nomeados, também chamados de FIFOs, porque os pipes funcionam primeiro a entrar, primeiro a sair. Os primeiros dados gravados no pipe são os primeiros dados a serem lidos. Não quero pipes, FIFOs não são objetos temporários, são entidades no sistema de arquivos que podem ser criadas com o comando mkfifo. Um processo pode usar um FIFO desde que tenha os direitos de acesso apropriados. FIFOs são abertos de forma ligeiramente diferente dos tubos. Um pipe (suas duas estruturas de dados de arquivo, o inode VFS e a página de dados compartilhados) é criado uma vez, enquanto o FIFO já existe e pode ser aberto e fechado por seu usuário. O Linux tem que lidar com processos que abrem o FIFO para leitura antes que o processo de escrita abra o FIFO, e processos que leem antes do processo de escrita gravar dados. Fora isso, os FIFOs são tratados quase exatamente como pipes e usam as mesmas estruturas e operações de dados.

5.3 Mecanismos IPC do Sistema V

O Linux suporta três mecanismos de comunicação entre processos que apareceram pela primeira vez no Unix System V (1983): filas de mensagens, semáforos e memória compartilhada (filas de mensagens, semáforos e memória compartilhada). O mecanismo IPC do System V compartilha um método de autenticação comum. Os processos só podem acessar esses recursos por meio de chamadas de sistema, passando um identificador de referência exclusivo para o kernel. As verificações de acesso a objetos IPC do System V usam permissões de acesso da mesma forma que as verificações de acesso a arquivos. O acesso a objetos IPC do System V é criado pelo criador do objeto por meio de uma chamada de sistema. Cada mecanismo usa o identificador de referência do objeto como um índice na tabela de recursos. Este não é um índice direto, algumas operações são necessárias para gerar o índice.

Todas as estruturas de dados do Linux no sistema que representam objetos IPC do System V incluem uma estrutura de dados ipc_perm, incluindo os identificadores de usuário e grupo do processo de criação, o modo de acesso (proprietário, grupo e outros) para o objeto e a chave do IPC objeto. A chave é usada como um método para localizar o identificador de referência de um objeto IPC do System V. Dois tipos de chaves são suportados: pública e quatro. Se a chave for pública, qualquer processo no sistema poderá localizar o identificador de referência do objeto System V IPC correspondente, desde que passe na verificação de permissão. Os objetos IPC do System V não podem ser referenciados por chave, eles devem ser referenciados usando seu identificador de referência.

Veja include/linux/ipc.h

5.4 Filas de mensagens

Uma fila de mensagens permite que um ou mais processos gravem mensagens e um ou mais processos leiam mensagens. O Linux mantém uma tabela de vetores msgque de uma série de filas de mensagens. Cada uma dessas unidades aponta para uma estrutura de dados msqid_ds que descreve completamente a fila de mensagens. Ao criar uma fila de mensagens, aloque uma nova estrutura de dados msqid_ds da memória do sistema e insira-a na tabela de vetores

Cada estrutura de dados msqid_ds inclui uma estrutura de dados ipc_perm e ponteiros para mensagens que entram na fila. Além disso, o Linux retém o tempo de alteração da fila, como o tempo da última gravação da fila, etc. A fila Msqid_ds também inclui duas filas de espera: uma para gravação na fila de mensagens e outra para leitura.

Veja include/linux/msg.h

Cada vez que um processo tenta gravar uma mensagem na fila de gravação, seus identificadores efetivos de usuário e grupo são comparados com o esquema da estrutura de dados ipc_perm da fila. Se o processo puder gravar nessa fila, a mensagem será gravada do espaço de endereço do processo na estrutura de dados msg e colocada no final da fila de mensagens. Cada mensagem carrega um token de um tipo especificado pelo aplicativo acordado entre os processos. No entanto, como o Linux limita o número e o comprimento das mensagens que podem ser gravadas, pode não haver espaço para mensagens. Neste momento, o processo será colocado na fila de espera de gravação da fila de mensagens e, em seguida, o escalonador será chamado para selecionar um novo processo a ser executado. Acorde quando uma ou mais mensagens forem lidas dessa fila de mensagens.

A leitura da fila é um processo semelhante. Os direitos de acesso do processo também são verificados. Um processo de leitura pode escolher entre ler a primeira mensagem da fila independentemente do tipo de mensagem ou escolher um tipo especial de mensagem. Se não houver nenhuma mensagem elegível, o processo de leitura será adicionado ao processo de espera de leitura da fila de mensagens e, em seguida, será executado o agendador. Quando uma nova mensagem é gravada na fila, o processo será ativado e continuará em execução.

5.5 Semáforos

Em sua forma mais simples, um semáforo é um local na memória cujo valor pode ser verificado e definido por vários processos. A operação de check and set, pelo menos para cada processo associado, é ininterrupta ou atômica: não pode ser encerrada uma vez iniciada. O resultado da operação de checksum set é a soma do valor atual e o valor de set do semáforo, que pode ser positivo ou negativo. Dependendo dos resultados das operações de teste e definição, um processo pode ter que dormir até que o valor do semáforo seja alterado por outro processo. Os semáforos podem ser usados ​​para implementar regiões críticas, ou seja, áreas de código importantes onde apenas um processo pode ser executado por vez.

Digamos que você tenha muitos processos cooperativos lendo e gravando registros de um único arquivo de dados. Você pode querer que o acesso aos arquivos seja estritamente coordenado. Você pode usar um semáforo com um valor inicial de 1. No código para a operação de arquivo, adicione duas operações de semáforo, a primeira para verificar e diminuir o valor do semáforo e a segunda para verificar e aumentá-lo. O primeiro processo que acessa o arquivo tenta diminuir o valor do semáforo e, se for bem-sucedido, o valor do semáforo se torna 0. O processo agora pode continuar a ser executado e usar o arquivo de dados. No entanto, se outro processo precisar usar esse arquivo e agora tentar diminuir o valor do semáforo, ele falhará porque o resultado será -1. O processo será suspenso até que o primeiro processo termine de processar o arquivo de dados. Quando o primeiro processo termina de processar o arquivo de dados, ele incrementa o semáforo para 1. Agora o processo de espera será acordado e sua tentativa de diminuir o semáforo terá sucesso desta vez.

Cada objeto de semáforo IPC do System V descreve uma matriz de semáforos, que o Linux usa a estrutura de dados semid_ds para representar. Todas as estruturas de dados semid_ds no sistema são apontadas pela tabela de vetores de ponteiro secundário. Cada array de semáforos possui sem_nsems, que são descritos por uma estrutura de dados sem apontada por sem_base. Todos os processos que têm permissão para operar na matriz de semáforos de um objeto de semáforo IPC do System V podem operar neles por meio de chamadas do sistema. Uma chamada de sistema pode especificar uma variedade de operações, cada uma das quais é descrita por três entradas adicionais: o índice do semáforo, o valor da operação e um conjunto de sinalizadores. O índice do semáforo é o índice da matriz do semáforo e o valor da operação é o valor a ser adicionado ao valor do semáforo atual. Primeiro, o Linux verifica se todas as operações foram bem-sucedidas. A operação é bem-sucedida apenas se o valor atual do operando mais o semáforo for maior que 0 ou se o operando e o valor atual do semáforo forem 0. Se qualquer operação de semáforo falhar, o Linux suspenderá o processo desde que o sinalizador de operação não exija que a chamada do sistema não seja bloqueante. Se o processo for suspenso, o Linux deve salvar o estado da operação do semáforo a ser executada e colocar o processo atual na fila de espera. Ele faz isso construindo uma estrutura de dados sem_queue na pilha e preenchendo-a. A nova estrutura de dados sem_queue é colocada no final da fila de espera do objeto semáforo (usando os ponteiros sem_pending e sem_pending_last). O processo atual é colocado na fila de espera (dormente) dessa estrutura de dados sem_queue, e o escalonador é chamado para executar outro processo.

Veja include/linux/sem.h

Se todas as operações de semáforo forem bem-sucedidas, o processo atual não precisará ser suspenso. O Linux segue em frente e aplica essas operações aos membros apropriados do array de semáforos. Agora o Linux deve verificar se há processos suspensos ou suspensos, suas ações podem agora ser executáveis. O Linux procura sequencialmente cada membro da fila de espera de operação (sem_pending) para verificar se sua operação de semáforo pode ser bem-sucedida agora. Ele remove a estrutura de dados sem_queue da lista de espera da operação, se possível, e aplica a operação de semáforo à matriz de semáforos. Ele ativa o processo de suspensão para que possa continuar em execução na próxima vez em que o agendador for executado. O Linux verifica a fila de espera do início ao fim até que nenhum processo possa ser ativado executando uma operação de semáforo.

Há um problema na operação do semáforo: deadlock. Isso acontece quando um processo altera o valor de um semáforo para uma região crítica, mas não sai da região crítica porque travou ou foi morto. O Linux evita isso mantendo uma tabela de ajuste do array de semáforos. Ou seja, se esses ajustes forem implementados, o semáforo retorna ao estado de um processo antes da operação do semáforo. Esses ajustes são colocados na estrutura de dados sem_undo, enfileirados na estrutura de dados sem_ds e enfileirados na estrutura de dados task_struct do processo usando esses semáforos.

Uma ação de ajuste pode ser necessária para ser mantida para cada operação individual do sinalizador. O Linux mantém pelo menos uma estrutura de dados sem_undo para cada array de semáforos por processo. Se o processo solicitado não tiver um, crie um para ele quando necessário. Essa nova estrutura de dados sem_undo é enfileirada na estrutura de dados task_struct do processo e na estrutura de dados semid_ds da fila de semáforos. Ao executar uma operação em um semáforo na fila de semáforos, o valor deslocado pelo valor da operação é adicionado à entrada do semáforo na fila de ajuste da estrutura de dados sem_undo do processo. Portanto, se o valor da operação for 2, isso adiciona -2 à entrada de ajuste para este semáforo.

Quando um processo é excluído, como sair, o Linux percorre seu conjunto de estruturas de dados sem_undo e implementa ajustes no array de semáforos. Se um semáforo for excluído, sua estrutura de dados sem_undo permanece na fila task_struct do processo, mas o identificador de matriz de semáforo correspondente é marcado como inválido. Nesse caso, o código para limpar o semáforo simplesmente descarta a estrutura de dados sem_undo.

5.6 Memória Compartilhada

A memória compartilhada permite que um ou mais processos se comuniquem por meio da memória que está presente simultaneamente em seu espaço de endereço virtual. As páginas desta memória virtual são referenciadas pelas entradas da tabela de páginas na tabela de páginas de cada processo compartilhado. Mas não há necessidade de ter o mesmo endereço na memória virtual de todos os processos. Como acontece com todos os objetos IPC do System V, o acesso às áreas de memória compartilhada é controlado por chaves e verificado quanto aos direitos de acesso. Depois que a memória é compartilhada, não há mais como verificar como o processo usa essa memória. Eles devem contar com outros mecanismos, como semáforos do System V, para sincronizar o acesso à memória.

Cada região de memória recém-criada é representada por uma estrutura de dados shmid_ds. Essas estruturas de dados são mantidas na tabela de vetores shm_segs. A estrutura de dados Shmid_ds descreve o tamanho desse acesso à memória compartilhada, quantos processos estão usando e como a memória compartilhada é mapeada em seu espaço de endereço. Cabe ao criador da memória compartilhada controlar o acesso a essa memória e se suas chaves são públicas ou privadas. Ele também pode bloquear a memória compartilhada na memória física se tiver privilégios suficientes.

Veja include/linux/sem.h

Todo processo que deseja compartilhar essa memória deve se conectar à memória virtual por meio de uma chamada de sistema. Isso cria uma nova estrutura de dados vm_area_struct para o processo que descreve a memória compartilhada. Um processo pode escolher onde a memória compartilhada está localizada em seu espaço de endereço virtual ou o Linux pode escolher uma área livre suficiente.

Essa nova estrutura vm_area_struct é colocada na lista vm_area_struct apontada por shmid_ds. Vincule-os via vm_next_shared e vm_prev_shared. A memória virtual não é realmente criada quando é colada, acontece quando o primeiro processo tenta acessá-la.

Uma falha de página ocorre na primeira vez que um processo acessa uma das páginas da memória virtual compartilhada. Quando o Linux trata essa falha de página, ele encontra a estrutura de dados vm_area_struct que a descreve. Isso contém ponteiros para essas rotinas de manipulador de memória virtual compartilhada. O código de tratamento de falhas de página de memória compartilhada procura na lista de entradas da tabela de páginas shmid_ds para ver se há uma entrada para esta página de memória virtual compartilhada. Se não existir, ele aloca uma página física e cria uma entrada de tabela de páginas para ela. Esta entrada não é apenas inserida na tabela de páginas do processo atual, mas também armazenada neste shmid_ds. Isso significa que quando o próximo processo tentar acessar essa memória e obtiver uma falha de página, o código de tratamento de erros da memória compartilhada também permitirá que esse processo use a página física recém-criada. Portanto, é o primeiro processo a acessar uma página de memória compartilhada que faz com que a página seja criada, e outros processos que a acessam posteriormente fazem com que a página seja adicionada ao seu espaço de endereço virtual.

Quando os processos não precisam mais de memória virtual compartilhada, eles são desconectados dela. Essa separação afeta apenas o processo atual enquanto outros processos ainda estiverem usando essa memória. Seu vm_area_struct é removido da estrutura de dados shmid_ds e liberado. A tabela de páginas do processo atual também é atualizada, invalidando sua área de memória virtual compartilhada. Quando o último processo que compartilha essa memória é desvinculado dele, as páginas da memória compartilhada atualmente na memória física são liberadas e a estrutura de dados shmid_ds dessa memória compartilhada também é liberada.

É mais complicado se a memória virtual compartilhada não estiver bloqueada na memória física. Nesse caso, as páginas de memória compartilhada podem ser trocadas para o disco de troca do sistema quando o sistema estiver usando muita memória. Como a memória compartilhada é trocada de e para a memória física é descrita na Seção 3.

6、Interconexão de componentes periféricos(PCI)

PCI, como o próprio nome indica, é um padrão que descreve como conectar componentes periféricos em um sistema de maneira estruturada e controlada. A especificação padrão do barramento local PCI descreve como os componentes do sistema são conectados eletricamente e como eles se comportam. Esta seção explora como o kernel do Linux inicializa o barramento e os dispositivos PCI do sistema.

A Figura 6.1 é um diagrama lógico de um sistema baseado em PCI. O barramento PCI e a ponte PCI-PCI são a cola que mantém os componentes do sistema juntos. Os dispositivos CUP e de vídeo são conectados ao barramento PCI principal, barramento PCI 0. Um dispositivo PCI especial, a ponte PCI-PCI, conecta o barramento primário ao barramento PCI secundário, barramento PCI 1. Na terminologia de especificação PCI, o barramento PCI 1 é descrito como a jusante da ponte PCI-PCI e o barramento PCI 0 é a montante da ponte. Conectados ao barramento PCI secundário estão os dispositivos SCSI e Ethernet do sistema. Fisicamente, a ponte, o barramento PCI secundário e ambos os dispositivos podem estar na mesma placa PCI. A ponte PCI-ISA no sistema suporta dispositivos ISA antigos e legados.Esta figura mostra um chip controlador de super E/S que controla o teclado, o mouse e a unidade de disquete.

6.1 Espaço de Endereço PCI

Os dispositivos CPU e PCI precisam acessar a memória que compartilham. Essa memória permite que os drivers de dispositivo controlem esses dispositivos PCI e passem informações entre eles. A memória comumente compartilhada inclui os registros de controle e status do dispositivo. Esses registradores são usados ​​para controlar o dispositivo e ler seu status. Por exemplo, um driver de dispositivo PCI SCSI pode ler o registro de status do dispositivo SCSI para determinar se ele pode gravar uma informação no disco SCSI. Ou pode escrever no registrador de controle para que o dispositivo que desligou comece a funcionar.

A memória de sistema usada pela CPU pode ser usada como memória compartilhada, mas se for assim, toda vez que um dispositivo PCI acessa a memória, a CPU tem que parar, esperando que o dispositivo PCI seja concluído. O acesso à memória geralmente é restrito e apenas um componente do sistema tem permissão de acesso por vez. Isso vai desacelerar o sistema. Também não é uma boa ideia permitir que dispositivos externos ao sistema acessem a memória principal de forma descontrolada. Isso pode ser muito perigoso: um dispositivo malicioso pode tornar o sistema muito instável.

Dispositivos externos têm seu próprio espaço de memória. A CPU pode acessar esses espaços, mas o acesso do dispositivo à memória do sistema é estritamente controlado e deve passar pelo canal DMA (Direct Memory Access). Os dispositivos ISA podem acessar dois espaços de endereço: E/S ISA (entrada/saída) e memória ISA. PCI consiste em três partes: PCI I/O, memória PCI e espaço de configuração PCI. A CPU tem acesso a todos os espaços de endereço onde os espaços de endereço PCI I/O e memória PCI são usados ​​por drivers de dispositivo e o espaço de configuração PCI é usado pelo Linux e o código de inicialização PCI em mente.

O processador Alpha AXP não possui modos de acesso nativos a espaços de endereçamento além do espaço de endereçamento do sistema. Requer o uso de chips de suporte para acessar outros espaços de endereço, como o espaço de configuração PCI. Ele usa um esquema de mapeamento de espaço de endereço que rouba uma parte do enorme espaço de endereço virtual e o mapeia para o espaço de endereço PCI.

6.2 Cabeçalhos de Configuração PCI

Cada dispositivo PCI no sistema, incluindo a ponte PCI-PCI, consiste em uma estrutura de dados de configuração localizada no espaço de endereço de configuração PCI. O cabeçalho de configuração PCI permite que o sistema identifique e controle o dispositivo. A localização exata desse cabeçalho no espaço de endereço de configuração PCI depende da topologia PCI usada pelo dispositivo. Por exemplo, uma placa de vídeo PCI conectada a um slot PCI na placa-mãe de um PC terá seu cabeçalho de configuração em um local, enquanto se for inserida em outro slot PCI, seu cabeçalho aparecerá em outro local na memória de configuração PCI. Mas não importa onde esses dispositivos e pontes PCI estejam localizados, o sistema pode descobri-los e configurá-los usando os registros de status e configuração em seus cabeçalhos de configuração.

Normalmente, os sistemas são projetados para que o cabeçalho de configuração PCI de cada slot PCI tenha um deslocamento em relação ao seu slot na placa. Assim, por exemplo, a configuração PCI para o primeiro slot na placa pode estar no deslocamento 0 e o segundo slot no deslocamento 256 (todos os cabeçalhos têm o mesmo comprimento, 256 bytes) e assim por diante. Define mecanismos de hardware específicos do sistema para que o código de configuração PCI possa tentar verificar todos os cabeçalhos de configuração PCI possíveis em um determinado barramento PCI, tente ler um campo no cabeçalho (geralmente o campo Identificação do fornecedor) e obter alguns erros para saber que esses dispositivos existem e esses dispositivos não. A especificação PCI Local Bus descreve uma possível mensagem de erro: uma tentativa de ler os campos Verdor Identification e Device Identification de um slot PCI vazio retorna 0xFFFFFFFF.

 

A Figura 6.2 mostra o layout do cabeçalho de configuração PCI de 256 bytes. Inclui os seguintes domínios:

veja include/linux/pci.h

Identificação do fornecedor Um número exclusivo que descreve o inventor deste dispositivo PCI. A identificação do fornecedor PCI da Digital é 0x1011 e a da Intel é 0x8086.

Identificação do dispositivo Um número exclusivo que descreve o próprio dispositivo. Por exemplo, o dispositivo Fast Ethernet 21141 da Digital tem um identificador de dispositivo de 0x0009.

Status Este campo fornece o status do dispositivo, o significado de seus bits é especificado pela especificação PCI Local Bus.

O sistema de comando controla o dispositivo gravando neste campo. Por exemplo: Permitir que o dispositivo acesse a memória de E/S PCI.

O código de classe identifica o tipo de dispositivo. Existem classificações padrão para cada tipo de dispositivo: monitor, SCSI e assim por diante. O código de tipo para SCSI é 0x0100.

Registros de endereço de base Esses registros são usados ​​para determinar e atribuir o tipo, tamanho e localização de PCI I/O e memória PCI que um dispositivo pode usar.

O pino de interrupção 4 dos pinos físicos da placa PCI é usado para fornecer interrupções ao barramento PCI. Eles são rotulados A, B, C e D no padrão. O campo Interrupt Pin descreve qual pino este dispositivo PCI usa. Normalmente, para um dispositivo, isso é determinado pelo hardware. Ou seja, toda vez que o sistema é iniciado, o dispositivo usa o mesmo pino de interrupção. Essas informações permitem que o subsistema de tratamento de interrupções gerencie interrupções para esses dispositivos.

Linha de interrupção O campo Linha de interrupção no cabeçalho de configuração PCI é usado para transferir o controle de interrupção entre o código de inicialização PCI, drivers de dispositivo e o subsistema de tratamento de interrupção do Linux. O número escrito aqui não tem sentido para o driver de dispositivo, mas permite que o manipulador de interrupção envie corretamente uma interrupção do dispositivo PCI para o código de tratamento de interrupção do driver de dispositivo correto no sistema operacional Linux. Veja a Seção 7 para saber como o Linux lida com interrupções.

6.3 E/S PCI e Endereço de Memória PCI

Esses dois espaços de endereço são usados ​​para comunicação do driver de dispositivo entre os dispositivos e o kernel Linux em execução na CPU. Por exemplo: O dispositivo Fast Ethernet DECchip 21141 mapeia seus registros internos para o espaço de E/S PCI. Seu driver de dispositivo Linux então controla o dispositivo lendo e gravando nesses registradores. Drivers de vídeo normalmente usam uma grande quantidade de espaço de memória PCI para colocar informações de exibição.

Os dispositivos não podem acessar esses espaços de endereço até que o sistema PCI seja estabelecido e o acesso do dispositivo a esses espaços de endereço seja ativado usando o campo Comando no cabeçalho de configuração PCI. Deve-se notar que apenas o código de configuração PCI lê e grava endereços de configuração PCI, os drivers de dispositivo Linux apenas lêem e gravam endereços PCI I/O e memória PCI.

6.4 Pontes PCI-ISA

Essa ponte converte acessos de espaço de endereço de memória PCI e E/S PCI em acessos de memória ISA E/S e ISA para dar suporte a dispositivos ISA. A maioria dos sistemas vendidos hoje inclui vários slots de barramento ISA e vários slots de barramento PCI. A necessidade dessa compatibilidade com versões anteriores continuará a diminuir e haverá sistemas somente PCI no futuro. Nos primeiros dias do PC baseado em Intel 8080, o espaço de endereço ISA (E/S e memória) dos dispositivos ISA no sistema foi corrigido. Mesmo uma unidade de disquete ISA de um sistema de computador baseado em S5000 Alpha AXP teria os mesmos endereços de E/S ISA que o primeiro IBM PC. A especificação PCI reserva as regiões inferiores dos espaços de endereço PCI I/O e memória PCI para periféricos ISA no sistema e usa uma ponte PCI-ISA para converter todos os acessos de memória PCI a essas regiões em acessos ISA.

 

6.5 Pontes PCI-PCI(PCI-PCI桥)

As pontes PCI-PCI são dispositivos PCI especiais que unem os barramentos PCI no sistema. Havia apenas um barramento PCI em um sistema simples e, naquela época, havia um limite elétrico para o número de dispositivos PCI que um único barramento PCI podia suportar. Adicionar mais barramentos PCI usando uma ponte PCI-PCI permite que o sistema suporte mais dispositivos PCI. Isso é especialmente importante para servidores de alto desempenho. Claro, o Linux suporta totalmente o uso de pontes PCI-PCI.

6.5.1 Pontes PCI-PCI: PCI I/O e PCI Memory Windows

A ponte PCI-PCI só passa a jusante de um subconjunto de leituras e gravações de memória PCI I/O e PCI. Por exemplo, na Figura 6.1, a ponte PCI-PCI passará os endereços de leitura e gravação do barramento PCI 0 para o barramento 1 somente se os endereços de leitura e gravação pertencerem a dispositivos SCSI ou Ethernet, e o restante for ignorado. Essa filtragem evita que informações de endereço desnecessárias atravessem o sistema. Para conseguir isso, as pontes PCI-PCI devem ser programadas para definir a base e os limites dos acessos ao espaço de endereço de memória PCI e E/S PCI que devem passar do barramento primário para o barramento secundário. Uma vez que a ponte PCI-PCI no sistema é configurada, a ponte PCI-PCI fica invisível desde que o driver de dispositivo Linux só acesse PCI I/O e espaço de memória PCI através dessas janelas. Esse é um recurso importante que facilita a vida dos autores de drivers de dispositivo PCI para Linux. Mas também torna a ponte PCI-PCI no Linux um pouco complicada de configurar, como veremos em breve.

6.5.2 Pontes PCI-PCI: Ciclos de configuração PCI e numeração de barramento PCI

Como o código de inicialização PCI da CPU pode localizar dispositivos que não estão no barramento PCI primário, deve haver um mecanismo pelo qual a ponte possa decidir se deve passar os ciclos de configuração de sua interface primária para a interface secundária. Um ciclo é o endereço que mostra no barramento PCI. A especificação PCI define dois formatos de configuração de endereço PCI: Tipo 0 e Tipo 1, mostrados na Figura 6.3 e Figura 6.4, respectivamente. Um ciclo de configuração PCI do tipo 0 não contém um número de barramento e é interpretado por todos os dispositivos PCI nesse barramento PCI para configuração de endereço PCI. Os bits 32:11 do ciclo de configuração são considerados o campo de seleção do dispositivo. Uma maneira de projetar um sistema é fazer com que cada bit selecione um dispositivo diferente. Nesse caso, 11 pode selecionar o dispositivo PCI no slot 0, o bit 12 pode selecionar o dispositivo PCI no slot 1 e assim por diante. Outra maneira é escrever o número do slot do dispositivo diretamente nos bits 31:11. O mecanismo que um sistema usa depende do controlador de memória PCI do sistema.

Um ciclo de configuração PCI tipo 1 que inclui um número de barramento PCI é ignorado por todos os dispositivos PCI, exceto pontes PCI-PCI. Todas as pontes PCI-PCI que veem um ciclo de configuração PCI Tipo 1 podem passar essas informações downstream para elas. Se uma ponte PCI-PCI ignora o ciclo de configuração PCI ou o passa downstream depende de como a ponte está configurada. Cada ponte PCI-PCI tem um número de interface de barramento primário e um número de interface de barramento secundário. A interface do barramento primário está mais próxima da CPU e a interface do barramento secundário é a mais distante da CPU. Cada ponte PCI-PCI também possui um número de barramento secundário, que é o número máximo de barramentos PCI que podem ser interligados fora da segunda interface de barramento. Em outras palavras, o número do barramento secundário é o maior número do barramento PCI downstream da ponte PCI-PCI. Quando uma ponte PCI-PCI vê um ciclo de configuração PCI tipo 1, ela faz o seguinte:

Se o número do barramento especificado não estiver entre o número do barramento secundário da ponte e o número secundário do barramento, ele será ignorado.

Converte-o em um comando de configuração do tipo 0 se o número do barramento especificado corresponder ao número do barramento secundário da ponte

Se o número do barramento especificado for maior que o número do barramento secundário e menor ou igual ao número do barramento subordinado, ele é passado para a interface do barramento secundário inalterado.

Assim, se desejamos endereçar o dispositivo 1 no barramento 3 na topologia da Figura 6.9, devemos gerar um comando de configuração do tipo 1 da UCP. A Ponte 1 o passa inalterado para o Barramento 1, a Ponte 2 o ignora, mas o Ponte 3 o converte em um comando de configuração do tipo 0 e o envia para o Barramento 3, fazendo com que o Dispositivo 1 responda a ele.

Cada sistema operacional individual é responsável por atribuir números de barramento durante a fase de configuração PCI, mas independentemente do esquema de codificação usado, as seguintes declarações devem ser verdadeiras para todas as pontes PCI-PCI no sistema:

Todos os barramentos PCI localizados atrás de uma ponte PCI-PCI devem ser numerados entre (inclusive) o número do barramento secundário e o número do barramento auxiliar

Se esta regra for violada, a ponte PCI-PCI não poderá passar e traduzir corretamente o ciclo de configuração PCI tipo 1 e o sistema não poderá localizar e inicializar com êxito os dispositivos PCI no sistema. Para completar o esquema de codificação, o Linux configura esses dispositivos especiais em uma ordem específica. Consulte a Seção 6.6.2 para obter uma descrição do esquema de codificação de barramento e ponte PCI do Linux e um exemplo funcional.

6.6 Inicialização do PCI do Linux (processo de inicialização do PCI do Linux)

O código de inicialização PCI no Linux é dividido em três partes lógicas:

Driver de dispositivo PCI Este pseudo driver de dispositivo pesquisa o sistema PCI a partir do barramento 0 e localiza todos os dispositivos PCI e pontes no sistema. Ele cria uma lista de estruturas de dados vinculadas que descrevem a topologia do sistema. Além disso, ele codifica todas as pontes do sistema.

参见drivers/pci/pci.ce include/linux/pci.h

PCI BIOS Esta camada de software fornece os serviços descritos na especificação PCI BIOS ROM. Embora o Alpha AXP não tenha serviço de BIOS, existe um código equivalente no kernel do Linux que fornece a mesma funcionalidade.

Veja arch/*/kernel/bios32.c

PCI Fixup Código de limpeza relacionado ao sistema para limpar a frouxidão de memória relacionada ao sistema no final da inicialização do PCI.

Veja arch/*/kernel/bios32.c

6.6.1 Estruturas de Dados PCI Kernel Linux

Quando o kernel do Linux inicializa um sistema PCI, ele cria estruturas de dados que refletem a topologia PCI real do sistema. A Figura 6.5 mostra a relação entre as estruturas de dados usadas para descrever o sistema PCI ilustrado na Figura 6.1.

Cada dispositivo PCI (incluindo pontes PCI-PCI) é descrito por uma estrutura de dados pci_dev. Cada barramento PCI é descrito por uma estrutura de dados pci_bus. O resultado é uma árvore de barramentos PCI, cada um com dispositivos sub-PCI conectados a ele. Como um barramento PCI só pode ser acessado através de uma ponte PCI-PCI (exceto o barramento PCI principal, barramento 0), cada pci_bus inclui um ponteiro para o dispositivo PCI pelo qual está passando (a ponte PCI-PCI). Este dispositivo PCI é um dispositivo filho do barramento pai deste barramento PCI.

Não é mostrado na Figura 6.5 um ponteiro para todos os dispositivos PCI no sistema: pci_devices. As estruturas de dados pci_dev de todos os dispositivos PCI no sistema são enfileiradas nessa fila. O kernel do Linux usa essa fila para localizar rapidamente todos os dispositivos PCI no sistema.

6.6.2 O Driver de Dispositivo PCI

O driver de dispositivo PCI não é um driver de dispositivo real, mas uma função chamada pelo sistema operacional quando o sistema é inicializado. O código de inicialização PCI deve varrer todos os barramentos PCI no sistema para localizar todos os dispositivos PCI no sistema (incluindo dispositivos de ponte PCI-PCI). Ele usa o código PCI BIOS para ver se todos os slots possíveis no barramento PCI que está digitalizando estão ocupados. Se o slot PCI estiver ocupado, ele cria uma estrutura de dados pci_dev descrevendo o dispositivo e o vincula à lista de dispositivos PCI conhecidos (apontados por pci_deivices).

Veja drivers/pci/pci.c Scan_bus()

O código de inicialização PCI inicia a varredura do barramento PCI 0. Ele tenta ler os campos Identificação do fornecedor e Identificação do dispositivo de todos os dispositivos PCI possíveis em todos os slots PCI possíveis. Quando encontra um slot ocupado, cria uma estrutura de dados pci_dev para descrevê-lo. Todas as estruturas de dados pci_dev criadas pelo código de inicialização PCI (incluindo todas as pontes PCI-PCI) são vinculadas a uma tabela vinculada: pci_devices.

Se o dispositivo encontrado for uma ponte PCI-PCI, construa uma estrutura de dados pci_bus e vincule-a à árvore de estruturas de dados pci_bus e pci_dev apontadas por pci_root. O código inicial do PCI pode determinar se um dispositivo PCI é uma ponte PCI-PCI, pois seu código de classe é 0x060400. O kernel do Linux então configura o barramento PCI (downstream) na outra extremidade da ponte PCI-PCI que acabou de encontrar. Se mais pontes PCI-PCI forem encontradas, todas serão configuradas da mesma forma. Esse processo se torna o algoritmo depthwize: o sistema desenrola em profundidade antes de procurar por largura. Observando a Figura 6.1, o Linux primeiro configurará o barramento PCI 1 e seus dispositivos Ethernet e SCSI e, em seguida, configurará o dispositivo de exibição no barramento PCI 0.

Quando o Linux procura um barramento PCI downstream, ele deve configurar os números de barramento secundário e auxiliar das pontes PCI-PCI intervenientes. Estes são descritos em detalhes na Seção 6.6.2 abaixo:

Configurando pontes PCI-PCI - Atribuindo números de barramento PCI

Para leituras e gravações de espaço de endereço de configuração PCI I/O, memória PCI ou PCI que passam por eles, a ponte PCI-PCI deve ser até o seguinte:

Número do barramento primário O número do barramento logo a montante da ponte PCI-PCI

Número do barramento secundário O número do barramento logo abaixo da ponte PCI-PCI

Número de Barramento Subordinado O número de barramento mais alto de todos os barramentos que podem ser alcançados a partir desta ponte.

PCI I/O e PCI Memory Windows A base e o tamanho do espaço de endereço PCI I/O e a janela do espaço de memória PCI para todos os endereços abaixo desta ponte PCI-PCI.

O problema é que quando você deseja configurar uma determinada ponte PCI-PCI você não sabe o número de barramentos conectados a essa ponte. Você não sabe se existem outras pontes PCI-PCI downstream. Mesmo se você fizer isso, você não sabe qual número eles serão atribuídos. A resposta é usar um algoritmo recursivo em profundidade. Quaisquer pontes PCI-PCI recebem números conforme são encontradas em cada barramento. Para cada ponte PCI-PCI encontrada, ele atribui um número ao seu barramento secundário, atribui a ele um número de barramento secundário temporário 0xFF e verifica todas as pontes PCI-PCI downstream e atribui números. Isso parece bastante complicado, mas o exemplo prático a seguir tornará o processo mais claro.

Numeração da ponte PCI-PCI: Etapa 1 Referindo-se à topologia na Figura 6.6, a primeira ponte encontrada pela varredura é a Bridge1. O número do barramento PCI a jusante da ponte 1 é 1, e à ponte 1 é atribuído um número de barramento secundário 1 e um número de barramento auxiliar temporário 0xFF. Isso significa que um endereço de configuração PCI do tipo 1 que especifica o barramento PCI 1 ou superior passará pela ponte 1 para o barramento PCI 1. Se seu número de barramento for 1, eles serão convertidos em ciclos de configuração do tipo 0, caso contrário, eles não serão alterados para outros números de barramento. Isso é exatamente o que o código de inicialização PCI do Linux precisa fazer para acessar e escanear o barramento PCI 1.

Numeração da ponte PCI-PCI: Passo 2 O Linux usa um algoritmo profundo, então o código de inicialização começa a escanear o barramento PCI 1. Isso significa que ele encontrou a ponte PCI-PCI 2. Não há outra ponte PCI-PCI além da ponte 2, então seu número de barramento subsidiário se torna 2, que é o mesmo que sua interface secundária. A Figura 6.7 mostra como o barramento e a ponte PCI-PCI são codificados neste momento.

Numeração da ponte PCI-PCI: Etapa 3 O código de inicialização PCI volta para varrer o barramento PCI 1 e encontra outra ponte PCI-PCI 3. Sua interface de barramento primário é atribuído um valor de 1 e sua interface de barramento secundário é 3, e seu número de barramento secundário é 0xFF. A Figura 6.8 mostra como o sistema está agora configurado. Os ciclos de configuração PCI do tipo 1 com números de barramento 1, 2 ou 3 agora são roteados corretamente para o barramento PCI apropriado.

 

6.6.3 Funções PCI BIOS

As funções PCI BIOS são uma série de rotinas padrão que são comuns em todas as plataformas. Por exemplo, eles são os mesmos para os sistemas Intel e Alpha AXP. Eles permitem que a CPU controle o acesso a todos os espaços de endereço PCI. Apenas o kernel do Linux e os drivers de dispositivo precisam usá-los.

Veja arch/*/kernel/bios32.c

6.6.4 Correção PCI

O código de agrupamento PCI nos sistemas Alpha AXP funciona mais do que a Intel (que basicamente não faz nada). Para sistemas Intel, o BIOS do sistema executado na inicialização possui sistemas PCI totalmente configurados. O Linux não precisa fazer muito mais do que mapear a configuração PCI. Para sistemas não Intel, mais configurações precisam ser feitas:

Veja arch/kernel/bios32.c

Alocar PCI I/O e espaço de memória PCI para cada dispositivo

As janelas de endereço de memória PCI e E/S PCI devem ser configuradas para cada ponte PCI-PCI no sistema

Para o dispositivo gerar um valor de linha de interrupção, eles controlam o processamento de interrupção do dispositivo

A seguir, descreve como esses códigos funcionam.

Descobrindo quanto PCI I/O e espaço de memória PCI um dispositivo precisa

(Descubra quanto PCI I/O e espaço de memória PCI um dispositivo precisa)

Consulte cada dispositivo PCI encontrado para descobrir a quantidade de E/S PCI e espaço de endereço de memória necessários. Para fazer isso, escreva cada Registro de Endereço Base como 1 e leia-o. O dispositivo retornará 1 nos bits de endereço com os quais não se importa, especificando efetivamente o espaço de endereço necessário.

Com dois registradores de endereços básicos básicos (Base Address Register), o primeiro indica o registrador do dispositivo e em qual espaço de endereço devem estar os espaços de E/S PCI e memória PCI. Isso é representado pelo bit 0 do registrador. A Figura 6.10 mostra duas formas do registrador de endereço base para memória PCI e E/S PCI.

Para descobrir quanto espaço de endereçamento é necessário para cada registrador de endereço base, é necessário escrever e ler de todos os registradores. O dispositivo define os bits de endereço com os quais não se importa como 0, especificando efetivamente o espaço de endereço de que precisa. Esse design implica que todos os espaços de endereço usados ​​são índices de 2 e são alinhados inerentemente.

Por exemplo, quando você inicializa o dispositivo PCI Fast Ethernet DECChip 21142, ele informa que ele precisa de um endereço de 0x100 bytes no espaço de memória PCI I/O ou PCI. O código de inicialização aloca espaço para ele. Após alocar espaço, os registradores de controle e status do 21142 podem ser vistos nestes endereços.

Alocação de PCI I/O e memória PCI para pontes e dispositivos PCI-PCI

(Aloca PCI I/O e memória PCI para pontes e dispositivos PCI-PCI)

Como toda memória, o espaço PCI I/O e memória PCI é limitado, alguns dos quais são bastante apertados. O código de agrupamento PCI para sistemas não Intel (e o código BIOS para sistemas Intel) deve alocar com eficiência a cada dispositivo a quantidade de memória necessária. As alocações de memória PCI I/O e PCI alocadas para um dispositivo devem ser alinhadas naturalmente. Por exemplo, se um dispositivo solicitar o endereço de E/S PCI 0xB0, o endereço atribuído deverá ser um múltiplo de 0xB0. Além disso, as bases dos endereços PCI I/O e de memória PCI atribuídos a qualquer bridge devem ser alinhadas aos limites de 4K e 1M, respectivamente. O espaço de endereço fornecido pelo dispositivo downstream deve estar no meio do intervalo de memória de todas as suas pontes PCI-PCI upstream. Portanto, alocar espaço de endereço com eficiência é um problema mais difícil.

O algoritmo usado pelo Linux depende de cada dispositivo descrito pela árvore de barramento/dispositivo estabelecida pelo driver de dispositivo PCI, que aloca espaço de endereço em ordem crescente de memória de E/S PCI. Novamente, um algoritmo recursivo é usado para percorrer as estruturas de dados pci_bus e pci_dev estabelecidas pelo código de inicialização PCI. O código de limpeza do BIOS começa na raiz do barramento PCI (apontado por pci_root):

Alinhar as bases de E/S e memória PCI globais atuais em limites de 4K e 1M, respectivamente

Para cada dispositivo no barramento atual (na ordem da memória PCI I/O necessária)

- alocar sua E/S PCI e/ou memória PCI

- Movida a E/S PCI global e a base de memória pela quantidade apropriada

- Permitir que o dispositivo use a E/S PCI e a memória PCI fornecidas

Aloque espaço separadamente para todos os barramentos downstream do barramento atual, observe que isso altera a E/S PCI global e a base de memória.

Alinhe as bases de memória e E/S PCI globais atuais nos limites de 4K e 1M, respectivamente, e indique a base e o tamanho das janelas de memória PCI e E/S PCI exigidas pela ponte PCI-PCI atual

Para a ponte PCI-PCI conectada ao barramento atual, defina seus endereços e limites de memória PCI-PCI I/O e PCI.

Habilita a capacidade de fazer a ponte de PCI I/O e acessos de memória PCI na ponte PCI-PCI. Isso significa que qualquer endereço PCI I/O e memória PCI visto no barramento PCI primário da ponte será conectado ao seu barramento secundário se estiver em sua janela de endereço PCI I/O e memória PCI.

Tome o sistema PCI da Figura 6.1 como um exemplo de código de agrupamento PCI:

Alinhar a base PCI (inicial) PCI I/O é 0x4000, memória PCI é 0x100000. Isso permite que a ponte PCI-ISA converta todos os endereços abaixo para endereços ISA.

O dispositivo de vídeo solicita a memória PCI de 0x200000, pois ela deve ser alinhada de acordo com o tamanho necessário, então iniciamos a alocação da memória PCI 0x200000, o endereço base da memória PCI é movido para 0x400000 e o endereço de E/S PCI ainda é 0x4000.

As pontes PCI-PCI Atravessamos agora a ponte PCI-PCI, onde a memória é alocada. Observe que não precisamos de seus endereços base, pois eles já estão devidamente alinhados.

O dispositivo Ethernet solicita 0xB0 bytes no espaço de memória PCI I/O e PCI. Ele é alocado no endereço PCI I/O 0x4000, memória PCI 0x400000. A base da memória PCI foi movida para 0x4000B0 e a base de E/S PCI tornou-se 0x40B0.

O dispositivo SCSI solicita memória PCI em 0x1000, portanto, aloca em 0x401000 após o alinhamento. O endereço base da PCI I/O ainda é 0x40B0 e a base da memória PCI é movida para 0x402000.

Janelas de Memória e E/S PCI da Ponte PCI-PCI Agora voltamos para a ponte e configuramos sua janela de E/S PCI entre 0x4000 e 0x40B0, e sua janela de memória PCI entre 0x400000 e 0x402000. Isso significa que a ponte PCI-PCI ignorará o acesso à memória PCI ao dispositivo de exibição, se o acesso ao dispositivo Ethernet ou SCSI puder passar.

7. Interrupções e Manuseio de Interrupções

Embora o kernel tenha mecanismos e interfaces gerais para lidar com interrupções, a maioria dos detalhes do tratamento de interrupções depende da arquitetura.

O Linux usa muitos hardwares diferentes para muitas tarefas diferentes. Monitores de unidade de dispositivos de exibição, discos de unidade de dispositivos IDE e assim por diante. Você pode conduzir esses dispositivos de forma síncrona, ou seja, você pode emitir uma solicitação para realizar alguma operação (como gravar um bloco de memória em disco) e aguardar a conclusão da operação. Essa abordagem, enquanto funciona, é muito ineficiente, e o sistema operacional passa muito tempo "ocupado sem fazer nada" enquanto aguarda a conclusão de cada operação. Uma abordagem boa e mais eficiente é fazer a solicitação e fazer algo mais útil e, em seguida, ser interrompido pelo dispositivo quando o dispositivo concluir a solicitação. Sob este esquema, pode haver solicitações de vários dispositivos no sistema ao mesmo tempo.

Deve haver algum suporte de hardware para o dispositivo interromper o trabalho atual da CPU. A maioria, se não todos os processadores de uso geral, como o Alpha AXP, usam uma abordagem semelhante. Alguns dos pinos físicos da CPU têm circuitos que simplesmente alteram a voltagem (de +5V para -5V, por exemplo) para fazer com que a CPU pare o que está fazendo e comece a executar um código especial que lida com interrupções: código de manipulação de interrupção. Um desses pinos pode estar conectado a um meio interno que recebe uma interrupção a cada milésimo de segundo, e o outro pode estar conectado a outros dispositivos no sistema, como um controlador SCSI.

Os sistemas normalmente usam um controlador de interrupção para agrupar as interrupções do dispositivo e, em seguida, encaminhar os sinais para um único pino de interrupção na CPU. Isso economiza o gerenciamento de interrupção da CPU e traz flexibilidade ao sistema de design. O controlador de interrupção possui registradores de máscara e de status para controlar essas interrupções. As interrupções podem ser habilitadas e desabilitadas definindo bits no registrador de máscara, e o registrador de status retorna as interrupções atuais no sistema.

As interrupções em alguns sistemas podem ser conectadas, por exemplo, o relógio interno do relógio de tempo real pode estar permanentemente conectado ao pino 3 do controlador de interrupção. No entanto, quais outros pinos estão conectados pode ser determinado por qual placa de controle está inserida em um determinado slot ISA ou PCI. Por exemplo, o 4º pino do controlador de interrupção pode estar conectado ao slot PCI 0, pode haver uma placa Ethernet um dia e, em seguida, pode ser uma placa controladora SCSI. Cada sistema tem seu próprio mecanismo de retransmissão de interrupção e o sistema operacional deve ser flexível o suficiente para lidar com isso.

A maioria dos microprocessadores modernos de uso geral lidam com interrupções da mesma maneira. Quando ocorre uma interrupção de hardware, a CPU interrompe a instrução que está sendo executada e salta para um local na memória onde contém o código de tratamento de interrupção ou uma instrução que salta para o código de tratamento de interrupção. Esse código geralmente funciona em um modo especial da CPU: modo de interrupção, no qual outras interrupções normalmente não podem ser geradas. Existem exceções aqui: algumas CPUs dividem as interrupções em níveis e podem ocorrer interrupções de nível superior. Isso significa que o primeiro nível de manipuladores de interrupção deve ser escrito com muito cuidado. Os manipuladores de interrupção geralmente têm sua própria pilha, que é usada para armazenar o estado de execução da CPU (todos os registradores de uso geral e contexto da CPU) e tratar as interrupções. Algumas CPUs têm um conjunto de registradores que existem apenas no modo de interrupção, que o código de tratamento de interrupção pode usar para armazenar a maioria das informações contextuais que precisa salvar.

Quando a interrupção é processada, o estado da CPU é restaurado e a interrupção termina. A CPU continuará fazendo o que estava fazendo antes da interrupção. Importante Os manipuladores de interrupção devem ser tão eficientes quanto possível, geralmente o sistema operacional não pode bloquear interrupções com muita frequência ou por longos períodos de tempo.

7.1 Controladores de interrupção programáveis

Os projetistas de sistemas são livres para usar qualquer arquitetura de interrupção que desejarem, mas todos os PCs IBM usam o Controlador de interrupção programável Intel 82C59A-2 CMOS ou seus derivados. Este controlador tem sido usado desde o início do PC. É programável por meio de registradores em locais bem conhecidos no espaço de endereço ISA. Mesmo chipsets lógicos muito modernos mantêm registradores equivalentes nos mesmos locais na memória ISA. Sistemas não Intel, como o Alpha AXP PC, estão isentos dessas arquiteturas e geralmente usam um controlador de interrupção diferente.

A Figura 7.1 mostra dois controladores de 8 bits em série: cada um tem uma máscara e um registrador de status de interrupção, PIC1 e PIC2. Os registradores de máscara estão localizados nos endereços 0x21 e 0xA1, enquanto os registradores de status estão localizados em 0x20 e 0xA0. Escrever um 1 em um bit especial no registrador de máscara habilita uma interrupção e escrever um 0 a desabilita. Portanto, escrever um 1 no bit 3 habilita a interrupção 3 e escrever um 0 a desabilita. Infelizmente (e irritantemente), o registro da máscara de interrupção só é gravável, você não pode ler o valor que escreveu. Isso significa que o Linux deve manter uma cópia local do registro de máscara que ele define. Ele modifica essas máscaras salvas nas rotinas de habilitação e desabilitação de interrupção, cada vez gravando a máscara inteira em um registrador.

Quando um sinal de interrupção é gerado, o manipulador de interrupção lê os dois registradores de status de interrupção (ISR). Ele considera o ISR de 0x20 como o 8º bit do registrador de interrupção de 16 bits e o ISR em 0xA0 como os 8 bits superiores. Portanto, uma interrupção que ocorre no bit 1 do ISR em 0xA0 é considerada interrupção 9. O bit 2 do PCI1 não está disponível porque é usado como interrupção para o PIC2 serial, qualquer interrupção do PIC2 definirá o bit 2 do PIC1.

7.2 Inicializando as Estruturas de Dados de Manipulação de Interrupções

As principais estruturas de dados de tratamento de interrupção são criadas quando o driver de dispositivo requer o controle das interrupções do sistema. Para fazer isso, os drivers de dispositivo usam uma série de serviços do kernel Linux para solicitar uma interrupção, habilitá-la e desabilitá-la. Esses drivers de dispositivo chamam essas rotinas para registrar os endereços de suas rotinas de tratamento de interrupção.

参见arch/*/kernel/irq.c request_irq() enable_irq() e disable_irq()

A arquitetura do PC corrige algumas interrupções por conveniência, de modo que o driver simplesmente solicita suas interrupções durante a inicialização. Um driver de dispositivo de disquete é exatamente isso: ele sempre solicita a interrupção 6. Mas também é possível que um driver de dispositivo não saiba quais interrupções o dispositivo usará. Isso não é um problema para drivers de dispositivo PCI, pois eles sempre sabem seu número de interrupção. Infelizmente para dispositivos ISA não há uma maneira fácil de encontrar seus números de interrupção, o Linux permite que os drivers de dispositivo testem suas interrupções para resolver esse problema.

Primeiro, o driver de dispositivo faz com que o dispositivo gere interrupções e, em seguida, todas as interrupções não atribuídas no sistema são habilitadas. Isso significa que as interrupções que aguardam processamento pelo dispositivo agora são entregues através do controlador de interrupção programável. O Linux lê o registrador de status de interrupção e retorna seu conteúdo ao driver do dispositivo. Um resultado diferente de zero indica que uma ou mais interrupções ocorreram durante o teste. O driver agora desativa a sondagem e desativa as interrupções para todas as atribuições de bits. Se o driver de dispositivo ISA encontrar seu número de IRQ com êxito, ele poderá solicitar o controle dele normalmente.

veja arch/*/kernel/irq.c irq_probe_*()

Os sistemas PCI são mais dinâmicos que os sistemas ISA. As interrupções do dispositivo ISA geralmente são definidas com jumpers no dispositivo de hardware e são corrigidas para o driver do dispositivo. Por outro lado, as interrupções para dispositivos PCI são alocadas na inicialização do sistema pelo BIOS PCI ou pelo subsistema PCI durante a inicialização do PCI. Cada dispositivo PCI pode usar um dos quatro pinos de interrupção: A, B, C ou D. Neste momento, é determinado quando o dispositivo é fabricado, e a maioria dos dispositivos usa o pino de interrupção A por padrão. As linhas de interrupção PCI A, B, C e D de cada slot PCI vão para o controlador de interrupção. Assim, o pino A do slot 4 pode ir para o pino 6 do controlador de interrupção, o pino B do slot 4 pode ir para o pino 7 do controlador de interrupção e assim por diante.

Como as interrupções PCI são encaminhadas (roteadas) é totalmente dependente do sistema e deve haver algum código de configuração que entenda essa topologia de roteamento de interrupção PCI. Nos PCs Intel, isso é feito pelo código do BIOS do sistema no momento da inicialização. Mas para sistemas sem BIOS (como sistemas Alpha AXP), o Linux faz essa configuração. O código de configuração PCI grava o número do pino do controlador de interrupção no cabeçalho de configuração PCI de cada dispositivo. Ele usa a topologia de interrupção PCI que conhece e o slot PCI do dispositivo e o pino de interrupção PCI que está usando para determinar o número do pino de interrupção (ou IRQ). Os pinos de interrupção usados ​​pelo dispositivo são determinados e colocados em um campo do cabeçalho de configuração PCI. Ele grava essas informações no campo de linha de interrupção (que é reservado para essa finalidade). Quando o driver de dispositivo é executado, ele lê essas informações e as utiliza para solicitar o controle da interrupção do kernel do Linux.

Veja arch/alpha/kernel/bios32.c

Muitos recursos de interrupção PCI podem ser usados ​​no sistema. Por exemplo, ao usar uma ponte PCI-PCI. O número de fontes de interrupção pode exceder o número de pinos do controlador de interrupção programável do sistema. Nesse caso, os dispositivos PCI podem compartilhar interrupções: um pino no controlador de interrupção recebe interrupções de mais de um dispositivo PCI. O Linux suporta o compartilhamento de interrupção permitindo que a primeira fonte solicitando uma interrupção declare (declare) se ela pode ser compartilhada. Resultados de interrupção compartilhada são estruturas de dados onde uma entrada na tabela de vetor irq_action pode apontar para várias irqactions. Quando ocorre uma interrupção compartilhada, o Linux chama todos os manipuladores de interrupção para esta fonte. Todos os drivers de dispositivo (que devem ser drivers de dispositivo PCI) que podem compartilhar interrupções devem estar preparados para serem chamados quando nenhuma interrupção for atendida.

7.3 Manipulação de Interrupção

Uma das principais tarefas do subsistema de tratamento de interrupção do Linux é rotear as interrupções para o segmento de código de tratamento de interrupção correto. Tal código deve compreender a topologia de interrupção do sistema. Por exemplo, se o controlador da unidade de disquete interrompe no pino 6 do controlador de interrupção, ele deve ser capaz de reconhecer que a interrupção é da unidade de disquete e encaminhá-la para o código do manipulador de interrupção do driver de dispositivo da unidade de disquete. O Linux usa uma série de ponteiros para estruturas de dados que contêm os endereços das rotinas que tratam das interrupções do sistema. Essas rotinas pertencem aos drivers de dispositivo dos dispositivos do sistema, e cada driver de dispositivo deve ser responsável por solicitar as interrupções que desejar quando o driver for inicializado. A Figura 7.2 mostra que irq_action é uma tabela vetorial de ponteiros para a estrutura de dados irqaction. Cada estrutura de dados irqaction contém informações sobre o manipulador de interrupção, incluindo o endereço do manipulador de interrupção. O número de interrupções e como elas são tratadas são diferentes para sistemas diferentes.Geralmente, entre sistemas diferentes, o código de manipulação de interrupções do Linux depende da arquitetura. Isso significa que o tamanho da tabela de vetores irq_action varia dependendo do número de fontes de interrupção.

Quando ocorre uma interrupção, o Linux deve primeiro determinar sua origem lendo o registro de status do controlador de interrupção programável do sistema. Em seguida, converta essa origem em um deslocamento na tabela de vetores irq_action. Por exemplo, uma interrupção no pino 6 do controlador de interrupção do controlador de disquete será transferida para o 7º ponteiro na tabela de vetores do manipulador de interrupção. Se ocorrer uma interrupção sem um manipulador de interrupção correspondente, o kernel do Linux registrará um erro, caso contrário, ele chamará o manipulador de interrupção em todas as estruturas de dados irqaction dessa fonte de interrupção.

Quando o kernel do Linux chama a rotina de tratamento de interrupção do driver de dispositivo, ele deve determinar efetivamente por que foi interrompido e responder. Para descobrir a causa da interrupção, o driver de dispositivo lê o registro de status do dispositivo de interrupção. O dispositivo pode responder: Ocorreu um erro ou uma operação solicitada foi concluída. Por exemplo, o controlador da unidade de disquete pode relatar que posicionou a cabeça de leitura da unidade de disquete no setor correto no disquete. Depois que a causa da interrupção é determinada, o driver de dispositivo pode precisar fazer mais trabalho. Nesse caso, o kernel do Linux possui mecanismos para permitir que essa operação seja adiada por um tempo. Isso evita que a CPU passe muito tempo no modo de interrupção.

8. Drivers de dispositivo

Um dos propósitos de um sistema operacional é ocultar do usuário as especificidades dos dispositivos de hardware do sistema. Por exemplo, um sistema de arquivos virtual apresenta uma visão unificada do sistema de arquivos montado, independentemente do dispositivo físico subjacente. Esta seção descreve como o kernel do Linux gerencia os dispositivos físicos no sistema.

A CPU não é o único dispositivo inteligente no sistema, cada dispositivo físico é controlado por seu próprio hardware. Teclado, mouse e porta serial são controlados pelo chip SuperIO, o disco IDE é controlado pelo controlador IDE, o disco SCSI é controlado pelo controlador SCSI e assim por diante. Cada controlador de hardware tem seu próprio controlador de controle e estado (CSR), que varia de dispositivo para dispositivo. O CSR de um controlador SCSI Adaptec 2940 é completamente diferente daquele de um controlador SCSI NCR 810. O CSR é usado para iniciar e parar o dispositivo, inicializar o dispositivo e diagnosticar problemas com ele. O código para gerenciar esses controladores de hardware não é colocado em todos os aplicativos, mas no kernel do Linux. Esses pés de software que manipulam ou gerenciam controladores de hardware são chamados de drivers de dispositivo. Os drivers de dispositivo no kernel do Linux são essencialmente uma biblioteca compartilhada de rotinas de controle de hardware de baixo nível privilegiadas e residentes na memória. São as idiossincrasias dos drivers de dispositivo Linux que lidam com os dispositivos que eles gerenciam.

Uma característica fundamental do UN*X é que ele abstrai o manuseio de dispositivos. Todos os dispositivos de hardware são tratados como arquivos normais: eles podem ser abertos, fechados, lidos e gravados usando as mesmas chamadas de sistema padrão dos arquivos. Cada dispositivo no sistema é representado por um arquivo especial de dispositivo. Por exemplo, o primeiro disco rígido IDE no sistema é representado por /dev/had. Para dispositivos de bloco (disco) e de caractere, esses arquivos especiais de dispositivo são criados com o comando mknod e usam números de dispositivo principais e secundários para descrever o dispositivo. Os dispositivos de rede também são representados por arquivos especiais de dispositivo, mas são criados pelo Linux quando encontra e inicializa o controlador de rede no sistema. Todos os dispositivos controlados pelo mesmo driver de dispositivo são numerados por um dispositivo principal comum. Números de dispositivos menores são usados ​​para distinguir entre diferentes dispositivos e seus controladores. Por exemplo, as diferentes partições do disco IDE primário são numeradas por um dispositivo secundário diferente. Portanto, /dev/hda2, a segunda partição do disco IDE principal tem um número maior de 3 e um número menor de 2. O Linux usa a tabela de números de dispositivos principais e algumas tabelas de sistema (como a tabela de dispositivos de caracteres chrdevs) para mapear arquivos especiais de dispositivos passados ​​em chamadas de sistema (como montar um sistema de arquivos em um dispositivo de bloco) para o driver de dispositivo desse dispositivo.

Veja fs/dispositivos.c

O Linux suporta três tipos de dispositivos de hardware: caractere, bloco e rede. Dispositivos de caracteres são lidos e escritos diretamente, sem buffers, como as portas seriais do sistema /dev/cua0 e /dev/cua1. Dispositivos de bloco só podem ser lidos e escritos em múltiplos de um bloco (geralmente 512 bytes ou 1024 bytes). Os dispositivos de bloco são acessados ​​por meio do cache de buffer e podem ser acessados ​​aleatoriamente, ou seja, qualquer bloco pode ser lido ou escrito independentemente de onde esteja no dispositivo. Dispositivos de bloco podem ser acessados ​​por meio de seus arquivos especiais de dispositivo, mas são mais comumente acessados ​​por meio do sistema de arquivos. Apenas um dispositivo de bloco pode suportar um sistema de arquivos montado. Os dispositivos de rede são acessados ​​por meio da interface de soquete BSD e o subsistema de rede é descrito na Seção 10.

O Linux tem muitos drivers de dispositivo diferentes (o que também é um dos pontos fortes do Linux), mas todos eles têm algumas propriedades gerais:

Código do kernel Drivers de dispositivo, como outros códigos no kernel, fazem parte do kenel e podem danificar seriamente o sistema se ocorrer um erro. Um driver escrito incorretamente pode até destruir o sistema, possivelmente corrompendo o sistema de arquivos e perdendo dados.

Interfaces do Kenel Um driver de dispositivo deve fornecer uma interface padrão para o kernel do Linux ou o subsistema no qual ele reside. Por exemplo, o driver de terminal fornece uma interface de E/S de arquivo para o kernel do Linux e o driver de dispositivo SCSI fornece a interface de dispositivo SCSI para o subsistema SCSI, que por sua vez fornece ao kernel interfaces de E/S de arquivo e cache de buffer.

Mecanismos e serviços do kernel Os drivers de dispositivo usam serviços principais padrão, como alocação de memória, encaminhamento de interrupção e filas de espera para fazer seu trabalho

Linux carregável A maioria dos drivers de dispositivo pode ser carregado como módulos principais quando necessário e descarregado quando não for mais necessário. Isso torna o núcleo muito adaptável e eficiente em relação aos recursos do sistema.

Drivers de dispositivo Linux configuráveis ​​podem ser incorporados ao núcleo. Quais dispositivos são integrados ao núcleo podem ser configurados no tempo de compilação do núcleo.

Dinâmico Na inicialização do sistema, ele procura os dispositivos de hardware que gerencia toda vez que o programa de inicialização do dispositivo é inicializado. Não importa se o dispositivo controlado por um driver de dispositivo não existe. Neste ponto, o driver do dispositivo é apenas redundante e ocupa muito pouca memória do sistema sem causar danos.

8.1 Poling e Interrupções

Toda vez que um comando é dado ao dispositivo, como "mover a cabeça de leitura para o setor 42 do disquete", o driver do dispositivo pode escolher como ele determina se o comando foi finalizado. Drivers de dispositivo podem pesquisar o dispositivo ou usar interrupções.

Sondar um dispositivo geralmente significa ler constantemente seu registro de status até que o estado do dispositivo mude para indicar que ele concluiu a solicitação. Como o driver de dispositivo faz parte do núcleo, seria desastroso se o driver estivesse constantemente pesquisando e o núcleo não pudesse executar mais nada até que o dispositivo concluísse a solicitação. Portanto, o driver de dispositivo pesquisado usa um temporizador do sistema para permitir que o sistema chame uma rotina no driver de dispositivo posteriormente. Essa rotina de timer verifica o status do comando, que é como o driver de disquete do Linux funciona. Polling com um timer é a melhor abordagem, e uma abordagem mais eficiente é usar interrupções.

Um driver de dispositivo de interrupção emite uma interrupção de hardware quando o dispositivo de hardware que ele controla precisa ser atendido. Por exemplo, um driver de dispositivo Ethernet será interrompido quando o dispositivo receber um pacote Ethernet na rede. O kernel do Linux precisa ser capaz de encaminhar interrupções de dispositivos de hardware para os drivers de dispositivo corretos. Isso é feito pelo driver de dispositivo registrando com o kernel as interrupções que ele usa. Ele registra o endereço da rotina de tratamento de interrupção e o número de interrupção que deseja ter. Você pode ver quais interrupções são usadas pelo driver de dispositivo e quantas vezes cada tipo de interrupção é usada em /proc/interrupts:

0: 727432 timer
1: 20534 keyboard
2: 0 cascade
3: 79691 + serial
4: 28258 + serial
5: 1 sound blaster
11: 20868 + aic7xxx
13: 1 math error
14: 247 + ide0
15: 170 + ide1

As solicitações de recursos de interrupção ocorrem no momento da inicialização do driver. Algumas interrupções no sistema foram corrigidas, um legado da arquitetura IBM PC. Por exemplo, o controlador de disquete sempre usa a interrupção 6. Outras interrupções, como interrupções de dispositivo PCI, são alocadas dinamicamente no momento da inicialização. Neste momento, o driver de dispositivo deve primeiro descobrir o número de interrupção do dispositivo que ele controla e, em seguida, pode solicitar essa interrupção. Para interrupções PCI, o Linux oferece suporte a retornos de chamada PCI BIOS padrão para determinar informações sobre dispositivos no sistema, incluindo seus IRQs.

Como uma interrupção em si é encaminhada para a CPU depende da arquitetura. Mas na maioria das arquiteturas, as interrupções são entregues em um modo especial que impede que outras interrupções ocorram no sistema. Um driver de dispositivo deve trabalhar o mínimo possível em sua rotina de tratamento de interrupções para que o kernel do Linux possa encerrar a interrupção e retornar de onde parou. Drivers de dispositivo que fazem muito trabalho depois de receber uma interrupção podem usar o manipulador da metade inferior do núcleo ou uma fila de tarefas para enfileirar a rotina para invocação posterior.

8.2 Acesso Direto à Memória (DMA)

Usar um driver de dispositivo controlado por interrupção para transferir dados para ou através de um dispositivo funciona muito bem quando a quantidade de dados é pequena. Por exemplo, um modem de 9600 bauds pode transmitir aproximadamente um caractere por milissegundo (1/1000 de segundo). Se a latência da interrupção, ou seja, o tempo que o dispositivo de hardware leva para emitir a interrupção até o início da chamada do manipulador de interrupção no driver do dispositivo, for relativamente pequena (como 2 milissegundos), então a imagem da transferência de dados no o sistema como um todo é muito pequeno. Os dados do modem com taxa de transmissão de 9600 bauds ocuparão apenas 0,002% do tempo de processamento da CPU. Mas para dispositivos de alta velocidade, como controladores de disco rígido ou dispositivos Ethernet, a taxa de transferência de dados é bastante alta. Um dispositivo SCSI pode transmitir até 40 Mbytes de informações por segundo.

O acesso direto à memória, ou DMA, foi inventado para resolver esse problema. Um controlador DMA permite que o dispositivo crie dados na memória do sistema sem intervenção do processador. O controlador ISA DMA do PC consiste em 8 canais DMA, 7 dos quais estão disponíveis para drivers de dispositivo. Cada canal DMA está associado a um registrador de endereço de 16 bits e a um registrador de contagem de 16 bits. Para iniciar uma transferência de dados, o driver do dispositivo precisa estabelecer o endereço e os registros de contagem do canal DMA, além da direção da transferência de dados, leitura ou gravação. Quando a transferência termina, o dispositivo interrompe o PC. Desta forma, enquanto a transferência está ocorrendo, a CPU pode fazer outras coisas.

Os drivers de dispositivo devem ter cuidado ao usar o DMA. Primeiro, todos os controladores DMA não têm conhecimento da memória virtual, só podem acessar a memória física do sistema. Portanto, a memória que precisa ser transferida por DMA deve ser um bloco contíguo na memória física. Isso significa que você não pode ter acesso DMA ao espaço de endereço virtual de um processo. Mas você também pode bloquear fisicamente o processo na memória enquanto executa operações de DMA. Segundo: O controlador DMA não pode acessar toda a memória física. O registrador de endereço do canal DMA representa os primeiros 16 bits do endereço DMA e os próximos 8 bits vêm do registrador de página. Isso significa que as solicitações de DMA são limitadas aos 16M inferiores de memória.

Canais DMA são recursos escassos, apenas 7, e não podem ser compartilhados entre drivers de dispositivo. Assim como as interrupções, um driver de dispositivo deve ter a capacidade de descobrir qual canal DMA pode usar. Assim como as interrupções, alguns dispositivos possuem canais DMA fixos. Por exemplo, as unidades de disquete sempre usam o canal DMA 2. Às vezes, o canal DMA de um dispositivo pode ser definido com jumpers: alguns dispositivos Ethernet usam essa técnica. Alguns dispositivos mais flexíveis podem dizer (através de seu CSR) qual canal DMA usar, e nesse ponto o driver do dispositivo pode simplesmente descobrir um canal DMA disponível.

O Linux usa uma tabela de vetores de estruturas de dados dma_chan (uma para cada canal DMA) para rastrear o uso do canal DMA. A estrutura de dados Dma_chan tem apenas dois jades: um ponteiro de caractere que descreve o proprietário do canal DMA e um sinalizador que mostra se o canal DMA está alocado. Quando você cat /proc/dma, a tabela de vetores dma_chan é exibida.

8.3 Memória

Os drivers de dispositivo devem usar a memória com cuidado. Como eles fazem parte do kernel do Linux, eles não podem usar memória virtual. Sempre que um driver de dispositivo é executado, ele pode receber uma interrupção ou agendar um manipulador de meio botão ou uma fila de tarefas e o processo atual pode ser alterado. Um driver de dispositivo não pode depender de um processo especial em execução. Como outras partes do kernel, um driver de dispositivo usa estruturas de dados para rastrear os dispositivos que ele controla. Essas estruturas de dados podem ser alocadas estaticamente na seção de código do driver de dispositivo, mas isso tornaria o núcleo desnecessariamente grande e dispendioso. A maioria dos drivers de dispositivo aloca memória não paginada do kernel para seus dados.

O kernel do Linux fornece as rotinas de alocação e desalocação de memória do kernel, que são usadas pelos drivers de dispositivo. A memória principal é alocada em blocos de potência de 2. Por exemplo, 128 ou 512 bytes, mesmo que o driver do dispositivo não peça tanto. O número de bytes solicitados pelo driver de dispositivo é arredondado para o tamanho do próximo bloco. Isso facilita a recuperação de memória pelo núcleo, pois blocos livres menores podem ser combinados em blocos maiores.

O Linux também precisa fazer mais trabalho adicional ao solicitar memória do kernel. Se a quantidade total de memória livre for muito baixa, as páginas físicas precisam ser descartadas ou gravadas para troca. Normalmente, o Linux suspenderá o solicitante e colocará o processo em uma fila de espera até que haja memória física suficiente disponível. Nem todos os drivers de dispositivo (ou, na verdade, o código do kernel Linux) querem que isso aconteça, as rotinas de alocação de memória do kernel podem solicitar falha se a memória não puder ser alocada imediatamente. Se o driver de dispositivo desejar alocar memória para acesso DMA, ele também precisará indicar que essa memória é compatível com DMA. Porque é necessário deixar o kernel do Linux entender qual memória no sistema é contígua para DMA, em vez de deixar o driver do dispositivo decidir.

8.4 Interfaceando Drivers de Dispositivos com o Kernel

O kernel do Linux deve ser capaz de trabalhar com eles de maneira padrão. Cada tipo de driver de dispositivo: caractere, bloco e rede, fornece uma interface comum para o núcleo usar quando precisar solicitar seus serviços. Essas interfaces comuns significam que o núcleo pode observar dispositivos muito diferentes e seus drivers de dispositivo exatamente da mesma maneira. Por exemplo, os discos SCSI e IDE se comportam de maneira muito diferente, mas o kernel do Linux usa a mesma interface para eles.

O Linux é muito dinâmico e toda vez que o kernel do Linux é iniciado, ele pode encontrar dispositivos físicos diferentes e exigir drivers de dispositivo diferentes. O Linux permite incluir drivers de dispositivo por meio de scripts de configuração no momento da compilação do kernel. Quando esses drivers de dispositivo são inicializados na inicialização, eles podem não encontrar nenhum hardware que possam controlar. Outros drivers podem ser carregados como módulos principais quando necessário. Para lidar com essa natureza dinâmica dos drivers de dispositivo, os drivers de dispositivo são registrados no kernel quando são inicializados. O Linux mantém uma lista de drivers de dispositivo registrados como parte da interface com eles. Essas listas incluem ponteiros para rotinas e informações sobre as interfaces que suportam esse tipo de dispositivo.

8.4.1 Dispositivos de Personagem

Um dispositivo de caractere, o dispositivo mais simples no Linux, é acessado como um arquivo. Os aplicativos usam chamadas de sistema padrão para abrir, ler, gravar e fechar, exatamente como se o dispositivo fosse um arquivo comum. Até o modem usado pelo daemon PPP conectando um sistema Linux à Internet é assim. Quando um dispositivo de caractere é inicializado, seu driver de dispositivo é registrado no kernel do Linux, adicionando uma entrada de estrutura de dados device_struct à tabela de vetores chrdevs. O identificador de dispositivo principal do dispositivo (por exemplo, 4 para dispositivos tty) é usado como um índice nesta tabela de vetores. O MID de um dispositivo é fixo. Cada entrada na tabela de vetores Chrdevs, uma estrutura de dados device_struct, consiste em dois elementos: um ponteiro para o nome do driver de dispositivo registrado e um ponteiro para um conjunto de operações de arquivo. As próprias operações de arquivo residem nos drivers de dispositivo de caractere do dispositivo, cada um dos quais lida com operações de arquivo específicas, como abrir, ler, gravar e fechar. O conteúdo dos dispositivos de caracteres em /proc/devices vem da tabela de vetores chrdevs

Veja include/linux/major.h

Quando um arquivo especial de caractere representando um dispositivo de caractere (por exemplo, /dev/cua0) é aberto, o kernel deve fazer algo para se livrar das rotinas de manipulação de arquivo que usam o driver de dispositivo de caractere correto. Como arquivos ou diretórios comuns, cada arquivo específico do dispositivo é representado por um inode VFS. O inode VFS para este arquivo especial de caractere (na verdade, todos os arquivos especiais de dispositivo) inclui os identificadores principais e secundários do dispositivo. O inode VFS é criado pelo sistema de arquivos subjacente (por exemplo, EXT2) com base no sistema de arquivos real ao procurar por esse arquivo específico do dispositivo.

Veja fs/ext2/inode.c ext2_read_inode()

Cada inode VFS está associado a um conjunto de operações de arquivo que variam dependendo do objeto do sistema de arquivos representado pelo inode. Sempre que um inode VFS representando um arquivo especial de caractere é criado, sua operação de arquivo é definida como a operação padrão para dispositivos de caractere. Existe apenas uma operação de arquivo: a operação de abertura. Quando um aplicativo abre esse arquivo especial de caractere, a operação genérica de abertura de arquivo usa o identificador de dispositivo principal do dispositivo como um índice na tabela de vetores chrdevs para buscar o bloco de operação de arquivo para esse dispositivo especial. Ele também cria a estrutura de dados do arquivo que descreve o arquivo especial de caracteres, fazendo com que suas operações de arquivo apontem para operações no driver de dispositivo. Em seguida, todas as operações do sistema de arquivos do aplicativo são mapeadas para as operações de arquivos do dispositivo de caractere.

Veja fs/devices.c chrdev_open() def_chr_fops

8.4.2 Bloquear Dispositivos

Dispositivos de bloco também suportam ser acessados ​​como arquivos. O mecanismo para fornecer o conjunto correto de operações de arquivo para arquivos especiais de bloco aberto é muito semelhante ao dos dispositivos de caracteres. O Linux mantém arquivos de dispositivo de bloco registrados com a tabela de vetor blkdevs. Como a tabela de vetores chrdevs, ela usa o número principal do dispositivo como índice. Suas entradas também são estruturas de dados device_struct. Ao contrário dos dispositivos de caracteres, os dispositivos de bloco são classificados. SCSI é uma categoria e IDE é outra. As classes são registradas no kernel do Linux e fornecem operações de arquivo ao kernel. Um driver de dispositivo para uma classe de dispositivo de bloco fornece interfaces relacionadas à classe para essa classe. Por exemplo, um driver de dispositivo SCSI deve fornecer uma interface para o subsistema SCSI que possa ser usada pelo subsistema SCSI para fornecer operações de arquivo para esses dispositivos ao kernel

Veja fs/dispositivos.c

Cada driver de dispositivo de bloco deve fornecer uma interface de operação de arquivo comum e uma interface para o cache de buffer. Cada driver de dispositivo de bloco preenche sua estrutura de dados blk_dev_struct na tabela de vetores blk_dev. O índice nesta tabela de vetores também é o número principal do dispositivo. A estrutura de dados blk_dev_struct contém o endereço de uma rotina de solicitação e um ponteiro para uma lista de estruturas de dados de solicitação, cada uma representando uma solicitação para o cache de buffer ler ou gravar um bloco de dados no dispositivo.

Veja drivers/block/ll_rw_blk.c include/linux/blkdev.h

Cada vez que o cache de buffer deseja ler ou gravar um bloco de dados de ou para um dispositivo registrado, ele adiciona uma estrutura de dados de solicitação ao seu blk_dev_struc. A Figura 8.2 mostra que cada solicitação tem um ponteiro para uma ou mais estruturas de dados buffer_head, cada uma das quais é uma solicitação para ler ou gravar um bloco de dados. A estrutura de dados buffer_head está bloqueada (cache de buffer) e pode haver um processo aguardando a conclusão do processo de bloqueio desse buffer. Cada estrutura de solicitação é alocada a partir de uma tabela estática, a tabela all_request. Se a solicitação for adicionada a uma lista de solicitações vazia, a função de solicitação do driver será chamada para processar a fila de solicitações. Caso contrário, o driver simplesmente processa cada solicitação na fila de solicitações.

Depois que o driver de dispositivo conclui uma solicitação, ele deve remover cada estrutura buffer_head da estrutura de solicitação, marcá-las como atualizadas e desbloqueá-las. Desbloquear o buffer_head despertará todos os processos que estão aguardando a conclusão dessa operação de bloqueio. Exemplos disso incluem análise de arquivos: deve esperar que o sistema de arquivos EXT2 leia o bloco que contém a próxima entrada de diretório EXT2 do dispositivo de bloco que contém o sistema de arquivos, o processo ficará suspenso na fila buff_head que conterá a entrada do diretório, até que o driver do dispositivo o acorda. Essa estrutura de dados de solicitação será marcada como livre e poderá ser usada por outra solicitação de bloco.

8.5 Discos Rígidos

Os discos rígidos armazenam dados em pratos giratórios, fornecendo uma maneira mais permanente de armazenar dados. Para gravar dados, pequenas cabeças magnetizam um pequeno ponto na superfície do disco. A cabeça magnética pode detectar se a partícula especificada está magnetizada, para que os dados possam ser lidos.

Uma unidade de disco consiste em um ou mais pratos, cada um dos quais é feito de vidro ou cerâmica bastante liso e coberto com uma fina camada de óxido metálico. O disco é colocado em um eixo central e gira a uma velocidade constante. A velocidade de rotação varia de 3000 a 1000 RPM (rotações por minuto) dependendo do modelo. As cabeças de leitura/gravação do disco são responsáveis ​​pela leitura e escrita dos dados, e cada disco possui um par, um para cada lado. A cabeça de leitura/gravação e a superfície do prato não estão em contato físico, mas flutuam em uma fina almofada de ar (um centésimo de polegada). As cabeças de leitura e gravação são movidas pela superfície do disco por uma unidade. Todas as cabeças se unem e se movem juntas na superfície do disco.

A superfície de cada disco é dividida em anéis concêntricos estreitos chamados trilhas. A trilha 0 é a trilha mais externa e a trilha numerada mais alta é a trilha mais próxima do eixo central. Um cilindro é uma combinação de faixas numeradas de forma idêntica. Assim, todas as 5 faixas de cada lado de cada disco são o 5º cilindro. Como o número de cilindros é o mesmo que o número de trilhas, o tamanho de um disco é frequentemente descrito em cilindros. Cada trilha é dividida em setores. Um setor é a menor unidade de dados que pode ser lida ou gravada de um disco rígido, que é o tamanho do bloco do disco. Normalmente, o tamanho do setor é de 512 bytes e o tamanho do setor geralmente é definido quando o disco é formatado durante a fabricação.

Um disco é geralmente descrito por sua geometria: o número de cilindros, o número de cabeças e o número de setores. Por exemplo, o Linux descreve meu disco IDE assim ao inicializar:

hdb: Conner Peripherals 540MB - CFS540A, 516MB w/64kB Cache, CHS=1050/16/63

Isto significa que é composto por 1050 cilindros (faixas), 16 cabeças (8 discos) e 63 setores/faixa. Para um tamanho de setor ou bloco de 512 bytes, a capacidade do disco é de 529200K bytes. Isso é inconsistente com a capacidade de armazenamento declarada do disco de 516 M, porque alguns setores são usados ​​para armazenar as informações de partição do disco. Alguns discos podem encontrar automaticamente setores defeituosos e reindexá-los.

Os discos rígidos podem ser subdivididos em partições. Uma partição é um grande grupo de setores alocados para uma finalidade específica. Particionar um disco permite que o disco seja usado para vários sistemas operacionais ou para vários propósitos. A maioria dos sistemas Linux de disco único consiste em 3 partições: uma contém o sistema de arquivos DOS, outra é o sistema de arquivos EXT2 e a terceira é a partição swap. A partição do disco rígido é descrita pela tabela de partições, e cada entrada descreve a posição inicial e final da partição com o número do cabeçote, setor e cilindro. Para discos DOS formatados com fdisk, pode haver 4 partições de disco primário. Nem todas as 4 entradas da tabela de partição devem ser usadas. O Fdisk suporta três tipos de partições: primária, estendida e lógica. Uma partição estendida não é uma partição real, ela pode incluir qualquer número de partições lógicas. As partições estendidas e as partições lógicas foram inventadas para romper o limite de 4 partições primárias. Aqui está a saída do fdisk para um disco que inclui 2 partições primárias:

Disk /dev/sda: 64 heads, 32 sectors, 510 cylinders
Units = cylinders of 2048 * 512 bytes
Device Boot Begin Start End Blocks Id System
/dev/sda1 1 1 478 489456 83 Linux native
/dev/sda2 479 479 510 32768 82 Linux swap
Expert command (m for help): p
Disk /dev/sda: 64 heads, 32 sectors, 510 cylinders
Nr AF Hd Sec Cyl Hd Sec Cyl Start Size ID
1 00 1 1 0 63 32 477 32 978912 83
2 00 0 1 478 63 32 509 978944 65536 82
3 00 0 0 0 0 0 0 0 0 00
4 00 0 0 0 0 0 0 0 0 00

Mostra que a primeira partição começa no cilindro ou pista 0, cabeçote 1 e setor 1, até o cilindro 477, setor 32 e cabeçote 63. Como uma trilha consiste em 32 setores e 64 cabeças de leitura/gravação, os cilindros dessa partição estão totalmente incluídos. Por padrão, o Fdisk alinha as partições nos limites do cilindro. Começa no cilindro mais externo (0) e se expande 478 cilindros para dentro em direção ao eixo central. A segunda partição, a partição swap, começa no próximo cilindro (478) e se estende até o cilindro mais interno do disco.

Durante a inicialização, o Linux mapeia a topologia dos discos rígidos no sistema. Ele descobre quantos discos rígidos estão no sistema e o tipo de discos rígidos. O Linux também descobre como cada disco é particionado. Estas são todas as representações de lista de um conjunto de estruturas de dados gendisk apontadas por uma lista de ponteiros gendisk_head. Para cada subsistema de disco, como o IDE, a inicialização gera uma estrutura de dados gendisk que representa os discos encontrados. Este processo ocorre ao mesmo tempo em que registra suas operações de arquivo e incrementa sua entrada na estrutura de dados blk_dev. Cada estrutura de dados gendisk possui um número de dispositivo principal exclusivo, o mesmo de um dispositivo específico de bloco. Por exemplo, o subsistema de disco SCSI cria uma entrada gendisk separada ("sd") com o número principal 8 (o número principal para todos os dispositivos de disco SCSI). A Figura 8.3 mostra duas entradas gendisk, sendo a primeira o subsistema de disco SCSI e a segunda o controlador de disco IDE. Aqui está ide0, o controlador IDE principal.

Embora o subsistema de disco crie entradas de gendisk correspondentes durante a inicialização, o Linux o usa apenas para verificação de partição. Cada subsistema de disco deve manter suas próprias estruturas de dados que permitem mapear os números principais e secundários do dispositivo para partições de disco físico. Sempre que um dispositivo de bloco é lido ou escrito, seja por meio do cache de buffer ou de uma operação de arquivo, o kernel direciona a operação para o dispositivo apropriado com base nos números maiores e menores encontrados no arquivo de dispositivo especial do bloco (por exemplo, /dev/sda2) . . É cada driver de dispositivo ou subsistema que mapeia o número menor para o dispositivo físico real.

8.5.1 Discos IDE

Os discos mais usados ​​em sistemas Linux hoje são os discos IDE (Integrated Disk Electronic). IDE, como SCSI, é uma interface de disco em vez de um barramento de E/S. Cada controlador IDE pode suportar até 2 discos, um é o mestre e o outro é o escravo. Mestre e escravo geralmente são configurados com jumpers no disco. O primeiro controlador IDE no sistema é chamado de controlador IDE mestre, o próximo é chamado de controlador escravo e assim por diante. O IDE pode transferir 3,3 M/s de/para o disco, e o tamanho máximo do disco IDE é de 538 M bytes. O IDE estendido ou EIDE aumenta o tamanho máximo do disco para 8,6 G bytes e a taxa de transferência de dados chega a 16,6 M/s. Os discos IDE e EIDE são mais baratos que os discos SCSI, e a maioria dos PCs modernos tem um ou mais controladores IDE na placa-mãe.

O Linux nomeia os discos IDE na ordem em que os controladores descobrem. O disco primário no controlador mestre é /dev/had e o disco escravo é /dev/hdb. /dev/hdc é o disco mestre no controlador IDE secundário. O subsistema IDE registra controladores IDE com Linux em vez de discos. O identificador primário do controlador IDE primário é 3 e o identificador do controlador IDE secundário é 22. Isso significa que, se um sistema tiver dois controladores IDE, haverá entradas para o subsistema IDE nos índices 3 e 22 nas tabelas de vetor blk_dev e blkdevs. Os arquivos especiais de bloco para discos IDE refletem esta numeração: discos /dev/had e /dev/hdb, ambos conectados ao controlador IDE principal, ambos possuem o número principal 3. O núcleo usa o identificador de dispositivo principal como um índice e todas as operações de cache de arquivo ou buffer executadas pelo subsistema IDE para esses arquivos especiais de bloco são direcionadas para o subsistema IDE correspondente. Ao executar uma solicitação, o subsistema IDE é responsável por determinar para qual disco IDE a solicitação se destina. Para fazer isso, o subsistema IDE usa o número menor no arquivo especial do dispositivo, informação que permite direcionar a solicitação para a partição correta no disco correto. /dev/hdb, o identificador de dispositivo do disco IDE escravo no controlador IDE mestre é (3, 64). O identificador de dispositivo de sua primeira partição (/dev/hdb1) é (3, 65).

8.5.2 Inicializando o Subsistema IDE

Os discos IDE existem durante a maior parte da história dos PCs IBM. Durante esse período, as interfaces desses dispositivos foram alteradas. Isso torna o processo de inicialização do subsistema IDE mais complicado do que quando ele apareceu pela primeira vez.

O número máximo de controladores IDE que o Linux pode suportar é 4. Cada controlador é representado por uma estrutura de dados ide_hwif_t em uma tabela de vetores ide_hwifs. Cada estrutura de dados ide_hwif_t contém duas estruturas de dados ide_drive_t, representando as possíveis unidades IDE mestre e escrava suportadas, respectivamente. Durante a inicialização do subsistema IDE, o Linux primeiro verifica as informações do disco gravadas na memória CMOS do sistema. Esta memória com bateria não perde seu conteúdo quando o PC é desligado. Esta memória CMOS está, na verdade, dentro do dispositivo de relógio em tempo real do sistema e funciona com o seu PC ligado ou desligado. A localização da memória CMOS é definida pelo BIOS do sistema e informa ao Linux quais controladores e unidades IDE são encontrados no sistema. O Linux obtém a geometria do disco encontrado do BIOS e usa essas informações para definir a estrutura de dados ide_hwif_t da unidade. A maioria dos PCs modernos usa chipsets PCI, como o chipset 82430 VX da Intel, que inclui um controlador PCI EIDE. O subsistema IDE usa retornos de chamada PCI BIOS para localizar o controlador IDE PCI(E) no sistema. As rotinas de consulta para esses chipsets são então chamadas.

Uma vez que uma interface ou controlador IDE é encontrado, seu ide_hwif_t é definido para refletir o controlador e os discos nele. Durante a operação, o driver IDE grava comandos no registro de comando IDE no espaço de memória de E/S. Os endereços de E/S padrão para os registros de controle e status do controlador IDE principal são 0x1F0-0x1F7. Esses endereços eram convenções nos primeiros PCs da IBM. O driver IDE registra cada controlador com o cache de buffer do Linux e o VFS, adicionando-o às tabelas de vetor blk_dev e blkdevs, respectivamente. O driver IDE também solicita o controle das interrupções apropriadas. Novamente, essas interrupções têm uma convenção, 14 para o controlador IDE primário e 15 para o controlador IDE secundário. No entanto, como todos os detalhes do IDE, eles podem ser alterados com as principais opções de linha de comando. O driver IDE também adiciona uma entrada gendisk à lista gendisk para cada controlador IDE encontrado na inicialização. Esta lista é usada posteriormente para visualizar as tabelas de partição de todos os discos rígidos encontrados na inicialização. O código de verificação de partição entende que cada controlador IDE pode controlar dois discos IDE.

8.5.3 Discos SCSI

O barramento SCSI (Small Computer System Interface) é um barramento de dados ponto a ponto eficaz, cada barramento suporta até 8 dispositivos, cada host pode ter um ou mais. Cada dispositivo deve receber um identificador exclusivo, geralmente definido por jumpers no disco. Os dados podem ser transferidos de forma síncrona ou assíncrona entre quaisquer dois dispositivos no barramento e podem ser transferidos em dados de 32 bits a velocidades de até 40 M/s. O barramento SCSI pode transferir dados e informações de status entre dispositivos, e as transações entre o iniciador e o destino envolvem até 8 fases distintas. Você pode determinar o estágio atual por 5 sinais no barramento SCSI. As 8 etapas são:

BUS FREE Nenhum dispositivo tem controle do barramento e nenhuma transação está ocorrendo no momento.

ARBITRAGEM Um dispositivo SCSI tenta obter o controle do barramento SCSI afirmando seu identificador SCSI no pino de endereço. O identificador SCSI de número mais alto é bem-sucedido.

SELEÇÃO Um dispositivo arbitrou com sucesso o controle do barramento SCSI e agora deve sinalizar o destino SCSI para o qual deseja enviar comandos. Ele declara o identificador SCSI do destino nos pinos de endereço.

RESELEÇÃO O dispositivo SCSI pode se desconectar durante o processamento da solicitação e o destino selecionará novamente o iniciador. Nem todos os dispositivos SCSI suportam este estágio.

COMANDO Comandos de 6, 10 ou 12 bytes podem ser enviados do iniciador ao destino.

DATA IN, DATA OUT Neste estágio, os dados são transferidos entre o iniciador e o destino.

STATUS Após completar todos os comandos, entre nesta etapa. Permite que o destino envie um byte de status ao iniciador indicando sucesso ou falha.

MESSAGE IN, MESSAGE OUT Informações adicionais passadas entre o iniciador e o destino.

O subsistema Linux SCSI consiste em dois elementos básicos, cada um representado por uma estrutura de dados:

Host Um host SCSI é uma peça física de hardware, um controlador SCSI. O controlador SCSI PCI NCR810 é um exemplo de host SCSI. Se um sistema Linux tiver mais de um controlador SCSI do mesmo tipo, cada instância será representada por um host SCSI. Isso significa que um driver de dispositivo SCSI pode controlar mais de uma instância do controlador. O host SCSI geralmente é sempre o iniciador do comando SCSI.

Dispositivos Dispositivos SCSI geralmente são discos, mas o padrão SCSI suporta vários tipos: fita, CD-ROM e dispositivos SCSI genéricos. Os dispositivos SCSI geralmente são o destino dos comandos SCSI. Esses dispositivos devem ser tratados de forma diferente. Por exemplo, mídia removível, como CD-ROM ou fita, o Linux precisa detectar se a mídia foi removida. Diferentes tipos de disco têm diferentes números principais, permitindo que o Linux direcione solicitações de dispositivo de bloco para o tipo apropriado de SCSI.

Inicializando o subsistema SCSI

A inicialização do subsistema SCSI é bastante complexa, refletindo a natureza dinâmica do barramento e dos dispositivos SCSI. O Linux inicializa o subsistema SCSI no momento da inicialização: ele procura o controlador SCSI (host SCSI) no sistema e sonda cada barramento SCSI, procurando cada dispositivo. Esses dispositivos são inicializados para que o restante do kernel do Linux possa acessá-los por meio de operações normais de dispositivo de bloco de cache de arquivo e buffer. Este processo de inicialização tem quatro etapas:

Primeiro, o Linux descobre qual adaptador host SCSI ou controlador embutido no kernel possui hardware que ele pode controlar quando o kernel é compilado. Cada host SCSI integrado possui uma entrada Scsi_Host_Template na tabela de vetor buildin_scsi_hosts. A estrutura de dados Scsi_Host_Template contém ponteiros para rotinas que podem executar ações relacionadas ao host SCSI, como detectar quais dispositivos SCSI estão conectados ao host SCSI. Essas rotinas são chamadas durante a configuração do subsistema SCSI e fazem parte dos drivers de dispositivo SCSI que oferecem suporte a esse tipo de host. Para cada controlador SCSI descoberto (com um dispositivo SCSI real conectado), sua estrutura de dados Scsi_Host_Template é adicionada à lista scsi_hosts, representando um host SCSI válido. Cada instância de cada tipo de host detectado é representada por uma estrutura de dados Scsi_Host na lista scsi_hostlist. Por exemplo, um sistema com duas controladoras PCI SCSI NCR810 teria duas entradas Scsi_Host nesta lista, uma para cada controladora. Cada Scsi_Host_Template apontado por Scsi_Host representa seu driver de dispositivo.

Agora que cada host SCSI foi encontrado, o subsistema SCSI deve localizar todos os dispositivos SCSI em cada barramento de host. Os números de dispositivos SCSI variam de 0 a 7, e cada número de dispositivo ou identificador SCSI é exclusivo no barramento SCSI ao qual está conectado. Os identificadores SCSI geralmente são definidos com jumpers no dispositivo. O código de inicialização SCSI encontra cada dispositivo SCSI em um barramento SCSI enviando o comando TEST_UNIT_READY para cada dispositivo. Quando um dispositivo responder, envie um comando INQUIRY para concluir sua determinação. Isso dá ao Linux o nome do fornecedor e o modelo e número de revisão do dispositivo. Os comandos SCSI são representados por uma estrutura de dados Scsi_Cmnd e esses comandos são passados ​​para o driver de dispositivo chamando as rotinas do driver de dispositivo na estrutura de dados Scsi_Host_Template desse host SCSI. Cada dispositivo SCSI encontrado é representado por uma estrutura de dados Scsi_Device, cada um apontando para seu Scsi_Host pai. Todas as estruturas de dados Scsi_Device são adicionadas à lista scsi_devices. A Figura 8.4 mostra a relação entre as principais estruturas de dados e outras estruturas de dados.

Existem quatro tipos de dispositivos SCSI: disco, fita, CD e genérico. Cada tipo SCSI é registrado com o núcleo separadamente e possui um tipo de dispositivo de bloco primário diferente. No entanto, eles só se registram quando um ou mais dispositivos de um determinado tipo de dispositivo SCSI são encontrados. Cada tipo SCSI, como disco SCSI, mantém sua própria tabela de dispositivos. Ele usa essas tabelas para direcionar as operações do bloco principal (arquivo ou cache de buffer) para o driver de dispositivo ou host SCSI correto. Cada tipo SCSI é representado por uma estrutura de dados Scsi_Type_Template. Inclui informações sobre este tipo de dispositivo SCSI e endereços de rotinas que executam várias tarefas. O subsistema SCSI usa esses modelos para chamar as rotinas de tipo SCSI para cada tipo de dispositivo SCSI. Em outras palavras, se o subsistema SCSI desejar anexar um dispositivo de disco SCSI, ele chamará a rotina de tipo de disco SCSI. Se um ou mais dispositivos SCSI de um determinado tipo forem detectados, sua estrutura de dados Scsi_Type_Templates será adicionada à lista scsi_devicelist.

O estágio final da inicialização do subsistema SCSI é chamar a função de conclusão de cada Scsi_Device_Template registrado. Para tipos de disco SCSI, gire todos os discos SCSI e registre seu tamanho de disco. Ele também adiciona uma lista vinculada de discos representando todos os discos SCSI à estrutura de dados do gendisk, conforme mostrado na Figura 8.3.

Entregando solicitações de dispositivo de bloco

Uma vez que o Linux tenha inicializado o subsistema SCSI, os dispositivos SCSI podem ser usados. Cada tipo de dispositivo SCSI válido se registra no kernel, então o Linux pode direcionar solicitações de dispositivo de bloco para ele. Essas solicitações podem ser solicitações de cache de buffer via blk_dev ou operações de arquivo via blkdevs. Pegue uma unidade de disco SCSI que é particionada por um ou mais sistemas de arquivos EXT2 como exemplo, como as solicitações de buffer do kernel são direcionadas para o disco SCSI correto quando suas partições EXT2 são montadas?

Cada solicitação para ler/gravar um bloco de dados de/para uma partição de disco SCSI adiciona uma nova estrutura de dados de solicitação à lista current_request para esse disco SCSI na tabela de vetor blk_dev. Se a lista de solicitações estiver sendo processada, o cache do buffer não fará nada. Caso contrário, deve permitir que o subsistema de disco SCSI lide com sua fila de solicitações. Cada disco SCSI no sistema é representado por uma estrutura de dados Scsi_Disk. Eles são armazenados na tabela de vetores rscsi_disks, indexados por parte do número do dispositivo secundário da partição de disco SCSI. Por exemplo, /dev/sdb1 tem um número de dispositivo principal de 8 e um número de dispositivo secundário de 17, então é 1. Cada estrutura de dados Scsi_Disk inclui um ponteiro para a estrutura de dados Scsi_Device que representa o dispositivo. Scsi_Device, por sua vez, aponta para uma estrutura de dados Scsi_Host que "a possui". A estrutura de dados de solicitação no cache de buffer é convertida em uma estrutura de dados Scsi_Cmd que descreve os comandos SCSI que precisam ser enviados ao dispositivo SCSI e é enfileirada na estrutura de dados Scsi_Host que representa esse dispositivo. Depois que o bloco de dados apropriado é lido/gravado, ele é tratado pelo respectivo driver de dispositivo SCSI.

8.6 Dispositivos de Rede

Um dispositivo de rede, no que diz respeito ao subsistema de rede Linux, é uma entidade que envia e recebe pacotes. Geralmente um dispositivo físico, como uma placa Ethernet. Mas alguns dispositivos de rede são apenas de software, como dispositivos de loopback, que enviam dados para si mesmos. Cada dispositivo de rede é representado por uma estrutura de dados do dispositivo. Um driver de dispositivo de rede registra os dispositivos que ele controla no Linux quando o kernel inicia a inicialização da rede. A estrutura de dados do dispositivo contém informações sobre este dispositivo e endereços de funções que permitem que vários protocolos de rede suportados usem os serviços deste dispositivo. A maioria dessas funções está relacionada à transferência de dados usando este dispositivo de rede. O dispositivo transmite os dados recebidos para a camada de protocolo apropriada usando mecanismos de suporte de rede padrão. Todos os dados de rede (pacotes) transmitidos e recebidos são representados pela estrutura de dados sk_buff, que é uma estrutura de dados flexível que permite que cabeçalhos de protocolo de rede sejam facilmente adicionados e removidos. Como a camada de protocolo de rede usa os dispositivos de rede e como eles passam dados de um lado para o outro usando a estrutura de dados sk_buff é descrito em detalhes na Seção 10. O foco aqui está na estrutura de dados do dispositivo e como os dispositivos de rede são descobertos e inicializados.

Veja include/linux/netdevice.h

A estrutura de dados do dispositivo inclui informações sobre dispositivos de rede:

Nome Ao contrário dos dispositivos de bloco e caractere, cujos arquivos especiais de dispositivo são criados com o comando mknod, os arquivos especiais de dispositivo de rede aparecem naturalmente quando os dispositivos de rede do sistema são descobertos e inicializados. Seus nomes são padrão e cada nome indica seu tipo de dispositivo. Vários dispositivos do mesmo tipo são numerados sequencialmente de 0 para cima. Assim, os dispositivos ethernet são numerados /dev/eth0, /dev/eth1, /dev/eth2 e assim por diante. Alguns dispositivos de rede comuns são:

/dev/ethN 以太网设备
/dev/slN SLIP设备
/dev/pppN PPP设备
/dev/lo loopback 设备

Informações do barramento Essas são as informações que o driver do dispositivo precisa para controlar o dispositivo. Irq é a interrupção usada pelo dispositivo. O endereço base é o endereço dos registradores de controle e status do dispositivo na memória de E/S. Canal DMA é o número do canal DMA usado por este dispositivo de rede. Todas essas informações são definidas no momento da inicialização quando o dispositivo é inicializado.

Sinalizadores de interface Descrevem as características e capacidades deste dispositivo de rede.

Interface IFF_UP ativa, em execução

O endereço de broadcast do dispositivo IFF_BROADCAST é válido

A opção de depuração do dispositivo IFF_DEBUG está ativada

IFF_LOOPBACK Este é um dispositivo de loopback

IFF_POINTTOPOINT Esta é uma conexão ponto a ponto (SLIP e PPP)

IFF_NOTRAILERS Nenhum trailer de rede

IFF_RUNNING recursos alocados

IFF_NOARP não suporta protocolo ARP

O dispositivo IF_PROMISC está em modo de recebimento promíscuo, ele receberá todos os pacotes independente de seus endereços.

IFF_ALLMULTI recebe todos os quadros IP Multicast

IFF_MULTICAST pode receber frames multicast IP

Informações de protocolo Cada dispositivo descreve como pode ser usado pela camada de protocolo de rede:

O Mtu não inclui o cabeçalho da camada de link adicionado para o pacote de tamanho máximo que a rede pode transmitir. Esse valor máximo é usado por camadas de protocolo, como IP, para selecionar um tamanho de pacote apropriado para enviar.

Família da família mostra a família de protocolos que o dispositivo pode suportar. A família suportada por todos os dispositivos de rede Linux é AF_INET, a família de endereços da Internet.

Tipo O tipo de interface de hardware descreve o meio ao qual este dispositivo de rede está conectado. Os dispositivos de rede Linux suportam uma variedade de tipos de mídia. Inclui Ethernet, X.25, Token Ring, Slip, PPP e Apple LocalTalk.

A estrutura de dados do dispositivo Addresses contém alguns endereços associados a este dispositivo de rede, incluindo o endereço IP

Fila de Pacotes Esta é uma fila de pacotes sk_buff esperando que os dispositivos de rede transmitam

Funções de suporte Cada dispositivo fornece um conjunto de rotinas padrão para a camada de protocolo chamar como parte da interface para a camada de link do dispositivo. Inclui rotinas de configuração e transferência de quadros, bem como rotinas para adicionar cabeçalhos de quadros padrão e coletar estatísticas. Estas estatísticas podem ser vistas com ifcnfig

8.6.1 Inicializando Dispositivos de Rede

Drivers de dispositivo de rede, como outros drivers de dispositivo Linux, podem ser incorporados ao kernel do Linux. Cada dispositivo de rede possível é representado por uma estrutura de dados de dispositivo na lista de dispositivos de rede apontados pelo ponteiro de lista dev_base. Se forem necessárias operações relacionadas ao dispositivo, a camada de rede chama uma das rotinas de serviço do dispositivo de rede (na estrutura de dados do dispositivo). No entanto, inicialmente, cada estrutura de dados do dispositivo contém apenas o endereço de uma rotina de inicialização ou detecção.

Drivers de rede devem resolver dois problemas. Primeiro, nem todos os drivers de dispositivo de rede integrados ao kernel do Linux terão dispositivos controlados; segundo, os dispositivos ethernet no sistema são sempre chamados de /dev/eth0, /dev/eth1, etc., independentemente do driver de dispositivo subjacente. O problema de dispositivos de rede "ausentes" é fácil de corrigir. Quando a rotina de inicialização de cada dispositivo de rede é chamada, ela retorna um status que mostra se localizou uma instância do controlador que ele aciona. Se o driver não encontrar nenhum dispositivo, sua entrada na lista de dispositivos apontada por dev_base será removida. Se o driver puder encontrar um dispositivo, ele preencherá o restante da estrutura de dados do dispositivo com informações sobre o dispositivo e os endereços das funções de suporte no driver do dispositivo de rede.

O segundo problema, atribuir dinamicamente dispositivos ethernet ao arquivo especial de dispositivo padrão /dev/ethN, é resolvido de uma maneira mais elegante. Existem 8 entradas padrão na lista de dispositivos: eth0, eth1 a eth7. A rotina de inicialização é a mesma para todas as entradas. Ele tenta construir sequencialmente cada driver de dispositivo ethernet no núcleo até encontrar um. Quando o driver encontra seu dispositivo ethernet, ele preenche a estrutura de dados do dispositivo ethN que agora possui. Nesse ponto, o driver de rede também inicializa o hardware físico que ele controla e descobre quais IRQs, DMAs e assim por diante ele usa. Um driver pode encontrar várias instâncias do dispositivo de rede que ele controla e, nesse caso, ele ocupa várias estruturas de dados de dispositivo /dev/ethN. Depois que todos os 8 /dev/ethNs padrão forem alocados, nenhum outro dispositivo Ethernet será testado.

9. O Sistema de Arquivos

Um dos recursos mais importantes do Linux permite que ele suporte muitos sistemas de arquivos diferentes. Isso o torna muito flexível e pode coexistir com muitos outros sistemas operacionais. O inux sempre suportou 15 sistemas de arquivos: ext, ext2, xia, minix, umsdos, msdos, vfat, proc, smb, ncp, iso9660, sysv, hpfs, affs e ufs, e não há dúvidas de que com o tempo, serão adicionados Mais sistemas de arquivos.

No Linux, como no Unix, os diferentes sistemas de arquivos que o sistema pode usar não são acessados ​​por identificadores de dispositivos (como números de unidades ou nomes de dispositivos), mas são vinculados em uma única estrutura semelhante a uma árvore, representada por um sistema de arquivos unificado de entidade única. O Linux o adiciona a essa única árvore do sistema de arquivos no momento da montagem do sistema de arquivos. Todos os sistemas de arquivos, não importa o tipo, são montados em um diretório, e os arquivos do sistema de arquivos montado mascaram o conteúdo original do diretório. Esse diretório é chamado de diretório de montagem ou ponto de montagem. Quando o sistema de arquivos é desmontado, os próprios arquivos do diretório de instalação podem aparecer novamente.

Quando um disco é inicializado (por exemplo, com fdisk), uma estrutura de partição é usada para dividir o disco físico em um conjunto de partições lógicas. Cada partição pode conter um sistema de arquivos, como um sistema de arquivos EXT2. O sistema de arquivos organiza os arquivos em uma estrutura de árvore lógica em blocos de dispositivos físicos por meio de diretórios, links virtuais, etc. Dispositivos que podem incluir sistemas de arquivos são dispositivos de bloco. A primeira partição da primeira unidade de disco IDE no sistema, a partição de disco IDE /dev/hda1, é um dispositivo de bloco. O sistema de arquivos Linux trata esses dispositivos de bloco como simples combinações lineares de blocos e não conhece ou se preocupa com o tamanho do disco físico subjacente. É tarefa de cada driver de dispositivo de bloco mapear uma solicitação de leitura para um bloco específico do dispositivo em termos significativos para o dispositivo: a trilha, o setor e o cilindro onde esse bloco está armazenado no disco rígido. Um sistema de arquivos deve funcionar da mesma maneira e ter a mesma aparência, independentemente do dispositivo em que está armazenado. Além disso, com o sistema de arquivos do Linux, não importa (pelo menos para o usuário do sistema) se esses diferentes sistemas de arquivos estão em diferentes mídias físicas sob o controle de diferentes controladores de hardware. O sistema de arquivos pode nem estar no sistema local, pode ser montado remotamente em uma conexão de rede. Considere o exemplo a seguir, em que o sistema de arquivos raiz de um sistema Linux está em um disco SCSI.

AE boot etc lib opt tmp usr

CF cdrom fd proc root var sbin

D bin dev home mnt perdido+encontrado

Nem o usuário nem o programa que manipula esses arquivos precisam saber que /C é na verdade um sistema de arquivos VFAT montado no primeiro disco IDE do sistema. Neste exemplo (na verdade, meu sistema Linux em casa), /E é o disco IDE mestre no controlador IDE secundário. Não importa que o primeiro controlador IDE seja um controlador PCI e o segundo seja um controlador ISA, que também controla o CDROM IDE. Posso discar para minha rede de trabalho com um modem e protocolo de rede PPP e, neste ponto, posso montar remotamente o sistema de arquivos do meu sistema Alpha AXP Linux em /mnt/remote.

Os arquivos no sistema de arquivos contêm coleções de dados: O arquivo que contém a fonte para esta seção é um arquivo ASCII chamado filesystems.tex. Um sistema de arquivos contém não apenas os dados dos arquivos que ele contém, mas também a estrutura do sistema de arquivos. Ele contém todas as informações que os usuários e processos do Linux veem, como arquivos, diretórios, links virtuais, informações de proteção de arquivos e muito mais. Além disso, ele deve armazenar essas informações com segurança, e a consistência básica do sistema operacional depende de seu sistema de arquivos. Ninguém pode usar um sistema operacional que perde dados e arquivos aleatoriamente (não sei se existe, embora tenha sido prejudicado por um sistema operacional que tem mais advogados do que desenvolvedores Linux).

Minix é o primeiro sistema de arquivos do Linux, que possui limitações consideráveis ​​e baixo desempenho. Seu nome de arquivo não pode ter mais de 14 caracteres (o que ainda é melhor que 8,3 nomes de arquivo), e o tamanho máximo do corpus é de 64M bytes. À primeira vista, 64 milhões de bytes podem parecer grandes o suficiente, mas configurar um banco de dados médio requer um tamanho de arquivo maior. O primeiro sistema de arquivos projetado especificamente para Linux, o Extended File System ou EXT (Extend File System), foi introduzido em abril de 1992 e resolveu muitos problemas, mas ainda sentia baixo desempenho. Assim, em 1993, foi adicionado o Extended File System Versão 2, ou EXT2. Esse sistema de arquivos é descrito em detalhes posteriormente nesta seção.

Um grande desenvolvimento ocorreu quando o sistema de arquivos EXT foi adicionado ao Linux. O sistema de arquivos real é separado do sistema operacional e dos serviços do sistema por meio de uma camada de interface chamada sistema de arquivos virtual ou VFS. O VFS permite que o Linux suporte muitos sistemas de arquivos (geralmente diferentes), cada um apresentando uma interface de software comum ao VFS. Todos os detalhes do sistema de arquivos Linux são traduzidos por software, portanto, todos os sistemas de arquivos parecem iguais para o restante do kernel Linux e programas em execução no sistema. A camada de sistema de arquivos virtual do Linux permite montar de forma transparente muitos sistemas de arquivos diferentes ao mesmo tempo.

A implementação do sistema de arquivos virtuais Linux torna o acesso aos seus arquivos o mais rápido e eficiente possível. Ele também deve garantir que os arquivos e os dados dos arquivos sejam armazenados corretamente. Esses dois requisitos podem ser desiguais entre si. O Linux VFS armazena informações em cache na memória à medida que cada sistema de arquivos é montado e usado. Esses dados armazenados em cache são alterados à medida que os arquivos e diretórios são criados, gravados e excluídos, e muito cuidado deve ser tomado para atualizar adequadamente o sistema de arquivos. Se você puder ver as estruturas de dados do sistema de arquivos no kernel em execução, poderá ver o sistema de arquivos ler e gravar blocos de dados, estruturas de dados que descrevem os arquivos e diretórios acessados ​​são criadas e destruídas e os drivers de dispositivo não são Parar e executar , capturar e salvar dados. O mais importante desses caches é o Buffer Cache, que é incorporado quando os sistemas de arquivos acessam seus dispositivos de bloco subjacentes. Quando os blocos são acessados, eles são colocados no Buffer Cache e colocados em diferentes filas dependendo de seu estado. O Buffer Cache não apenas armazena buffers de dados, mas também ajuda a gerenciar a interface assíncrona do driver de dispositivo de bloco.

9.1 O Segundo Sistema de Arquivo Estendido (EXT2)

O EXT2 foi inventado (Remy Card) como um sistema de arquivos escalável e poderoso para Linux. É o sistema de arquivos de maior sucesso, pelo menos na comunidade Linux, e é a base para todas as distribuições Linux atuais. O sistema de arquivos EXT2, como todos os sistemas de arquivos, é construído com base na premissa de que os dados de um arquivo são armazenados em blocos de dados. Esses blocos de dados são todos do mesmo comprimento, embora o comprimento do bloco de diferentes sistemas de arquivos EXT2 possa ser diferente, mas para um sistema de arquivos EXT2 específico, seu comprimento de bloco é determinado quando ele é criado (usando mke2fs). O comprimento de cada arquivo é arredondado em blocos. Se o tamanho do bloco for 1024 bytes, um arquivo de 1025 bytes ocupará dois blocos de 1024 bytes. Infelizmente, isso significa que, em média, você desperdiça meio bloco por arquivo. Normalmente na computação você troca a utilização do disco pelo uso da memória da CPU.Neste caso, o Linux, como a maioria dos sistemas operacionais, usa uma utilização de disco relativamente ineficiente em troca de menos carga da CPU. Nem todos os blocos em um sistema de arquivos contêm dados, alguns blocos devem ser usados ​​para colocar informações que descrevem a estrutura do sistema de arquivos. EXT2 descreve cada arquivo no sistema com uma estrutura de dados inode, que define a topologia do sistema. Um inode descreve quais blocos ocupam os dados em um arquivo, bem como os direitos de acesso do arquivo, o tempo de modificação do arquivo e o tipo do arquivo. Cada arquivo no sistema de arquivos EXT2 é descrito por um inode e cada inode é identificado por um número único. Os inodes do sistema de arquivos são mantidos juntos, na tabela de inodes. Os diretórios EXT2 são simplesmente arquivos especiais (eles também são descritos usando inodes) que incluem ponteiros para os inodes de suas entradas de diretório.

A Figura 9.1 mostra um sistema de arquivos EXT2 ocupando uma série de blocos em um dispositivo estruturado em blocos. Sempre que um sistema de arquivos é mencionado, um dispositivo de bloco pode ser pensado como uma série de blocos que podem ser lidos e gravados. O sistema de arquivos não precisa se importar em qual bloco de mídia física ele se coloca, esse é o trabalho do driver do dispositivo. Quando um sistema de arquivos precisa ler informações ou dados do dispositivo de bloco que o contém, ele solicita que um número inteiro de blocos seja lido do driver de dispositivo que ele suporta. O sistema de arquivos EXT2 divide as partições lógicas que ocupa em Grupos de Blocos. Além de manter arquivos e diretórios reais como informações e blocos de dados, cada grupo replica informações críticas para a consistência do sistema de arquivos. Essas informações replicadas são necessárias no caso de um desastre e o sistema de arquivos precisa ser recuperado. O conteúdo de cada grupo de blocos é descrito em detalhes abaixo.

9.1.1 O Inode EXT2

No sistema de arquivos EXT2, o inode é a pedra angular da construção: cada arquivo e diretório no sistema de arquivos é descrito por um e apenas um inode. Os inodes EXT2 para cada grupo de blocos são colocados na tabela de inodes, juntamente com um bitmap que permite que o sistema acompanhe os inodes alocados e não alocados. A Figura 9.2 mostra o formato de um inode EXT2, entre outras informações, inclui alguns campos:

Veja include/linux/ext2_fs_i.h

mode contém dois conjuntos de informações: o que o inode descreve e os direitos do usuário sobre ele. Para EXT2, um inode pode descrever um arquivo, diretório, link simbólico, dispositivo de bloco, dispositivo de caractere ou FIFO.

Informações do proprietário Identificadores de usuário e grupo para os dados deste arquivo ou diretório. Isso permite que o sistema de arquivos controle adequadamente as permissões de acesso a arquivos

Tamanho O tamanho do arquivo (bytes)

Timestamps A hora em que este inode foi criado e a hora em que foi modificado pela última vez.

Datablocks Ponteiro para os blocos de dados descritos por este inode. Os primeiros 12 são blocos físicos que apontam para os dados descritos por este inode, e os últimos 3 ponteiros incluem mais níveis de indireção. Por exemplo, um ponteiro de bloco indireto de dois níveis aponta para um ponteiro de bloco que aponta para um ponteiro de bloco de um bloco de dados. Isso significa que arquivos com tamanho de bloco de dados menor ou igual a 12 são mais rápidos de acessar do que arquivos maiores.

Você deve observar que os inodes EXT2 podem descrever arquivos de dispositivos especiais. Estes não são arquivos reais que os programas podem usar para acessar o dispositivo. Todos os arquivos de dispositivo em /dev são projetados para permitir que programas acessem dispositivos Linux. Por exemplo, o programa de montagem usa o arquivo de dispositivo que deseja montar como argumento.

9.1.2 O Superbloco EXT2

O superblock contém uma descrição do tamanho e formato básico do sistema de arquivos. As informações nele contidas permitem que o gerenciador do sistema de arquivos as use para manter o sistema de arquivos. Normalmente, apenas os superblocos do grupo de blocos 0 são lidos quando o sistema de arquivos é montado, mas cada grupo de blocos contém uma cópia replicada para uso em caso de falha do sistema. Além de algumas outras informações, inclui:

Veja include/linux/ext2_fs_sb.h

Magic Number permite que o software de instalação verifique se este é um superbloco de um sistema de arquivos EXT2. Para a versão atual do EXT2 é 0xEF53.

Os níveis de revisão principal e secundária do Nível de Revisão permitem que o código de instalação determine se este sistema de arquivos suporta recursos que estão disponíveis apenas nesta revisão específica do sistema de arquivos. Este também é o campo de compatibilidade de recursos, que ajuda o código de instalação a determinar quais novos recursos são seguros para uso neste sistema de arquivos.

Mount Count e Maximum Mount Count Juntos, permitem que o sistema determine se este sistema de arquivos precisa de uma verificação completa. Cada vez que o sistema de arquivos é montado, a contagem de montagens aumenta. Quando é igual à contagem máxima de montagens, a mensagem de aviso "contagem máxima de montagem alcançada, é recomendado executar e2fsck" é exibida.

Número do grupo de blocos Armazena o número do grupo de blocos desta cópia de superbloco.

Tamanho do Bloco O tamanho em bytes dos blocos do sistema de arquivos, por exemplo, 1024 bytes.

Blocos por Grupo O número de blocos no grupo. Assim como o tamanho do bloco, isso é determinado quando o sistema de arquivos é criado.

Blocos Livres O número de blocos livres no sistema de arquivos.

Inodes Livres Inodes livres no sistema de arquivos.

Primeiro Inode Este é o número do primeiro inode no sistema. O primeiro inode em um sistema de arquivos raiz EXT2 é a entrada de diretório para o diretório '/'.

 

9.1.3 O Descritor do Grupo EXT2

Cada grupo de blocos tem uma descrição da estrutura de dados. Como os superblocos, os descritores de grupo de todos os grupos vencedores são replicados em cada grupo de blocos. Cada descritor de grupo inclui as seguintes informações:

Veja include/linux/ext2_fs.h ext2_group_desc

Blocks Bitmap O número do bloco do bitmap de alocação de blocos deste grupo de blocos, usado no processo de alocação e recuperação de blocos

Inode Bitmap O número do bloco do bitmap do inode para este grupo de blocos. Usado na alocação e reciclagem de inodes.

Tabela de inodes O número do bloco inicial da tabela de inodes deste grupo de blocos. O inode representado por cada estrutura de dados do inode EXT2 é descrito abaixo .

Contagem de blocos livres,Contagem de inodes livres,Contagem de diretórios usados

Os descritores de grupo são organizados em sequência e juntos formam a tabela de descritores de grupo. Cada grupo de blocos inclui uma cópia completa da tabela do descritor do grupo de blocos e seu superbloco. Apenas a primeira cópia (no grupo de blocos 0) é realmente usada pelo sistema de arquivos EXT2. Outras cópias, como outras cópias do superbloco, são usadas apenas quando a cópia primária está corrompida.

9.1.4 Diretórios EXT2

No sistema de arquivos EXT2, os diretórios são arquivos especiais usados ​​para criar e armazenar caminhos de acesso aos arquivos no sistema de arquivos. A Figura 9.3 mostra o layout de uma entrada de diretório na memória. Um arquivo de diretório é uma lista de entradas de diretório, cada entrada de diretório contendo as seguintes informações:

Veja include/linux/ext2_fs.h ext2_dir_entry

inode O inode desta entrada de diretório. Este é um índice no array de inodes colocado na tabela de inodes do grupo de blocos. Figura 9.3 O inode referenciado pela entrada do diretório para o arquivo chamado file é i1.

Comprimento do nome O comprimento em bytes desta entrada de diretório

Nome O nome desta entrada de diretório

As duas primeiras entradas em cada diretório são sempre o padrão "." e "..", significando "este diretório" e "diretório pai", respectivamente.

9.1.5 Encontrando um arquivo em um sistema de arquivos EXT2

Os nomes de arquivos do Linux têm o mesmo formato que todos os nomes de arquivos do Unix. É uma série de nomes de diretório separados por "/" e terminando com o nome do arquivo. Um exemplo de nome de arquivo é /home/rusling/.cshrc, onde /home e /rusling são os nomes de diretório e o nome do arquivo é .cshrc. Como outros sistemas Unix, o Linux não se preocupa com o formato do nome do arquivo em si: ele pode ter qualquer tamanho e consiste em caracteres imprimíveis. Para encontrar o inode que representa este arquivo no sistema de arquivos EXT2, o sistema deve analisar os nomes dos arquivos no diretório um por um até que o arquivo seja encontrado.

O primeiro inode que precisamos é o inode da raiz deste sistema de arquivos. Encontramos seu número através do superblock do sistema de arquivos. Para ler um inode EXT2, devemos procurar na tabela de inodes no grupo de blocos apropriado. Por exemplo, se o número do inode da raiz for 42, precisamos do 42º inode na tabela de inode no grupo de blocos 0. O inode raiz é um diretório EXT2, em outras palavras, o esquema do inode raiz o descreve como um diretório cujos blocos de dados incluem entradas de diretório EXT2.

Home é uma dessas entradas de diretório, esta entrada de diretório nos dá o número do inode que descreve o diretório /home. Temos que ler este diretório (primeiro ler seu inode, depois ler a entrada do diretório do bloco de dados descrito por este inode), procurar a entrada rusling e fornecer o número do inode que descreve o diretório /home/rusling. Finalmente, lemos a entrada de diretório apontada pelo inode que descreve o diretório /home/rusling para encontrar o número do inode do arquivo .cshrc, então obtemos o bloco de dados contendo as informações no arquivo.

9.1.6 Alterando o tamanho de um arquivo em um sistema de arquivos EXT2

Um problema comum com sistemas de arquivos é que eles tendem a ser mais fragmentados. Blocos contendo dados de arquivos são distribuídos por todo o sistema de arquivos. Quanto mais dispersos os blocos de dados, menos eficiente será o acesso seqüencial aos blocos de dados de arquivos. O sistema de arquivos EXT2 tenta superar essa situação atribuindo novos blocos a um arquivo que esteja fisicamente próximo ou pelo menos dentro de um grupo de blocos de seus blocos de dados atuais. Somente se isso falhar, ele aloca blocos de dados em outros grupos de blocos.

Sempre que um processo tenta gravar dados em um arquivo, o sistema de arquivos Linux verifica se os dados excederiam o final do último bloco alocado do arquivo. Se for, deve alocar um novo bloco de dados para este arquivo. Até que essa alocação seja concluída, o processo não pode ser executado, ele deve aguardar que o sistema de arquivos aloque novos blocos de dados e grave os dados restantes antes de continuar. A primeira coisa que a rotina de alocação de bloco EXT2 faz é bloquear o superbloco EXT2 para este sistema de arquivos. Alocar e liberar blocos requer a alteração dos campos no superblock, e o sistema de arquivos Linux não pode permitir que mais de um processo faça alterações ao mesmo tempo. Se outro processo precisar alocar mais blocos de dados, ele deverá aguardar até que esse processo seja concluído. Um processo esperando por um superblock é suspenso e não pode ser executado até que o controle do superblock seja liberado por seu usuário atual. O acesso ao superblock é concedido por ordem de chegada, uma vez que um processo tem o controle do superblock, ele mantém o controle até que seja concluído. Após bloquear o superbloco, o processo verifica se o sistema de arquivos possui blocos livres suficientes. Se não houver blocos livres suficientes, as tentativas de alocar mais falharão e o processo abrirá mão do controle do superbloco do sistema de arquivos.

Se houver blocos livres suficientes no sistema de arquivos, o processo tentará alocar um bloco. Se o sistema de arquivos EXT2 já criou blocos de dados pré-alocados, podemos acessá-los. Blocos pré-alocados na verdade não existem, eles são apenas blocos reservados no bitmap dos blocos alocados. O inode VFS usa dois campos específicos de EXT2 para representar o arquivo ao qual estamos tentando alocar novos blocos de dados: prealloc_block e prealloc_count, que são o número do primeiro bloco no bloco pré-alocado e o número de blocos pré-alocados, respectivamente. Se não houver blocos pré-alocados ou a pré-alocação estiver desabilitada, o sistema de arquivos EXT2 deverá alocar um novo bloco de dados. O sistema de arquivos EXT2 verifica primeiro se o bloco de dados após o último bloco de dados do arquivo está livre. Logicamente, este é o bloco mais eficiente que pode ser alocado porque os acessos sequenciais são mais rápidos. Se o bloco não estiver livre, continue a procurar o bloco de dados ideal nos próximos 64 blocos. Este bloco, embora não seja o ideal, é pelo menos bastante próximo do restante do arquivo, em um grupo de blocos.

Veja fs/ext2/balloc.c ext2_new_block()

Se nenhum desses blocos estiver livre, o processo começará a procurar sequencialmente todos os outros grupos de blocos até encontrar um bloco livre. O código de alocação de bloco procura clusters de 8 blocos de dados livres nesses grupos de blocos. Reduz o requisito se não conseguir encontrar 8 de cada vez. Se a pré-alocação de bloco for desejada e permitida, ele atualizará prealloc_block e prealloc_count de acordo.

Onde quer que um bloco de dados livre seja encontrado, o código de alocação de bloco atualiza o bitmap de bloco do grupo de blocos e aloca um buffer de dados do cache de buffer. Esse buffer de dados é identificado exclusivamente usando o identificador de dispositivo que sustenta o sistema de arquivos e o número do bloco alocado. Os dados no buffer são definidos como 0 e o buffer é marcado como "sujo" para indicar que seu conteúdo não foi gravado no disco físico. Por fim, o próprio superblock também marca o bit "sujo", indicando que ele fez alterações e, em seguida, seus bloqueios são liberados. Se houver um processo aguardando o superblock, o primeiro processo na fila poderá ser executado, obterá o controle exclusivo do superblock e realizará suas operações de arquivo. Os dados do processo são gravados em um novo bloco de dados.Se o bloco de dados estiver cheio, todo o processo é repetido e outros blocos de dados são alocados.

9.2 O Sistema de Arquivo Virtual (VFS)

A Figura 9.4 mostra a relação entre o sistema de arquivos virtual do kernel Linux e seu sistema de arquivos real. O sistema de arquivos virtual deve gerenciar todos os diferentes sistemas de arquivos montados ao mesmo tempo. Para este fim, ele gerencia as estruturas de dados que descrevem todo o sistema de arquivos (virtual) e os sistemas de arquivos reais montados individuais.

De forma bastante confusa, o VFS também usa os termos superblock e inode para descrever os arquivos do sistema, da mesma forma que o sistema de arquivos EXT2 usa superblocks e inodes. Assim como os inodes EXT2, os inodes VFS descrevem arquivos e diretórios no sistema: o conteúdo e a topologia do sistema de arquivos virtual. De agora em diante, para evitar confusão, usarei inodes VFS e superblocks VFS para distingui-los dos inodes e superblocks EXT2.

veja fs/*

Quando cada sistema de arquivos é inicializado, ele se registra no VFS. Isso acontece quando o sistema inicializa o sistema operacional para inicializar a si mesmo. O próprio sistema de arquivos é embutido no kernel ou como um módulo carregável. Os módulos do sistema de arquivos são carregados quando o sistema precisa deles, portanto, se um sistema de arquivos VFAT for implementado como um módulo principal, ele será carregado apenas quando um sistema de arquivos VFAT for instalado. Quando um sistema de arquivos de dispositivo de bloco é montado (incluindo o sistema de arquivos raiz), o VFS deve ler seu superbloco. A rotina de leitura de superblocos para cada tipo de sistema de arquivos deve descobrir a topologia do sistema de arquivos e mapear essas informações para uma estrutura de dados de superblocos VFS. Um VFS mantém uma lista de sistemas de arquivos montados no sistema e sua lista de superblocos VFS. Cada superbloco VFS contém informações do sistema de arquivos e ponteiros para rotinas que executam funções específicas. Por exemplo, o superblock que representa um sistema de arquivos EXT2 montado contém um ponteiro para a rotina de leitura de um inode relacionado a EXT2. Essa rotina de leitura de inode EXT2, como todas as rotinas de leitura de inode relacionadas ao sistema de arquivos, preenche o campo de inode VFS. Cada superbloco VFS contém um ponteiro para um inode VFS no sistema de arquivos. Para o sistema de arquivos raiz, este é o inode que representa o diretório "/". Esse mapeamento de informações é bastante eficiente para o sistema de arquivos EXT2, mas relativamente ineficiente para outros sistemas de arquivos.

 

Quando o sistema processa diretórios e arquivos de acesso, eles chamam rotinas do sistema para percorrer os inodes VFS no sistema. Por exemplo, digite ls ou cat em um arquivo em outro diretório e deixe o VFS localizar o inode VFS que representa esse sistema de arquivos. Cada arquivo e diretório no sistema de mapeamento é representado por um inode VFS, portanto, alguns inodes serão acessados ​​repetidamente. Esses inodes são mantidos no cache de inode, o que torna o acesso a eles mais rápido. Se um inode não estiver no cache do inode, uma rotina relacionada ao sistema de arquivos deverá ser chamada para ler o inode apropriado. O ato de ler o inode faz com que ele seja colocado no cache do inode, e futuros acessos ao inode fazem com que ele permaneça no cache. Os inodes VFS menos usados ​​são removidos desse cache.

veja fs/inode.c

Todos os sistemas de arquivos Linux usam um cache de buffer comum para armazenar em cache o buffer de dados do dispositivo subjacente, o que pode acelerar o acesso ao dispositivo físico que armazena o sistema de arquivos, acelerando assim o acesso ao sistema de arquivos. Esse cache de buffer é independente do sistema de arquivos e está integrado ao mecanismo do kernel do Linux para alocar, ler e gravar buffers de dados. Há benefícios especiais em ter um sistema de arquivos Linux independente da mídia subjacente e dos drivers de dispositivo de suporte. Todos os dispositivos estruturados em blocos são registrados no kernel do Linux e apresentam uma interface uniforme, baseada em blocos, geralmente assíncrona. Isso é verdade mesmo para dispositivos de bloco relativamente complexos, como dispositivos SCSI. Quando o sistema de arquivos real lê dados do disco físico subjacente, ele faz com que os drivers de dispositivo de bloco leiam blocos físicos do dispositivo que eles controlam. O cache de buffer está integrado nesta interface de dispositivo de bloco. Quando o sistema de arquivos lê blocos, eles são armazenados em um cache de buffer global compartilhado por todos os sistemas de arquivos e pelo kernel Linux. Os buffers são marcados com seu número de bloco e um identificador exclusivo do dispositivo que está sendo lido. Portanto, se os mesmos dados forem necessários com frequência, eles serão lidos do cache do buffer em vez do disco (o que levará mais tempo). Alguns dispositivos suportam leitura antecipada, onde blocos de dados são lidos antecipadamente para possíveis leituras posteriores.

Veja fs/buffer.c

O VFS também mantém um cache de pesquisa de diretório, para que o inode de um diretório usado com frequência possa ser encontrado rapidamente. Como experiência, tente listar um diretório que você não listou recentemente. Na primeira vez que você fizer uma lista, você notará uma breve pausa, e na segunda vez que fizer uma lista, os resultados sairão imediatamente. O cache de diretório em si não armazena os inodes no diretório, que é de responsabilidade do cache de inode.O cache de diretório armazena apenas os nomes completos dos itens do diretório e seus números de inode.

Veja fs/dcache.c

9.2.1 O Superbloco VFS

Cada sistema de arquivos montado é representado por um superbloco VFS. Entre outras informações, o superblock VFS inclui:

Veja include/linux/fs.h

Dispositivo Este é o identificador de dispositivo do dispositivo de bloco que contém o sistema de arquivos. Por exemplo, /dev/hda1, o primeiro disco IDE no sistema, possui um identificador de dispositivo de 0x301

Ponteiros de inode O ponteiro de inode montado aponta para o primeiro inode do sistema de arquivos. O ponteiro inode coberto aponta para o inode do diretório no qual o sistema de arquivos está montado. Para o sistema de arquivos raiz, não há ponteiro coberto em seu superbloco VFS.

Blocksize Tamanho do bloco do sistema de arquivos em bytes, por exemplo 1024 bytes.

Operações de superbloco Ponteiro para um conjunto de rotinas de superbloco para este sistema de arquivos. Entre outros tipos, o VFS usa essas rotinas para ler e escrever inodes e superblocks

Tipo de sistema de arquivos Um ponteiro para a estrutura de dados file_system_type para este sistema de arquivos montado

Sistema de arquivos Especifique um ponteiro para as informações exigidas por este sistema de arquivos

9.2.2 O Inode VFS

Como o sistema de arquivos EXT2, cada arquivo, diretório, etc. no VFS é representado por um e apenas um inode VFS. As informações em cada inode VFS são recuperadas do sistema de arquivos subjacente usando rotinas específicas do sistema de arquivos. Os inodes VFS existem apenas na memória principal e são mantidos no cache de inodes VFS enquanto forem úteis para o sistema. Entre outras informações, o inode VFS inclui alguns campos:

Veja include/linux/fs.h

device O identificador do dispositivo que contém este arquivo (ou outra entidade representada por este inode VFS).

Número do inode O número deste inode, exclusivo neste sistema de arquivos. A combinação do número do dispositivo e do inode é exclusiva em todo o sistema de arquivos virtual.

Modo Como EXT2, este campo descreve o que este inode VFS representa e os direitos de acesso a ele.

Identificador do proprietário dos IDs de usuário

Tempos Criado, modificado e escrito

Tamanho do bloco O tamanho em bytes do bloco deste arquivo, por exemplo, 1024 bytes

Operações de inode Ponteiro para um conjunto de endereços de rotina. Essas rotinas estão relacionadas ao sistema de arquivos e realizam operações neste inode, como truncar o arquivo representado por este inode

Contagem O número de componentes do sistema atualmente usando este inode VFS. Contagem 0 significa que o inode é livre e pode ser descartado ou reutilizado.

Lock Este campo é usado para bloquear o inode VFS. Por exemplo, ao lê-lo a partir do sistema de arquivos

Dirty mostra se esse inode VFS foi gravado e, em caso afirmativo, o sistema de arquivos subjacente precisa ser atualizado.

Informações específicas do sistema de arquivos

9.2.3 Registrando os Sistemas de Arquivos

Quando você compila o kernel do Linux, é perguntado se você precisa de todos os sistemas de arquivos suportados. Quando o kernel é construído, o código de inicialização do sistema de arquivos inclui chamadas para todas as rotinas de inicialização do sistema de arquivos embutidas. Os sistemas de arquivos Linux também podem ser construídos como módulos, caso em que podem ser carregados sob demanda ou manualmente usando insmod. Quando home está em um módulo do sistema de arquivos, ele se registra no kernel e, quando descarregado, cancela o registro. A rotina de inicialização de cada sistema de arquivos se registra no sistema de arquivos virtual e é representada por uma estrutura de dados file_system_type, que contém o nome do sistema de arquivos e um ponteiro para sua rotina de leitura de superbloco VFS. A Figura 9.5 mostra que a estrutura de dados file_system_type é colocada em uma lista apontada pelo ponteiro file_systems. Cada estrutura de dados file_system_type inclui as seguintes informações:

Veja fs/filesystems.c sys_setup()

veja include/linux/fs.h file_system_type

Rotina de leitura do Superblock Esta rotina é chamada pelo VFS quando é montada uma instância deste sistema de ficheiros

Nome do sistema de arquivos O nome do sistema de arquivos, como ext2

Dispositivo necessário Este sistema de arquivos precisa de um suporte de dispositivo? Nem todos os sistemas de arquivos requerem um dispositivo para mantê-los. Por exemplo, o sistema de arquivos /proc não requer um dispositivo de bloco

Você pode verificar /proc/filesystems para ver quais sistemas de arquivos estão registrados, por exemplo:

ramal2

processo nodev

iso9660

9.2.4 Montando um sistema de arquivos

Quando o superusuário tenta montar um sistema de arquivos, o kernel Linux deve primeiro validar os parâmetros passados ​​na chamada do sistema. Embora o mount possa realizar algumas verificações básicas, ele não sabe se a compilação do núcleo é um sistema de arquivos suportável ou se o ponto de montagem proposto existe. Considere o seguinte comando de montagem:

$ mount –t iso9660 –o ro /dev/cdrom /mnt/cdrom

O comando mount passa três informações para o núcleo: o nome do sistema de arquivos, o dispositivo de bloco físico que contém o sistema de arquivos e onde na topologia do sistema de arquivos existente o novo sistema de arquivos deve ser montado.

A primeira coisa que um sistema de arquivos virtual faz é encontrar o sistema de arquivos. Ele primeiro examina cada estrutura de dados file_system_type na lista apontada por file_systems, examinando todos os sistemas de arquivos conhecidos. Se encontrar um nome correspondente, ele vai até que o núcleo suporte o tipo de sistema de arquivos e obtenha o endereço das rotinas relacionadas ao sistema de arquivos para ler o superblock do sistema de arquivos. Se não encontrar um nome de sistema de arquivos correspondente, ele pode continuar se o kernel tiver suporte embutido para carregamento sob demanda de módulos principais (consulte a Seção 12). Nesse caso, o kernel solicitará ao daemon do kernel que carregue o módulo do sistema de arquivos apropriado antes de continuar.

veja fs/super.c do_mount()

Veja fs/super.c get_fs_type()

Na segunda etapa, se o dispositivo físico passado pela montagem não tiver sido montado, deve-se encontrar o inode VFS do diretório que se tornará o ponto de montagem do novo sistema de arquivos. O inode VFS pode estar no cache do inode ou deve ser lido a partir do dispositivo de bloco do sistema de arquivos que suporta este ponto de montagem. Assim que o inode for encontrado, verifique se é um diretório e nenhum outro sistema de arquivos está montado lá. O mesmo diretório não pode ser usado como ponto de montagem para mais de um sistema de arquivos.

Neste ponto, o código de montagem VFS deve alocar um superbloco VFS e passar as informações de montagem para a rotina de leitura de superbloco do sistema de arquivos. Todos os superblocos VFS no sistema são armazenados na tabela de vetores super_blocks que consiste na estrutura de dados super_block, e uma estrutura deve ser alocada para esta instalação. A rotina de leitura de superbloco deve preencher os campos do superbloco VFS com base nas informações que lê do dispositivo físico. Para o sistema de arquivos EXT2, o mapeamento ou conversão dessas informações é bastante fácil, basta ler o superblock EXT2 e preenchê-lo no superblock VFS. Para outros sistemas de arquivos, como o sistema de arquivos MS DOS, não é uma tarefa tão simples. Independentemente do sistema de arquivos, preencher o superblock VFS significa que as informações que descrevem esse sistema de arquivos devem ser lidas do dispositivo de bloco que o suporta. O comando mount falhará se o dispositivo de bloco não puder ser lido ou se não contiver esse tipo de sistema de arquivos.

Cada sistema de arquivos montado é descrito por uma estrutura de dados vfsmount, veja a Figura 9.6. Eles são enfileirados em uma lista apontada por vfsmntlist. Outro ponteiro, vfsmnttail, aponta para a última entrada na lista, e o ponteiro mru_vfsmnt aponta para o sistema de arquivos usado mais recentemente. Cada estrutura vfsmount inclui o número do dispositivo do bloco que armazena o sistema de arquivos, o diretório onde o sistema de arquivos está montado e um ponteiro para o superblock VFS alocado quando o sistema de arquivos foi montado. O superbloco VFS aponta para a estrutura de dados file_system_type desse tipo de sistema de arquivos e o inode raiz desse sistema de arquivos. Este inode reside no cache de inode do VFS durante a montagem do sistema de arquivos.

Veja fs/super.c add_vfsmnt()

9.2.5 Localizando um arquivo no sistema de arquivos virtual

Para localizar o inode VFS de um arquivo no sistema de arquivos virtual, o VFS deve nomear sequencialmente, um diretório por vez, para localizar o inode VFS para cada diretório intermediário. Cada pesquisa de diretório chama a rotina de pesquisa associada ao sistema de arquivos (endereço colocado no inode VFS que representa o diretório pai). Como sempre há o inode raiz do sistema de arquivos no superbloco VFS do sistema de arquivos, indicado por um ponteiro no superbloco, todo o processo pode continuar. Toda vez que um inode no sistema de arquivos real é consultado, o cache do diretório para este diretório é verificado. Se o cache do diretório não tiver essa entrada, o sistema de arquivos real obtém o inode VFS do sistema de arquivos subjacente ou do cache do inode.

9.2.6 Criando um arquivo no sistema de arquivos virtual

9.2.7 Desmontando um sistema de arquivos

Minhas pastas de trabalho geralmente descrevem a montagem como o inverso da desmontagem, mas é um pouco diferente para desmontar sistemas de arquivos. Um sistema de arquivos não pode ser desmontado se algo no sistema estiver usando um arquivo no sistema de arquivos. Por exemplo, se um processo estiver usando o diretório /mnt/cdrom ou seus subdiretórios, você não poderá desmontar /mnt/cdrom. Se algo estiver usando o sistema de arquivos que está sendo desmontado, seu inode VFS estará no cache do inode VFS. O código de descarregamento examina toda a lista de inodes procurando por inodes que pertençam ao dispositivo ocupado por este sistema de arquivos. Se o superbloco VFS do sistema de arquivos montado estiver sujo, ou seja, foi modificado, ele deve ser gravado de volta no sistema de arquivos no disco. Depois de gravar no disco, a memória ocupada por esse superbloco VFS é devolvida ao pool de memória livre do kernel. Finalmente, essa estrutura de dados vmsmount montada também é removida do vfsmntlist e liberada.

Veja fs/super.c do_umount()

Veja fs/super.c remove_vfsmnt()

9.2.8 O Cache de Inode VFS

Ao percorrer sistemas de arquivos montados, seus inodes VFS são constantemente lidos e, às vezes, gravados. O sistema de arquivos virtual mantém um cache de inode, que é usado para acelerar o acesso a todos os sistemas de arquivos montados. Cada vez que um inode VFS é lido do cache do inode, o sistema pode salvar o acesso aos dispositivos físicos.

veja fs/inode.c

O cache de inodes VFS é implementado na forma de uma tabela de hash e as entradas são ponteiros para a lista de inodes VFS com o mesmo valor de hash. O valor de hash de um inode é calculado a partir de seu número de inode e do número do dispositivo físico subjacente que contém o sistema de arquivos. Sempre que o sistema de arquivos virtual precisa acessar um inode, ele primeiro procura no cache do inode VFS. Para pesquisar um inode na tabela de hash de inode, o sistema primeiro calcula seu valor de hash e, em seguida, o usa como um índice na tabela de hash de inode. Isso fornece um ponteiro para uma lista de inodes com o mesmo valor de hash. Em seguida, ele lê todos os inodes de uma só vez até encontrar um inode com o mesmo número de inode e o mesmo identificador de dispositivo que o inode que estava procurando.

Se o inode puder ser encontrado no cache, sua contagem será incrementada, indicando que ele possui outro usuário, e o acesso ao sistema de arquivos continuará. Caso contrário, um inode VFS livre deve ser encontrado para que o sistema de arquivos leia o inode na memória. Como obter um inode gratuito, o VFS possui uma gama de opções. Se o sistema pode alocar mais inodes VFS, ele o faz: aloca páginas principais e as divide em novos inodes livres, que são colocados na lista de inodes. Todos os inodes VFS no sistema também estão em uma lista apontada por first_inode além da tabela de hash do inode. Se o sistema já possui todos os inodes permitidos, ele deve encontrar um inode que possa ser reutilizado. Bons candidatos são inodes que têm uma contagem de 0: isso significa que o sistema não os está usando no momento. Os inodes VFS realmente importantes, como o inode raiz do sistema de arquivos, já têm um uso maior que 0, portanto nunca são escolhidos para reutilização. Uma vez que um candidato a reutilização é localizado, ele é limpo. Este inode VFS pode estar sujo, nesse caso o sistema deve esperar que ele seja desbloqueado antes de continuar. Os candidatos a este inode VFS devem ser limpos antes da reutilização.

Enquanto um novo inode VFS foi encontrado, uma rotina relacionada ao sistema de arquivos deve ser chamada para preencher o inode com informações do envenenamento do sistema de arquivos real subjacente. Quando ele é preenchido, este novo inode VFS tem um uso de 1 e está bloqueado, então nenhum outro processo pode acessá-lo até que seja preenchido com informações válidas.

Para obter os inodes VFS de que realmente precisa, o sistema de arquivos pode precisar acessar alguns outros inodes. Isso acontece quando você lê um diretório: apenas o inode do diretório final é necessário, mas o inode do diretório intermediário também deve ser lido. Quando o cache de inode do VFS está em uso e é preenchido, os inodes menos usados ​​são descartados e os inodes mais usados ​​permanecem no cache.

9.2.9 O Cache do Diretório

Para acelerar o acesso a diretórios usados ​​com frequência, o VFS mantém um cache de entradas de diretório. Quando o sistema de arquivos real procura por diretórios, os detalhes desses diretórios são adicionados ao cache do diretório. Na próxima vez que você procurar o mesmo diretório, como listar ou abrir um arquivo nele, ele será encontrado no cache do diretório. Somente entradas de diretório curtas (até 15 caracteres) são armazenadas em cache, mas isso é razoável, pois nomes de diretório mais curtos são os mais usados. Por exemplo: /usr/X11R6/bin é acessado com muita frequência quando o servidor X é iniciado.

Veja fs/dcache.c

O cache de diretório contém uma tabela de hash, cada entrada aponta para uma lista de entradas de cache de diretório com o mesmo valor de hash. A função Hash usa o número do dispositivo e o nome do diretório do dispositivo que contém este sistema de arquivos para calcular o deslocamento ou índice na tabela de hash. Ele permite encontrar rapidamente entradas de diretório em cache. Um cache é inútil se demorar muito para procurar ou não for encontrado.

Para manter esses caches válidos e atualizados, o VFS mantém uma lista de entradas de cache de diretório LRU (menos recentemente usadas). Quando uma entrada de diretório é colocada no cache pela primeira vez, ou seja, quando é consultada pela primeira vez, ela é adicionada ao final da lista LRU de primeiro nível. Para um cache completo, isso remove as entradas que existem na frente da lista LRU. Quando a entrada de diretório é acessada novamente, ela é movida para o final da segunda lista de cache LRU. Novamente, desta vez ele remove a entrada do diretório de cache L2 na frente da lista de cache LRU no segundo nível. Não há problema em remover entradas de diretório das listas LRU primária e secundária. Essas entradas estão na frente da lista apenas porque não foram acessadas recentemente. Se acessados, eles estarão no final da lista. As entradas na lista de cache L2 LRU são mais seguras do que as entradas na lista de cache L1 LRU. Porque essas entradas não são apenas pesquisadas, mas uma vez referenciadas repetidamente.

9.3 O Cache de Buffer

Ao usar sistemas de arquivos montados, eles geram um grande número de solicitações para ler e gravar blocos de dados em dispositivos de bloco. Todas as solicitações de leitura e gravação de dados de bloco são passadas para o driver de dispositivo na forma de uma estrutura de dados buffer_head por meio de chamadas de rotina de núcleo padrão. Essas estruturas de dados fornecem todas as informações de que um driver de dispositivo precisa: o identificador de dispositivo identifica exclusivamente o dispositivo e o número do bloco informa ao driver qual bloco ler. Todos os dispositivos de bloco são vistos como uma combinação linear de blocos do mesmo tamanho. Para acelerar o acesso a dispositivos de blocos físicos, o Linux mantém um cache de buffers de blocos. Todos os buffers de bloco no sistema são armazenados nesse cache de buffer, mesmo os novos buffers não utilizados. Esse buffer é compartilhado por todos os dispositivos de bloco físico: a qualquer momento há muitos buffers de bloco no buffer, que podem pertencer a qualquer um dos dispositivos de bloco do sistema, geralmente com estados diferentes. Isso economiza o acesso do sistema a dispositivos físicos se houver dados válidos no cache do buffer. Qualquer buffer de bloco usado para ler/gravar dados de/para o dispositivo de bloco vai para esse cache de buffer. Com o tempo, ele pode ser removido desse buffer para dar espaço a outros buffers mais necessários, ou pode permanecer no buffer se for acessado com frequência.

Os buffers de bloco neste buffer são identificados exclusivamente pelo identificador do dispositivo e pelo número do bloco ao qual esse buffer pertence. O cache de buffer consiste em duas partes funcionais. A primeira parte é uma lista de buffers de bloco livres. Uma lista para cada buffer do mesmo tamanho (que o sistema pode suportar). Os buffers de bloco livres do sistema são enfileirados nessas listas quando são criados ou descartados pela primeira vez. Os tamanhos de buffer atualmente suportados são 512, 1024, 2048, 4096 e 8192 bytes. A segunda parte funcional é o próprio cache. Esta é uma tabela de hash, uma tabela vetorial de ponteiros usada para vincular buffers com o mesmo índice de hash. O índice Hash é gerado a partir do identificador do dispositivo e do número do bloco ao qual o bloco de dados pertence. A Figura 9.7 mostra essa tabela de hash e algumas entradas. O buffer de bloco está em uma das listas livres ou no cache de buffer. Quando eles estão no cache de buffer, eles também são enfileirados na lista LRU. Uma lista de LRUs por tipo de buffer que o sistema usa para executar operações em um tipo de buffer. Por exemplo, gravando um buffer com novos dados no disco. O tipo de buffer reflete seu estado e o Linux atualmente suporta os seguintes tipos:

clean 未使用,新的缓冲区(buffer)
locked 锁定的缓冲区,等待被写入
dirty 脏的缓冲区。包含新的有效的数据,将被写到磁盘,但是直到现在还没有调度到写
shared 共享的缓冲区
unshared 曾经共享的缓冲区,但是现在没有共享

Sempre que o sistema de arquivos precisa ler um buffer de seu dispositivo físico subjacente, ele tenta obter um bloco do cache de buffer. Se ele não puder obter um buffer do cache de buffer, ele pegará um buffer limpo da lista livre de tamanho apropriado e esse novo buffer entrará no cache de buffer. Se o buffer necessário já estiver no cache de buffer, ele pode ou não estar atualizado. Se não estiver atualizado, ou se for um novo buffer de bloco, o sistema de arquivos deve solicitar ao driver de dispositivo que leia o bloco apropriado do disco.

Como todos os caches, o cache de buffer deve ser mantido de modo que funcione com eficiência e distribua as entradas de cache de maneira justa entre os dispositivos de bloco que usam o cache de buffer. O Linux usa o daemon principal bdflush para realizar muitas limpezas nesse buffer, mas algumas outras coisas são feitas automaticamente no processo de uso do buffer.

9.3.1 O bdflush Kernel Daemon (o daemon principal bdflsuh)

O daemon de núcleo bdflush é um daemon de núcleo simples que fornece respostas dinâmicas para sistemas com muitos buffers sujos (buffers contendo dados que devem ser gravados no disco simultaneamente). Ele começa como um thread principal quando o sistema inicializa, o que é bastante confuso, e chama a si mesmo de "kflushd", que é o nome que você verá quando usar ps para exibir os processos no sistema. Esse processo fica suspenso a maior parte do tempo, esperando que o número de buffers sujos no sistema cresça até ficar muito grande. Quando os buffers são alocados e liberados, verifique o número de buffers sujos no sistema e desperte o bdflush. O limite padrão é 60%, mas o bdflush também será ativado se o sistema precisar de muitos buffers. Este valor pode ser verificado e definido com o comando update:

#update –d
bdflush version 1.4
0: 60 Max fraction of LRU list to examine for dirty blocks
1: 500 Max number of dirty blocks to write each time bdflush activated
2: 64 Num of clean buffers to be loaded onto free list by refill_freelist
3: 256 Dirty block threshold for activating bdflush in refill_freelist
4: 15 Percentage of cache to scan for free clusters
5: 3000 Time for data buffers to age before flushing
6: 500 Time for non-data (dir, bitmap, etc) buffers to age before flushing
7: 1884 Time buffer cache load average constant
8: 2 LAV ratio (used to determine threshold for buffer fratricide).

Sempre que os dados são gravados e se tornam um buffer sujo, todos os buffers sujos são vinculados na lista BUF_DIRTY LRU e o bdflush tentará gravar um número razoável de buffers em seu disco. Este número também pode ser verificado e definido com o comando update, o padrão é 500 (veja o exemplo acima).

9.3.2 O processo de atualização

O comando de atualização não é apenas um comando, é também um daemon. Ao executar como superusuário (inicialização do sistema), ele grava periodicamente todos os buffers sujos antigos no disco. Ele executa essas tarefas chamando rotinas de serviço do sistema, mais ou menos o mesmo que bdflush. Quando um buffer sujo é gerado, ele é marcado com a hora do sistema em que deve ser gravado em seu próprio disco. Cada vez que a atualização é executada, ela examina todos os buffers sujos no sistema, procurando por buffers com tempos de gravação expirados. Cada buffer expirado é gravado no disco.

Veja fs/buffer.c sys_bdflush()

9.3.3 O Sistema de Arquivos /proc

O sistema de arquivos /proc realmente incorpora os recursos do sistema de arquivos virtuais Linux. Na verdade, ele não existe (outro truque do Linux), nem /proc nem seus subdiretórios e os arquivos neles existem. Mas por que você pode cat /proc/devices? O sistema de arquivos /proc, como um sistema de arquivos real, também se registra no sistema de arquivos virtual, mas o sistema de arquivos /proc só usa os inodes no núcleo quando seus arquivos e diretórios são abertos e o VFS executa chamadas que solicitam seus inodes. esses arquivos e diretórios. Por exemplo, o arquivo /proc/devices do kernel é gerado a partir das estruturas de dados do kernel que descrevem seus dispositivos.

O sistema de arquivos /proc representa uma janela legível pelo usuário no espaço de trabalho interno do kernel. Alguns subsistemas Linux, como os módulos do kernel Linux descritos na Seção 12, criam entradas no sistema de arquivos /proc.

9.3.4 Arquivos Especiais do Dispositivo

O linux, como todas as versões do Unix, representa seus dispositivos de hardware como arquivos especiais. Por exemplo, /dev/null é um dispositivo vazio. Um arquivo de dispositivo não ocupa nenhum espaço de dados no sistema de arquivos, é apenas um ponto de acesso para drivers de dispositivo. Tanto o sistema de arquivos EXT2 quanto o VFS do Linux tratam os arquivos de dispositivo como um tipo especial de inode. Existem dois tipos de arquivos de dispositivo: arquivos especiais de caractere e de bloco. Dentro do próprio núcleo, os drivers de dispositivo implementam as operações básicas dos arquivos: você pode abrir, fechar e assim por diante. Os dispositivos de caractere permitem operações de E/S no modo de caractere, enquanto os dispositivos de bloco exigem que todas as E/S passem pelo cache de buffer. Quando uma solicitação de E/S é executada em um arquivo de dispositivo, ela é encaminhada ao driver de dispositivo apropriado no sistema. Normalmente, este não é um driver de dispositivo real, mas um driver de pseudodispositivo para subsistemas, como a camada de driver de dispositivo SCSI. Os arquivos de dispositivo são referenciados por um número de dispositivo principal (que identifica o tipo de dispositivo) e um tipo secundário (que identifica uma unidade ou instância de um tipo principal). Por exemplo, para o disco IDE no primeiro controlador IDE no sistema, o número do dispositivo principal é 3 e o número do dispositivo secundário da primeira partição do disco IDE deve ser 1, portanto, ls -l /dev/hda1 outputs

$ brw-rw---- 1 disco raiz 3, 1 nov 24 15:09 /dev/hda1

Veja todos os principais números do Linux em /include/linux/major.h

No kernel, cada dispositivo é descrito exclusivamente por um tipo de dados kdev_t. Este tipo tem dois bytes de comprimento, o primeiro contém o número menor do dispositivo e o segundo contém o número principal. O dispositivo IDE acima é salvo como 0x0301 no núcleo. Um inode EXT2 representando um dispositivo de bloco ou caractere coloca os números de dispositivo principal e secundário do dispositivo em seu primeiro ponteiro de bloco direto. Quando é lido pelo VFS, o campo I_rdev da estrutura de dados do inode VFS que o representa é definido para o identificador de dispositivo correto.

Veja include/linux/kdev_t.h

10. Redes

Linux e rede são quase sinônimos. Na verdade, o Linux é um produto da Internet ou WWW. Seus desenvolvedores e usuários usam a web para trocar informações, ideias e códigos, e o próprio Linux é frequentemente usado para dar suporte às necessidades de rede de algumas organizações. Esta subseção descreve como o Linux suporta os protocolos de rede conhecidos coletivamente como TCP/IP.

O protocolo TCP/IP foi projetado para suportar a comunicação entre computadores conectados à ARPANET. ARPANET é uma rede de pesquisa americana financiada pelo governo dos EUA. A ARPANET é precursora de alguns conceitos de rede, como troca de mensagens e camadas de protocolo, que permitem que um protocolo utilize serviços fornecidos por outros protocolos. A ARPANET saiu em 1988, mas seus sucessores (NSF NET e Internet) cresceram ainda mais. A World Wide Web como agora é conhecida foi desenvolvida em ARPANET, que é suportada pelo protocolo TCP/IP. Unix é muito usado na ARPANET, a primeira versão de rede do Unix lançada foi 4.3BSD. A implementação de rede do Linux é baseada no modelo 4.3BSD, que suporta soquetes BSD (e algumas extensões) e toda a gama de funções de rede TCP/IP. Essa interface de programação foi escolhida por causa de sua popularidade e capacidade de portar programas entre Linux e outras plataformas Unix.

10.1 Uma Visão Geral da Rede TCP/IP

Esta seção fornece uma visão geral dos principais princípios da rede TCP/IP. Esta não é uma descrição exaustiva. Para uma descrição mais detalhada, leia o Livro de Referência 10 (Apêndice).

Em uma rede IP, cada máquina recebe um endereço IP, que é um número de 32 bits que identifica exclusivamente essa máquina. A WWW é uma rede IP muito grande e crescente, e cada máquina conectada a ela recebe um endereço IP exclusivo. Os endereços IP são representados por quatro números separados por pontos, por exemplo, 16.42.0.9. Na verdade, um endereço IP é dividido em duas partes: o endereço de rede e o endereço do host. Esses endereços podem variar em tamanho (dimensões) (existem várias classes de endereços IP), no caso de 16.42.0.9, o endereço de rede é 16,42 e o endereço de host é 0,9. Os endereços de host podem ser divididos em sub-redes e endereços de host. Tomando 16.42.0.9 como exemplo novamente, o endereço de sub-rede pode ser 16.42.0 e o endereço do host é 16.42.0.9. A divisão adicional de endereços IP permite que as organizações dividam suas próprias redes. Por exemplo, supondo que 16.42 seja o endereço de rede da ACME Computer Corporation, 16.42.0 poderia ser a sub-rede 0 e 16.42.1 poderia ser a sub-rede 1. Essas sub-redes podem estar em prédios separados, talvez conectadas por linhas telefônicas dedicadas ou mesmo por micro-ondas. Os endereços IP são atribuídos pelos administradores de rede e o uso de sub-redes IP é uma boa maneira de descentralizar as tarefas de gerenciamento de rede. Os administradores de sub-redes IP são livres para atribuir endereços IP dentro de suas próprias sub-redes.

No entanto, geralmente os endereços IP são difíceis de lembrar, enquanto os nomes são mais fáceis de lembrar. Linux.acme.com é mais lembrado do que 16.42.0.9. Um mecanismo deve ser usado para converter nomes de rede em endereços IP. Esses nomes podem ser armazenados estaticamente no arquivo /etc/hosts ou fazendo com que o Linux consulte um DNS do Servidor de Nomes Distribuídos para resolver os nomes. Neste caso, o localhost deve conhecer os endereços IP de um ou mais servidores DNS, especificados em /etc/resolv.conf.

Sempre que você se conectar a outra máquina, como ler uma página da Web, use seu endereço IP para trocar dados com essa máquina. Esses dados são incluídos nos pacotes IP, cada um dos quais possui um cabeçalho IP (incluindo os endereços IP das máquinas de origem e destino, uma soma de verificação e outras informações úteis. Essa soma de verificação é derivada do pacote IP). os dados da mensagem IP, que permite ao receptor da mensagem IP determinar se a mensagem IP foi danificada durante a transmissão (talvez uma linha telefônica ruidosa). Os dados transmitidos pelo aplicativo podem ser divididos em pedaços menores e mais fáceis de O tamanho dos datagramas IP varia dependendo do meio conectado: Os pacotes Ethernet são geralmente maiores que os pacotes PPP. O host de destino deve remontar esses datagramas antes que eles possam ser entregues ao programa receptor. Se você passar um equivalente Acesse uma página da web que inclui um grande número de imagens gráficas em uma conexão serial lenta, e você pode ver graficamente a desagregação e reorganização dos dados.

Hosts conectados à mesma sub-rede IP podem enviar pacotes IP diretamente entre si, enquanto outros pacotes IP devem ser enviados por meio de um host especial (gateway). Os gateways (ou roteadores) são conectados a mais de uma sub-rede e reenviam os pacotes IP recebidos de uma sub-rede para outra. Por exemplo, se as sub-redes 16.42.1.0 e 16.42.0.0 estiverem conectadas por meio de um gateway, todos os pacotes enviados da sub-rede 0 para a sub-rede 1 deverão ser enviados ao gateway antes de serem encaminhados. O host local estabelece uma tabela de roteamento para que possa enviar os pacotes IP a serem encaminhados para a máquina correta. Para cada destino IP, há uma entrada na tabela de roteamento que informa ao Linux para qual host enviar o pacote IP antes de chegar ao destino. Essas tabelas de roteamento são dinâmicas e mudam constantemente conforme os aplicativos usam a rede e a topologia da rede muda.

O protocolo IP é um protocolo da camada de transporte usado por outros protocolos para transportar seus dados. O Transmission Control Protocol (TCP) é um protocolo confiável de ponta a ponta que usa IP para transmitir e receber seus pacotes. Assim como os pacotes IP têm seus próprios cabeçalhos, o TCP também tem seus próprios cabeçalhos. O TCP é um protocolo orientado à conexão, dois aplicativos de rede são conectados por uma conexão virtual e pode até haver muitas sub-redes, gateways e roteadores entre eles. O TCP transmite e recebe dados de forma confiável entre dois aplicativos e garante que não haverá perda ou dados duplicados. Quando o TCP usa o IP para transmitir suas mensagens, os dados contidos na mensagem IP são a própria mensagem TCP. A camada IP de cada host em comunicação é responsável por transmitir e receber pacotes IP. O User Datagram Protocol (UDP) também usa a camada IP para transmitir suas mensagens, mas ao contrário do TCP, o UDP não é um protocolo confiável, apenas fornece serviços de datagramas. Outros protocolos também podem usar IP, o que significa que quando um pacote IP é recebido, a camada IP receptora deve saber para qual protocolo da camada superior passar os dados contidos no pacote IP. Para isso, o cabeçalho de cada pacote IP possui um byte que contém um identificador de protocolo. Quando o TCP solicita que a camada IP transmita um pacote IP, o cabeçalho do pacote IP indica que ele contém um pacote TCP. A camada IP receptora, usa este identificador de protocolo para decidir qual protocolo passar os dados recebidos até, neste caso, a camada TCP. Quando os aplicativos se comunicam via TCP/IP, eles devem especificar não apenas o endereço IP do destino, mas também o endereço da porta do aplicativo de destino. Um endereço de porta identifica exclusivamente um aplicativo e os aplicativos da Web padrão usam endereços de porta padrão: por exemplo, os servidores da Web usam a porta 80. Esses endereços de porta registrados podem ser encontrados em /etc/services.

As camadas de protocolo não param apenas em TCP, UDP e IP. O próprio protocolo IP usa muitas mídias físicas diferentes e outros hosts IP para transmitir pacotes IP. A própria mídia também pode adicionar seus próprios cabeçalhos de protocolo. Exemplos disso são a camada Ethernet, PPP e SLIP. Uma Ethernet permite que muitos hosts sejam conectados simultaneamente em um único cabo físico. Cada quadro Ethernet transmitido pode ser visto por todos os hosts conectados, portanto, cada dispositivo Ethernet possui um endereço exclusivo. Cada quadro Ethernet enviado para esse endereço será recebido pelo host nesse endereço e ignorado por outros hosts conectados à rede. Esse endereço exclusivo é incorporado ao dispositivo quando cada dispositivo Ethernet é fabricado, geralmente armazenado no SROM da placa Ethernet. Um endereço de éter tem 6 bytes de comprimento, por exemplo, pode ser 08-00-2b-00-49-4A. Alguns endereços Ethernet são reservados para uso multicast e os quadros Ethernet enviados com esses endereços de destino são recebidos por todos os hosts da rede. Como os quadros Ethernet podem transportar muitos protocolos diferentes (como dados), como pacotes IP, todos eles contêm um identificador de protocolo no cabeçalho. Desta forma, a camada Ethernet pode receber corretamente os pacotes IP e transmitir os dados para a camada IP.

Para transmitir pacotes IP em vários protocolos de conexão, como Ethernet, a camada IP deve descobrir o endereço Ethernet do host IP. Isso ocorre porque os endereços IP são apenas um conceito de endereçamento e os próprios dispositivos Ethernet têm seus próprios endereços físicos. Os endereços IP podem ser atribuídos e reatribuídos conforme necessário pelo administrador da rede, enquanto o hardware da rede responde apenas aos quadros Ethernet com seu próprio endereço físico ou um endereço multicast especial (que todas as máquinas devem receber). O Linux usa o Address Resolution Protocol (ARP) para permitir que as máquinas traduzam endereços IP em endereços de hardware reais, como endereços Ethernet. Para obter o endereço de hardware ao qual um endereço IP está associado, um host envia um pacote de solicitação ARP, contendo o endereço IP que deseja traduzir, para um endereço multicast que pode ser recebido por todos os pontos da rede. O host de destino com esse endereço IP responde com uma resposta ARP, que inclui seu endereço de hardware físico. O APR não se limita apenas a dispositivos Ethernet, mas também pode resolver endereços IP de outras mídias físicas, como FDDI. Dispositivos que não podem ARP são marcados para que o Linux não precise tentar ARP neles. Há também uma função oposta, ARP reverso ou RARP, que converte endereços físicos em endereços IP. Isso é usado para gateways, respondendo a solicitações ARP para endereços IP que representam a rede remota.

10.2 As Camadas de Rede TCP/IP do Linux

Assim como os protocolos de rede, a Figura 10.2 mostra a implementação do Linux da família de endereços de protocolo da Internet como uma série de camadas de software conectadas. Os soquetes BSD são suportados pelo software de gerenciamento de soquete genérico que está associado apenas aos soquetes BSD. Apoiá-los é a camada de soquete INET, que gerencia os terminais de comunicação para os protocolos baseados em IP TCP e UDP. O UDP é um protocolo sem conexão, enquanto o TCP é um protocolo confiável de ponta a ponta. Ao enviar pacotes UDP, o Linux não sabe e não se importa se eles chegam ao destino com segurança. Os pacotes TCP são numerados e cada extremidade de uma conexão TCP garante que os dados transmitidos sejam recebidos corretamente. A camada IP inclui a implementação de código do Protocolo de Internet. Este código adiciona um cabeçalho IP aos dados transmitidos e sabe como encaminhar pacotes IP de entrada para a camada TCP ou UDP. Abaixo da camada IP, suportando a rede Linux estão os dispositivos de rede, como PPP e Ethernet. Os dispositivos de rede nem sempre se comportam como dispositivos físicos: alguns deles, como dispositivos de loopback, são dispositivos puramente de software. Ao contrário dos dispositivos Linux padrão criados com o comando mknod, os dispositivos de rede não aparecem até que o software subjacente os encontre e inicialize. Você só poderá ver o arquivo de dispositivo /dev/eth0 depois de construir um kernel contendo os drivers de dispositivo ethereum apropriados. O protocolo ARP fica entre a camada IP e os protocolos que suportam ARP.

10.3 A Interface de Soquete BSD

Esta é uma interface geral que não apenas suporta várias formas de rede, mas também um mecanismo de comunicação entre processos. Um soquete descreve uma extremidade de uma conexão de comunicação e dois processos de comunicação terão, cada um, um soquete descrevendo sua própria parte da conexão de comunicação entre eles. Os soquetes podem ser imaginados como uma forma especial de pipes, mas, diferentemente dos pipes, os soquetes não têm limite para a quantidade de dados que podem conter. Linux suporta vários tipos de sockets, essas classes são chamadas de famílias de endereços. Isso ocorre porque cada classe tem seu próprio método de endereçamento de comunicação. O Linux suporta as seguintes famílias ou domínios de endereços de soquete:

UNIX Unix domain sockets,
INET The Internet address family supports communications via
TCP/IP protocols
AX25 Amateur radio X25
IPX Novell IPX
APPLETALK Appletalk DDP
X25 X25

Existem vários tipos de soquete, cada um representando o tipo de serviço suportado na conexão. Nem todas as famílias de endereços suportam todos os tipos de serviços. O soquete Linux BSD suporta os seguintes tipos de soquete.

Fluxo Este tipo de soquete fornece um fluxo de dados sequencial bidirecional confiável, garantindo que os dados não sejam perdidos, danificados ou duplicados durante a transmissão. O soquete de fluxo é suportado pelo protocolo TCP na família de endereços INET

Os soquetes de datagrama também fornecem transferência de dados bidirecional, mas, diferentemente dos soquetes de fluxo, eles não garantem que as mensagens cheguem. Mesmo que chegue, não há garantia de que chegarão sequencialmente ou sem duplicação ou corrupção. Esse tipo de soquete é suportado pelo protocolo UDP na família de endereços da Internet.

RAW Isso permite que o processo acesse diretamente (daí o nome "raw") ao protocolo subjacente. Por exemplo, você pode abrir um soquete bruto para um dispositivo Ethernet e observar o fluxo de dados IP bruto.

Mensagens Confiáveis ​​Entregues Isto é muito parecido com um datagrama, mas os dados são garantidos para chegar

Pacotes sequenciados são como soquetes de fluxo, mas o tamanho do pacote de dados é fixo

Pacote Este não é um tipo de soquete BSD padrão, é uma extensão específica do Linux que permite que processos acessem pacotes diretamente na camada do dispositivo

Os processos que se comunicam usando soquetes usam um modelo cliente-servidor. O servidor fornece o serviço e o cliente consome o serviço. Um exemplo disso é um servidor web que serve páginas web e um cliente web (ou navegador) que lê essas páginas. Um servidor usando soquetes primeiro cria um soquete e, em seguida, vincula um nome a ele. O formato desse nome está relacionado à família de endereços do soquete, que é o endereço local do servidor. O nome ou endereço do Socket é especificado usando a estrutura de dados sockaddr. Um soquete INET está vinculado a um endereço de porta IP. O número da porta registrada pode ser visto em /etc/services: por exemplo, a porta para o servidor web é 80. Depois de vincular um endereço ao soquete, o servidor escuta as solicitações de conexão de entrada para o endereço vinculado. O iniciador da solicitação, o cliente, cria um soquete e executa uma solicitação de conexão nele, especificando o endereço de destino do servidor. Para um soquete INET, o endereço do servidor é seu endereço IP e seu endereço de porta. Essas solicitações de entrada precisam passar por várias camadas de protocolo, encontrar o caminho e aguardar na porta de escuta do servidor. Depois que o servidor recebe uma solicitação de entrada, ele pode aceitá-la ou rejeitá-la. Para aceitar uma solicitação recebida, o servidor deve criar um novo soquete para aceitá-la. Depois que um soquete for usado para escutar solicitações de conexão recebidas, ele não poderá mais ser usado para dar suporte a uma conexão. Após a conexão ser estabelecida, ambas as extremidades estão livres para enviar e receber dados. Finalmente, quando uma conexão não é mais necessária, ela pode ser fechada. Deve-se tomar cuidado para garantir o processamento correto dos datagramas que estão sendo transmitidos.

O significado exato das operações em um soquete BSD depende de sua família de endereços subjacente. Estabelecer uma conexão TCP/IP é muito diferente de estabelecer uma conexão X.25 de rádio amador. Assim como o sistema de arquivos virtual, o Linux abstrai a interface de soquete entre soquetes BSD e aplicativos na camada de soquete BSD suportado por software associado a famílias de endereços independentes. Quando o kernel é inicializado, a família de endereços embutida no kernel se registra com a interface de soquete BSD. Mais tarde, quando um aplicativo cria e usa um soquete BSD, uma associação é estabelecida entre o soquete BSD e sua família de endereços de suporte. Esta ligação é conseguida através de uma tabela de intersecção de estruturas de dados e rotinas de apoio à família. Por exemplo, quando um aplicativo cria um novo soquete, a interface de soquete BSD usa as rotinas de criação de soquete associadas à família de endereços.

Ao configurar o núcleo, um conjunto de famílias de endereços e protocolos é incorporado à tabela de vetores de protocolos. Cada um é representado por seu nome (por exemplo, "INET") e o endereço de sua rotina de inicialização. Quando iniciada, a interface de soquete é inicializada e o código de inicialização para cada protocolo é chamado. Para famílias de endereços de soquete, uma série de operações de protocolo são registradas nelas. Essas são rotinas, cada uma das quais executa uma operação especial associada à família de endereços. As operações de protocolo registradas são armazenadas na tabela de vetores pops, que contém ponteiros para a estrutura de dados proto_ops. A estrutura de dados Proto_ops inclui o tipo de família de protocolos e uma coleção de ponteiros para rotinas de operação de soquete associadas a uma família de endereços específica. A tabela de vetores Pops é indexada com um identificador de família de endereços, como o identificador de família de endereços da Internet (AF_INET é 2).

veja include/linux/net.h

10.4 A camada de soquete INET

A camada de soquete INET suporta a família de endereços da Internet, incluindo o protocolo TCP/IP. Conforme discutido acima, esses protocolos são em camadas, cada um usando os serviços do outro. O código TCP/IP do Linux e as estruturas de dados refletem essa camada. Ele faz interface com a camada de soquete BSD por meio das operações de soquete da família de endereços da Internet que registra com a camada de soquete BSD quando a rede é inicializada. Estes são colocados na tabela de vetores pops junto com outras famílias de endereços registradas. A camada de soquete BSD faz seu trabalho chamando as rotinas de suporte de soquete da camada INET na estrutura de dados proto_ops registrada. Por exemplo, uma solicitação de criação de soquete BSD cuja família de endereços é INET usará a função de criação de soquete INET subjacente. A estrutura de dados de soquete representando o soquete BSD é passada para a camada INET para cada operação da camada de soquete BSD. A camada de soquete INET usa seu próprio soquete de estrutura de dados, que se conecta à estrutura de dados do soquete BSD, em vez de sobrecarregar o soquete BSD com informações relacionadas ao TCP/IP. Veja a Figura 10.3 para esta conexão. Ele usa o ponteiro de dados no soquete BSD para vincular a estrutura de dados do sock à estrutura de dados do soquete BSD. Isso significa que as chamadas de soquete INET subsequentes podem obter facilmente essa estrutura de dados sock. Os ponteiros de operação de protocolo para a estrutura de dados sock também são criados no momento da criação, dependendo do protocolo solicitado. Se o TCP for solicitado, o ponteiro de operação do protocolo da estrutura de dados do sock apontará para uma série de operações do protocolo TCP exigidas pela conexão TCP.

veja include/net/sock.h

10.4.1 Criando um Soquete BSD

A chamada de sistema que cria um novo soquete precisa passar seu identificador de família de endereços, tipo de soquete e protocolo. Primeiro, procure uma família de endereços correspondente na tabela de vetores pops com a família de endereços solicitada. Pode ser uma família de endereços especial implementada usando um módulo do kernel, se assim for, o processo do núcleo do kerneld deve carregar este módulo antes que possamos prosseguir. Em seguida, aloque uma nova estrutura de dados de soquete para representar esse soquete BSD. Na verdade, a estrutura de dados do soquete é fisicamente parte da estrutura de dados do inode VFS, e alocar um soquete é, na verdade, alocar um inode VFS. Isso parece estranho, a menos que você considere sockets que podem ser manipulados da mesma forma que arquivos normais. Como todos os arquivos são representados por uma estrutura de dados de inode VFS, para suportar operações de arquivo, um soquete BSD também deve ser representado por uma estrutura de dados de inode VFS.

A estrutura de dados de socket BSD recém-criada contém um ponteiro para a rotina de socket associada à família de endereços, esse ponteiro é definido para a estrutura de dados proto_ops obtida da tabela de vetores pops. Seu tipo é definido para o tipo de soquete solicitado: um de SOCK_STREAM, SOCK_DGRAM, etc., e então a rotina de criação associada à família de endereços é chamada com o endereço armazenado na estrutura de dados proto_ops.

Em seguida, aloque um descritor de arquivo livre da tabela de vetores fd do processo atual e a estrutura de dados do arquivo para a qual ele aponta também é inicializada. Isso inclui configurar o ponteiro de manipulação de arquivos para as rotinas de manipulação de arquivos de soquete BSD suportadas pela interface de soquete BSD. Todas as operações futuras são direcionadas para a interface de soquete, que por sua vez é passada para a família de endereços correspondente chamando a rotina de operação da família de endereços suportada.

10.4.2 Vinculando um endereço a um soquete INET BSD

Para ouvir as solicitações de conexão de Internet recebidas, cada servidor deve criar um soquete INET BSD e vincular seu próprio endereço a ele. A maioria das operações do Bind são tratadas pela camada de soquete INET, e algumas requerem o suporte das camadas de protocolo TCP e UDP subjacentes. Um soquete vinculado a um endereço não pode ser usado para outras comunicações. Isso significa que o estado desse soquete deve ser TCP_CLOSE. O sockaddr passado para a operação de vinculação inclui o endereço IP para vincular e um número de porta (opcional). Normalmente, o endereço vinculado será um dos endereços atribuídos aos dispositivos de rede que suportam a família de endereços INET e a interface deve estar ativa e disponível para uso. Você pode usar o comando ifconfig para ver quais interfaces de rede estão atualmente ativas no sistema. Um endereço IP também pode ser um endereço de broadcast IP (todos os 1s ou 0s). Este é um endereço especial que significa "enviar para todos". Este endereço IP também pode ser definido para qualquer endereço IP se a máquina funcionar como um proxy transparente ou firewall. No entanto, apenas processos com privilégios de superusuário podem se vincular a qualquer endereço IP. Esse endereço IP vinculado é armazenado nos campos recv_addr e saddr da estrutura de dados sock. Eles são usados ​​para pesquisa de hash e envio de endereços IP, respectivamente. O número da porta é opcional, se não for definido, um gratuito será solicitado à rede de suporte. Por convenção, números de porta menores que 1024 não podem ser usados ​​por processos sem privilégios de superusuário. Se a rede subjacente atribuir um número de porta, ela sempre atribuirá uma porta maior que 1024.

Quando o dispositivo de rede subjacente recebe pacotes, esses pacotes devem ser encaminhados para os soquetes INET e BSD corretos para serem processados. Para este fim, o UDP e o TCP mantêm tabelas de hash, que são usadas para procurar os endereços das mensagens IP recebidas e encaminhá-las para o par soquete/sock correto. O TCP é um protocolo orientado à conexão, portanto, o processamento de pacotes TCP contém mais informações do que o processamento de pacotes UDP.

O UDP mantém uma tabela de hash de portas UDP alocadas, udp_table. Isso inclui um ponteiro para a estrutura de dados sock, indexada por uma função hash com base no número da porta. Como a tabela de hash UDP é muito menor do que os números de porta permitidos (udp_hash é apenas 128, UDP_HTABLE_SIZE), algumas entradas na tabela apontam para uma lista vinculada de estruturas de dados sock, vinculadas pelo próximo ponteiro de cada sock.

O TCP é mais complicado porque mantém várias tabelas hast. No entanto, durante a operação de vinculação, o TCP não adiciona a estrutura de dados sock vinculada à sua tabela de hash, apenas verifica se a porta solicitada não está em uso no momento. A estrutura de dados sock é adicionada à tabela de hash TCP durante a operação de escuta.

10.4.3 Fazendo uma conexão com um soquete INET BSD

Depois que um soquete é criado, ele pode ser usado para estabelecer solicitações de conexão de saída se não estiver ouvindo solicitações de conexão de entrada. Para protocolos sem conexão como o UDP, essa operação de soquete não precisa fazer muito, mas para protocolos orientados a conexão como o TCP, ela envolve o estabelecimento de um circuito virtual entre dois aplicativos.

Uma conexão de saída só pode ser feita em um soquete INET BSD que esteja no estado correto: ou seja, uma conexão não foi estabelecida e não há escuta para conexões de entrada. Isso significa que a estrutura de dados do soquete BSD deve estar no estado SS_UNCONNECTED. O protocolo UDP não estabelece uma conexão virtual entre duas aplicações, todas as mensagens enviadas são datagramas, que podem ou não chegar ao seu destino. No entanto, também suporta a operação de conexão de soquetes BSD. Uma operação de conexão em um soquete UDP INET BSD simplesmente estabelece o endereço do aplicativo remoto: seu endereço IP e seu número de porta IP. Além disso, ele também configura um buffer de entradas da tabela de roteamento, para que os datagramas UDP enviados neste soquete BSD não precisem verificar o banco de dados da tabela de roteamento (a menos que a rota se torne inválida). Essas informações de roteamento em cache são apontadas pelo ponteiro ip_route_cache na estrutura de dados sock INET. Se nenhuma informação de endereço for fornecida, as mensagens enviadas por esse soquete BSD usarão automaticamente essas informações de roteamento e endereço IP em cache. O UDP altera o estado do sock para TCP_ESTABLISHED.

Para uma operação de conexão em um soquete TCP BSD, o TCP deve estabelecer uma mensagem TCP contendo as informações de conexão e enviá-la ao destino IP fornecido. Essa mensagem TCP inclui informações sobre a conexão: um número de sequência de mensagem inicial exclusivo, o tamanho máximo de mensagens que o host de origem pode gerenciar, o tamanho da janela para envio e recebimento e assim por diante. No TCP, todas as mensagens são numeradas e o número de sequência inicial é usado como o primeiro número da mensagem. O Linux escolhe um número aleatório razoável para evitar ataques de protocolo maliciosos. Cada mensagem enviada de uma extremidade de uma conexão TCP e recebida com sucesso pela outra extremidade é reconhecida, informando que chegou com sucesso e sem danos. As mensagens não confirmadas serão reenviadas. O tamanho da janela de envio e recebimento é o número de mensagens permitidas antes da confirmação. Se o tamanho máximo da mensagem suportado pelo dispositivo de rede do destinatário for menor, a conexão usará o menor dos dois. Um aplicativo que executa uma solicitação de conexão TCP de saída deve agora aguardar uma resposta do aplicativo de destino para aceitar ou rejeitar a solicitação de conexão. Para o sock TCP que espera mensagens de entrada, ele é adicionado a tcp_listening_hash para que as mensagens TCP de entrada possam ser direcionadas para essa estrutura de dados do sock. O TCP também inicia temporizadores para que as solicitações de conexão de saída atinjam o tempo limite se o aplicativo de destino não responder à solicitação.

10.4.4 Ouvindo em um soquete INET BSD

Uma vez que um soquete tenha um endereço associado, ele poderá escutar solicitações de conexão de entrada especificando o endereço associado. Um aplicativo de rede pode escutar diretamente em um soquete sem vincular um endereço; nesse caso, a camada de soquete INET encontra um número de porta não utilizado (para esse protocolo) e o vincula automaticamente ao soquete. A função de escuta do soquete coloca o soquete no estado TCP_LISTEN e executa o trabalho necessário relacionado à rede enquanto permite conexões de entrada.

Para soquetes UDP, basta alterar o estado do soquete, mas o TCP o ativou agora para adicionar a estrutura de dados sock do soquete às suas duas tabelas de hash. Aqui estão as tabelas tcp_bound_hash e tcp_listening_hash. Ambas as tabelas são indexadas por uma função de hash com base no número da porta IP.

Sempre que uma solicitação de conexão TCP de entrada para um soquete de escuta ativo é recebida, o TCP cria uma nova estrutura de dados sock para representá-la. A estrutura de dados sock torna-se a metade inferior da conexão TCP antes de ser finalmente aceita. Ele também clona o sk_buff de entrada que contém a solicitação de conexão e o enfileira na fila receive_queue da estrutura de dados do sock de escuta. Este sk_buff clonado inclui um ponteiro para essa estrutura de dados de meia recém-criada.

10.4.5 Aceitando Solicitações de Conexão

O UDP não suporta o conceito de conexão. Aceitar a solicitação de conexão do soquete INET só se aplica ao protocolo TCP. A operação de aceitação em um sock de escuta clonará uma nova estrutura de dados de soquete do soquete de escuta original. A operação de aceitação é então passada para a camada de protocolo de suporte, neste caso, INET, para aceitar quaisquer solicitações de conexão recebidas. Se o protocolo subjacente, como UDP, não suportar conexões, a operação de aceitação na camada de protocolo INET falhará. Caso contrário, a solicitação de conexão é passada para o protocolo real, neste caso, o TCP. A operação de aceitação pode ser bloqueante ou não bloqueante. No caso sem bloqueio, se não houver conexões de entrada para aceitar, a operação de aceitação falhará e a estrutura de dados do soquete recém-criada será descartada. No caso de bloqueio, o aplicativo de rede que executa a operação de aceitação será adicionado a uma fila de espera e depois suspenso até que uma solicitação de conexão TCP seja recebida. Uma vez que uma solicitação de conexão é recebida, o sk_buff que contém a solicitação é descartado e a estrutura de dados sock é retornada para a camada de soquete INET, onde é conectada à nova estrutura de dados de soquete criada anteriormente. O descritor de arquivo (fd) do novo soquete é retornado ao aplicativo de rede e o aplicativo pode usar esse descritor de arquivo para executar operações de soquete no soquete INET BSD recém-criado.

10.5 A Camada IP

10.5.1 Buffers de soquete

Um problema com esses protocolos de rede é que cada protocolo precisa adicionar cabeçalhos e trailers de protocolo aos dados durante a transmissão e, ao processar os dados recebidos, eles precisam ser divididos em várias camadas. Isso dificulta bastante a transferência de buffers de dados entre protocolos, pois cada camada precisa descobrir onde estão seus cabeçalhos e trailers de protocolo específicos. Uma solução alternativa é copiar o buffer em cada camada, mas isso é ineficiente. Em vez disso, o Linux usa buffers de soquete ou sock_buffs para transferir dados entre a camada de protocolo e o driver de dispositivo de rede. Sk_buffs incluem campos de ponteiro e comprimento que permitem que cada camada de protocolo use funções ou métodos padrão para manipular dados de aplicativos.

A Figura 10.4 mostra a estrutura de dados do sk_buff: cada sk_buff tem seu dado associado. Sk_buff tem quatro ponteiros de dados para manipular e gerenciar dados de buffer de soquete.

Veja include/linux/skbuff.h

head aponta para o início da área de dados na memória. Determinado quando sk_buff e seu bloco de dados associado são alocados.

Os dados apontam para o início atual dos dados do protocolo até o momento. Este ponteiro varia de acordo com a camada de protocolo que atualmente possui o sk_buff.

A cauda aponta para o final atual dos dados do protocolo. Novamente, esse ponteiro varia de acordo com a camada de protocolo que você possui.

End aponta para o final da área de dados na memória. Isso é determinado quando este sk_buff é alocado.

Há também dois campos de comprimento, len e truesize, que descrevem o comprimento da mensagem de protocolo atual e o comprimento total do buffer de dados, respectivamente. O código de manipulação Sk_buff fornece mecanismos padrão para adicionar e remover cabeçalhos e trailers de protocolo dos dados do aplicativo. Este código manipula com segurança os campos data, tail e len em sk_buff.

Push Isso move o ponteiro de dados para o início da área de dados e incrementa o campo len. Usado para adicionar dados ou cabeçalhos de protocolo na frente dos dados transmitidos

Veja include/linux/skbuff.h skb_push()

Pull move o ponteiro de dados do início ao fim da área de dados e reduz o campo len. Usado para remover dados ou cabeçalhos de protocolo dos dados recebidos.

Veja include/linux/skbuff.h skb_pull()

Put move o ponteiro de cauda para o final da área de dados e adiciona o campo len para adicionar dados ou informações de protocolo no final dos dados transmitidos

Veja include/linux/skbuff.h skb_put()

trim move o ponteiro de cauda para o início da área de dados e reduz o campo len. Usado para remover dados ou trailers de protocolo dos dados recebidos

Veja include/linux/skbuff.h skb_trim()

A estrutura de dados sk_buff também inclui alguns ponteiros. Usando esses ponteiros, essa estrutura de dados pode ser armazenada na lista duplamente vinculada de sk_buff durante o processamento. Existem rotinas gerais de sk_buff que adicionam e removem sk_buffs da cabeça e da cauda dessas listas.

10.5.2 Recebendo Pacotes IP

A Seção 8 descreve como os drivers de dispositivo de rede Linux são integrados ao kernel e inicializados. Isso produz uma série de estruturas de dados do dispositivo, vinculadas na lista dev_base. Cada estrutura de dados de dispositivo descreve seu dispositivo e fornece um conjunto de rotinas de retorno de chamada que a camada de protocolo de rede pode chamar quando o driver de rede precisa funcionar. A maioria dessas funções está relacionada à transferência de dados e endereços de dispositivos de rede. Quando um dispositivo de rede recebe um pacote de dados de sua rede, ele deve converter os dados recebidos na estrutura de dados sk_buff. Esses sk_buffs recebidos são adicionados à fila de pendências pelo driver de rede à medida que são recebidos. Se a fila de pendências ficar muito grande, o sk_buff recebido será descartado. Se houver trabalho a ser executado, a metade do botão desta rede é marcada como pronta para ser executada.

Veja net/core/dev.c netif_rx()

Quando o manipulador da metade inferior da rede é chamado pelo escalonador, ele primeiro processa qualquer pacote de rede esperando para ser entregue e, em seguida, processa a fila de pendências do sk_buff para determinar para qual camada de protocolo o pacote recebido precisa ser entregue. Quando a camada de rede Linux é inicializada, cada protocolo se registra e adiciona uma estrutura de dados packet_type à lista ptype_all ou à tabela de hash ptype_base. A estrutura de dados packet_type inclui o tipo de protocolo, um ponteiro para um dispositivo de driver de rede, um ponteiro para a rotina de processamento de recepção de dados do protocolo e um ponteiro para o próximo tipo de dados packet_type nesta lista ou tabela de hash. A lista vinculada Ptype_all é usada para bisbilhotar todos os pacotes de dados recebidos de qualquer dispositivo de rede e geralmente não é usada. A tabela de hash Ptype_base usa o hash do identificador de protocolo para determinar qual protocolo deve receber os pacotes de rede de entrada. A metade inferior da rede corresponde ao tipo de protocolo do sk_buff de entrada com uma ou mais entradas packet_type em qualquer tabela. O protocolo pode corresponder a uma ou mais entradas, por exemplo, ao bisbilhotar todo o tráfego da rede, caso em que o sk_buff será clonado. Este sk_buff é passado para a rotina de processamento do protocolo correspondente.

Veja net/core/dev.c net_bh()

Veja net/ipv4/ip_input.c ip_recv()

10.5.3 Envio de pacotes IP

As mensagens são transmitidas no processo de troca de dados entre aplicativos, ou também podem ser geradas por protocolos de rede para suportar conexões estabelecidas ou para estabelecer conexões. Independentemente de como os dados são gerados, um sk_buff é criado contendo os dados e vários cabeçalhos são adicionados à medida que passam pela camada de protocolo.

Este sk_buff precisa ser passado para o dispositivo de rede que está transmitindo. Mas primeiro, um protocolo, como o IP, precisa decidir qual dispositivo de rede usar. Isso depende da melhor rota para este pacote. Para computadores conectados a uma rede por meio de um modem, como o protocolo PPP, esse roteamento é mais fácil. Os pacotes devem ser passados ​​pelo dispositivo de loopback para o host local ou para o gateway na outra extremidade da conexão do modem PPP. Para computadores conectados à Ethernet, essa escolha é difícil porque há muitos computadores conectados à rede.

Para cada pacote IP enviado, o IP usa a tabela de roteamento para resolver a rota para o endereço IP de destino. Para cada pesquisa de destino IP na tabela de roteamento, o sucesso retorna uma estrutura de dados rtable descrevendo a rota a ser usada. Isso inclui o endereço IP de origem usado, o endereço da estrutura de dados do dispositivo de rede e, às vezes, um cabeçalho de hardware pré-construído. Esse cabeçalho de hardware está associado a dispositivos de rede e contém endereços físicos de origem e destino e outras informações relacionadas à mídia. Se o dispositivo de rede for um dispositivo Ethernet, o cabeçalho do hardware será mostrado na Figura 10.1, onde os endereços de origem e destino serão endereços Ethernet físicos. O cabeçalho de hardware e a rota são armazenados em cache juntos, porque cada pacote IP transmitido nessa rota precisa anexar esse cabeçalho e leva tempo para construir esse cabeçalho. O cabeçalho de hardware pode conter endereços físicos que devem ser resolvidos usando o protocolo ARP. Neste momento, os pacotes de saída serão suspensos até que a resolução do endereço seja bem-sucedida. Depois que o endereço de hardware é resolvido e o cabeçalho de hardware é estabelecido, o cabeçalho de hardware é armazenado em cache para que futuros pacotes IP usando essa interface não precisem executar ARP.

veja include/net/route.h

10.5.4 Fragmentação de Dados

Cada dispositivo de rede tem um tamanho máximo de pacote e não pode transmitir ou receber pacotes de dados maiores. O protocolo IP permite esse tipo de dados, dividindo-os em unidades menores de tamanho de pacote que o dispositivo de rede pode manipular. O cabeçalho do protocolo IP contém um campo de divisão contendo um marcador e o deslocamento da divisão.

Quando um pacote IP deve ser transmitido, o IP procura o dispositivo de rede usado para enviar o pacote IP. Encontre o dispositivo através da tabela de roteamento IP. Cada dispositivo possui um campo que descreve sua unidade máxima de transmissão (bytes), que é o campo mtu. Se o mtu do dispositivo for menor que o tamanho do pacote IP esperando para ser transmitido, o pacote IP deve ser dividido em fragmentos menores (tamanho do mtu). Cada fragmento é representado por um sk_buff: seu cabeçalho IP marca que ele foi fragmentado e o deslocamento desse pacote IP nos dados. O último pacote é marcado como o último fragmento de IP. Se o IP não puder alocar um sk_buff durante o processo de fragmentação, a transferência falhará.

Receber fragmentos de IP é mais difícil do que enviar, porque os fragmentos de IP podem ser recebidos em qualquer ordem e todos devem ser recebidos antes da remontagem. Cada vez que um pacote IP é recebido, é verificado se é um fragmento IP. Ao receber o primeiro fragmento de uma mensagem, o IP constrói uma nova estrutura de dados ipq e se conecta à lista ipqueue de fragmentos IP esperando para serem montados. Quando mais fragmentos IP são recebidos, a estrutura de dados ipq correta é encontrada e uma nova estrutura de dados ipfrag é criada para descrever o fragmento. Cada estrutura de dados ipq descreve exclusivamente um quadro de recebimento IP fragmentado, incluindo seus endereços IP de origem e destino, o identificador de protocolo da camada superior e o identificador do quadro IP. Quando todos os fragmentos são recebidos, eles são reunidos em um único sk_buff e passados ​​para a próxima camada de protocolo para processamento. Cada ipq inclui um temporizador que reinicia toda vez que um fragmento válido é recebido. Se o cronômetro expirar, a estrutura de dados ipq e seu ipfrag serão removidos e supõe-se que a mensagem foi perdida em trânsito. O protocolo de nível superior é então responsável por retransmitir a mensagem.

Veja net/ipv4/ip_input.c ip_rcv()

10.6 O Protocolo de Resolução de Endereço (ARP)

A tarefa do Address Resolution Protocol é fornecer tradução de endereços IP para endereços de hardware físico, como endereços Ethernet. O IP só precisa dessa conversão quando transfere dados (na forma de um sk_buff) para o driver do dispositivo para transferência. Ele faz algumas verificações para ver se este dispositivo precisa de um cabeçalho de hardware e, em caso afirmativo, se o cabeçalho de hardware para este pacote precisa ser reconstruído. O Linux armazena em cache os cabeçalhos de hardware para evitar reconstruções frequentes. Se o cabeçalho de hardware precisar ser reconstruído, ele chama a rotina de reconstrução do cabeçalho de hardware associada ao dispositivo. Todos os dispositivos usam a mesma rotina de reconstrução de cabeçalho comum e, em seguida, usam o serviço ARP para traduzir o endereço IP do destino em um endereço físico.

Veja net/ipv4/ip_output.c ip_build_xmit()

Veja net/ethernet/eth.c rebuild_header()

O protocolo ARP em si é muito simples e consiste em dois tipos de mensagens: solicitação ARP e resposta ARP. A solicitação ARP inclui o endereço IP que precisa ser traduzido e a resposta (espero) inclui o endereço IP traduzido e o endereço de hardware. As solicitações ARP são transmitidas para todos os hosts conectados à rede, portanto, para uma Ethernet, todas as máquinas conectadas à Ethernet podem ver a solicitação ARP. A máquina com o endereço IP incluído na solicitação responderá à solicitação ARP com uma resposta ARP contendo seu próprio endereço físico.

A camada de protocolo ARP no Linux é construída em torno de uma tabela da estrutura de dados arp_table. Cada um descreve uma correspondência entre um IP e um endereço físico. Essas entradas são criadas quando os endereços IP precisam ser traduzidos e excluídos quando ficam obsoletos com o tempo. Cada estrutura de dados arp_table contém os seguintes campos:

Last used 这个ARP条目上一次使用的时间
Last update 这个ARP条目上一次更新的时间
Flags 描述这个条目的状态:它是否完成等等
IP address 这个条目描述的IP地址
Hardware address 转换(翻译)的硬件地址
Hardware header 指向一个缓存的硬件头的指针
Timer 这是一个timer_list的条目,用于让没有回应的ARP请求超时
Retries 这个ARP请求重试的次数
Sk_buff queue 等待解析这个IP地址的sk_buff条目的列表

A tabela ARP contém uma tabela de ponteiros (a tabela vetorial arp_tables) que liga as entradas arp_table. Essas entradas são armazenadas em cache para acelerar o acesso a elas. Cada entrada é pesquisada usando os dois últimos bytes de seu endereço IP como um índice na tabela, e a cadeia de entradas é seguida até que a entrada correta seja encontrada. O Linux também armazena em cache cabeçalhos de hardware pré-construídos de entradas arp_table, na forma da estrutura de dados hh_cache.

Ao solicitar uma tradução de endereço IP sem uma entrada arp_table correspondente, o ARP DEVE enviar uma mensagem de solicitação ARP. Ele cria uma nova entrada arp_table na tabela e coloca o sk_buff incluindo pacotes de rede que precisam de tradução de endereço na fila sk_buff dessa nova entrada. Ele emite uma solicitação ARP e permite que o cronômetro obsoleto ARP seja executado. Se não houver resposta, o ARP tentará novamente várias vezes. Se ainda não houver resposta, o ARP excluirá a entrada arp_table. Quaisquer estruturas de dados sk_buff enfileiradas para tradução por este endereço IP são notificadas, e cabe ao protocolo da camada superior que as transmite para lidar com tais falhas. O UDP não se importa com pacotes perdidos, mas o TCP tentará reenviar em uma conexão TCP estabelecida. Se o proprietário desse endereço IP responder com seu endereço de hardware, a entrada arp_table será marcada como concluída, quaisquer sk_buffs enfileirados serão removidos da fila de pares e a transmissão continuará. O endereço de hardware é escrito no cabeçalho de hardware de cada sk_buff.

A camada de protocolo ARP também deve responder às solicitações ARP especificando seu endereço IP. Ele registra seu tipo de protocolo (ETH_P_ARP), produzindo uma estrutura de dados packet_type. Isso significa que todos os pacotes ARP recebidos por um dispositivo de rede serão passados ​​para ele. Assim como as respostas ARP, isso também inclui solicitações ARP. Ele gera uma resposta ARP usando o endereço de hardware na estrutura de dados do dispositivo receptor.

As topologias de rede mudam constantemente e os endereços IP podem ser reatribuídos a diferentes endereços de hardware. Por exemplo, alguns serviços dial-up atribuem um endereço IP a cada conexão feita. Para manter a tabela ARP com as entradas mais recentes, o ARP executa um cronômetro periódico que verifica todas as entradas arp_table para ver quais expiraram. É muito cuidadoso para não remover entradas contendo cabeçalhos de hardware contendo um ou mais caches. A exclusão dessas entradas é perigosa porque outras estruturas de dados dependem delas. Algumas entradas arp_table são permanentes e marcadas para que não sejam liberadas. A tabela ARP não pode ficar muito grande: cada entrada arp_table consome alguma memória de núcleo. Sempre que uma nova entrada precisa ser alocada e a tabela ARP atinge seu tamanho máximo, a tabela é cortada localizando as entradas mais antigas e excluindo-as.

10.7 Roteamento IP

A função de roteamento IP determina para onde os pacotes IP destinados a um endereço IP específico devem ser enviados. Ao transmitir pacotes IP, há muitas opções. O destino é alcançável? Em caso afirmativo, qual dispositivo de rede deve ser usado para enviá-lo? Existe mais de um dispositivo de rede que pode ser usado para chegar ao destino e qual é o melhor? As informações mantidas pelo banco de dados de roteamento IP podem responder a essas perguntas. Existem dois bancos de dados, sendo o mais importante o banco de dados de informações de encaminhamento. Este banco de dados é uma lista exaustiva de destinos IP conhecidos e suas melhores rotas. Outro banco de dados menor e mais rápido, o cache de rotas, é usado para localizar rapidamente rotas para destinos IP. Como todos os caches, ele deve incluir apenas as rotas acessadas com mais frequência e seu conteúdo é derivado do banco de dados de informações de encaminhamento.

As rotas adicionam e excluem solicitações IOCTL por meio da interface de soquete BSD. Essas solicitações são passadas para protocolos específicos para processamento. A camada de protocolo INET só permite que processos com privilégios de superusuário adicionem e excluam rotas IP. Essas rotas podem ser fixas ou dinâmicas e em constante mudança. A maioria dos sistemas usa roteamento fixo, a menos que eles próprios sejam roteadores. Os roteadores executam protocolos de roteamento que verificam constantemente as rotas disponíveis para todos os destinos IP conhecidos. Sistemas que não são roteadores são chamados de sistemas finais. Os protocolos de roteamento são implementados como daemons, como o GATED, que também usa o IOCTL da interface de soquete BSD para adicionar e excluir rotas.

10.7.1 O Cache de Rota

Sempre que uma rota IP é pesquisada, o cache da rota é verificado primeiro para uma rota correspondente. Se não houver nenhuma rota correspondente no cache de rotas, o banco de dados de informações de encaminhamento será pesquisado. Se a rota não puder ser encontrada aqui, a transmissão do pacote IP falhará e o aplicativo será notificado. Se a rota estiver no banco de dados de informações de encaminhamento, mas não no cache de rotas, uma nova entrada será criada para a rota e adicionada ao cache de rotas. O cache de rota é uma tabela (ip_rt_hash_table) que inclui ponteiros para uma cadeia de estruturas de dados rtable. O índice da tabela de roteamento é baseado na função hash dos dois bytes mínimos do endereço IP. Esses dois bytes geralmente são muito diferentes no destino, permitindo que o valor do hash se espalhe melhor. Cada entrada rtable inclui informações de roteamento: o endereço IP de destino, o dispositivo de rede (estrutura do dispositivo) a ser usado para alcançar esse endereço IP, o tamanho máximo da mensagem que pode ser usado e assim por diante. Ele também tem uma contagem de referência, uma contagem de uso e o último registro de data e hora usado (em instantes). Esse contador de referência é incrementado cada vez que a rota é usada, mostrando o número de conexões de rede usando a rota e diminuído quando o aplicativo para de usar a rota. O contador de uso é incrementado cada vez que uma rota é pesquisada e é usado para envelhecer as entradas rtable nesta cadeia de entradas de hash. O último timestamp usado de todas as entradas no cache de roteamento é usado para verificar periodicamente se esta rtable é muito antiga. Se a rota não tiver sido usada recentemente, ela será descartada da tabela de roteamento. Se as rotas forem armazenadas no cache de rotas, elas serão ordenadas de forma que as entradas usadas com mais frequência estejam na frente da cadeia de hash. Isso significa que será mais rápido encontrar essas rotas ao procurar rotas.

Veja net/ipv4/route.c check_expire()

10.7.2 O Banco de Dados de Informações de Encaminhamento

O banco de dados de informações de encaminhamento (mostrado na Figura 10.5) contém as rotas disponíveis para o sistema do ponto de vista IP no momento. É uma estrutura de dados muito complexa e, embora esteja organizada de forma razoavelmente eficiente, não é um banco de dados rápido para referência. Especialmente se cada pacote IP transmitido estiver procurando o alvo neste banco de dados, pode ser muito lento. Por isso existe um cache de rotas: para agilizar a entrega de pacotes IP que já conhecem a melhor rota. O cache de rota obtém desse banco de dados de informações de encaminhamento e representa suas entradas usadas com mais frequência.

Cada sub-rede IP é representada por uma estrutura de dados fib_zone. Todos são apontados pela tabela de hash fib_zones. O índice de hash é obtido da máscara de sub-rede IP. Todas as rotas para a mesma sub-rede são descritas por pares de estruturas de dados fib_node e fib_info enfileiradas na fila fz_list de cada estrutura de dados fib_zone. Se o número de rotas nessa sub-rede se tornar muito grande, uma tabela de hash será gerada para facilitar a pesquisa da estrutura de dados fib_node.

Para a mesma sub-rede IP, pode haver várias rotas que podem passar por um dos vários gateways. A camada de roteamento IP não permite mais de uma rota para uma sub-rede usando o mesmo gateway. Em outras palavras, se houver várias rotas para uma sub-rede, certifique-se de que cada rota use um gateway diferente. Associada a cada rota está sua métrica, que é usada para medir o benefício dessa rota. A medida de uma rota é, basicamente, o número de sub-redes que ela precisa pular antes de chegar à sub-rede de destino. Quanto maior essa métrica, pior o roteamento.

11. Mecanismos do Kernel

11.1 Manuseio da metade inferior

Muitas vezes, no núcleo, há momentos em que você não quer realizar o trabalho. Um bom exemplo é durante o tratamento de interrupção. Quando uma interrupção é gerada, o processador interrompe o que está fazendo e o sistema operacional passa a interrupção para o driver de dispositivo apropriado. Os drivers de dispositivo não devem gastar muito tempo processando interrupções, porque durante esse tempo nada mais no sistema pode ser executado. Normalmente, algum trabalho pode ser feito mais tarde. O Linux inventou o meio manipulador boffom para que os drivers de dispositivo e outras partes do kernel do Linux pudessem enfileirar o trabalho que poderia ser feito mais tarde. A Figura 11.1 mostra as principais estruturas de dados associadas ao processamento da metade inferior. Existem até 32 diferentes manipuladores da metade inferior: bh_base é uma tabela vetorial de ponteiros para cada manipulador da metade inferior do núcleo, bh_active e bh_mask definem seus bits de acordo com os manipuladores instalados e ativados. Se o bit N de bh_mask for definido, o Nth elemento em bh_base conterá o endereço de uma rotina da metade inferior. Se o enésimo bit de bh_active for definido, então o manipulador da metade inferior para o enésimo bit será chamado assim que o escalonador considerar razoável. Esses índices são definidos estaticamente: o manipulador da metade inferior do temporizador tem a prioridade mais alta (índice 0), o manipulador da metade inferior do console tem a próxima prioridade mais alta (índice 1) e assim por diante. Normalmente, o manipulador da metade inferior terá uma lista de tarefas associada a ele. Por exemplo, o manipulador de meio botão imediato funciona por meio de uma fila de tarefas imediatas (tq_immediate) que contém tarefas que precisam ser executadas imediatamente.

Veja include/linux/interrupt.h

Alguns dos manipuladores da metade inferior do núcleo são específicos do dispositivo, mas outros são mais gerais:

TIMER Este manipulador é marcado como ativo toda vez que a interrupção do relógio de tempo do sistema é usada para acionar o mecanismo de enfileiramento do relógio do núcleo

CONSOLE Este manipulador é usado para lidar com mensagens do console

TQUEUE Este manipulador é usado para lidar com mensagens TTY

NET Este manipulador é usado para lidar com o processamento geral da rede

IMEDIATO Manipulador genérico, usado por alguns drivers de dispositivo para enfileirar trabalhos para mais tarde

Quando um driver de dispositivo, ou outra parte do núcleo, precisa agendar trabalho para ser feito mais tarde, ele adiciona o trabalho à fila de sistema apropriada, como a fila de relógio, e então envia um sinal para o núcleo de que alguma metade inferior está processando precisa acontecer. Ele faz isso definindo os bits apropriados em bh_active. O bit 8 é definido se o driver enfileirar algo na fila imediata e esperar que o manipulador imediato da metade inferior execute e processe. No final de cada chamada do sistema, a máscara de bits bh_active é verificada antes de retornar o controle ao programa de chamada. Se qualquer um dos bits estiver definido, a rotina de tratamento da metade inferior ativa correspondente é chamada. O bit 0 é verificado primeiro, depois 1 até o bit 31. O bit correspondente em bh_active é limpo cada vez que o manipulador da metade inferior é chamado. Bh_active é volátil: só faz sentido entre chamadas para o agendador e, ao configurá-lo, o manipulador da metade inferior correspondente não pode ser chamado quando não há trabalho a fazer.

Kernel/softirq.c do_bottom_half()

11.2 Filas de Tarefas

As filas de tarefas são o método que o núcleo usa para adiar o trabalho para um momento posterior. O Linux tem um mecanismo geral para enfileirar trabalhos e processá-los posteriormente. As filas de tarefas são frequentemente usadas com manipuladores da metade inferior: a fila de tarefas do temporizador é processada enquanto o manipulador da metade inferior do temporizador está em execução. Uma fila de tarefas é uma estrutura de dados simples, veja a Figura 11.2, consistindo em uma lista encadeada de estruturas de dados tq_struct, cada uma contendo um ponteiro para uma rotina e um ponteiro para alguns dados.

Veja include/linux/tqueue.h Esta rotina é chamada quando a unidade da fila de tarefas é processada e um ponteiro para os dados é passado para ela.

Qualquer coisa no núcleo, como drivers de dispositivo, pode criar e usar filas de tarefas, mas há três filas de tarefas que são criadas e gerenciadas pelo núcleo:

timer Essa fila é usada para enfileirar o trabalho para ser executado o maior tempo possível após o próximo relógio do sistema. A cada ciclo de clock, essa fila é verificada quanto a uma entrada e, em caso afirmativo, o manipulador da metade inferior da fila de clock é marcado como ativo. Este manipulador da metade inferior da fila de relógio e outros manipuladores da metade inferior são processados ​​quando o escalonador é executado em uma execução. Não confunda essa fila com um timer do sistema, esse é um mecanismo mais complicado

imediata Essa fila também é processada quando o planejador processa o manipulador ativo da metade inferior. O manipulador imediato da metade inferior não tem prioridade mais alta do que o manipulador da metade inferior da fila do temporizador, portanto, essas tarefas hesitarão em executar.

Agendador Esta fila de tarefas é tratada diretamente pelo agendador. Ele é usado para suportar outras filas de tarefas no sistema, caso em que a tarefa a ser executada seria uma rotina que trata da fila de tarefas (por exemplo, um driver de dispositivo).

Ao processar uma fila de tarefas, um ponteiro para um elemento na fila é removido da fila e substituído por um ponteiro nulo. Na verdade, essa exclusão é uma operação atômica que não pode ser interrompida. Sua rotina de tratamento é então chamada sequencialmente para cada elemento na fila. As células na fila geralmente são dados alocados estaticamente. Mas não há nenhum mecanismo inerente para descartar a memória alocada. A rotina de processamento da fila de tarefas simplesmente se move para a próxima célula da lista. É tarefa da própria tarefa garantir que qualquer memória de núcleo alocada seja devidamente limpa.

11.3 Temporizadores

Um sistema operacional precisa ser capaz de agendar uma atividade para um horário no futuro, o que requer um mecanismo para permitir que as atividades sejam agendadas para serem executadas em um horário relativamente preciso. Qualquer microprocessador que deseje suportar um sistema operacional precisa de um intervalo moderadamente programável, interrompendo o processador periodicamente. Essa interrupção periódica é o tique-taque do relógio do sistema, que age como um metrônomo e direciona a atividade do sistema. O Linux vê o tempo de uma maneira muito simples: ele mede o tempo em ciclos de clock desde que o sistema inicializa. Qualquer hora do sistema é baseada nessa medida, chamada jiffers, que tem o mesmo nome da variável global.

O Linux tem dois tipos de temporizadores de sistema, cada um organizando rotinas para serem chamadas em horários específicos do sistema, mas eles diferem um pouco na maneira como são implementados. A Figura 11.3 mostra dois mecanismos. O primeiro, o mecanismo de timer antigo, tem um array estático de 32 ponteiros para a estrutura de dados timer_struct e uma máscara de relógios ativos, timer_active. Onde o cronômetro é colocado nesta tabela de cronômetros é definido estaticamente (diferente de bh_base na metade inferior do manipulador). As entradas são adicionadas a esta tabela durante a inicialização do sistema. O segundo mecanismo usa uma lista vinculada de estruturas de dados timer_list para organizar os dados por tempo de expiração.

Veja include/linux/timer.h

Cada método usa o tempo em jiffies como o tempo de expiração, então um cronômetro que deseja executar por 5 segundos terá uma unidade de jiffies que pode ser convertida em 5 segundos mais a hora atual do sistema para obter a hora do sistema quando o cronômetro expirar (em unidades jiffies). A cada ciclo de relógio do sistema, o manipulador da metade inferior do cronômetro é marcado como ativo, portanto, na próxima vez que o escalonador for executado, a fila do cronômetro será processada. O manipulador da metade inferior do Timer lida com os dois tipos de timers do sistema. Para temporizadores de sistema antigos, verifique se a máscara de bits timer_active está definida. Se um timer ativo expirar (o tempo de expiração é menor que o tempo atual do sistema), sua rotina de timer é chamada e seu bit ativo é apagado. Para novos temporizadores de sistema, verifique as entradas na tabela vinculada da estrutura de dados timer_list. Cada timer expirado é removido desta lista e sua rotina é chamada. A vantagem do novo mecanismo de timer é que ele pode passar parâmetros para rotinas de timer.

Veja kernel/sched.c timer_bh() run_old_timers() run_timer_list()

11.4 Filas de espera

Muitas vezes um processo deve esperar por um recurso do sistema. Por exemplo, um processo pode precisar de um inode VFS descrevendo um diretório no sistema de arquivos, mas esse inode pode não estar no cache do buffer. Neste ponto, o sistema deve esperar que o inode seja obtido do meio físico que contém o sistema de arquivos antes de continuar.

O kernel do Linux usa uma estrutura de dados simples, uma fila de espera (veja a Figura 11.4), contendo um ponteiro para task_struct do processo e um ponteiro para o próximo elemento na fila de espera.

Veja include/linux/wait.h

Quando os processos são adicionados ao final de uma fila de espera, eles podem ou não ser interrompíveis. Processos que podem ser interrompidos podem ser interrompidos por eventos enquanto aguardam na fila de espera, como um temporizador expirado ou um sinal enviado. Será refletido o status do processo em espera, que pode ser INTERRUPTÍVEL ou ININTERRUPTO. Como o processo não pode continuar em execução agora, o escalonador inicia a execução e, quando escolhe um novo processo para ser executado, o processo em espera é suspenso.

Ao processar a fila de espera, o status de cada processo na fila de espera é definido como RUNNING. Se o processo for removido da fila de execução, ele será colocado de volta na fila de execução. Na próxima vez que o escalonador for executado, os processos que estavam na fila de espera agora são candidatos a serem executados porque não estão mais esperando. Quando um processo em espera em uma fila é agendado, a primeira coisa a fazer é se retirar da fila de espera. As filas de espera podem ser usadas para sincronizar o acesso aos recursos do sistema, e o Linux implementa seus semáforos dessa maneira.

11.5 Buzz Locks

Muitas vezes chamado de bloqueios de rotação, este é um método primitivo de proteger uma estrutura de dados ou segmento de código. Eles permitem que apenas um processo por vez esteja em uma área importante do código. O Linux os usa para restringir o acesso a campos em estruturas de dados, usando um campo inteiro como um bloqueio. Cada processo que deseja entrar nesta área tenta alterar o valor inicial do bloqueio de 0 para 1. Se a corrente causar 1, o processo tentará novamente, girando em um loop de código apertado. O acesso ao local de memória que contém este bloqueio deve ser atômico, ler seu valor, verificar se é 0 e depois alterá-lo para 1, esta ação não deve ser interrompida por nenhum outro processo. A maioria das arquiteturas de CPU suporta isso com instruções especiais, mas você também pode implementar esse buzz lock usando a memória principal sem cache.

Quando o processo proprietário sai dessa importante área de código, ele reduz o buzz lock, retornando seu valor para 0. Qualquer processo em loop neste bloqueio agora lerá 0, e o primeiro processo a fazê-lo irá incrementá-lo para 1 e entrar nesta região importante.

11.6 Semáforos

Os semáforos são usados ​​para proteger regiões de código ou estruturas de dados importantes. Lembre-se de que todo acesso a estruturas de dados importantes, como um inode VFS que descreve um diretório, é realizado pelo kernel para o processo. É muito perigoso permitir que um processo altere estruturas de dados importantes usadas por outro processo. Uma maneira de fazer isso é usar um buzz lock na parte importante do código a ser acessada, embora essa seja a maneira mais fácil, mas não terá um desempenho muito bom do sistema. O Linux usa uma implementação de semáforo para permitir que apenas um processo acesse código e áreas de dados importantes por vez: todos os outros processos que desejam acessar esse recurso são forçados a esperar até que o semáforo esteja livre. O processo em espera é raspado e outros processos no sistema são executados normalmente como de costume.

Uma estrutura de dados de semáforo do Linux inclui as seguintes informações:

Veja include/asm/semaphore.h

contagem Este campo registra o número de processos que desejam utilizar este recurso. Um número positivo indica que o recurso está disponível. Um valor negativo ou 0 indica que um processo está aguardando. Um valor inicial de 1 significa que um e apenas um processo pode usar o recurso por vez. Quando os processos desejam usar o recurso, eles diminuem a contagem e quando terminam de usar o recurso, aumentam a contagem

wake O número de processos aguardando esse recurso, que também é o número de processos aguardando para serem acordados quando o recurso estiver livre.

Fila de espera Quando os processos estão esperando por este recurso eles são colocados nesta fila de espera

Bloquear o bloqueio do Buzz usado ao acessar o domínio de vigília

Supondo que o semáforo comece com um valor de 1, o primeiro processo a chegar verá que a contagem é positiva e a diminuirá de 1 para 0. O processo agora "possui" a parte importante do código ou recurso protegido pelo semáforo. Ele incrementa a contagem de semáforos quando o processo sai dessa área importante. Idealmente, nenhum outro processo está competindo pela propriedade dessa importante área. A implementação de semáforos no Linux funciona de forma muito eficiente neste caso mais comum.

Também diminui essa contagem se outro processo desejar entrar nessa área importante enquanto já pertence a um processo. Como a contagem agora é -1, o processo não pode entrar nessa região importante. Ele deve esperar até que o processo proprietário seja encerrado. O Linux coloca o processo em espera para dormir até que o processo proprietário saia dessa área importante para ativá-lo. O processo de espera se adiciona à fila de espera do semáforo e faz um loop para verificar o valor do campo de despertar, chamando o escalonador até que o despertar seja diferente de zero.

O dono desta importante área aumenta a contagem do semáforo, se for menor ou igual a 0, então ainda existem processos adormecidos, aguardando este recurso. Idealmente, a contagem do semáforo retornaria ao seu valor inicial de 1, para que nenhum trabalho fosse necessário. O processo proprietário incrementa o contador de despertar e desperta os processos adormecidos na fila de espera do semáforo. Quando o processo de espera é acordado, o contador de vigília agora é 1 e sabe que agora pode entrar nessa área importante. Ele diminui o contador de despertar, retorna a 0 e continua. Todo o acesso ao campo de vigília deste semáforo é protegido pelo buzz lock da fechadura do semáforo.

12, Módulos

Como o kernel do Linux carrega dinamicamente as funções apenas quando necessário, por exemplo, o sistema de arquivos?

O Linux é um núcleo completo, ou seja, é um único programa enorme, e os componentes funcionais do núcleo têm acesso a todas as suas estruturas e rotinas de dados internas. Outro método é usar uma estrutura de microkernel, onde as peças funcionais do núcleo são divididas em unidades independentes que possuem mecanismos de comunicação estritos entre si. Dessa forma, não leva muito tempo para adicionar novos componentes ao núcleo por meio do processo de configuração. Por exemplo, se você deseja adicionar um driver SCSI para uma placa NCR 810 SCSI, não é necessário conectá-lo ao núcleo. Caso contrário, você terá que configurar e construir um novo núcleo para usar este NCR 810. Como solução alternativa, o Linux permite que os componentes do sistema operacional sejam carregados e descarregados dinamicamente conforme você precisar deles. Um módulo Linux é um bloco de código que pode ser dinamicamente vinculado ao kernel a qualquer momento após a inicialização do sistema. Eles podem ser removidos do núcleo e desinstalados quando não forem necessários. A maioria dos módulos do kernel Linux são drivers de dispositivos, drivers de pseudodispositivos, como drivers de rede ou sistemas de arquivos.

Você pode carregar e descarregar explicitamente os módulos do kernel Linux usando os comandos insmod e rmmod, ou o próprio kernel pode pedir ao daemon do kernel (kerneld) para carregar e descarregar esses módulos quando necessário. Carregar código dinamicamente quando necessário é bastante atraente porque mantém o núcleo mínimo e o núcleo é muito flexível. Meu atual núcleo Intel faz uso pesado de módulos e tem apenas 406K de tamanho. Normalmente, uso apenas o sistema de arquivos VFAT, então construo meu kernel Linux para montar automaticamente o sistema de arquivos VFAT quando monto uma partição VFAT. Quando desmontei o sistema de arquivos VFAT, o sistema detectou que eu não precisava mais do módulo do sistema de arquivos VFAT e o removi do sistema. Os módulos também podem ser usados ​​para testar o novo código do núcleo sem criar e reiniciar o núcleo a cada vez. No entanto, não existem coisas tão boas, e o uso de módulos principais geralmente vem com um pequeno desempenho e sobrecarga de memória. Um módulo carregável deve fornecer mais código, e esse código e estruturas de dados adicionais ocupam um pouco mais de memória. Além disso, devido ao acesso indireto aos recursos principais, a eficiência do módulo é ligeiramente reduzida.

Uma vez que o kernel do Linux é carregado, ele se torna parte do kernel assim como o código do kernel normal. Ele tem os mesmos direitos e obrigações que qualquer código de kernel: em outras palavras, um módulo de kernel Linux tem a mesma probabilidade de travar o kernel como qualquer código de kernel ou driver de dispositivo.

Como os módulos podem usar os recursos principais quando precisam deles, eles devem ser capazes de encontrá-los. Por exemplo, um módulo precisa chamar kmalloc(), a rotina de alocação de memória do kernel. Quando compilado, o módulo não sabe onde kmalloc() está na memória, portanto, quando o módulo é carregado, o kernel deve classificar todas as referências a kmalloc() pelo módulo antes que o módulo possa funcionar. O núcleo mantém uma lista de todos os recursos do núcleo na tabela de símbolos do núcleo, portanto, quando o módulo é carregado, ele pode resolver as referências a esses recursos no módulo. O Linux permite o empilhamento (empilhamento) de módulos, onde um módulo precisa dos serviços de outro módulo. Por exemplo, o módulo do sistema de arquivos VFAT precisa dos serviços do módulo do sistema de arquivos FAT porque o sistema de arquivos VFAT é mais ou menos uma extensão do sistema de arquivos FAT. A situação em que um módulo precisa de serviços ou recursos de outro módulo é muito semelhante à situação em que um módulo precisa de seus próprios serviços e recursos, exceto que o serviço solicitado está em outro módulo previamente carregado. À medida que cada módulo é carregado, o núcleo modifica sua tabela de símbolos, adicionando todos os recursos ou símbolos exportados do módulo recém-carregado à tabela de símbolos do núcleo. Isso significa que quando o próximo módulo for carregado, ele poderá acessar os serviços do módulo já carregado.

Quando o gráfico está descarregando um módulo, o núcleo precisa saber que o módulo não está mais em uso e também precisa de alguma forma para notificá-lo sobre o módulo que está pronto para descarregar. Desta forma, um módulo pode liberar quaisquer recursos do sistema que ocupe, como memória do kernel ou interrupções, antes de ser removido do kernel. Quando um módulo é descarregado, o kernel remove todos os símbolos que o módulo exporta para a tabela de símbolos principal.

Além da possibilidade de corromper o sistema operacional por módulos carregáveis ​​mal escritos, existe outro perigo. O que acontece se você carregar um módulo criado para um núcleo anterior ou posterior ao que você está executando no momento? Podem surgir problemas se o módulo executar uma rotina principal com os argumentos errados. O núcleo pode optar por evitar isso fazendo uma verificação rigorosa da versão quando o módulo é carregado.

12.1 Carregando um Módulo

Um módulo principal pode ser carregado de duas maneiras. A primeira é inseri-lo manualmente no núcleo usando o comando insmod. Uma segunda maneira mais inteligente é carregar o módulo quando necessário: isso é chamado de carregamento de demanda. Quando o kernel descobre que um módulo é necessário, como quando o usuário monta um sistema de arquivos que não está no kernel, o kernel pede ao daemon do kernel (kerneld) para tentar carregar o módulo apropriado.

Kerneld e insmod, lsmod e rmmod estão todos no pacote de módulos.

O daemon principal é geralmente um processo de usuário normal com privilégios de superusuário. Quando é iniciado (geralmente na inicialização do sistema), ele abre um canal IPC para o kernel. O kernel usa essa conexão para enviar mensagens ao kerneld solicitando que ele execute uma série de tarefas. A função principal do Kerneld é carregar e descarregar módulos centrais, mas também pode executar outras tarefas, como iniciar uma conexão PPP em uma linha serial quando necessário e fechá-la quando não for necessário. O próprio Kerneld não executa essas tarefas, ele executa os programas necessários, como o insmod, para fazer o trabalho. Kerneld é apenas um proxy para o núcleo, agendando seu trabalho.

Veja include/linux/kerneld.h

O comando insmod deve encontrar o módulo principal solicitado para ser carregado. Os módulos principais carregados pelo Xu geralmente são colocados no diretório /lib/mmodules/kernel-version. Os módulos do kernel são arquivos de objeto de programa vinculados como outros programas no sistema, mas são vinculados a uma imagem realocável. É uma imagem que não está conectada a um endereço específico para ser executada. Eles podem ser arquivos de objeto em formato a.out ou elf. Insmod aponta para uma chamada de sistema privilegiada para descobrir os símbolos de saída do sistema. Eles são armazenados em pares na forma de um nome simbólico e um valor como seu endereço. A tabela de símbolos de saída do núcleo é colocada na estrutura de dados do primeiro módulo na lista de módulos mantida pelo núcleo, apontada pelo ponteiro module_list. Somente símbolos especialmente designados são adicionados a esta tabela quando o núcleo é compilado e vinculado, nem todo símbolo no núcleo exporta seu módulo. Por exemplo, o símbolo "request_irq" é uma rotina do sistema que deve ser chamada quando um driver deseja controlar uma interrupção específica do sistema. No meu núcleo atual, seu valor é 0x0010cd30. Você pode inspecionar o arquivo /proc/ksyms ou usar a ferramenta ksyms para simplesmente visualizar a saída dos símbolos principais e seus valores. A ferramenta Ksyms pode mostrar todos os símbolos principais exportados ou apenas aqueles exportados por módulos carregados. O Insmod lê o módulo em sua memória virtual e usa os símbolos de exportação do kernel para classificar as referências não resolvidas do módulo às rotinas e recursos do kernel. Esse processo de limpeza é feito corrigindo a imagem do módulo na memória, o insmod grava fisicamente o endereço do símbolo no local apropriado no módulo.

Veja kernel/module.c kernel_syms() include/linux/module.h

Quando o insmod termina de classificar a referência do módulo para os símbolos de núcleo exportados, ele solicita espaço suficiente do núcleo para colocar o novo núcleo, novamente por meio de uma chamada de sistema privilegiada. O kernel aloca uma nova estrutura de dados de módulo e memória de kernel suficiente para conter o novo módulo e o coloca no final da lista de módulos do kernel. Este novo módulo está marcado como UNINITIALIZED. A Figura 12.1 mostra que os dois últimos módulos na lista de módulos principais: FAT e VFAT são carregados na memória. Não é mostrado na figura o primeiro módulo da lista: este é um pseudo-módulo onde é colocada a tabela de símbolos de saída do núcleo. Você pode usar o comando lsmod para listar todos os módulos principais carregados e suas dependências. Lsmod simplesmente reorganiza /proc/modules extraídos da lista de estruturas de dados do módulo principal. A memória alocada pelo kernel para o módulo é mapeada no espaço de endereço do processo insmod, para que ele possa acessá-lo. O Insmod copia o módulo para o espaço alocado e o realoca para que possa ser executado a partir do endereço do núcleo alocado. As realocações são necessárias porque um módulo não pode ser carregado no mesmo endereço duas vezes ou carregado no mesmo endereço em dois sistemas Linux diferentes. Desta vez, a realocação consiste em corrigir a imagem do módulo com o endereço apropriado.

Veja kernel/module.c create_module()

Novos módulos também exportam símbolos para o kernel, e o Insmod constrói um mapa de exportação. Cada módulo do kernel deve conter o processo de inicialização e limpeza do módulo.Esses símbolos devem ser privados e não exportados, mas o insmod deve conhecer seus endereços e poder passá-los para o kernel. Feito tudo isso, o Insmod está pronto para inicializar o módulo, que executa uma chamada de sistema privilegiada passando o endereço das rotinas de inicialização e limpeza do módulo para o kernel.

Veja kernel/module.c sys_init_module()

Quando um novo módulo é adicionado ao núcleo, ele deve atualizar a tabela de símbolos do núcleo e alterar os módulos usados ​​pelo novo módulo. Módulos dos quais outros módulos dependem devem manter uma lista de referência após sua tabela de símbolos, apontada por sua estrutura de dados de módulo. A Figura 12.1 mostra que o módulo do sistema de arquivos VFAT depende do módulo do sistema de arquivos FAT. Portanto, o módulo FAT contém uma referência ao módulo VFAT: essa referência é incrementada quando o módulo VFAT é carregado. O núcleo chama a rotina de inicialização do módulo e, se for bem-sucedido, inicia a instalação do módulo. O endereço da rotina de limpeza do módulo é armazenado em sua estrutura de dados do módulo e é chamado pelo kernel quando o módulo é descarregado. Por fim, o estado do módulo é definido como RUNNING.

12.2 Descarregando um Módulo

Os módulos podem ser removidos usando o comando rmmod, mas o kerneld pode remover todos os módulos não utilizados carregados sob demanda do sistema. Toda vez que seu temporizador ocioso expira, o kerneld executa uma chamada de sistema solicitando que todos os módulos sob demanda desnecessários sejam removidos do sistema. O valor deste temporizador é definido quando você inicia o kerneld: meu kerneld verifica a cada 180 segundos. Se você instalar um CD-ROM iso9660 e seu sistema de arquivos iso9660 for um módulo carregável, o módulo iso9660 será removido do núcleo logo após a desmontagem do CD-ROM.

Se outros componentes no núcleo dependerem de um módulo, ele não poderá ser removido. Por exemplo, se você tiver um ou mais sistemas de arquivos VFAT instalados, não poderá desinstalar o módulo VFAT. Se você verificar a saída ls, verá um contador associado a cada módulo. Por exemplo:

Module: #pages: Used by:
msdos 5 1
vfat 4 1 (autoclean)
fat 6 [vfat msdos] 2 (autoclean)

A contagem (contagem) é o número de entidades principais que dependem deste módulo. No exemplo acima, tanto vfat quanto msdos dependem do módulo fat, então o contador do módulo fat é 2. Ambos os módulos Vfat e msdos têm uma contagem de dependência de 1 porque ambos têm um sistema de arquivos montado. Se eu carregar outro sistema de arquivos VFAT, o contador do módulo vfat se torna 2. O contador de um módulo é colocado na primeira palavra longa de sua imagem.

Como também coloca os sinalizadores AUTOCLEAN e VISITED, esse campo tem uma pequena sobrecarga. Ambos os sinalizadores são usados ​​para carregar módulos sob demanda. Esses módulos são marcados como AUTOCLEAN para que o sistema possa identificar quais ele pode descarregar automaticamente. O sinalizador VISITED indica que este módulo é utilizado por um ou mais componentes do sistema: este sinalizador é acionado sempre que outro componente o utiliza. Toda vez que o kerneld pede ao sistema para remover um módulo sob demanda não utilizado, ele examina todos os módulos do sistema e encontra um candidato adequado. Ele apenas analisa os módulos marcados como AUTOCLEAN e cujo status é RUNNING. Se o sinalizador candidato VISITED for apagado, ele exclui o módulo, caso contrário, ele apaga o sinalizador VISITED e passa para o próximo módulo no sistema.

Assumindo que um módulo pode ser descarregado, sua rotina de limpeza é chamada para liberar os recursos principais alocados. A estrutura de dados do módulo é marcada como DELTED e removida da lista de módulos principais. As listas de referência de quaisquer outros módulos dos quais ele depende são modificadas para que não o considerem mais um dependente. Toda a memória de núcleo necessária para este módulo é liberada.

Veja kernel/module.c delete_module()

13. As fontes do kernel Linux

Onde o programa de origem do núcleo do Linux começa a examinar as funções principais específicas?

Pratique a visualização do código-fonte principal para obter uma compreensão aprofundada do sistema operacional Linux. Esta seção fornece uma visão geral dos principais programas-fonte: como eles são organizados e onde você deve começar a procurar um código específico.

Onde obter as fontes do kernel Linux

Todas as principais distribuições Linux (Craftworks, Debian, Slackware, RedHat, etc.) têm fontes centrais no meio. Normalmente, o kernel Linux instalado em seu sistema Linux é construído com esses programas de origem. Na verdade, essas fontes parecem estar um pouco desatualizadas, então você pode querer obter a fonte mais recente dos sites mencionados no Apêndice C. Eles são colocados em ftp://ftp.cs.helsinki.fi e em todos os outros sites espelhados. O site de Helsinque está atualizado, mas outros sites, como MIT e Sunsite, não ficam muito atrás.

Se você não tiver acesso à web, existem muitos fabricantes de CDROM que oferecem blocos dos principais sites do mundo por preços bastante razoáveis. Alguns até oferecem serviços de assinatura, com atualizações trimestrais ou mensais. Seu grupo de usuários Linux local também é uma boa fonte de código-fonte.

Os programas de origem do núcleo Linux têm um sistema de numeração muito simples. Qualquer núcleo de número par (por exemplo, 2.0.30) é um núcleo liberado estável, e qualquer núcleo de número ímpar (por exemplo, 2.1.42) é um núcleo em desenvolvimento. Este livro é baseado no código-fonte estável 2.0.30. Os núcleos de desenvolvimento têm todos os recursos e suporte mais recentes para todos os dispositivos mais recentes, mas podem não ser estáveis ​​e podem não ser o que você deseja, mas é importante que a comunidade Linux teste os núcleos mais recentes. Isso permite que toda a comunidade seja testada. Lembre-se, mesmo se você estiver testando núcleos que não são de produção, é uma boa ideia fazer backup do seu sistema.

As alterações no programa de origem principal são distribuídas como arquivos de patch. O patch de ferramenta pode aplicar uma série de modificações a uma série de arquivos de origem. Por exemplo, se você tem uma árvore de código fonte 2.0.29 e deseja mover para 2.0.30, você pode pegar os arquivos de patch 2.0.30 e aplicar esses patches (edições) à árvore de código fonte:

$ cd /usr/src/linux
$ patch -p1 < patch-2.0.30

Isso evita que você tenha que copiar toda a árvore de origem, especialmente para conexões seriais lentas. Uma boa fonte de patches principais (oficiais e informais) é http://www.linuxhq.com

Como as fontes do kernel são organizadas

No topo da árvore de código você verá alguns diretórios:

arch O subdiretório arch contém todo o código principal relacionado à arquitetura. Ele também possui subdiretórios mais profundos, cada um representando uma arquitetura suportada, como i386 e alpha.

O subdiretório include inclui a maioria dos arquivos incluídos necessários para compilar o núcleo. Ele também possui subdiretórios mais profundos, um para cada arquitetura suportada. Include/asm é um soft link para o diretório include real exigido por esta arquitetura, por exemplo, include/asm-i386. Para alterar a arquitetura, você precisa editar o makefile principal e executar novamente o configurador principal do Linux

Init Este diretório contém o código de inicialização do núcleo e é um bom ponto de partida para estudar como o núcleo funciona.

Mm Este diretório contém todo o código de gerenciamento de memória. O código de gerenciamento de memória relacionado à arquitetura está localizado em arch/*/mm/, como arch/i386/mm/fault.c

Drivers Todos os drivers de dispositivo do sistema estão neste diretório. Eles são divididos em classes de driver de dispositivo, como bloco.

Ipc Este diretório contém o código principal de comunicação entre processos

Módulos Este é apenas um diretório para armazenar os módulos estabelecidos

Fs Todo o código do sistema de arquivos. é dividido em subdiretórios, um para cada sistema de arquivos suportado, como vfat e ext2

O código principal do Kernel. Da mesma forma, o código principal relacionado ao sistema é colocado em arch/*/kernel

Código de rede do núcleo da rede

Lib Este diretório abriga o código da biblioteca principal. O código da biblioteca relacionado à arquitetura está em arch/*/lib/

Scripts Este diretório contém scripts (como scripts awk e tk) que configuram o núcleo

Por onde começar a procurar

É muito difícil olhar para um programa tão grande e complexo quanto o kernel do Linux. É como uma bola gigante de linha que não mostra fim. Observar uma parte do código principal geralmente leva a vários outros arquivos relacionados, caso contrário, você esquecerá o que viu. A próxima seção fornece uma dica sobre onde é melhor procurar na árvore de código-fonte para um determinado tópico.

Inicialização e inicialização do sistema

Em um sistema Intel, o kernel inicia quando loadlin.exe ou LILO carrega o kernel na memória e passa o controle para ele. Veja arch/i386/kernel/head.S para esta parte. head.S executa algum trabalho de configuração dependente da arquitetura e salta para a rotina main() em init/main.c.

Gerenciamento de memória

A maior parte do código está em mm, mas o código relacionado à arquitetura está em arch/*/mm. O código de tratamento de falha de página está em mm/memory.c, e o mapa de memória e o código de cache de página estão em mm/filemap.c. O cache de buffer é implementado em mm/buffer.c, e o cache de troca está em mm/swap_state.ce mm/swapfile.c.

Núcleo

A maior parte do código relativamente geral está no kernel, e o código relacionado à arquitetura está em arch/*/kernel. O escalonador está em kernel/sched.c, e o código do fork está em kernel/fork.c. A metade inferior do código de processamento está em include/linux/interrupt.h. A estrutura de dados task_struct pode ser encontrada em include/linux/sched.h

computador

O pseudo driver PCI está em drivers/pci/pci.c, e a definição de todo o sistema está em include/linux/pci.h. Cada arquitetura tem algum código PCI BIOS especial, o Alpha AXP está localizado em arch/alpha/kernel/bios32.c

Comunicação entre processos

Tudo no diretório ipc. Todos os objetos IPC do System V incluem a estrutura de dados ipc_perm, que pode ser encontrada em include/linux/ipc.h. As mensagens do System V são implementadas em ipc/msg.c, a memória compartilhada está em ipc/shm.c e os semáforos estão em ipc/sem.c. Pipes são implementados em ipc/pipe.c.

Manipulação de interrupção

O código principal de tratamento de interrupção é quase sempre específico do microprocessador (e geralmente da plataforma). O código de tratamento de interrupção da Intel está em arch/i386/kernel/irq.ce sua definição está em inude/asm-i386/irq.h.

Drivers de dispositivo

A maioria das linhas de código-fonte do núcleo do Linux está em seus drivers de dispositivo. Todo o código-fonte do driver de dispositivo para Linux está em drivers, mas eles são categorizados:

/block bloqueia drivers de dispositivo como ide (ide.c). Se você quiser ver como todos os dispositivos que podem conter sistemas de arquivos são inicializados, você pode ver device_setup() em drivers/block/genhd.c. Ele não apenas inicializa o disco rígido, mas também inicializa a rede, porque você precisa da rede ao montar o sistema de arquivos nfs. Dispositivos de bloco incluem dispositivos baseados em IDE e SCSI.

/char Aqui você pode ver dispositivos baseados em caracteres como tty, porta serial, etc.

/cdrom Todo o código de CDROM para Linux. Dispositivos de CDROM especiais (por exemplo, CDROM Soundblaster) podem ser encontrados aqui. Observe que o driver do CD ide é ide-cd.c em drivers/block e o driver do CD SCSI está em drivers/scsi/scsi.c

/pci pseudo-driver PCI. Este é um bom lugar para observar como o subsistema PCI é mapeado e inicializado. O código de agrupamento Alpha AXP PCI também vale a pena olhar em arch/alpha/kernel/bios32.c

/scsi Aqui você pode encontrar não apenas drivers para todos os dispositivos scsi suportados pelo Linux, mas também todos os códigos SCSI

/net Aqui você pode encontrar drivers de dispositivos de rede como o driver DEC Chip 21040 PCI Ethernet em tulip.c

/som a localização de todos os drivers da placa de som

Sistemas de arquivos

Os programas fonte do sistema de arquivos EXT2 estão no subdiretório fs/ext2/, e as estruturas de dados são definidas em include/linux/ext2_fs.h, ext2_fs_i.he ext2_fs_sb.h. A estrutura de dados do sistema de arquivos virtual é descrita em include/linux/fs.h, o código é fs/*. O cache de buffer e os daemons de núcleo de atualização são implementados com fs/buffer.c

Rede

O código de rede é colocado no subdiretório net e a maioria dos arquivos de inclusão está em include/net. O código do soquete BSD está em net/socket.c, e o código do soquete Ipv4 INET está em net/ipv4/af_inet.c. O código de suporte para protocolos comuns (incluindo rotinas de manipulação sk_buff) está em net/core, e o código de rede TCP/IP está em net/ipv4. Drivers de dispositivo de rede estão em drivers/net

Módulos

O código do módulo principal está parcialmente no núcleo e parcialmente no pacote de módulos. O código do núcleo está todo em kernel/modules.c, o resultado dos dados e a mensagem do daemon do núcleo kerneld estão em include/linux/module.he include/linux/kerneld.h respectivamente. Você também pode querer ver a estrutura de um arquivo de objeto ELF em include/linux/elf.h.

Apêndice A

Estruturas de dados do Linux

Este apêndice lista as principais estruturas de dados usadas pelo Linux conforme descrito neste livro. Eles foram levemente editados para acessibilidade na página.

Block_dev_struct

A estrutura de dados block_dev_struct é usada para registrar dispositivos de bloco disponíveis para uso de cache de buffer. Eles são colocados na tabela de vetores blk_dev.

veja include/linux/blkdev.h

struct blk_dev_struct {
void (*request_fn)(void);
struct request * current_request;
struct request plug;
struct tq_struct plug_tq;
};

 

buffer_head

A estrutura de dados buffer_head armazena informações sobre um buffer de bloco no cache de buffer.

Veja include/linux/fs.h

dispositivo

Cada dispositivo de rede no sistema é representado por uma estrutura de dados do dispositivo.

Veja include/linux/netdevice.h

device_struct

A estrutura de dados device_struct é usada para registrar dispositivos de caracteres e blocos (mantendo o nome do dispositivo e possíveis operações de arquivo). Cada membro válido das tabelas de vetores Chrdevs e blkdevs representa um caractere ou dispositivo de bloco, respectivamente.

Veja fs/dispositivos.c

struct device_struct {
const char * name;
struct file_operations * fops;
};

Arquivo

Cada arquivo aberto, soquete, etc. é representado por uma estrutura de dados de arquivo.

Veja include/linux/fs.h

estrutura_arquivo

A estrutura de dados file_struct descreve os arquivos abertos por um processo.

veja include/linux/sched.h

gendisk

A estrutura de dados gendisk armazena informações sobre o disco rígido. Usado no processo de inicialização para localizar o disco, ao detectar partições.

Veja include/linux/genhd.h

inode

A estrutura de dados do inode VFS armazena informações sobre um arquivo ou diretório no disco.

Veja include/linux/fs.h

ipc_perm

A estrutura de dados ipc_perm descreve as permissões de acesso para um objeto System V IPC.

Veja include/linux/ipc.h

irqação

A estrutura de dados irqaction descreve os manipuladores de interrupção do sistema.

Veja include/linux/interrupt.h

linux_binfmt

Cada formato de arquivo binário compreendido pelo Linux é representado por uma estrutura de dados linux_binfmt.

Veja include/linux/binfmt.h

mem_map_t

A estrutura de dados mem_map_t (também chamada de página) é usada para armazenar informações sobre cada página da memória física.

veja include/linux/mm.h

estrutura mm

A estrutura de dados mm_struct é usada para descrever a memória virtual de uma tarefa ou processo.

veja include/linux/sched.h

pci_bus

Cada barramento PCI no sistema é representado por uma estrutura de dados pci_bus.

veja include/linux/pci.h

pci_dev

Cada dispositivo PCI no sistema, incluindo dispositivos de ponte PCI-PCI e PCI-ISA, é representado por uma estrutura de dados pci_dev.

veja include/linux/pci.h

solicitar

request é usado para fazer uma solicitação a um dispositivo de bloco no sistema. As solicitações são para ler/gravar blocos de dados de/para o cache de buffer.

veja include/linux/blkdev.h

tabela

Cada estrutura de dados rtable armazena informações sobre a rota de envio de pacotes para um host IP. A estrutura de dados Rtable é usada no cache de rota IP.

veja include/net/route.h

semáforo

Semáforos são usados ​​para proteger estruturas de dados importantes e áreas de código.

Veja include/asm/semaphore.h

sk_buff

A estrutura de dados sk_buff descreve os dados da rede à medida que se movem entre as camadas de protocolo.

Veja include/linux/sk_buff.h

meia

Cada estrutura de dados sock armazena informações relacionadas ao protocolo em um soquete BSD. Por exemplo, para um soquete INET, essa estrutura de dados conterá todas as informações relacionadas a TCP/IP e UDP/IP.

veja include/linux/net.h

soquete

Cada estrutura de dados de soquete armazena informações sobre um soquete BSD. Ele não se sustenta sozinho e, na verdade, faz parte da estrutura de dados do inode VFS.

veja include/linux/net.h

task_struct

Cada task_struct descreve uma tarefa ou processo no sistema.

veja include/linux/sched.h

timer_list

A estrutura de dados timer_list é usada para implementar o timer em tempo real do processo.

Veja include/linux/timer.h

tq_struct

Cada estrutura de dados da fila de tarefas (tq_struct) contém informações sobre o trabalho que está sendo enfileirado. Normalmente, uma tarefa que um driver de dispositivo precisa, mas não precisa ser feita imediatamente.

Veja include/linux/tqueue.h

vm_area_struct

Cada estrutura de dados vm_area_struct descreve uma área de memória virtual de um processo.

veja include/linux/mm.h

Suplementos Adicionais:

1. Hardware Básico (noções básicas de hardware)

1)CPU

A CPU, ou microprocessador, é o coração de qualquer sistema de computador. O microprocessador realiza operações matemáticas, operações lógicas e lê e executa instruções da memória, controlando assim o fluxo de dados. Nos primórdios do desenvolvimento do computador, os vários módulos funcionais dos microprocessadores eram compostos de unidades separadas (e enormes em tamanho). Esta é também a origem do termo "unidade central de processamento". Os microprocessadores modernos combinam esses blocos funcionais em um circuito integrado feito de uma pastilha de silício muito pequena. Neste livro, os termos CPU, microprocessador e processador são usados ​​alternadamente.

Os microprocessadores lidam com dados binários: esses dados consistem em 1s e 0s. Esses 1s e 0s correspondem à ativação ou desativação do interruptor elétrico. Assim como 42 representa 4 unidades de 10 e 2, um número binário consiste em uma série de números que representam potências de 2. Aqui, potência significa o número de vezes que um número é multiplicado por ele mesmo. A primeira potência de 10 é 10, a segunda potência de 10 é 10x10, a terceira potência de 10 é 10x10x10 e assim por diante. O binário 0001 é o decimal 1, o binário 0010 é o decimal 2, o binário 0011 é o decimal 3, o binário 0100 é o decimal 4 e assim por diante. Então, 42 em decimal é 101010 em binário ou (2+8+32 ou 21+23+25). Além de usar binário para representar números em programas de computador, outra base, hexadecimal, é frequentemente usada. Nesta base, cada dígito representa uma potência de 16. Como os números decimais vão de 0 a 9, 10 a 15 em hexadecimal são representados pelas letras A, B, C, D, E, F, respectivamente. Por exemplo, E em hexadecimal é 14 em decimal e 2A em hexadecimal é 42 em decimal (2 16+10). Na notação C (usada em todo este livro), os números hexadecimais são prefixados com "0x": 2A em hexadecimal é escrito como 0x2A.

O microprocessador pode realizar operações aritméticas como adição, multiplicação e divisão, bem como operações lógicas como "é X maior que Y".

A execução do processador é controlada por um relógio externo. Esse clock, o clock do sistema, gera pulsos de clock constantes para o processador e, em cada pulso de clock, o processador realiza algum trabalho. Por exemplo, um processador pode executar uma instrução por pulso de clock. A velocidade do processador é descrita pela frequência do relógio do sistema. Um processador de 100Mhz recebe 100.000.000 pulsos de clock por segundo. É um mal-entendido descrever as capacidades de uma CPU em termos de frequência de clock, porque diferentes processadores realizam diferentes quantidades de trabalho em cada pulso de clock. No entanto, todas as coisas sendo iguais, uma frequência de clock mais rápida significa um processador mais capaz. As instruções executadas pelo processador são muito simples, por exemplo: "Leia o conteúdo da posição de memória X no registrador Y". Os registradores são o espaço de armazenamento interno dos microprocessadores, usados ​​para armazenar dados e realizar operações. A operação realizada pode fazer com que o processador pare a operação atual e, em vez disso, execute instruções em outro lugar da memória. São essas pequenas coleções de instruções que dão ao microprocessador moderno capacidades quase ilimitadas, pois ele pode executar milhões ou mesmo bilhões de instruções por segundo.

As instruções devem ser buscadas na memória quando são executadas, e as próprias instruções podem referenciar dados na memória, que também devem ser buscados na memória e salvos na memória quando necessário.

O tamanho, número e tipo dos registradores internos de um microprocessador são inteiramente determinados por seu tipo. Um processador Intel 80486 e um processador Alpha AXP têm conjuntos de registros completamente diferentes. Além disso, a Intel tem 32 bits de largura e o Alpha AXP tem 64 bits de largura. Em geral, no entanto, todos os processadores específicos terão alguns registradores de propósito geral e alguns registradores de propósito especial. A maioria dos processadores tem registradores dedicados para os seguintes propósitos especiais:

Contador de programa (PC) contador de programa

Este registrador registra o endereço da próxima instrução a ser executada. O conteúdo do PC é incrementado automaticamente cada vez que uma instrução é buscada.

Ponteiro de pilha (SP) ponteiro de pilha

O processador deve ter acesso a uma grande quantidade de memória de acesso aleatório (RAM) de leitura/gravação externa usada para armazenar dados temporariamente. A pilha é um método para armazenar e restaurar dados temporários na memória externa. Normalmente, os processadores fornecem instruções especiais para colocar dados na pilha e buscá-los posteriormente, conforme necessário. A pilha usa o método LIFO (last in first out). Em outras palavras, se você colocar dois valores x e y na pilha e, em seguida, retirar um valor da pilha, você obtém o valor de y.

Alguns processadores têm pilhas que crescem na parte superior da memória, enquanto outros crescem na parte inferior da memória. Existem também alguns processadores que podem suportar as duas formas, por exemplo: ARM.

Status do processador(PS)

As instruções podem produzir resultados. Por exemplo: "O conteúdo do registrador X é maior que o conteúdo do registrador Y?" pode gerar um resultado verdadeiro ou falso. O registrador PS contém esses resultados e outras informações sobre o estado atual do processador. A maioria dos processadores tem pelo menos dois modos: kernel (modo kernel) e usuário (modo usuário), e o registrador PS registrará as informações que podem determinar o modo atual.

2) Memória

Todos os sistemas possuem uma estrutura de memória hierárquica composta por memória em diferentes níveis de velocidade e capacidade.

A memória mais rápida é a memória cache, como o próprio nome indica - usada para armazenar temporariamente ou armazenar em cache o conteúdo da memória principal. Esse tipo de memória é muito rápido, mas relativamente caro, portanto, a maioria dos chips de processador possui uma pequena quantidade de memória cache incorporada, e a maior parte da memória cache está na placa-mãe do sistema. Alguns processadores usam uma memória cache para armazenar instruções e dados, enquanto outros têm dois caches - um para instruções e outro para dados. O processador Alpha AXP possui dois armazenamentos de cache integrados na memória: um para dados (D-Cache) e outro para instruções (I-Cache). Seu cache externo (ou B-Cache) mistura os dois.

O último tipo de memória é a memória principal. Muito lenta em relação à memória cache externa, a memória principal é literalmente um rastreamento para a memória cache incorporada à CPU.

A memória cache e a memória principal devem estar sincronizadas (coerentes). Em outras palavras, se uma palavra na memória principal for mantida em um ou mais locais na memória cache, o sistema deve garantir que o conteúdo da memória cache e da memória principal seja o mesmo. Parte do trabalho de sincronização dos caches é feito pelo hardware e a outra parte é feita pelo sistema operacional. Para algumas outras tarefas importantes do sistema, hardware e software também devem trabalhar juntos.

3) Ônibus

Os vários componentes da placa do sistema são interconectados por um sistema de conexões chamado barramento. O barramento do sistema é dividido em três funções lógicas: barramento de endereço, barramento de dados e barramento de controle. O barramento de endereço especifica o local de memória (endereço) da transferência de dados e o barramento de dados armazena os dados transferidos. O barramento de dados é bidirecional, permite que a CPU leia e também permite que a CPU escreva. O barramento de controle contém várias linhas de sinal usadas para enviar sinais de relógio e controle no sistema. Existem muitos tipos de barramento diferentes, os barramentos ISA e PCI são as formas comuns que os sistemas usam para conectar periféricos.

4) Controladores e Periféricos

Periféricos referem-se a dispositivos físicos, como placas gráficas ou discos controlados por um chip de controle na placa do sistema ou placas complementares da placa do sistema. O chip controlador IDE controla os discos IDE, enquanto o chip controlador SCSI controla os discos SCSI. Esses controladores são conectados à CPU e entre si por meio de diferentes barramentos. A maioria dos sistemas fabricados atualmente usa o barramento PCI ou ISA para conectar os principais componentes do sistema. O próprio controlador também é um processador como a CPU, eles podem ser considerados como o assistente inteligente da CPU, e a CPU tem o maior controle do sistema.

Todos os controladores são diferentes, mas geralmente possuem registradores usados ​​para controlá-los. O software rodando na CPU deve ser capaz de ler e escrever estes registradores de controle. Um registrador pode conter um código de status descrevendo o erro e outro registrador pode ser usado para fins de controle, alterando o modo do controlador. Cada controlador em um barramento pode ser endereçado individualmente pela CPU, para que os drivers de dispositivo de software possam ler e gravar seus registros para controlá-lo. Um cabo IDE é um bom exemplo, pois permite acessar cada unidade no barramento individualmente. Outro bom exemplo é o barramento PCI, que permite que cada dispositivo (como uma placa gráfica) seja acessado independentemente.

5) Espaços de endereçamento

O barramento do sistema que conecta a CPU e a memória principal e o barramento que conecta a CPU e os periféricos de hardware do sistema são separados. O espaço de memória pertencente aos periféricos de hardware é chamado de espaço de E/S. O próprio espaço de E/S pode ser dividido ainda mais, mas não o discutiremos por enquanto. A CPU pode acessar o espaço de memória do sistema e o espaço de E/S, enquanto o controlador só pode acessar a memória do sistema indiretamente através da CPU. Da perspectiva de um dispositivo, como um controlador de unidade de disquete, ele vê apenas o espaço de endereço (ISA) onde residem seus registros de controle, não a memória do sistema. Uma CPU usa instruções diferentes para acessar a memória e o espaço de E/S. Por exemplo, pode haver uma instrução que diz "ler um byte do endereço de E/S 0x3f0 no registrador X". Este também é o método pelo qual a CPU controla os periféricos lendo e escrevendo os registradores dos periféricos de hardware do sistema no espaço de endereço de E/S. No espaço de endereçamento, os registros de periféricos comuns (como controladores IDE, portas seriais, controladores de disquete, etc.) tornaram-se a norma no desenvolvimento de periféricos de PC ao longo dos anos. O endereço 0x3f0 do espaço de E/S é o endereço do registro de controle da porta serial (COM1).

Às vezes, o controlador precisa ler grandes quantidades de memória diretamente da memória do sistema ou gravar grandes quantidades de dados diretamente na memória do sistema. Por exemplo, grave os dados do usuário no disco rígido. Neste caso, um controlador de acesso direto à memória (DMA) é usado, permitindo que dispositivos de hardware acessem diretamente a memória do sistema, mas é claro que esse acesso deve ser feito sob estrito controle e supervisão da CPU. 

6) Temporizador (relógio)

Todos os sistemas operacionais precisam saber a hora, e os PCs modernos incluem um periférico especial chamado relógio em tempo real (RTC). Ele fornece duas coisas: datas confiáveis ​​e intervalos de tempo precisos. O RTC tem sua própria bateria, portanto, mesmo que o PC não esteja ligado, ele ainda está funcionando. É também por isso que o PC sempre "sabe" a data e hora corretas. O tempo de intervalo permite que o sistema operacional agende com precisão o trabalho essencial.

A arquitetura Alpha AXP é uma arquitetura RISC de carregamento/armazenamento de 64 bits projetada para velocidade. Todos os registradores têm 64 bits: 32 registradores inteiros e 32 registradores de ponto flutuante. O 31º registrador inteiro e o 31º registrador de ponto flutuante são usados ​​para operações nulas: lê-los dá 0, escrever neles não produz nada. Todas as instruções e operações de memória (sejam leitura ou gravação) são de 32 bits. Diferentes implementações são permitidas desde que a implementação concreta siga esta arquitetura.

Não há instruções para manipular valores diretamente na memória: todas as operações de dados são realizadas entre registradores. Então, se você quiser incrementar um contador na memória, você deve primeiro lê-lo em um armazenamento, modificá-lo e depois gravá-lo de volta. A interação entre instruções só é possível se uma instrução escreve em um registrador ou local de memória e outra lê esse registrador ou local de memória. Uma característica interessante do Alpha AXP é que ele possui instruções que podem gerar bits de flag, como testar se dois inteiros são iguais, essa estrutura não é armazenada em um dos registradores de status do processador, mas em um terceiro registrador. Pode parecer estranho no começo, mas não confiar no registrador de status significa que é mais fácil fazer com que a CPU execute várias instruções por ciclo. Instruções que usam registradores não relacionados durante a execução não precisam esperar umas pelas outras e devem esperar se houver apenas um registrador de status. Não há manipulação direta da memória e o grande número de registradores também é útil para várias instruções ao mesmo tempo.

A arquitetura Alpha AXP usa uma série de sub-rotinas chamadas de código de biblioteca de arquitetura privilegiada PALcode. A implementação específica do PALcode e do sistema operacional e da CPU do sistema Alpha AXP está relacionada ao hardware do sistema. Essas sub-rotinas fornecem ao sistema operacional suporte básico para troca de contexto, interrupções, exceções e gerenciamento de memória. Estas sub-rotinas podem ser chamadas por hardware ou pela instrução CALL_PAL. O PALcode é escrito no montador Alpha AXP padrão, com a implementação de algumas extensões especiais para fornecer acesso direto ao hardware de baixo nível, como registradores internos do processador. O PALcode é executado no modo PAL, um modo privilegiado que impede o envio de alguns eventos do sistema e permite que o PALcode assuma o controle total do hardware físico do sistema.

2. Software Básico

Um programa é uma combinação de instruções do computador para executar uma tarefa específica. Os programas podem ser escritos em linguagem assembly, uma linguagem de computador de nível muito baixo ou em linguagens independentes de máquina de alto nível, como C. Um sistema operacional é um programa especial que permite aos usuários executar aplicativos por meio dele, como planilhas e processadores de texto.

2.1 Linguagens de Computador

2.1.1. Linguagem de montagem

As instruções que a CPU lê e executa da memória são incompreensíveis para os humanos. São códigos de máquina que dizem ao computador exatamente o que fazer. Por exemplo, o número hexadecimal 0x89E5 é uma instrução Intel 80486 para copiar o conteúdo do registrador ESP para o registrador EBP. Uma das primeiras ferramentas de software nos primeiros computadores foi o montador, que pegava arquivos de origem legíveis por humanos e os montava em código de máquina. A linguagem assembly lida explicitamente com operações em registradores e dados específicos de um microprocessador específico. A linguagem assembly do microprocessador Intel X86 é completamente diferente da linguagem assembly do microprocessador Alpha AXP. O seguinte código de montagem Alpha AXP demonstra os tipos de operações que um programa pode realizar:

Ldr r16, (r15) ; 第一行
Ldr r17, 4(r15) ; 第二行
Beq r16,r17,100; 第三行
Str r17, (r15); 第四行
100: ; 第五行

A primeira instrução (linha 1) carrega o conteúdo do endereço especificado pelo registrador 15 no registrador 16. A segunda instrução carrega o conteúdo da memória seguinte no registrador 17. A terceira linha compara o registrador 16 e o ​​registrador 17, se forem iguais, ramifica para o rótulo 100, caso contrário, continua a executar a quarta linha e armazena o conteúdo do registrador 17 na memória. Se os dados na memória forem os mesmos, não há necessidade de armazenar os dados. Escrever programas em nível de assembly é complicado, tedioso e propenso a erros. Poucas partes do núcleo do sistema Linux são escritas em linguagem assembly, e a razão pela qual essas partes usam linguagem assembly é apenas para melhorar a eficiência e está relacionada a microprocessadores específicos.

2.1.2 A linguagem de programação C e o compilador

Escrever programas grandes em linguagem assembly é difícil, demorado, propenso a erros e os programas resultantes não são portáveis ​​e estão vinculados a uma família de processadores específica. Uma opção melhor é usar uma linguagem independente de máquina, como C. C permite descrever programas e dados a serem processados ​​em algoritmos lógicos. Um programa especial chamado compilador lê um programa C e o converte em linguagem assembly, que por sua vez produz código dependente de máquina. Um bom compilador pode gerar instruções de montagem próximas da eficiência de um programa escrito por um bom programador de montagem. A maior parte do kernel do Linux é escrita em linguagem C. O seguinte trecho de C:

if (x != y)
x = y;

Executa exatamente a mesma operação que o código assembly no exemplo anterior. Se o conteúdo da variável x não for igual ao conteúdo da variável y, o conteúdo da variável y será copiado para a variável x. O código C é composto de rotinas, cada uma das quais executa uma tarefa. As rotinas podem retornar qualquer número ou tipo de dados suportado por C. Programas grandes, como o kernel do Linux, são compostos de muitos módulos da linguagem C, cada um com suas próprias rotinas e estruturas de dados. Esses módulos de código-fonte C constituem coletivamente o código de processamento para funções lógicas, como o sistema de arquivos.

C suporta muitos tipos de variáveis. Uma variável é um local específico na memória que pode ser referenciado por um nome simbólico. No trecho de C acima, x e y referem-se a locais na memória. O programador não precisa se preocupar com a localização exata da variável na memória, é isso que o linker (descrito abaixo) tem que lidar. Algumas variáveis ​​contêm vários dados, como números inteiros, números de ponto flutuante, etc. e outras contêm ponteiros.

Um ponteiro é uma variável que contém o endereço de outros dados na memória. Assumindo uma variável x, localizada no endereço de memória 0x80010000, você pode ter um ponteiro px que aponta para x. Px pode estar no endereço 0x80010030. O valor de Px é o endereço da variável x, 0x80010000.

C permite agrupar variáveis ​​relacionadas em estruturas. Por exemplo:

Struct {
Int I;
Char b;
} my_struct;

é uma estrutura de dados chamada my_struct, que consiste em dois elementos: um inteiro (32 bits) I e um caractere (dados de 8 bits) b.

2.1.3 Ligantes

O vinculador vincula vários módulos de objeto e arquivos de biblioteca em um único programa completo. Um módulo de objeto é a saída de código de máquina de um montador ou compilador, que inclui código de máquina, dados e informações do vinculador para uso do vinculador. Por exemplo, um módulo de objeto pode incluir todas as funções de banco de dados do programa, enquanto outro módulo de objeto inclui funções que tratam de argumentos de linha de comando. O vinculador determina o relacionamento de referência entre os módulos de destino, ou seja, determina a localização real das rotinas e dados referenciados por um módulo em outro módulo. O núcleo do Linux é um grande programa independente conectado por vários módulos de objeto.

2.2 O que é um sistema operacional

Sem software, um computador é apenas um monte de componentes eletrônicos quentes. Se o hardware é o coração de um computador, o software é sua alma. Um sistema operacional é um conjunto de programas de sistema que permitem aos usuários executar aplicativos. O sistema operacional abstrai o hardware do sistema e apresenta uma máquina virtual na frente de usuários e aplicativos. É um software que caracteriza um sistema de computador. A maioria dos PCs pode executar um ou mais sistemas operacionais, cada um com aparência e sensação muito diferentes. O Linux é composto de partes com funções diferentes, e a combinação geral dessas partes compõe o sistema operacional Linux. A parte mais óbvia do Linux é o próprio Kernel, mas é inútil sem shell ou bibliotecas.

Para entender o que é um sistema operacional, veja o que acontece quando você digita o comando mais simples:

$ls
Mail c images perl
Docs tcl
$

O $ aqui é a saída do prompt do shell conectado (bash neste caso): significa que o shell está esperando que você (o usuário) digite um comando. Digitar ls faz com que o driver do teclado reconheça os caracteres inseridos e o driver do teclado passa os caracteres reconhecidos para o shell para processamento. O shell primeiro procura uma imagem executável com o mesmo nome, encontra /bin/ls e, em seguida, chama o serviço principal para carregar o executor ls na memória virtual e iniciar a execução. O executor ls encontra arquivos executando chamadas de sistema do subsistema de arquivos do núcleo. O sistema de arquivos pode usar as informações do sistema de arquivos em cache ou ler as informações do arquivo do disco através do driver do dispositivo de disco, ou pode ler as informações detalhadas do arquivo remoto acessado pelo sistema trocando informações com o host remoto através do dispositivo de rede driver (Os sistemas de arquivos podem ser montados remotamente via sistemas de arquivos de rede NFS). Independentemente de como as informações do arquivo são obtidas, ls gera as informações e as exibe na tela por meio do driver de vídeo.

O processo acima parece bastante complicado, mas mostra que mesmo os comandos mais simples são resultado da cooperação entre vários módulos funcionais do sistema operacional, e somente desta forma pode fornecer a você (usuário) uma visão completa do sistema.

2.2.1 Gerenciamento de memória

Com recursos ilimitados, como memória, muito do que o sistema operacional precisa fazer pode ser redundante. Um truque fundamental de todos os sistemas operacionais é fazer com que uma pequena quantidade de memória física funcione como se houvesse uma quantidade considerável de memória. Essa memória superficialmente grande é chamada de memória virtual e, quando o software está em execução, faz com que acredite que tem muita memória. O sistema divide a memória em páginas gerenciáveis ​​e troca essas páginas para o disco rígido enquanto o sistema está em execução. O software aplicativo não sabe, pois o sistema operacional também utiliza outra tecnologia: o multiprocessamento.

2.2.2 Processos

Um processo pode ser visto como um programa em execução, e cada processo é uma entidade independente de um programa específico que está sendo executado. Se você observar seu sistema Linux, verá que existem muitos processos em execução. Por exemplo: digitar ps no meu sistema mostra os seguintes processos:

$ ps
PID TTY STAT TIME COMMAND
158 pRe 1 0:00 -bash
174 pRe 1 0:00 sh /usr/X11R6/bin/startx
175 pRe 1 0:00 xinit /usr/X11R6/lib/X11/xinit/xinitrc --
178 pRe 1 N 0:00 bowman
182 pRe 1 N 0:01 rxvt -geometry 120x35 -fg white -bg black
184 pRe 1 < 0:00 xclock -bg grey -geometry -1500-1500 -padding 0
185 pRe 1 < 0:00 xload -bg grey -geometry -0-0 -label xload
187 pp6 1 9:26 /bin/bash
202 pRe 1 N 0:00 rxvt -geometry 120x35 -fg white -bg black
203 ppc 2 0:00 /bin/bash
1796 pRe 1 N 0:00 rxvt -geometry 120x35 -fg white -bg black
1797 v06 1 0:00 /bin/bash
3056 pp6 3 < 0:02 emacs intro/introduction.tex
3270 pp6 3 0:00 ps
$

Se meu sistema tiver várias CPUs, cada processo pode (pelo menos em teoria) estar sendo executado em uma CPU diferente. Infelizmente, há apenas um, então o sistema operacional novamente usa truques para executar cada processo por um curto período de tempo. Esse período de tempo é chamado de fatia de tempo. Esse truque é chamado de multiprocessamento ou escalonamento, e faz com que todos os processos pareçam ser os únicos. Os processos são protegidos uns dos outros, portanto, se um processo travar ou não funcionar, isso não afetará outros processos. O sistema operacional implementa a proteção dando a cada processo um espaço de endereçamento separado, e um processo só pode acessar seu próprio espaço de endereçamento.

2.2.3 Drivers de Dispositivo

Os drivers de dispositivo formam a parte principal do kernel do Linux. Como outras partes do sistema operacional, eles funcionam em um ambiente de alta prioridade e, se algo der errado, podem causar sérios problemas. Os drivers de dispositivo controlam a interação entre o sistema operacional e os dispositivos de hardware que ele controla. Por exemplo, o sistema de arquivos grava blocos de dados em discos IDE usando a interface de dispositivo de bloco genérico. O driver controla os detalhes e manipula as partes específicas do dispositivo. Um driver de dispositivo está relacionado ao chip controlador específico que ele dirige, portanto, se o seu sistema tiver um controlador SCSI NCR810, você precisará do driver NCR810.

2.2.4 Os Sistemas de Arquivos

Como o Unix, no Linux, o sistema não usa identificadores de dispositivo (como números de unidade ou nomes de unidade) para acessar sistemas de arquivos individuais, mas está vinculado a uma estrutura de árvore. Quando o Linux instala um novo sistema de arquivos, ele o instala em um diretório de instalação especificado, como /mnt/cdrom, fundindo-se assim nessa única árvore do sistema de arquivos. Uma característica importante do Linux é que ele suporta muitos sistemas de arquivos diferentes. Isso o torna muito flexível e pode coexistir bem com outros sistemas operacionais. O sistema de arquivos mais comumente usado para Linux é o EXT2, que é suportado pela maioria das distribuições Linux.

O sistema de arquivos fornece os arquivos e diretórios armazenados no disco rígido do sistema para o usuário de forma compreensível e unificada, para que o usuário não precise considerar o tipo do sistema de arquivos ou as características do dispositivo físico subjacente. O Linux oferece suporte transparente a vários sistemas de arquivos (como MS-DOS e EXT2) e integra todos os arquivos e sistemas de arquivos instalados em um sistema de arquivos virtual. Portanto, usuários e processos geralmente não precisam saber exatamente em que tipo de sistema de arquivos estão os arquivos que eles usam, apenas use-os.

Os drivers de dispositivo de bloco mascaram a distinção entre os tipos de dispositivo de bloco físico (por exemplo, IDE e SCSI). Para um sistema de arquivos, um dispositivo físico é uma coleção linear de blocos de dados. O tamanho do bloco de diferentes dispositivos pode ser diferente.Por exemplo, unidades de disquete geralmente têm 512 bytes, enquanto os dispositivos IDE geralmente têm 1024 bytes. Novamente, essas diferenças são mascaradas para usuários do sistema. O sistema de arquivos EXT2 parece o mesmo, independentemente do dispositivo usado.

2.3 Estruturas de Dados Kernet

O sistema operacional deve registrar muitas informações sobre o estado atual do sistema. Se algo acontecer no sistema, essas estruturas de dados devem mudar de acordo para refletir a realidade atual. Por exemplo, quando um usuário efetua login no sistema, um novo processo precisa ser criado. O kernel deve criar as estruturas de dados que representam esse novo processo, vinculadas às estruturas de dados que representam outros processos no sistema.

Tais estruturas de dados estão principalmente na memória física e só podem ser acessadas pelo núcleo e seus subsistemas. As estruturas de dados incluem dados e ponteiros (endereços de outras estruturas de dados ou rotinas). À primeira vista, as estruturas de dados usadas pelo kernel do Linux podem ser bastante confusas. Na verdade, cada estrutura de dados tem uma finalidade e, embora algumas estruturas de dados sejam usadas em vários subsistemas, elas são muito mais simples do que quando você as vê pela primeira vez.

A chave para entender o kernel do Linux é entender suas estruturas de dados e o grande número de funções que o kernel usa para processar essas estruturas de dados. Este livro descreve o kernel do Linux em termos de estruturas de dados. Discute os algoritmos de cada subsistema principal, a maneira como são processados ​​e o uso das estruturas de dados principais.

2.3.1 Listas Vinculadas

O Linux usa uma técnica de engenharia de software para conectar suas estruturas de dados. Na maioria das vezes, ele usa uma estrutura de dados de lista vinculada. Se cada estrutura de dados descreve uma única instância de um objeto ou evento, como um processo ou um dispositivo de rede, o kernel deve ser capaz de encontrar todas as instâncias. Em uma lista vinculada, o ponteiro raiz contém o endereço da primeira estrutura de dados ou unidade e cada estrutura de dados na lista contém um ponteiro para o próximo elemento da lista. O próximo ponteiro para o último elemento pode ser 0 ou NULL, indicando que este é o fim da lista. Em uma estrutura de lista duplamente vinculada, cada elemento inclui não apenas um ponteiro para o próximo elemento da lista, mas também um ponteiro para o elemento anterior da lista. O uso de uma lista duplamente vinculada facilita a adição ou remoção de elementos do meio da lista, mas requer mais acesso à memória. Este é um dilema típico do sistema operacional: o número de acessos à memória ou o número de ciclos de CPU.

2.3.2 Tabelas de Hash

As listas vinculadas são uma estrutura de dados comum, mas percorrer as listas vinculadas pode não ser eficiente. Se você estiver procurando por um elemento específico, talvez seja necessário pesquisar a tabela inteira para encontrá-lo. O Linux usa outra técnica: Hashing para resolver essa limitação. A tabela de hash é uma tabela de vetores ou array de ponteiros. Arrays ou tabelas de vetores são objetos que são colocados sequencialmente na memória. Pode-se dizer que uma estante é um conjunto de livros. Arrays são acessados ​​usando índices, que são deslocamentos dentro do array. Voltando ao exemplo da estante, você pode usar a posição na estante para descrever cada livro: digamos o livro 5.

Uma tabela de hash é uma matriz de ponteiros para estruturas de dados cujos índices são derivados das informações nas estruturas de dados. Se você usar uma estrutura de dados para descrever a população de uma vila, poderá usar a idade como um índice. Para descobrir os dados de uma determinada pessoa, você pode usar sua idade como um índice para pesquisar na tabela de hash da população e encontrar a estrutura de dados, incluindo as informações detalhadas por ponteiro. Infelizmente, pode haver muitas pessoas em uma aldeia da mesma idade, então o ponteiro da tabela de hash aponta para outra estrutura de dados de lista vinculada, cada elemento descrevendo um par. Mesmo assim, pesquisar essas listas vinculadas menores ainda é mais rápido do que pesquisar todas as estruturas de dados.

As tabelas de hash podem ser usadas para acelerar o acesso a estruturas de dados comumente usadas, e as tabelas de hash são comumente usadas no Linux para implementar buffering. Buffering é uma informação que precisa ser acessada rapidamente e é um subconjunto do total de informações disponíveis. As estruturas de dados são colocadas em buffers e mantidas lá porque o núcleo frequentemente acessa essas estruturas. O uso de buffers também tem efeitos colaterais, pois é mais complicado de usar do que uma simples lista vinculada ou tabela de hash. Se a estrutura de dados puder ser encontrada no buffer (isso é chamado de buffer hit), tudo estará perfeito. Mas se a estrutura de dados não estiver no buffer, a estrutura de dados relevante usada deve ser pesquisada e, se encontrada, será adicionada ao buffer. Adicionar novas estruturas de dados ao buffer pode exigir o descarte de uma entrada de buffer antiga. O Linux precisa decidir qual estrutura de dados deve ser preterida, correndo o risco de preterir qual estrutura de dados o Linux pode acessar em seguida.

2.3.3 Interfaces Abstratas

O kernel do Linux geralmente abstrai suas interfaces. Uma interface é uma série de rotinas e estruturas de dados que funcionam de uma maneira específica. Por exemplo: Todos os drivers de dispositivo de rede devem fornecer rotinas específicas para lidar com estruturas de dados específicas. Na forma de interface abstrata, a camada de código geral pode usar os serviços (interfaces) fornecidos pelo código especial subjacente. Por exemplo, a camada de rede é genérica, embora seja suportada pelo código específico do dispositivo subjacente que está em conformidade com uma interface padrão.

Normalmente, essas camadas inferiores são registradas com as camadas superiores na inicialização. Esse processo de registro geralmente é implementado adicionando uma estrutura de dados à lista vinculada. Por exemplo, cada sistema de arquivos vinculado ao kernel é registrado quando o kernel é iniciado (ou se você usa módulos, na primeira vez que o sistema de arquivos é usado). Você pode visualizar o arquivo /proc/filesystems para verificar quais sistemas de arquivos estão registrados. As estruturas de dados usadas para registro geralmente incluem ponteiros para funções. Este é o endereço de uma função de software que executa uma tarefa específica. Usando o exemplo de registro do sistema de arquivos novamente, cada estrutura de dados passada para o kernel do Linux durante o registro do sistema de arquivos inclui o endereço de uma rotina associada a um sistema de arquivos específico, que deve ser chamado quando o sistema de arquivos é montado.

5. Gerenciamento de kernel Linux

1. Pasta virtual

1. Introdução às pastas virtuais

Pastas virtuais, porque seu conteúdo de dados é armazenado na memória, não no disco rígido, /proc e /sys são pastas virtuais. Alguns desses arquivos retornarão muitas informações quando visualizados com o comando view, mas o tamanho do arquivo em si será exibido como 0 bytes. Além disso, os atributos de hora e data da maioria desses arquivos especiais são geralmente a data e hora atuais do sistema, pois são atualizados (armazenados na RAM) a qualquer momento.

Visão externa do desempenho do sistema de arquivos /proc e fornece aos usuários uma visão das estruturas de dados internas do kernel. Ele pode ser usado para visualizar e modificar certas estruturas de dados internas do kernel, alterando assim o comportamento do kernel.

O sistema de arquivos /proc fornece uma maneira fácil de melhorar o desempenho geral do aplicativo e do sistema ajustando os recursos do sistema.O sistema de arquivos /proc é um sistema de arquivos virtual criado dinamicamente pelo kernel para gerar dados. Ele é organizado em diretórios, cada um dos quais corresponde a opções ajustáveis ​​para um subsistema específico.

O diretório /proc contém muitos subdiretórios nomeados com números, e esses números representam o número do processo em execução no sistema, que contém vários arquivos de informações relacionados ao processo correspondente.

[root@rhel5 ~]# ll /proc
total 0
dr-xr-xr-x  5 root      root              0 Feb  8 17:08 1
dr-xr-xr-x  5 root      root              0 Feb  8 17:08 10
dr-xr-xr-x  5 root      root              0 Feb  8 17:08 11
dr-xr-xr-x  5 root      root              0 Feb  8 17:08 1156
dr-xr-xr-x  5 root      root              0 Feb  8 17:08 139
dr-xr-xr-x  5 root      root              0 Feb  8 17:08 140
dr-xr-xr-x  5 root      root              0 Feb  8 17:08 141
dr-xr-xr-x  5 root      root              0 Feb  8 17:09 1417
dr-xr-xr-x  5 root      root              0 Feb  8 17:09 1418

Listados acima estão alguns diretórios relacionados ao processo no diretório /proc, e cada diretório é um arquivo com informações sobre o próprio processo. Seguem arquivos relacionados de um processo saslauthd com PID 2674 rodando no sistema do autor (RHEL5.3), alguns dos quais são arquivos que todo processo terá.

[root@rhel5 ~]# ll /proc/2674
total 0
dr-xr-xr-x 2 root root 0 Feb  8 17:15 attr
-r-------- 1 root root 0 Feb  8 17:14 auxv
-r--r--r-- 1 root root 0 Feb  8 17:09 cmdline
-rw-r--r-- 1 root root 0 Feb  8 17:14 coredump_filter
-r--r--r-- 1 root root 0 Feb  8 17:14 cpuset
lrwxrwxrwx 1 root root 0 Feb  8 17:14 cwd -> /var/run/saslauthd
-r-------- 1 root root 0 Feb  8 17:14 environ
lrwxrwxrwx 1 root root 0 Feb  8 17:09 exe -> /usr/sbin/saslauthd
dr-x------ 2 root root 0 Feb  8 17:15 fd
-r-------- 1 root root 0 Feb  8 17:14 limits
-rw-r--r-- 1 root root 0 Feb  8 17:14 loginuid
-r--r--r-- 1 root root 0 Feb  8 17:14 maps
-rw------- 1 root root 0 Feb  8 17:14 mem
-r--r--r-- 1 root root 0 Feb  8 17:14 mounts
-r-------- 1 root root 0 Feb  8 17:14 mountstats
-rw-r--r-- 1 root root 0 Feb  8 17:14 oom_adj
-r--r--r-- 1 root root 0 Feb  8 17:14 oom_score
lrwxrwxrwx 1 root root 0 Feb  8 17:14 root -> /
-r--r--r-- 1 root root 0 Feb  8 17:14 schedstat
-r-------- 1 root root 0 Feb  8 17:14 smaps
-r--r--r-- 1 root root 0 Feb  8 17:09 stat
-r--r--r-- 1 root root 0 Feb  8 17:14 statm
-r--r--r-- 1 root root 0 Feb  8 17:10 status
dr-xr-xr-x 3 root root 0 Feb  8 17:15 task
-r--r--r-- 1 root root 0 Feb  8 17:14 wchan

cmdline — o comando completo para iniciar o processo atual, mas esse arquivo no diretório do processo zumbi não contém informações;

[root@rhel5 ~]# more /proc/2674/cmdline 
/usr/sbin/saslauthd

environ — uma lista de variáveis ​​de ambiente para o processo atual, separadas umas das outras por um caractere NULL; as variáveis ​​são representadas por letras maiúsculas e seus valores são representados por letras minúsculas;

[root@rhel5 ~]# more /proc/2674/environ 
TERM=linuxauthd

cwd — um link simbólico para o diretório onde o processo atual está sendo executado;

exe — um link simbólico para o executável (caminho completo) que iniciou o processo atual, uma cópia do processo atual pode ser iniciada via /proc/N/exe;

fd — este é um diretório contendo um descritor de arquivo para cada arquivo aberto pelo processo atual, que é um link simbólico para o arquivo real;

[root@rhel5 ~]# ll /proc/2674/fd
total 0
lrwx------ 1 root root 64 Feb  8 17:17 0 -> /dev/null
lrwx------ 1 root root 64 Feb  8 17:17 1 -> /dev/null
lrwx------ 1 root root 64 Feb  8 17:17 2 -> /dev/null
lrwx------ 1 root root 64 Feb  8 17:17 3 -> socket:[7990]
lrwx------ 1 root root 64 Feb  8 17:17 4 -> /var/run/saslauthd/saslauthd.pid
lrwx------ 1 root root 64 Feb  8 17:17 5 -> socket:[7991]
lrwx------ 1 root root 64 Feb  8 17:17 6 -> /var/run/saslauthd/mux.accept

limites — limites flexíveis, limites rígidos e unidades de gerenciamento para cada recurso restrito usado pelo processo atual; este arquivo só pode ser lido pelo usuário UID que realmente iniciou o processo atual; (esse recurso é suportado nas versões do kernel após 2.6.24) ;

mapas — uma lista de regiões mapeadas na memória e suas permissões de acesso para cada arquivo executável e de biblioteca associado ao processo atual;

[root@rhel5 ~]# cat /proc/2674/maps 
00110000-00239000 r-xp 00000000 08:02 130647     /lib/libcrypto.so.0.9.8e
00239000-0024c000 rwxp 00129000 08:02 130647     /lib/libcrypto.so.0.9.8e
0024c000-00250000 rwxp 0024c000 00:00 0 
00250000-00252000 r-xp 00000000 08:02 130462     /lib/libdl-2.5.so
00252000-00253000 r-xp 00001000 08:02 130462     /lib/libdl-2.5.so

mem — o espaço de memória ocupado pelo processo atual, usado por chamadas de sistema como open, read e lseek, e não pode ser lido pelo usuário;

root — um link simbólico para o diretório raiz do processo atual; em sistemas Unix e Linux, o comando chroot geralmente é usado para fazer com que cada processo seja executado em um diretório raiz separado;

stat — informações de status do processo atual, incluindo uma coluna de dados formatada pelo sistema, com baixa legibilidade, normalmente utilizada pelo comando ps;

statm — informações de status sobre a memória ocupada pelo processo atual, geralmente expressas em "páginas";

status — semelhante às informações fornecidas pelo stat, mas com melhor legibilidade, conforme mostrado abaixo, cada linha representa uma informação de atributo; consulte a página man do proc para obter detalhes;

[root@rhel5 ~]# more /proc/2674/status 
Name:   saslauthd
State:  S (sleeping)
SleepAVG:       0%
Tgid:   2674
Pid:    2674
PPid:   1
TracerPid:      0
Uid:    0       0       0       0
Gid:    0       0       0       0
FDSize: 32
Groups:
VmPeak:     5576 kB
VmSize:     5572 kB
VmLck:         0 kB
VmHWM:       696 kB
VmRSS:       696 kB
…………

task — Um arquivo de diretório que contém informações sobre cada encadeamento executado pelo processo atual. O arquivo de informações relevantes para cada encadeamento é armazenado em um diretório nomeado pelo número do encadeamento (tid), que é semelhante ao conteúdo de cada encadeamento. do diretório do processo; (esta função é suportada após a versão 2.6 do kernel).

2. Introdução aos arquivos comuns no diretório /proc

/proc

proc é a abreviação de processo. Este arquivo de diretório armazena as informações relevantes do processo.As informações do processo e o estado do kernel no sistema são colocados no proc, que é uma pasta virtual, e as informações de dados correspondentes são o estado na memória;

/proc/aprox

Informações de versão do Advanced Power Management (APM) e informações de status relacionadas à bateria, geralmente usadas pelo comando apm;

/proc/buddyinfo

Arquivos de informações relevantes para diagnosticar problemas de fragmentação de memória;

/proc/cmdline

As informações de parâmetros relevantes passadas ao kernel na inicialização, que geralmente são passadas por ferramentas de gerenciamento de inicialização, como lilo ou grub;

[root@rhel5 ~]# more /proc/cmdline 
ro root=/dev/VolGroup00/LogVol00 rhgb quiet

/proc/cpuinfo

Um arquivo com informações sobre o processador; 

/proc/crypto

Uma lista de algoritmos criptográficos usados ​​pelos kernels instalados no sistema e detalhes de cada algoritmo;

[root@rhel5 ~]# more /proc/crypto 
name         : crc32c
driver       : crc32c-generic
module       : kernel
priority     : 0
type         : digest
blocksize    : 32
digestsize   : 4
…………

/proc/dispositivos

Informações sobre todos os dispositivos de bloco e dispositivos de caracteres carregados pelo sistema, incluindo o número do dispositivo principal e o nome do grupo de dispositivos (tipo de dispositivo correspondente ao número do dispositivo principal); 

[root@rhel5 ~]# more /proc/devices 
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  4 ttyS
  …………

Block devices:
  1 ramdisk
  2 fd
  8 sd
  …………

/proc/diskstats

Lista de estatísticas de E/S de disco para cada dispositivo de disco (versões após o kernel 2.5.69 suportam esta função);

/proc/dma

Uma lista de informações sobre cada canal ISA DMA em uso e registrado;

[root@rhel5 ~]# more /proc/dma
2: floppy
4: cascade

/proc/execdomains

Uma lista de informações sobre os domínios de execução atualmente suportados pelo kernel (a "personalidade" exclusiva de cada sistema operacional); 

[root@rhel5 ~]# more /proc/execdomains 
0-0     Linux                   [kernel]

/proc/fb

Arquivo de lista de dispositivos de buffer de quadro, incluindo o número do dispositivo e informações relacionadas ao driver do dispositivo de buffer de quadro; 

/proc/sistemas de arquivos

O arquivo de lista de tipos de sistema de arquivos atualmente suportado pelo kernel, o sistema de arquivos marcado como nodev indica que o suporte a dispositivos de bloco não é necessário; geralmente ao montar um dispositivo, se o tipo de sistema de arquivos não for especificado, este arquivo será usado para determinar o sistema de arquivos necessário.tipo;

[root@rhel5 ~]# more /proc/filesystems 
nodev   sysfs
nodev   rootfs
nodev   proc
        iso9660
        ext3
…………
…………

/proc/interrompe

Uma lista de números de interrupção relacionados a cada IRQ em um sistema de arquitetura X86 ou X86_64; cada CPU em uma plataforma multiprocessadora tem seu próprio número de interrupção para cada dispositivo de E/S; 

[root@rhel5 ~]# more /proc/interrupts 
           CPU0       
  0:    1305421    IO-APIC-edge  timer
  1:         61    IO-APIC-edge  i8042
185:       1068   IO-APIC-level  eth0
…………

/proc/iomem

As informações de mapeamento da memória (RAM ou ROM) em cada dispositivo físico na memória do sistema;

[root@rhel5 ~]# more /proc/iomem 
00000000-0009f7ff : System RAM
0009f800-0009ffff : reserved
000a0000-000bffff : Video RAM area
000c0000-000c7fff : Video ROM
  …………

/proc/ioports

Uma lista de informações de intervalo de porta de entrada-saída que está atualmente em uso e foi registrada para se comunicar com dispositivos físicos; conforme mostrado abaixo, a primeira coluna representa o intervalo de porta de E/S registrado, seguido por dispositivos relacionados;

[root@rhel5 ~]# less /proc/ioports 
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-006f : keyboard
…………

/proc/kallsyms

A ferramenta de gerenciamento de módulo é usada para vincular ou vincular dinamicamente as definições de símbolos de módulos carregáveis, que são gerados pelo kernel; (versões após o kernel 2.5.71 suportam esta função); geralmente a quantidade de informações neste arquivo é bastante grande;

[root@rhel5 ~]# more /proc/kallsyms 
c04011f0 T _stext
c04011f0 t run_init_process
c04011f0 T stext
  …………

/proc/kcore

A memória física usada pelo sistema, armazenada no formato de arquivo principal ELF (arquivo principal), cujo tamanho do arquivo é a memória física usada (RAM) mais 4 KB; este arquivo é usado para verificar o estado atual da estrutura de dados do kernel , portanto, geralmente por GBD Normalmente usado por ferramentas de depuração, mas não pode usar o comando de visualização de arquivo para abrir este arquivo;

/proc/kmsg

Este arquivo é usado para salvar as informações de saída do kernel, normalmente usado por programas como /sbin/klogd ou /bin/dmsg, não tente abrir este arquivo com o comando view;

/proc/loadavg

Salve as médias de carga sobre CPU e E/S de disco. As três primeiras colunas representam as médias de carga a cada 1 segundo, a cada 5 segundos e a cada 15 segundos, respectivamente, semelhantes às informações relevantes geradas pelo comando uptime; a quarta coluna é Dois valores separados por barras, o primeiro representa o número de entidades (processos e threads) atualmente sendo agendadas pelo kernel, e o último representa o número de entidades de agendamento do kernel atualmente sobreviventes no sistema; a quinta coluna representa o mais recente arquivo antes de visualizar o PID de um processo criado pelo kernel;

[root@rhel5 ~]# more /proc/loadavg 
0.45 0.12 0.04 4/125 5549

[root@rhel5 ~]# uptime
06:00:54 up  1:06,  3 users,  load average: 0.45, 0.12, 0.04

/proc/locks

Salva informações sobre arquivos atualmente bloqueados pelo kernel, incluindo dados de depuração dentro do kernel; cada bloqueio ocupa uma linha e possui um número exclusivo; a segunda coluna de cada linha nas informações de saída a seguir indica o tipo de bloqueio usado pelo bloqueio atual, POSIX Indica o novo tipo de bloqueio de arquivo atual, que é gerado pela chamada do sistema lockf. FLOCK é um bloqueio de arquivo tradicional do UNIX, que é gerado pela chamada do sistema bando; a terceira coluna também é geralmente composta por dois tipos. ADVISORY significa que outros usuários não têm permissão para bloquear este arquivo, mas a leitura é permitida, OBRIGATÓRIO significa que outros usuários não têm permissão para acessar de qualquer forma durante este período de bloqueio de arquivo;

[root@rhel5 ~]# more /proc/locks 
1: POSIX  ADVISORY  WRITE 4904 fd:00:4325393 0 EOF
2: POSIX  ADVISORY  WRITE 4550 fd:00:2066539 0 EOF
3: FLOCK  ADVISORY  WRITE 4497 fd:00:2066533 0 EOF

/proc/mdstat

Salve as informações de status atuais de vários discos relacionados ao RAID. Em uma máquina que não usa RAID, ela é exibida da seguinte forma:

[root@rhel5 ~]# less /proc/mdstat 
Personalities : 
unused devices: <none>

/proc/meminfo

As informações sobre a utilização atual de memória no sistema são frequentemente usadas pelo comando free; você pode usar o comando file view para ler diretamente este arquivo, e seu conteúdo é exibido em duas colunas, a primeira é o atributo estatístico e a segunda é o valor correspondente;

[root@rhel5 ~]# less /proc/meminfo 
MemTotal:       515492 kB
MemFree:          8452 kB
Buffers:         19724 kB
Cached:         376400 kB
SwapCached:          4 kB
…………

Verifique a quantidade de memória livre:

grep MemFree /proc/meminfo    

/proc/mounts

Antes do kernel versão 2.4.29, o conteúdo deste arquivo são todos os sistemas de arquivos atualmente montados pelo sistema. No kernel após 2.4.19, o método de usar um namespace de montagem independente para cada processo é introduzido, e este arquivo muda torna-se um link simbólico para o arquivo /proc/self/mounts (uma lista de todos os pontos de montagem no próprio namespace de montagem de cada processo); /proc/self é um diretório único, que será descrito mais adiante; 

[root@rhel5 ~]# ll /proc |grep mounts
lrwxrwxrwx  1 root      root             11 Feb  8 06:43 mounts -> self/mounts

Conforme mostrado abaixo, a primeira coluna indica o dispositivo montado, a segunda coluna indica o ponto de montagem na árvore de diretórios atual, o terceiro ponto indica o tipo do sistema de arquivos atual e a quarta coluna indica o atributo de montagem (ro ou rw) , a quinta e sexta colunas são usadas para corresponder ao atributo dump no arquivo /etc/mtab;

[root@rhel5 ~]# more /proc/mounts 
rootfs / rootfs rw 0 0
/dev/root / ext3 rw,data=ordered 0 0
/dev /dev tmpfs rw 0 0
/proc /proc proc rw 0 0
/sys /sys sysfs rw 0 0
/proc/bus/usb /proc/bus/usb usbfs rw 0 0
…………

/proc/modules

Uma lista de todos os nomes de módulos atualmente carregados no kernel, que podem ser usados ​​pelo comando lsmod ou visualizados diretamente; como mostrado abaixo, a primeira coluna indica o nome do módulo, a segunda coluna indica o espaço de memória ocupado pelo módulo e o terceira coluna indica o módulo Quantas instâncias estão carregadas, a quarta coluna indica de quais outros módulos este módulo depende, a quinta coluna indica o status de carregamento deste módulo (Ao vivo: já carregado; Carregando: carregando; Descarregando: descarregando), sexta coluna Indica o deslocamento deste módulo na memória do kernel;

[root@rhel5 ~]# more /proc/modules 
autofs4 24517 2 - Live 0xe09f7000
hidp 23105 2 - Live 0xe0a06000
rfcomm 42457 0 - Live 0xe0ab3000
l2cap 29505 10 hidp,rfcomm, Live 0xe0aaa000
…………

/proc/partições

Informações como o número do dispositivo principal (principal) e o número do dispositivo secundário (secundário) de cada partição do dispositivo de bloco, incluindo o número de blocos contidos em cada partição (conforme mostrado na terceira coluna da saída abaixo);

[root@rhel5 ~]# more /proc/partitions 
major minor  #blocks  name

   8     0   20971520 sda
   8     1     104391 sda1
   8     2    6907950 sda2
   8     3    5630782 sda3
   8     4          1 sda4
   8     5    3582463 sda5

/proc/pci

Uma lista de todos os dispositivos PCI e suas informações de configuração encontradas durante a inicialização do kernel. As informações de configuração são principalmente informações de IRQ relacionadas a um dispositivo PCI, que não são muito legíveis. Você pode usar o comando "/sbin/lspci –vb" para obter mais informações relacionadas compreensíveis. ; Após o kernel 2.6, este arquivo foi substituído pelo diretório /proc/bus/pci e os arquivos sob ele;

/proc/slabinfo

Objetos que são frequentemente usados ​​no kernel (como inode, dentry, etc.) página de manual slapinfo na documentação do kernel;

[root@rhel5 ~]# more /proc/slabinfo 
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <ac
tive_slabs> <num_slabs> <sharedavail>
rpc_buffers            8      8   2048    2    1 : tunables   24   12    8 : slabdata      4      4      0
rpc_tasks              8     20    192   20    1 : tunables  120   60    8 : slabdata      1      1      0
rpc_inode_cache        6      9    448    9    1 : tunables   54   27    8 : slabdata      1      1      0
…………
…………
…………

/sys

O arquivo de diretório /sys armazena algumas informações relacionadas ao hardware e também é uma pasta virtual, não uma pasta no disco rígido real, mas também os dados na memória;

Reconhecimento de discos rígidos recém-adicionados:

echo "- - -" > /sys/class/scsi_host/hostX/scan #X表示数字,从0开始的

/proc/sys

Diferente do atributo "somente leitura" de outros arquivos em /proc, as informações neste arquivo de diretório podem ser modificadas e as características operacionais do kernel podem ser controladas por meio dessas configurações;

De antemão, você pode usar o comando "ls -l" para verificar se um arquivo é "gravável". As operações de gravação geralmente são feitas usando um formato semelhante a "echo DATA > /path/to/your/filename". Deve-se notar que mesmo que o arquivo seja gravável, ele geralmente não pode ser editado com um editor.

O número máximo de encadeamentos suportados pelo sistema de consulta:

cat /proc/sys/kernel/threads-max

subdiretório /proc/sys/debug

Esse diretório geralmente é um diretório vazio; 

subdiretório /proc/sys/dev

O diretório que fornece arquivos de informações de parâmetros para dispositivos especiais no sistema e os arquivos de informações de diferentes dispositivos são armazenados em diferentes subdiretórios, como /proc/sys/dev/cdrom e /proc/sys/dev na maioria dos sistemas /raid ( se a função de suporte ao raid estiver habilitada quando o kernel for compilado), que normalmente armazena os arquivos de informações de parâmetros relevantes do cdrom e raid no sistema;

proc/stat

Rastreia várias estatísticas em tempo real desde a última inicialização do sistema; conforme mostrado abaixo, onde
os oito valores após a linha "cpu" representam estatísticas no modo 1/100 (jiffies), modo de usuário de baixa prioridade, modo de sistema operacional, modo ocioso, tempo em modo de espera de E/S, etc.);
a linha "intr" fornece a informação da interrupção, a primeira é todas as interrupções que ocorreram desde que o sistema foi iniciado O número de vezes; depois cada número corresponde ao número de vezes que uma interrupção específica ocorreu desde que o sistema foi iniciado;
"ctxt" fornece o número de trocas de contexto de CPU que ocorreram desde que o sistema foi iniciado.
"btime" fornece o tempo desde que o sistema foi iniciado, em segundos;
"processes (total_forks) o número de tarefas criadas desde que o sistema foi iniciado;
"procs_running": o número de tarefas atualmente em execução na fila;
"procs_blocked ": O número de tarefas atualmente bloqueadas;

[root@rhel5 ~]# more /proc/stat
cpu  2751 26 5771 266413 2555 99 411 0
cpu0 2751 26 5771 266413 2555 99 411 0
intr 2810179 2780489 67 0 3 3 0 5 0 1 0 0 0 1707 0 0 9620 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5504 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 12781 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 427300
btime 1234084100
processes 3491
procs_running 1
procs_blocked 0

/proc/swaps

A partição de troca e suas informações de utilização de espaço no sistema atual, se houver várias partições de troca, as informações de cada partição de troca serão armazenadas em um arquivo separado no diretório /proc/swap, e quanto menor o número de prioridade, maior a possibilidade de ser utilizada, segue a informação de saída quando há apenas uma partição swap no sistema do autor;

[root@rhel5 ~]# more /proc/swaps 
Filename                                Type            Size    Used    Priority
/dev/sda8                               partition       642560  0       -1

/proc/tempo de atividade

O tempo de execução desde a última inicialização do sistema, conforme mostrado abaixo, o primeiro número representa o tempo de execução do sistema e o segundo número representa o tempo ocioso do sistema, em segundos;
 

[root@rhel5 ~]# more /proc/uptime 
3809.86 3714.13

/proc/versão

O número da versão do kernel em execução no sistema atual também exibirá a versão do gcc instalada pelo sistema no RHEL5.3 do autor, conforme mostrado abaixo;

[root@rhel5 ~]# more /proc/version 
Linux version 2.6.18-128.el5 ([email protected]) (gcc version 4.1.2 20080704 (Red Hat 4.1.2-44)) #1 SMP Wed Dec 17 11:42:39 EST 2008

/proc/vmstat

Vários dados estatísticos da memória virtual do sistema atual, a quantidade de informações pode ser relativamente grande, o que varia de sistema para sistema, e a legibilidade é melhor; o seguinte é um fragmento das informações de saída na máquina do autor; (O kernel após 2.6 suporta este arquivo)

[root@rhel5 ~]# more /proc/vmstat 
nr_anon_pages 22270
nr_mapped 8542
nr_file_pages 47706
nr_slab 4720
nr_page_table_pages 897
nr_dirty 21
nr_writeback 0
…………

/proc/zoneinfo

A lista de informações detalhadas da zona de memória (zona), a quantidade de informações é grande, o seguinte é um trecho de saída:

[root@rhel5 ~]# more /proc/zoneinfo 
Node 0, zone      DMA
  pages free     1208
        min      28
        low      35
        high     42
        active   439
        inactive 1139
        scanned  0 (a: 7 i: 30)
        spanned  4096
        present  4096
    nr_anon_pages 192
    nr_mapped    141
    nr_file_pages 1385
    nr_slab      253
    nr_page_table_pages 2
    nr_dirty     523
    nr_writeback 0
    nr_unstable  0
    nr_bounce    0
        protection: (0, 0, 296, 296)
  pagesets
  all_unreclaimable: 0
  prev_priority:     12
  start_pfn:         0
…………

3. Crie o subnó do diretório /proc

O mecanismo do módulo do kernel e o sistema de arquivos /proc são recursos típicos dos sistemas Linux. Você pode aproveitar esses recursos para criar nós correspondentes no diretório /proc para arquivos especiais, dispositivos, variáveis ​​públicas etc.? A resposta é, claro, sim.

Existem muitas maneiras pelas quais os módulos do kernel interagem fora do espaço do kernel, e o sistema de arquivos /proc é uma das principais maneiras.

Apresentando o sistema de arquivos /proc, aqui revisaremos alguns dos fundamentos. Um sistema de arquivos é a maneira como o sistema operacional organiza os arquivos em um disco ou outro periférico. Linux suporta muitos tipos de sistemas de arquivos: minix, ext, ext2, msdos, umsdos, vfat, proc, nfs, iso9660, hpfs, sysv, smb, ncpfs, etc. Ao contrário de outros sistemas de arquivos, o sistema de arquivos /proc é um pseudo-sistema de arquivos. A razão pela qual ele é chamado de pseudo file system é que ele não possui nenhuma parte relacionada ao disco, ele existe apenas na memória e não ocupa o espaço da memória externa. E tem muitas semelhanças com sistemas de arquivos. Por exemplo, ele fornece uma interface para acessar os dados do kernel do sistema na forma de um sistema de arquivos e pode ser operado com todas as ferramentas de arquivos comuns. Por exemplo, podemos visualizar as informações no arquivo proc através do comando cat, more ou outras ferramentas de edição de texto. Mais importante, usuários e aplicativos podem obter informações do sistema por meio de proc e podem alterar alguns parâmetros do kernel. Como as informações do sistema, como processos, são alteradas dinamicamente, quando um usuário ou um aplicativo lê um arquivo proc, o proc lê dinamicamente as informações necessárias do kernel do sistema e as envia. O sistema de arquivos /proc geralmente é colocado no diretório /proc.

Como fazer o sistema de arquivos /proc refletir o estado dos módulos do kernel? Vamos dar uma olhada neste exemplo um pouco mais complexo abaixo.

proc_example.c
 
…………
 
int init_module()
 
{
 
            int rv = 0;
 
   
 
            /* 创建目录 */
 
            example_dir = proc_mkdir(MODULE_NAME, NULL);
 
            if(example_dir == NULL) {
 
                    rv = -ENOMEM;
 
                    goto out;
 
            }
 
            example_dir->owner = THIS_MODULE;
 
           
 
            /* 快速创建只读文件 jiffies */
 
            jiffies_file = create_proc_read_entry("jiffies", 0444, example_dir,
 
                                           proc_read_jiffies, NULL);
 
            if(jiffies_file == NULL) {
 
                    rv  = -ENOMEM;
 
                    goto no_jiffies;
 
            }
 
            jiffies_file->owner = THIS_MODULE;
 
   
 
            /* 创建规则文件foo 和 bar */
 
            foo_file = create_proc_entry("foo", 0644, example_dir);
 
            if(foo_file == NULL) {
 
                    rv = -ENOMEM;
 
                    goto no_foo;
 
            }
 
            strcpy(foo_data.name, "foo");
 
            strcpy(foo_data.value, "foo");
 
            foo_file->data = &foo_data;
 
            foo_file->read_proc = proc_read_foobar;
 
            foo_file->write_proc = proc_write_foobar;
 
            foo_file->owner = THIS_MODULE;
 
                   
 
            bar_file = create_proc_entry("bar", 0644, example_dir);
 
            if(bar_file == NULL) {
 
                    rv = -ENOMEM;
 
                    goto no_bar;
 
            }
 
            strcpy(bar_data.name, "bar");
 
            strcpy(bar_data.value, "bar");
 
            bar_file->data = &bar_data;
 
            bar_file->read_proc = proc_read_foobar;
 
            bar_file->write_proc = proc_write_foobar;
 
            bar_file->owner = THIS_MODULE;
 
               
 
       /* 创建设备文件 tty */
 
            tty_device = proc_mknod("tty", S_IFCHR | 0666, example_dir, MKDEV(5, 0));
 
            if(tty_device == NULL) {
 
                    rv = -ENOMEM;
 
                    goto no_tty;
 
            }
 
            tty_device->owner = THIS_MODULE;
 
   
 
            /* 创建链接文件jiffies_too */
 
            symlink = proc_symlink("jiffies_too", example_dir, "jiffies");
 
            if(symlink == NULL) {
 
                    rv = -ENOMEM;
 
                    goto no_symlink;
 
            }
 
            symlink->owner = THIS_MODULE;
 
   
 
            /* 所有创建都成功 */
 
            printk(KERN_INFO "%s %s initialised\n",
 
                   MODULE_NAME, MODULE_VERSION);
 
            return 0;
 
    /*出错处理*/
 
    no_symlink:  remove_proc_entry("tty", example_dir);
 
    no_tty:      remove_proc_entry("bar", example_dir);
 
    no_bar:      remove_proc_entry("foo", example_dir);
 
    no_foo:      remove_proc_entry("jiffies", example_dir);
 
    no_jiffies:    remove_proc_entry(MODULE_NAME, NULL);
 
    out:        return rv;
 
    }
 
    …………
 

O módulo do kernel proc_example primeiro cria seu próprio subdiretório proc_example no diretório /proc. Em seguida, três arquivos proc normais (foo, bar, jiffies), um arquivo de dispositivo (tty) e um link de arquivo (jiffies_too) são criados neste diretório. Especificamente, foo e bar são dois arquivos de leitura e gravação que compartilham as funções proc_read_foobar e proc_write_foobar. jiffies é um arquivo somente leitura que obtém os jiffies de hora atuais do sistema. jiffies_too é um link simbólico para o arquivo jiffies.

2. Ferramentas de gerenciamento de kernel

1. ferramenta de gerenciamento sysctl

Os parâmetros modificados por sysctl são temporariamente efetivos e são persistentemente efetivos escrevendo um arquivo de configuração.

#配置文件
/run/sysctl.d/*.conf
/etc/sysctl.d/*.conf
/usr/local/lib/sysctl.d/*.conf
/usr/lib/sysctl.d/*.conf
/lib/sysctl.d/*.conf
/etc/sysctl.conf  #主要存放在这里面,一般都在这个配置文件里面编写设置

Formato:

Ao contrário do formato no arquivo, use pontos (.) para separar os caminhos. Não há necessidade de escrever /proc/sys, pois este arquivo de configuração corresponde ao gerenciamento da pasta /proc/sys.

Parâmetros comuns:

-w   临时改变某个指定参数的值
-a   显示所有生效的系统参数
-p   从指定的文件加载系统参数

exemplo:

Proibir ping na máquina:

[root@centos8 ~]#cat /etc/sysctl.d/test.conf
net.ipv4.icmp_echo_ignore_all=1
[root@centos8 ~]#sysctl -p /etc/sysctl.d/test.conf

Limpar método de cache:

echo 1|2|3 >/proc/sys/vm/drop_caches

2. ulimit limita os recursos do sistema

ulimit limita certos recursos do sistema do usuário, incluindo o número de arquivos que podem ser abertos, o tempo de CPU que pode ser usado, a quantidade total de memória que pode ser usada, etc.

gramática:

 ulimit [-acdfHlmnpsStvw] [size] 

Opções e parâmetros:

-H :  hard limit ,严格的设定,必定不能超过这个设定的数值 
-S :  soft limit ,警告的设定,可以超过这个设定值,但是若超过则有警告讯息 
-a :  后面不接任何选项与参数,可列出所有的限制额度 
-c :  当某些程序发生错误时,系统可能会将该程序在内存中的信息写成档案,这种档案就被称为核心档案(core file)。 
-f :  此 shell 可以建立的最大档案容量(一般可能设定为 2GB)单位为 Kbytes 
-d :  程序可使用的最大断裂内存(segment)容量 
-l :  可用于锁定 (lock) 的内存量 
-m :  设置可以使用的常驻内存的最大值.单位:kbytes 
-n :  设置内核可以同时打开的文件描述符的最大值.单位:n 
-p :  设置管道缓冲区的最大值.单位:kbytes 
-s :  设置堆栈的最大值.单位:kbytes 
-v :  设置虚拟内存的最大值.单位:kbytes 
-t :  可使用的最大 CPU 时间 (单位为秒) 
-u :  单一用户可以使用的最大程序(process)数量 

Configurações simples gerais:

ulimit -SHn 65535 

Para torná-lo permanente:

[root@www ~]# vi /etc/security/limits.conf 
* soft noproc 65535 
* hard noproc 65535 
* soft nofile 409600 
* hard nofile 409600 

ilustrar:

* significa para todos os usuários

noproc é o número máximo de processos

nofile é o número máximo de arquivos abertos

Caso:

[root@www ~]# vi /etc/security/limits.conf 
# End of file 
*           soft  core   unlimit 
*           hard  core   unlimit 
*           soft  fsize  unlimited 
*           hard  fsize  unlimited 
*           soft  data   unlimited 
*           hard  data   unlimited 
*           soft  nproc  65535 
*           hard  nproc  63535 
*           soft  stack  unlimited 
*           hard  stack  unlimited 
*           soft  nofile  409600 
*           hard  nofile  409600 

cat /etc/security/limits.conf

cat /etc/security/limits.d/90-nproc.conf

A configuração inadequada de sysctl/ulimit pode tornar a resposta do sistema muito lenta quando os indicadores acima são muito normais.

Caso: Por conveniência, configurei todos os parâmetros sysctl/ulimit como redis/elasticsearch/network no script de inicialização do sistema configure_server.py de uma só vez. Como resultado, o parâmetro vm.max_map_count que o elasticsearch precisa definir faz com que o servidor redis responda lentamente após uma longa execução.

3, limite de recursos do sistema linux

oracle roda em linux. Existem certos requisitos para restrições de recursos.

limites.conf e sysctl.conf 

A instalação do oracle não pode escapar dos parâmetros de configuração nesses dois arquivos: o arquivo sysctl.conf é principalmente para restrições de recursos no sistema. O limit.conf principalmente para usuários fazerem restrições de recursos, ele depende do mecanismo PAM (Pluggable Authentication Modules Pluggable Authentication Modules), as configurações não podem ultrapassar as configurações do sistema operacional.

sintaxe de limites.conf:

 username|@groupname type resource limit

username|@groupname: Defina o nome de usuário a ser restrito. Adicione @ antes do nome do grupo para distingui-lo do nome de usuário. Você também pode usar o curinga * para limitar todos os usuários.

parâmetro:

  type:

   可以指定 soft,hard 和 -,soft 指的是当前系统生效的设置值。hard 表明系统中所能设定的最大值。soft 的限制不能比har 限制高。用 - 就表明同时设置了 soft 和 hard 的值。

  resource:
  core - 限制内核文件的大小
  date - 最大数据大小
  fsize - 最大文件大小
  memlock - 最大锁定内存地址空间
  nofile - 打开文件的最大数目
  rss - 最大持久设置大小
  stack - 最大栈大小
  cpu - 以分钟为单位的最多 CPU 时间
  noproc - 进程的最大数目
  as - 地址空间限制
  maxlogins - 此用户允许登录的最大数目

Configurações de consulta: comando ulimit

Válido apenas para o tty atual (terminal). O próprio comando ulimit tem configurações soft e hard. Adicionar -H é difícil, e adicionar -S é suave.

parâmetro:

-H 设置硬件资源限制.
-S 设置软件资源限制.
-a 显示当前所有的资源限制.
-c size:设置core文件的最大值.单位:blocks
-d size:设置数据段的最大值.单位:kbytes
-f size:设置创建文件的最大值.单位:blocks
-l size:设置在内存中锁定进程的最大值.单位:kbytes
-m size:设置可以使用的常驻内存的最大值.单位:kbytes
-n size:设置内核可以同时打开的文件描述符的最大值.单位:n
-p size:设置管道缓冲区的最大值.单位:kbytes
-s size:设置堆栈的最大值.单位:kbytes
-t size:设置CPU使用时间的最大上限.单位:seconds
-v size:设置虚拟内存的最大值.单位:kbytes

Perceber:

ilimitado é um valor especial usado para indicar ilimitado

Descrição do parâmetro sys .c onf

A maioria dos parâmetros do kernel são armazenados no diretório /proc/sys e podem ser alterados enquanto o sistema está em execução, mas falharão após reiniciar a máquina. /etc/sysctl.conf é uma interface que permite alterações em um sistema Linux em execução. Ele contém algumas opções avançadas para a pilha TCP/IP e o sistema de memória virtual. A modificação dos parâmetros do kernel tem efeito permanente. Ou seja, existe uma relação correspondente entre os arquivos do kernel em /proc/sys e as variáveis ​​no arquivo de configuração sysctl.conf.

Configuração comum:

kernel.shmall=4294967296
vm.min_free_kbytes=262144
kernel.sem=4096 524288 4096 128
fs.file-max=6815744
net.ipv4.ip_local_port_range=9000 65500
net.core.rmem_default=262144
net.core.rmem_max=4194304
net.core.wmem_default=262144
net.core.wmem_max=1048576
fs.aio-max-nr=1048576
kernel.shmmni=4096
vm.nr_hugepages=8029

ilustrar:

kernel.shmmax:
是核心参数中最重要的参数之一,用于定义单个共享内存段的最大值。设置应该足够大,能在一个共享内存段下容纳下整个的SGA ,设置的过低可能会导致需要创建多个共享内存段,这样可能导致系统性能的下降。至于导致系统下降的主要原因为在实例启动以及ServerProcess创建的时候,多个小的共享内存段可能会导致当时轻微的系统性能的降低(在启动的时候需要去创建多个虚拟地址段,在进程创建的时候要让进程对多个段进行“识别”,会有一些影响),但是其他时候都不会有影响。
官方建议值:
32位Linux系统:可取最大值为4GB(4294967296bytes)-1byte,即4294967295。建议值为多于内存的一半,所以如果是32为系统,一般可取值为4294967295。32位系统对SGA大小有限制,所以SGA肯定可以包含在单个共享内存段中。
64位linux系统:可取的最大值为物理内存值-1byte,建议值为多于物理内存的一半,一般取值大于SGA_MAX_SIZE即可,可以取物理内存-1byte。例如,如果为12GB物理内存,可取12*1024*1024*1024-1=12884901887,SGA肯定会包含在单个共享内存段中。 
kernel.shmall:
    该参数控制可以使用的共享内存的总页数。Linux共享内存页大小为4KB,共享内存段的大小都是共享内存页大小的整数倍。一个共享内存段的最大大小是16G,那么需要共享内存页数是16GB/4KB=16777216KB /4KB=4194304(页),也就是64Bit系统下16GB物理内存,设置kernel.shmall = 4194304才符合要求(几乎是原来设置2097152的两倍)。这时可以将shmmax参数调整到16G了,同时可以修改SGA_MAX_SIZE和SGA_TARGET为12G(您想设置的SGA最大大小,当然也可以是2G~14G等,还要协调PGA参数及OS等其他内存使用,不能设置太满,比如16G)
kernel.shmmni:
该参数是共享内存段的最大数量。shmmni缺省值4096,一般肯定是够用了。
fs.file-max:
该参数决定了系统中所允许的文件句柄最大数目,文件句柄设置代表linux系统中可以打开的文件的数量。
fs.aio-max-nr:
      此参数限制并发未完成的请求,应该设置避免I/O子系统故障。
kernel.sem:
以kernel.sem = 250 32000 100 128为例:
       250是参数semmsl的值,表示一个信号量集合中能够包含的信号量最大数目。
       32000是参数semmns的值,表示系统内可允许的信号量最大数目。
       100是参数semopm的值,表示单个semopm()调用在一个信号量集合上可以执行的操作数量。
       128是参数semmni的值,表示系统信号量集合总数。
net.ipv4.ip_local_port_range:
    表示应用程序可使用的IPv4端口范围。
net.core.rmem_default:
表示套接字接收缓冲区大小的缺省值。
net.core.rmem_max:
表示套接字接收缓冲区大小的最大值。
net.core.wmem_default:
表示套接字发送缓冲区大小的缺省值。
net.core.wmem_max:表示套接字发送缓冲区大小的最大值。            
vm.nr_hugepages=8029                 大页数

Seis, otimização do kernel Linux

Método de avaliação do kernel:

Adicione initcall_debug aos parâmetros de inicialização para obter mais logs do kernel:

[ 3.750000] calling ov2640_i2c_driver_init+0x0/0x10 @ 1
[ 3.760000] initcall ov2640_i2c_driver_init+0x0/0x10 returned 0 after 544 usecs
[ 3.760000] calling at91sam9x5_video_init+0x0/0x14 @ 1
[ 3.760000] at91sam9x5-video f0030340.lcdheo1: video device registered @ 0xe0d3e340, irq = 24
[ 3.770000] initcall at91sam9x5_video_init+0x0/0x14 returned 0 after 10388 usecs
[ 3.770000] calling gspca_init+0x0/0x18 @ 1
[ 3.770000] gspca_main: v2.14.0 registered
[ 3.770000] initcall gspca_init+0x0/0x18 returned 0 after 3966 usecs
...

Alternativamente, você pode usar scripts/bootgraph.pl para converter as informações do dmesg em uma imagem:

$ scripts/bootgraph.pl boot.log > boot.svg

 Em seguida, encontre os aspectos mais demorados e otimize-os.

1. Otimizando o compilador

ARM vs Thumb2

Compare sistemas e aplicativos compilados no conjunto de instruções ARM ou Thumb2.

ARM: 3,79 MB para rootfs, 227 KB para ffmpeg.

Thumb2:3,10 MB (-18%),183 KB (-19%)。

Desempenho: O desempenho do Thumb2 foi significativamente melhorado ligeiramente (cerca de menos de 5%).

Embora o desempenho tenha melhorado, eu pessoalmente ainda escolho o conjunto de instruções ARM.

musl vs uClibc

Existem 3 tipos de bibliotecas C para escolher no Buildroot: glibc, musl , uClibc, aqui apenas comparamos os 2 últimos tipos de bibliotecas menores.

musl: 680 KB (diretório stats/lib).

uClibc:570 KB (-16%)。

uClibc economiza 110 KB e escolhemos uClibc.

2. Otimize o aplicativo

Podemos escolher os componentes funcionais do FFmpeg através do ./configure.

Além disso, você pode usar os comandos strace e perf para depurar e otimizar o d-code interno do FFmpeg.

O resultado após a otimização:

Sistema de arquivos: Reduza de 16,11 MB para 3,54 MB (-78%).

Tempo de carregamento e execução do programa: 150 ms mais curto.

Tempo total de inicialização: 350 ms mais curto.

A otimização no espaço é grande, mas a otimização no tempo de inicialização é pequena, pois o Linux só carrega as partes necessárias do programa quando o executa.

3. Otimize o Init e o sistema de arquivos raiz

Ideias:

Use o bootchartd para analisar a inicialização do sistema e aparar serviços desnecessários.

Combine os scripts de inicialização em /etc/init.d/ em um.

/proc e /sys não são montados.

Corte BusyBox, quanto menor o sistema de arquivos, mais rápida a montagem do kernel pode ser.

Substitua o programa Init pelo nosso aplicativo.

Compile o aplicativo estaticamente.

Apare os arquivos usados ​​com pouca frequência e encontre os arquivos que não são acessados ​​há muito tempo:

$ find / -atime -1000 -type f

O resultado após a otimização:

Sistema de arquivos: Reduzido de 3,54 MB para 2,33 MB (-34%) após cortar o Busybox.

Tempo de inicialização: basicamente inalterado, provavelmente porque o próprio sistema de arquivos é pequeno o suficiente.

4. Use initramfs como rootfs

Em circunstâncias normais, o sistema Linux irá montar o initramfs primeiro, o init ramfs é pequeno e está localizado na memória, e então o initramfs é responsável por carregar o sistema de arquivos raiz.

Quando aparamos o rootfs do Buildroot muito pequeno, podemos considerar usá-lo diretamente como initramfs.

Qual é o benefício disso?

O initramfs pode ser spliced ​​com o Kernel, e o Bootloader é responsável por carregar o Kernel+initramfs na memória, e o kernel não precisa mais acessar o disco.

O kernel não precisa mais de funções relacionadas a bloco/armazenamento e sistema de arquivos, o tamanho ficará menor e o tempo de carregamento e de inicialização serão reduzidos.

Observe que a compactação initramfs precisa ser desativada (CONFIG_INITRAMFS_COMPRESSION_NONE).

O resultado após a otimização:

Mesmo com CONFIG_BLOCK e CONFIG_MMC desabilitados, o tempo total de inicialização ainda é 20ms maior. Isso pode ser porque depois que Kernel + initramfs são colocados juntos, o kernel fica muito maior e a imagem do kernel precisa ser descompactada, o que aumenta o tempo de descompactação.

5. Corte o rastreamento

Desabilite os recursos relacionados ao Tracers em hackers de kernel.

Tempo de inicialização: 550ms mais curto.

Tamanho do kernel: Encolher em 217 KB.

6. Corte algumas funções de hardware que não são necessárias

omap8250_platform_driver_init() // (660 ms)
cpsw_driver_init()  // (112 ms)
am335x_child_init() // (82 ms)
...

7.  Loops predefinidos por instante

A cada inicialização, o kernel calibra o valor do loop de atraso para a função udelay().

Isso mede o valor de loops por instante (lpj). Nós só precisamos iniciar o kernel uma vez e procurar o valor lpj no log:

Calibrating delay loop... 996.14 BogoMIPS (lpj=4980736)

Em seguida, preencha lpj=4980736 nos parâmetros de inicialização, você pode:

Calibrating delay loop (skipped) preset value.. 996.14 BogoMIPS (lpj=4980736)

Cerca de 82 ms mais curto.

8.  Desative CONFIG_SMP

A inicialização do SMP é lenta. Geralmente é habilitado na configuração padrão, mesmo para uma CPU de núcleo único.

Se nossa plataforma for single core, o SMP pode ser desabilitado.

Após o desligamento, o kernel diminui: -188 KB (-4,6%) e o tempo de inicialização diminui em 126ms.

9.  Desativar registro

Adicionar silêncio aos parâmetros de inicialização reduz o tempo de inicialização em 577 ms.

Com CONFIG_PRINTK e CONFIG_BUG desabilitados, o kernel é reduzido em 118 KB (-5,8%).

Após desabilitar CONFIG_KALLSYMS, o kernel encolhe em 107 KB (-5,7%).

No total, o tempo de inicialização é reduzido em 767 ms.

10.  Ative CONFIG_EMBEDDED e CONFIG_EXPERT

Isso torna as chamadas do sistema mais enxutas e o kernel menos genérico, mas é o suficiente para manter seu aplicativo em execução.

O kernel é reduzido em 51 KB.

O tempo de inicialização é reduzido em 34 ms.

11.  Selecione alocadores de memória SLAB

Geralmente SLAB, SLOB, SLUB escolhem um dos três.

SLAB: A escolha padrão, a mais versátil, a mais tradicional e a mais confiável.

SLOB: Mais conciso, menos código, mais economia de espaço, adequado para sistemas embarcados, após a ativação, o kernel é reduzido em 5 KB, mas o tempo de inicialização é aumentado em 1,43 S!

SLUB: É mais indicado para grandes sistemas, após habilitar o tempo de inicialização aumenta em 2 ms.

Portanto, ainda usamos SLAB.

12.  Otimização da compactação do kernel

As características dos diferentes métodos de compressão são as seguintes:

Efeito medido:

Parece que gzip e lzo têm um desempenho melhor. O efeito do teste deve estar relacionado ao desempenho da CPU/disco. 

13. Parâmetros de compilação do kernel

Habilite CONFIG_CC_OPTIMIZE_FOR_SIZE, esta opção pode substituir gcc -O2 por gcc -Os.

Observe que este é apenas um resultado de teste no BeagleBone Black + Linux 5.1, existem diferenças entre as diferentes plataformas. 

14. Desabilite pseudo sistemas de arquivos como /proc

Para considerar a compatibilidade do aplicativo.

O ffmpeg depende do /proc, portanto, apenas algumas opções relacionadas ao proc podem ser desativadas: CONFIG_PROC_SYSCTL, CONFIG_PROC_PAGE_MONITOR CONFIG_CONFIGFS_FS e o tempo de inicialização não foi alterado.

Desligar o sysfs reduz o tempo de inicialização em 35 ms.

15.  Emenda DTB

Ative CONFIG_ARM_APPENDED_DTB:

$ cat arch/arm/boot/zImage arch/arm/boot/dts/am335x-boneblack-lcd4.dtb > zImage
$ setenv bootcmd 'fatload mmc 0:1 81000000 zImage; bootz 81000000'

O tempo de inicialização é reduzido em 26 ms.

16. Otimize o carregador de inicialização

Aqui usamos a melhor solução: use o modo Uboot Falcon.

O modo Falcon executa apenas o primeiro estágio do Uboot: SPL, depois pula o estágio 2 e executa o carregamento do Kernel.

O tempo de inicialização é reduzido em 250 ms.

Resumo da otimização do kernel:

Neste ponto, a otimização de inicialização está basicamente concluída, e o efeito final é o seguinte:

[0.000000 0.000000]
[0.000785 0.000785] U-Boot SPL 2019.01 (Oct 27 2019 - 08:04:06 +0100)
[0.057822 0.057822] Trying to boot from MMC1
[0.378878 0.321056] fdt_root: FDT_ERR_BADMAGIC
[0.775306 0.396428] Waiting for /dev/video0 to be ready...
[1.966367 1.191061] Starting ffmpeg
...
[2.412284 0.004277] First frame decoded

Desde a inicialização até a exibição do LCD no primeiro quadro da imagem, o tempo total é de 2,41 segundos. 

Os passos mais eficazes são os seguintes :

Ainda vale a pena otimizar o espaço :

O sistema ficou 1,2 segundo esperando a câmera USB enumerar, tem como agilizar aqui?

É possível desativar o tty e o login do terminal?

Finalmente, existem alguns princípios a serem seguidos quando se trata de otimizar o tempo de inicialização :

Por favor, não otimize prematuramente.

Comece a otimizar a partir de alguns pontos com menos influência.

Otimização de cima para baixo de rootfs, kernel, bootloader.

Acho que você gosta

Origin blog.csdn.net/qq_35029061/article/details/126210028
Recomendado
Clasificación