Nesta seção vamos descrever cada sistema operacional em questão. Como eles lidam com syscalls, trapframes etc., todo o material de baixo nível. Também descrevemos a maneira como eles entendem primitivas comuns UNIX®, como o que é um PID, o que é uma thread, etc. Na terceira subseção, falamos sobre como UNIX® em emuladores UNIX® pode ser feita em geral.
UNIX® é um sistema operacional com um longo histórico que influenciou quase todos os outros sistemas operacionais atualmente em uso. Começando na década de 1960, seu desenvolvimento continua até hoje (embora em projetos diferentes). O desenvolvimento de UNIX® logo se bifurcou em duas formas principais: as famílias BSDs e System III/V. Eles se influenciaram mutuamente ao desenvolver um padrão UNIX® comum. Entre as contribuições originadas no BSD, podemos nomear memória virtual, rede TCP/IP, FFS e muitas outras. A ramificação SystemV contribuiu para as primitivas de comunicação entre processos SysV, copy-on-write, etc. UNIX® em si não existe mais, mas suas idéias têm sido usadas por muitos outros sistemas operacionais amplos formando assim os chamados sistemas operacionais como UNIX®. Hoje em dia os mais influentes são Linux®, Solaris e possivelmente (até certo ponto) FreeBSD. Existem sistemas UNIX® de companhias derivados como (AIX, HP-UX etc.), mas estas foram cada vez mais migrados para os sistemas acima mencionados. Vamos resumir as características típicas do UNIX®.
Todo programa em execução constitui um processo que representa um estado da computação. O processo de execução é dividido entre o espaço do kernel e o espaço do usuário. Algumas operações podem ser feitas somente a partir do espaço do kernel (lidando com hardware, etc.), mas o processo deve passar a maior parte de sua vida útil no espaço do usuário. O kernel é onde o gerenciamento dos processos, hardware e detalhes de baixo nível acontecem. O kernel fornece uma API unificada padrão UNIX® para o espaço do usuário. Os mais importantes são abordados abaixo.
A API comum do UNIX® define uma syscall como uma forma de emitir comandos de um processo do espaço do usuário para o kernel. A implementação mais comum é usando uma instrução de interrupção ou especializada (pense em instruções SYSENTER
/SYSCALL
para ia32). Syscalls são definidos por um número. Por exemplo, no FreeBSD, a syscall número 85 é a syscall swapon(2) e a syscall número 132 é a syscall mkfifo(2). Algumas syscalls precisam de parâmetros, que são passados do espaço do usuário para o espaço do kernel de várias maneiras (dependente da implementação). Syscalls são síncronas.
Outra maneira possível de se comunicar é usando uma trap. As traps ocorrem de forma assíncrona após a ocorrência de algum evento (divisão por zero, falha de página, etc.). Uma trap pode ser transparente para um processo (falha de página) ou pode resultar em uma reação como o envio de um signal (divisão por zero).
Existem outras APIs (System V IPC, memória compartilhada, etc.), mas a API mais importante é o signal. Os signals são enviados por processos ou pelo kernel e recebidos por processos. Alguns signals podem ser ignorados ou manipulados por uma rotina fornecida pelo usuário, alguns resultam em uma ação predefinida que não pode ser alterada ou ignorada.
As instâncias do kernel são processadas primeiro no sistema (chamado init). Todo processo em execução pode criar sua cópia idêntica usando a syscall fork(2). Algumas versões ligeiramente modificadas desta syscall foram introduzidas, mas a semântica básica é a mesma. Todo processo em execução pode se transformar em algum outro processo usando a syscall exec(3). Algumas modificações desta syscall foram introduzidas, mas todas servem ao mesmo propósito básico. Os processos terminam suas vidas chamando a syscall exit(2). Todo processo é identificado por um número único chamado PID. Todo processo tem um processo pai definido (identificado pelo seu PID).
O UNIX® tradicional não define nenhuma API nem implementação para threading, enquanto POSIX® define sua API de threading, mas a implementação é indefinida. Tradicionalmente, havia duas maneiras de implementar threads. Manipulando-as como processos separados (threading 1:1) ou envolver todo o grupo de thread em um processo e gerenciando a threading no espaço do usuário (threading 1:N). Comparando as principais características de cada abordagem:
1:1 threading
- threads pesadas
- o agendamento não pode ser alterado pelo usuário (ligeiramente mitigado pela API POSIX ®)
+ não necessita de envolvimento do syscall
+ pode utilizar várias CPUs
1: N threading
+ threads leves
+ agendamento pode ser facilmente alterado pelo usuário
- syscalls devem ser acondicionadas
- não pode utilizar mais de uma CPU
O projeto FreeBSD é um dos mais antigos sistemas operacionais de código aberto atualmente disponíveis para uso diário. É um descendente direto do verdadeiro UNIX®, portanto, pode-se afirmar que ele é um verdadeiro UNIX® embora os problemas de licenciamento não permitam isso. O início do projeto remonta ao início dos anos 90, quando uma equipe de usuários BSD corrigiu o sistema operacional 386BSD. Baseado neste patchkit surgiu um novo sistema operacional, chamado FreeBSD por sua licença liberal. Outro grupo criou o sistema operacional NetBSD com diferentes objetivos em mente. Vamos nos concentrar no FreeBSD.
O FreeBSD é um sistema operacional baseado no UNIX® com todos os recursos do UNIX®. Multitarefa preemptiva, necessidades de multiusuário, rede TCP/IP, proteção de memória, suporte a multiprocessamento simétrico, memória virtual com VM mesclada e cache de buffer, todos eles estão lá. Um dos recursos interessantes e extremamente úteis é a capacidade de emular outros sistemas operacionais UNIX®-like. A partir de dezembro de 2006 e do desenvolvimento do 7-CURRENT, as seguintes funcionalidades de emulação são suportadas:
Emulação FreeBSD/i386 no FreeBSD/amd64
Emulação de FreeBSD/i386 no FreeBSD/ia64
Emulação-Linux® do sistema operacional Linux ® no FreeBSD
Emulação de NDIS da interface de drivers de rede do Windows
Emulação de NetBSD do sistema operacional NetBSD
Suporte PECoff para executáveis PECoff do FreeBSD
Emulação SVR4 do UNIX® System V revisão 4
Emulações ativamente desenvolvidas são a camada Linux® e várias camadas FreeBSD-on-FreeBSD. Outros não devem funcionar corretamente nem ser utilizáveis nos dias de hoje.
O FreeBSD é o gostinho tradicional de UNIX® no sentido de dividir a execução dos processos em duas metades: espaço do kernel e execução do espaço do usuário. Existem dois tipos de entrada de processo no kernel: uma syscall e uma trap. Há apenas uma maneira de retornar. Nas seções subseqüentes, descreveremos as três portas de/para o kernel. Toda a descrição se aplica à arquitetura i386, pois o Linuxulator só existe lá, mas o conceito é semelhante em outras arquiteturas. A informação foi retirada de [1] e do código fonte.
O FreeBSD tem uma abstração chamada loader de classes de execução, que é uma entrada na syscall execve(2). Isto emprega uma estrutura sysentvec
, que descreve uma ABI executável. Ele contém coisas como tabela de tradução de errno, tabela de tradução de sinais, várias funções para atender às necessidades da syscall (correção de pilha, coredumping, etc.). Toda ABI que o kernel do FreeBSD deseja suportar deve definir essa estrutura, como é usado posteriormente no código de processamento da syscall e em alguns outros lugares. As entradas do sistema são tratadas pelos manipuladores de traps, onde podemos acessar o espaço do kernel e o espaço do usuário de uma só vez.
Syscalls no FreeBSD são emitidos executando a interrupção 0x80
com o registrador %eax
definido para um número de syscall desejado com argumentos passados na pilha.
Quando um processo emite uma interrupção 0x80
, a syscall manipuladora de trap int0x80
é proclamada (definida em sys/i386/i386/exception.s
), que prepara argumentos (ou seja, copia-os para a pilha) para uma chamada para uma função C syscall(2) (definida em sys/i386/i386/trap.c
), que processa o trapframe passado. O processamento consiste em preparar a syscall (dependendo da entrada sysvec
), determinando se a syscall é de 32 ou 64 bits (muda o tamanho dos parâmetros), então os parâmetros são copiados, incluindo a syscall. Em seguida, a função syscall real é executada com o processamento do código de retorno (casos especiais para erros ERESTART
e EJUSTRETURN
). Finalmente, um userret()
é agendado, trocando o processo de volta ao ritmo do usuário. Os parâmetros para a syscall manipuladora atual são passados na forma de argumentos struct thread *td
, struct syscall args*
onde o segundo parâmetro é um ponteiro para o copiado na estrutura de parâmetros.
O manuseio de traps no FreeBSD é similar ao manuseio de syscalls. Sempre que ocorre uma trap, um manipulador de assembler é chamado. É escolhido entre alltraps, alltraps com regs push ou calltrap, dependendo do tipo de trap. Este manipulador prepara argumentos para uma chamada para uma função C trap()
(definida em sys/i386/i386/trap.c
), que então processa a trap ocorrida. Após o processamento, ele pode enviar um sinal para o processo e/ou sair para o espaço do usuário usando userret()
.
As saídas do kernel para o userspace acontecem usando a rotina assembler doreti
, independentemente de o kernel ter sido acessado por meio de uma trap ou via syscall. Isso restaura o status do programa da pilha e retorna ao espaço do usuário.
O sistema operacional FreeBSD adere ao esquema tradicional UNIX®, onde cada processo possui um número de identificação único, o chamado PID (ID do processo). Números PID são alocados de forma linear ou aleatória variando de 0
para PID_MAX
. A alocação de números PID é feita usando pesquisa linear de espaço PID. Cada thread em um processo recebe o mesmo número PID como resultado da chamada getpid(2).
Atualmente existem duas maneiras de implementar o threading no FreeBSD. A primeira maneira é o threading M:N seguido pelo modelo de threading 1:1. A biblioteca padrão usada é o threading M:N (libpthread
) e você pode alternar no tempo de execução para threading 1:1 (libthr
). O plano é mudar para a biblioteca 1:1 por padrão em breve. Embora essas duas bibliotecas usem as mesmas primitivas do kernel, elas são acessadas por API(s) diferentes. A biblioteca M:N usa a família kse_*
das syscalls enquanto a biblioteca 1:1 usa a família thr_*
das syscalls. Por causa disso, não existe um conceito geral de ID de threading compartilhado entre o kernel e o espaço do usuário. Obviamente, as duas bibliotecas de threads implementam a API de ID de threading pthread. Todo threading do kernel (como descrito por struct thread
) possui identificadores td tid, mas isso não é diretamente acessível a partir do espaço do usuário e serve apenas as necessidades do kernel. Ele também é usado para a biblioteca de threading 1:1 como o ID de threading do pthread, mas a manipulação desta é interna à biblioteca e não pode ser confiável.
Como dito anteriormente, existem duas implementações de threads no FreeBSD. A biblioteca M:N divide o trabalho entre o espaço do kernel e o espaço do usuário. Thread é uma entidade que é agendada no kernel, mas pode representar vários números de threads do userspace. Threads M do userspace são mapeadas para threads N do kernel, economizando recursos e mantendo a capacidade de explorar o paralelismo de multiprocessadores. Mais informações sobre a implementação podem ser obtidas na página do manual ou [1]. A biblioteca 1:1 mapeia diretamente um segmento userland para uma thread do kernel, simplificando muito o esquema. Nenhum desses designs implementa um mecanismo justo (tal mecanismo foi implementado, mas foi removido recentemente porque causou séria lentidão e tornou o código mais difícil de lidar).
Linux® é um kernel do tipo UNIX® originalmente desenvolvido por Linus Torvalds, e agora está sendo contribuído por uma grande quantidade de programadores em todo o mundo. De seu simples começo até hoje, com amplo suporte de empresas como IBM ou Google, o Linux® está sendo associado ao seu rápido ritmo de desenvolvimento, suporte completo a hardware e seu benevolente modelo despota de organização.
O desenvolvimento do Linux® começou em 1991 como um projeto amador na Universidade de Helsinque na Finlândia. Desde então, ele obteve todos os recursos de um sistema operacional semelhante ao UNIX: multiprocessamento, suporte multiusuário, memória virtual, rede, basicamente tudo está lá. Também há recursos altamente avançados, como virtualização, etc.
A partir de 2006, o Linux parece ser o sistema operacional de código aberto mais utilizado com o apoio de fornecedores independentes de software como Oracle, RealNetworks, Adobe, etc. A maioria dos softwares comerciais distribuídos para Linux® só pode ser obtido de forma binária, portanto a recompilação para outros sistemas operacionais é impossível.
A maior parte do desenvolvimento do Linux® acontece em um sistema de controle de versão Git. O Git é um sistema distribuído, de modo que não existe uma fonte central do código Linux®, mas algumas ramificações são consideradas proeminentes e oficiais. O esquema de número de versão implementado pelo Linux® consiste em quatro números A.B.C.D. Atualmente, o desenvolvimento acontece em 2.6.C.D, onde C representa a versão principal, onde novos recursos são adicionados ou alterados, enquanto D é uma versão secundária somente para correções de bugs.
Mais informações podem ser obtidas em [3].
O Linux® segue o esquema tradicional do UNIX® de dividir a execução de um processo em duas metades: o kernel e o espaço do usuário. O kernel pode ser inserido de duas maneiras: via trap ou via syscall. O retorno é tratado apenas de uma maneira. A descrição mais detalhada aplica-se ao Linux® 2.6 na arquitetura i386™. Esta informação foi retirada de [2].
Syscalls em Linux® são executados (no espaço de usuário) usando macros syscallX
onde X substitui um número que representa o número de parâmetros da syscall dada. Essa macro traduz um código que carrega o registro % eax
com um número da syscall e executa a interrupção 0x80
. Depois disso, um retorn da syscall é chamado, o que traduz valores de retorno negativos para valores errno
positivos e define res
para -1
em caso de erro. Sempre que a interrupção 0x80
é chamada, o processo entra no kernel no manipulador de trap das syscalls. Essa rotina salva todos os registros na pilha e chama a entrada syscall selecionada. Note que a convenção de chamadas Linux® espera que os parâmetros para o syscall sejam passados pelos registradores como mostrado aqui:
parameter -> %ebx
parameter -> %ecx
parameter -> %edx
parameter -> %esi
parameter -> %edi
parameter -> %ebp
Existem algumas exceções, onde Linux® usa diferentes convenções de chamada (mais notavelmente a syscall clone
).
Os manipuladores de traps são apresentados em arch/i386/kernel/traps.c
e a maioria desses manipuladores vive em arch/i386/kernel/entry.S
, onde a manipulação das traps acontecem.
O retorno da syscall é gerenciado pela syscall exit(3), que verifica se o processo não está concluído e verifica se usamos seletores fornecidos pelo usuário . Se isso acontecer, a correção da pilha é aplicada e, finalmente, os registros são restaurados da pilha e o processo retorna ao espaço do usuário.
Na versão 2.6, o sistema operacional Linux® redefiniu algumas das primitivas tradicionais do UNIX®, especialmente PID, TID e thread. O PID é definido para não ser exclusivo para cada processo, portanto, para alguns processos (threading) getppid(2) retorna o mesmo valor. A identificação exclusiva do processo é fornecida pelo TID. Isso ocorre porque o NPTL (Nova Biblioteca de threading POSIX®) define threading para serem processos normais (assim chamado threading 1:1). Gerar um novo processo no Linux® 2.6 acontece usando a syscall clone
(as variantes do fork são reimplementadas usando-o). Esta syscall clone define um conjunto de sinalizadores que afetam o comportamento do processo de clonagem em relação à implementação do threading. A semântica é um pouco confusa, pois não existe uma única bandeira dizendo a syscall para criar uma thread.
Flags de clone implementados são:
CLONE_VM
- os processos compartilham seu espaço de memória
CLONE_FS
- compartilha umask, cwd e namespace
CLONE_FILES
- compartilham arquivos abertos
CLONE_SIGHAND
- compartilha manipuladores de sinais e bloqueia sinais
CLONE_PARENT
- compartilha processo pai
CLONE_THREAD
- ser a thread (mais explicações abaixo)
CLONE_NEWNS
- novo namespace
CLONE_SYSVSEM
- compartilha SysV sob estruturas
CLONE_SETTLS
- configura o TLS no endereço fornecido
CLONE_PARENT_SETTID
- define o TID no processo pai
CLONE_CHILD_CLEARTID
- limpe o TID no processo filho
CLONE_CHILD_SETTID
- define o TID no processo filho
CLONE_PARENT
define o processo real para o processo pai do requisitante. Isso é útil para threads porque, se a thread A criar a thread B, queremos que a thread B parenteada para o processo pai de todo o grupo de threads. CLONE_THREAD
faz exatamente a mesma coisa que CLONE_PARENT
, CLONE_VM
e CLONE_SIGHAND
, reescreve o PID para ser o mesmo que PID do requisitante, define o sinal de saída como none e entra no grupo de threads. CLONE_SETTLS
configura entradas GDT para tratamento de TLS. O conjunto de flags CLONE_*_*TID
define/limpa o endereço fornecido pelo usuário para TID ou 0.
Como você pode ver, o CLONE_THREAD
faz a maior parte do trabalho e não parece se encaixar muito bem no esquema. A intenção original não é clara (mesmo para autores, de acordo com comentários no código), mas acho que originalmente havia uma flag de thread, que foi então dividida entre muitas outras flags, mas essa separação nunca foi totalmente concluída. Também não está claro para que serve esta partição, uma vez que a glibc não usa isso, portanto, apenas o uso do clone escrito à mão permite que um programador acesse esses recursos.
Para programas não segmentados, o PID e o TID são os mesmos. Para programas em threadings, os primeiros PID e TID da thread são os mesmos e todos os threading criados compartilham o mesmo PID e são atribuídos a um TID exclusivo (porque CLONE_THREAD
é passado), o processo pai também é compartilhado para todos os processos que formam esse threading do programa.
O código que implementa pthread_create(3) no NPTL define as flags de clone como este:
int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM #if __ASSUME_NO_CLONE_DETACHED == 0 | CLONE_DETACHED #endif | 0);
O CLONE_SIGNAL
é definido como
#define CLONE_SIGNAL (CLONE_SIGHAND | CLONE_THREAD)
o último 0 significa que nenhum sinal é enviado quando qualquer uma das threads finaliza.
De acordo com uma definição de dicionário, emulação é a capacidade de um programa ou dispositivo de imitar um outro programa ou dispositivo. Isto é conseguido fornecendo a mesma reação a um determinado estímulo que o objeto emulado. Na prática, o mundo do software vê três tipos de emulação - um programa usado para emular uma máquina (QEMU, vários emuladores de consoles de jogos etc.), emulação de software de uma instalação de hardware (emuladores OpenGL, emulação de unidades de ponto flutuante etc.) e emulação do sistema (no kernel do sistema operacional ou como um programa de espaço do usuário).
Emulação é geralmente usada em um lugar, onde o uso do componente original não é viável nem possível a todos. Por exemplo, alguém pode querer usar um programa desenvolvido para um sistema operacional diferente do que eles usam. Então a emulação vem a calhar. Por vezes, não há outra maneira senão usar emulação - por ex. Quando o dispositivo de hardware que você tenta usar não existe (ainda/mais), então não há outro caminho além da emulação. Isso acontece com frequência ao transferir um sistema operacional para uma nova plataforma (inexistente). Às vezes é mais barato emular.
Olhando do ponto de vista da implementação, existem duas abordagens principais para a implementação da emulação. Você pode emular a coisa toda - aceitando possíveis entradas do objeto original, mantendo o estado interno e emitindo a saída correta com base no estado e/ou na entrada. Este tipo de emulação não requer condições especiais e basicamente pode ser implementado em qualquer lugar para qualquer dispositivo/programa. A desvantagem é que a implementação de tal emulação é bastante difícil, demorada e propensa a erros. Em alguns casos, podemos usar uma abordagem mais simples. Imagine que você deseja emular uma impressora que imprime da esquerda para a direita em uma impressora que imprime da direita para a esquerda. É óbvio que não há necessidade de uma camada de emulação complexa, mas a simples reversão do texto impresso é suficiente. Às vezes, o ambiente de emulação é muito semelhante ao emulado, portanto, apenas uma camada fina de alguma tradução é necessária para fornecer uma emulação totalmente funcional! Como você pode ver, isso é muito menos exigente de implementar, portanto, menos demorado e propenso a erros do que a abordagem anterior. Mas a condição necessária é que os dois ambientes sejam semelhantes o suficiente. A terceira abordagem combina os dois anteriores. Na maioria das vezes, os objetos não fornecem os mesmos recursos, portanto, em um caso de emulação, o mais poderoso é o menos poderoso que temos para emular os recursos ausentes com a emulação completa descrita acima.
Esta tese de mestrado lida com a emulação de UNIX® em UNIX®, que é exatamente o caso, onde apenas uma camada fina de tradução é suficiente para fornecer emulação completa. A API do UNIX® consiste em um conjunto de syscalls, que geralmente são autônomas e não afetam algum estado global do kernel.
Existem algumas syscalls que afetam o estado interno, mas isso pode ser resolvido fornecendo algumas estruturas que mantêm o estado extra.
Nenhuma emulação é perfeita e as emulações tendem a não ter algumas partes, mas isso geralmente não causa nenhuma desvantagem séria. Imagine um emulador de console de jogos que emula tudo, menos a saída de música. Não há dúvida de que os jogos são jogáveis e pode-se usar o emulador. Pode não ser tão confortável quanto o console original, mas é um compromisso aceitável entre preço e conforto.
O mesmo acontece com a API do UNIX®. A maioria dos programas pode viver com um conjunto muito limitado de syscalls funcionando. Essas syscalls tendem a ser as mais antigas (read(2)/write(2),fork(2)family,signal(3)handling, exit(3), socket(2) API), portanto, é fácil emular porque sua semântica é compartilhada entre todos os UNIX®, que existem hoje.
All FreeBSD documents are available for download at https://download.freebsd.org/ftp/doc/
Questions that are not answered by the
documentation may be
sent to <freebsd-questions@FreeBSD.org>.
Send questions about this document to <freebsd-doc@FreeBSD.org>.