UNIVERSIDADE DO VALE DO ITAJAÍ CENTRO DE CIÊNCIAS TECNOLÓGICAS DA TERRA E DO MAR CURSO DE CIÊNCIA DA COMPUTAÇÃO PIQUENO UM SISTEMA OPERACIONAL EDUCACIONAL PARA O RASPBERRY PI por Daniel Santos Bathke Itajaí (SC), novembro de 2013 UNIVERSIDADE DO VALE DO ITAJAÍ CENTRO DE CIÊNCIAS TECNOLÓGICAS DA TERRA E DO MAR CURSO DE CIÊNCIA DA COMPUTAÇÃO PIQUENO UM SISTEMA OPERACIONAL EDUCACIONAL PARA O RASPBERRY PI Área de Sistemas Operacionais por Daniel Santos Bathke Relatório apresentado à Banca Examinadora do Trabalho Técnico-científico de Conclusão do Curso de Ciência da Computação para análise e aprovação. Orientador: Fabrício Bortoluzzi, M.Sc. Itajaí (SC), novembro de 2013 Dedico este trabalho ao meu avô materno, Lúcio dos Santos, que sempre foi e ainda é referência familiar nos quesitos de comportamento. Este pescador da Praia Alegre, Penha, que sempre deu muito valor a sua família, era chamado de Quininho. Presto uma homenagem a seu nome ao batizar o PIqueno, derivado de piqueno, piquininho e quininho. RESUMO BATHKE, Daniel S. PIqueno: Um sistema operacional educacional para o Raspberry Pi. Itajaí, 2013. 111. Trabalho Técnico-científico de Conclusão de Curso (Graduação em Ciência da Computação) – Centro de Ciências Tecnológicas da Terra e do Mar, Universidade do Vale do Itajaí, Itajaí, 2013. Este trabalho documenta a criação de uma ferramenta educacional para ser utilizada em sala de aula na disciplina de sistemas operacionais, nos cursos da Ciência da Computação e Engenharia da Computação da Universidade do Vale do Itajaí (UNIVALI). Acredita-se que há uma lacuna entre a teoria e a prática no ensino desta disciplina, quando o aluno sai de outras disciplinas com atividades mais práticas e entra nesta, onde são apresentados vários conceitos teóricos, mas sem exercita-los e conecta-los aos conhecimentos previamente obtidos. Trata-se do PIqueno, um sistema operacional simples o suficiente para ser compreendido e estudado, para o Raspberry Pi, uma plataforma simples e barata desenvolvida também para propósitos eduacionais. Desenvolvido em Assembly e C, o PIqueno abre mão de otimizações de código-fonte para ganhar em clareza de entendimento. Espera-se que o PIqueno possa contribuir para aumentar a qualidade do ensino, servindo como ferramenta para os professores de sistemas operacionais que utilizam metodologias de ensino com base construtivista. Palavras-chave: Sistemas Operacionais. Raspberry Pi. Educação. ABSTRACT This paper documents the creation of an educational tool to be used in the classroom in the discipline of operating systems of Computer Science and Computer Engineering courses at Universidade do Vale do Itajaí (UNIVALI). It is believed that there is a gap between theory and practice in teaching this subject, when the student leaves other disciplines with more practical activities and enters this, which presents various theoretical concepts, but without exercising them and connecting them to the previously obtained knowledge. It's PIqueno, an operating system simple enough to be understood and studied, for the Raspberry Pi, a platform developed to be simple and inexpensive also for educational purposes. Developed in C and Assembly, PIqueno waive optimizations source to gain in clarity of understanding. It is expected that PIqueno might contribute to increasing the quality of education, serving as a tool for teachers of operating systems that use constructivist based teaching methods. Keywords: Operating System. Raspberry Pi. Education. LISTA DE FIGURAS Figura 1. O Raspberry Pi .......................................................................................................... 20 Figura 2. Ilustração de componentes ........................................................................................ 22 Figura 3. (a) A multiprogramação, (b) contadores de 4 processos e (c) somente 1 processo executando por vez ................................................................................................................... 24 Figura 4. Hierarquia de processos ............................................................................................ 25 Figura 5. Estados de processos ................................................................................................. 27 Figura 6. Ilustração de vetores de interrupção .......................................................................... 30 Figura 7. Alternância do processador para tratar interrupções ................................................. 31 Figura 8. Fluxograma de mudanças do modo do processador ................................................. 32 Figura 9. Ilustração da organização da memória principal ....................................................... 33 Figura 10. Ilustração de execução de processos usando FIFO ................................................. 36 Figura 11. Ilustração de execução de processos usando SJF.................................................... 37 Figura 12. Ilustração de execução de processos usando Round Robin .................................... 39 Figura 13. Diagrama de Casos de Uso ..................................................................................... 47 Figura 14. Diagrama de Domínio do PIqueno .......................................................................... 49 LISTA DE TABELAS Tabela 1. Tabela comparativa de sistemas operacionais semelhantes e o PIqueno ................. 45 LISTA DE QUADROS Quadro 1. Código de inicialização ........................................................................................... 50 Quadro 2. Procedimento Main.................................................................................................. 51 Quadro 3. Procedimento create_main_process ........................................................................ 52 Quadro 4. Definição da struct process ..................................................................................... 52 Quadro 5. Processo invocando uma Syscall para término ........................................................ 54 Quadro 6. Função interrupts_init configurando vetor de interrupções .................................... 55 Quadro 7. Tratamento de uma interrupção de IRQ invocando o escalonador ......................... 56 Quadro 8. Escalonador trocando processos do processador ..................................................... 58 LISTA DE ABREVIATURAS E SIGLAS A ARM CPU DHCP FIFO GPIO GPU HDMI IDE Mbps RAM RCA RISC ROM SJF SoC TTC UML UNIVALI USB V Ampere Advanced RISC Machine Central Processing Unit Dinamic Host Configuration Protocol First In First Out General Purpose Input/Output Graphic Processing Unit High Definition Media Interface Integrated Development Environment Mega bits por segundo Random Access Memory Radio Corporation of America Reduced Instruction Set Computer Read Only Memory Shortest Job First System on Chip Trabalho Técnico-científico de Conclusão de Curso Unified Modeling Language Universidade do Vale do Itajaí Universal Serial Bus Volt SUMÁRIO 1 INTRODUÇÃO .................................................................................................................. 15 1.1 PROBLEMATIZAÇÃO..................................................................................................... 16 1.2 FORMULAÇÃO DO PROBLEMA .................................................................................. 16 1.2.1 Solução Desenvolvida ................................................................................................... 17 1.3 OBJETIVOS ..................................................................................................................... 17 1.3.1 Objetivo Geral .............................................................................................................. 17 1.3.2 Objetivos Específicos .................................................................................................... 17 1.4 METODOLOGIA ............................................................................................................. 18 1.5 ESTRUTURA DO TRABALHO ........................................................................................ 19 2 FUNDAMENTAÇÃO TEÓRICA ..................................................................................... 20 2.1 RASPBERRY PI ............................................................................................................... 20 2.1.1 Especificações técnica .................................................................................................. 21 2.2 SISTEMAS OPERACIONAIS ........................................................................................... 22 2.2.1 Processo ........................................................................................................................ 23 2.3 CONSTRUTIVISMO ........................................................................................................ 40 3 DESENVOLVIMENTO .................................................................................................... 45 3.1 DIAGRAMA DE CASOS DE USO ................................................................................... 47 3.2 REQUISITOS ................................................................................................................... 48 3.2.1 Requisitos não funcionais ............................................................................................. 48 3.2.2 Requisitos funcionais .................................................................................................... 48 3.3 DIAGRAMA DE DOMÍNIO............................................................................................. 48 3.4 CENÁRIO DE UTILIZAÇÃO EM SALA DE AULA ........................................................ 58 4 CONSIDERAÇÕES FINAIS ............................................................................................ 60 4.1 TRABALHOS FUTUROS ................................................................................................. 60 4.1.1 Drivers para dispositivos de E/S .................................................................................. 61 4.1.2 Sistema de arquivos ...................................................................................................... 61 4.1.3 Gerenciamento de E/S .................................................................................................. 61 4.1.4 Definição de arquivo binário ........................................................................................ 62 4.1.5 Compilador para o PIqueno ......................................................................................... 62 4.1.6 Gerenciamento de memória .......................................................................................... 62 4.1.7 Interface de linha de comando...................................................................................... 63 4.1.8 Driver para dispositivo de rede .................................................................................... 63 4.1.9 Conjunto de ferramentas para execução em linha de comando ................................... 64 4.1.10 Gerenciamento de usuários ........................................................................................ 64 4.1.11 Permissões .................................................................................................................. 64 4.1.12 Interface gráfica ......................................................................................................... 65 APÊNDICE A. CÓDIGO FONTE ....................................................................................... 67 15 1 INTRODUÇÃO Atualmente, um hardware sozinho não é capaz de fazer muita coisa, mas, em conjunto com um software pode executar tarefas rotineiras como acessar uma página na internet, escutar uma música, assistir a um vídeo ou tarefas grandiosas como fazer cálculos de previsão de desastres, simulação de ambientes biológicos, entre outras. Esse poder e flexibilidade alcançados devem-se grande parte ao software que conceitualmente é chamado de Sistema Operacional. Este veio para resolver dois problemas principais: (i) dificuldade na programação de aplicativos e (ii) falta de um gerenciador de recursos. Ao se construir softwares aplicativos para o usuário, exigia-se que o programador conhecesse cada tipo de hardware que fosse usado e que tratasse cada uma de suas particularidades para que, desta forma, pudesse utilizá-los em favor do usuário. Essa exigência tornava muito custoso o desenvolvimento de um software aplicativo e, praticamente nulo o reaproveitamento de código de um hardware para outro. Com isso, viu-se a necessidade de abstrair a programação de hardware em partes específicas, feias e difíceis de programar para uma camada de nível maior, com chamadas comuns e simples de usar. Entende-se esse conceito por máquina estendida (TANENBAUM, 2010). Pensando no aumento de poder de computação de um hardware, viu-se a necessidade de este executar mais tarefas ao mesmo tempo, pois o que se tinha até dado momento era um hardware específico para cada tarefa. Entende-se que um processador, por exemplo, é rápido suficiente para executar muito mais do que uma tarefa, desde que este destinasse uma parte do seu tempo a cada tarefa em execução. Dentro desse cenário, duas tarefas poderiam tentar utilizar a impressora ao mesmo tempo, o que poderia ser desastroso e arruinar o resultado de ambas no ponto de vista do usuário. Para resolver este problema, criou-se um conjunto de software que fizesse essa organização de tal forma que, em um caso como este cada tarefa imprimisse seu trabalho até o fim sem ser interrompida. Denomina-se esta solução como gerenciador de recursos (TANENBAUM, 2010). A combinação desses dois conceitos forma o que se conhece hoje como Sistema Operacional, provendo uma máquina estendida fácil de programar e gerenciando os recursos de hardware a fim de executar as tarefas do usuário com sucesso. 16 Por se tratar desse nível de complexidade e extensão, muitos professores de sistemas operacionais consideram que a melhor maneira de aprendizado dos alunos é a interação com o mundo real, através de problemas concretos. Isso se traduz em explorar o núcleo de um sistema operacional com código-fonte disponível (MAZIERO, 2002). Porém, a didática do contato direto pode causar diversos problemas. Entre eles estão o excessivo número de linhas de código-fonte em um sistema operacional real, como o caso do Linux que possui 3,3 milhões ou o do próprio MINIX, com 35 mil, a dificuldade de compreensão das otimizações, a dificuldade em depuração ou no ciclo de desenvolvimento no interior do kernel (núcleo) e, por fim, um custo elevado para disponibilizar computadores exclusivos para este fim (MAZIERO, 2002). Este trabalho técnico-científico se propôs a desenvolver o PIqueno, um sistema operacional para o computador Raspberry Pi com foco no aprendizado de alguns conceitos, abrindo mão de complexidade e otimizações para ganhar legibilidade no código-fonte. O objetivo é que estudantes da disciplina de sistemas operacionais com duração de apenas um semestre tenham como possibilidade o estudo prático de um sistema operacional simples, em complemento ao método tradicional baseado na interpretação de conceitos. O Raspberry Pi é um minicomputador de uso geral, pequeno o bastante para caber em um bolso e relativamente barato, custando cerca de 35 dólares no Reino Unido e 150 reais no Brasil (em meados de maio de 2013). Depois de comprado pode-se utiliza-lo com monitores ou televisores convencionais, teclado e mouse. Foi feito para ser inicializado do cartão de memória com um sistema operacional de código-fonte aberto, facilitando crianças a programarem e tentarem coisas novas sem se preocupar em estragar o computador da família ou com o tamanho do prejuízo caso algo dê errado (HALFACREE; UPTON, 2012). 1.1 PROBLEMATIZAÇÃO 1.2 FORMULAÇÃO DO PROBLEMA As disciplinas de sistemas operacionais ministradas nos cursos de graduação na área de comptuação na UNIVALI abordam os mesmos conceitos e sofrem das mesmas carências que estas disciplinas enfrentam nas demais universidades: um curto período de tempo para tratar de tantos conceitos fundamentais do assunto, como o escalonamento de processos, o 17 chaveamento de contextos, o funcionamento das interrupções, a proteção da memória principal, impasses e diversos outros. Ao optar por abordar tais temas também sob a forma de experimentos práticos, carece o professor muitas vezes de um conjunto de experimentos que sejam confiáveis, realistas, corretos e principalmente: simplistas e livres de complexidades desnecessárias que poderiam causar uma curva de aprendizado complexa e inviável. Acredita-se que se poderia obter resultados melhores de aprendizagem utilizando de recursos mais práticos, fazendo uso de estratégias fundamentadas no construtivismo, como trabalho em grupo e interação entre o aluno e o objeto. 1.2.1 Solução Desenvolvida Este trabalho propõe o desenvolvimento e uso do PIqueno, um sistema operacional educacional para o Raspberry Pi, com foco na legibilidade do código-fonte e recursos limitados para ganhar simplicidade e tornar o processo de aprendizagem prática mais rápida. O mesmo limita-se, a princípio, a exemplificar e permitir explorações do conceito dos conceitos de escalonamento de processos e de seu mecanismo de chaveamento de contexto subjacente. Como consequência, os conceitos de (i) modo real e protegido e (ii) escalonamento de processos, também são abordados. 1.3 OBJETIVOS 1.3.1 Objetivo Geral O objetivo geral deste trabalho foi desenvolver e documentar o PIqueno, de forma que o mesmo possa ser utilizado como ferramenta de apoio no ensino da disciplina de sistemas operacionais do curso de Ciência da Computação da UNIVALI. 1.3.2 Objetivos Específicos O objetivo geral deste trabalho foi alcançado através do cumprimento dos objetivos específicos a seguir: Compreender a arquitetura do Raspberry, seu funcionamento interno e planejar sua inicialização; 18 Pesquisar sistemas operacionais de porte e propósito similar ao ansiado por este trabalho; Modelar o PIqueno alinhado às expectativas de finalidade de apoio ao ensino. Os artefatos produzidos nesta etapa foram: (i) análise de requisitos funcionais e não funcionais, (ii) diagrama de casos de uso e (iii) diagrama de domínio; Estudar estratégias pedagógicas para aplicação do PIqueno em sala de aula; Executar a implementação do PIqueno; Documentar o TTC em formato de um artigo de iniciação científica e publicar seu código-fonte em um repositório de acesso público. 1.4 METODOLOGIA A metodologia deste trabalho foi dividida em seis partes, cada uma para alcançar diretamente um objetivo específico para que, ao final, o objetivo geral também fosse alcançado. Revisão: Nesta etapa foi feita uma revisão sobre os conceitos de sistemas operacionais, para que fosse definido os que o PIqueno deveria apoiar. Também foi feita uma análise sobre a arquitetura do Raspberry Pi e suas características, gerando uma documentação para apoio na implementação; Análise de similares: Foi feito uma pesquisa sobre os sistemas operacionais semelhantes ao PIqueno, em tamanho e propósito. Com essa pesquisa é possível gerar um quadro comparando as características de cada um, a fim de conhecer onde cada um se diferia; Modelagem: Foram elaborados diagramas de caso de uso e de domínio e elicitados os requisitos funcionais e não-funcionais, de acordo com a UML, que o PIqueno deveria atender para alcançar seu propósito; Estudo pedagógico: Estratégias pedagógicas foram estudadas para proporcionar melhores aplicações do PIqueno em sala de aula; 19 Implementação: Etapa onde o PIqueno foi própriamente feito. Foram utilizadas as linguagens Assembly e C, compiladas para ARM em um PC x86 com a toolchain gcc-arm-linux-gnueabihf, disponível para Linux; 1.5 ESTRUTURA DO TRABALHO Este trabalho está dividido em 3 partes: (i) introdução, (ii) fundamentação e (iii) projeto. Na introdução é exposta a problemática que tangiu a direção deste trabalho e a solução desenvolvida. A problemática baseia-se no artigo de MAZIERO (2002), onde afirma existir uma deficiência nas ferramentas disponíveis para o ensino da disciplina de sistemas operacionais. A fundamentação teórica recupera os conceitos-chave de sistemas operacionais, introduz o Raspberry Pi e trás a metodologia pedagógica que se pretende alcançar com o desenvolvimento e publicação do PIqueno. O desenvolvimento descreve como a parte prática da implementação do PIqueno foi executada, bem como sua documentação, requisitos funcionais e não funcionais e diagrama de domínio. 20 2 FUNDAMENTAÇÃO TEÓRICA A fundamentação teórica deste trabalho está dividida em três seções. A primeira seção, 2.1, será destinada a definir e descrever o Raspberry Pi. A seção 2.2 descreve os conceitos de sistemas operacionais com especial atenção àqueles que foram empregados no PIqueno. A seção 2.3 trata das metodologias de ensino que justificaram a elaboração deste trabalho. 2.1 RASPBERRY PI Raspberry Pi é um minicomputador do tamanho de um cartão de crédito. Este foi feito por um grupo de pesquisadores (entre eles Eben Upton, principal idealizador) com o propósito de estimular crianças a aprenderem a programar. (HALFACREE; UPTON, 2012). Fruto de uma longa pesquisa, este teve sua maior oportunidade de realização depois de 2008, quando os processadores projetados para dispositivos móveis começaram a ficar mais acessíveis. Nessa época, Upton trabalhava na Broadcom como arquiteto de chip (Ibidem). A Figura 1 mostra o Raspberry Pi. Figura 1. O Raspberry Pi 21 Fonte: Adaptado de http://www.raspberrypi.org/faqs (2013). 2.1.1 Especificações técnica Sua arquitetura consiste de um SoC (System on Chip, sistema em chip), equipado com um processador de multimídia da família ARM (Advanced RISC Machine, máquina RISC avançada), GPU (Graphic Processor Unit, unidade de processamento gráfico) e controlador de áudio integrado. Trata-se do chip BCM2835, fabricado pela Broadcom (Ibidem). Seu processador é a chave para juntar-se baixo custo, simplicidade, bom poder gráfico e baixo consumo de energia. Ele possibilita que o Raspberry Pi seja alimentado com apenas 5V e consuma 1A, através de uma porta no padrão micro USB (Universal Serial Bus, Barramento Serial Universal) (Ibidem). O minicomputador ainda conta com um controlador USB, controlador de rede Ethernet 10/100Mbps, 512MB de memória RAM (Random Access Memory, Memória de Acesso Randômico) e um leitor de cartão de memória. Seus meios de entrada e saída contam com 2 portas USB, 1 porta HDMI (High Definition Multimedia Interface, Interface de mídia de alta definição), 1 porta RCA (proveniente do nome da fabricante Radio Corporation of America) e um conjunto de pinos GPIO (General Purpose Input/Output, entrada e saída de propósito genérico) (Ibidem). A Figura 2 mostra uma ilustração dos componentes do Raspberry Pi. 22 Figura 2. Ilustração de componentes Fonte: Adaptado de http://www.raspberrypi.org/faqs (2013). O alinhamento entre os propósitos educacionais do PIqueno e do Raspberry Pi foram decisivos para a sua escolha. A simplicidade e custo baixo do Raspberry Pi aumentam as chances do PIqueno ser utilizado em salas de aula, laboratórios ou até mesmo ser adquirido por alunos. 2.2 SISTEMAS OPERACIONAIS Um sistema operacional pode ser visto sob duas óticas: (i) como máquina estendida ou (ii) como gerenciador de recursos. Como máquina estendida, faz a abstração do hardware para o programador de aplicativo. Como gerenciador de recursos, ele organiza o hardware de maneira que cada aplicativo possa utiliza-lo de maneira justa e coerente (TANENBAUM, 2010). 23 Essa técnica presente nos sistemas operacionais modernos foi resultado de uma mudança no paradigma da computação, em que o computador executava uma tarefa por vez e passou a executar várias coisas ao mesmo tempo, pelo menos na percepção do usuário. Essa percepção de simultaneidade ou de paralelismo chama-se pseudoparalelismo: a alternância da CPU entre programas é tão rápida que dá ao usuário a falsa impressão de executar todos os programas ao mesmo tempo. Com a alternância da CPU entre programas criou-se um problema: a concorrência por recursos. Agora, quando um programa executa, ele não pode mais assumir controle total do computador. Foi para resolver este problema que foi criado o conceito de gerenciador de recursos, para que o usuário possa executar mais tarefas ao mesmo tempo sem que estas atrapalhem o resultado uma das outras, tornando-se assim inúteis. O programador de aplicativo não deve mais assumir o controle total do hardware: de fato nem o controlar, mas solicitar ao sistema operacional para que este execute de forma organizada e padronizada algum procedimento. Em outra perspectiva, que visa facilitar e impulsionar o desenvolvimento de aplicativos, um sistema operacional pode ser visto como máquina estendida, criando uma interface simplificada e padronizada de acesso ao hardware pelo programador de aplicativo. Com isso, os mesmos deixaram de se preocupar, por exemplo, com a velocidade do disco rígido, qual trilha a agulha disco rígido se encontra e apenas se preocupam em solicitar ao sistema operacional que abra um arquivo e retorne seu conteúdo. Feita essa revisão, é possível aprofundar-se no estudo de sistemas operacionais, começando pelo conceito mais central de todo sistema operacional (TANENBAUM, 2010), e que será o foco inicial de aprendizagem proporcionada pelo PIqueno, o processo. 2.2.1 Processo Para que vários programas possam compartilhar o processador, faz-se necessário uma troca ou alternância entre eles, até que os mesmos sejam finalizados. Como o processador só consegue executar um processo de cada vez, é dever do sistema operacional determinar quando e como eles serão alternados. Para que um programa seja interrompido pela metade para ceder tempo de processador a outro programa, seu estado atual deve ser armazenado, para que posteriormente possa ter seus dados recuperados e ter sua execução continuada da parte onde parou. 24 O programa em execução, suas variáveis, o contador de programa e seus registradores formam o conceito de processo (TANENBAUM, 2010). O sistema operacional deve organizar os processos, de forma que possa priorizar e executar todos da forma mais conveniente para o usuário. Naturalmente, o primeiro processo a ser executado é o processo de inicialização, onde o conjunto de softwares do sistema operacional é carregado, dando posteriormente ao usuário um meio de solicitar a execução de outros processos de seu interesse. A Figura 3 mostra Figura 3. (a) A multiprogramação, (b) contadores de 4 processos e (c) somente 1 processo executando por vez Fonte: Adaptado de TANENBAUM (2010). 2.2.1.1 Relacionamento entre processos Na maioria dos sistemas operacionais, os processos são criados por outros processos através de uma chamada de sistema, gerando assim uma hierarquia de processos. Quando o processo de inicialização termina, ele cria alguns processos, como por exemplo, uma shell (interface de linha de comando) ou gerenciador gráfico, que por sua vez pode iniciar o antivírus, o gerenciador de conexões de rede, entre outros. Neste ponto, o próprio usuário pode selecionar quais programas quer utilizar, gerando assim um processo para cada um. Percebe-se então o conceito de hierarquia, chamada de árvore de processos, onde cada processo tem um pai e nenhum ou muitos filhos (OLIVEIRA; TOSCANI; CARISSIMI, 2010). 25 A Figura 4 ilustra um possível estado da árvore de processos derivado do exemplo anterior, sendo P1 o processo de inicialização, P2 o gerenciador gráfico, P5 antivírus, P6 o gerenciador de conexões de rede e P7 algum programa executado pelo usuário. Figura 4. Hierarquia de processos Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). Quando um processo termina ou é encerrado pelo usuário, o tronco de processos que deriva deste tem de ser tratado. Cada sistema operacional tem sua maneira de lidar com essa situação, podendo (i) destruir todos os processos filhos do processo terminado, (ii) manter como referência o processo encerrado na árvore de processos até que todos seus filhos tenham sido encerrados ou ainda (iii) vincular todos os processos filhos ao seu processo “avô” (OLIVEIRA; TOSCANI; CARISSIMI, 2010). 2.2.1.2 Estados de um processo Sabe-se que um computador tem um número muito limitado de processador. Até a década passada era mais comum que tivesse apenas um (1). Para simplificar este estudo, considera-se o caso de apenas um processador por computador. Seguindo, mesmo com um processador, um computador é capaz de executar diversos processos. De fato, muitas partes de um sistema operacional executam como processos separados entre si, talvez apenas com alguma troca de informação. O mesmo acontece com programas aplicativos, normalmente eles dependem muito de outros recursos do computador além do processador, ou seja, fazem muitas chamadas de sistema. 26 Quando um processo faz uma chamada de sistema, este tem de esperar o seu resultado para poder seguir a execução do programa. Neste momento, não faz mais sentido que este ocupe o processador. Inclusive, as próprias chamadas ao sistema dependem às vezes de processador para serem executadas. Tem-se então a primeira necessidade de alternância de processos no processador, para que outros processos dependentes possam ser executados a fim de liberar o processo original a ter sua execução continuada (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Outra situação é a necessidade de pseudoparalelismo que um sistema operacional deve prover. Ou seja, um sistema operacional moderno tem de dar ao usuário a capacidade de executar várias tarefas ao mesmo tempo, e quanto menos ele perceber a alternância de processos, mais agradável fica sua experiência. Para tal, o sistema operacional tem que impedir que um processo domine o processador até que este seja encerrado, pois dessa maneira outras tarefas ficariam bloqueadas, inclusive a interface gráfica, dando a impressão de travamento para o usuário. Para atender esta necessidade, o sistema operacional conta com um módulo chamado escalonador de processos, que basicamente enfileira os processos prontos para execução e coloca-os para executar em fatias de tempo no processador. Essa organização e priorização podem ser feita de várias maneiras e será abordada posteriormente. A Figura 5 demonstra os cinco (5) estados de processos mais comuns entre os sistemas operacionais atuais. 27 Figura 5. Estados de processos Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). Quando uma chamada de sistema é feita para a criação de um processo, fork() (se dividir) nos casos mais comuns, o sistema operacional faz uma cópia do processo original insere na árvore de processos como seu filho e, daí para frente, carrega outro programa para ser executado nele. Durante este procedimento, o processo filho fica em estado de (i) criação. Após o término da criação do processo filho, o mesmo muda seu estado para (ii) apto. Este estado não significa que o mesmo entrará em execução imediatamente. Apenas significa que já pode entrar na fila do escalonador de processos para concorrer por um tempo no processador (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Sendo selecionado para execução pelo escalonador, o processo vai para o estado (iii) executando. Isso significa que todas suas variáveis, registradores e o contador de programa foram carregados para o processador. A partir deste momento o processo encontra-se efetivamente em execução dentro do processador (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Quando um processo tem de ser interrompido porque seu programa teve a necessidade de alguma chamada de sistema, o mesmo vai para o estado (iv) bloqueado. Isso significa que este processo já não tem mais o que executar no processador até que a chamada de sistema realizada seja executada. Exemplo: Um programa solicita ao sistema operacional a leitura de 28 um arquivo. Até que o arquivo seja lido e retornado, o programa não tem mais como seguir adiante em sua execução. Durante este estado, o processo não participa do rateio do processador pelo escalonador de processos. Quando a chamada de sistema dependente for executada, o mesmo volta para o estado (ii) apto. Existem algumas exceções para esta mudança, como por exemplo quando a chamada de sistema é executada muito rapidamente. Para este caso, o processo é autorizado a voltar diretamente para o estado (iii) em execução (OLIVEIRA; TOSCANI; CARISSIMI, 2010). A qualquer momento o sistema operacional, através do escalonador de processos, pode interromper a execução de qualquer processo e eleger outro para entrar em execução. Quando isso acontece, o processo interrompido sai do estado (iii) em execução para o estado (ii) apto, e volta para a fila de rateio do processador (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Sempre quando um processo sai do estado (iii) em execução, o sistema operacional tem de executar uma série de procedimentos para que o mesmo possa voltar em execução mais tarde. Esses procedimentos consistem normalmente em salvar todos os dados relacionados ao processo que estão no processador. Entre os dados estão o contador de programa, todas as suas variáveis e registradores relacionados (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Quando o processo está no estado (iii) em execução e seu programa termina de ser executado, o mesmo vai para o estado (v) de destruição. Neste ponto, tudo que o programa deveria fazer, já foi feito. Exemplo: no comando cat, no Linux, o programa lê um ou uma série de arquivos e exibe ao usuário. Depois disto, ao entrar no estado (v) de destruição, o sistema operacional pode a qualquer momento apagar todos os seus recursos, como registradores e espaço em memória e liberá-los para serem utilizados por outros programas. Após a conclusão deste passo, o processo já não constará na árvore de processos (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Pode ser que um processo solicite uma chamada de sistema ilegal, causando erro. Para este caso, após detectado o erro na chamada de sistema, o processo é passado do estado (iv) bloqueado para o estado (v) de destruição, por motivo de erro (OLIVEIRA; TOSCANI; CARISSIMI, 2010). 2.2.1.3 Interrupções 29 Toda alternância de processos no processador é feita por interrupções, seja ela de hardware ou de software. As interrupções de hardware são geradas pelos controladores de periféricos de entrada e saída (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Quando o disco rígido, por exemplo, termina de ler uma informação e quer disponibiliza-la para o processador, o controlador do disco rígido gera uma interrupção de hardware, chamando a atenção do processador para o fato de que os dados já estão prontos. Outro exemplo importante é o teclado e mouse, que o usuário utiliza para se expressar dentro dos aplicativos. O processador pode estar ocupado com algum processo, mas quando o usuário mexe o mouse ou digita algo no teclado, o controlador específico desses hardwares enviam para o processador uma interrupção informando este evento. A interrupção de hardware é feita através do barramento de controle, com sinais elétricos gerados pelo controlador do dispositivo, que chegam diretamente no processador. Cada interrupção tem um tipo associado, pois cada uma precisa ser tratada de maneira diferente, e uma prioridade. Tipicamente os tipos variam de 0 a 255, em que quanto mais perto do zero, maior prioridade se tem. Ao tratar uma interrupção, é desabilitado temporariamente o tratamento de qualquer interrupção com prioridade menor. Após o tratamento de uma interrupção, o processador verifica por interrupções menos prioritárias pendentes e depois habilita novamente o tratamento de qualquer interrupção (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Quando o processador recebe uma interrupção de hardware, ele salta para um endereço de rotina, encontrado no vetor de interrupções, associado ao tipo de interrupção recebida. Esse endereço possui o endereço de uma rotina de software para tratar a interrupção, então o processador interrompe a execução do processo que está executando, salva todos os seus dados e executa a rotina. (OLIVEIRA; TOSCANI; CARISSIMI, 2010). A Figura 6 exemplifica um vetor de interrupções, com endereços diferentes para interrupções diferentes. 30 Figura 6. Ilustração de vetores de interrupção Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). As interrupções via software, também chamadas de trap, são geradas quando um programa precisa fazer uma chamada de sistema. De forma semelhante, cada interrupção de software também possui um tipo e um endereço de rotina conhecido pelo sistema operacional. Dessa forma, o programa não precisa conhecer quem irá tratar sua interrupção (OLIVEIRA; TOSCANI; CARISSIMI, 2010). A Figura 7 demonstra em uma linha de tempo a alternância do processador enquanto executa um processo e trata duas interrupções. 31 Figura 7. Alternância do processador para tratar interrupções Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). 2.2.1.4 Modos de operação do processador O sistema operacional é responsável por manter a proteção dos dispositivos, para que tudo funcione corretamente. Para que isso aconteça, os processadores precisam dispor em sua arquitetura um chaveamento de contexto, para alternar entre modo supervisor e modo usuário. Quando o computador liga, o processador procura o código de inicialização do sistema operacional em um local específico da memória, geralmente em ROM. Para carregar o sistema operacional, o processador está no modo supervisor. Em seguida, o sistema operacional executa o seu processo de inicialização, e sempre que executar um programa de usuário, ele chaveia o processador para modo usuário. No momento que o programa de usuário necessita de acesso a algum hardware ou instrução privilegiada, ele faz uma chamada de sistema (através de uma interrupção de software). Quando a interrupção é acionada, o processador muda para modo supervisor e carrega o tratador dessa interrupção em modo supervisor, para poder atender a chamada do usuário. Desta maneira o núcleo do sistema operacional, também chamado de kernel e que possui as partes que precisam de acesso direto ao hardware, sempre executa em modo supervisor. Já os processos de usuário sempre executam em modo usuário, pois não têm permissão para invocar diretamente o hardware. Quando algum processo de usuário tenta 32 executar instruções diretas a periféricos (as quais não tem privilégio), ocorre então uma interrupção e o sistema operacional é carregado em modo supervisor para tratar o acesso inválido. A Figura 8 mostra um fluxograma com as possíveis maneiras de mudanças de modo do processador. Figura 8. Fluxograma de mudanças do modo do processador Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). 2.2.1.5 Proteção de memória Da mesma maneira que o processador, a memória também precisa de proteção. Se um programa de usuário conseguir acessar endereços de memória do sistema operacional, todo seu funcionamento ficaria comprometido. O usuário poderia substituir uma rotina de tratamento de alguma interrupção, por exemplo, e com isso executar instruções no processador em modo supervisor. Outro problema que motivou a criação do mecanismo de proteção de memória, é a proteção de endereços de memória de um processo contra outro processo. Ou seja, o sistema operacional deve garantir que endereços de memória do processo A seja unicamente acessado e alterado por ele, e não pelo processo B. Isso também geraria problemas de funcionamento, embora de escala menor a nível de hardware, mas definitivamente poderá ser devastador para o usuário que depende do processo A. A proteção da memória também necessita de uma ajuda da arquitetura de hardware. A maneira clássica, porém funcional, de se fazer essa proteção é a utilização de registradores 33 limites. Antes de fazer o chaveamento do processador para modo usuário, o sistema operacional carrega nos registradores-limite os endereços de limite inferior e superior de memória do próximo processo a ser executado e, então, faz o chaveamento. Quando o processador está em modo usuário, ele só aceita chamadas para leitura e escrita de endereços de memória que estejam dentro dos limites impostos pelos registradores-limite. Caso isso não seja respeitado, uma interrupção será gerada e o sistema operacional será novamente carregado para informar o acesso ilegal da memória. Quando o processador volta para o modo supervisor, esse mecanismo é desligado, pois o sistema operacional deve possuir privilégio de acesso total à memória, inclusive de endereços pertencentes a processos de usuário. Isso é necessário pois muitas vezes o sistema operacional precisa escrever a resposta de uma interrupção gerada por algum processo de usuário em uma região de memória pertencente a ele, para que o mesmo consiga acessá-la. A Figura 9 demonstra como a memória principal é organizada, delimitando áreas específicas para o sistema operacional e processos de usuário. Figura 9. Ilustração da organização da memória principal Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). 2.2.1.6 Escalonamento de processo A tarefa de escolher quando o processador deve trocar de processo em execução é do escalonador, também chamado de agendador de processos. Cabe a ele também decidir qual 34 processo no estado “apto” deverá possuir o processador na sequência. Existem vários algoritmos diferentes para implementá-lo, mas este deve ser escolhido com atenção (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Existem dois pontos-chave que um escalonador procura, embora um acabe prejudicando o outro, são eles: (i) throughput (vazão) e (ii) turnaround (tempo de resposta ao usuário) (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Um alto índice de throughput significa que o processador está altamente otimizado, quase sempre ocupado com processos “aptos”, dessa forma produz-se mais em menos tempo. Ao colocar um processo em execução, não se tem a necessidade de trocá-lo antes que o mesmo termine, pois o próprio procedimento de troca de processos no processador tem um custo. Contudo, isso fere gravemente o índice de turnaround, pois normalmente os processos de interface gráfica do usuário é executado em processos diferentes dos programas que ele executa suas tarefas, ou ainda que só permita a execução de uma shell por vez, fazendo que outros usuários tenham que esperar mais tempo para poderem executar outra tarefa. Para um bom índice de turnaround é necessário que as trocas de processos em execução aconteçam com mais frequência. Assim o usuário tem uma impressão sólida de simultaneidade e, também, é possível que vários usuários consigam usar o computador com a mesma impressão. Na prática, o escalonador tem que agir mais rapidamente e compartilhar o processador de forma justa entre os processos, mantendo sempre um bom andamento de cada um deles. Contudo, não se deve ter trocas excessivas pois isso deixaria o índice de throughput muito baixo, a ponto de também causar insatisfação ao usuário por demorar mais a executar sua tarefa principal. Para Tanenbaum, um bom algoritmo de escalonamento de processos deve ser um equilíbrio entre as seguintes características: (i) imparcialidade, dando a cada processo um tempo justo no processador, (ii) a eficiência, mantendo o processador ocupado 100% do tempo se possível, (iii) throughput maximizado e (iv) turnaround minimizado (TANENBAUM, 2010). 35 Em algoritmos mais complexos, o escalonador separa os processos em grupos iobound (alto uso de entrada e saída) e cpu-bound (alto uso do processador). Isso possibilita uma melhor distribuição de recursos do sistema, já que enquanto um processo espera por dados de entrada e saída de outro dispositivo, outro processo pode ocupar o processador. Quando a entrada e saída é executada, o processo dependente volta a ganhar prioridade pois provavelmente irá ocupar o processador por pouco tempo e então solicitará mais entrada e saída. Assim cada recurso do computador mantem-se ocupado por mais tempo, fazendo com que algumas coisas (não o processamento) sejam executadas paralelamente (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Dentre os principais algoritmos de escalonamento, estão: (i) First-in first-out (ordem de chegada), (ii) Shortest job first (processo menor antes), (iii) por prioridade, (iv) múltiplasfilas e (v) Round Robin (por fatia de tempo). FIFO (First-in first-out, primeiro a entrar é o primeiro a sair) Este é o algoritmo mais simplista, mas o menos eficiente na maioria dos casos. Ele consiste em fazer uma fila única de processos ao processador, que serão executados em ordem de chegada. Neste modelo, um processo somente sai do processador quando efetua uma chamada de sistema ou quando termina. Por este motivo o índice de turnaround fica muito prejudicado, perdendo sensação de paralelismo do computador. Outro problema é que ao executar um processo característico de cpu-bound, todos os outros processos esperarão muito até conseguirem a vez no processador (OLIVEIRA; TOSCANI; CARISSIMI, 2010). A Figura 10 mostra como seriam executados os processos neste algoritmo, sendo o eixo x o tempo de processamento e o eixo y os processos. 36 Figura 10. Ilustração de execução de processos usando FIFO Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). SJF (Shortest job first, trabalho mais curto primeiro) Este algoritmo consiste basicamente em ordenar a fila de processos baseado no tempo que cada processo irá utilizar. Com isso, a média de espera por processo melhoraria, mas também poderia cair em situações piores. Se a todo momento processos menores entrassem na fila, os processos maiores nunca seriam executados, entrando em um estado de postergação indefinida (OLIVEIRA; TOSCANI; CARISSIMI, 2010). O grande fato que impossibilita que este algoritmo seja implementado é que não se consegue prever o futuro, ou seja, prever com precisão quanto um processo vai consumir de tempo ininterrupto de processador. Mesmo assim, ele é muito útil para comparar resultados em simulações com outros tipos de algoritmos, por fornecer o limite do tempo mínimo de espera de processos. A Figura 11 ilustra como seriam executados os processos com esse conceito. 37 Figura 11. Ilustração de execução de processos usando SJF Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). Por prioridade O escalonamento por prioridade funciona de forma parecida com o SJF, ordenando-se uma lista de processos, mas desta vez por sua prioridade. A prioridade não precisa ser dada necessariamente dentro do escalonador, é comum até que esta seja designada externamente conforme interesses dos usuários (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Um exemplo de prioridade seria priorizar mais os processos de professores e menos os processos de alunos. Também poderia ser por processos que interferem na interface do usuário, tornando processos do sistema operacional menos prioritários. Caso a prioridade seja definida pela característica io-bound do processo, este se assemelharia muito com o SJF, pois estes seriam executados rapidamente e entrariam na fila pelo dispositivo de entrada e saída, dando a vez para os demais processos (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Um cuidado à parte do escalonador que implementa este modelo, é que se processos com prioridades altas são frequentemente criados, os processos de baixa prioridade correm o risco de também sofrerem a postergação indefinida (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Para que isso não ocorra, é necessário que se implemente também um algoritmo de aging (envelhecimento). Conforme o tempo que os processos ficam na fila, estes vão 38 envelhecendo e, quando chegam a um determinado limite podem ganhar prioridades mais altas temporariamente. Dessa forma garante-se que os processos menos prioritários sejam executados eventualmente, mesmo que demore um pouco. Ao ganharem a vez de serem processados, caso saiam do processador por alguma chamada de sistema e tenham que entrar na fila de processos novamente, estes entram de novo com sua prioridade baixa (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Outra otimização geralmente feita é a troca de processos do processador sem nenhuma chamada de sistema ou interrupção, simplesmente pelo fato de que um processo com prioridade mais alta entrou na fila, então deve-se parar a execução do processo menos prioritário que está em execução para que ceda sua vez. Isso é o que faz valer todo o esforço de se priorizar todos os processos. Essa característica é o que leva o algoritmo a ser chamado de preemptivo (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Múltiplas filas Uma evolução dos algoritmos FIFO e por prioridade é tratar tipos de processos em diferentes filas. Exemplo: processos de foreground (de primeiro plano para o usuário) e de background (segundo plano). Pode-se dizer que a fila de foreground recebe 60% do tempo de processador, enquanto a fila background recebe 40%, para que o usuário tenha mais sensação de continuidade dos processos de interface, fazendo com que não aconteça travamentos. Outra forma é dizer que processos de background só executam quando a fila de foreground está vazia (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Os algoritmos anteriores podem ser aplicados para cuidar da política de escalonamento dentro de uma mesma fila. Exemplo: na fila foreground os processos são ordenados por prioridade, enquanto que na fila background os processos são executados em fila FIFO. Outra forma é dividir o processador em fatias de tempo, que será abordada a seguir. Round robin Também conhecido por escalonamento por fatia de tempo. Neste método define-se um tempo máximo para que um processo fique no processador, chamado de quantum, e organizase os processos em uma fila FIFO. Quando esse tempo termina, o escalonador tira o processo do processador, o coloca no final da fila, e seleciona o próximo da fila para ser executado. Quando um processo para de ser executado por uma chamada de sistema, o escalonador 39 apenas seleciona o próximo processo apto da fila (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Uma característica desse algoritmo é ser bastante justo pois onde todos os processos ganham a mesma chance de processamento. Com isso, não há a possibilidade de postergação indefinida. A fatia de tempo de execução é controlada por um relógio de tempo real implementado em hardware. Quando o tempo de execução termina, esse relógio causa uma interrupção de alta prioridade e força o processador a carregar o escalonador de processos já em modo supervisor (OLIVEIRA; TOSCANI; CARISSIMI, 2010). A grande atenção nesse algoritmo é o tamanho do quantum. Caso esse seja muito grande, a ponto de todos os processos conseguirem executar por completo na primeira rodada, produz o mesmo resultado que uma FIFO. Caso este seja muito pequeno, igual ou menor do que o tempo de chaveamento de contexto, sua eficiência cairia muito, tornando o processador ocupado em mais da metade do tempo com a troca de processos (OLIVEIRA; TOSCANI; CARISSIMI, 2010). Quando se procura mais eficiência, aumenta-se o quantum, e quando se precisa de mais paralelismo, diminui-se o quantum, cuidando-se com os limites citados acima. A Figura 12 ilustra como os processos seriam executados no processador, ignorandose o tempo das trocas de processos. Figura 12. Ilustração de execução de processos usando Round Robin 40 Fonte: Adaptado de OLIVEIRA; TOSCANI; CARISSIMI (2010). Por ser um equilíbrio entre eficiência e simplicidade, este algoritmo foi escolhido para implementar o escalonador de processos do PIqueno. 2.3 CONSTRUTIVISMO Para se falar de construtivismo tem-se que ter em mente a palavra “ideia”. Tanto nos aspectos sociais como nos aspectos cognitivos, o que sustenta o indivíduo é a ideia formada a partir da interação dos aspectos sociais e cognitivos: o homem não é um mero produto do ambiente. O homem é formado pelas relações de interação entre esses dois fatores – social e cognitivo. Assim, “o conhecimento não é uma cópia da realidade, mas, sim, uma construção do ser humano.” (CARRETERO, 1997, p. 10); essa construção a que se refere o autor nada mais é do que a representação que temos da nova informação interagindo com a atividade que realizaremos sobre essa nova informação. Precedida pelas teorias empirismo de Skinner – em que encontramos a Psicologia behaviorista (ou do comportamento) – e da psicologia Gestalt (ou da forma), a teoria construtivismo se apresenta com base na construção e na interação: o conhecimento não se centraliza no sujeito nem no objeto/informação, mas, sobre tudo, na construção e na interação do sujeito com o objeto. Na psicologia behaviorista o conhecimento é a relação do objeto com o sujeito, tendo o objeto supremacia sobre o sujeito; já na Psicologia Gestalt o conhecimento é o resultado da interação do sujeito com o objeto, tendo supremacia do sujeito sobre o objeto. Para Piaget o conhecimento é a interação do sujeito com o objeto sem, contudo, que haja supremacia de um sobre o outro (FRANCO, 1995). O objeto e o sujeito não são a condição fundamental para a existência do conhecimento, mas no “concurso da ação que se instalará a diferenciação entre o sujeito que conhece e o objeto a ser conhecido.” (FRANCO, 1995). 41 Assim, Piaget considera que o conhecimento surge da ação, ou seja, de uma ação que o sujeito exerce sobre o objeto. Esta ação não se configura como necessariamente prática, mas também mental. Para Vargas (2008), a interiorização das informações se processa de maneira diferente para cada indivíduo; dependendo da experiência ou situação em que se deu esse processo, ele será arquivado. Assim, “cada ser humano vai tecendo sua rede de informações” (Vargas 2008, p.32) que formará o seu conhecimento prévio. Segundo Smith (1989), O que possuímos em nossas cabeças é uma teoria de como é o mundo, uma teoria que é a base de todas as nossas percepções e compreensão do mundo, a raiz de todo aprendizado, a fonte de esperanças e medo, motivos e expectativas, raciocínio e criatividade. E esta teoria é tudo que temos. Se podemos extrair sentido do mundo, isto ocorre devido à interpretação de nossas interações com o mundo, à luz de nossa teoria (SMITH, 1989, p. 22). A teoria a que se refere Smith nada mais é do que o conjunto de tudo que já aprendemos, de todos os conceitos, informações do aspecto cognitivo e motor, que se encontram armazenadas numa estrutura mental a que os estudiosos da mente chamam de memória de longa duração. A interpretação de interações, como coloca Smith, reforça a ideia postulada pelo construtivismo de que o conhecimento de um indivíduo não representa uma realidade apenas copiada, mas sobre tudo interpretada, construída por este indivíduo. Esta interpretação a que nos referimos, referenciando Smith, é resultado da interação do indivíduo com o momento da aprendizagem, levando-se em conta o contexto situacional desse momento. Desta forma, “a memória é a reprodução mental das experiências captadas pelo corpo por meio de movimentos e dos sentidos. Essas representações são evocadas na hora de executar atividades, tomar decisões e resolver problemas, na escola e na vida” (LIMA, 2005, p.52) 42 A interpretação ocorrida no momento da aprendizagem acontece diariamente, sempre que estejamos em contato com o novo conhecimento, seja realmente aprendendo ou apenas reformulando conceitos aprendidos anteriormente. Assim, na teoria construtivista o indivíduo “aprende partindo de suas próprias ações em relação ao mundo que o cerca, ou seja, ele é capaz de interagir sobre as informações que está interiorizando” (VARGAS, 2008, p.54). Essa interação nada mais é do que o indivíduo refletindo sobre sua aprendizagem no processo de aprendizagem. A reflexão da aprendizagem se dá no momento em que, ao iniciar o processo de aprendizagem, o conhecimento prévio do indivíduo entra em ação juntamente com o conhecimento novo. Nessa relação entre conhecimento prévio e conhecimento novo “Não podemos esquecer que o elo significativo é o princípio fundamental da reestruturação da rede de conhecimentos” (VARGAS, 2008, p. 54, grifo nosso), ou seja, a aprendizagem. Entende-se como elo significativo toda ligação de uma informação com outra através de uma relação semântica, significativa, relacionada. Aqui convém falar-se das redes de aprendizagem. Na representação mental de tudo que se aprende, formam-se redes; essas redes representam todas as informações que já armazenamos e cada nó que liga uma ponta da rede à outra é o elo significativo que os relacionam e que pode ser diferente para cada indivíduo, pois depende do contexto situacional da aprendizagem e também de toda a estrutura cognitiva de cada um. Assim, se o processo de aprendizagem ocorre porque temos uma estrutura cognitiva ou conhecimento prévio aliado a uma capacidade de percepção da realidade, do mundo exterior; assim, podemos dizer que a aprendizagem depende mais do próprio sujeito (do que lhe é interior) do que do que lhe é apresentado na situação de aprendizagem. Piaget explica, então, a aprendizagem através da interação do conhecimento que está no sujeito (de que trata a teoria Gestalt) com o objeto de aprendizagem (de que trata a teoria Behaviorista). Assim, com sua estrutura cognitiva o indivíduo segue aprendendo partindo de suas próprias ações em relação às situações que o cercam, ou seja, com suas informações já armazenadas é capaz de interagir com as novas, reformulando suas redes de conhecimento. 43 Piaget conclui que o conhecimento se dá através dessa ação; ele não só acontece da ação, como sempre será uma ação, mesmo que esta ação seja somente a nível mental. Assim o conhecimento por mais teórico que seja resulta de uma ação do indivíduo sobre o objeto de aprendizagem. Desta forma, através desta interação o indivíduo vai construindo seu conhecimento, sua estrutura cognitiva - daí a teoria de Piaget chamar-se “Construtivismo”. Para Franco, (...) se encararmos o construtivismo como uma “teoria”, ele se esvaziará e não trará novidades. Mas se ele se tornar um instrumento para ajudar o professor a entender a realidade do seu aluno, e a partir deste entendimento ele passar a criar modos (métodos e técnicas) de agir em sala de aula, então sim estaremos resgatando a “Teoria”. E mais ainda, devolvendo à Pedagogia o caráter científico que muitas vezes foi negligenciado nesta área do conhecimento (FRANCO, 1995, p. 15). Essa teoria inspirou o desenvolvimento deste projeto. O PIqueno possibilitará uma interação em que, tanto o erro como o acerto fazem parte de todo o processo de aprendizado. Para Piaget o erro é algo tão positivo tanto quanto o acerto, pois ele acredita que ambos levam o indivíduo a elaboração de hipóteses a respeito da realidade que está vivendo naquele momento e, assim, usando seus próprios procedimentos para experimentá-las ou testá-las. Entende-se também que no indivíduo em situação de aprendizagem, (...) o erro (...) pode ser resultado de sua própria atividade assimilativa, da aplicação dos seus esquemas mentais (ou de ação) a determinado objeto ou conteúdo. Quando a atividade assimilativa resulta em erro, e principalmente se de forma repetida, ocorre uma desequilibração das estruturas cognitivas (...). Isso faz com que ela, por meio da atividade cognitiva, modifique seus esquemas, o que resulta em uma reequilibração e, portanto, no aperfeiçoamento de sua maneira de agir e de pensar e em um nível mais complexo de conhecimento sobre o objeto (FONTANA, 1996, p. 71). 44 É nesta interação de erros e acertos, no processo em que o aluno usará tanto seu conhecimento prévio quanto a situação de aprendizagem a ele apresentada, que o PIqueno terá seu espaço, otimizando o processo educacional de sistemas operacionais. Entende-se que como se vai ensinar e o que se pretende ensinar dependerá das habilidades e conhecimento com que serão construídas as atividades programadas. Para Bruner (1975, p.79) “a arte de programar a máquina é, naturalmente, uma extensão da arte de ensinar”. A utilização dos recursos e dispositivos utilizados harmonicamente num sistema é o maior desafio do professor na organização do processo do ensino e aprendizagem, em que atuará somente como mediador. Isso quer dizer que o professor oferecerá o apoio, conduzindo o processo através da sua instrução, ou seja, que ele considere o conhecimento prévio do aluno, incentivando-o e proporcionando atividades que o levem a novas descobertas. Essa relação de instrução e apoio, ou mediação de Piaget, é colocada por Bruner (1986: 74) como Instructional Scaffolding. 45 3 DESENVOLVIMENTO As características do PIqueno, como linguagem de programação e plataforma, foram escolhidas com base em uma análise de similares. Com base nos resultados obtidos e exibidos na Tabela 1, observou-se uma lacuna na arquitetura ARM e com baixo grau de complexidade. É nesta que o PIqueno foi projetado. Nome MINIX MikeOS KolibriOS MenuetOS Pintos FreeNOS PIqueno Plataforma x86/ARM x86 x86 x86 x86 x86 ARM Linguagem Assembly/C Assembly Assembly Assembly Assembly/C Assembly/C++ Assembly/C Linhas de código 6.000 9.606 128.000 36.000 30.000 2.000 1.500 Escalonador Multi Priority Round Robin Multi Priority Round Robin Round Robin Round Robin Round Robin Propósito Acadêmico Open Source Open Source Acadêmico Experimental Acadêmico Tabela 1. Tabela comparativa de sistemas operacionais semelhantes e o PIqueno A arquitetura ARM foi escolhida por ser razoavelmente mais simples e barata do que x86. Fazê-lo em arquitetura x86 incentivaria indiretamente os alunos a testarem suas modificações em computadores de laboratório, o que não era a intenção deste projeto. O Raspberry Pi foi escolhido por ter propósitos muito semelhantes ao deste projeto, a aprendizagem e ensino. Além de possuir uma arquitetura ARM, este é relativamente barato em relação a outros computadores, custando 35 Dólares no Reino Unido e 150 Reais no Brasil (em meados de maio de 2013). O projeto original previa somente a utilização da linguagem Assembly, porém, detectou-se que a toolchain gcc-arm-linux-gnueabihf poderia ser útil ao compilar códigofonte em C para um arquivo binário do ARM, através de uma plataforma x86. Isso permitiu com que a maior parte do código-fonte do PIqueno fosse feito em C, com o objetivo de tornalo mais claro para entendimento. Para potencializar o sucesso de aprendizagem, seu código fonte foi feito para ser mais legível do que otimizado, tendo as partes que envolvem o escalonador de processos bem comentadas. O código-fonte foi desenvolvido e testado em ambiente Linux (Ubuntu 13.04), utilizando o editor Geany, que possui realce de sintaxe para linguagens de programação e pode ser encontrado no repositório oficial do Ubuntu. O mesmo pode ser editado em qualquer editor ou IDE. 46 A compilação do código-fonte foi feito através do comando make, que lê as configurações dos arquivos Makefile e linkscript e faz a compilação utilizando a toolchain personalizada para ARM. O código-fonte compilado tem como resultado o arquivo kernel.img. Para depuração foi utilizado um fork do qemu (software de virtualização), com configurações especiais para o Raspberry Pi, disponível em https://github.com/Torlus/qemu/tree/rpi, em conjunto com a ferramenta gdb, disponível no repositório do Ubuntu. O qemu pode ser carregado com o arquivo kernel.img, e assim executado virtualmente. Uma vez em execução, pode-se utilizar a ferramenta gdb para configurar breakpoints e controlar a execução do PIqueno no qemu. Isso torna o processo de programação mais ágil. Para testes reais, o arquivo kernel.img foi colocado na partição de boot do cartão de memória e executado no próprio Raspberry Pi, ligado a uma televisão. O código-fonte do PIqueno foi disponibilizado no GitHub, onde pode ser obtido através da URL https://github.com/danielbathke/PIqueno. Com o código-fonte completo, o professor poderá utilizar a estratégia pedagógica Instructional Scaffolding, mostrando primeiro o PIqueno completo, funcional e navegando sobre a estrutura dos arquivos de código-fonte. Posteriormente, o mesmo pode propor uma atividade para reimplementar um pedaço simples à sua escolha, utilizando outro algoritmo. Um bom exemplo seria reimplementar o escalonador de processos utilizando outro algoritmo além do Round Robin. Após essa primeira atividade, uma outra semelhante pode ser proposta, mas agora em uma parte de maior complexidade. Esse ciclo poderá se repetir conforme o objetivo do professor. Para alcançar o objetivo de ensino no conceito de escalonador de processos, foi desenvolvido os procedimentos de inicialização, uma interface gráfica simples, a organização básica de um processo e um escalonador de processos. O algoritmo escolhido foi o Round Robin, por ser eficiente e ao mesmo tempo simples. Pensando na facilidade de comparação de resultados feita pelos alunos, depois de modificarem o PIqueno, identificou-se a necessidade de prover algumas informações em 47 tempo de execução. Então foram programadas saídas na interface gráfica, exibindo informações a cada troca de processo em execução. 3.1 DIAGRAMA DE CASOS DE USO A Figura 13 ilustra o diagrama de casos de uso do Aluno quanto ao PIqueno. Figura 13. Diagrama de Casos de Uso Conforme ilustrado na Figura 13, há cinco casos de uso previstos para o PIqueno. O primeiro é aquele onde o Aluno realiza o download do código-fonte do sistema operacional. O segundo prevê que o Aluno fará a modificação do código-fonte do sistema utilizando uma IDE, visando alcançar objetivos propostos em atividades na disciplina. Em seguida, o terceiro caso de uso é o momento em que o Aluno recompila o códigofonte, através da mesma IDE, para gerar os arquivos executáveis e poder testar suas 48 modificações. Após recompilar, os executáveis são copiados para o cartão de memória, que será inserido no Raspberry Pi. Após inserir o cartão de memória, o Aluno poderá ligar o minicomputador para dar início ao processo de boot no PIqueno, caracterizado como o quarto caso de uso. Por último, com o PIqueno em execução, o aluno verá seus processos exemplo sendo executados, visualizando cada mudança de processo do processador até que terminem-se todos. Essa visualização será a parte essencial para que o aluno veja na prática as consequências de suas mudanças no código. 3.2 REQUISITOS Este trabalho possui 5 requisitos não funcionais e 10 requisitos funcionais. 3.2.1 Requisitos não funcionais 1. O PIqueno deverá ser implementado em Assembly e C; 2. O PIqueno deverá ser implementado na plataforma Raspberry Pi; 3. O PIqueno deverá ter seu código-fonte aberto; 4. O código-fonte deverá ser publicado no GitHub; 5. O código-fonte deverá ter todas as suas partes essenciais comentadas. 3.2.2 Requisitos funcionais 1. O usuário deverá conseguir fazer o boot no Raspberry Pi com um cartão de memória; 2. O usuário deverá ter um feedback visual durante processo de boot; 3. O PIqueno deverá organizar os processos em uma árvore; 4. O PIqueno deverá possuir um escalonador de processos; 5. O escalonador de processos deverá utilizar o algoritmo Round Robin; 6. O usuário deverá visualizar a execução dos processos na tela; 7. O PIqueno deverá permitir a execução de processos simultâneos; 8. O PIqueno deverá possuir 3 aplicativos de exemplo para testes do usuário. 3.3 DIAGRAMA DE DOMÍNIO 49 A Figura 14 ilustra o Diagrama de Domínio do PIqueno. Figura 14. Diagrama de Domínio do PIqueno O PIqueno seguirá os conceitos consolidados de sistemas operacionais atuais, tendo em sua organização partes como Scheduler, Memory, Process, Framebuffer, Interrupts e Syscall. O código fonte dessas partes, bem como seus nomes, serão mantidos em Inglês para incentivar o uso do Inglês instrumental do aluno. A parte inicial do PIqueno, chamada de start, é desenvolvida em Assembly, contendo partes essenciais de inicialização como inicializar as stacks de cada modo do processador, ligar o acesso à memória e invocar o procedimento initsys, como visto no Quadro 1. # start.s .global _start: _start 50 mov r4, #0x80000000 // Configuring stack to every cpu mode that we will use cps #0x13 /* Change to supervisor (SVC) mode */ add sp, r4, #0x2400 cps #0x17 /* Change to Abort mode */ add sp, r4, #0x2800 cps #0x12 /* Change to IRQ mode */ add sp, r4, #0x2c00 cps #0x1f /* Change to system mode */ add sp, r4, #0x3c00 /* Turn on unaligned mrc p15, #0, r4, c1, orr r4, #0x400000 /* mcr p15, #0, r4, c1, memory access */ c0, #0 1<22 */ c0, #0 /* Jump to memory map initialisation code */ b initsys Quadro 1. Código de inicialização A base dos endereços das stacks é o endereço 0x80000000, configurada no registrador r4. À partir daí, configura-se cada stack alterando o modo do processador com o comando cps e somando o endereço base (r4) com o endereço da stack. O comando b faz com que o processador salte para o procedimento initsys, feito em C, que termina de configurar um gerenciamento básico da memória, se caracterizando como boot. Após isso, invoca-se o procedimento Main. O procedimento Main, já em um nível de abstração maior, inicializa o vetor de interrupções (Interrupts), o Framebuffer (espaço da memória reservado para o dispositivo de vídeo) e a Memory (Memória). Após essas inicializações, são registrados na árvore de processos o processo pai, referente ao procedimento Main, e os processos filhos que são os processos exemplo. Em seguida, com toda organização feita, o timer do processador é inicializado e as interrupções ligadas, fazendo com que os processos comecem a ser alternados no processador. O procedimento está demonstrado no Quadro 2. # main.c ... void main(unsigned int r0, unsigned int machtype, unsigned int atagsaddr) { // Initialise basic memory management 51 mem_init(); // Initialize led management led_init(); // Initialize framebuffer (video reserved memory location) fb_init(); // First boot message console_write("Welcome to PIqueno\n\n"); // Creating my process in the process table create_main_process(); // Create three sample processes with name and address of function fork("Sample process 1", &sample_process_1); fork("Sample process 2", &sample_process_2); fork("Sample process 3", &sample_process_2); // Configuring iterrupts interrupts_init(); // Do nothing and wait for scheduler to stop executing me main_endloop(); } ... Quadro 2. Procedimento Main A função fb_init inicializa o espaço da memória reservado para o framebuffer, que é lido pelo GPU. Este é responsável por gerar o sinal de vídeo através da porta HDMI. Depois disso, pode-se gerar mensagens na tela através da função console_write. O procedimento create_main_process é responsável por gerar um processo para o programa que se está executando no momento, o Main, conforme visto no Quadro 3. # scheduler.c ... // Function to create the main process void create_main_process() { // Creating process process main_process; main_process.id = process_count; main_process.name = "Main"; main_process.pc = &main_endloop; main_process.times_loaded = 1; main_process.status = PROCESS_STATUS_ZOMBIE; 52 // Gets the actual stack address unsigned int stack_pointer; asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) ); // Use it as base address processes stack pointer stack_base = stack_pointer; // Output the stack address console_write("Main stack is 0x"); console_write(tohex(stack_pointer, 4)); console_write("\n"); // Saving the process in the process table process_list[process_count] = main_process; // Set it as the current active process active_process_index = process_count; // Increments the process counter process_count++; } ... Quadro 3. Procedimento create_main_process O procedimento cria um novo process (Quadro 4) e o salva no array de processos. Nota-se que este processo é criado com o status PROCESS_STATUS_ZOMBIE, que indica que este processo não deve mais ser colocado em execução, mas que não pode terminar completamente pois ainda terá processos filhos aptos a executarem. Isso serve para que os processos seguintes possam ser gerados e associados com um processo pai. # process.h ... typedef struct { unsigned int char * unsigned int unsigned long unsigned long unsigned int unsigned int id; name; parent; stack_pointer; pc; times_loaded; status; } process; ... Quadro 4. Definição da struct process 53 A função fork cria novos processos filhos do processo que a chama. Esta tem dois parâmetros, o nome do processo e o endereço de memória em que o seu código está. Esta funciona semelhante a função create_main_process, com algumas diferenças. A principal delas é o status do novo processo, que recebe o valor PROCESS_STATUS_WAITING, informando ao escalonador que este processo está apto para ser executado, esperando sua vez para que o processador o execute. Quando os processos terminam de executar, eles invocam uma Syscall, através de uma software interruption (interrupção de software) com o comando SWI (Quadro 5), para que sejam marcados com estado de termino. Nesse estado, o processo é ignorado pelo escalonador de processos. # process.c (com processador no modo user) ... // Just some counting for easy debug on the screen. Simulate user process. void sample_process_2() { console_write("Starting process 2 "); int to = 300; int i, j; for (i=0; i<to; i++) { console_write(todec(i,0)); console_write(" "); for (j=0; j<to*to; j++) { asm volatile("NOP"); } } // Call software interrupt #0 (terminate) asm volatile("SWI #0"); } ... # syscall.c (já com processador em modo system) ... switch (swi) { case SYSCALL_TERMINATE_PROCESS: console_write("Invoking syscall terminate_process()"); console_write("\n"); 54 // Calling the right intra-kernel procedure terminate_process(); break; } ... Quadro 5. Processo invocando uma Syscall para término Analisando novamente o código do procedimento Main, encontra-se a função interrupts_init, que configura o vetor de interrupções do processador (Quadro 6). Esta contém endereços de procedimentos para cada tipo de interrupção, dentre elas a interrupção por IRQ e a interrupção por software, vista previamente. # interrupts.c ... void interrupts_init(void) { // Set interrupt base register asm volatile("mcr p15, 0, %[addr], c12, c0, 0" : : [addr] "r" (&interrupt_vectors)); // Turn on interrupts asm volatile("cpsie i"); // Enable ARM timer IRQ *irqEnableBasic = 0x00000001; // Interrupt every x * 256 (prescaler) timer ticks *armTimerLoad = 0x00000400; // Timer enabled *armTimerControl = 0x000000aa; } ... __attribute__ ((naked, aligned(32))) static void interrupt_vectors(void) { asm volatile("b bad_exception\n" // Reset "b bad_exception\n" // Undefined "b interrupt_swi\n" // Software interrupt "b interrupt_prefetch_abort \n" // Prefech abort interrupt "b interrupt_data_abort \n" // Data abort interrupt "b bad_exception;\n" // Unused vector "b interrupt_irq \n" // IRQ interrupt "b bad_exception\n" // Fast Interrupt (FIQ), not used ); 55 } ... Quadro 6. Função interrupts_init configurando vetor de interrupções Após a criação do processo principal (create_main_process), da criação dos processos do usuário (fork) e do procedimento de inicialização de interrupções (interrupts_init), o sistema está pronto para começar a executar os processos do usuário. Nesse momento, o procedimento Main termina invocando a função main_endloop, que apenas aguardará a interrupção do timer para ser tirado de execução. Quando o timer expira, é gerada uma interrupção de IRQ. Em sistemas operacionais reais, as interrupções de IRQ podem ser geradas por vários dispositivos, como teclado e mouse por exemplo. No caso do PIqueno, o timer é o único dispositivo que gera uma interrupção de IRQ, então, dados os tratamentos iniciais para salvar os registradores na stack do processo atual, invoca-se o escalonador através da função schedule_timeout, visto no Quadro 7. # interrupts.c ... // IRQ __attribute__ ((interrupt ("IRQ"))) void interrupt_irq(void) { // This function starts on IRQ mode // Push all registers into the IRQ mode stack (R13) asm volatile("push {R0-R12}"); // Put LR register of IRQ mode (PC of interrupted process) on R0 asm volatile("MOV R0, LR"); // Change to system mode asm volatile("cps #0x1f"); // Push R0 (interrupted PC) to the system mode stack asm volatile("push {R0}"); // Return to IRQ mode asm volatile("cps #0x12"); // Pop all registers again asm volatile("pop {R0-R12}"); 56 // Return to system mode asm volatile("cps #0x1f"); // Push all registers into the system mode stack asm volatile("push {R0-R12}"); // Push the interrupted LR to system mode stack asm volatile("push {LR}"); // Copy the processor status to R0 asm volatile("MRS R0, SPSR"); // Push the processor status to system mode stack asm volatile("push {R0}"); // Return to IRQ mode asm volatile("cps #0x12"); // Copy LR to R0 asm volatile("MOV R0, LR"); // Back to system mode asm volatile("cps #0x1f"); unsigned long pc; unsigned long stack_pointer; // Getting pc and stack just to debug asm volatile ("MOV %0, R0\n\t" : "=r" (pc) ); asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) ); // Invert led to inform context switch activity led_invert(); // Jump to scheduler to do the context switch schedule_timeout(stack_pointer, pc); } ... Quadro 7. Tratamento de uma interrupção de IRQ invocando o escalonador Ao ser invocado, o escalonador salva a stack e o pc do processo atual, mudando seu status para PROCESS_STATUS_WAITING novamente. Esta é a parte em comum entre a maioria dos algoritmos de escalonador. Posteriormente, já observando-se as características do algoritmo Round Robin, seleciona-se o próximo processo da lista apto para execução. Incrementa-se sua variável times_loaded, para estatísticas, muda-se o status para PROCESS_STATUS_RUNNING, e por fim recupera-se os valores da stack e do pc com o comando pop, gerando um branch automático para o novo valor do pc. Este procedimento está exemplificado no Quadro 8. 57 # interrupts.c ... void schedule_timeout(unsigned long stack_pointer, unsigned long pc) { // Saving stack and pc process_list[active_process_index].stack_pointer = stack_pointer; process_list[active_process_index].pc = pc; // Updating process status to waiting if (process_list[active_process_index].status == PROCESS_STATUS_RUNNING) { process_list[active_process_index].status = PROCESS_STATUS_WAITING; } ... // Get next process id int next_process = next_waiting_process_index(); // If -1, halt if (next_process < 0) { console_write("No more waiting processes, halting."); halt(); } // Updating next running process active_process_index = next_process; // Increasing statistics, updating status to running process_list[active_process_index].times_loaded++; process_list[active_process_index].status = PROCESS_STATUS_RUNNING; ... // Assembly to load the sp with the new process stack address asm volatile("MOV SP, %[addr]" : : [addr] "r" ((unsigned long )(process_list[active_process_index].stack_pointer)) ); // If its is not the first time if (process_list[active_process_index].times_loaded > 1) { timer_reset(); // Pops registers asm volatile("pop asm volatile("MSR asm volatile("pop asm volatile("pop from the stack {R0}"); SPSR_cxsf, R0"); {LR}"); {R0 - R12}"); // Turn on interrupt again asm volatile("cpsie i"); // Pops the last register into PC to resume processing asm volatile("pop {PC}"); } else { // Push the first pc address into the stack 58 unsigned long addr = (unsigned long )(process_list[active_process_index].pc); asm volatile("MOV R0, %[addr]" : : [addr] "r" (addr) ); asm volatile("push {R0}"); timer_reset(); // Turn on interrupt again asm volatile("cpsie i"); // Pops the last register into PC to resume processing asm volatile("pop {PC}"); } } ... Quadro 8. Escalonador trocando processos do processador Se o escalonador não encontrar mais processos aptos para execução, o sistema é interrompido com o comando Halt do processador. 3.4 CENÁRIO DE UTILIZAÇÃO EM SALA DE AULA Para uso em sala de aula, criou-se um cenário baseado na teoria de Bruner, Instructional Scaffolding. O professor dará uma solução parcialmente pronta, e solicitará aos alunos que a melhorem de acordo com a teoria, mostrando seus resultados na prática. O PIqueno seria a solução parcial, com o quantum do escalonador configurado para um grande valor, onde todos os processos do usuário consigam ser executados passando apenas uma vez pelo processador. Após demonstrar a solução funcionando, bem como explicar suas saídas na tela, o professor comentaria sobre a falha em seu estado atual e qual seria o comportamento esperado. A atividade proposta seria ajustar o escalonador. Sem mencionar o que esta errado, ou aonde, espera-se que os alunos explorem o código fonte, conhecendo-o e tendo um contato maior com a programação de um sistema operacional. O processo de evolução conta com alguns testes, e possivelmente alguns erros. Estes erros são importantes no processo de aprendizado, onde o aluno constrói seu conhecimento com base em experiências práticas. 59 A solução adequada para este problema seria ajustar o quantum para um valor equilibrado. Nem tão pequeno a ponto de retardar muito a execução dos processos, nem tão alta causando sua execução em 1 ou poucas passagens pelo processador. Uma próxima atividade, mais complexa que a primeira, seria trocar o algoritmo do escalonador. Poderia-se propor o desenvolvimento do algoritmo de escalonamento por prioridade, onde alguns tipos de processo devem receber mais vezes o processador e serem terminados antes dos processos não-prioritários. Esta atividade requer um nível de conhecimento mais aprofundado do código-fonte, que a primeira atividade pode proporcionar. Este é apenas um cenário dos vários possíveis com o PIqueno em seu estado atual. Essas possibilidades podem aumentar ainda mais com o desenvolvimento contínuo do PIqueno em outros trabalhos técnicos de conclusão de curso. 60 4 CONSIDERAÇÕES FINAIS Este trabalho teve por objetivo construir um sistema operacional simples o suficiente para ser estudado em sala de aula. Trata-se do PIqueno, e com este espera-se diminuir a lacuna entre a teoria e a prática na disciplina de sistemas operacionais nos cursos de Ciência da Computação e Engenharia da Computação da Universidade do Vale do Itajaí. Para que os objetivos estipulados fossem alcansados, foi preciso resgatar os conceitos de sistemas operacionais previamente estudados, para posteriormente eleger quais conceitos iniciais seriam abordados nesta primeira versão. O conceito eleito foi o escalonador de processos. Acredita-se que este conceito, em conjunto com outros dependentes deste, sejam os mais eficientes para o propósito deste trabalho. Todas as ferramentas utilizadas neste projeto são livres, o que pode contribuir para experiências não somente em sala de aula, mas em casa. Os resultados obtidos não só foram satisfatorios, mas também são prova de que é possível estimular o desenvolvimento de um sistema operacional em sala de aula, e com isso elevar a eficiência de ensino e desmistificar os conceitos teóricos da discipina. Não é objetivo do PIqueno ser um sistema operacional para utilização em situações reais. Por esse motivo o PIqueno não foi concebido com otimizações de código-fonte, de desempenho, ou ainda com uma larga escala de hardwares compatíveis. 4.1 TRABALHOS FUTUROS Um trabalho técnico científico dispõe de pouco tempo para se construir um sistema operacional completo. Por este motivo, a primeira versão do PIqueno se limita a exemplificar o conceito de escalonador de processos, e por consequência a estrutura básica de um processo, chaveamento de contexto, interrupção proveniente do timer e uma chamada de sistema. Isso deixa uma grande abertura para trabalhos futuros, que podem tanto ser implementados durante a disciplina ou como trabalhos de conclusão de curso. De fato, acredita-se que este trabalho inicial trará novos entusiastas para encarar o desafio de continualo. 61 Serão descritas brevemente as possibilidades para trabalhos futuros levantadas neste ponto do projeto, em uma lista de sugestões de ordem baseada em dependências. 4.1.1 Drivers para dispositivos de E/S Uma das principais tarefas de um computador é ler e escrever em dispositivos de entrada e saída, sendo para interagir com o usuário ou para tratar outros dispositivos. Sugere-se que seja iniciado pelo desenvolvimento de drivers para acesso ao leitor de cartão de memória, teclado e mouse. Posteriormente, pode-se extender essa funcionalidade para dispositivos de armazenamento em massa ligados na porta USB. Isso abrirá a gama de possibilidades de desenvolvimento para o PIqueno. Uma vez feito isso, o PIqueno começaria a se tornar mais útil para o usuário. 4.1.2 Sistema de arquivos Para que o sistema operacional possa ler arquivos e executar tarefas do usuário efetivamente, precisa-se que seja definido e desenvolvido uma camada de compatibilidade com algum sistema de arquivos já conhecido. Isso irá permitir por exemplo, que as tarefas do usuário envolvam leitura e escrita em arquivos. Para manter uma boa compatibilidade com outros sistemas operacionais, recomendase que seja implementado no PIqueno o sistema de arquivos FAT32, reconhecido hoje por todos os sistemas operacionais modernos. Posteriormente, pode-se desenvolver o reconhecimento aos sistemas de arquivos NTFS, EXT2 e EXT3, pensando na evolução do gerenciamento de permissões e segurança. Esse passo faz-se necessário para que posteriormente possa ser carregado um programa do usuário à partir de um arquivo. 4.1.3 Gerenciamento de E/S Trata-se de um aperfeiçoamento do passo anterior. Neste ponto, precisa-se gerenciar o acesso de entrada e saída através de várias Syscalls. Estas serão invocadas através de interrupções de software, de maneira que o programador de aplicativo não tenha acesso direto ao dispositivo de entrada e saída em questão. 62 Talvez ainda não seja possível retirar os aplicativos de usuário de dentro do kernel, pela falta de definição de arquivo binário. Porém, já seria de grande valia fazer com que as chamadas de leitura e escrita aconteçam única e exclusivamente através de Syscalls, e sendo atendidas pelo sistema operacional com processador em modo system. 4.1.4 Definição de arquivo binário Talvez um dos maiores passos para o PIqueno seja que o usuário consiga carregar e executar seus aplicativos à partir de arquivos binários. Isso resolve o problema de isolamento de baixo nível do kernel, fazendo com que os programas sejam carregados de dispositivos de armazenamento em massa. Isso permite que o usuário faça um programa em outro computador, o compile, copie no cartão de memória e execute no PIqueno. O papel do padrão do arquivo binário é determinar de que forma o programa já compilado será carregado na memória, para que posteriormente seja executado. O cabeçalho do arquivo define informações de quais bibliotecas externas serão utilizadas, por exemplo. Por questões de compatibilidade e utilização de um padrão já consolidado, recomendase que o padrão de arquivo binário seja o ELF, utilizado no Linux. 4.1.5 Compilador para o PIqueno Com o arquivo binário definido, pode-se criar uma ferramenta externa, para Windows ou Linux por exemplo, para escrever e compilar um programa em C e transforma-lo já em um arquivo binário do padrão do PIqueno, contendo informações em seu cabeçalho e o programa binário para ARM. Isso facilitaria a criação de ferramentas e outros aplicativos para o PIqueno. 4.1.6 Gerenciamento de memória O gerenciamento de memória utilizado atualmente não deve suprir todas as necessidades de um sistema operacional moderno. Com o foco em estudar um sistema operacional com conceitos modernos, o gerenciamento de memória deve ser atualizado. 63 Deve-se criar um controle de memória por processos, sendo que cada processo deve receber ou requerer um espaço exclusivo na memória. Os processos devem ser impedidos de invadir o espaço de outro processo, através de barreiras na memória. O processador ARM utilizado no Raspberry PI contém uma série de comandos para que esse isolamento seja possível. O espaço do kernel do sistema operacional também deverá ser isolado, não sendo acessado de nenhuma forma por processos do usuário, sendo acessado somente com o processador em modo system. 4.1.7 Interface de linha de comando A interface de linha de comando é a ferramenta que faltava para que o usuário possa utilizar o PIqueno de forma mais interativa, executando tarefas através de comandos digitados. Esta é comumente chamada de shell, que expõe os programas a serem executados para o usuário, e até mesmo chamadas de sistema. Recomenda-se o estudo em cima do padrão Bash, utilizado em sistemas operacionais baseados no Unix. 4.1.8 Driver para dispositivo de rede Trata-se de um conjunto de programas com a função de gerenciar o dispositivo de rede, possibilitando que o PIqueno possa se logar em uma rede, ganhar um endereço IP, e interagir com outros computadores. Sendo um driver, deve ser executado em modo system, não disponível para o usuário. Este deve entrar em execução logo na inicialização. Recomenda-se a criação de ferramentas para reiniciar a comunicação da rede cabeada, exibição de informações da rede, como endereço IP, mascara de rede e gateway. Primeiramente, pode-se trabalhar com configurações de rede fixas, definidas em arquivo. Posteriormente pode ser implementado o protocolo DHCP (dynamic host configuration protocol, protocolo dinâmico de configuração de hospedeiro), que visa obter as configurações de rede automaticamente, quando houver um servidor DHCP na rede. 64 4.1.9 Conjunto de ferramentas para execução em linha de comando Constitui-se de aplicativos que extenderão as capacidades do usuário quando utilizando a interface de linha de comando do PIqueno. Recomenda-se o estudo em cima dos comandos de sistemas baseados no padrão POSIX (portable operational system interface based on UNIX, Interface para sistemas operacionais portáveis baseados em UNIX). Dentre as tarefas possíveis, estariam a capacidade de (i) listar arquivos, (ii) criar arquivos, (iii) remover arquivos, (iv) criar diretórios, (v) remover diretórios, (vi) entrar em um diretório, (vii) sair de um diretório, (viii) listar processos em execução, (ix) exibir o conteúdo de um arquivo na tela, entre outros. 4.1.10 Gerenciamento de usuários O gerenciamento de usuários permite um nível maior de isolação de itens de segurança, como propriedade de arquivos, possíveis permissões, definição de um usuário administrador do computador. Também facilita a separação de arquivos, estimulando o uso do computador por mais de um usuário, simultaneamente ou não. A definição de um usuário administrador visa a proteção da integridade do sistema operacional, bem como suas configurações, para que fique o maior tempo possível apto a ser utilizado por todos os usuários. 4.1.11 Permissões Em conjunto com o gerenciamento de usuário e um sistema de arquivos mais recente, pode-se atribuir permissões para cada usuário. Tanto para arquivos, quanto para dispositivos que podem ser expostos ao usuário como um arquivo, pode-se aplicar o padrão de permissões POSIX. Nesse padrão, cada arquivo pode receber 3 octetos (4, 2 ou 1) de permissões, cada um referenciando a um grupo específico de usuários, bem como seu dono e seu grupo. 65 Dentro de um octeto, quando soma-se 4 refere-se a leitura, 2 refere-se a escrita e 1 refere-se a execução. Sendo assim, 7 significa permissão total, 6 permissão de leitura e escrita, 5 leitura e execução e 3 escrita e execução. O primeiro octeto refere-se as permissões do dono do arquivo. O segundo refere-se as permissões dos membros do mesmo grupo do arquivo. O último octeto descreve as permissões públicas. Desta forma, 777 significa permissão total para todos usuários, 700, permissão total somente para o dono, 711, permissão total para o dono e de execução para membros do mesmo grupo ou usuários públicos. 4.1.12 Interface gráfica A interface gráfica, já em um nível de maior maturidade do projeto, pode ser feita para agilizar a execução de tarefas pelos usuários, permitindo sua interação mais precisa com o mouse, a visualização de gráficos mais elaborados, imagens vídeos, etc. Cada gerenciador gráfico tem pelo menos um decorador, gerenciador de janelas, gerenciador de painéis, gerenciador de widgets e um gerenciador de login. O sucesso do PIqueno não depende deste passo do projeto, mas caso sua evolução ocorra em grande intensidade, pode-se pensar em implementar uma interface gráfica. 66 REFERÊNCIAS OLIVEIRA, R. S. de; TOSCANI, S. S.; CARISSIMI, A. da S. Sistemas Operacionais. 4.ed. Porto Alegre: Bookman, 2010. MAZIERO, C. Reflexões sobre o ensino prático de Sistemas Operacionais. In: Congresso da Sociedade Brasileira da Computação, 12., 2002, Florianópolis. Anais do X Workshop Sobre Educação em Computação da SBC. Florianópolis: Sociedade Brasideira de Computação, 2002. p. 1-12. TANEMBAUM, A. S. Sistemas Operacionais Modernos. 3.ed. São Paulo: Pearson Education do Brasil, 2010. HALFACREE, G.; UPTON, E. Meet the Raspberry Pi. 1.ed. Reino Unido: John Wiley & Sons, 2012. SMITH, F. Compreendendo a leitura. Porto Alegre: Artes Médicas, 1989. LIMA, E. S. É assim que se aprende. Nova Escola, São Paulo, v. 20, n. 179, p. 52-57, jan. 2005. VARGAS, E. S. M. Psicolinguística. Itajaí: UNIVALI, 2008. FRANCO, S. R. K. O Construtivismo e a educação. Porto Alegre: Mediação Editora, 1995. CARRETERO, M. Construtivismo e educação. Porto Alegre: ArtMed, 1997. FONTANA, R. C. Mediação pedagógica na sala de aula. São Paulo: Autores Associados, 1996. BRUNER, J. S. O processo da educação. 5.ed. São Paulo: Companhia Editora Nacional, 1975. ____________. Actual Minds, Possible Worlds. Cambridge: Harvard University Press, 1986. 67 APÊNDICE A. CÓDIGO FONTE # start.s .global _start _start: mov r4, #0x80000000 // Configuring stack to every cpu mode that we will use cps #0x13 /* Change to supervisor (SVC) mode */ add sp, r4, #0x2400 cps #0x17 /* Change to Abort mode */ add sp, r4, #0x2800 cps #0x12 /* Change to IRQ mode */ add sp, r4, #0x2c00 cps #0x1f /* Change to system mode */ add sp, r4, #0x3c00 /* Turn on unaligned mrc p15, #0, r4, c1, orr r4, #0x400000 /* mcr p15, #0, r4, c1, memory access */ c0, #0 1<22 */ c0, #0 /* Jump to memory map initialisation code */ b initsys 68 # initsys.c /* Virtual memory layout * * 0x00000000 - 0x7fffffff (0-2GB) = user process memory * 0x80000000 - 0xa0ffffff (2GB) = physical memory * includes peripherals at 0x20000000 - 0x20ffffff * 0xc0000000 - 0xefffffff = kernel heap/stack * 0xf0000000 - 0xffffffff = kernel code * * Memory from 0x80000000 upwards won't be accessible to user processes */ static unsigned int *initpagetable = (unsigned int * const)0x4000; /* 16K */ static unsigned int *kerneldatatable = (unsigned int * const)0x3c00; /* 1K */ /* initsys calls main() when it's finished, so we need to tell the compiler * it's an external symbol */ extern void main(void); /* Memory locations. Defined in linkscript, set during linking */ extern unsigned int _physdatastart, _physbssstart, _physbssend; extern unsigned int _kstart, _kend; __attribute__((naked)) void { register unsigned int register unsigned int register unsigned int register unsigned int initsys(void) x; pt_addr; control; *bss; /* Save r0-r2 as they contain the start values used by the kernel */ asm volatile("push {r0, r1, r2}"); /* The MMU has two translation tables. Table 0 covers the bottom * of the address space, from 0x00000000, and deals with between * 32MB to 4GB of the virtual address space. * Translation table 1 covers the rest of the memory. For now, * both tables are set to the same thing, and, by default, table * 0 manages the entire virtual address space. Later on, the * table 0 register will be pointed to process-specific tables for * each process's virtual memory */ /* * * * * * * * * * * * * * * Set up translation table ARM1176JZF-S manual, 6-39 The memory is divided in to 4096 1MB sections. Most of these are unmapped (resulting in prefetch/data aborts), except 0x8000000-0xa1000000, which are mapped to 0x00000000-0x2a000000 (physical memory and peripherals), and the kernel code and data Memory privilege is set by APX/AP bits (3 bits in total) APX is at bit 15 of the sector definition, while AP are at bits 10 and 11. APX AP Privileged 1 11 (0x8c00) = read-only 1 01 (0x8400) = read-only Unprivileged read-only no access 69 * 0 10 (0x0800) = read-write read-only * 0 01 (0x0400) = read-write no-access * * eXecute Never (XN) is at bit 4 (0x10) - sections with this flag * cannot be executes, even by privileged processor modes * * Bits 0 and 1 identify the table entry type * 0 or 3 = translation fault (3 is reserved and shouldn't be used) * 1 = course page table * 2 = section or supersection */ for(x=0; x<4096; x++) { if((x >= (0x80000000>>20)) && (x < (0xa1000000>>20))) { /* Map physical memory to virtual * Read/write for priviledged modes, no execute */ initpagetable[x] = (x-2048)<<20 | 0x0410 | 2; } else { /* No entry in table; translation fault */ initpagetable[x] = 0; } } /* Map 0x00000000-0x000fffff into virtual memory at the same address. * This is temporary: it's where the code is currently running. Once * initsys has branched tothe kernel at 0xf0000000, it will be removed * * Read/write for privileged mode only */ initpagetable[0] = 0<<20 | 0x0400 | 2; /* Map 1MB at 0xf00000000-0xf00fffff to the kernel code in memory. * Typically, this is loaded at 0x9000, and appears in virtual memory * from 0xf0009000. This means we can use a single 1MB entry to * cover it all (as long as the kernel doesn't get above 1MB...), * reducing the number of TLB entries * * Map as read-only for privileged modes only * * It will also map any free memory surrounding the kernel code (eg. * 0x00000000-0x8fffffff, kernel data). However, as this mapping is * read-only and only available to the kernel, the potential for harm * is minimal */ initpagetable[3840] = 0<<20 | 0x8400 | 2; /* 0xc0000000-0xc00fffff is mapped to physical memory by a course * page table * * This maps the kernel data from where it has been loaded in memory * (after the kernel code, eg. at 0x0001f000) to 0xc0000000 * Only memory in use is mapped (to the next 4K). The rest of the * table is unmapped. */ initpagetable[3072] = 1 | (unsigned int)kerneldatatable; 70 /* Populate kerneldatatable - see ARM1176JZF-S manual, 6-40 * * APX/AP bits for a page table entry are at bits 9 and 4&5. The * meaning is the same as for a section entry. * 0 01 (0x0010) = read-write privileded modes, no access otherwise * * bits 0 and 1 determine the page type: * 0 = unmapped, translation fault * 1 = large page (64K) (XN is bit 15) * 2 = small page (4K), executable (XN is bit 0) * 3 = small page (4K), not-executable (XN is bit 0) * * 256 entries, one for each 4KB in the 1MB covered by the table */ for(x=0; x<256; x++) { /* &_physbssend is the physical address of the end of the * kernel data - somewhere between 0x00009000 and 1MB (any * more than that and this code will need rewriting...) */ if(x <= ((unsigned int)&_physbssend >> 12)) kerneldatatable[x] = ((unsigned int)&_physdatastart + (x<<12)) | 0x0010 | 2; else kerneldatatable[x] = 0; } /* The .bss section is allocated in physical memory, but its contents * (all zeroes) are not loaded in with the kernel. * It needs to be zeroed before it can be used */ bss = &_physbssstart; while(bss<&_physbssend) { *bss = 0; bss++; } pt_addr = (unsigned int) initpagetable; /* Translation table 0 - ARM1176JZF-S manual, 3-57 */ asm volatile("mcr p15, 0, %[addr], c2, c0, 0" : : [addr] "r" (pt_addr)); /* Translation table 1 */ asm volatile("mcr p15, 0, %[addr], c2, c0, 1" : : [addr] "r" (pt_addr)); /* Use translation table 0 for everything, for now */ asm volatile("mcr p15, 0, %[n], c2, c0, 2" : : [n] "r" (0)); /* Set Domain 0 ACL to "Client", enforcing memory permissions * See ARM1176JZF-S manual, 3-64 * Every mapped section/page is in domain 0 */ asm volatile("mcr p15, 0, %[r], c3, c0, 0" : : [r] "r" (0x1)); /* Read control register */ asm volatile("mrc p15, 0, %[control], c1, c0, 0" : [control] "=r" (control)); /* Turn on MMU */ control |= 1; /* Enable ARMv6 MMU features (disable sub-page AP) */ 71 control |= (1<<23); /* Write value back to control register */ asm volatile("mcr p15, 0, %[control], c1, c0, 0" : : [control] "r" (control)); /* Set the LR (R14) to the address of main(), then pop off r0-r2 * before exiting this function (which doesn't store anything else * on the stack). The "mov lr" comes first as it's impossible to * guarantee the compiler wouldn't use one of r0-r2 for %[main] */ asm volatile("mov lr, %[main]" : : [main] "r" ((unsigned int)&main) ); asm volatile("pop {r0, r1, r2}"); asm volatile("bx lr"); } 72 # main.c #include #include #include #include #include #include #include #include #include "led.h" "framebuffer.h" "interrupts.h" "mailbox.h" "memory.h" "memutils.h" "textutils.h" "process.h" "scheduler.h" // Location of the initial page table in RAM static unsigned int *initpagetable = (unsigned int *) mem_p2v(0x4000); void main(unsigned int r0, unsigned int machtype, unsigned int atagsaddr) { // Initialise basic memory management mem_init(); // Initialize led management led_init(); // Initialize framebuffer (video reserved memory location) fb_init(); // First boot message console_write("Welcome to PIqueno\n\n"); // Creating my process in the process table create_main_process(); // Create three sample fork("Sample process fork("Sample process fork("Sample process processes with name and address of function 1", &sample_process_1); 2", &sample_process_2); 3", &sample_process_2); // Configuring iterrupts interrupts_init(); // Do nothing and wait for scheduler to stop executing me main_endloop(); } void main_endloop(void) { // Repeatedly halt the CPU and wait for interrupt halt(); } 73 # interrupts.h #ifndef INTERRUPTS_H #define INTERRUPTS_H extern void interrupts_init(void); #endif /* INTERRUPTS_H */ # interrups.c #include "interrupts.h" #include #include #include #include #include #include "framebuffer.h" "led.h" "memory.h" "textutils.h" "scheduler.h" "syscall.h" static volatile unsigned int *irqEnable1 = (unsigned int *) mem_p2v(0x2000b210); static volatile unsigned int *irqEnable2 = (unsigned int *) mem_p2v(0x2000b214); static volatile unsigned int *irqEnableBasic = (unsigned int *) mem_p2v(0x2000b218); static volatile unsigned mem_p2v(0x2000b400); static volatile unsigned mem_p2v(0x2000b404); static volatile unsigned mem_p2v(0x2000b408); static volatile unsigned mem_p2v(0x2000b40c); int *armTimerLoad = (unsigned int *) int *armTimerValue = (unsigned int *) int *armTimerControl = (unsigned int *) int *armTimerIRQClear = (unsigned int *) /* Interrupt vectors called by the CPU. Needs to be aligned to 32 bits as the * bottom 5 bits of the vector address as set in the control coprocessor must * be zero * * The RESET vector is set to bad_exception. On CPU reset the interrupt vectors * are set back to 0x00000000, so it won't be used. Any attempt to call this * vector is clearly an error. Also, resetting the Pi will reset VideoCore, * and reboot. */ __attribute__ ((naked, aligned(32))) static void interrupt_vectors(void) { asm volatile("b bad_exception\n" // Reset "b bad_exception\n" // Undefined "b interrupt_swi\n" // Software interrupt "b interrupt_prefetch_abort \n" // Prefech abort interrupt "b interrupt_data_abort \n" // Data abort interrupt "b bad_exception;\n" // Unused vector "b interrupt_irq \n" // IRQ interrupt "b bad_exception\n" // Fast Interrupt (FIQ), not used 74 ); } // Unhandled exceptions, halt __attribute__ ((naked)) void bad_exception(void) { console_write("Bad exception. System halted."); while(1); } // Software interrupt __attribute__ ((interrupt ("SWI"))) void interrupt_swi(void) { unsigned int addr; unsigned int swi; // Read link register into addr - contains the address of the instruction after the SWI asm volatile("mov %[addr], lr" : [addr] "=r" (addr) ); addr -= 4; // Bottom 24 bits of the SWI instruction are the SWI number swi = *((unsigned int *)addr) & 0x00ffffff; // Debug message console_write("\n"); console_write(COLOUR_PUSH FG_GREEN "SWI call. Address: 0x"); console_write(tohex(addr, 4)); console_write(" SWI number "); console_write(todec(swi, 0)); console_write(COLOUR_POP "\n"); // Changing processor mode asm volatile("cps #0x1f"); // Handle syscall syscall(swi); } // IRQ __attribute__ ((interrupt ("IRQ"))) void interrupt_irq(void) { // This function starts on IRQ mode // Push all registers into the IRQ mode stack (R13) asm volatile("push {R0-R12}"); // Put LR register of IRQ mode (PC of interrupted process) on R0 asm volatile("MOV R0, LR"); // Change to system mode asm volatile("cps #0x1f"); // Push R0 (interrupted PC) to the system mode stack asm volatile("push {R0}"); // Return to IRQ mode asm volatile("cps #0x12"); 75 // Pop all registers again asm volatile("pop {R0-R12}"); // Return to system mode asm volatile("cps #0x1f"); // Push all registers into the system mode stack asm volatile("push {R0-R12}"); // Push the interrupted LR to system mode stack asm volatile("push {LR}"); // Copy the processor status to R0 asm volatile("MRS R0, SPSR"); // Push the processor status to system mode stack asm volatile("push {R0}"); // Return to IRQ mode asm volatile("cps #0x12"); // Copy LR to R0 asm volatile("MOV R0, LR"); // Back to system mode asm volatile("cps #0x1f"); unsigned long pc; unsigned long stack_pointer; // Getting pc and stack just to debug asm volatile ("MOV %0, R0\n\t" : "=r" (pc) ); asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) ); // Invert led to inform context switch activity led_invert(); // Jump to scheduler to do the context switch schedule_timeout(stack_pointer, pc); } __attribute__ ((interrupt ("ABORT"))) void interrupt_data_abort(void) { register unsigned int addr, far; asm volatile("mov %[addr], lr" : [addr] "=r" (addr) ); /* Read fault address register */ asm volatile("mrc p15, 0, %[addr], c6, c0, 0": [addr] "=r" (far) ); console_write("Data abort!\n"); console_write("Instruction address: 0x"); /* addr = lr, but the very start of the abort routine does * sub lr, lr, #4 * lr = address of aborted instruction, plus 8 */ console_write(tohex(addr-4, 4)); 76 console_write(" fault address: 0x"); console_write(tohex(far, 4)); console_write("\n"); /* Routine terminates by returning to LR-4, which is the instruction * after the aborted one * GCC doesn't properly deal with data aborts in its interrupt * handling - no option to return to the failed instruction */ asm volatile("cps #0x1f"); asm volatile("cpsid aif"); } // Return to this function after a prefetch abort extern void main_endloop(void); __attribute__ ((interrupt ("ABORT"))) void interrupt_prefetch_abort(void) { register unsigned int addr; asm volatile("mov %[addr], lr" : [addr] "=r" (addr) ); console_write("Prefetch abort!\n"); console_write("Instruction address: 0x"); /* lr = address of aborted instruction, plus 4 * addr = lr, but the very start of the abort routine does * sub lr, lr, #4 */ console_write(tohex(addr, 4)); console_write("\n"); /* Set the return address to be the function main_endloop(), by * putting its address into the program counter * * THIS IS JUST A TEST - you can't normally do this as it doesn't * restore the registers or put the stack pointer back where it was, * so repeated aborts will overflow the stack. */ asm volatile("movs pc, %[addr]" : : [addr] "r" ((unsigned int)(&main_endloop)) ); /* Doesn't reach this point */ } /* Initialise the interrupts * * Enable the ARM timer interrupt */ void interrupts_init(void) { // Set interrupt base register asm volatile("mcr p15, 0, %[addr], c12, c0, 0" : : [addr] "r" (&interrupt_vectors)); // Turn on interrupts asm volatile("cpsie i"); // Enable ARM timer IRQ *irqEnableBasic = 0x00000001; // Interrupt every x * 256 (prescaler) timer ticks 77 *armTimerLoad = 0x00000400; // Timer enabled *armTimerControl = 0x000000aa; } void timer_reset(void) { *armTimerIRQClear = 0; } 78 # scheduler.h #ifndef SCHEDULER_H #define SCHEDULER_H #include "process.h" void void void void void create_main_process(); fork(char * name, unsigned long addr); schedule_timeout(unsigned long stack_pointer, unsigned long pc); terminate_process(); halt(); #endif /* SCHEDULER_H */ # scheduler.c #include #include #include #include static static static static "process.h" "framebuffer.h" "textutils.h" "interrupts.h" process process_list[1024]; unsigned int stack_base; unsigned int process_count = 0; unsigned int active_process_index; extern void main_endloop(); extern void timer_reset(); void terminate_process() { // Set it to terminated status, so the scheduler will ignore it process_list[active_process_index].status = PROCESS_STATUS_TERMINATED; } // Function to create the main process void create_main_process() { // Creating process process main_process; main_process.id = process_count; main_process.name = "Main"; main_process.pc = &main_endloop; main_process.times_loaded = 1; main_process.status = PROCESS_STATUS_ZOMBIE; // Gets the actual stack address unsigned int stack_pointer; asm volatile ("MOV %0, SP\n\t" : "=r" (stack_pointer) ); // Use it as base address processes stack pointer stack_base = stack_pointer; // Output the stack address console_write("Main stack is 0x"); console_write(tohex(stack_pointer, 4)); console_write("\n"); // Saving the process in the process table 79 process_list[process_count] = main_process; // Set it as the current active process active_process_index = process_count; // Increments the process counter process_count++; } /* * Procedure to fork this process, creating a new one, pointing the pc * to the memory address of the desired procedure */ void fork(char * name, unsigned long * pc) { process fork_process; // Basic memory organization to get new stack addr. just add 1024 bytes from the main stack unsigned int * forked_stack_pointer = stack_base + (process_count * 1024); console_write("Forked stack is 0x"); console_write(tohex(forked_stack_pointer, 4)); console_write("\n"); fork_process.id = process_count; fork_process.name = name; fork_process.pc = pc; fork_process.parent = active_process_index; fork_process.times_loaded = 0; fork_process.stack_pointer = forked_stack_pointer; fork_process.status = PROCESS_STATUS_WAITING; process_list[process_count] = fork_process; process_count++; } /* * Function to get the next ready process in the list */ int next_waiting_process_index() { // Start in the active index int next_process_index = active_process_index; // Do this while the actual process isnt in the waiting status and not reach the actual running process do { next_process_index++; // Just rewind the list if (next_process_index == process_count) { next_process_index = 0; } } while ((process_list[next_process_index].status != PROCESS_STATUS_WAITING) && (next_process_index != active_process_index)); // If the found process isnt waiting 80 if (process_list[next_process_index].status != PROCESS_STATUS_WAITING) { return -1; } return next_process_index; } /* * Just keep the processor busy */ void halt() { while(1); } /* * Procedure coming from the IRQ interrupt, to change the running process */ void schedule_timeout(unsigned long stack_pointer, unsigned long pc) { // Saving stack and pc process_list[active_process_index].stack_pointer = stack_pointer; process_list[active_process_index].pc = pc; // Updating process status to waiting if (process_list[active_process_index].status == PROCESS_STATUS_RUNNING) { process_list[active_process_index].status = PROCESS_STATUS_WAITING; } console_write("\n"); console_write("\n"); console_write("Schedule timeout. Current active pid is "); console_write(todec(process_list[active_process_index].id, 0)); console_write(" with name "); console_write(process_list[active_process_index].name); console_write(". Switching to next process.\n"); console_write("Saving stack..."); console_write(" stack saved, was 0x"); console_write(tohex(process_list[active_process_index].stack_pointer, 4)); console_write("\n"); console_write("Saving pc..."); console_write(" pc saved, was 0x"); console_write(tohex(process_list[active_process_index].pc, 4)); console_write("\n"); // Get next process id int next_process = next_waiting_process_index(); // If -1, halt if (next_process < 0) { console_write("No more waiting processes, halting."); halt(); } // Updating next running process active_process_index = next_process; 81 // Increasing statistics, updating status to running process_list[active_process_index].times_loaded++; process_list[active_process_index].status = PROCESS_STATUS_RUNNING; console_write("Restoring stack 0x"); console_write(tohex(process_list[active_process_index].stack_pointer, 4)); console_write("\n"); console_write("Restoring pc 0x"); console_write(tohex(process_list[active_process_index].pc, 4)); console_write("\n"); // Assembly to load the sp with the new process stack address asm volatile("MOV SP, %[addr]" : : [addr] "r" ((unsigned long )(process_list[active_process_index].stack_pointer)) ); // If its is not the first time if (process_list[active_process_index].times_loaded > 1) { timer_reset(); // Pops registers asm volatile("pop asm volatile("MSR asm volatile("pop asm volatile("pop from the stack {R0}"); SPSR_cxsf, R0"); {LR}"); {R0 - R12}"); // Turn on interrupt again asm volatile("cpsie i"); // Pops the last register into PC to resume processing asm volatile("pop {PC}"); } else { // Push the first pc address into the stack unsigned long addr = (unsigned long )(process_list[active_process_index].pc); asm volatile("MOV R0, %[addr]" : : [addr] "r" (addr) ); asm volatile("push {R0}"); timer_reset(); // Turn on interrupt again asm volatile("cpsie i"); // Pops the last register into PC to resume processing asm volatile("pop {PC}"); } } 82 # process.h #ifndef PROCESS_H #define PROCESS_H #define PROCESS_STATUS_RUNNING #define PROCESS_STATUS_WAITING processor #define PROCESS_STATUS_ZOMBIE to terminate #define PROCESS_STATUS_TERMINATED #define PROCESS_STATUS_ABORTED 0 1 // In the processor // Waiting its time on the 2 // Dead, but waiting child process 3 // Terminated gracefully 4 // Terminated abnormally typedef struct { unsigned int id; char * name; unsigned int parent; unsigned long stack_pointer; unsigned long pc; unsigned int times_loaded; unsigned int status; } process; extern void sample_process_1(); extern void sample_process_2(); #endif /* PROCESS_H */ # process.c #include "textutils.h" #include "framebuffer.h" #include "scheduler.h" // Just some counting for easy debug on the screen. Simulate user process. void sample_process_1() { console_write("Starting process 1 "); int to = 300; int i, j; for (i=0; i<to; i++) { console_write(todec(i,0)); console_write(" "); for (j=0; j<to*to; j++) { asm volatile("NOP"); } } // Call software interrupt #0 (terminate) asm volatile("SWI #0"); } // Just some counting for easy debug on the screen. Simulate user process. void sample_process_2() { console_write("Starting process 2 "); int to = 300; 83 int i, j; for (i=0; i<to; i++) { console_write(todec(i,0)); console_write(" "); for (j=0; j<to*to; j++) { asm volatile("NOP"); } } // Call software interrupt #0 (terminate) asm volatile("SWI #0"); } 84 # syscall.h #ifndef SYSCALL_H #define SYSCALL_H #define SYSCALL_TERMINATE_PROCESS 0 void syscall(unsigned int swi); #endif /* SYSCALL_H */ # syscall.c #include #include #include #include "scheduler.h" "syscall.h" "framebuffer.h" "textutils.h" /* * Procedure to handle software interrupts */ void syscall(unsigned int swi) { console_write("Handling syscall: "); console_write(todec(swi, 0)); console_write("\n"); switch (swi) { case SYSCALL_TERMINATE_PROCESS: console_write("Invoking syscall terminate_process()"); console_write("\n"); // Calling the right intra-kernel procedure terminate_process(); break; } console_write("Turning interrupt on again"); console_write("\n"); // Turn on interrupt again asm volatile("cpsie i"); // Wait for timer interrupt. Probably not the best thing to do. halt(); } 85 # barrier.h #ifndef BARRIER_H #define BARRIER_H /* * Data memory barrier * No memory access after the DMB can run until all memory accesses before it * have completed */ #define dmb() asm volatile \ ("mcr p15, #0, %[zero], c7, c10, #5" : : [zero] "r" (0) ) /* * Data synchronisation barrier * No instruction after the DSB can run until all instructions before it have * completed */ #define dsb() asm volatile \ ("mcr p15, #0, %[zero], c7, c10, #4" : : [zero] "r" (0) ) /* * Clean and invalidate entire cache * Flush pending writes to main memory * Remove all data in data cache */ #define flushcache() asm volatile \ ("mcr p15, #0, %[zero], c7, c14, #0" : : [zero] "r" (0) ) #endif /* BARRIER_H */ 86 # memory.h #ifndef MEMORY_H #define MEMORY_H /* Convert a virtual address to a physical one by following the page tables * Returns physical address, or 0xffffffff if the virtual address does not map */ extern unsigned int mem_v2p(unsigned int); /* Convert a physical address to a virtual one - essentially, just add * 0x80000000 to it */ #define mem_p2v(X) (X+0x80000000) extern void mem_init(void); #endif /* MEMORY_H */ # memory.c #include "memory.h" /* Virtual memory layout * * 0x00000000 - 0x7fffffff * 0x80000000 - 0xa0ffffff * includes peripherals * 0xc0000000 - 0xefffffff * 0xf0000000 - 0xffffffff */ (0-2GB) = user process memory = physical memory at 0x20000000 - 0x20ffffff = kernel data = kernel code /* Need to access the page table, etc as physical memory */ static unsigned int *pagetable = (unsigned int * const) mem_p2v(0x4000); /* 16k */ /* Last used location in physical RAM */ extern unsigned int _physbssend; /* Start of kernel in physical RAM */ extern unsigned int _highkernelload; /* Convert a virtual address to a physical one by following the page tables * Returns physical address, or 0xffffffff if the virtual address does not map * See ARM1176-TZJS technical reference manual, page 6-39 (6.11.2) */ unsigned int mem_v2p(unsigned int virtualaddr) { unsigned int pt_data = pagetable[virtualaddr >> 20]; unsigned int cpt_data, physaddr; if((pt_data & 3)==0 || (pt_data & 3)==3) { /* Nothing mapped */ return 0xffffffff; } if((pt_data & 3)==2) { /* (Super)Section */ physaddr = pt_data & 0xfff00000; 87 if(pt_data & (1<<18)) { /* 16MB Supersection */ physaddr += virtualaddr & 0x00ffffff; } else { /* 1MB Section */ physaddr += virtualaddr & 0x000fffff; } return physaddr; } /* Coarse page table */ cpt_data = ((unsigned int *)(0x80000000 + (pt_data & 0xfffffc00)))[(virtualaddr>>12)&0xff] ; if((cpt_data & 3) == 0) { /* Nothing mapped */ return 0xffffffff; } if(cpt_data & 2) { /* Small (4k) page */ return (cpt_data & 0xfffff000) + (virtualaddr & 0xfff); } /* Large (64k) page */ return (cpt_data & 0xffff0000) + (virtualaddr & 0xffff); } /* mem_p2v is a simple macro defined in memory.h which adds 0x80000000 to an * address, to put the physical memory address (0x00000000-0x20ffffff) into * the corresponding mapped area of virtual memory (0x80000000-0xa0ffffff) */ /* Translation table 0 - covers the first 64 MB, for now * Needs to be aligned to its size (ie 64*4 bytes) */ unsigned int pagetable0[64] __attribute__ ((aligned (256))); /* Initialise memory - actually, there's not much to do now, since initsys * covers most of it. It just sets up a pagetable for the first 64MB of RAM * (all unmapped) */ void mem_init(void) { unsigned int x; unsigned int pt0_addr; /* Translation table 0 - covers the first 64 MB, for now * Currently nothing mapped in it. */ for(x=0; x<64; x++) { pagetable0[x] = 0; 88 } /* Get physical address of pagetable0 */ pt0_addr = mem_v2p((unsigned int) &pagetable0); /* Use translation table 0 up to 64MB */ asm volatile("mcr p15, 0, %[n], c2, c0, 2" : : [n] "r" (6)); /* Translation table 0 - ARM1176JZF-S manual, 3-57 */ asm volatile("mcr p15, 0, %[addr], c2, c0, 0" : : [addr] "r" (pt0_addr)); /* Invalidate the translation lookaside buffer (TLB) * ARM1176JZF-S manual, p. 3-86 */ asm volatile("mcr p15, 0, %[data], c8, c7, 0" : : [data] "r" (0)); } 89 # memutils.h #ifndef MEMUTILS_H #define MEMUTILS_H /* Clear length bytes of memory (set to 0) starting at address */ extern void memclr(void *address, unsigned int length); /* Move length bytes from src to dest. Memory areas may overlap */ extern void *memmove(void *dest, const void *src, unsigned int length); #endif /* MEMUTILS_H */ # memutils.c /* Various memory utilities */ #include "memutils.h" /* Clear (set to 0) length bytes of memory starting at address */ void memclr(void *address, unsigned int length) { register unsigned int addr = (unsigned int)address; /* If the start address is unaligned, fill in the first 1-3 bytes * until it is */ while((addr & 3) && length) { *((unsigned char *)addr) = 0; addr++; length--; } /* Fill in the remaining 32-bit word-aligned memory locations */ while(length & 0xfffffffc) { *((unsigned int *)addr) = 0; addr+=4; length-=4; } /* Deal with the remaining 1-3 bytes, if any */ while(length) { addr++; length--; *((unsigned char *)addr) = 0; } } /* Move length bytes from src to dest. Memory areas may overlap * Four possibilities: * ..[...src...]..[...dest...].. -- non-overlapping * ..[...dest...]..[...src...].. -- non-overlapping * ..[...src..[.]..dest...].. -- overlapping * ..[...dest..[.]..src...].. -- overlapping * The first two can be dealt with by copying from either end of the source * block * The third (overlapping, source first) by copying from the end of the block * back to the start 90 * The fourth (overlapping, destination first) by copying from the start of the * source block to the end * * Returns the address of the destination memory area */ void *memmove(void *dest, const void *src, unsigned int length) { /* Turn destination and source pointers into integers for easier * calculations */ register unsigned int d = (unsigned int)dest; register unsigned int s = (unsigned int)src; if(!length) return dest; /* Assume the memory blocks are word aligned. Most will be, and the * CPU can deal with unaligned accesses if necessary (as long as it * is configured to do so) */ if(d>s && d<(s+length)) { /* Destination starts inside source area - work backwards */ /* If length isn't a multiple of 4 bytes, copy the last 1-3 * bytes first */ while(length & 3) { length--; ((unsigned char *)d)[length] = ((unsigned char *)s)[length]; } /* Copy everything else as 32-bit words. If one or both of the * memory areas aren't aligned, this will cause unaligned * reads. Inefficient, but less so than doing everything as * a series of bytes */ while(length) { length-=4; *((unsigned int *)d+length) = *((unsigned int *)s+length); } } else { /* Source starts inside destination area - working forwards * is fine - or two areas don't overlap (or they overlap * exactly, but that's an unlikely edge case) * * Copy as much as possible as 32-bit words. See above for * alignment issues */ while(length & 0xfffffffc) { *((unsigned int *)d) = *((unsigned int *)s); d+=4; s+=4; length-=4; 91 } /* Deal with 1-3 remaining bytes, if applicable */ while(length) { *((unsigned char *)d) = *((unsigned char *)s); d++; s++; length--; } } return dest; } 92 # framebuffer.h #ifndef FRAMEBUFFER_H #define FRAMEBUFFER_H extern void fb_init(void); extern void console_write(char *text); /* Control characters for the console */ #define FG_RED "\001" #define FG_GREEN "\002" #define FG_BLUE "\003" #define FG_YELLOW "\004" #define FG_MAGENTA "\005" #define FG_CYAN "\006" #define FG_WHITE "\007" #define FG_BLACK "\010" #define FG_HALF "\011" #define COLOUR_PUSH "\013" #define COLOUR_POP "\014" #define #define #define #define #define #define #define #define #define #endif BG_RED "\021" BG_GREEN "\022" BG_BLUE "\023" BG_YELLOW "\024" BG_MAGENTA "\025" BG_CYAN "\026" BG_WHITE "\027" BG_BLACK "\030" BG_HALF "\031" /* FRAMEBUFFER_H */ # framebuffer.c #include "framebuffer.h" #include "led.h" #include "mailbox.h" #include "memory.h" #include "memutils.h" #include "textutils.h" /* SAA5050 (teletext) character definitions */ #include "teletext.h" /* Framebuffer initialisation failure codes * If the FB can't be initialised, one of the following numbers will be * flashed on the OK LED */ /* Mailbox call to get screen resolution failed */ #define FBFAIL_GET_RESOLUTION 1 /* Mailbox call returned bad resolution */ #define FBFAIL_GOT_INVALID_RESOLUTION 2 /* Mailbox call to setup FB failed */ #define FBFAIL_SETUP_FRAMEBUFFER 3 /* Setup call FB returned an invalid list of response tags */ #define FBFAIL_INVALID_TAGS 4 /* Setup FB call returned an invalid response for the framebuffer tag */ #define FBFAIL_INVALID_TAG_RESPONSE 5 /* Setup FB call returned an invalid address/size */ #define FBFAIL_INVALID_TAG_DATA 6 93 /* Read #define /* Read #define FB pitch call returned an invalid response */ FBFAIL_INVALID_PITCH_RESPONSE 7 FB pitch call returned an invalid pitch value */ FBFAIL_INVALID_PITCH_DATA 8 /* Character cells are 6x10 */ #define CHARSIZE_X 6 #define CHARSIZE_Y 10 /* Screen parameters set in fb_init() */ static unsigned int screenbase, screensize; static unsigned int fb_x, fb_y, pitch; /* Max x/y character cell */ static unsigned int max_x, max_y; /* Framebuffer initialisation failed. Can't display an error, so flashing * the OK LED will have to do */ static void fb_fail(unsigned int num) { while(1) output(num); } /* Initialise the framebuffer */ void fb_init(void) { unsigned int var; unsigned int count; unsigned int physical_screenbase; /* Storage space for the buffer used to pass information between the * CPU and VideoCore * Needs to be aligned to 16 bytes as the bottom 4 bits of the address * passed to VideoCore are used for the mailbox number */ volatile unsigned int mailbuffer[256] __attribute__((aligned (16))); /* Physical memory address of the mailbuffer, for passing to VC */ unsigned int physical_mb = mem_v2p((unsigned int)mailbuffer); /* Get the display size */ mailbuffer[0] = 8 * 4; mailbuffer[1] = 0; mailbuffer[2] = 0x40003; mailbuffer[3] = 8; mailbuffer[4] = 0; mailbuffer[5] = 0; mailbuffer[6] = 0; mailbuffer[7] = 0; // Total size // Request // Display size // Buffer size // Request size // Space for horizontal resolution // Space for vertical resolution // End tag writemailbox(8, physical_mb); var = readmailbox(8); /* Valid response in data structure */ if(mailbuffer[1] != 0x80000000) fb_fail(FBFAIL_GET_RESOLUTION); fb_x = mailbuffer[5]; 94 fb_y = mailbuffer[6]; /* If both fb_x and fb_y are both zero, assume we're running on the * qemu Raspberry Pi emulation (which doesn't return a screen size * at this point), and request a 640x480 screen */ if(fb_x==0 && fb_y==0) { fb_x = 640; fb_y = 480; } if(fb_x==0 || fb_y==0) fb_fail(FBFAIL_GOT_INVALID_RESOLUTION); // Setup screen unsigned int c = 1; mailbuffer[c++] = 0; // Request mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] = = = = = 0x00048003; // Tag id (set physical size) 8; // Value buffer size (bytes) 8; // Req. + value length (bytes) fb_x; // Horizontal resolution fb_y; // Vertical resolution mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] = = = = = 0x00048004; // Tag id (set virtual size) 8; // Value buffer size (bytes) 8; // Req. + value length (bytes) fb_x; // Horizontal resolution fb_y; // Vertical resolution mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] = = = = 0x00048005; // Tag id (set depth) 4; // Value buffer size (bytes) 4; // Req. + value length (bytes) 16; // 16 bpp mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] mailbuffer[c++] = = = = = 0x00040001; // Tag id (allocate framebuffer) 8; // Value buffer size (bytes) 4; // Req. + value length (bytes) 16; // Alignment = 16 0; // Space for response mailbuffer[c++] = 0; // Terminating tag mailbuffer[0] = c*4; // Buffer size writemailbox(8, physical_mb); var = readmailbox(8); /* Valid response in data structure */ if(mailbuffer[1] != 0x80000000) fb_fail(FBFAIL_SETUP_FRAMEBUFFER); count=2; /* First tag */ while((var = mailbuffer[count])) { if(var == 0x40001) break; 95 /* Skip to next tag * Advance count by 1 (tag) + 2 (buffer size/value size) * + specified buffer size */ count += 3+(mailbuffer[count+1]>>2); if(count>c) fb_fail(FBFAIL_INVALID_TAGS); } /* 8 bytes, plus MSB set to indicate a response */ if(mailbuffer[count+2] != 0x80000008) fb_fail(FBFAIL_INVALID_TAG_RESPONSE); /* Framebuffer address/size in response */ physical_screenbase = mailbuffer[count+3]; screensize = mailbuffer[count+4]; if(physical_screenbase == 0 || screensize == 0) fb_fail(FBFAIL_INVALID_TAG_DATA); /* physical_screenbase is the address of the screen in RAM * screenbase needs to be the screen address in virtual memory */ screenbase=mem_p2v(physical_screenbase); /* Get the framebuffer pitch mailbuffer[0] = 7 * 4; mailbuffer[1] = 0; mailbuffer[2] = 0x40008; mailbuffer[3] = 4; mailbuffer[4] = 0; mailbuffer[5] = 0; mailbuffer[6] = 0; (bytes per line) */ // Total size // Request // Display size // Buffer size // Request size // Space for pitch // End tag writemailbox(8, physical_mb); var = readmailbox(8); /* 4 bytes, plus MSB set to indicate a response */ if(mailbuffer[4] != 0x80000004) fb_fail(FBFAIL_INVALID_PITCH_RESPONSE); pitch = mailbuffer[5]; if(pitch == 0) fb_fail(FBFAIL_INVALID_PITCH_DATA); /* Need to set up max_x/max_y before using console_write */ max_x = fb_x / CHARSIZE_X; max_y = fb_y / CHARSIZE_Y; } /* Current console text cursor position (ie. where the next character will * be written */ static int consx = 0; static int consy = 0; /* Current fg/bg colour */ static unsigned short int fgcolour = 0xffff; 96 static unsigned short int bgcolour = 0; /* A small stack to allow temporary colour changes in text */ static unsigned int colour_stack[] = { 0, 0, 0, 0, 0, 0, 0, 0 }; static unsigned int colour_sp = 8; /* Move to a new line, and, if at the bottom of the screen, scroll the * framebuffer 1 character row upwards, discarding the top row */ static void newline() { unsigned int source; /* Number of bytes in a character row */ register unsigned int rowbytes = CHARSIZE_Y * pitch; consx = 0; if(consy<(max_y-1)) { consy++; return; } /* Copy a screen's worth of data (minus 1 character row) from the * second row to the first */ /* Calculate the address to copy the screen data from */ source = screenbase + rowbytes; memmove((void *)screenbase, (void *)source, (max_y-1)*rowbytes); /* Clear last line on screen */ memclr((void *)(screenbase + (max_y-1)*rowbytes), rowbytes); } /* Write null-terminated text to the console * Supports control characters (see framebuffer.h) for colour and newline */ void console_write(char *text) { volatile unsigned short int *ptr; unsigned int row, addr; int col; unsigned char ch; /* Double parentheses to silence compiler warnings about * assignments as boolean values */ while((ch = (unsigned char)*text)) { text++; /* Deal with control codes */ switch(ch) { case 1: fgcolour = 0b1111100000000000; case 2: fgcolour = 0b0000011111100000; case 3: fgcolour = 0b0000000000011111; case 4: fgcolour = 0b1111111111100000; case 5: fgcolour = 0b1111100000011111; case 6: fgcolour = 0b0000011111111111; continue; continue; continue; continue; continue; continue; 97 case 7: fgcolour = 0b1111111111111111; continue; case 8: fgcolour = 0b0000000000000000; continue; /* Half brightness */ case 9: fgcolour = (fgcolour >> 1) & 0b0111101111101111; continue; case 10: newline(); continue; case 11: /* Colour stack push */ if(colour_sp) colour_sp--; colour_stack[colour_sp] = fgcolour | (bgcolour<<16); continue; case 12: /* Colour stack pop */ fgcolour = colour_stack[colour_sp] & 0xffff; bgcolour = colour_stack[colour_sp] >> 16; if(colour_sp<8) colour_sp++; continue; case 17: bgcolour = 0b1111100000000000; continue; case 18: bgcolour = 0b0000011111100000; continue; case 19: bgcolour = 0b0000000000011111; continue; case 20: bgcolour = 0b1111111111100000; continue; case 21: bgcolour = 0b1111100000011111; continue; case 22: bgcolour = 0b0000011111111111; continue; case 23: bgcolour = 0b1111111111111111; continue; case 24: bgcolour = 0b0000000000000000; continue; /* Half brightness */ case 25: bgcolour = (bgcolour >> 1) & 0b0111101111101111; continue; } /* Unknown control codes, and anything >127, get turned into * spaces. Anything >=32 <=127 gets 32 subtracted from it to * turn it into a value between 0 and 95, to index into the * character definitions table */ if(ch<32) { ch=0; } else { if(ch>127) ch=0; else ch-=32; } /* Plot character onto screen * * CHARSIZE_Y and CHARSIZE_X are the size of the block the * character occupies. The character itself is one pixel * smaller in each direction, and is located in the upper left * of the block */ for(row=0; row<CHARSIZE_Y; row++) { addr = (row+consy*CHARSIZE_Y)*pitch + consx*CHARSIZE_X*2; for(col=(CHARSIZE_X-2); col>=0; col--) { 98 ptr = (unsigned short int *)(screenbase+addr); addr+=2; if(row<(CHARSIZE_Y-1) && (teletext[ch][row] & (1<<col))) *ptr = fgcolour; else *ptr = bgcolour; } ptr = (unsigned short int *)(screenbase+addr); *ptr = bgcolour; } if(++consx >=max_x) { newline(); } } } 99 # textutils.h #ifndef TEXTUTILS_H #define TEXTUTILS_H extern char *tohex(unsigned int value, unsigned int size); extern char *todec(unsigned int value, int leading); #endif /* TEXTUTILS_H */ # textutils.c #include "textutils.h" /* Convert an unsigned value to hex (without the trailing "0x") * size = size in bytes (only 1, 2 or 4 permitted) */ char *tohex(unsigned int value, unsigned int size) { static char buffer[9]; unsigned int offset; unsigned char ch; if(size!=1 && size!=2 && size!=4) return "error"; offset=size*2; buffer[offset] = 0; while(offset--) { ch = 48 + (value & 15); if(ch>=58) ch+=7; buffer[offset] = ch; value = value >> 4; } return buffer; } /* Convert unsigned value to decimal * leading = 0 - no leading spaces/zeroes * leading >0 - number of leading zeroes * leading <0 - number (when +ve) of leading spaces */ char *todec(unsigned int value, int leading) { /* Biggest number is 4294967295 (10 digits) */ static char buffer[11]; static char leadchar; unsigned int offset = 10; unsigned char ch; if(leading <0) { leading = -leading; leadchar = ' '; 100 } else { leadchar = '0'; } if(leading>10) return "error"; buffer[offset] = 0; while(value || (offset == 10)) { offset--; leading--; ch = 48 + (value % 10); buffer[offset] = ch; value = value/10; } while(leading>0) { offset--; leading--; buffer[offset] = leadchar; } return &buffer[offset]; } 101 # teletext.h // Character definitions from SAA5050 datasheet // Each character is a 5x9 bit matrix // 9 rows of 5-bit numbers static unsigned char teletext[][9] = { { 0,0,0,0,0,0,0,0,0 }, // space { 4,4,4,4,4,0,4,0,0 }, // ! { 10,10,10,0,0,0,0,0,0 }, // " { 6,9,8,28,8,8,31,0,0 }, // £ # in ASCII { 14,21,20,14,5,21,14,0,0 }, // $ { 24,25,2,4,8,19,3,0,0 }, // % { 8,20,20,8,21,18,13,0,0 }, // & { 4,4,4,0,0,0,0,0,0 }, // ' { 2,4,8,8,8,4,2,0,0 }, // ( { 8,4,2,2,2,4,8,0,0 }, // ) { 4,21,14,4,14,21,4,0,0 }, // * { 0,4,4,31,4,4,0,0,0 }, // + { 0,0,0,0,0,4,4,8,0 }, // , { 0,0,0,14,0,0,0,0,0 }, // { 0,0,0,0,0,0,4,0,0 }, // . { 0,1,2,4,8,16,0,0,0 }, // / { 4,10,17,17,17,10,4,0,0 }, // 0 { 4,12,4,4,4,4,14,0,0 }, // 1 { 14,17,1,6,8,16,31,0,0 }, // 2 { 31,1,2,6,1,17,14,0,0 }, // 3 { 2,6,10,18,31,2,2,0,0 }, // 4 { 31,16,30,1,1,17,14,0,0 }, // 5 { 6,8,16,30,17,17,14,0,0 }, // 6 { 31,1,2,4,8,8,8,0,0 }, // 7 { 14,17,17,14,17,17,14,0,0 }, // 8 { 14,17,17,15,1,2,12,0,0 }, // 9 { 0,0,4,0,0,0,4,0,0 }, // : { 0,0,4,0,0,4,4,8,0 }, // ; { 2,4,8,16,8,4,2,0,0 }, // < { 0,0,31,0,31,0,0,0,0 }, // = { 8,4,2,1,2,4,8,0,0 }, // > { 14,17,2,4,4,0,4,0,0 }, // ? { 14,17,23,21,23,16,14,0,0 }, // @ { 4,10,17,17,31,17,17,0,0 }, // A { 30,17,17,30,17,17,30,0,0 }, // B { 14,17,16,16,16,17,14,0,0 }, // C { 30,17,17,17,17,17,30,0,0 }, // D { 31,16,16,30,16,16,31,0,0 }, // E { 31,16,16,30,16,16,16,0,0 }, // F { 14,17,16,16,19,17,15,0,0 }, // G { 17,17,17,31,17,17,17,0,0 }, // H { 14,4,4,4,4,4,14,0,0 }, // I { 1,1,1,1,1,17,14,0,0 }, // J { 17,18,20,24,20,18,17,0,0 }, // K { 16,16,16,16,16,16,31,0,0 }, // L { 17,27,21,21,17,17,17,0,0 }, // M { 17,17,25,21,19,17,17,0,0 }, // N { 14,17,17,17,17,17,14,0,0 }, // O { 30,17,17,30,16,16,16,0,0 }, // P { 14,17,17,17,21,18,13,0,0 }, // Q { 30,17,17,30,20,18,17,0,0 }, // R { 14,17,16,14,1,17,14,0,0 }, // S { 31,4,4,4,4,4,4,0,0 }, // T { 17,17,17,17,17,17,14,0,0 }, // U { 17,17,17,10,10,4,4,0,0 }, // V { 17,17,17,21,21,21,10,0,0 }, // W 102 { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { { }; 17,17,10,4,10,17,17,0,0 }, 17,17,10,4,4,4,4,0,0 }, 31,1,2,4,8,16,31,0,0 }, 0,4,8,31,8,4,0,0,0 }, 16,16,16,16,22,1,2,4,7 }, 0,4,2,31,2,4,0,0,0 }, 0,4,14,21,4,4,0,0,0 }, 10,10,31,10,31,10,10,0,0 }, 0,0,0,31,0,0,0,0,0 }, 0,0,14,1,15,17,15,0,0 }, 16,16,30,17,17,17,30,0,0 }, 0,0,15,16,16,16,15,0,0 }, 1,1,15,17,17,17,15,0,0 }, 0,0,14,17,31,16,14,0,0 }, 2,4,4,14,4,4,4,0,0 }, 0,0,15,17,17,17,15,1,14 }, 16,16,30,17,17,17,17,0,0 }, 4,0,12,4,4,4,14,0,0 }, 4,0,4,4,4,4,4,4,8 }, 8,8,9,10,12,10,9,0,0 }, 12,4,4,4,4,4,14,0,0 }, 0,0,26,21,21,21,21,0,0 }, 0,0,30,17,17,17,17,0,0 }, 0,0,14,17,17,17,14,0,0 }, 0,0,30,17,17,17,30,16,16 }, 0,0,15,17,17,17,15,1,1 }, 0,0,11,12,8,8,8,0,0 }, 0,0,15,16,14,1,30,0,0 }, 4,4,14,4,4,4,2,0,0 }, 0,0,17,17,17,17,15,0,0 }, 0,0,17,17,10,10,4,0,0 }, 0,0,17,17,21,21,10,0,0 }, 0,0,17,10,4,10,17,0,0 }, 0,0,17,17,17,17,15,1,14 }, 0,0,31,2,4,8,31,0,0 }, 8,8,8,8,9,3,5,7,1 }, 10,10,10,10,10,10,10,0,0 }, 24,4,24,4,25,3,5,7,1 }, 0,4,0,31,0,4,0,0,0 }, 31,31,31,31,31,31,31,0,0 } // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // X Y Z left arrow [ in ASCII 1/2 \ in ASCII right arrow ] in ASCII up arrow ^ in ASCII # _ in ASCII dash ` in ASCII a b c d e f g h i j k l m n o p q r s t u v w x y z 1/4 { in ASCII || | in ASCII 3/4 } in ASCII divide sign ~ in ASCII character-sized block not defined in ASCII 103 # led.h #ifndef LED_H #define LED_H void void void void led_init(void); led_invert(void); output32(unsigned int num); output(unsigned int num); #endif /* LED_H */ # led.c #include "led.h" #include "memory.h" /* Addresses of ARM GPIO devices (with conversion to virtual addresses) * See BCM2835 peripherals guide */ static volatile unsigned int *gpioGPFSEL1 = (unsigned int *) mem_p2v(0x20200004); static volatile unsigned int *gpioGPSET0 = (unsigned int *) mem_p2v(0x2020001c); static volatile unsigned int *gpioGPCLR0 = (unsigned int *) mem_p2v(0x20200028); static volatile unsigned int *gpioGPLEV0 = (unsigned int *) mem_p2v(0x20200034); static volatile unsigned int *gpioGPPUD = (unsigned int *) mem_p2v(0x20200094); static volatile unsigned int *gpioPUDCLK0 = (unsigned int *) mem_p2v(0x20200098); static volatile unsigned int *gpioPUDCLK1 = (unsigned int *) mem_p2v(0x2020009c); /* Short delay loop */ static void delay(void) { unsigned int timer=150; while(timer--) asm ("mov r0, r0"); /* No-op */ } void led_init(void) { unsigned int var; /* Each GPIO has 3 bits which determine its function * GPIO 14 and 16 are in GPFSEL1 */ /* Read current value of GPFSEL1 */ var = *gpioGPFSEL1; /* GPIO 16 = 001 - output */ var&=~(7<<18); var|=1<<18; /* GPIO 14 = 000 - input */ var&=~(7<<12); /* Write back updated value */ *gpioGPFSEL1 = var; 104 /* Set up pull-up on GPIO14 */ /* Enable pull-up control, then wait at least 150 cycles * The delay loop actually waits longer than that */ *gpioGPPUD = 2; delay(); /* Set the pull up/down clock for pin 14*/ *gpioPUDCLK0 = 1<<14; *gpioPUDCLK1 = 0; delay(); /* Disable pull-up control and reset the clock registers */ *gpioGPPUD = 0; *gpioPUDCLK0 = 0; *gpioPUDCLK1 = 0; } static unsigned int led_status = 0; void led_invert(void) { led_status = !led_status; if(led_status) *gpioGPCLR0 = 1<<16; else *gpioGPSET0 = 1<<16; /* on */ /* off */ } /* Shortish delay loop */ static void shortdelay(void) { unsigned int timer = 3000000; while(timer--) asm("mov r0, r0"); /* No-op */ } /* Massively long delay loop */ static void longdelay(void) { unsigned int timer = 10000000; while(timer--) asm("mov r0, r0"); /* No-op */ } static void output_n(unsigned int num, unsigned int count) { longdelay(); longdelay(); longdelay(); longdelay(); /* Flash quickly to indicate start */ *gpioGPCLR0 = 1<<16; shortdelay(); *gpioGPSET0 = 1<<16; 105 shortdelay(); *gpioGPCLR0 = 1<<16; shortdelay(); *gpioGPSET0 = 1<<16; longdelay(); longdelay(); while(count--) { *gpioGPCLR0 = 1<<16; if(num & 1) longdelay(); else shortdelay(); *gpioGPSET0 = 1<<16; longdelay(); num = num >> 1; } } void output32(unsigned int num) { output_n(num, 32); } void output(unsigned int num) { output_n(num, 8); } 106 # mailbox.h #ifndef MAILBOX_H #define MAILBOX_H extern unsigned int readmailbox(unsigned int channel); extern void writemailbox(unsigned int channel, unsigned int data); #endif /* MAILBOX_H */ # mailbox.c /* * Access system mailboxes */ #include "mailbox.h" #include "barrier.h" #include "memory.h" /* Mailbox memory addresses */ static volatile unsigned int *MAILBOX0READ = (unsigned int *) mem_p2v(0x2000b880); static volatile unsigned int *MAILBOX0STATUS = (unsigned int *) mem_p2v(0x2000b898); static volatile unsigned int *MAILBOX0WRITE = (unsigned int *) mem_p2v(0x2000b8a0); /* Bit 31 set in status register if the write mailbox is full */ #define MAILBOX_FULL 0x80000000 /* Bit 30 set in status register if the read mailbox is empty */ #define MAILBOX_EMPTY 0x40000000 unsigned int readmailbox(unsigned int channel) { unsigned int count = 0; unsigned int data; /* Loop until something is received from channel * If nothing recieved, it eventually give up and returns 0xffffffff */ while(1) { while (*MAILBOX0STATUS & MAILBOX_EMPTY) { /* Need to check if this is the right thing to do */ flushcache(); /* This is an arbritarily large number */ if(count++ >(1<<25)) { return 0xffffffff; } } /* Read the data * Data memory barriers as we've switched peripheral */ dmb(); data = *MAILBOX0READ; dmb(); 107 if ((data & 15) == channel) return data; } } void writemailbox(unsigned int channel, unsigned int data) { /* Wait for mailbox to be not full */ while (*MAILBOX0STATUS & MAILBOX_FULL) { /* Need to check if this is the right thing to do */ flushcache(); } dmb(); *MAILBOX0WRITE = (data | channel); } 108 # Makefile # These values should work for any Linux where the arm-linux-gnueabihf gcc # suite is installed, including Raspbian # # If they don't, you'll need to manually add the paths for gcc, ld, as and # objcopy CC:=arm-linux-gnueabihf-gcc LD:=$(shell $(CC) -print-prog-name=ld) AS:=$(shell $(CC) -print-prog-name=as) OBJCOPY:=$(shell $(CC) -print-prog-name=objcopy) # Location of libgcc.a (contains ARM AEABI functions such as numeric # division) # # The standard libgcc.a in some (cross-compiling) versions of gcc appears to # contain Thumb2 instructions, which don't exist on the ARM1176. This causes # undefined instruction exceptions. If the code is being compiled on a # Raspberry Pi, it ought to be fine. Otherwise, you may need to use a # different libgcc.a to the one which comes with your compiler # # If compiling on a Broadcom 2708 (a Raspberry Pi), use the libgcc.a # provided by the compiler. Otherwise, use rpi-libgcc/libgcc.a (this won't # definitely work on anything other than arm-linux-gnueabihf-gcc 4.6, but # is likely to) # If LOCALLIBGCC is set, use the libgcc.a supplied with the compiler ifdef LOCALLIBGCC LIBGCC:=$(shell $(CC) -print-libgcc-file-name) endif # If this is a Raspberry Pi (specifically, a Broadcom 2708 SoC), use the # libgcc.a supplied with the compiler, unless an alternative is set in # $(LIBGCC) ifneq ($(shell grep BCM2708 /proc/cpuinfo 2>/dev/null),) LIBGCC ?= $(shell $(CC) -print-libgcc-file-name) endif # If no alternative is set in $(LIBGCC) by this point, use rpilibgcc/libgcc.a LIBGCC ?= rpi-libgcc/libgcc.a # Assembler options: # -mcpu=arm1176jzf-s will cause the assembler to error on ARM opcodes which are # not valid for that specific processor ASOPT=--warn -mcpu=arm1176jzf-s # Compiler options: # -marm prevents gcc building Thumb code # -mcpu sets the exact CPU in the RPi, for optimised code # -nostdinc so as not to use any system #include locations # -ffreestanding to tell the compiler the usual system libraries aren't # available CCOPT=-Wall -O6 -nostdinc -ffreestanding -marm -g -mcpu=arm1176jzf-s # Object files built from C COBJS=framebuffer.o initsys.o interrupts.o led.o mailbox.o \ 109 main.o memory.o memutils.o textutils.o scheduler.o process.o syscall.o # Object files build from assembler ASOBJS=start.o all: make.dep kernel.img clean: rm -f make.dep *.o kernel.elf kernel.img .PHONY: all clean # Build the list of dependencies included at the bottom make.dep: *.c *.h gcc -M $(COBJS:.o=.c) >make.dep # If gcc -M fails, delete make.dep rather than allowing a half-finished file # to sit around for the next build .DELETE_ON_ERROR: make.dep # Build the assembler bits start.o: start.s $(ASOBJS): $(AS) $(ASOPT) -o $@ $< # Make an ELF kernel which loads at 0x8000 (RPi default) from all the object # files kernel.elf: linkscript $(ASOBJS) $(COBJS) $(LD) -T linkscript -nostdlib -nostartfiles -gc-sections \ -o kernel.elf \ $(ASOBJS) $(COBJS) $(LIBGCC) # Turn the ELF kernel into a binary file. This could be combined with the # step above, but it's easier to disassemble the ELF version to see why # something's not working kernel.img: kernel.elf $(OBJCOPY) kernel.elf -O binary kernel.img # Generic builder for C files $(COBJS): $(CC) $(CCOPT) -c -o $@ $< # Include the auto-generated make.dep file. The hyphen before "include" # stops it complaining that the file isn't there. -include make.dep 110 # linkscript /* Linker script for kernel */ /* Define the kernel entry point. Needed to prevent ld's -gc-sections option * getting rid of everything */ ENTRY(_start) /* Define the memory as starting at 0x8000, and being a megabyte long * (shouldn't need to be more than this) * "kernel" is where the majority of the kernel is run - it is linked at * this address, but loaded into memory after initsys at 0x9000 (probably, * as long as initsys is less than 4K) */ MEMORY { initsys : org = 0x8000, len = 1M kernel : org = 0xf0000000, len = 1M data : org = 0xc0000000, len = 1M } /* Output kernel code (.text) and read-only data (.rodata) into kernel * code space (0xf0000000), read/write data (.data) and initialised-to-zero * data (.bss) into kernel data space (0xc0000000), and the initial * start-up code/data (in start.s/initsys.c) at 0x8000 * * Make sure start.o comes first, so the entry point is at the start of the * file * * The file ends up looking something like this: * * File offset Load address Remapped to * 0x00000000 0x00008000 0x00008000 start.o/initsys.o * 0x00001000 0x00009000 0xf0009000 .text, .rodata * 0x00010000 0x00018000 0xc0000000 .data * n/a 0x00020000 0xc0008000 .bss * * Various pointers are calculated to help initsys map the kernel's virtual * memory, and clear .bss */ SECTIONS { /* Initial section, loads/runs at 0x8000 */ .init : { start.o(.text* .data* .bss* .rodata*) initsys.o (.text* .data* .bss* .rodata*) } >initsys _highkernelload = ALIGN(4k); /* Main kernel text/data. Loads at 0x9000, is remapped to * 0xf0009000 by initsys */ .text (_highkernelload + 0xf0000000) : AT(_highkernelload) { _kstart = ABSOLUTE(.); /* Address of the kernel */ *(.text*) } >kernel .rodata : { 111 /* Where the kernel code ends and read-only data begins */ _krodata = ABSOLUTE(.); *(.rodata*) } >kernel _kend = .; /* Address of the end of the kernel (RO data) */ _data_kmem = ALIGN(4k); /* Kernel read/write data */ .data : AT(_data_kmem - 0xf0000000) { _datastart = ABSOLUTE(.) ; *(.data) } >data /* Kernel read/write memory - needs to be zeroed by the kernel before * being used */ .bss : { _bssstart = ABSOLUTE(.) ; *(.bss) *(COMMON) _bssend = ABSOLUTE(.) ; } >data /* Calculate physical addresses of data memory for the kernel */ _physdatastart = _data_kmem - 0xf0000000; _physbssstart = _physdatastart + (_bssstart - _datastart); _physbssend = _physdatastart + (_bssend - _datastart); } 112 # start-debug.sh #!/bin/bash qemu-system-arm -kernel kernel.img -cpu arm1176 -M raspi -m 512 -s -vnc :1