Vinícius Vidal

Full-Stack Developer

Básico de Hardware - Processador (CPU)

Acredito que, além de escrever código, um programador deve ter um conhecimento mínimo sobre o ambiente em que trabalha.

Eu também já achei que essas coisas não importavam. Porém com o tempo percebi que ter esse conhecimento te faz um programador melhor, te ajudam com problemas aparentemente sem explicação.

”Com 20% do conhecimento você consegue resolver 80% dos problemas. Mas pra resolver os últimos 20%, vai precisar dos 80% de conhecimento que ainda não tem.” Palavras que ouvi em um vídeo do Fabio Akita. Ter o 20% que falta te fará excepcional.

Trago uma análise bem simplória e direta de um processador. Entender o funcionamento de uma CPU moderna é algo exorbitante. ”Processadores reais, memórias, discos e outros dispositivos são muito complicados e apresentam interfaces difíceis, desajeitadas, idiossincráticas e inconsistentes para as pessoas que têm de escrever softwares para elas utilizarem.”

Palavras do livro "Modern Operating Systems, 4th ed", que estou tomando como guia enquanto escrevo.

Espero que aqueles que lerem consigam sair desse post sabendo algo a mais. 😁

O que é CPU

O processador, ou CPU, acrônimo para “Central Processing Unit”, é considerada o “cérebro” do computador. Executa programas repetindo continuamente o seguinte ciclo: busca uma instrução na memória, entende o que precisa ser feito (decodifica), executa a ação e então passa para a próxima instrução. Esse processo se repete até o fim do programa.

image.png

CPUs podem conter diferentes arquiteturas, portanto um processador x86 não pode executar programas ARM, e um processador ARM não pode executar programas x86. Cada arquitetura possui seu conjunto de instruções na qual podem executar.

O que exatamente é uma instrução e como é executada?

As instruções são basicamente linguagem de máquina, uma sequência de bits (zeros e uns) que representam instruções codificadas de acordo com as regras da arquitetura do processador.

Mas como o processador entende que essa sequência aparentemente aleatória (não é!) significa algum comando?

A resposta está no que chamamos de ISA (Instruction Set Architecture), que funciona como um dicionário do processador e representa exatamente quais instruções ele reconhece e como elas são representadas em bits. Por isso, um programa ARM não irá executar em um processador x86: as instruções do programa não estarão presentes no “dicionário”.

É importante entender, a CPU não interpreta código. Quando falamos que ela “entende” um comando, isso quer dizer que os bits da instrução seguem por caminhos físicos no chip e acionam transistores específicos que realizam a operação correspondente.

Assembly 😰

Como seres humanos não entendem código de máquina, é impraticável escrever programas com zeros e uns, então é necessário um intermédio, para isso surgiu uma linguagem um pouco mais amigável, o Assembly, e o seu compilador o Assembler, cumpre essa tarefa de transformar texto em linguagem de máquina.

O assembly é uma linguagem de baixo nível que representa essas instruções binárias de forma textual. Cada comando em Assembly geralmente se traduz diretamente em uma instrução de máquina, tornando-se um espelho quase 1:1 do que realmente será executado pelo processador.

image.png

Diferentes Assemblers

Como já mencionado, cada arquitetura (x86, ARM, RISC-V...) tem seu próprio conjunto de instruções. Um mesmo comando em C pode virar códigos bem diferentes em cada uma delas, embora o resultado final seja o mesmo.

Para cada arquitetura, existe um Assembler específico, responsável por traduzir o Assembly daquela arquitetura para seu respectivo código de máquina. Por exemplo, um assembler para x86 não entende Assembly para ARM, e vice-versa.

Mesmo que você nunca vá escrever código Assembly na prática, é interessante entender que todo código de alto nível (C, JavaScript ou Python, entre outros) acaba sendo traduzido, no fim das contas, para uma sequência de instruções simples que o processador realmente entende.

Registradores

Buscar dados na memória é lento comparado à execução, então processadores possuem registradores, que são pequenas áreas de armazenamento extremamente rápidas dentro da própria CPU. Eles armazenam variáveis temporárias, valores intermediários e endereços.

O conjunto de instruções inclui comandos que transferem dados entre registradores e memória. Existem ainda registradores especializados em realizar operações matemáticas e lógicas, controladas pela ALU (Unidade Aritmética e Lógica), uma FPU (Unidade de Ponto Flutuante) para operações com números reais, entre diversos outros.

Além disso, caches (L1, L2, L3) ajudam a reduzir o tempo de acesso à memória RAM, funcionando como camadas intermediárias entre os registradores e a memória principal.

Alguns registradores especiais

Dentre os registradores gerais, há alguns especiais que o programador tem acesso, como o program counter, o stack pointer e o PSW (Program Status Word).

O program counter contém o endereço de memória da próxima instrução a ser executada, funcionando como uma espécie de guia que orienta a CPU sobre "o que vem a seguir" no programa.

O stack pointer é um registrador que aponta para o topo da pilha na memória. A pilha é uma estrutura do tipo LIFO (Last In, First Out) usada principalmente para controlar chamadas de funções. Sempre que uma função é chamada, o computador cria um stack frame, que é uma área reservada na pilha para guardar os parâmetros de entrada, as variáveis locais e os valores temporários dessa função. Quando a função termina, esse frame é removido da pilha, e o ponteiro é atualizado.

Por exemplo:

function sum(a, b) {
  const result = a + b;

  return result;
}

Na chamada sum(2, 3), um frame com a = 2, b = 3 e result = 5 é alocado na pilha.

O stack pointer aponta para esse frame enquanto a função está sendo executada. Ao final da execução, o frame é removido da pilha e o ponteiro retorna ao estado anterior.

O PSW (Program Status Word), por fim, é outro registrador essencial, pois ele guarda diversas informações sobre o estado atual da CPU. Entre essas informações estão: Os bits de condição, definidos por instruções de comparação (por exemplo, se um número é maior que outro), a prioridade da CPU, o modo de operação (modo usuário ou modo kernel), e outros bits de controle. Programas de usuário normalmente podem ler todo o conteúdo do PSW, mas só podem escrever em alguns campos. O PSW é especialmente importante para o funcionamento de system calls (será explicado posteriormente) e I/O, pois ajuda a controlar o comportamento e os privilégios do código que está sendo executado.

32 bits vs. 64 bits

Você provavelmente já ouviu falar em processadores de 32 e 64 bits, e que os de 64 bits são melhores. Mas por quê?

Essas duas opções referem-se à largura dos registradores da CPU, o que afeta diretamente a capacidade de endereçamento de memória, o tamanho dos operandos que podem ser manipulados e o desempenho geral do sistema.

Lembrando que, ao falar de largura, estamos nos referindo à quantidade de bits que os registradores conseguem armazenar, não ao tamanho físico.

Um sistema de 32 bits pode endereçar no máximo 2³² bytes, algo em torno de 4 GB (gigabytes) de RAM, ou seja, o processador só consegue acessar e usar até 4 GB de memória ao mesmo tempo. Por conta disso, esses sistemas não são adequados para executar softwares pesados ou que exigem muitos recursos.

Já um sistema de 64 bits pode, teoricamente, endereçar até 2⁶⁴, impressionantes 16 milhões de terabytes de RAM! Obviamente, nenhum dispositivo atualmente chega nem perto desse limite, e provavelmente não chegará por muitas décadas, mas o ponto importante é que sistemas de 64 bits podem usar mais do que os 4 GB de RAM que limitam os sistemas de 32 bits. Eles também são compatíveis com softwares antigos de 32 bits, enquanto o contrário nem sempre é possível: sistemas de 32 bits podem ter dificuldades para rodar programas feitos para 64 bits, especialmente por causa da limitação de memória.

image.png

Além da memória, a largura dos registradores também influencia na performance. Com registradores de 64 bits, o processador consegue manipular números maiores e realizar operações com maior precisão e velocidade, reduzindo a quantidade de instruções necessárias para lidar com grandes volumes de dados. Isso significa que programas compilados para 64 bits podem ser mais rápidos e eficientes do que suas versões de 32 bits.

Por fim, os sistemas operacionais de 64 bits são capazes de rodar tanto programas de 64 bits quanto de 32 bits (graças a modos de compatibilidade), mas o inverso não é verdadeiro: sistemas de 32 bits não conseguem executar programas de 64 bits, nem utilizar mais de 4 GB de RAM, independentemente da quantidade instalada na máquina.

Kernel mode vs. User mode

O processador opera em dois modos principais: modo kernel e modo usuário, e essa distinção é essencial para a segurança, estabilidade e funcionamento do sistema operacional.

No modo kernel, o processador tem acesso total a todos os recursos do sistema, como memória, dispositivos de entrada/saída (I/O) e operações privilegiadas. Esse modo é utilizado pelo sistema operacional para gerenciar o hardware, controlar processos e alocar recursos. O código executado nesse modo pode realizar operações críticas, como manipulação direta de memória e controle de dispositivos.

O modo usuário é restrito e usado para a execução de programas e aplicativos. Nesse modo, o código tem acesso limitado aos recursos do sistema, sem a capacidade de acessar diretamente a memória ou dispositivos críticos. Isso garante que falhas em programas de usuários não comprometam a integridade do sistema operacional ou de outros processos.

image.png

Quando um programa em modo usuário necessita realizar uma operação que exige permissões elevadas, como acessar arquivos do sistema ou alocar memória, ele faz uma system call (chamada de sistema). Ao fazer uma system call, o código em modo usuário faz uma interrupção de software, o que provoca uma transição do modo usuário para o modo kernel. Dessa forma, o sistema operacional executa a tarefa solicitada e, após concluí-la, o processador retorna ao modo usuário, permitindo que o programa continue sua execução. Segue a ordem de execução:

  1. Programa em modo usuário pede acesso a recurso privilegiado.
  2. System call é feita → transição para modo kernel.
  3. Kernel executa a operação.
  4. Retorno ao modo usuário.

Um exemplo de system call em C:

#include <stdio.h>
#include <unistd.h>

int main() {
    char *pathname = "example.txt";

    if (access(pathname, R_OK) == -1) {
        printf("%s: File not found or not readable\n", pathname);
    } else {
        printf("%s: File exists and can be read\n", pathname);
    }

    return 0;
}

Nesse exemplo, o programa em modo usuário utiliza a função access para verificar se o arquivo example.txt pode ser lido. Essa função faz uma system call, que provoca a transição para o modo kernel. O kernel realiza a verificação de permissões e devolve o resultado ao programa. Após isso, o programa continua sua execução em modo usuário.

O PSW, citado anteriormente, controla a transição entre os modos kernel e usuário. Ele registra o modo de operação e, quando ocorre uma system call ou interrupção (execuções podem ser interrompidas), é atualizado para refletir a mudança de contexto. Após a execução da system call, o PSW garante que o processador retorne ao modo usuário de forma segura, preservando o estado do sistema.

Interrupções

Durante a execução de um programa, podem acontecer eventos que exigem a atenção imediata da CPU. Para que esses eventos sejam tratados de maneira correta, existe um mecanismo em processadores chamados de interrupções. As interrupções podem ser classificadas em três categorias principais:

  1. Interrupções de hardware

São geradas por dispositivos físicos (como teclado, mouse, disco, placa de rede). Exemplos:

  • Teclado: tecla pressionada → interrupção → a CPU lê o caractere.
  • Disco: quando termina de ler/escrever dados.

Tais eventos que exigem tratamento de forma imediata e eficiente.

  1. Interrupções de software

São acionadas por programas em execução que solicitam serviços ao sistema operacional. Como no caso de system calls, explicado anteriormente.

  1. Exceções (ou interrupções internas)

São geradas pela própria CPU durante a execução de instruções que resultam em alguma condição anormal.

Exemplos:

  • Divisão por zero.
  • Acesso inválido à memória.
  • Instrução ilegal.

Nesses casos, a interrupção serve para avisar que algo deu errado, e o sistema pode reagir de diferentes formas: encerrar o processo, lançar um erro, ou até reiniciar o sistema em situações mais críticas.

Pipelining

Com o objetivo de otimizar a performance, os responsáveis pelo design de processadores criaram uma técnica chamada pipelining. Essa abordagem permite que múltiplas instruções sejam processadas simultaneamente, mas em diferentes estágios. Em vez de esperar que uma instrução seja completamente executada antes de começar a próxima, o pipelining divide o processo de execução em várias etapas (como busca, decodificação e execução), e cada etapa pode trabalhar em uma parte de uma instrução enquanto outras instruções avançam para estágios seguintes. Isso permitiu que a CPU consiga executar mais operações em menos tempo, melhorando significativamente a perfomance e eficiência de um processador.

O que é um núcleo

Um núcleo (ou core) é uma unidade de processamento independente dentro de uma CPU. Cada núcleo é basicamente uma CPU em miniatura, com sua sua própria capacidade de buscar, decodificar e executar instruções de código. Em um processador com múltiplos núcleos, esses núcleos podem trabalhar em conjunto para processar diferentes tarefas, ou até mesmo dividir uma tarefa maior entre si. Esse processo é chamado de multithreading.

Multithreading

No ano de 1965, Gordon Moore, co-fundador da Intel, formulou o que é conhecido por Moore’s Law (Lei de Moore), que prevê que o número de transistores em um chip de silício dobraria aproximadamente a cada dois anos, resultando em um aumento exponencial no poder de processamento dos computadores. Com essa crescente abundância de transitores, o próximo passo foi aumentar a capacidade de execução paralela dentro dos processadores. Em vez de depender exclusivamente de uma única linha de execução, os engenheiros começaram a projetar CPUs capazes de lidar com múltiplas tarefas simultaneamente, o que deu origem ao multithreading moderno.

Uma thread (ou linha de execução) é a menor unidade de processamento que pode ser executada de forma independente dentro de um programa (falarei mais sobre em outro post). Disso vem o nome multithreading, são mais de uma thread executando em paralelo.

Processadores dual-core possuem dois núcleos, ou seja, conseguem executar duas threads em paralelo, processadores quad-core possuem 4 núcleo, e assim por diante.

image.png

Hyperthreading (ou SMT - Simultaneous Multithreading)

Imagine que um núcleo do processador é como uma estação de trabalho com todas as ferramentas para executar tarefas. Quando ele está processando algo, nem sempre todos os recursos internos estão sendo usados ao máximo, algumas partes ficam ociosas por instantes, esperando dados da memória ou o fim de uma operação anterior.

O Hyperthreading, nome comercial da Intel para SMT, tenta resolver isso. Ele permite que um único núcleo execute duas threads (linhas de execução) ao mesmo tempo, compartilhando os recursos internos da melhor forma possível. Em vez de deixar partes do núcleo paradas, ele aproveita para executar instruções de outra thread enquanto espera.

Na prática, o sistema operacional vê dois núcleos lógicos, mas fisicamente é um só. O ganho de desempenho real depende da carga de trabalho: em alguns casos, o aumento é pequeno; em outros, pode ser significativo.

image.png

É importante notar que não dobra a performance, já que os dois threads ainda disputam os mesmos recursos físicos, mas ajuda a aproveitar melhor o que já existe.

E por hoje é isso.

Até a próxima, dev! 🤠