[blog] Kernels monolíticos e microkernels

«in my proudy opinion, as an architecat, we should put a middleware under the application layer»

Sistema operativo e kernel

Antes, uma ligeira digressão: onde termina o sistema operativo e começa o kernel? Conceituá-lo como o núcleo do SO fatalmente amarra esta diferenciação ao sistema. Eu diria quem nem todo kernel é um sistema operativo, mas todo sistema operativo tem um kernel. Devido à hardware-dependência, a discussão sobre a estrutura do SO/kernel não pode ser desvinculada do hardware em que este sistema está implantado. Do ponto de vista do programador da aplicação, um SO provê uma forma de acessar o hardware (abstração), além dos mecanismos para gerenciar recursos que as aplicações demandam.

Para microprocessadores mais simples, as possíveis escolhas arquiteturais podem (e muitas vezes devem) fazer uma diferenciação entre kernel/SO e aplicação. Entretanto se na prática não houver diferenciação entre usuário e supervisor, o sistema todo é um único objeto, e a vantagem advinda desta diferenciação é majoritariamente do ponto de vista de modelagem, manutenção e reuso do software.

Durante o desenvolvimento de um sistema desta natureza, se o programador desejar, a aplicação pode acessar recursos do hardware sem passar pelo sistema operacional, como ilustra a Figura 1. Aliás, o mais simples bare-metal é conceitualmente um sistema operativo.

Figura 1. Sistema operativo baseado em bibliotecas [1]

Em microprocessadores mais complexos, nos quais existe separação entre espaço de supervisor e usuário o acesso ao hardware pela aplicação pode ser limitado por um mecanismo supervisor. Para acessar o hardware as threads da aplicação precisam executar system calls: pedir acesso aos recursos de sistema.

Na Figura 2 (a), o sistema operativo é dito “monolítico” porque é um objeto à parte, entretanto existe ainda uma simbiose entre aplicação e sistema operativo. Perceba que esta simbiose já não ocorre entre aplicação e hardware.

Figura 2. Sistemas operativos (a) monolítico (b) em camadas [1]

Na Figura 2 (b), o SO em camadas é uma arquitetura padrão para sistemas de uso mais generalista, atingido através da definição de camadas de software com o menor acoplamento possível. O layer logo abaixo da camada de aplicação poderia ser um AAL (Application Abstraction Layer), por exemplo. Também, a camada logo acima do hardware poderia ser um BSP (Board Support Package). As representações na Figura 2 entretanto, não dizem nada sobre o kernel.

Kernel Monolítico e Microkernel

Em linhas gerais, dois modelos de kernel constam na literatura: monolítico e microkernel. A diferença crucial entre ambos está no volume de recursos do sistema que correm no espaço do supervisor e no espaço do usuário.

A Figura 3 ilustra conceitualmente as duas abordagens. No microkernel um conjunto menor de abstrações corre em kernel space. Assim, programas de sistema, como o sistema de arquivos, device drivers, etc., estarão também no domínio de usuário e operam como servidores. No domínio do kernel ficam os módulos mais essenciais, o escalonador e a comunicação inter-processos, por exemplo. A função primordial do microkernel é gerenciar recursos, e em menor parte abstrair o hardware. A modularidade é um ponto forte da arquitetura, normalmente descrita como um conjunto de layers com baixo acoplamento, no modelo cliente-servidor.

Figura 3. Kernel Monolítico vs Microkernel [2]

O kernel monolítico por sua vez concentra todos os serviços em uma única unidade de compilação, e somente a aplicação estará a correr no espaço de usuário. Neste tipo de sistema o kernel além de um gerenciador de recursos é também uma extensão da máquina (“extended machine”), porque abstrai completamente o hardware para o usuário.

Kernel + SO

Na Figura 4 uma ilustração conceitual famosa do GNU/Linux (Linus Torvalds é um grande defensor dos monolíticos). As bibliotecas (glibc) e as aplicações estão em user space. No kernel space existe uma parte que é “universal” para todos os Linuxes, e outra que depende da plataforma em hardware.

Figura 4. GNU/Linux [4]

Ainda em domínios de aplicação mais generalistas, o Minix do Prof. Tanembaum é um microkernel raíz. Os layers bem definidos correm a maior parte do sistema no espaço de usuário. Uma alegada vantagem deste tipo de arquitetura é que caso o driver de um dispositivo entre em deadlock, o kernel pode resetar o sistema. Em um sistema com kernel monolítico isto não seria possível porque o sistema estaria trancado já em modo supervisor. Os microkernels são entretanto conhecidos por serem mais lentos – em sistemas de uso generalista, principalmente – além de adicionarem complexidade à comunicação inter-processos, que pode ser eventualmente explorada por código malicioso.

Figura 5. Minix 3 [5]

Outros flavours para microkernel são possíveis. Na Figura 6a, o SO monolítico significa que todos os servidores estão concentrados em um único programa, e as aplicações utilizam-se de clientes para acessar estes serviços concentrados. Na Figura 6b, os servidores estão distribuídos modularmente. Na Figura 6c., uma aplicação especializada (dedicada) interage diretamente com o microkernel, sem um SO propriamente dito.

Figura 6. Sistemas com microkernel: a) SO monolítico com Microkernel b) SO distribuído com microkernel c) Sistema monolítico com microkernel [3]

[1] RANKL, Wolfgang. Smart Card Handbook. 2010

[2] KEN, Yu. RTOS Model and Simulation using System C. 2010

[3] HERDER, Jorrit. Torwards a true Microkernel Operating System. 2005

[4] Anatomy of the Linux kernel – IBM Developer

[5] https://upload.wikimedia.org/wikipedia/commons/7/7d/The_MINIX_3_Microkernel_Architecture.png

[blog] O Amplificador Diferencial

Comentários básicos sobre este workhorse em eletrônica. O conceito de amplificador diferencial é elemento-chave em uma infinidade de aplicações analógicas: buffers, filtros, osciladores, reguladores de tensão, referências de tensão/corrente, processamento de sinais, PLLs, etc., devido às suas propriedades úteis. As figuras foram retiradas do livro CMOS Circuit, Design, Layout and Simulation (Baker).

Figura retirada de analog.com

O Par diferencial

O conceito de diferencial é comum em engenharia e consiste na ideia de que determinado fluxo de energia é distribuído entre dois ramos ‘complementares’ de um dispositivo (no automóvel, isto permite que uma roda gire mais rápido que a oposta para que possamos fazer uma curva – cada uma das rodas vai percorrer diferentes comprimentos de arco no mesmo tempo). Em eletrônica o par diferencial nos permite principalmente eliminar ruídos de modo-comum (neste caso especificamente aqueles provindos de uma mesma fonte DC que alimenta os circuitos que geram os sinais AC de entrada, e possivelmente o amp-op).

O Amplificador Diferencial

A apresentação clássica do amplificador diferencial é assim:

Vout = (V+ – V)*Ganho [V]

Também sempre é dito que idealmente temos:

  • impedância de entrada infinita
  • impedância de saída nula
  • ganho infinito em malha aberta
  • rejeição de modo comum infinita

Vou tentar mostrar aqui porque estas características são importantes, utilizando circuitos analógicos CMOS básicos e análises intuitivas.

Talvez a primeira coisa que vem à mente de quem é introduzido ao assunto é que se o ganho é muito grande (como deve ser), a tensão Vout vai explodir. Primeiramente, esta equação está em malha aberta, sem a realimentação negativa, quando parte do sinal de saída é subtraído do sinal de entrada. Depois, na maior parte das vezes estamos interessados em trabalhar em pequenos sinais AC – centésimos ou milésimos de Vdd (v+, v-). Cabe ainda dizer que ganho mede-se em V/V (é um número sem dimensão), e independente do seu tamanho a máxima tensão alcançada será sempre Vdd.

Os pequenos sinais (v+,- v-) variam em torno da polarização DC (V+, V-). Polarizar significa determinar um ponto de operação do circuito, de cada transistor na verdade (em elementos semicondutores, as características elétricas variam conforme às tensões elétricas a que o dispositivo é submetido).  Na figura acima o ganho de malha aberta (open-loop) é mostrado como função da frequência do sinal de entrada. Uma topologia clássica do circuito amplificador diferencial em CMOS é assim:

(PS: transistores NMOS estarão em região linear se VGS >= Vth, e PMOS com VSG >= Vth e IDS = f(VDS,VGS), onde f assume formas distintas a depender da região de operação)

A corrente Iss é resultado da tensão de gate em M6, que estará com a corrente estável em um range de tensão dreno-fonte. Supondo uma tensão Vn, de modo comum aplicada aos gates de M1 e M2, a corrente drenada por M6 seja um valor Iss = Id1 + Id2.

Se extrairmos a tensão de saída no dreno de cada um dos transistores M1 e M2, Vout1,2= Id1,2*RCarga1,2. Isto basicamente diz que o ganho Av=Vout/Vin é proporcional à transcondutância do transistor em determinado ponto de polarização multiplicado pela carga, pois ID/Vin = Gm = 1/(Rds).

A saída Vout diferencial é VD1-VD2 ou ao contrário, a depender do que estabelecermos como saída positiva e negativa. Com a topologia acima é impossível dizer qualquer coisa sobre qual será a entrada positiva e qual será a negativa. Só podemos dizer isto quando definirmos qual a entrada que aumenta a tensão de saída, e a entrada que a diminui.

Para tanto, completa-se o par diferencial com uma carga espelho de corrente do tipo PMOS, uma carga ativa.

Se a tensão do gate M1 aumenta, mais corrente é drenada, maior a tensão VSG de M3 e M4 (ou menor a tensão VGS, se preferir). Se ambos M1 e M2 tiverem a mesma tensão de gate, a corrente no sentido dreno-fonte de cada elemento do par será ID1 = ID2 = Iss/2 [A]. Se tensão de gate em M1 aumenta em relação à tensão de gate M2, menos corrente é drenada por M2 e portanto menor a queda dreno-fonte em M2. Se M2 continuar no ponto de operação desejável, sua (trans)condutância é constante. Se a condutância de M2 mantém-se constante, mas sua corrente diminui, a tensão Vout = VDD – VSD4 obrigatoriamente aumenta. Então VG1 = V+. O raciocínio inverso vale para V: quanto maior a corrente drenada em M2, menor a tensão Vout. É este balanço na entrada que nos permite reduzir drasticamente o ruído de modo comum, se comparado a um amplificador de entrada única. Se adicionarmos mais um estágio de amplificação teremos ganho suficiente para chamá-lo de amplificador operacional. Tradicionalmente, o próximo estágio seria um amplificador PMOS, dreno-comum, com carga NMOS.

Ainda nesta topologia de só um estágio, a curva de transferência assume o seguinte, sendo o eixo horizontal a tensão de modo comum Vin nos gates do par diferencial, e no eixo vertical, Vout a tensão medida nos drenos de M2 e M4.

A derivada no ponto de polarização é o ganho do amplificador quando polarizado com a tensão correspondente Vin. Um bom ponto de polarização é aquela cuja derivada em relação ao pequeno sinal vin mantenha o maior intervalo em vout sem perturbar a transcondutância de M2 // M4. Idealmente é um ponto com derivada infinita. Pois para o sinal vout não remover M2 e M4 do ponto de operação desejável, as correntes drenadas não podem causar quedas de tensão que retire os transistores do ponto de operação, o que no limite significa impedância nula no nodo vout. Para a impedância de entrada ser infinita, a variação da tensão vgs de M1 e M2, não deve causar variação de tensão vgs + Vgs que retire o transistor da região de operação desejada. Se for saturado na região linear, a tensão dreno-fonte precisa manter-se em Vds >= tensão de overdrive (Vgs – Vth). A rejeição de modo comum será tão boa quanto a precisão do espelhamento de corrente – o mais iguais o módulo das correntes de dreno de M3 e M4, mais ruídos do sinal de tensão de modo-comum Vin estarão subtraídos no nodo Vout = Iout*RLout.

[blog] Synchronous versus asynchronous reset on ASIC Design

Reset is about getting a system back to a known initial state. Temporary data is flushed. When I say data I mean 0s and 1s. Everything will be put to a known state so the circuit starts up. Sounds important.

When we go for synchronous or asynchronous reset, active low, our HDL code will look like one of these:

module synch_reset_pipeline(in1, in2, ..., out1, out2,...);
// #1 module with synchronous reset
always @(posedge clk) 
begin
  if (!rst) begin
   // reset logic: put this module to known state 
  end
  else begin
  // functional logic: processes data
  end
end
endmodule
module asynch_reset_pipeline(in1, in2, ..., out1, out2,...);
// #2 module with asynchronous reset
always @(posedge clk or negedge rst) 
begin
  if (!rst) begin
   // reset logic 
  end
  else begin
  // functional logic
  end
end
endmodule

To simplify, firstly, think about a single sequential element (a flip-flop). Then, imagine it is starting up, catching the very first data signal arriving on the input and propagating it to other pipeline stages. How would be a desirable reset signal? I would say:

  • it resets the circuit to a known state
  • not prone to any metastability
  • the duty cycle is just long enough to meet the above criteria

Given the above bullets what is the go for approach? Whatever fits the system better, on area, timing and power. On module #1, we are telling the tool that the reset signal is under the domain of the clock, and the reset logic will take place when reset level is low. So when the clock positive edge triggers and reset is asserted, the flops that are connected to that logic will reset. Therefore, the reset signal is pretty much another data signal (like: assign current_input = (!rst_sel) ? reset_value : next_input), selecting between the next value to be sampled or the reset data value, when a reset happens. And we should make sure it will be filtered from glitches and never prone to any meta-stability – but this is valid to any other external input too. It cannot take the fastest way through the datapath when asserted, otherwise it might reset the logic violating setup requirements, and therefore put it to a unknown state, that will be sampled and propagated. The deassertion by its turn need to respect the hold time of the signals that were propagated through the pipeline. As data and reset are on the same clock domain, the work relies pretty much on the clock tree synthesis. It costs more circuitry in general. (Although flops are smaller, they say)

On #2, the asynchronous reset is a high priority interruption, per say. The or inside the sensitivity list changes everything. Whenever reset is low (asserted) it comes to bring everything down and up again. If the pulse has a very low duty cycle, maybe the flop will not be stable yet, and will put the the circuit to an unknown state when reset is released; maybe a very bad combination (boom). That is, we need to model this asynchronous path to respect a time after asserting and before deasserting the signal, so we make sure every pipeline stage is at a known state. To prevent timing violation when asserting, we need to make sure the reset will take place respecting the (worst) removal time, so the initial data to be sampled is valid. For deasserting, we need to make sure the clock samples a stable value, by respecting the (worst) recovery time. Besides, you need to tell the tool that the signal that multiplex the input to your flops between operation data and reset data is a false path – that is the clock must ignore the impact of this signal on the data path, and not make any efforts to support setup and hold timings of the sequential elements. You will take care by yourself by controlling the reset behavior. A technique could be to model the buffers safely enough to charge and discharge on a determined relative time, so the reset removal and recovery time are always safe. That is you fix a positive delay for reset assertion and a negative delay for deassertion, regarding the active clock edge. You will need to synchronize the input reset signal to a common in both aproaches to mitigate metastability. Asynchronous reset costs less circuitry, but is also more complex: a very critical asynchronous control signal is on the game.