Curso de Pós-Graduação “Lato Sensu” (Especialização) a Distância Administração em Redes Linux
KERNEL DO LINUX
Marluce Rodrigues Pereira
Universidade Federal de Lavras – UFLA Fundação de Apoio ao Ensino, Pesquisa e Extensão – FAEPE Lavras – MG
PARCERIA UFLA – Universidade Federal de Lavras FAEPE – Fundação de Apoio ao Ensino, Pesquisa e Extensão REITOR Antônio Nazareno Guimarães Mendes VICE-REITOR Ricardo Pereira Reis DIRETOR DA EDITORA Marco Antônio Rezende Alvarenga PRÓ-REITOR DE PÓS-GRADUAÇÃO Joel Augusto Muniz PRÓ-REITOR ADJUNTO DE PÓS-GRADUAÇÃO “LATO SENSU” Marcelo Silva de Oliveira COORDENADOR DO CURSO Heitor Augustus Xavier Costa PRESIDENTE DO CONSELHO DELIBERATIVO DA FAEPE Luis Antônio Lima EDITORAÇÃO Grupo Ginux (http://ginux.comp.ufla.br/) IMPRESSÃO Gráfica Universitária/UFLA Ficha Catalográfica preparada pela Divisão de Processos Técnicos da Biblioteca Central da UFLA Pereira, Marluce Rodrigues Kernel do Linux/ Marluce Rodrigues Pereira. - - Lavras: UFLA/FAEPE, 2006. 83 p. : il. - Curso de Pós-Graduação “Lato Sensu” (Especialização) à Distância: Administração em Redes Linux. Bibliografia. 1. Informática 2. Sistemas operacionais. 3. Linux. 4. Gerência de processos. 5. Gerência de memória. 6. Gerência de arquivos. 7. Gerência de E/S. I. Universidade Federal de Lavras. II. Fundação de Apoio ao Ensino, Pesquisa e Extensão. III. Título. CDD-005.133 -005.4 Nenhuma parte desta publicação pode ser reproduzida, por qualquer meio ou forma, sem a prévia autorização.
SUMÁRIO
1 Introdução 1.1 Estrutura da apostila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Agradecimentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7 8 9
2 Visão Geral de Sistemas Operacionais e o Kernel do Linux 2.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Visão geral dos sistemas operacionais . . . . . . . . . . . . . . . 2.3 História do Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4 O Kernel do Linux . . . . . . . . . . . . . . . . . . . . . . . . . . 2.4.1 Obtenção, configuração e compilação do kernel do Linux 2.5 Inicialização de um sistema em um PC . . . . . . . . . . . . . . 2.6 Resumo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
11 11 12 15 16 19 22 23
3 Gerência de Processos 3.1 Introdução . . . . . . . . . . . . . . . . . . . . . 3.2 Criação de processos . . . . . . . . . . . . . . 3.3 Descritor de processo e a estrutura de tarefas 3.4 Contexto do processo . . . . . . . . . . . . . . 3.5 Árvore da família de processos . . . . . . . . . 3.6 Implementação de threads no Linux . . . . . . 3.7 Escalonamento de processos . . . . . . . . . . 3.7.1 Interrupções . . . . . . . . . . . . . . . 3.7.2 Sincronização no kernel . . . . . . . . . 3.8 Resumo . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
25 25 28 30 32 32 33 33 37 38 40
. . . . . . . . .
41 41 41 44 47 48 48 49 52 53
. . . . .
55 55 55 56 57 58
4 Gerência de memória 4.1 Introdução . . . . . . . . . . . . . . . . . . . . . 4.2 Organização da memória . . . . . . . . . . . . 4.3 Memória virtual . . . . . . . . . . . . . . . . . . 4.4 Thrashing . . . . . . . . . . . . . . . . . . . . . 4.5 Gerência de memória no kernel do Linux . . . 4.5.1 Gerência da memória física . . . . . . . 4.5.2 Memória virtual no Linux . . . . . . . . 4.5.3 Mapeamento de programas na memória 4.6 Resumo . . . . . . . . . . . . . . . . . . . . . . 5 Sistema de arquivos 5.1 Introdução . . . . . . . . . . . . 5.2 Conceito de arquivo . . . . . . 5.3 Métodos de acesso a arquivos 5.4 Estrutura de diretórios . . . . . 5.4.1 Diretório de um nível . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . . . . . .
. . . . .
. . . . . . . . .
. . . . .
5.5 5.6 5.7 5.8
5.4.2 Diretório em dois níveis . . . . . . . . . 5.4.3 Diretório estruturado em árvore . . . . . 5.4.4 Diretório de grafos cíclicos . . . . . . . 5.4.5 Diretório como estrutura de grafo geral Sistemas de arquivos no Linux . . . . . . . . . O sistema de arquivos ext2 do Linux . . . . . . O sistema de arquivos proc do Linux . . . . . . Resumo . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
58 59 59 60 60 62 64 65
6 Gerência de Entrada e Saída 6.1 Introdução . . . . . . . . . . . . . . . . . . 6.1.1 Escalonamento . . . . . . . . . . . 6.1.2 Alocação em buffers . . . . . . . . 6.1.3 Alocação em caches . . . . . . . . 6.1.4 Spooling e reserva de dispositivos 6.1.5 Manipulação de erros . . . . . . . 6.2 Entrada e Saída no Linux . . . . . . . . . 6.2.1 Dispositivos de blocos . . . . . . . 6.2.2 Dispositivos de caracteres . . . . . 6.2.3 Estrutura de rede . . . . . . . . . . 6.3 Resumo . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
67 67 67 68 68 69 69 69 70 71 71 71
7 Comparação entre Solaris, Linux e FreeBSD 7.1 Escalonamento e escalonadores . . . . . 7.2 Gerência de memória e paginação . . . . 7.2.1 Paginação . . . . . . . . . . . . . 7.3 Sistema de arquivos . . . . . . . . . . . . 7.4 Resumo . . . . . . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
73 73 74 75 76 77
8 Considerações finais Referências Bibliográficas 8.1 SOBRE SISTEMAS OPERACIONAIS EM GERAL 8.2 SOBRE SISTEMA UNIX . . . . . . . . . . . . . . . 8.3 SOBRE SISTEMA LINUX . . . . . . . . . . . . . . 8.4 OUTROS . . . . . . . . . . . . . . . . . . . . . . .
79 . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
81 81 81 81 82
LISTA DE FIGURAS
2.1 Visualização da arquitetura do kernel [Maxwell (2000)] . . . . . . . . . . . . .
17
3.1 3.2 3.3 3.4
. . . .
30 31 33 39
4.1 Endereço lógico na paginação . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Formato da memória para programas ELF [Silberschatz (2004)] . . . . . . .
45 53
5.1 5.2 5.3 5.4 5.5 5.6 5.7
. . . . . . .
56 58 58 59 60 61 63
6.1 Estrutura de blocos do driver de dispositivo . . . . . . . . . . . . . . . . . . .
70
O descritor do processo e a lista de tarefas [Love (2005)] . . Diagrama de estados de processos no kernel do Linux . . . . Código da estrutura para interação entre processo pai e filho. Exemplo de seção crítica com dois processos. . . . . . . . .
Arquivo de acesso seqüencial [Silberschatz (2004)] . . Estrutura de diretório de um nível. . . . . . . . . . . . . Estrutura de diretório em dois níveis. . . . . . . . . . . . Estrutura de diretório em árvore. . . . . . . . . . . . . . Estrutura de diretório de grafos cíclicos . . . . . . . . . Estrutura de diretório de grafo geral. . . . . . . . . . . . Políticas de alocação de blocos do ext2fs [Love (2005)]
. . . . . . .
. . . . . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
. . . .
. . . . . . .
LISTA DE TABELAS
2.1 Diret贸rios na 谩rvore do fonte do kernel [Love (2005)] . . . . . . . . . . . . . .
20
3.1 Timeslices do escalonador . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Principais chamadas de sistema relacionadas ao escalonador . . . . . . . .
35 36
7.1 Lista parcial de sistemas de arquivos [Bruning (2005)] . . . . . . . . . . . . .
76
1 INTRODUÇÃO
Um sistema operacional é responsável por gerenciar o sistema de computação. Por isso, conhecer quais são os componentes de um sistema operacional, como os componentes são gerenciados, como surgiu o kernel do Linux e como é a gerência destes componentes no Linux torna-se imprescindível para um administrador de uma rede Linux. Um sistema de computação pode ser dividido em quatro componentes: o hardware, o sistema operacional, os programas aplicativos e os usuários [Silberschatz (2004)]. O hardware é composto pela Unidade Central de Processamento (CPU - Central Processing Unit), a memória e os dispositivos de entrada e saída (I/O - Input/Output). Os programas aplicativos compreendem os processadores de texto, planilhas, compiladores e navegadores da web. Cabe aos programas aplicativos definirem como serão utilizados os recursos computacionais para resolver os problemas computacionais dos usuários. O sistema operacional é responsável pela coordenação e controle do uso do hardware pelos programas aplicativos para os diversos usuários. Com o desenvolvimento da tecnologia utilizada nos sistemas computacionais o desempenho e a facilidade de interação com o usuário também melhoraram. Conseqüentemente, os sistemas operacionais tendem a se desenvolver com o passar dos anos, buscando atender ao planejamento das atividades computacionais de modo a assegurar o bom desempenho do sistema de computação e proporcionar um ambiente adequado para o desenvolvimento e execução de programas. Um programa é um conjunto de instruções a serem executadas. Quando o programa está sendo executado, ele pode ser denominado de processo e necessita de vários recursos, tais como tempo de CPU, memória, arquivos e dispositivos de entrada e saída. Em um sistema com vários processos, estes recursos serão utilizados por todos eles, mas não ao mesmo tempo. Para uma utilização eficiente dos recursos é necessário algum gerenciamento, que é realizado pelo sistema operacional. Vários sistemas operacionais foram desenvolvidos buscando utilizar eficientemente os recursos computacionais. Em 1969, o grupo de desenvolvedores dos laboratórios Bell Labs [Garrels (2002)] começaram o desenvolvimento de um sistema operacional, escrito em linguagem C. O pro-
8
EDITORA - UFLA/FAEPE - Kernel do Linux
jeto recebeu o nome de UNIX 1 e foi sendo aprimorado para diferentes arquiteturas com o passar dos anos [UNIX (2007)]. Em 1991, Linus Torvalds desenvolveu o sistema Linux baseando-se no sistema operacional UNIX. Linus implementou o Linux conforme o padrão POSIX (Portable Operating System) 2 , que é o mesmo utilizado pela API (Application Programming Inteface) UNIX, mas não utilizou o código fonte do UNIX. Por isso, se diz que Linux é um Unix e não um UNIX [Cisneiros (2007)]. Gerada a primeira versão do Linux, o código fonte foi disponibilizado pela Internet e tornou-se popular. A partir de então, tornou-se um projeto colaborativo e várias distribuições do Linux foram geradas, atendendo a diferentes plataformas de hardware. A parte principal é o kernel do Linux, que possui código aberto e livre. Desenvolvedores podem realizar modificações no kernel do Linux, porém as funcionalidades básicas são comuns a todas as distribuições. As funcionalidades básicas do kernel do Linux são: tratamento de interrupções, escalonamento do processador, gerência de memória, gerência de entrada e saída e serviços de sistema tais como rede e comunicação entre processos. Este texto aborda detalhadamente estas funcionalidades. O objetivo deste curso é apresentar os conceitos gerais sobre sistemas operacionais e como o kernel do Linux implementa estes conceitos.
1.1
ESTRUTURA DA APOSTILA
A apostila está dividida em sete capítulos, cujo conteúdo aborda desde uma visão geral de sistemas operacionais, história do UNIX e Linux, implementação do Linux, até uma comparação entre Linux e algumas versões de sistemas baseados em Unix POSIX. O Capítulo 2 mostra uma visão geral dos sistemas operacionais, apresentando um histórico de desenvolvimento dos sistemas operacionais, como o kernel do Linux surgiu e suas características principais. Além disso, é explicado como obter o kernel do Linux. O Capítulo 3 apresenta o conceito de processos, como criar processo e as estruturas de dados utilizadas pelo kernel para armazenar processo, como é realizada a comunicação entre processos, como é feito o escalonamento de processos e como é realizado o sincronismo entre processos. O Capítulo 4 apresenta gerência de memória, incluindo paginação, tabelas hierárquicas e memória virtual. A forma como o kernel do Linux gerencia memória e quais as estruturas utilizadas são destacadas neste capítulo. 1A
marca UNIX é registrada por The Open Group [Bell (2007)]. estabelecida pelo IEEE (Institute of Electrical and Electronics Engineers) para intercomunicação entre diferentes sistemas operacionais [Santos (1999)] 2 Norma
Introdução
9
O Capítulo 5 apresenta o sistema de arquivos em um sistema operacional e no Linux, em específico. Os métodos de acesso a arquivos e a estrutura de diretórios são abordados. O Capítulo 6 aborda gerência de entrada e saída. Para terminar este apostila, o Capítulo 7 apresenta uma comparação entre os sistemas operacionais Solaris, FreeBSD e Linux. Esta apostila tem a maior parte de suas informações sobre sistemas operacionais baseada na referência [Silberschatz (2004)]. A maioria das informações sobre kernel do Linux foram baseadas nas referências [Love (2005), Maxwell (2000)]. A comparação entre Linux e sistemas baseado em Unix tem como referência o trabalho de [Bruning (2005)]. Para o leitor ter um conhecimento mais aprofundado sobre sistemas operacionais, os sistemas Unix e o kernel do Linux, sugere-se a leitura destas referências.
1.2
AGRADECIMENTOS
Aos professores Joaquim Quinteiro Uchôa e Denilson Vedoveto Martins, pela revisão deste texto e sugestões em relação a organização do texto e bibliografia utilizada.
10
EDITORA - UFLA/FAEPE - Kernel do Linux
2 VISÃO GERAL DE SISTEMAS OPERACIONAIS E O KERNEL DO LINUX
O objetivo deste capítulo é apresentar uma visão geral de sistemas operacionais como base para o surgimento do Linux. Por isso, são abordados os principais conceitos em sistemas operacionais e sua evolução história, com ênfase na história do Linux. As principais referências bibliográficas utilizadas foram [Silberschatz (2004)], [Love (2005)], [Bovet (2002)] e [Mazioli (2006)].
2.1
INTRODUÇÃO
Um sistema operacional é uma parte essencial de qualquer computador e pode ser definido como um programa que atua como um intermediário entre o usuário e o hardware de um computador. Sua finalidade é prover um ambiente no qual um usuário possa executar programas de uma forma conveniente e eficiente [Silberschatz (2004)]. Outra definição para um sistema operacional moderno é considerá-lo como sendo as partes do sistema responsáveis pelo uso básico e administração, o que inclui o kernel e device drivers, boot loader, shell ou outras interfaces com o usuário, e arquivos básicos e utilitários do sistema [Love (2005)]. Os primeiros sistemas operacionais eram responsáveis apenas por transferir automaticamente o controle de uma tarefa para a seguinte. Com o objetivo de melhorar o desempenho dos sistemas, foram sendo introduzidos novos recursos de hardware, como, por exemplo, a tecnologia de disco, exigindo também uma evolução dos sistemas operacionais, que passaram a oferecer mais serviços aos usuários e aos programas destes usuários. Alguns dos serviços oferecidos pelos sistemas operacionais atuais são listados a seguir: • Carregamento de programa na memória e execução deste programa • Gerenciamento de operações de entrada e saída • Manipulação de arquivos (criação, leitura, escrita, busca, entre outras) • Detecção de erros relacionados à CPU, acesso à memória, entrada e saída ou programa do usuário
12
EDITORA - UFLA/FAEPE - Kernel do Linux
• Alocação de recursos, tais como CPU e memória, aos processos dos usuários O Linux, portanto, provê estes serviços e busca acompanhar a evolução dos sistemas computacionais para prover serviços a diferentes plataformas.
2.2
VISÃO GERAL DOS SISTEMAS OPERACIONAIS
Os primeiros sistemas computacionais surgiram na década de 1950 com os mainframes, que eram usados para atender aplicações comerciais e científicas. Inicialmente, estes computadores eram operados a partir de uma console. Os dispositivos de entrada eram as leitoras de cartões e os drives de fita. Os dispositivos de saída eram as impressoras de linha, os drives de fita e as perfuradoras de cartões. Os jobs do usuário (cartões perfurados contendo o programa, dados e informações de controle do job) eram submetidos aos operador da máquina. Estes jobs eram armazenados em lotes (batches) de acordo com suas características semelhantes na tentativa de aumentar a velocidade de processamento. O sistema operacional era responsável somente por transferir o controle de um job para o seguinte. Estes sistemas eram denominados sistemas batch (lote) e apresentavam grande ociosidade da CPU. Em seguida, surgiu a tecnologia dos discos, permitindo ao sistema operacional deixar todos os jobs em um disco, podendo acessá-los diretamente. Assim, o sistema operacional poderia executar o agendamento ou escalonamento (scheduling) de jobs de modo a utilizar os recursos e executar as tarefas eficientemente. Com o agendamento de jobs surgiu a capacidade de multiprogramar, aumentando a utilização da CPU, pois os jobs eram organizados de modo que um deles sempre estaria utilizando a CPU. Estes sistemas foram denominados sistemas multiprogramados. Em um sistema multiprogramado, o sistema operacional possui a função de simplesmente redirecionar para um novo job e o executar. Quando o job selecionado necessitar aguardar, a CPU é redirecionada para outro job e assim por diante. O agendamento dos jobs, a gerência dos programas em memória e a escolha do próximo job a ser executado são responsabilidades do sistema operacional. Os sistemas multiprogramados não propiciavam a interação do usuário com o sistema computacional. A extensão da multiprogramação para tempo compartilhado permitiu que a CPU executasse múltiplos jobs, mas permutando entre eles de forma que as permutas ocorressem com muita freqüência de forma que os usuários pudessem interagir com cada programa, diretamente, usando um teclado ou um mouse, e aguardar por resultados imediatos. Assim, vários usuários podem compartilhar o computador simultaneamente. Os sistemas operacionais de tempo compartilhado usam o agendamento da CPU e a multiprogramação para atender cada usuário com uma pequena porção do computador de tempo compartilhado. Além de gerência e proteção de memória, os sistemas de tempo
Visão Geral de Sistemas Operacionais e o Kernel do Linux
13
compartilhado precisam gerar um tempo de resposta para os jobs que seja razoável. Para isso, os jobs são permutados entre a memória principal e o disco, que então funciona como uma memória de retaguarda para a memória principal. O método utilizado para alcançar este objetivo é a memória virtual, pois permite a execução de um job que pode não estar completamente na memória. A vantagem deste esquema é que os programas podem ser maiores do que a memória física. Os sistemas de tempo compartilhado devem prover, também, um sistema de arquivos e um mecanismo para execução concorrente, necessitando de um esquema de escalonamento de CPU mais sofisticado. Além disso, o sistema deve fornecer mecanismos para sincronização e comunicação de jobs, para garantir execução ordenada. Deve, também, assegurar que os jobs não ficarão eternamente um esperando pelo outro (deadlock). A construção de um sistema que atenda a todas estas necessidades é dispendioso. Por isso, a idéia de tempo compartilhado foi demonstrada desde de 1960, mas somente no início da década de 1970 é que se tornou mais comum. Foi na década de 1970 que surgiram os computadores pessoais (PCs). Inicialmente, os PCs não possuíam as facilidades necessárias para proteger um sistema operacional dos programas dos usuários. Mas, com o tempo, os objetivos dos sistemas operacionais passaram de maximizar a utilização da CPU e dos periféricos para maximizar a eficácia e a capacidade de resposta para o usuário. Estes sistemas incluem os PCs que executam sob o Microsoft Windows 1 e o Apple Macintosh 2 . O sistema operacional Linux foi inicialmente construído para um PC. Os sistemas operacionais para PCs beneficiaram-se dos sistemas operacionais já existentes para mainframes. Porém, a utilização de CPU para um PC não era mais um problema primordial, pois os indivíduos faziam uso isolado do computador. Mas algumas características dos sistemas operacionais dos mainframes ainda eram importantes. Por exemplo, a proteção de arquivo, que a princípio não seria necessária para um PC, com a interligação dos computadores por uma rede ou outras conexões da Internet, tornou-se necessária. Na década de 1980 começaram a surgir os sistemas multiprocessadores (ou sistemas paralelos ou sistemas fortemente acoplados). Estes sistemas possuem mais de um processador, que se comunicam, compartilham o mesmo barramento, o relógio e, algumas vezes, a memória e os dispositivos periféricos. Os sistemas multiprocessadores mais comuns usam multiprocessamento simétrico (SMP - Symmetric Multiprocesing), no qual cada processador executa concorrentemente uma cópia idêntica do sistema operacional, e estas cópias comunicam-se umas com as outras quando necessário. Outros utilizam multiprocessamento assimétrico, no qual a cada processador é designada uma tarefa específica. Este esquema define um relacionamento 1 Marca 2 Marca
registrada pela Microsoft Corporation [Microsoft (2007)] registrada pela Apple [Apple (2007)]
14
EDITORA - UFLA/FAEPE - Kernel do Linux
mestre-escravo, onde um processador mestre controla o sistema e os demais (escravos) ou se dirigem ao mestre para instruções ou possuem tarefas pré-definidas. A diferença entre os multiprocessamentos simétrico e assimétrico pode ser apresentada no hardware ou no software. Um hardware especial pode diferenciar os processadores múltiplos, ou um software pode ser escrito para permitir somente um mestre e múltiplos escravos. Por exemplo, o sistema operacional SunOS Versão 4 oferece multiprocessamento assimétrico. A Versão 5 (Solaris 2) é simétrica sobre o mesmo hardware. Com o crescimento das redes de computadores, especialmente a Internet e a World Wide Web (WWW), praticamente todos os PCs e estações de trabalho podem acessar a Internet por uma rede local ou conexão por telefone. Assim, as redes de computadores constituem-se de uma coleção de processadores que não compartilham memória ou um relógio. Cada processador tem sua memória local e comunicam-se através de barramentos de alta velocidade ou linha telefônica. Estes sistemas são denominados sistemas distribuídos ou sistemas fracamente acoplados. Os sistemas distribuídos podem compartilhar tarefas de computação e oferecer um rico conjunto de recursos aos usuários. Com o avanço da tecnologia, os PCs têm-se tornado mais rápidos, poderosos e baratos, fazendo com que os projetistas afastem-se de sistemas centralizados e concentremse mais em PCs. Assim, os sistemas centralizados atuam hoje como sistemas servidores para satisfazer as requisições geradas por sistemas clientes e são denominados clienteservidor. Um sistema servidor pode ser classificado em sistema servidor de processamento (compute-server) e sistema servidor de arquivos (file-server system). O sistema servidor de processamento oferece uma interface que permite ao cliente enviar requisições para realizar uma ação. O servidor, em resposta, executa a ação e envia os resultados de volta ao cliente. Já o sistema servidor de arquivos oferece uma interface para o sistema de arquivos, permitindo ao cliente criar, atualizar, ler e excluir arquivos. Um sistema distribuído pode ter também a estrutura de um sistema peer-to-peer (P2P), onde clientes e servidores não são diferenciados um do outro. Todos os nós dentro do sistema podem atuar tanto como cliente quanto como servidor. A vantagem deste sistema em relação aos sistemas cliente-servidor é que os serviços podem ser oferecidos por vários nós, distribuídos por meio da rede, evitando o gargalo em um único servidor. Outro desenvolvimento em sistemas operacionais envolve os sistemas em clusters, que reúnem diversas CPUs para realizar trabalho de computação. Estes sistemas são utilizados para oferecer serviço de alta disponibilidade, ou seja, um serviço que continuará a ser fornecido mesmo com falha em um ou mais sistemas no cluster. Uma camada de software de cluster é executada nos nós do cluster, permitindo, assim, que cada nó monitore um ou mais nós da rede. Se a máquina monitorada falhar, aquela que a estiver monitorando
Visão Geral de Sistemas Operacionais e o Kernel do Linux
15
poderá assumir o armazenamento e reiniciar as aplicações que estavam sendo executadas na máquina que falhou. Os sistemas em cluster podem ser assimétricos ou simétricos. No modo assimétrico, uma máquina está no modo hot-standby, enquanto a outra está executando as aplicações. A máquina hot-standby apenas monitora o servidor ativo. Se esse servidor falhar, o hotstandby se torna o servidor ativo. No modo simétrico, dois ou mais hosts estão executando aplicações, e eles estão monitorando um ao outro. Outra forma de sistema operacional de uso especial é o sistema de tempo real (realtime system), que é usado quando existem requisitos de tempo rígidos na operação de um processador ou do fluxo de dados. O processamento precisa ser feito dentro das restrições definidas, ou então o sistema falhará. Os sistemas portáteis são dispositivos de tamanho limitado, conseqüentemente, possuem pequena quantidade de memória, processadores lentos e telas de vídeo pequenas, para que o consumo de energia também seja pequeno. Entre estes dispositivos estão o Palm, Pocket-PCs e telefones celulares. O sistema operacional e as aplicações para estes dispositivos precisam ser elaborados de forma a não sobrecarregar o processador. As limitações na funcionalidade dos PDAs é compensada por sua conveniência e portabilidade. Dentro desta evolução dos sistemas operacionais, surgiu o Linux, que é apresentado na seção seguinte.
2.3
HISTÓRIA DO LINUX
O sistema operacional UNIX foi criado em 1969 por Dennis Ritchie e Ken Thompson, que eram pesquisadores da Bell Labs AT&T. A partir de 1977, começaram a ser liberadas versões do sistema operacional UNIX com código fonte, permitindo o desenvolvimento de outras versões de sistemas operacionais, baseadas no UNIX, por outras organizações. Como exemplo, podemos citar os sistemas operacionais Berkeley Software Distributions (BSD), da Universidade da Califórnia em Berkeley, o Solaris da Sun e o AIX da IBM. Como havia várias distribuições do UNIX, mas nenhuma padronização, surgiram duas iniciativas de padronização: POSIX e X/Open [Tanenbaum (1995)]. O Comitê de Padronização do IEEE, organização muito respeitada, no projeto POSIX (POS = Portable Operating System; IX foi adicionado para lembrar o Unix) criou o padrão 1003.x, que define um conjunto de rotinas de biblioteca que todo sistema Unix produzido em conformidade com este padrão deve suprir. O X/Open foi um consórcio fundado por diversos fornecedores de sistemas Unix da Europa com o objetivo de identificar e promover padrões abertos na área de tecnologia da informação. Seus membros originais eram a Bull, ICL, Siemens, Olivetti e Nixdorf. O
16
EDITORA - UFLA/FAEPE - Kernel do Linux
X/Open gerenciou a marca UNIX de 1993 até 1996, quando ele foi fundido com o Open Software Foundation para formar o The Open Group [UNIX (2007)]. A marca UNIX tornou-se uma marca registrada da Bell Labs AT&T, pois esta empresa havia criado originalmente o UNIX e por algum tempo somente seus clientes podiam chamar seus sistemas de UNIX. Assim, os sistemas operacionais que eram baseados no padrão POSIX, não podiam ser considerados sistemas UNIX. Em 1991, Linus Torvalds desenvolveu o Linux como um sistema operacional para computadores que utilizavam o microprocessador 80386 da Intel, que naquele tempo era um processador novo e avançado. Ele se baseou inicialmente no Minix, que era um sistema UNIX de baixo custo usado como ferramenta de ensino, mas depois decidiu escrever seu próprio sistema operacional. Assim, o Linux compartilha algumas idéias do UNIX, mas não é um descendente direto do código fonte UNIX, pois foi desenvolvido utilizando o padrão POSIX. A primeira versão do Linux foi distribuída pela Internet e conquistou novos desenvolvedores e usuários, tornando-se um projeto colaborativo. in Hoje em dia, Linux é um sistema operacional que pode ser executado em diferentes plataformas, como AMD x8664, ARM, Compaq Alpha, CRIS, DEC VAX, H8/300, Hitachi SuperH, HP PA-RISC, IBM S/390, Intel IA-64, MIPS, Motorola 68000, PowerPC, SPARC e UltraSPARC [Love (2005)]. A principal característica do Linux é ser um projeto colaborativo, desenvolvido por diferentes desenvolvedores utilizando a Internet como meio de troca de informações e disponibilização. Desta forma, apesar de Linus permanecer como o criador do Linux e mantenedor do kernel o progresso do sistema continua através dos grupos de desenvolvedores. O kernel do Linux é um software de código aberto, licenciado sob a GNU General Public License (GPL), e qualquer pessoa pode realizar download do código fonte. Um sistema Linux básico é composto pelo kernel, bibliotecas, compilador, ferramentas e utilitários básicos do sistema, tais como processo de login e shell. Mas o sistema pode incluir também uma moderna implementação X Window System (sistema de janelas), incluindo um ambiente desktop, tal como GNOME.
2.4
O KERNEL DO LINUX
O kernel de um sistema operacional é a parte mais interna, o centro. Seus componentes típicos são os tratadores de interrupções para requisições de serviços de interrupção, um escalonador para um processador de tempo compartilhado entre múltiplos processos, um sistema de gerenciamento de memória para gerenciar espaços de endereçamento de processos e serviços de sistema, tais como rede e comunicação entre processos. A Figura 2.1 apresenta uma visualização da arquitetura do kernel, mas esta figura não ilustra todos os componentes.
Visão Geral de Sistemas Operacionais e o Kernel do Linux
17
Figura 2.1: Visualização da arquitetura do kernel [Maxwell (2000)]
Em sistemas onde as unidades de gerência de memória são protegidas, o kernel reside em um estado do sistema mais elevado do que as aplicações de um usuário normal. Este estado do kernel permite o acesso ao espaço de memória protegido e ao hardware e é referido como espaço do kernel (kernel-space). Quando o kernel está executando o sistema está no modo kernel. As aplicações do usuário executam no espaço do usuário (userspace), possuem acesso somente a um subconjunto de recursos disponíveis da máquina e são capazes de realizar certas funções do sistema. Quando um usuário normal está executando, o sistema está no modo usuário. As aplicações que estão executando em um sistema comunicam-se com o kernel através de chamadas de sistema. O kernel gerencia o hardware do sistema. O hardware utiliza interrupções para comunicar com o sistema. Uma interrupção gerada pelo hardware interrompe o kernel e pode ser identificada por um número. O kernel utiliza o número para executar e responder a interrupção. Por exemplo, quando o usuário digita alguma coisa, o controlador do
18
EDITORA - UFLA/FAEPE - Kernel do Linux
teclado caracteriza uma interrupção e comunica ao sistema que há um novo dado no buffer do teclado. Através do número da interrupção o kernel executa o tratador de interrupção correto. O tratador de interrupção processa o dado do teclado e notifica o controlador do teclado de que está pronto para novos dados. Como mencionado anteriormente, o kernel do Linux possui a mesma API dos kernels modernos do Unix. Para realizar uma comparação entre o kernel do Linux e os kernels clássicos do Unix, considere a classificação dos kernels em monolítico e microkernel. Os kernels monolíticos são implementados como um processo grande executando em um único espaço de endereçamento. Desta forma, o kernel fica em um disco como uma única biblioteca estática binária. Todos os serviços do kernel existem e executam em um espaço grande de endereçamento do kernel. O kernel pode invocar funções diretamente assim como uma aplicação do espaço do usuário. A maioria dos sistemas Unix são monolíticos. Os microkernels não são implementados como um único processo grande. As funcionalidades do kernel são quebradas em processos separados, chamados servidores. Somente os servidores que necessitarem de modo de execução privilegiado é que podem ser executados neste modo. Os demais servidores executam no espaço do usuário. Todos os servidores são mantidos separados e executam em diferentes espaços de endereçamento. A invocação direta de funções não é possível. A comunicação é feita através da passagem de mensagens. Um mecanismo de comunicação entre processos (InterProcess Communication - IPC) é construído no sistema e os vários servidores comunicam-se e invocam serviços entre si enviando mensagens. O kernel do Linux é do tipo monolítico, isto é, executa em um único espaço de endereçamento inteiramente no modo kernel. Porém, possui algumas características de microkernel: possui um projeto modular com kernel preemptivo, suporte a threads de kernel e a capacidade de dinamicamente carregar bibliotecas separadas do kernel (kernel modules). Tudo é executado no modo kernel, com a invocação direta de funções. Linux é modular e baseado em threads. Algumas diferenças do kernel do Linux em relação a outras variantes do Unix são apresentadas a seguir. • O Linux suporta o carregamento dinâmico de módulos do kernel • O Linux tem suporte a multiprocessador simétrico (Symmetrical Multiprocessor SMP), enquanto que a maioria das implementações Unix não suportam • O kernel do Linux é preemptivo, permitindo interromper uma tarefa mesmo se ela estiver executando no kernel, o que não é comum na maioria dos kernels tradicionais do Unix • Linux possui uma estratégia interessante de suporte a thread: não diferencia entre threads e processos normais. Para o kernel, todos os processos são iguais, per-
Visão Geral de Sistemas Operacionais e o Kernel do Linux
19
mitindo o compartilhamento de recursos. (Os conceitos de processo e thread são detalhados no Capítulo 3) • Linux provê um modelo de dispositivos orientados a objeto com classes de dispositivos, eventos hotpluggable e um user-space device filesystem (sysfs) (Maiores detalhes são encontrados no Capítulo 5) O kernel do Linux suportar o carregamento dinâmico de módulos significa que o kernel não precisa estar todo na memória. Algumas porções são fundamentais para o funcionamento do kernel, tais como, o código para escalonamento de processos. Mas outras, só preciam ser carregadas quando se fazem necessárias pelo kernel, podendo estar ausentes no resto do tempo, como é o caso da maioria dos drivers de dispositivos. Considere, por exemplo, o código que habilita o kernel a acessar seu drive de CDROM. Este código, só precisa estar em memória durante o acesso ao aparelho, de forma que o kernel pode ser configurado para carregar tal código um pouco antes de cada acesso. Após o acesso o código poderá ser removido. As seções do kernel que podem ser adicionadas ou removidas durante o funcionamento do sistema são chamadas de módulos do kernel. Os módulos do kernel introduzem um custo em nível de complexidade, pois a adição e remoção de partes do kernel durante a execução necessita de uma codificação extra. Este custo de complexidade pode ser diminuído se for delegado a um programa externo, pois distribuirá a complexidade. O programa externo utilizado para este propósito é o modprobe. Existem várias funções do kernel que interagem com o modprobe para carregar módulos. Por exemplo, a função request_module() é a função que o restante do kernel chama quando descobre que há necessidade de se carregar um módulo e a função exec_modprobe() executa o programa que anexa o módulo ao kernel. Os kernels do Linux distinguem-se entre estáveis e em desenvolvimento com um esquema de nomes formado por três números, separados por um ponto [Love (2005), Mazioli (2006)]. O primeiro valor corresponde à release maior, o segundo é a release menor e o terceiro é revisão (patchlevel). A release menor determina se o kernel é estável ou está em desenvolvimento. Um número par significa que o kernel é estável e um número ímpar significa que está em desenvolvimento. Por exemplo, kernel 2.6.0 significa que tem uma versão maior 2, tem uma versão menor 6 (que é estável) e está na revisão 0. 2.4.1
Obtenção, configuração e compilação do kernel do Linux
O código fonte mais atual do Linux está sempre disponível, na versão completa e em patches incrementais, na página oficial do kernel do Linux: http://www.kernel.org. A versão completa do código fonte do kernel do Linux é distribuída no formato GNU zip (gzip) e no formato bzip2. O formato bzip2 possui o nome linux-x.y.z.tar.bz2, onde x.y.z é a versão de uma release particular do fonte do kernel. Após realizar o download do fonte,
20
EDITORA - UFLA/FAEPE - Kernel do Linux
é necessário descompactá-lo. Se for a versão completa compactada com bzip2, basta executar: $ tar xvjf linux-x.y.z.tar.bz2. Se for a versão compactada com GNU zip, basta executar: $ tar xvzf linux-x.y.z.tar.gz. Após descompactar o fonte será gerado o diretório linux-x.y.z. A comunidade de kernel do Linux distribui mudanças no código através de patches. Assim, os patches incrementais permitem passar de uma versão do fonte do kernel para outra. Ao invés de realizar download da versão completa do fonte do kernel, o desenvolvedor pode simplesmente aplicar um patch incremental e passar de uma versão para outra. Esta opção é mais rápida pois os arquivos são menores. Para aplicar um patch incremental, de dentro da árvore de fonte do kernel, basta executar: $ patch p1 < ../patch-x.y.z Geralmente, um patch para uma dada versão do kernel é aplicada à versão anterior. A árvore de arquivos fontes do kernel do Linux é dividida em diretórios e subdiretórios. A Tabela 2.1 apresenta os diretórios e suas descrições. Diretório arch Documentation drivers fs include init ipc kernel lib mm net scripts security sound usr
Descrição Códigos específicos para arquiteturas (i386, alpha, mips, etc.) Documentação do fonte do kernel Device drivers para placas de vídeo, placas de rede, adaptadores SCSI, etc. Sistemas de arquivos suportados pelo Linux, como, por exemplo, o ext2 Arquivos de cabeçalhos do kernel (.h) Código de inicialização do kernel Código de comunicação entre processos Parte principal do kernel: processos, execução de programa, sinais, módulos, escalonador, etc Funções do kernel de propósito geral Subsistema de gerência de memória Subsistema de rede Programas externos usados para construir a imagem do kernel Módulo de Segurança do Linux Subsistema de som Código de espaço de usuários
Tabela 2.1: Diretórios na árvore do fonte do kernel [Love (2005)]
Visão Geral de Sistemas Operacionais e o Kernel do Linux
21
O kernel 2.6 do Linux introduziu opções novas de configuração e construção do sistema. Como o código fonte do kernel do Linux está disponível, é possível configurar e customizá-lo antes de compilá-lo. O kernel possui várias características e suporte a diferentes tipos de hardware, por isso é necessário configurá-lo. As opções de configuração são prefixadas pelo CONFIG na forma CONFIG_FEATURE. Por exemplo, multiprocessamento simétrico (SMP) é controlado pela opção de configuração CONFIG_SMP. Se esta opção estiver habilitada, SMP é habilitado, caso contrário ficará desabilitado. As opções de configuração são usadas para decidir quais os arquivos a serem gerados e quais códigos manipulados via diretivas de preprocessador. O kernel possui múltiplas ferramentas para facilitar a configuração. A mais simples é baseada em texto, utilizando o utilitário em linha de comando: $ make config Este utilitário vai através de cada opção, uma por uma, solicitando ao usuário para responder yes (sim), no (não) ou module. Esta forma gasta muito tempo para ser executada. Porém, existem outras formas mais rápidas. Por exemplo, um utilitário gráfico, pode ser executado pelo comando: $ make menuconfig Um utilitário gráfico baseado em X11 pode ser invocado por: $ make xconfig Um utilitário gráfico baseado em gtk+ é chamado pelo comando: $ make gconfig Estes três utilitários dividem as várias opções de configuração em categorias, de acordo com o tipo de processador e características. Pode-se mover através das categorias, ver as opções de kernel e alterar seus valores. O comando $ make defconfig cria uma configuração baseada nos valores default da arquitetura. As opções de configuração são armazenadas no root da árvore do fonte do kernel, no arquivo denominado .config. Este arquivo pode ser editado diretamente. Depois de configurado o kernel, o seguinte comando é necessário para construí-lo: $ make Após a construção do kernel é necessário instalá-lo, mas isso depende da arquitetura e do boot loader. É necessário consultar as orientações do boot loader usado de onde copiar a imagem do kernel e como setá-lo para boot. Por exemplo, considere a arquitetura x86 usando o boot loader Grub 3 . É necessário copiar arch/i386/boot/bzImage para /boot, nomeá-lo como algo do tipo vmlinuz-version e editar /boot/grub/grub.conf com a nova 3 Grub
- Grand Unified Boot Loader é um gerenciador de boot que permite inicializar diferentes sistemas operacionais e dá suporte a diferentes sistemas de arquivos [Mazioli (2006)]
22
EDITORA - UFLA/FAEPE - Kernel do Linux
entrada para o novo kernel. Em sistemas usando LILO 4 para boot, o arquivo a ser editado é /etc/lilo.conf e em seguida é necessário reexecutar lilo. A instalação de módulos é automatizada e independente da arquitetura. Como root, basta executar o comando: % make modules_install e serão instalados todos os módulos compilados no diretório /lib. O processo de construção também cria o arquivo System.map no root da árvore de fontes do kernel. Ele contém uma tabela de símbolos, que mapeia símbolos do kernel para seus endereços iniciais. É usado durante depuração para tradução de endereços de memória para nomes de funções e variáveis.
2.5
INICIALIZAÇÃO DE UM SISTEMA EM UM PC
Cada CPU existente no PC deve inicializar e para isso executa um auto-teste durante uma fração de segundo [Bovet (2002)]. Se houver dois processadores Pentium, por exemplo, uma das CPUs é sempre a CPU primária e a outra é a secundária. A primária se encarrega de todo o trabalho restante na inicialização e o kernel ativará a segunda CPU mais tarde. Para o restante do procedimento de boot há apenas uma CPU para se preocupar, mas posteriormente o kernel deverá ativar explicitamente quaisquer CPUs adicionais. A seguir, a CPU busca e executa as instruções localizadas no endereço 0xfffffff0, bem próximas do último endereço possível em uma CPU de 32 bits. Não havendo memória RAM normal localizada neste endereço, o hardware de memória simula sua existência. A instrução existente neste endereço é um salto para o BIOS (Basic Input/Output System) que está montado na placa-mãe e é responsável pelo próximo estágio da inicialização. O BIOS começa pela escolha de um dispositivo de inicialização, utilizando regras incorporadas a seu funcionamento. O mais comum é o BIOS tentar, em primeiro lugar, dar andamento à inicialização a partir de uma unidade para discos flexíveis e, no caso de falha, a partir do disco rígido primário. Se esta tentativa falhar, a tentativa poderá ser a partir de um drive de CD-ROM. Esta discussão assumirá a inicialização pelo disco rígido. A partir do dispositivo de inicialização, o BIOS lê o primeiro setor denominado Master Boot Record (MBR). O que acontece a seguir depende de como o Linux foi instalado no sistema. Assumindo que o LILO é o carregador do kernel, o BIOS chega alguns valores presentes no MBR e inspeciona para obter a localização do setor de inicialização. O BIOS carregará esse setor, que contém o início do LILO, para a memória e se posicionará em seu princípio. a partir de então o LILO estará no controle e carregará sua parte restante e encontrará seus dados de configuração no disco, que lhe informará onde encontrar o kernel 4 LILO
- Linux Loader é gerenciador de partida padrão para quem deseja iniciar o GNU/Linux através do disco rígido. Ele permite selecionar qual sistema operacional será iniciado (caso haja mais de um) e funciona tanto em discos rígidos IDE como SCSI [Mazioli (2006)].
Visão Geral de Sistemas Operacionais e o Kernel do Linux
23
e quais as opções a serem passadas para a inicialização. O LILO carregará o kernel para a memória e passará a utilizá-lo. O kernel que está inicialmente compactado se descompacta e transfere o controle para o kernel não compactado. Após a carga do kernel na memória e depois que alguns dispositivos de hardware essenciais, como a unidade de gerência de memória, forem configurados em um nível baixo, o kernel pulará para a função start_kernel() que inicializa todas as estruturas de dados necessárias ao kernel, habilita interrupções e cria outra thread do kernel (processo 1), conhecida como processo init, pois executa a função init(), que torna completa a inicialização do kernel. A função init() invoca a chamada de sistema execve() para carregar o programa executável init. Como resultado, a thread do kernel init torna-se um processo regular tendo sua própria estrutura de dados do kernel por processo. O processo init fica ativo até que o sistema seja desligado, já que ele cria e monitora a atividade de todos os processos que implementam os outros níveis do sistema operacional.
2.6
RESUMO
Este capítulo apresentou noções gerais sobre o histórico de evolução dos sistemas operacionais, os serviços que são oferecidos por um sistema operacional, o histórico e características do Linux, estrutura do kernel do Linux, como obter os fontes e configurar instalar o kernel do Linux.
24
EDITORA - UFLA/FAEPE - Kernel do Linux
3 GERÊNCIA DE PROCESSOS
Este capítulo apresenta conceitos importantes sobre processos, como os estados, a criação, as estruturas de dados utilizadas na implementação do Linux e o escalonamento. A referência principal utilizada neste capítulo foi [Silberschatz (2004)].
3.1
INTRODUÇÃO
Nos sistemas computacionais atuais, a multiprogramação permite que vários programas sejam carregados na memória e executados de forma concorrente. Além disso, espera-se que o usuário seja atendido na execução de seus programas o melhor e mais rápido possível. Desta forma, foi verificada a necessidade de um controle melhor da execução dos programas e tarefas do sistema. Por isso, o conceito de processo surgiu. Um processo pode ser definido como um programa em execução. Mas um processo também inclui um conjunto de recursos tais como arquivos abertos e sinais pendentes, dados internos do kernel, estado do processador, um espaço de endereçamento, um ou mais threads de execução e uma seção de dados contendo variáveis globais. Assim, um programa por si só não é um processo. Um processo é um programa ativo e seus recursos relacionados. Dois ou mais processos existentes podem estar executando o mesmo programa e estarão, neste caso, compartilhando vários recursos, tais como arquivos abertos ou um espaço de endereçamento. Do ponto de vista do kernel, o propósito de um processo é atuar como uma entidade para a qual os recursos do sistema (tempo de CPU, memória, etc) são alocados. A interface entre um processo e o sistema operacional é denominada chamada de sistema. Ela ocorre quando um processo de usuário solicita um srviço proporcionado pelo kernel por meio da chamada de uma função especial. O processo do usuário é colocado em espera e o kernel examina a solicitação, tenta executá-la e passa o resultado de volta para o processo de usuário, que então é reiniciado. Exemplos de chamadas de sistema relacionadas a processos são: fork, execve, kill. Um processo, durante sua execução, passa por diferentes estados. Os estados possíveis para um processo são os seguintes [Silberschatz (2004)]:
26
EDITORA - UFLA/FAEPE - Kernel do Linux
• Novo: quando o processo está sendo criado • Executando: as instruções estão sendo executadas • Esperando: o processo está esperando pela ocorrência de algum evento (como o término de entrada e saída ou a recepção de um sinal) • Pronto: o processo está esperando para ser designado a um processador • Terminado: o processo terminou sua execução Quando um processo filho é criado, ele é quase idêntico ao seu pai (processo que o criou). Ele recebe um cópia lógica do espaço de endereçamento do pai e executa o mesmo código como o pai, começando na próxima instrução após a chamada de sistema que criou o processo. Embora pai e filho possam compartilhar as páginas contendo o código do programa (texto), eles possuem cópias separadas de dados (pilha e heap), tal que as mudanças realizadas pelo filho em uma locação de memória são invisíveis ao pai (e vice-versa). Cada processo é representado no sistema operacional por um bloco de controle de processo (PCB - Process Control Block), que contém informações associadas ao processo específico, tais como: Estado do processo: o estado pode ser novo, pronto, executando, esperando, etc Contador de programa: indica o endereço da próxima instrução a ser executada pelo processo Registradores da CPU: incluem acumuladores, registradores de índice, ponteiros de pilha, registradores de uso e informações de código de condição. Quando ocorre uma interrupção, esta informação de estado deve ser salva juntamente com o contador de programa, para que o processo possa continuar corretamente mais tarde Informação de escalonamento de CPU: inclui prioridade do processo, ponteiros para filas de escalonamento e quaisquer outros parâmetros de escalonamento Informação de gerência de memória: inclui os valores dos registradores base e limite, as tabelas de páginas ou as tabelas de segmentos, dependendo do sistema de memória utilizado pelo sistema operacional Informação de contabilização: inclui a quantidade de tempo de CPU e de tempo real utilizado, limites de tempo, registros de contabilidade, números de jobs ou processos, etc Informação de estado de E/S: inclui a lista de dispositivos de E/S alocados a este processo, uma lista de arquivos abertos, etc O PCB serve como repositório para qualquer informação que possa variar de um processo para outro.
Gerência de Processos
27
Threads de execução ou simplesmente threads, são os objetos de atividade dentro do processo. Cada thread é composta por um contador de programa único, pilha de processo e um conjunto de registradores do processador. Os sistemas Unix modernos suportam aplicações multithread, onde os programas dos usuários possuem muitos fluxos de execução relativamente independentes que compartilham uma grande porção das estruturas de dados da aplicação. Nestes sistemas, um processo é composto por diversas threads, cada uma representando um fluxo de execução do processo. A maioria das aplicações multithread são escritas usando um conjunto padrão de funções de biblioteca chamadas bibliotecas pthread (POSIX thread). Versões mais antigas do kernel do Linux não ofereciam suporte a aplicações multithread. Do ponto de vista do kernel, uma aplicação multithread era um processo normal. Os múltiplos fluxos de execução de uma aplicação multithread eram criados, tratados e escalonados inteiramente no modo usuário, geralmente por uma biblioteca pthread. Porém, tais implementações não eram muito satisfatórias. Por exemplo, suponha que um programa de xadrez utilize duas threads: uma para controlar o tabuleiro gráfico, esperar pelo movimento do jogador humano e mostrar os movimentos do computador, enquanto a outra thread reflete sobre o próximo movimento do jogo. Enquanto a primeira thread espera pelo movimento humano, a segunda thread deve executar continuamente, explorando o tempo de pensamento do jogador humano. Entretanto, se o programa de xadrez é um único processo, a primeira thread não pode simplesmente realizar uma chamada de sistema bloqueante para esperar por uma ação do usuário, pois neste caso a segunda thread seria bloqueada também. Assim, a primeira thread deve utilizar técnicas não bloqueantes sofisticadas para garantir que o processo permaneça executando. O Linux utiliza processos leves (lightweight processes) para oferecer melhor suporte para aplicações multithread. Basicamente, dois processos leves podem compartilhar alguns recursos, como espaço de endereçamento, arquivos abertos, etc. Quando um deles modifica um recurso compartilhado, o outro imediatamente vê a alteração. Porém, é necessário que os dois processos realizem sincronização para acessar o recurso compartilhado. Se processos leves estão disponíveis, uma forma de implementar aplicações multithread é associando um processo leve com cada thread. Desta forma, as threads podem acessar o mesmo conjunto de estruturas de dados da aplicação simplesmente compartilhando o mesmo espaço de endereçamento de memória, o mesmo conjunto de arquivos abertos e assim por diante. Ao mesmo tempo, cada thread pode ser escalonada independentemente pelo kernel tal que possa dormir enquanto a outra permaneça executando. Dois exemplos de bibliotecas pthreads compatíveis com POSIX que utilizam processos leves do Linux são Linux Threads [Bovet (2002)] e o Next Generation Posix Threading Package (NGPT) da IBM [Seebac (2004)].
28
EDITORA - UFLA/FAEPE - Kernel do Linux
O kernel escalona threads individuais, não processos, pois thread é considerada como um tipo especial de processo.
3.2
CRIAÇÃO DE PROCESSOS
A criação de processos no Linux ocorre através da chamada de sistema fork(), que cria um novo processo duplicando um processo existente. O processo que chama o fork() é o processo pai, enquanto que o novo processo é o processo filho. O pai prossegue a execução e o filho começa a execução do mesmo ponto onde a chamada do fork() foi feita. Esta chamada de sistema retorna do kernel duas vezes: uma no processo pai e outro no processo filho. Os dois processos diferem no seu PID, que é um identificador único para cada processo. Imediatamente após um fork() é desejável executar um programa diferente. A família exec() de chamadas de função é usada para criar um novo espaço de endereçamento e carregar um novo programa nele. Um programa termina sua execução via chamada de sistema exit(). Esta função termina o processo e libera todos os seus recursos. Um processo pai pode informar-se sobre o estado de um filho terminado via a chamada de sistema wait(), que permite a um processo esperar pela terminação de um processo específico. Quando um processo termina, ele é colocado no estado zumbi, que é usado para representar um processo filho que terminou, até que o processo pai chame wait() ou waitpid(). O Linux implementa fork() via a chamada de sistema clone(). Esta chamada possui uma série de flags que especificam que recursos devem ser compartilhados entre os processos pai e filho. As chamadas fork(), vfork() e __clone() invocam a chamada sistema clone() com as flags de requisito. A chamada de sistema clone(), por sua vez, chama do_fork(), que é definido em kernel/fork.c. A função do_fork(), chama copy_process() e então inicia a execução do processo. copy_process() realiza as seguintes atividades: • Cria uma nova pilha de kernel, uma estrutura com informações de threads e um descritor de processo para o novo processo. Os novos valores são idênticos àqueles da tarefa corrente. Neste ponto, os descritores de processo do processo pai e do filho são idênticos • Verifica se o novo filho não excederá os limites de recursos para o número de processo do usuário atual • Para o filho se diferenciar do pais, vários campos do descritor de processo são limpados ou setados para valores iniciais. Membros do descritor de processo que não são herdados são informações estatísticas primárias. O conjunto de dados no descritor de processo é compartilhado
Gerência de Processos
29
• Em seguida, o estado do filho é setado para TASK_UNINTERRUPTIBLE, para garantir que ele não execute ainda • Então, copy_process() seta as flags do descritor de processo • Dependendo das flags passadas para clone(), copy_process() duplica ou compartilha arquivos abertos, informações sobre sistema de arquivos, tratadores de sinais, espaço de endereçamento de processos e espaço de nomes • A seguir, o timeslice restante entre pai e filho é dividido entre eles • copy_process() limpa e retorna, para o processo que o chamou, um ponteiro para o novo filho. Voltando ao do_fork(), se copy_process() retorna com sucesso, o novo filho é acordado e executa O código a seguir apresenta um exemplo de programa para a criação de processo no Linux [Silberschatz (2004)] e ilustra a utilização das chamadas de sistema fork(), wait() e exit(). 1 # i n c l u d e < s t d i o . h> 2 v o i d main ( i n t argc , char ∗ argv [ ] ) 3 { 4 i n t pid ; 5 / ∗ c r i o u um novo processo ∗ / 6 pid = fork ( ) ; 7 i f ( p i d <0) { / ∗ e r r o o c o r r i d o ∗ / 8 f p r i n t f ( s t d e r r , " Fork f a l h o u " ) ; 9 e x i t ( −1); 10 } e l s e i f ( p i d ==0) { / ∗ processo f i l h o ∗ / 11 e x e c l p ( " / b i n / l s " , " l s " , NULL ) ; 12 } 13 e l s e { / ∗ processo p a i ∗ / 14 / ∗ p a i i r á e s p e r a r p e l a conclusão do f i l h o ∗ / 15 w a i t ( NULL ) ; 16 p r i n t f ( " F i l h o completou " ) ; 17 exit (0); 18 } 19 }
Este programa cria um novo processo na linha 6 com a chamada fork(). Na variável pid é retornado zero para o processo filho e identificador do filho para o processo pai. Após o fork() pai e filho continuam a execução do código. Na linha 11, execlp é usada para realocar espaço de memória do processo a um novo programa. A chamada execlp carrega um arquivo binário na memória e inicia sua execução. Deste modo, pai e filho são capazes de estabelecer comunicação e então seguir seus caminhos separados. O filho executa o programa ls e o pai aguarda pela conclusão do processo filho com a chamada de sistema
30
EDITORA - UFLA/FAEPE - Kernel do Linux
wait (linha 15). Quando o processo filho termina, o processo pai reassume a partir da chamada wait e termina usando a chamada de sistema exit (linha17).
3.3
DESCRITOR DE PROCESSO E A ESTRUTURA DE TAREFAS
O kernel armazena a lista de processos em uma lista encadeada duplamente circular denominada lista de tarefas. Cada elemento na lista de tarefas é um descritor de processo do tipo struct task_struct, que é definido em <linux/sched.h>, e contém todas as informações sobre um processo específico. Então, no Linux o bloco de controle de processos é implementado nesta estrutura. Portanto, todas as informações sobre os arquivos abertos, o espaço de endereçamento do processo, os sinais pendentes, o estado do processo, etc, são armazenados nesta estrutura. A Figura 3.1 representa o descritor do processo e lista de tarefas.
Figura 3.1: O descritor do processo e a lista de tarefas [Love (2005)]
Os estados do processo no kernel do Linux são os seguintes: • TASK_RUNNING: o processo está executando • TASK_INTERRUPTIBLE: o processo está bloqueado (dormindo), esperando por alguma condição para existir. Quando esta condição existe o kernel seta o estado do processo para TASK_RUNNING. O processo também acorda prematuramente e torna-se executável se ele receber um sinal
Gerência de Processos
31
• TASK_UNINTERRUPTIBLE: estado idêntico ao estado TASK_INTERRUPTIBLE, exceto que ele não acorda e torna-se executável se ele receber um sinal. Este estado é usado em situações onde o processo deve esperar sem interrupção ou quando o evento é esperado ocorrer muito rapidamente • TASK_ZOMBIE: a tarefa terminou, mas seu processo pai ainda não realizou uma chamada de sistema wait4(). O descritor de processo da tarefa deve permanecer, pois o pai pode querer acessá-lo. Se o pai chama wait4(), o descritor do processo é desalocado • TASK_STOPPED: a execução do processo terminou; a tarefa não está executando nem está elegível para executar. Isto ocorre se a tarefa recebe o signal SIGSTOP, SIGTSTP, SIGTTIN ou SIGTTOU ou se ela recebe um sinal enquanto está sendo depurada A Figura 3.2 apresenta o diagrama de estados de processos no kernel do Linux e suas transições possíveis.
Figura 3.2: Diagrama de estados de processos no kernel do Linux
O código do kernel freqüentemente precisa mudar o estado do processo. O mecanismo preferido é utilizando a seguinte função:
32
EDITORA - UFLA/FAEPE - Kernel do Linux
set_task_state(task, state); Esta função seta a tarefa task para o estado state. Se aplicável, a função também providencia uma barreira para forçar a ordenação dos outros processo (isto é necessário somente em sistemas SMP).
3.4
CONTEXTO DO PROCESSO
Uma das partes mais importantes de um processo é a execução do código do programa, que é lido de um arquivo executável e executado dentro do espaço de endereçamento do programa. Um programa normal é executado no espaço do usuário. Quando este programa executa uma chamada de sistema ou dispara uma exceção, ele entra no espaço do kernel. Neste ponto, o kernel é dito estar no contexto do processo. Chamadas de sistema e tratadores de exceções são interfaces bem definidas dentro do kernel. Um processo pode começar a execução no espaço do kernel somente através de uma dessas interfaces.
3.5
ÁRVORE DA FAMÍLIA DE PROCESSOS
No Linux, há uma hierarquia entre os processos. Todos os processos são descendentes do processo init, cujo PID é 1 [Love (2005)]. O kernel inicializa init no último passo do processo de boot. O processo init, em resposta, lê os scripts de inicialização e executa mais programas, completando o processo de boot. Todo processo no sistema, tem exatamente um pai e todo processo tem zero ou mais filhos. Processos que são filhos do mesmo pai são denominados processos irmãos. O relacionamento entre os processo é armazenado no descritor de processo. Cada task_struct tem um ponteiro para a task_struct do pai, denominado parent, e uma lista de filhos, denominada children. Conseqüentemente, dado o processo corrente, é possível obter o descritor do processo de seu pai com o seguinte código: struct task_struct *my_parent = current->parent; Similarmente, é possível interagir sobre um filho do processo com o código apresentado na Figura 3.3. É possível seguir a hierarquia de qualquer processo no sistema pois a lista de tarefas é uma lista duplamente encadeada circular. Assim, para obter a próxima tarefa na lista, dada qualquer tarefa válida, basta usar: list_entry(task->tasks.next, struct task_struct, tasks) Para obter a tarefa anterior é da mesma forma: list_entry(task->tasks.prev, struct task_struct, tasks)
Gerência de Processos
33
Figura 3.3: Código da estrutura para interação entre processo pai e filho.
3.6
IMPLEMENTAÇÃO DE THREADS NO LINUX
Para o kernel do Linux, não há conceito de thread. Linux implementa todas as threads como processos padrão. Não há nenhuma semântica de escalonamento especial ou estruturas de dados para representar threads. Uma thread é meramente um processo que compartilha certos recursos com outros processos. Cada thread tem uma task_struct única e para o kernel é como um processo normal (que ocorre para compartilhar recursos, tais como um espaço de endereçamento, com outros processos). Para o Linux, threads são simplesmente uma forma de compartilhar recursos entre processos. Threads são criadas como tarefas normais, com a exceção que para a chamada de sistema clone() são passadas flags correspondendo aos recursos específicos a serem compartilhados: clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0); CLONE_VM: pai e filho compartilham o espaço de endereçamento. CLONE_FS: pai e filho compartilham informação do sistema de arquivos. CLONE_FILES: pai e filho compartilham arquivos abertos. CLONE_SIGHAND: pai e filho compartilham tratadores de sinais e sinais bloqueados. As flags de clone() são definidas em linux/sched.h. O código anterior resulta em um comportamento idêntico a um fork() normal, exceto que o espaço de endereçamento, recursos do sistema de arquivos, descritores de arquivos e tratadores de sinal são compartilhados. A nova tarefa e seu pai são chamados de threads.
3.7
ESCALONAMENTO DE PROCESSOS
O escalonador de processos é o componente do kernel que seleciona qual processo será o próximo a ser executado. Ele pode ser visto como um subsistema do kernel que divide o recurso finito do tempo de processador entre os processos executáveis em um sistema. Ao decidir que processo pode executar, o escalonador é responsável pela me-
34
EDITORA - UFLA/FAEPE - Kernel do Linux
lhor utilização do sistema e dar a impressão que múltiplos processos estão executando simultaneamente. O objetivo de um escalonador é utilizar melhor o tempo do processador, fazendo com que sempre haja um processo executando. Um sistema operacional multitarefa é aquele que pode permitir a execução de mais de um processo. Em máquinas com um único processador, dá a ilusão de que múltiplos processos estão executando concorrentemente. Em máquinas multiprocessadas, isto também permite que processos realmente executem concorrentemente, em paralelo, em diferentes processadores. Em sistemas operacionais Linux modernos é possível ter 100 processos em memória mas somente um no estado executável por processador. Linux é um sistema multitarefa e preemptivo. Neste modelo, o escalonador decide quando um processo deve interromper a execução e um novo processo deve ser escalonado para retomar a execução. O ato de suspender um processo que está executando é denominado preemptivo. O tempo que um processo executa antes de ser suspenso é predeterminado e é denominado timeslice do processo. O timeslice dá a cada processo pronto para ser executado uma fatia de tempo do processador. Gerenciar o timeslice permite ao escalonador tomar decisões de escalonamento global para o sistema e prevenir que um processo monopolize o processador. Para realizar um escalonamento é necessário que haja alguma política. Política de escalonamento é o comportamento do escalonador que determina o que e quando executar, de maneira que haja utilização ótima do tempo do processador. Processos podem ser classificados como I/O-bound ou CPU-bound. Processos I/O-bound são aqueles que gastam a maior parte de seu tempo submetendo e esperando por requisições de I/O (entrada e saída). Processos CPU-bound gastam mais tempo executando código. Eles tendem a executar até serem interrompidos, pois eles não bloqueiam para atender requisições de I/O com muita freqüência. O Linux busca proporcionar uma boa resposta interativa, otimizando a resposta a processos. Assim, favorece mais os processos I/O-bound do que os processos CPU-bound. Um algoritmo de escalonamento baseado em prioridade busca ordenar os processos baseado em sua necessidade de tempo de processador. Assim, processos com prioridade mais alta executam antes daqueles com uma prioridade mais baixa, enquanto que os processos com a mesma prioridade são escalonados de forma round-robin (um após o outro, repetidamente). No Linux, os processos com prioridade mais alta recebem timeslices maiores. Processos prontos para executar com timeslice restantes e prioridade mais alta sempre executam. Tanto o sistema quanto o usuário podem setar uma prioridade do processo para influenciar o comportamento do escalonamento do sistema. O Linux implementa um escalonamento baseado em prioridade dinâmica, onde há uma prioridade inicial para os processos e então é permitido ao escalonador aumentar ou
Gerência de Processos
35
diminuir a prioridade dinamicamente para preencher os objetivos do escalonamento. Por exemplo, um processo que está gastando mais tempo esperando por I/O do que executando é I/O-bound. No Linux, ele recebe um prioridade dinâmica elevada. Em contra partida, um processo que continuamente usa seu timeslice inteiro é CPU-bound e deveria receber uma prioridade dinâmica mais baixa. O kernel do Linux implementa dois intervalos de prioridade separados. O primeiro é um valor nice que varia de −20 a +19 com o default igual a 0. Valores maiores correspondem a uma prioridade mais baixa. Processos com um valor nice mais baixo (maior prioridade) executam antes de processos com um valor nice mais alto (menor prioridade). O valor nice ajuda determinar quão longo é um timeslice que um processo recebe. Um processo com um valor nice −20 recebe o timeslice maior possível, enquanto que um processo com um valor nice 19 recebe o timeslice menor possível. O segundo intervalo de valores é a prioridade de tempo real. Os valores são configuráveis, mas por default variam entre 0 e 99. Todos os processos de tempo real são de prioridade mais alta do que processos normais. Linux implementa prioridades de tempo real de acordo com o padrão POSIX. O Linux implementa os timeslices do escalonador de acordo com a Tabela 3.1. Ao ser criado, o processo filho recebe o valor nice do pai e timeslice correspondente à metade do tempo do processo pai. Tarefas com prioridade mínima têm duração de timeslice igual a 5 ms; tarefas com prioridade default a duração é de 100 ms. e tarefas com prioridade máxima é de 800 ms. Entretanto, o escalonador Linux dinamicamente determina o timeslice de um processo baseado na prioridade. Isto permite que processos com prioridade mais alta executam por tempo mais longo e com mais freqüência. A implementação de timeslices e prioridades dinâmicas provê um desempenho de escalonamento robusto. Tabela 3.1: Timeslices do escalonador
Tipo de tarefa Criada inicialmente Prioridade mínima Prioridade default Prioridade máxima
Valor Nice valor do processo pai +19 0 20
Duração do Timeslice metade do processo pai 5 ms. (MIN_TIMESLICE) 100 ms. (DEF_TIMESLICE) 800 ms. (MAX_TIMESLICE
Sendo o sistema operacional Linux um sistema preemptivo, quando um processo entra no estado TASK_RUNNING, o kernel verifica se sua prioridade é maior do que a prioridade do processo que está atualmente executando. Se for, o escalonador é invocado para interromper a processo em execução e colocar para executar um novo processo que estiver pronto para executar. Além disso, quando um timeslice do processo alcançar zero, ele é interrompido e o escalonador é novamente invocado para selecionar o novo processo.
36
EDITORA - UFLA/FAEPE - Kernel do Linux
O algoritmo de escalonamento do kernel do Linux está definido em kernel/sched.c. A estrutura de dados básica no escalonador está na fila de execução, que é definida como struct runqueue. Cada processo pronto para executar está exatamente em uma runqueue. A runqueue contém também informação de escalonamento por processador. Conseqüentemente, é uma estrutura de escalonamento primária para cada processador. Cada fila de execução contém dois vetores de prioridade, o vetor ativo e o expirado. Vetores de prioridade são definidos em kernel/sched.c como struct prio_array. Cada vetor de prioridade contém uma fila de processos prontos para executar por nível de prioridade. Essas filas contém lista de processos prontos para executar em cada nível de prioridade. Os vetores de prioridade também possuem um mapa de bits de prioridade usados para eficientemente descobrir a tarefa executável de mais alta prioridade no sistema. No Linux, a troca de contexto, ou seja, a troca de uma tarefa executável para outra, é realizada pela função context_switch() definida em kernel/sched.c. Quando um novo processo é selecionado para executar ocorre a troca do mapeamento da memória virtual do processo anterior para o novo processo; o estado do processador do processo anterior para o processo o processo corrente, salvando e restaurando as informações da pilha e registradores do processador. O Linux possui uma família de chamadas de sistema para gerenciar os parâmetros do escalonador. As chamadas de sistema permitem a manipulação de prioridade do processo, política de escalonamento, entre outras. A Tabela 3.2 apresenta algumas chamadas de sistema relacionadas ao escalonador e sua descrição. Tabela 3.2: Principais chamadas de sistema relacionadas ao escalonador
Chamada de sistema nice() sched_setscheduler() sched_getscheduler() sched_setparam() sched_getparam() sched_get_priority_max() sched_get_priority_min() sched_rr_get_interval()
Descrição Seta um valor nice de um processo Seta a política de escalonamento de um processo Obtém uma política de escalonamento de um processo Seta uma prioridade de tempo real de um processo Obtém uma prioridade de tempo real de um processo Obtém a prioridade de tempo real máxima Obtém a prioridade de tempo real mínima Obtém um valor de timeslice de um processo
As chamadas de sistema (denominadas syscalls no Linux) são acessadas via chamadas de funções, que necessitam de um ou mais argumentos (entradas) e pode resultar em um ou mais efeitos, por exemplo escrevendo em um arquivo ou copiando algum dado em um ponteiro. As chamadas de sistema também possuem um valor de retorno do tipo long que significa sucesso ou erro.
Gerência de Processos
37
O kernel do Linux implementa as chamadas de sistema e a cadeia de eventos necessários para executar uma chamada de sistema. Os eventos podem ser: execução de instruções trap dentro do kernel, transmissão do número da chamada de sistema e os argumentos necessários, execução da função de chamada de sistema correta e retorno ao espaço do usuário, em geral, com o valor de retorno da chamada de sistema. 3.7.1
Interrupções
Uma responsabilidade primária do kernel é gerenciar o hardware conectado à máquina. Por isso, o kernel precisa comunicar-se com os dispositivos individuais da máquina. Os processadores são bem mais rápidos que o restante do hardware. Portanto, não é ideal para o kernel fazer uma requisição e ficar esperando por uma resposta do hardware mais lento. O kernel deve ser livre para tratar outros trabalhos e realizar chamadas ao hardware somente depois que ele realmente completar seu trabalho. Uma solução para esse problema é polling, onde o kernel, periodicamente pode verificar o estado do hardware no sistema e dar uma resposta. Isto implica em overhead, independentemente se o hardware está ativo ou pronto, pois o polling ocorre repetidamente em intervalos regulares. Uma solução melhor é prover um mecanismo para o hardware sinalizar o kernel quando sua atenção for necessária. Esta solução são as interrupções, que permitem ao hardware comunicar-se com o processador. Por exemplo, quando o usuário digita alguma coisa, o hardware que gerencia (controlador) o teclado envia um sinal elétrico (interrupções) ao processador para alertar o sistema operacional para novamente disponibilizar o pressionamento de tecla. O processador recebe a interrupção e sinaliza o sistema operacional para permitir a resposta a um novo dado. Dispositivos de hardware geram interrupções assincronamente em relação ao clock do processador, pois elas podem ocorrer a qualquer momento. Conseqüentemente, o kernel pode ser interrompido a qualquer tempo para processar interrupções. Uma interrupção é fisicamente produzida por sinais eletrônicos originados dos dispositivos de hardware e dirigidos para o controlador de interrupções. Este controlador, em resposta, envia um sinal ao processador, que detecta este sinal e interrompe sua execução atual para tratar a interrupção. O processador pode notificar o sistema operacional que uma interrupção ocorreu e o sistema operacional pode tratar a interrupção adequadamente. Os valores de interrupção são chamados de interrupt request (IRQ). Por exemplo, em um PC, o IRQ 0 é a interrupção de tempo e o IRQ 1 é a interrupção do teclado. Uma interrupção específica está associada com um dispositivo específico e o kernel conhece esta informação. A função no kernel que executa em resposta a uma interrupção específica é chamada um tratador (handler) de interrupção ou rotina de serviço de interrupção (ISR - Interrupt Service Routine). Cada dispositivo que gera interrupção tem um tratador específico. O kernel
38
EDITORA - UFLA/FAEPE - Kernel do Linux
do Linux implementa uma família de interfaces para manipular o estado das interrupções em uma máquina. Essas interfaces permitem ao usuário desabilitar o sistema de interrupção para o processador corrente ou mascarar uma interrupção para a máquina inteira. Essas rotinas são dependentes da arquitetura e podem ser encontradas em <asm/system.h> e <asm/irq.g>. 3.7.2
Sincronização no kernel
Os recursos compartilhados no kernel necessitam de proteção de acesso concorrente porque se múltiplas threads de execução acessam e manipulam os dados ao mesmo tempo, as threads podem sobrescrever alterações realizadas por outras threads ou acessar dados enquanto estes estiverem em um estado inconsistente. Acesso concorrente a dados compartilhados é fonte de erros de execução. Por isso, é necessária a proteção apropriada de recursos compartilhados. Trechos de código que acessam e manipulam dados compartilhados são denominados seções críticas. Para prevenir acesso concorrente a regiões críticas, o programador deve garantir que o código execute atomicamente, isto é, a execução do código complete sem interrupção como se a região crítica inteira fosse uma instrução indivisível. Se for possível duas threads de execução estarem ao mesmo tempo na região crítica ocorre erro. Quando isso ocorre é denominado condição de corrida , pois as threads correram para acessarem a mesma região ao mesmo tempo. O ato de garantir que concorrência não segura seja prevenida e que condições de corrida não ocorram é denominada sincronização. Uma solução para o problema da seção crítica deve satisfazer às três seguintes exigências: Exclusão mútua: Se o processo Pi estiver executando em sua seção crítica, então nenhum outro processo poderá estar executando em sua seção crítica. Progresso: Se nenhum processo estiver executando em sua seção crítica e se algum processo quiser entrar em sua seção crítica, somente aqueles processos que não estiverem executando em sua seção remanescente poderão participar da decisão sobre qual processo será o próximo a entrar em sua seção crítica e esta seleção não poderá ser adiada indefinidamente. Espera limitada: Existe um limite parta o número de vezes em que outros processos podem obter permissão para entrar em suas seções críticas depois que um processo tenha feito uma solicitação para entrar em sua seção crítica e antes que a solicitação seja atendida. Ao apresentar um algoritmo, somente as variáveis usadas para sincronização são apresentadas. A Figura 3.4 mostra um exemplo de solução para seção crítica com 2 processos. Nesta figura, o processo Pi , verifica se é sua vez de entrar na seção crítica (flag[j]
Gerência de Processos
39
== TRUE) e se é a sua vez (turn == j). Se estas duas condições forem satisfeitas o processo entra na seção crítica, altera a variável compartilhada e libera a seção crítica (flag[i] = FALSE) para o outro processo.
Figura 3.4: Exemplo de seção crítica com dois processos.
Um método geral de sincronização é utilizar locks para proteger as regiões críticas. O lock mais comum no kernel do Linux é o spin lock. Um spin lock é um lock que pode ser segurado por no máximo uma thread de execução. Se uma thread de execução tenta obter (acquire) um spin lock enquanto ele já está ocupado (por outra thread), a thread fica em um loop (espera ocupada) até que o lock torne-se disponível. Se o lock não estiver ocupado, a thread pode imediatamente obter o lock e continuar. Há máquinas que fornecem instruções especiais de hardware que nos permitem tanto testar e modificar o conteúdo de uma palavra, como permitir os conteúdos de duas palavras atomicamente, ou seja, como uma unidade não interrompível. Estas instruções permitem resolver o problema da seção crítica. As instruções TestAndSet e Swap são exemplos destas instruções. Outra ferramenta de sincronização é o semáforo, que consiste em uma variável inteira, que à parte a inicialização, é acessada somente através de duas operações atômicas padrão: wait e signal. Além dos semáforos, existem os monitores, que são caracterizados por um conjunto de operadores definidos pelo programador. A representação de um tipo monitor consiste em declarações de variáveis cujos valores definem o estado de uma instância deste tipo, bem como os corpos de procedimentos ou funções que implementam operações sobre o tipo. Deadlock é uma condição que envolve um ou mais threads de execução e um ou mais recursos, tal que cada thread fica esperando por um dos recursos, mas todos os
40
EDITORA - UFLA/FAEPE - Kernel do Linux
recursos já estão sendo utilizados. As threads estão todas esperando umas pelas outras, mas elas nunca irão fazer progresso para liberar os recursos que já estão prendendo. Então, nenhuma das threads pode continuar, caracterizando um deadlock. Considere o exemplo onde uma thread tenta adquirir um lock que já está seguro por outra thread, ela tem que esperar que o lock seja liberado. Mas ele nunca liberará o lock, porque ele está ocupado esperando pelo lock e o resultado é deadlock.
3.8
RESUMO
Este capítulo apresentou uma visão geral sobre o conceito de processo, como pode ser criado um processo, os estados possíveis, threads de um processo, concorrência entre processos e problemas relacionados.
4 GERÊNCIA DE MEMÓRIA
Este capítulo aborda, inicialmente, gerência de memória de uma forma geral e, em seguida, explica como é realizada no Linux. Neste capítulo foram utilizadas basicamente as seguintes referências [Silberschatz (2004), Love (2005)].
4.1
INTRODUÇÃO
A memória de um sistema de computação consiste em um grande array de palavras ou bytes, cada um com seus próprios endereços. A CPU obtém instruções a partir da memória de acordo com o valor do contador de programa. A unidade de memória enxerga apenas uma cadeia de endereços de memória. Um programa reside em um disco como um arquivo binário executável. Para que seja executado, o programa deve ser carregado na memória e inserido em um processo. Dependendo do esquema de gerência de memória utilizado, o processo pode ser movimentado entre o disco e a memória durante sua execução. A memória principal deve acomodar tanto o sistema operacional como os vários processos dos usuários. Por isso, é necessário alocar porções diferentes da memória da maneira mais eficiente possível. A gerência de memória é responsável por controlar quais partes da memória estão em uso e quais não estão, de forma a alocar memória a processos quando estes precisarem, liberar a memória que estava sendo ocupada por um processo que terminou e tratar do problema do swapping entre a memória principal e o disco, quando a memória principal não for grande o suficiente para guardar todos os processos, entre outros aspectos.
4.2
ORGANIZAÇÃO DA MEMÓRIA
Em um sistema computacional tanto a CPU quanto o subsistema de entrada e saída interagem com a memória. Dado que cada palavra ou byte armazenado na memória possui seu próprio endereço, a interação é feita através de uma seqüência de leituras e escritas a endereços de memória específicos. Existem diferentes tipos de memória em um sis-
42
EDITORA - UFLA/FAEPE - Kernel do Linux
tema computacional, com diferentes características relativas à capacidade de armazenamento, custo e tempo de acesso. Estas memórias podem ser classificadas em memória secundária, memória principal e memória cache. A memória secundária é capaz de armazenar uma grande quantidade de informação, possui custo por bit menor e o tempo de acesso é maior do que à memória principal e à cache. A memória principal armazena relativamente menor quantidade de informação do que a memória secundária, possui custo por bit um pouco maior e tempo de acesso menor. A memória cache possui menor capacidade de armazenamento de informação em relação às memórias secundária e principal, possui custo por bit mais elevado e tempo de acesso bem menor. A organização de memória é o modo como a memória principal é vista pelo sistema, levando em consideração quantos usuários utilizarão a memória, quanto espaço de memória será dado a cada um deles, como a memória será dividida, em quantas partições, se os processos deverão ser alocados de forma contígua ou poderão estar espalhados pela memória principal, entre outras considerações. Na organização da memória em partições fixas, a memória é dividida em número fixo de partições ou regiões. A cada partição pode ser atribuído um processo para ser executado. Quando existe uma partição livre, um processo é selecionado de uma fila e carregado naquela partição. Quando ele termina sua execução, a partição torna-se livre para um outro processo. Em relação à fila de processos, esta pode ser única para cada partição ou única para as várias partições. Quando existe uma única fila para cada partição, a estratégia é classificar os processos segundo sua quantidade de memória necessária e colocá-lo na fila correspondente. Quando existe uma única fila para todas as partições, o escalonador de processos seleciona o próximo processo que será executado e em qual partição será carregado. Como existem diversos processos residentes na memória simultaneamente, devem haver mecanismos para proteger tanto o sistema operacional quanto os processos. A proteção deve ocorrer tanto na relocação estática quanto na relocação dinâmica. A proteção na relocação estática é realizada em tempo de montagem ou tempo de carga. Para isto são utilizados os dois registradores de limite inferior e superior. Cada endereço lógico gerado deve ser maior ou igual ao conteúdo armazenado no registrador de limite inferior e menor ou igual ao conteúdo armazenado no registrador de limite superior. Se for um acesso válido o endereço é então enviado à memória. A proteção na relocação dinâmica é feita em tempo de execução. Nesta proteção são empregados um registrador base e um registrador limite. O registrador base contém o valor do menor endereço físico. O registrador limite contém a faixa dos endereços lógicos.
Gerência de memória
43
Com os registradores base e limite, cada endereço lógico deve ser menor que o conteúdo armazenado no registrador limite. Esse endereço, se válido, é então calculado dinamicamente adicionando-se o valor contido no registrador base. O endereço calculado é então enviado à memória. Com a divisão da memória em partições fixas podemos ter ainda, o problema da fragmentação da memória. Existe fragmentação interna quando há um processo sendo executado em uma partição e ele não a ocupa por completo. Já a fragmentação externa existe quando uma partição não é utilizada, por ser pequena demais para qualquer processo que esteja esperando. Tanto a fragmentação interna quanto a externa são prejudiciais ao desempenho do sistema. Na organização de memória em partições fixas podem ocorrer os dois tipos de fragmentação. O problema principal das partições fixas é a determinação do tamanho das partições de modo que a fragmentação interna e externa seja mínima. A organização da memória com partições variáveis visa solucionar este problema permitindo que os tamanhos das partições variem dinamicamente. Para implementar a organização de memória com partições variáveis, o sistema operacional mantém uma tabela indicando quais partes da memória estão disponíveis e quais estão ocupadas. A princípio toda a memória está disponível e é considerada como um grande bloco de memória. Quando um processo chega e necessita de memória, é realizada uma busca por um espaço que seja grande o suficiente para armazená-lo. Se existe tal espaço é atribuído ao processo somente a quantidade de memória necessária. O restante do espaço, que pode haver, é deixado disponível para futuras requisições. Sempre que um processo termina sua execução ele libera seu espaço de memória. Esse espaço liberado é colocado de volta junto com os espaços de memória disponíveis. Neste ponto, procura-se verificar se há áreas adjacentes que possam ser recombinadas de modo a formar espaços de tamanhos maiores. Em qualquer momento, há um conjunto de espaços livres, de tamanhos variados e espalhados pela memória. Além disso, existe um conjunto de processos esperando para serem executados. Esta situação pode ser vista como uma aplicação geral do problema de alocação de memória dinâmica. O problema consiste em como satisfazer um pedido de tamanho n de uma lista de espaços livres. As estratégias mais comuns para selecionar um espaço de memória são: first-fit, best-fit ou worst-fit. A estratégia first-fit aloca o primeiro bloco que seja grande o suficiente. Não necessita de uma busca por toda a lista e é a estratégia mais rápida. A estratégia best-fit aloca o menor bloco que seja grande o suficiente. Se a lista não for ordenada por tamanho, é necessário varrer a lista inteira. Esta estratégia é a que provoca menor fragmentação da memória.
44
EDITORA - UFLA/FAEPE - Kernel do Linux
A estratégia worst-fit aloca o maior bloco. Se a lista não for ordenada por tamanho, é necessário varrer a lista inteira. Esta estratégia visa deixar espaços de memória maiores livres. Estas estratégias levam à fragmentação externa. À medida que os processos são carregados e removidos da memória, o espaço livre de memória é quebrado em pequenos pedaços. A fragmentação externa ocorre quando existe espaço de memória total suficiente para atender a solicitação, mas ele não é contíguo; a memória é fragmentada em um grande número de pequenos blocos livres. Uma solução para o problema de fragmentação externa é permitir que o espaço de endereçamento lógico de um processo seja não contíguo, possibilitando assim que seja alocada memória física a um processo em qualquer lugar onde haja disponibilidade. Duas técnicas complementares atendem a esta solução: a paginação e a segmentação. Estas técnicas também podem ser combinadas.
4.3
MEMÓRIA VIRTUAL
A técnica de memória virtual foi criada para permitir a execução de vários processos que não necessariamente estejam armazenados por inteiro na memória principal. A implementação de memória virtual é comumente realizada com paginação sob demanda ou com segmentação sob demanda. A técnica de swapping requer que o sistema possua um backing store ou memória de retaguarda (memória secundária, geralmente um disco rápido). A memória de retaguarda deve ser grande o suficiente para armazenar cópias de todos os programas de usuários e fornecer acesso direto a esses programas. O swapping nada mais é do que a troca do conteúdo de um determinado espaço de memória. Um processo é escolhido para ser retirado da memória e levado a memória de retaguarda. Este processo deve estar completamente ocioso. Na paginação o espaço de endereçamento físico de um processo não é contíguo. A memória física é vista como se estivesse dividida em blocos de tamanho fixo, denominados quadros (frames). A memória lógica é também dividida em blocos do mesmo tamanho, chamados páginas. Quando um processo está para ser executado, suas páginas são carregadas em quaisquer quadros de memória disponíveis, a partir da memória de retaguarda (disco). A memória de retaguarda é dividida em blocos de tamanho fixo do mesmo tamanho dos quadros de memória. O tamanho da página e o tamanho do quadro são definidos por hardware. Normalmente, o tamanho da página é uma potência de 2, variando entre 512 bytes e 16MB por página, dependendo da arquitetura do computador. Se o tamanho do espaço de endereços lógicos for 2m e o tamanho da página for 2n unidades de endereçamento, então os m − n bits
Gerência de memória
45
de alta ordem de um endereço lógico designam o número de página e os n bits de baixa ordem designam o deslocamento de página. Assim, o endereço lógico é o como mostrado na Figura 4.1, onde p é um índice para a tabela de página e d é o deslocamento dentro da página.
Figura 4.1: Endereço lógico na paginação
Um aspecto importante da paginação é a clara separação entre a visão que o usuário tem da memória e a memória física real. Os programas de usuário visualizam a memória como um espaço contíguo único contendo apenas seu programa. A diferença entre a visão do usuário sobre a memória e a memória física real é reconciliada pelo hardware de tradução de endereços. Os endereços lógicos são traduzidos para endereços físicos. Este mapeamento é controlado pelo sistema operacional. Por isso, ele mantém uma estrutura de dados chamada tabela de quadros, que tem uma entrada para cada quadro de página físico, indicando se o último quadro está livre ou alocado e, se ele estiver alocado, a qual página de qual processo ou processos estará alocado. A segmentação é um esquema de gerência de memória que admite a visão da memória pelo usuário como uma coleção de segmentos (espaço de endereços lógicos). Cada segmento possui um nome e um tamanho. Os endereços especificam o nome do segmento e o deslocamento dentro do segmento. O usuário especifica o endereço por um nome de segmento e um deslocamento. Para simplificar, os segmentos são numerados e referenciados com um número de segmento, em vez de um nome de segmento. Assim, um endereço lógico consiste em: <número-segmento, deslocamento> A tabela de segmentos é responsável por associar os endereços bidimensionais definidos pelo usuário aos endereços físicos unidimensionais. Cada entrada na tabela de segmentos possui uma base de segmento e um limite de segmento. A base de segmento possui o endereço físico inicial no qual o segmento reside na memória, enquanto o limite de segmento especifica a extensão do segmento. A proteção na segmentação é associada a cada segmento. Cada entrada na tabela de segmentos contém informações para prevenir acesso ilegal de leitura/escrita ou de acesso fora dos limites do segmento. A paginação sob demanda é similar a um sistema paginado com swapping. Os programas residem na memória secundária. Quando se inicia a execução, os programas
46
EDITORA - UFLA/FAEPE - Kernel do Linux
são trazidos para a memória principal. Porém, nunca uma página é trazida para a memória se ela não for necessária. Com isso, diminui-se o tempo de troca e a quantidade de memória física necessária. Para controlar o armazenamento das páginas trazidas para a memória, a tabela de páginas possui um bit de válido/inválido. Esse bit é ativado quando a página está presente na memória. Se o programa tenta acessar uma página que ainda não foi trazida para a memória, então é gerada uma interrupção por falta de página (page fault). Neste caso, a página será carregada na memória. Se a memória estivesse completa, ou seja, não houvesse mais quadros livres para serem alocados, seria necessário substituir alguma página. Para isso, seria necessário utilizar alguma estratégia de substituição de páginas. Existem várias técnicas de substituição de páginas: FIFO, OPT, LRU, Algoritmo da segunda chance, entre outras. O algoritmo FIFO (First-In First-Out) consiste em associar a cada página o tempo em que ela foi trazida para a memória. Quando uma página tiver que ser substituída, a página mais antiga na memória é escolhida. Se houver uma página bastante utilizada e há muito tempo que ela está na memória, esta página poderá ser retirada e causará novas faltas de páginas. Isso aumentará a taxa de falta de páginas e torna a execução do algoritmo mais lenta. O algoritmo OPT (Optimal Replacement) é o algoritmo que apresenta a menor taxa de falta de páginas. Este algoritmo substitui a página que não será utilizada pelo maior período de tempo. Este algoritmo requer conhecimento sobre o futuro das referências à memória. O algoritmo LRU (Least Recently Used) é uma tentativa de aproximação ao algoritmo ótimo. Ele utiliza o conhecimento da história passada recente das referências à memória, como uma aproximação do futuro. Este algoritmo associa a cada página seu último tempo de uso. Quando houver necessidade de substituir uma página, é escolhida aquela que não foi utilizada pelo maior período de tempo. Essa estratégia é conveniente ao princípio da localidade. Por esse princípio, quando uma página é referenciada, existe uma grande chance de que ela seja novamente referenciada em breve. O problema com este algoritmo encontra-se na forma de sua implementação. O sistema precisa manter uma lista das páginas da memória, ordenada por último uso. Há duas formas de implementação: • Contador: a cada entrada na tabela de páginas é associado um registrador de tempo de uso. Sempre que uma referência à página é feita, o valor do tempo é carregado no registrador. A página substituída deve ser aquela com o menor valor de tempo • Pilha: nessa abordagem é mantida uma estrutura de pilha dos números das páginas. Quando a página é referenciada, ela é removida da pilha e colocada no topo. Dessa forma, o fundo da pilha sempre contém a página usada menos recentemente
Gerência de memória
47
As implementações do LRU necessitam de algum auxílio do hardware. A atualização dos registradores ou da pilha deve ser feita a cada referência à memória. Se for utilizada uma interrupção a cada referência à memória, para permitir ao software atualizar as estruturas de dados, o sistema se degrada. Alguns sistemas oferecem uma ajuda de hardware sob a forma de um bit de referência. O bit de referência para uma página é ligado pelo hardware sempre que aquela página for referenciada (leitura ou gravação de qualquer byte da página). Os bits de referência são associados a cada entrada da tabela de páginas. O sistema operacional, inicialmente, zera todos os bits. Quando um processo do usuário é executado, o bit associado a cada página referenciada é ligado pelo hardware. Assim, é possível determinar que páginas foram utilizadas e quais não o foram, pela exame dos bits de referência. O algoritmo da segunda chance é um algoritmo de substituição FIFO. Quando uma página for selecionada seu bit de referência é inspecionado. Se o valor for 0, prossegue-se com a substituição da página. Se o bit for 1, dá-se àquela página uma segunda chance e passa-se a selecionar a próxima página FIFO. Quando uma página recebe uma segunda chance, seu bit de referência é desligado e sua hora de chegada é reposicionada para a hora corrente. Assim, a página à qual é dada uma segunda chance não será substituída até que todas as outras páginas tenham sido substituídas (ou tenham recebido segundas chances). Se uma página for utilizada com freqüência suficiente para manter seu bit de referência ligado, ela nunca será substituída. A implementação deste algoritmo é como uma fila circular. Um ponteiro indica qual é a página a ser substituída primeiro. Quando um quadro for necessário, o ponteiro avançará até encontrar uma página com um bit de referência igual a 0. Conforme o ponteiro avança, desliga os bits de referência. Uma vez que uma página vítima seja encontrada, ela é substituída e a nova página é inserida na fila circular, naquela posição.
4.4
THRASHING
Um processo está em thrashing, se ele passa mais tempo paginando do que executando. Este estado causa sérios problemas de desempenho no sistema. Para entender melhor como isso ocorre, considere que exista no sistema um número de páginas que estão em uso ativo. Como conseqüência, ocorre uma alta taxa de falta de páginas (page faults). Por exemplo, as páginas em uso ativo podem gerar referências às páginas que não estejam presentes na memória, logo elas precisam ser carregadas. Por sua vez, estas páginas recém carregadas geram novas referências e assim por diante. Esta alta atividade de paginação é denominada thrashing.
48
4.5
EDITORA - UFLA/FAEPE - Kernel do Linux
GERÊNCIA DE MEMÓRIA NO KERNEL DO LINUX
A gerência de memória no Linux possui dois componentes. O primeiro deles trata da alocação e liberação da memória física: páginas, grupos de páginas e pequenos blocos de memória. O segundo componente manipula a memória virtual, que é a memória mapeada no espaço de endereçamento dos processos em execução. 4.5.1
Gerência da memória física
O kernel trata página física como unidade básica da gerência de memória. Embora a menor unidade endereçável do processador seja uma palavra (ou mesmo um byte), a unidade de gerência de memória tipicamente trabalha com páginas. O gerenciador principal da memória física no kernel do Linux é o alocador de páginas, que é responsável por alocar e liberar todas as páginas físicas e é capaz de alocar intervalos de páginas fisicamente contíguas mediante solicitação. O alocador utiliza um algoritmo de agrupamento de parceiros (buddy-heap algorithm) para gerenciar páginas físicas disponíveis. Este alocador agrupa duas a duas as unidades adjacentes de memória alocável. Sempre que duas regiões parceiras alocadas forem liberadas, elas serão combinadas para formar uma região maior. Essa região mais ampla tem uma parceira com a qual pode se combinar para formar uma região livre ainda maior. Alternativamente, se uma solicitação por pouca memória não puder ser atendida através da alocação de uma pequena região livre existente, então uma região livre maior será subdividida em duas regiões parceiras para atender à solicitação. As regiões livres de memória de cada um dos tamanhos possíveis são registradas em listas encadeadas separadas. O menor tamanho alocável sob este mecanismo é uma única página física. Todas as alocações de memória no kernel do Linux ocorrem tanto de forma estática, por drivers que reservam uma área de memória contígua em tempo de inicialização do sistema, quanto de forma dinâmica, pelo alocador de páginas. Os subsistemas de gerência de memória mais importantes do kernel são: o alocador de tamanho variável kmalloc; e os dois caches de dados persistentes do kernel, o cachebuffer e o cache de páginas. O serviço kmalloc aloca páginas sob demanda, mas depois as subdivide em pedaços menores. Este serviço é útil para solicitações de tamanhos arbitrários, onde o tamanho de uma solicitação não é conhecido com antecedência e pode corresponder a somente poucos bytes e não a uma página inteira. Tanto as páginas quanto os alocadores kmalloc não sofrem interrupções. Uma função que queira alocar memória deve passar uma prioridade de solicitação para a função de alocação. As regiões de memória exigidas pelo sistema kmalloc ficam permanentemente alocadas até que sejam explicitamente liberadas.
Gerência de memória
49
O sistema kmalloc não pode realocar ou reclamar essas regiões em resposta à falta de memória. O cache-buffer é o principal cache do kernel para dispositivos orientados a blocos, como os drives de disco, e é o mecanismo principal através do qual é realizada I/O para estes dispositivos. Os sistemas de arquivos baseados em disco, nativos do Linux, e o sistema de arquivos em rede NFS utilizam cache de páginas. Este mecanismo aloca páginas inteiras de conteúdo de arquivos em caches e não é limitado aos dispositivos de blocos; ele também pode alocar em caches os dados da rede. O sistema de memória virtual administra o conteúdo do espaço de endereços virtuais de cada processo. O cache-buffer, o cache de páginas e o sistema de memória virtual interagem fortemente uns com os outros. A leitura de uma página de dados no cache de páginas exige a passagem temporária pelo cache-buffer. As páginas no cache de páginas também podem ser mapeadas no sistema de memória virtual se um processo tiver mapeado um arquivo no seu espaço de endereços. Um contador de referência é mantido pelo kernel em cada página da memória física, de forma que as páginas compartilhadas por dois ou mais desses subsistemas possam ser liberadas quando não estiverem mais em uso. 4.5.2
Memória virtual no Linux
O kernel do Linux gerencia sua própria memória e, também, o espaço de endereçamento do usuário, mantendo o espaço de endereços visível para cada processo. As páginas de memória virtual são criadas sob demanda. O kernel gerencia a carga dessas páginas a partir do disco ou o seu retorno ao disco, conforme necessário. O gerenciador de memória virtual mantém duas visões separadas do espaço de endereços de um processo: como um conjunto de regiões separadas e como um conjunto de páginas. A visão do espaço de endereços como um conjunto de regiões separadas é a visão lógica que descreve as instruções que o sistema de memória virtual recebeu em relação ao formato do espaço de endereços. O espaço de endereço é composto de um conjunto de regiões que não podem ser superpostas, cada uma destas representando um subconjunto do espaço de endereços contínuo e alinhado com as páginas. Cada região é descrita internamente por uma única estrutura vm_area_struct que define as propriedades da região, incluindo permissões de leitura, gravação e execução na região, bem como informação sobre quaisquer arquivos associados com a região. A visão do espaço de endereços como um conjunto de páginas armazena as páginas em tabelas. As entradas da tabela de páginas determinam a exata localização corrente de cada página da memória virtual, quer ela esteja em disco ou em memória física. A visão física é gerenciada por um conjunto de rotinas invocadas a partir dos manipuladores de interrupções de software do kernel sempre que um processo tentar acessar uma página que naquele momento não esteja presente na tabela de páginas.
50
EDITORA - UFLA/FAEPE - Kernel do Linux
Cada vm_area_struct na descrição do espaço de endereços contém um campo que aponta para uma tabela de funções que implementam as funções-chave de gerenciamento de páginas para cada região específica de memória virtual. Todas as solicitações para ler ou gravar uma página indisponível são finalmente despachadas para o manipulador apropriado na tabela de funções da vm_area_struct, de modo que as rotinas centrais de gerenciamento de memória não tenham que conhecer os detalhes de gerenciamento de cada tipo possível de região de memória. O Linux implementa diferentes tipos de região de memória virtual. A memória de retaguarda descreve de onde provêm as páginas para uma região. A maioria das regiões de memória é reservada tanto por um arquivo como por nada. Uma região reservada por nada é o tipo mais simples de memória virtual e representa a memória de demanda zero. Quando um processo tenta ler uma página em tal região, é retornada uma página de memória preenchida com zeros. Uma região reservada por arquivos atua como uma porta de visualização para uma seção do arquivo. Quando um processo tentar acessar uma página dentro da região, a tabela de páginas será preenchida com o endereço de uma página dentro do cache de páginas do kernel, correspondente ao deslocamento apropriado do arquivo. A mesma página de memória física é utilizada tanto pelo cache de páginas quanto pelas tabelas de páginas do processo, de modo que quaisquer mudanças feitas no arquivo pelo sistema de arquivos sejam imediatamente visíveis por quaisquer processos que tenham mapeado o arquivo no seu espaço de endereços. Uma região de memória virtual é também definida por sua reação às gravações. O mapeamento de uma região no espaço de endereços do processo pode ser feito de forma privada ou compartilhada. Se um processo gravar de forma privada em uma região mapeada, então o paginador detectará que é necessária uma operação de copy-on-write para manter as mudanças locais ao processo. Por outro lado, as gravações em uma região compartilhada resultam na atualização do objeto que foi mapeado nesta região, de modo que a mudança seja visível por qualquer outro processo que esteja mapeando aquele objeto. A criação de um novo espaço de endereços virtual pelo kernel ocorre quando um processo realiza uma chamada de sistema fork() para criar um novo processo e quando um processo executa um novo programa com a chamada de sistema exec(). A criação de um novo processo com fork() envolve a criação de uma cópia completa do espaço de endereços virtual do processo pai. O kernel copia os descritores vm_area_struct do processo pai, criando depois um novo conjunto de tabelas de páginas para o filho. As tabelas de páginas do pai são copiadas diretamente nas tabelas de páginas do filho, incrementandose o contador de referência de cada página. Após o fork(), pai e filho compartilham as mesmas páginas físicas de memória nos seus espaços de endereços.
Gerência de memória
51
Quando um novo programa é executado com a chamada exec(), o processo recebe um novo espaço de endereços virtuais completamente vazio. É tarefa das rotinas carregarem o programa para popular o espaço de endereços com regiões de memória virtual. Um sistema de memória virtual possui a tarefa importante de definir como é a expulsão de páginas da memória física para o disco, quando a memória for necessária. O Linux utiliza o mecanismo de paginação mais recente. Na paginação utilizada pelo Linux, o algoritmo decide que páginas gravar no disco e quando gravá-las. Em seguida, o mecanismo de paginação realiza a transferência e pagina os dados de volta na memória física quando forem novamente necessários. A política de expulsão de páginas do Linux utiliza um algoritmo similar ao algoritmo da segunda chance (descrito anteriormente). Porém, o Linux utiliza um relógio de passos múltiplos e toda página possui uma idade que é ajustada a cada passo do relógio. A idade é uma medida da juventude da página ou de quanta atividade a página realizou recentemente. A cada passo, páginas acessadas com freqüência irão atingir valores maiores de idade, enquanto a idade das páginas pouco acessadas irá tendendo a zero. Esta quantificação da idade permite ao paginador selecionar páginas a serem expulsas da memória baseando-se na política da menos freqüentemente utilizada (LFU - Least Frequently Used). O mecanismo de paginação suporta a paginação tanto para dispositivos de swap dedicados quanto para partições e para arquivos normais. A memória física mantém um mapa de bits (bitmap) relativos aos blocos utilizados. Os dispositivos de swap alocam os blocos de acordo com este mapa de bits. O alocador utiliza um algoritmo do tipo próximo-apto para tentar gravar páginas em contínuas execuções de blocos de disco, com o objetivo de melhorar o desempenho. O alocador registra o fato de que uma página foi expulsa para o disco ligando o bit página-não-presente da entrada da tabela de páginas, permitindo que o restante da entrada seja preenchido com um índice identificador do local onde a página foi gravada. O kernel reserva para seu uso interno uma região constante do espaço de endereços virtual de todo processo. As entradas na tabela de páginas que mapeiam para essas páginas do kernel são marcadas como protegidas, de modo que as páginas não sejam visíveis ou modificáveis quando o processador estiver operando em modo usuário. Esta área de memória virtual do kernel possui duas regiões. Uma área estática que contém referências da tabela de páginas para toda página física da memória disponível no sistema, de modo que ocorra uma simples tradução de endereços físicos para virtuais quando o código do kernel entrar em execução. O núcleo do kernel e todas as páginas alocadas pelo alocador normal residem nesta região. O restante da seção reservada do espaço de endereços do kernel não é destinado a qualquer propósito específico. As entradas da tabela de páginas neste intervalo de endereços podem ser modificadas pelo kernel para apontar para quaisquer outras áreas de
52
EDITORA - UFLA/FAEPE - Kernel do Linux
memória conforme necessário. O kernel fornece um par de recursos que permitem aos processos utilizarem esta memória virtual. A função vmalloc aloca um número arbitrário de páginas físicas de memória e as mapeia em uma única região da memória virtual do kernel, permitindo a alocação de grandes porções de memória contígua mesmo se não existirem suficientes páginas físicas livres adjacentes para atender à solicitação. A função vremap mapeia uma seqüência de endereços virtuais que apontam para uma área de memória utilizada por um driver de dispositivo para I/O mapeado em memória. 4.5.3
Mapeamento de programas na memória
No Linux, a execução de programas de usuário pelo kernel é disparada pela chamada de sistema exec(). Esta chamada ordena ao kernel que execute um novo programa dentro do processo corrente, superpondo o contexto de execução corrente com o contexto inicial do novo programa. O primeiro job deste serviço do sistema é verificar se o processo que invocou o novo programa possui direitos de permissão para o arquivo que está sendo executado. Em seguida, o kernel invoca uma rotina de carga para começar a execução do programa. O carregador binário não carrega um arquivo binário na memória física. As páginas do arquivo binário são mapeadas em regiões de memória virtual. Quando o programa tentar acessar uma determinada pagina é que irá ocorrer um erro de página com o objetivo de carregar a página desejada na memória física. A Figura 4.2 apresenta o formato típico das regiões de memória estabelecidas pelo carregador ELF. O kernel permanece um uma região reservada numa das extremidades do espaço de endereços. Os programas em modalidade de usuário não podem acessar a memória virtual do kernel. O restante da memória virtual é disponibilizada às aplicações que podem utilizar as funções de mapeamento em memória do kernel para criar regiões que mapeiam uma parte do arquivo ou que ficam disponíveis para os dados das aplicações. O carregador é responsável por estabelecer o mapeamento inicial em memória para permitir o início da execução do programa. As regiões que precisam ser inicializadas são a pilha e o texto do programa e suas regiões de dados. A pilha é criada no topo da memória virtual de modalidade de usuário e cresce de forma descendente, em direção aos endereços de numeração mais baixa. A pilha inclui cópias das variáveis de argumentos e do ambiente, fornecidas ao programa na chamada de sistema exec. As outras regiões são criadas perto da extremidade que compõe a base da memória virtual. As seções do arquivo binário que contêm texto de programa ou dados de somente leitura são mapeadas em seguida. Dados não inicializados são mapeados como uma região privada de demanda zero. Logo após estas regiões de tamanho fixo, fica uma região de tamanho variável que os programas podem expandir conforme necessário para manter dados alocados em tempo de
Gerência de memória
53
Figura 4.2: Formato da memória para programas ELF [Silberschatz (2004)]
execução. Cada processo possui um ponteiro, o brk, que aponta para a extensão corrente desta região de dados e os processos podem estender ou contrair sua região brk com uma única chamada de sistema. Após realizados todos os mapeamentos, o carregador inicializa o registrador contador de programas do processo com o ponto de início registrado no cabeçalho do ELF e o processo pode ser submetido ao escalonador. Quando o programa inicia sua execução, todos os conteúdos do arquivo binário já estão no espaço de endereços virtuais do processo.
4.6
RESUMO
Este capítulo apresentou um visão geral sobre a gerência de memória nos sistemas operacionais de uma forma geral e como o kernel do Linux implementa esta gerência.
54
EDITORA - UFLA/FAEPE - Kernel do Linux
5 SISTEMA DE ARQUIVOS
O sistema de arquivos fornece o mecanismo para armazenamento e acesso a dados de programas do sistema operacional e de todos os usuários do sistema de computação. É constituído de uma coleção de arquivos e uma estrutura de diretórios, que organiza e fornece informações sobre todos os arquivos do sistema. Este capítulo aborda as características de um sistema de arquivos e como é implementado no Linux. As principais referências utilizadas foram [Silberschatz (2004), Love (2005)].
5.1
INTRODUÇÃO
5.2
CONCEITO DE ARQUIVO
Um arquivo (file) pode ser definido como uma unidade lógica de armazenamento de informação, destinada a abstrair as propriedades físicas dos meios de armazenamento. Ou ainda, é uma coleção nomeada de informação relacionada, registrada em memória secundária. Para conveniência dos usuários humanos, um arquivo é referenciado por seu nome, que é composto por uma cadeia de caracteres. Um arquivo tem propriedades tais como nome, identificador, tipo, tempo de criação, tamanho, nome do proprietário, proteção, hora, data, entre outras. Essas informações ficam armazenadas em um diretório, que é uma tabela de símbolos que permite identificar tais informações. Um arquivo tem uma estrutura definida de acordo com a sua utilização. Por exemplo, um arquivo texto é uma seqüência de caracteres organizada em linhas e possivelmente em páginas. Um arquivo fonte é uma seqüência de subrotinas e funções, cada uma das quais organizada como declarações seguidas de comandos executáveis. Um arquivo executável é uma sequência de seções de código que o carregador do sistema pode conduzir à memória e executar. Um arquivo é um tipo abstrato de dados e existem certas operações que podem ser realizadas sobre ele através de chamadas ao sistema operacional, que são descritas a seguir:
56
EDITORA - UFLA/FAEPE - Kernel do Linux
Criação: é necessário encontrar um espaço para ele no dispositivo de armazenamento e colocar a entrada do arquivo no diretório informando seu nome e sua localização no dispositivo Escrita: é feita através de uma chamada ao sistema especificando o nome do arquivo e a informação a ser escrita Leitura: é realizada por uma chamada ao sistema especificando o nome do arquivo e a localização onde o bloco lido será colocado Reposicionamento dentro do arquivo: o diretório é percorrido em busca da entrada apropriada e a posição corrente do arquivo é posicionada para um determinado valor Apagando um arquivo: o diretório é pesquisado e quando a entrada associada ao arquivo é encontrada, é liberado todo o espaço destinado ao arquivo e invalidada sua entrada no diretório Truncando um arquivo: o usuário pode desejar apagar o conteúdo de um arquivo, mas conservar seus atributos. Esta função permite que todos os atributos permaneçam inalterados, exceto o tamanho, reposicionando o arquivo com tamanho zero e liberando seu espaço
5.3
MÉTODOS DE ACESSO A ARQUIVOS
Os acessos aos arquivos tanto podem ser seqüenciais quanto diretos. O acesso seqüencial é o modo de acesso de arquivos, onde a informação é buscada em ordem, uma posição após a outra. Após um registro avança-se o ponteiro para o próximo registro no arquivo. O grande volume de operações em um arquivo são leituras e escritas. Além disso, o arquivo pode ser reposicionado, e em alguns sistemas, um programa pode ser capaz de deslocar n registros para frente ou para trás, por algum valor de n inteiro como mostra a Figura 5.1.
Figura 5.1: Arquivo de acesso seqüencial [Silberschatz (2004)]
No acesso direto o arquivo é visto como uma seqüência numerada de blocos. Um bloco é geralmente uma quantidade de informação de tamanho fixo, definida pelo sistema
Sistema de arquivos
57
operacional. O acesso direto não tem restrições na ordem de acesso a cada bloco. Assim, qualquer bloco pode ser lido ou escrito aleatoriamente. O método de acesso direto é bastante utilizado para acesso imediato a grandes informações. Outros métodos de acesso podem ser construídos a partir do método de acesso direto. Estes métodos geralmente envolvem a construção de um índice para o arquivo. O índice contém ponteiros para os vários blocos. Para encontrar um registro no arquivo, primeiro é necessário pesquisar o índice e então utilizar o ponteiro para fazer acesso ao arquivo diretamente e encontrar o registro desejado.
5.4
ESTRUTURA DE DIRETÓRIOS
A estrutura de diretório é um meio de organizar os muitos arquivos presentes no sistema. No diretório são armazenados dois tipos de informação. A primeira informação está relacionada com o dispositivo físico (a localização do arquivo, seu tamanho e modo de alocação). A segunda está relacionada à organização lógica dos arquivos (nome, tipo, proprietário, códigos de proteção). As operações que podem ser desenvolvidas sobre o diretório são: • Busca de arquivo: operação para encontrar a entrada para um arquivo em particular no sistema de diretórios • Criar um arquivo: novos arquivos precisam ser criados e adicionados ao diretório • Apagar um arquivo: quando um arquivo não é mais necessário, é preciso removêlo do diretório • Listar um diretório: listar os arquivos de um diretório e o conteúdo da entrada do diretório para cada arquivo da lista • Renomear um arquivo: como o nome de um arquivo representa o seu conteúdo para os seus usuários, ele deve ser alterável quando mudar o conteúdo ou utilização do arquivo. A renomeação de um arquivo pode também permitir a modificação de sua posição dentro da estrutura do diretório • Percorrer o sistema de arquivos: acessar cada diretório e cada arquivo dentro de uma estrutura de diretórios Muitas estruturas de diretório diferentes têm sido utilizadas. O diretório é essencialmente uma tabela de símbolos. O sistema operacional utiliza o nome do arquivo simbólico para achar o arquivo. Ao considerar uma estrutura de diretório em particular é importante lembrar as operações que podem ser realizadas no diretório.
58
5.4.1
EDITORA - UFLA/FAEPE - Kernel do Linux
Diretório de um nível
Na estrutura de diretório de um nível todos os arquivos estão contidos no mesmo diretório. É fácil de dar suporte e entender. Este tipo de diretório tem a limitação de que todos os arquivos devem ter nomes distintos. A Figura 5.2 ilustra esta estrutura. A desvantagem desta estrutura é a possibilidade de confusão entre os nomes de arquivos de usuários diferentes. Todos os usuários compartilham o diretório.
Figura 5.2: Estrutura de diretório de um nível.
5.4.2
Diretório em dois níveis
Na estrutura de diretório de dois níveis, cada usuário tem seu próprio diretório de arquivo de usuário (user file directory - UFD). Cada diretório de usuário tem uma estrutura similar. Quando um usuário entra no sistema, o diretório de arquivo mestre (master file directory - MFD) do sistema é pesquisado. O MFD é indexado pelo nome do usuário ou um número de contabilidade. Cada entrada aponta para o diretório de um usuário. Quando um usuário se refere a um arquivo em particular, somente um diretório é pesquisado. Esta estrutura é ilustrada na Figura 5.3.
Figura 5.3: Estrutura de diretório em dois níveis.
Esta estrutura isola um usuário do outro. A desvantagem em dois níveis existe quando um usuário quer utilizar arquivos de outros usuários. Alguns sistemas não permitem este acesso. Se o acesso é permitido, ele é feito através do nome do usuário e do nome do arquivo, que definem o nome do caminho (path name). Todo arquivo no sistema tem um único path name.
Sistema de arquivos
5.4.3
59
Diretório estruturado em árvore
Nos diretórios estruturados em árvore existe um diretório raiz da árvore. Os nós intermediários da árvore são os diretórios dos usuários, que podem ainda criar seus próprios subdiretórios e organizar seus arquivos. A Figura 5.4 ilustra esta estrutura.
Figura 5.4: Estrutura de diretório em árvore.
Todo arquivo no sistema tem um único nome considerando seu path name, que é o caminho da raiz através de todos os subdiretórios ao arquivo especificado. A seqüência de diretórios pesquisada quando um arquivo é buscado é chamada de caminho de busca (search path). O path name pode ser apresentado de duas maneiras diferentes: • Completo ou absoluto: define um caminho da raiz ao arquivo • Relativo: define um caminho do diretório corrente ao arquivo Os arquivos podem ser facilmente compartilhados. Por exemplo, um usuário pode criar um subdiretório contendo os arquivos que serão compartilhados com outros usuários. Um destes usuários pode fazer o acesso aos arquivos compartilhados especificando o path name dos arquivos. A maneira de se apagar um diretório estruturado em árvore é uma política de decisão interessante. Se um diretório está vazio ele pode ser apagado. Se ele não estiver vazio podemos decidir entre duas abordagens. A primeira, é só apagar o diretório se estiver vazio. Isso implica em apagar todos os arquivos e subdiretórios contidos nele primeiro. A segunda abordagem, é assumir que quando é pedido para se apagar um diretório, deva-se apagar também todos os seus arquivos e subdiretórios. A escolha da forma de implementação é uma decisão de projeto e ambas são utilizadas nos sistemas. 5.4.4
Diretório de grafos cíclicos
Um diretório de grafos cíclicos permite que subdiretórios e arquivos sejam compartilhados. A Figura 5.5 ilustra esta estrutura de diretório.
60
EDITORA - UFLA/FAEPE - Kernel do Linux
Figura 5.5: Estrutura de diretório de grafos cíclicos
O compartilhamento de arquivos ou diretórios pode ser implementado de diversas formas. A maneira mais comum é criar uma entrada de diretório nova chamada hard link, que é um ponteiro para outro subdiretório ou arquivo. Uma outra implementação, é duplicar as informações em ambos os diretórios. O problema nesta abordagem é manter consistência nas informações se o arquivo for modificado. Uma estrutura de diretório de grafo acíclico é mais flexível que uma estrutura em árvore simples, mas também é mais complexa. Os problemas como buscar um determinado arquivo ou apagá-lo devem ser cuidadosamente considerados. Um arquivo pode ter vários nomes completos. Dessa forma, nomes com caminhos diferentes podem ser referenciar ao mesmo arquivo. Como cada arquivo tem mais de uma trajetória de busca, a eliminação de um arquivo pode ser feita de várias maneiras. Quando são utilizados hard links, apagar um arquivo implica na retirada do hard link, não afetando o arquivo. Esse só é removido quando forem retirados todos os hard links. 5.4.5
Diretório como estrutura de grafo geral
Um diretório como estrutura de grafo geral permite que subdiretórios formem ciclos, resultando numa estrutura como a ilustrada na Figura 5.6.
5.5
SISTEMAS DE ARQUIVOS NO LINUX
O Linux utiliza o modelo de sistema de arquivos padrão do Unix. Os arquivos Unix podem ser qualquer objeto capaz de manipular a entrada ou saída de uma cadeia de dados. O kernel do Linux manipula todos esses tipos de arquivos ocultando seus detalhes de implementação sob uma camada de software, o sistema de arquivos virtual (virtual file system - VFS).
Sistema de arquivos
61
Figura 5.6: Estrutura de diretório de grafo geral.
O VFS possui um conjunto de definições que estabelecem o que pode ser um objetoarquivo e uma camada de software para manipular esses objetos. Os tipos principais de objetos são: objeto-inode, estruturas do objeto-arquivo e o objeto-sistema-de-arquivos. O objeto-sistema-de-arquivos representa um conjunto conectado de arquivos que forma uma hierarquia de diretórios auto-suficientes. A principal responsabilidade do objeto sistemade-arquivos é dar acesso aos inodes. O VFS identifica todo inode por um único par (número do inode - sistema de arquivos) e encontra o inode correspondente a um número particular de inode, solicitando ao objeto sistema-de-arquivos que retorne o inode com aquele número. Um objeto-inode representa o arquivo como um todo e o objeto-arquivo representa um ponto de acesso aos dados do arquivo. Um processo não pode acessar o conteúdo de dados de um inode sem primeiro obter um objeto-arquivo que aponte para o inode. Para administrar I/O de arquivos seqüenciais, o objeto-arquivo rastreia o local do arquivo de onde o processo está lendo ou onde está gravando, a cada momento. É também registrado se o processo solicitou permissões de gravação quando o arquivo foi aberto e rastreada a atividade do processo se for necessário realizar leituras adiante adaptativas, extraindo os dados do arquivo para a memória antes que o processo os solicite, com objetivo de melhorar o desempenho. Os objetos-arquivo pertencem a um único processo. Mesmo que um arquivo não esteja mais sendo usado por nenhum processo, seu objeto-inode pode ser alocado a um cache pelo VFS, para melhorar o desempenho se o arquivo for novamente utilizado em
62
EDITORA - UFLA/FAEPE - Kernel do Linux
futuro próximo. Todos os dados do arquivo alocados a cache são encadeados em uma lista do objeto-inode do arquivo. O inode também mantém informação padrão sobre cada arquivo, como seu proprietário, tamanho e hora da última modificação.
5.6
O SISTEMA DE ARQUIVOS EXT2 DO LINUX
O Linux foi originalmente programado com um sistema de arquivos compatível com o Minix, para facilitar a troca de dados. Como o sistema de arquivos do Minix possuía algumas restrições em nomes de arquivos (máximo 14 caracteres) e tamanho máximo do sistema de arquivos (até 64 MB), foi desenvolvido um sistema de arquivos estendido (extfs - extended file system). Para melhorar o desempenho e escalabilidade, bem como adicionar algumas características que faltavam surgiu o segundo sistema de arquivos estendido (ext2fs - second extended file system). O ext2fs1 utiliza um mecanismo semelhante para localização dos blocos de dados pertencentes a um arquivo específico, armazenando ponteiros de blocos de dados em blocos indiretos com até três níveis, através do sistema de arquivos. Os arquivos de diretório são armazenados no disco como arquivos normais, mas seus conteúdos são interpretados de forma diferente. Cada bloco de um arquivo de diretório consiste em uma lista encadeada de entradas, onde cada entrada contém o tamanho da entrada, o nome de um arquivo e o numero do inode ao qual a entrada se refere. O sistema de arquivos do ext2fs é particionado em múltiplos grupos de blocos. Ao alocar um arquivo, o ext2fs deve primeiro selecionar o grupo de blocos para aquele arquivo. Para blocos de dados, ele tenta escolher o mesmo grupo de blocos no qual o inode do arquivo foi alocado. Para alocações de inodes, ele seleciona o mesmo grupo de blocos como diretório pai do arquivo, para arquivos não pertencentes a diretórios. Arquivos em diretórios não são mantidos juntos, sendo dispersos através dos grupos de blocos disponíveis. Essas políticas são projetadas para guardar informação relacionada dentro do mesmo grupo de blocos, mas também para distribuir a carga do disco entre os seus grupos de blocos, no sentido de reduzir a fragmentação das áreas do disco. O ext2fs busca tentar reduzir a fragmentação dentro de um grupo de blocos através da manutenção de alocações fisicamente contíguas. Ele mantém um bitmap de todos os blocos livres em um grupo de blocos. Ao alocar os primeiros blocos para um novo arquivo, ele começa buscando um bloco livre a partir do início do grupo de blocos; ao estender um arquivo, ele continua a busca a partir do último bloco alocado ao arquivo. A busca inicial é de um byte completamente livre no bitmap; se não o encontrar, é procurado qualquer bit livre. A busca por bytes livres objetiva alocar onde for possível o espaço do disco em pedaços de pelo menos oito blocos. Quando um bloco livre é identificado, a busca retroage até encontrar 1 http://en.wikipedia.org/wiki/Ext2
Sistema de arquivos
63
um bloco alocado. Quando um byte livre é encontrado no bitmap, esta busca retroativa evita que o ext2fs deixe um buraco entre o último bloco alocado no byte diferente de zero anterior e o byte igual a zero encontrado. Uma vez que o próximo bloco a ser alocado tenha sido encontrado, seja pela busca do bit, seja pela do byte o ext2fs avança a alocação até oito blocos e pré-aloca esses blocos extras ao arquivo. Esta pré-alocação ajuda a reduzir a fragmentação durante as gravações intercaladas para arquivos separados e reduz o custo da CPU devido à alocação de discos, já que múltiplos blocos são simultaneamente alocados. Os blocos pré-alocados são retornados ao bitmap de espaço livre quando o arquivo é fechado. A Figura 5.7 ilustra as políticas de alocação de blocos do ext2fs. Cada linha representa uma seqüência de bits ligados e desligados em um bitmap de alocação, indicando blocos utilizados e blocos livres no disco. Na alocação de blocos livres espalhados, se forem encontrados blocos livres suficientemente próximos ao início da busca, estes deverão ser alocados sem levar em conta o nível de fragmentação em que possam estar. A fragmentação é parcialmente compensada pelo fato de que os blocos estarão juntos e poderão provavelmente ser lidos sem a necessidade de quaisquer buscas no disco. Além disso, a alocação de todos eles a um arquivo é melhor a longo prazo do que a alocação de blocos isolados para arquivos separados, uma vez que grandes áreas livres se tornam escassas no disco. No segundo caso, não é encontrado de imediato um bloco livre próximo, e assim é necessário avançar na busca por um byte completamente livre no bitmap. Se esse byte é alocado como um todo, acaba-se criando, antes dele, uma área fragmentada de espaços livres. Assim, antes de realizar a alocação, é melhor reservá-la de modo que ela flua com a alocação precedente e realizar então uma alocação para diante de como a satisfazer a alocação default de oito blocos.
Figura 5.7: Políticas de alocação de blocos do ext2fs [Love (2005)]
64
EDITORA - UFLA/FAEPE - Kernel do Linux
O ext32 (third extended filesystem) é uma extensão do ext2, do qual difere por apresentar melhorias e ser um sistema de arquivos journalled [?]. O ext43 (fourth extended filesystem) ainda está em desenvolvimento e é um sistema de arquivos de 64 bits. Ele está sendo integrado ao kernel 2.6.19 do Linux [Vivier (2007)].
5.7
O SISTEMA DE ARQUIVOS PROC DO LINUX
O sistema de arquivos de processos do Linux, denominado sistema de arquivos proc, é um exemplo de um sistema de arquivos cujos conteúdos não estão realmente armazenados em qualquer lugar, mas são processados sob demanda de acordo com as solicitações de I/O dos arquivos do usuário. O sistema de arquivos proc foi introduzido pelo Unix SVR4, como uma interface eficiente para suporte à depuração de processos do kernel. Cada subdiretório do sistema de arquivos não correspondia a um diretório em qualquer disco, mas sim a um processo ativo no sistema corrente. Uma listagem do sistema de arquivos revela um diretório por processo, com o nome do diretório sendo a representação decimal ASCII do identificador único do processo (PID - process ID). O Linux adicionou diretórios extras e arquivos texto sob o diretório raiz deste sistema de arquivos. Esses novos arquivos e diretórios correspondem a estatísticas do kernel e drivers associados carregados. O sistema de arquivos proc fornece um meio para que os programas acessem esta informação como arquivos de texto plenos, que podem ser processador por intermédio de ferramentas poderosas oferecidas pelo ambiente de usuário padrão do Unix. O sistema de arquivos proc deve implementar uma estrutura de diretório e o conteúdo dos arquivos dentro dela. Um sistema de arquivos Unix é definido como um conjunto de arquivos e inodes de diretórios identificados por seus números de inode, o sistema de arquivos proc deve definir um número de inode persistente e único para cada diretório e arquivos associados. Uma vez que exista tal mapeamento, ele pode usar este número de inode para identificar que operação será necessária quando um usuário tentar ler a partir de um inode de arquivo em particular, ou realizar uma pesquisa em um determinado inode de diretório. Quando os dados forem lidos a partir de um desses arquivos, o sistema de arquivos proc irá coletar a informação apropriada, formatá-la em forma textual e colocá-la no buffer de leitura do processo solicitante. O mapeamento do número do inode para o tipo de informação subdivide o número do inode em dois campos. No Linux, um PID tem 16 bits, enquanto um número de inode tem 32 bits. Os 16 bits mais altos do número do inode são representados como um PID, 2 http://en.wikipedia.org/wiki/Ext3 3 http://en.wikipedia.org/wiki/Ext4
Sistema de arquivos
65
e os bits restantes definem o tipo de informação que está sendo solicitada sobre aquele processo. Um PID igual a zero não é válido, de modo que um campo de PID zero no número do inode significa que este inode contém informação global. Existem arquivos globais separados no proc para relatar informação como a versão do kernel, a memória livre, estatísticas de desempenho e drivers em execução corrente. O kernel pode alocar novos mapeamentos de inodes do proc dinamicamente, mantendo um bitmap de números de inodes alocados. Ele também mantém uma estrutura de dados em árvore de entradas globais registradas do sistema de arquivos proc. Cada entrada contém o número do inode do arquivo, o nome do arquivo, as permissões de acesso e funções especiais utilizadas para gerar o conteúdo dos arquivos. Os drivers podem incluir e excluir entradas nesta árvore a qualquer momento, e uma seção especial da árvore que aparece sob o diretório /proc/sys. Os arquivos sob esta árvore são trabalhados por um conjunto de manipuladores comuns que permitem tanto a leitura como a gravação dessas variáveis, de modo que o administrador do sistema possa ajustar o valor dos parâmetros do kernel simplesmente gravando os novos valores desejados em decimal ASCII para o arquivo apropriado.
5.8
RESUMO
Este capítulo apresentou uma visão geral sobre gerenciamento de arquivos e como é realizado no Linux.
66
EDITORA - UFLA/FAEPE - Kernel do Linux
6 GERÊNCIA DE ENTRADA E SAÍDA
O papel de um sistema operacional em relação a I/O (entrada e saída) do computador é gerenciar e controlar as operações e os dispositivos de I/O. Este capítulo apresenta uma visão geral sobre os fundamentos do hardware de I/O, os serviços de I/O fornecidos pelo sistema operacional e como o Linux implementa estes serviços. As principais referências utilizadas foram [Silberschatz (2004), Love (2005)].
6.1
INTRODUÇÃO
O kernel oferece diversos serviços relacionados a I/O, tais como escalonamento, alocação em buffers, alocação em caches, spooling, reserva de dispositivos e manipulação de erros, que são descritos a seguir. 6.1.1
Escalonamento
Os desenvolvedores de sistemas operacionais implementam o escalonamento mantendo uma fila de solicitações para cada dispositivo. Quando uma aplicação emite uma chamada de sistema para I/O bloqueada, a solicitação é colocada na fila do dispositivo de interesse. O escalonador reordena a fila para melhorar o tempo médio de resposta para as aplicações e a eficiência global do sistema. Existem vários algoritmos para realizar o escalonamento de disco: FCFS, SSTF, SCAN, C-SCAN, LOOK. O algoritmo FCFS (First-Come, First-Served) é um algoritmo de atendimento na ordem de chegada das solicitações. É um algoritmo justo, mas geralmente não oferece o serviço mais rápido. O algoritmo SSTF (Shortest-Seek-Time-First) ou algoritmo de prioridade para o menor tempo de busca atende a todas as solicitações perto da posição corrente do cabeçote antes de movê-lo para muito longe com o intuito de atender a outras solicitações. No algoritmo SCAN o braço do disco começa em uma extremidade do disco e se move em direção à outra extremidade, atendendo às solicitações à medida que passa pelos cilindros até atingir a outra extremidade do disco. Chegando aí, o sentido do movimento do
68
EDITORA - UFLA/FAEPE - Kernel do Linux
cabeçote é invertido e o atendimento continua. O cabeçote varre o disco continuamente de uma extremidade a outra. O algoritmo C-SCAN (SCAN circular) é uma variação do SCAN e foi projetado para fornecer um tempo de espera mais uniforme. Este algoritmo trata os cilindros como uma lista circular que, ao chegar ao cilindro final, volta ao primeiro. O C-SCAN move o cabeçote de uma extremidade a outra do disco atendendo solicitações, mas ao retornar volta ao início do disco sem atender solicitações pelo caminho. Na prática, a implementação do SCAN e do C-SCAN não ocorre como descrito. O mais comum é que o braço só vá até a última solicitação em cada sentido. Depois ele muda imediatamente de sentido, sem ir até o final do disco. Essas versões do SCAN e do C-SCAN são denominadas de escalonamento LOOK e C-LOOK, pois eles procuram por uma solicitação antes de continuar a se mover em um dado sentido. 6.1.2
Alocação em buffers
Um buffer é uma área de memória que armazena dados enquanto eles são transferidos entre dois dispositivos ou entre um dispositivo e uma aplicação. A utilização de buffers ocorre para fornecer uma interligação veloz entre o produtor e o consumidor de uma cadeia de dados; adaptação entre dispositivos de tamanhos diferentes para transferir dados e suportar semântica de cópias para o I/O da aplicação. Como exemplo de utilização para semântica de cópias, pode-se considerar que uma aplicação possua um buffer de dados a serem gravados em disco. Para isso, a aplicação invocará uma chamada de sistema write() fornecendo um ponteiro para o buffer e um inteiro correspondente ao número de bytes a serem gravados. A semântica de cópias garante que a versão dos dados gravada em disco é a versão vigente no momento da chamada de sistema da aplicação, independente de quaisquer mudanças subseqüentes realizadas no buffer da aplicação. 6.1.3
Alocação em caches
Cache é uma região de memória rápida que mantém cópias de dados. A diferença entre cache e buffer é que o buffer pode manter uma única cópia existente de um item de dado, enquanto um cache apenas mantém uma cópia em memória mais rápida de um item que resida em qualquer local. Os buffers são utilizados como cache para melhorar a eficiência de I/O no caso de arquivos compartilhados entre aplicações ou que estão sendo gravados e relidos rapidamente. Quando o kernel recebe uma solicitação de I/O para um arquivo, acessa em primeiro lugar o cache-buffer para ver se aquela região do arquivo já está disponível na memória principal. Em caso afirmativo, será evitado um I/O de disco físico.
Gerência de Entrada e Saída
6.1.4
69
Spooling e reserva de dispositivos
Spool é um buffer que mantém saídas para um dispositivo, tal como impressora, que não pode aceitar fluxos de dados intercalados. A saída para impressora, de cada aplicação, é interceptada pelo sistema operacional que a armazena em um spool para um arquivo de disco separado. Quando uma aplicação termina de imprimir, o sistema de criação do spool enfileira o arquivo spool correspondente para saída na impressora. O sistema de criação do spool copia arquivos spool enfileirados, um de cada vez. Em alguns sistemas operacionais, a alocação em spool é gerenciado por um processo de sistema daemon e em outros é manipulado por thread no kernel. Em ambos os casos, o sistema oferece uma interface de controle que habilita os usuários e administradores do sistema a exibir a fila, para que os jobs indesejados sejam removidos antes de serem impressos. 6.1.5
Manipulação de erros
Dispositivos e transferências de I/O podem falhar de diversas maneiras, por exemplo, por uma sobrecarga na rede (razão transitória) ou por um controlador de disco defeituoso (razão permanente). O sistema operacional Unix possui uma variável inteira adicional denominada errno que é utilizada para retornar um código de erro quando uma chamada de sistema para I/O falha. O código de erro indica a natureza geral da falha. Por exemplo, argumento fora do intervalo, ponteiro corrompido ou arquivo não aberto.
6.2
ENTRADA E SAÍDA NO LINUX
O kernel do Linux utiliza várias estruturas similares para rastrear as conexões de rede, comunicações de dispositivos de caracteres e outras atividades de I/O. Assim, todos os dispositivos são subdivididos em dispositivos de blocos, dispositivos de caracteres e dispositivos de rede. Os dispositivos de blocos incluem todos os dispositivos que permitem acesso aleatório a blocos de dados de tamanho fixo completamente independentes, inclusive discos rígidos, discos flexíveis e CD-ROMs. São utilizados para armazenar sistemas de arquivos. O acesso direto a um dispositivo de blocos é possível, assim programas podem criar e reparar o sistema de arquivos contido no dispositivo. Os dispositivos de caracteres incluem a maioria dos demais dispositivos, com exceção dos dispositivos de rede. Os dispositivos de rede são utilizados de maneira diferente dos demais. Os usuários não podem transferir dados diretamente para os dispositivos de rede. Eles podem se comunicar abrindo uma conexão para o subsistema de conexão de rede do kernel. A Figura 6.1 apresenta a estrutura de blocos do driver de dispositivo.
70
EDITORA - UFLA/FAEPE - Kernel do Linux
Figura 6.1: Estrutura de blocos do driver de dispositivo
6.2.1
Dispositivos de blocos
Os dispositivos de blocos representam a interface principal para todos os dispositivos de disco de um sistema. Por isso, possui dois componentes para fornecer funcionalidade que garanta o acesso mais rápido possível ao disco: o cache-buffer de blocos e o gerenciador de solicitações. O cache-buffer de blocos atua como uma cadeia de buffers para I/O ativo e como um cache para I/O que se completou. Os buffers são formados por um conjunto de páginas dinamicamente dimensionadas, alocadas a partir da cadeia de memória principal do kernel. Cada página é subdividida em buffers de mesmo tamanho. Um conjunto de descritores de buffers (cabeças de buffers) inclui um descritor para cada buffer do cache. As cabeças de buffers contêm toda a informação que o kernel mantém sobre os buffers, como, por exemplo, a identidade do buffer. Existem listas separadas para buffers vazios, ocupados e trancados, e livres. Cada buffer não incluído na lista de buffers livres é indexado por uma função hash aplicada sobre seu dispositivo e número de bloco, e é encadeado em uma lista correspondente de buscas por hash. O gerenciador de solicitações é a camada de software que gerencia a leitura e gravação do conteúdo do buffer para um driver de dispositivo de blocos a partir dele. O sistema de solicitações funciona com base numa função que realiza leituras e gravações de baixo nível associadas aos dispositivos de blocos. Esta função toma uma lista de descritores de buffers buffer_head e um flag de leitura-gravação como seu argumento, posicionando o I/O em curso para todos esses buffers, sem esperar que I/O complete.
Gerência de Entrada e Saída
71
Para cada driver de dispositivo de blocos é mantida uma lista separada de solicitações, que são incluídas em um escalonamento de acordo com um algoritmo unidirecional (CSCAN) que explora a ordem na qual as solicitações são inseridas nas listas por dispositivo e delas removidas. Quando novas solicitações de I/O são realizadas, o gerenciador de solicitações tenta intercalar as solicitações nas listas por dispositivo. 6.2.2
Dispositivos de caracteres
Quaisquer drivers de dispositivos de caracteres registrados para o kernel do Linux devem registrar um conjunto de funções que implementem as operações de I/O em arquivos que o driver possa manipular. O kernel simplesmente passa para o dispositivo a solicitação de leitura ou gravação de um arquivo relacionada a um dispositivo de caracteres. A exceção é o subconjunto especial de drivers de dispositivo de caracteres que implementam dispositivos de terminais. Neste caso, o kernel mantém uma interface padrão para esses drivers por meio de um conjunto de estruturas tty_struct. Essas estruturas fornecem alocação em buffers e controle de fluxo das cadeias de dados provenientes do dispositivo terminal e alimenta esses dados em um orientador de rota. O orientador de rota é um interpretador para a informação do dispositivo terminal. O orientador de rota mais comum é o tty, que adere a cadeia de dados do terminal às cadeias padrões de entrada e saída dos processo em execução de um usuário, permitindo que esses processos se comuniquem diretamente com o terminal do usuário. 6.2.3
Estrutura de rede
A conexão em rede do kernel do Linux é implementada por três camadas de software: a interface de socket, drivers de protocolos e drivers de dispositivo de rede. As aplicações de usuário realizam todas as solicitações de conexão à rede através da interface de socket. A camada de protocolo pode regravar pacotes, criar novos pacotes subdividir ou remontar pacotes em fragmentos ou simplesmente descartar dados que estiverem chegando. A camada de protocolo decide para qual socket ou dispositivo enviar o pacote. O conjunto mais importante de protocolos do Linux é o de protocolos de Internet (IP- Internet Protocol)
6.3
RESUMO
Este capítulo apresentou conceitos relacionados a gerenciamento de I/O e como o Linux implementa este gerenciamento. Com este último capítulo, encerra-se a apostila de Kernel do Linux, onde foi vista uma visão geral sobre os conceitos relacionados a sistemas operacionais, gerência de processos, gerência de memória, o sistema de arquivos e gerência de entrada e saída.
72
EDITORA - UFLA/FAEPE - Kernel do Linux
7 COMPARAÇÃO ENTRE SOLARIS, LINUX E FREEBSD
Max Bruning [Bruning 2005] comparou o kernel do Solaris 10, com o kernel do Linux 2.6 e o do FreeBSD 5.3. Os três subsistemas examinados foram: escalonamento, gerência de memória e arquitetura de sistema de arquivos, pois existem em qualquer sistema operacional. Cada um dos sistemas operacionais comparados suportam escalonamento de threads de tempo compartilhado, paginação por demanda com um algoritmo de substituição de página não recentemente utilizada e uma camada de sistema de arquivo virtual para permitir a implementação de diferentes arquiteturas de sistemas de arquivos. Linux utiliza os conceitos de alocador de memória slab do Solaris. Muito da terminologia encontrada no fonte do FreeBSD está também presente no Solaris.
7.1
ESCALONAMENTO E ESCALONADORES
A unidade básica de escalonamento no Solaris é a kthread_t; no FreeBSD, a thread; e no Linux, a task_struct. Solaris representa cada processo como um proc_t e cada thread dentro do processo tem uma kthread_t. Linux representa processos (e threads) pelas estruturas task_struct. Um processo com uma única thread, em Linux, tem uma única task_struct. Um processo com uma única thread, no Solaris tem um proc_t, uma única kthread_t e uma klwp_t. O klwp_t provê uma área salva para troca de threads entre os modos usuário e kernel. Um processo com única thread em FreeBSD tem uma proc struct, uma thread struct e uma ksegrp struct. O ksegrp é um grupo de entidades de escalonamento do kernel. Resumidamente, todos os 3 sistemas operacionais escalonam threads, onde uma thread é uma kthread_t em Solaris, uma struct thread em FreeBSD e uma task_struct em Linux. As decisões de escalonamento são baseadas em prioridade. Linux e FreeBSD possuem o menor valor de prioridade como o mais baixo. Isto é, um valor próximo a 0 representam uma prioridade mais alta. Em Solaris, o valor maior representa a prioridade mais alta.
74
EDITORA - UFLA/FAEPE - Kernel do Linux
Solaris, FreeBSD e Linux usam uma runqueue por CPU. FreeBSD e Linux usam uma fila active e uma fila expired. Threads são escalonadas em prioridade de fila active. Uma thread move de uma fila active para a fila expired quando ela usa todo seu time slice. Quando a fila active está vazia, o kernel troca as filas active e expired. FreeBSD possui uma terceira fila para threads idle. Threads entram nesta fila quando as outras duas filas estão vazias. Solaris usa um dispatch queue por CPU. Se uma thread usa seu time slice, o kernel dá a ela uma nova prioridade e a retorna para a dispatch queue. FreeBSD usa uma lista por prioridade para as 4 prioridades nas runqueues e Solaris e Linux usam listas separadas para cada prioridade. Uma grande diferença entre Solaris e os outros dois sistemas operacionais está na capacidade de suportar múltiplas classes de escalonamento no sistema ao mesmo tempo. Os três sistemas operacionais suportam Posix SCHED_FIFO, SCHED_RR, e SCHED_OTHER (ou SCHED_NORMAL). SCHED_FIFO e SCHED_RR tipicamente resultam em threads de tempo real. Solaris tem suporte para uma classe de prioridade fixa, uma classe de sistema para sistema de threads (tal como threads page-out), uma classe interativa usada para threads executnam em um ambiente de janelas sob controle de servidor X e um escalonador de recursos justo em suporte ao gerenciamento de recursos. O escalonador no FreeBSD é escolhido em tempo de compilação e no Linux o escalonador depende da versão do Linux.
7.2
GERÊNCIA DE MEMÓRIA E PAGINAÇÃO
No Solaris, todo processo tem um espaço de endereçamento composto de divisões de seções lógicas chamadas segmentos. Os segmentos do espaço de endereçamento do processo são visíveis via pmap(1). Solaris divide o código de gerência de memória e as estruturas de dados em partes independentes da plataforma e específicas da plataforma. A gerência de memória de porções específicas da plataforma está na camada HAT (Hardware Address Translation). FreeBSD descreve seu espaço de endereçamento de processo por um vmspace, dividido em seções lógicas chamadas regiões. Porções dependentes do hardware estão no módulo pmap (physical map) e rotinas vmap tratam porções independentes do hardware e estruturas de dados. Linux usa um descritor de memória para dividir o espaço de endereçamento de processo em seções lógicas chamadas áreas de memória para descrever o espaço de endereçamento de processo. Linux também tem um comando pmap para examinar o espaço de endereçamento de processo. Linux divide camadas dependentes da máquina de camadas dependentes da máquina em um nível muito mais alto no software. Em Solaris e FreeBSD, muito do código de tratamento, por exemplo, tratamento de falha de página é independente da máquina. No Linux, o código para tratar falha de página é muito mais dependente da máquina no início do tratamento de falha. Uma conseqüência disso é que o Linux pode tratar o código de paginação
Comparação entre Solaris, Linux e FreeBSD
75
mais rapidamente pois há menos abstração de dados no código. O custo disso é que uma mudança no hardware ou modelo requer mais mudanças no código. Solaris e FreeBSD isolam tais mudanças nas camadas HAT e pmap. 7.2.1
Paginação
Todos os três sistemas operacionais usam uma variação do algoritmo LRU para substituição de página. Todos os três possuem um daemon de processo/thread para fazer a substituição de página. No FreeBSD, o daemon vm_pageout acorda periodicamente e quando a memória livre torna-se baixa. Quando a memória livre torna-se abaixo de algum limite, vm_pageout executa uma rotina (vm_pageou_scan) para buscar memória para tentar liberar algumas páginas. A rotina vm_pageout_scan pode necessitar escrever páginas modificadas assincronamente para o disco antes de liberá-las. Solaris também tem um daemon pageout que executa periodicamente e em resposta a situações de baixa memória livre. Limites de paginação em Solaris são automaticamente calibrados no startup do sistema tal que o daemon não use excessivamente a CPU ou sobrecarregue o disco com requisições page-out. O daemon FreeBSD usa valores tais que, para a maior parte, são ajustados para determinar limites de paginação. Linux também usa um algoritmo LRU que é dinamicamente ajustado. No Linux, pode haver múltiplos daemons kswapd. Os 3 sistemas utilizam a política working set global. FreeBSD tem diversas listas de página para manter trilha de páginas recentemente usadas. As listas são de páginas ativas (active), inativas (inactive), em cache (cached) e livres (free). Páginas freqüentemente usadas tendem a ficar na lista ativa. Páginas de dados de um processo que sai pode ser imediatamente colocada na lista de livres. Linux também utiliza diferentes listas de páginas para facilitar um algoritmo estilo LRU. Linux divide memória física em 3 zonas: uma para páginas DMA, uma para páginas normais e uma para memória alocada dinamicamente. Páginas movem-se entre listas hot, cold e free. Listas freqüentemente acessadas ficarão na lista hot. Páginas livres estarão nas listas cold ou free. Solaris usa lista de páginas free, hash e vnode para manter sua variação de um algoritmo LRU. Solaris escaneia todas as páginas usando um algorimo two-handed clock. O front hand age sobre a página limpando os bits de referência para a página. Se nenhum processo tiver referenciado a página desde que o front hand visitou a página, o back hand liberará a página (a página é primeiro escrita para o disco se tiver sido modificada). Todos os 3 sistemas operacionais possuem localidade numa durante paginação. O cache buffer I/O e a page cache de memória virtual é merged em um sistema de page cache nos 3 sistemas. O sistema de page cache é usado para leituras e escritas de arquivos assim como arquivos mmapped e texto e dados de aplicações.
76
7.3
EDITORA - UFLA/FAEPE - Kernel do Linux
SISTEMA DE ARQUIVOS
Os 3 sistemas operacionais usam uma camada de abstração de dados para esconder das aplicações os detalhes de implementação do sistema de arquivos. São usadas as chamadas de sistema open, close, read, write, stat, etc. para acessar arquivos. Solaris e FreeBSD chamam este mecanismo de VFS (Virtual File System) e a estrutura de dados principal é o vnode ou virtual node. Todo arquivo que está sendo acessado em Solaris ou FreeBSD tem um vnode atribuído a ele. Além disso, para informação de arquivo genérico, o vnode contém ponteiros para informação específica do sistema de arquivos. Linux também usa um mecanismo similar, denominado VFS (Virtual Filesystem Switch). No Linux, a estrutura de dados independente do sistema de arquivos é um inode. Esta estrutura é similar ao vnode no Solaris/FreeBSD. VFS permite a implementação de muitos tipos de sistemas de arquivos Linux tem duas estruturas diferentes, uma para operações de arquivos e outra para operações inode. Solaris e FreeBSD combinam essas operações vnode. VFS permite a implementação de muitos tipos de sistemas de arquivos no sistema. A Tabela 7.1 lista alguns tipos de sistemas de arquivos implementados em cada sistema operacional.
Solaris
FreeBSD
Linux
ufs nfs namefs ctfs tmpfs swapfs objfs devfs ufs defvs ext2 nfs ntfs smbfs portalfs kernfs ext3 ext2 afs nfs coda procfs reiserfs
Sistema de arquivos local padrão (baseado no sistema de arquivos rápido do BSD) Arquivos remotos /proc Sistema de arquivo de nomes; permite abertura de portas ou streams como arquivos Sistema de arquivos contratado usado com a facilidade de gerência de serviço Usa espaço anônimo (memória/swap) para arquivos temporários Mantém informações de espaços anônimos (dados, heap, pilha, etc) Mantém informações de módulos do kernel Mantém informações de arquivos /devices Sistema de arquivos default (ufs2, baseado no sistema de arquivos rápido do BSD) Mantém informações de arquivos /dev Sistema de arquivos Linux ext2 (GNU-based) Arquivos remotos Sistema de arquivos Windows NT Sistema de arquivos Samba Monta um process sobre um diretório Arquivos contendo várias informações de sistema Sistema de arquivos baseado no ext2 Sistema de arquivos extent-based Suporte a cliente AFS para compartilhamento de arquivo remoto Arquivos remotos Outro sistema de arquivos de rede Processos, processadores, barramentos, plataformas específicas Sistema de arquivos
Tabela 7.1: Lista parcial de sistemas de arquivos [Bruning (2005)]
Comparação entre Solaris, Linux e FreeBSD
7.4
77
RESUMO
Os mecanismos de implementação dos subsistemas do kernel são similares nos três sistemas operacionais. O Linux tende a usar abstração de dados de nível mais alto, com código e estruturas de dados mais dependentes da máquina. Em termos de documentação, FreeBSD tem grande parte da implementação do kernel documentada e disponível. O Linux também tem bastante documentação, mas está associada a release. Além, disso, Linux tem uma comunidade extensa disponível para responder questões.
78
EDITORA - UFLA/FAEPE - Kernel do Linux
8 CONSIDERAÇÕES FINAIS
Esta apostila apresenta uma visão geral sobre sistemas operacionais, com ênfase no kernel do Linux. Pode-se observar que objetivo principal de um sistema operacional, que é realizar a interface entre o usuário e o hardware é atendido por qualquer sistema operacional. Porém, cada sistema operacional, inclusive cada distribuição do Linux, possui funcionalidades adicionais para tornar ainda mais fácil e rápida esta interação, e fazer com que o sistema operacional seja portável para várias plataformas. Para um aprofundamento maior no kernel do Linux são citadas várias referências para livros, sites e documentações sobre o assunto, em Referências Bibliográficas.
80
EDITORA - UFLA/FAEPE - Kernel do Linux
REFERÊNCIAS BIBLIOGRÁFICAS
8.1
SOBRE SISTEMAS OPERACIONAIS EM GERAL
[Oliveira (2001)] Oliveira, R. S., Carissimi, A. S. e Toscani, S. S. Sistemas Operacionais, Sagra Luzzatto. Série Livros Didáticos, Número 11, Segunda Edição. 2001. [Silberschatz (2004)] Silberzchatz, A., Galvin, P. B. and Gagne, G. Fundamentos de Sistemas Operacionais, LTC. Sexta Edição. Rio de Janeiro. 2004. [Tanenbaum (1995)] Tanenbaum, A. S. Sistemas Operacionais Modernos, LTC. 1995.
8.2
SOBRE SISTEMA UNIX
[Bell (2007)] Bell Labs. The Creation of the UNIX* Operating System. 2007. Available at http://www.bell-labs.com/history/unix/. [UNIX (2007)] The Open Group. History and Timeline. http://www.unix.org/what_is_unix/history_timeline.html.
8.3
2007.
Available
at
SOBRE SISTEMA LINUX
[Beck (1998)] Beck, M., Böhme, H., Dziadzka, M., Kunitz, U., Magnus, R. and Verworner,D. Linux Kernel Internals, Second Edition, Addison-Wesley. 1998. [Bovet (2002)] Bovet, D. P. & Cesati, M. Understanding the Linux Kernel, Second Edition, O’Reilly. 2002. [Cisneiros (2007)] Cisneiros, H. The Linux Manual. Capítulo 1. Introdução ao Linux 2006. Disponível em http://www.devin.com.br/eitch/tlm4/s1-diferenca-linux-unix.html.
82
EDITORA - UFLA/FAEPE - Kernel do Linux
[Garrels (2002)] Garrels, M. Introduction to Linux - A Hands on Guide. 2002. Available at http://www.tldp.org/LDP/intro-linux/intro-linux.pdf. [Godoy (2006)] Godoy, J., Hogbin, E. J., Komarinski, M. F. and Merrill, D. C. The Linux Documentation Project. 2006. Available at http://www.tldp.org/guides.html. [Kernel (2007)] Linux Kernel Organization, Inc. The Linux Kernel Archives. Available at http://www.kernel.org, Jan. 2007. [Love (2005)] Love, R. Linux Kernel Development, Second Edition, Sams Publishing. 2005. [Maxwell (2000)] Maxwell, S. Kernel do Linux, Makron Books. 2000. [Mazioli] Mazioli, G. Guia Foca http://focalinux.cipsga.org.br/
Linux,
em
vários
níveis.
Disponível
em
[Mazioli (2006)] Mazioli, G. Guia Foca GNU/Linux - Capítulo 16 - Kernel e Módulos. 2006. Disponível em http://focalinux.cipsga.org.br/guia/intermediario/ch-kern.htm [Mazioli (2006)] Mazioli, G. Guia Foca GNU/Linux Gerenciadores de Partida (boot loaders). 2006. http://focalinux.cipsga.org.br/guia/intermediario/ch-boot.htm
Capítulo 6 Disponível em
[Nemeth (2002)] Nemeth, E., Snyder, G. and Hein, T. R. Linux Administration Handbook, Prentice Hall PTR. 2002. [Corbet (2005)] Corbet, J., Rubini, A., Kroah-Hartman, G. Linux Device Drivers, Third Edition, O’Reilly, 2005. Available at http://lwn.net/Kernel/LDD3/. [Santos (1999)] Santos, C. A. M. Linux Portuguese-HOWTO. Universidade Federal de Pelotas. 1999. Disponível em http://br.tldp.org/projetos/howto/arquivos/html/Portuguese-HOWTO/PortugueseHOWTO.pt_BR-195.html. [Wirzenius (2005)] Wirzenius, L., Oja, J., Stafford, S. and Weeks, A. The Linux System Administrator’s Guide. 2005. Available at http://www.tldp.org/LDP/sag/sag.pdf.
8.4
OUTROS
[Apple (2007)] Apple Inc. 2007. Available at http://www.apple.com/. [Bruning (2005)] Max Bruning. A Comparison of Solaris, Linux, and FreeBSD Kernels, 2005. Available at http://www.opensolaris.org/os/article/2005-1014_a_comparison_of_solaris__linux__and_freebsd_kernels/ em February, 2007.
Referências Bibliográficas
83
[Microsoft (2007)] Microsoft Brasil. 2007. http://www.microsoft.com/brasil/exchange2007/default.mspx.
Disponível
em
[Seebac (2004)] Seebach, P. IBM: Basic use of pthreads. 2004. Available at http://www128.ibm.com/developerworks/linux/library/l-pthred.html. [Vivier (2007)] Vivier, L. Ext4 Development http://www.bullopensource.org/ext4/.
project.
2007.
Available
at
[Johnson (2001)] Johnson, M. K. Whitepaper: Red Hat’s New Journaling File System: ext3. 2001. Available at http://www.redhat.com/support/wpapers/redhat/ext3/.