Estágio 01 · 01-02
LockedO sistema operacional (SO) é o software que multiplexa a CPU, a memória, o disco e a rede entre múltiplos programas que pensam que cada um tem a máquina inteira. Sem entender o SO, conceitos como event loop, worker threads, epoll, process.fork, permission denied, EAGAIN, SIGTERM, pipe, mount parecem mágica.
Exemplos onde desconhecimento custa caro:
ulimit -n. Você não sabia que sockets, files, pipes, todos são FDs e há limite por processo.flock no mesmo arquivo. Você não conhecia file locks.fs.readFile e o Node mata o processo com OOM. Você não sabia distinguir leitura síncrona/buffered vs streaming.Este módulo te dá o vocabulário e os mecanismos do SO que sustentam toda a stack de runtime (Node, Postgres, Redis, Docker, Kubernetes).
O kernel é um programa especial que roda em modo privilegiado da CPU. Ele tem acesso direto a hardware (CPU, RAM, disco, rede). Aplicações rodam em user space, em modo não-privilegiado, e não podem tocar hardware diretamente.
┌────────────────────────────────────────┐
│ User space (apps, libs, runtime) │
│ Node, postgres, redis, etc │
└────────────────────┬───────────────────┘
│ system calls
▼
┌────────────────────────────────────────┐
│ Kernel (Linux, BSD, etc) │
│ Scheduler, VM, FS, Network, Drivers │
└────────────────────┬───────────────────┘
│
▼
Hardware
Quando uma app precisa fazer algo privilegiado (ler arquivo, abrir socket), ela faz uma system call (syscall), uma chamada que passa controle pro kernel via interrupção de software. Após o kernel executar, retorna ao user space.
Custo de syscall: ~100-1000 ns dependendo da operação. Não é grátis. Por isso runtimes como Node fazem batching (uma syscall writev em vez de várias write).
Um processo é uma instância em execução de um programa. Cada processo tem:
wait), stoppedCriação de processo (Linux): fork() cria uma cópia exata do processo atual. O filho recebe um PID novo, herda FDs do pai, e continua a execução do mesmo ponto do código. Geralmente o filho então faz exec() pra trocar o programa em execução por outro (assim que bash roda comandos).
Process tree: Linux tem init (PID 1) como ancestral de todos. pstree mostra a árvore.
Zombie process: quando processo termina, seu exit status fica esperando o pai chamar wait(). Se o pai nunca chama, o filho fica como zombie (consome só uma entrada na process table). Se o pai morre antes do filho, o filho é "adotado" por PID 1.
Uma thread é uma sequência de execução dentro de um processo. Threads do mesmo processo:
Vantagem: comunicação rápida via memória compartilhada. Custo: sincronização (mutex, semáforos, atomics) é difícil. Race conditions e deadlocks são fáceis de introduzir.
Em Node: o seu código JS roda em uma única thread (a main thread). Mas o Node usa thread pool internamente (libuv) pra I/O bloqueante (filesystem, DNS, crypto). Worker Threads (módulo node:worker_threads) permitem JS paralelo.
O kernel escalona threads/processos sobre os cores físicos (CPUs). Componentes:
Linux scheduler atual: CFS (Completely Fair Scheduler). Mantém uma red-black tree de threads runnable, ordenada por vruntime (tempo virtual de CPU acumulado). Sempre escolhe a thread com menor vruntime, daí "fair".
Estados de thread:
Quando uma thread faz syscall bloqueante (read num socket sem dado), o kernel a coloca em Sleeping. Quando o evento ocorre (dado chega), thread vai pra Runnable.
Implicações práticas:
taskset, sched_setaffinity) trava thread em cores específicos, útil pra cache locality em workloads críticas.CFS reinou de 2007 até 2024. A partir do Linux 6.6 (out/2023), o kernel mainline adotou EEVDF (Earliest Eligible Virtual Deadline First) substituindo CFS pra workloads não-realtime. Mudança discreta pra usuário comum, relevante pra quem ajusta latência fina.
EEVDF em uma frase: cada thread recebe um deadline virtual; scheduler sempre roda quem está "elegível" (acumulou direito) com menor deadline. CFS minimizava unfairness entre quem rodou; EEVDF agrega slice/lag explícitos, mais fácil raciocinar sobre latência tail.
Por que mudou:
sched_min_granularity_ns, etc.) pra balancear interatividade vs throughput. EEVDF expressa o trade-off via slice por entidade.Outras classes de scheduler em Linux (não substituídas por EEVDF):
SCHED_FIFO/SCHED_RR (real-time, prioridade fixa). Usada em audio, controle industrial. Sem timesharing, pode ser starver.SCHED_DEADLINE (EDF, Earliest Deadline First). Real-time hard. Você declara (runtime, deadline, period) e kernel admite só se cabe.SCHED_IDLE (background, prioridade mais baixa que normal).chrt muda a classe de um processo.Windows scheduler: multilevel feedback queue com 32 prioridades. Foreground apps recebem boost (UI responsivo), I/O-bound idem. Não é "fair" no sentido CFS, é "responsivo". A partir do Windows 11, há Thread Director que coopera com Intel hybrid CPUs (P-cores + E-cores) pra colocar work certo no core certo.
macOS/BSD scheduler: Mach + BSD scheduler layer. Threads têm quality of service class (QOS_CLASS_USER_INTERACTIVE, ..._USER_INITIATED, ..._UTILITY, ..._BACKGROUND). Apple Silicon tem heterogeneous cores (P/E), scheduler decide energy/perf.
Implicações práticas pra Senior:
SCHED_FIFO ou SCHED_DEADLINE em vez de só nice. Cuidado com starvation.cpu.weight é o que você ajusta em K8s resources.requests.cpu.taskset em P-core/E-core importa. Background scrapers em E-core, hot path em P-core.perf sched, bpftrace (eBPF), ou schedviz pra ver decisões reais do scheduler.Aplicação não chama syscalls diretamente, chama wrappers da libc (em C, libc é a implementação que faz a syscall). Em outras linguagens, há equivalente (Node usa libuv que chama syscalls).
Syscalls clássicas que você precisa saber existir:
| Categoria | Syscalls | O que faz |
|---|---|---|
| Process | fork, execve, wait, exit, getpid | Criar/finalizar processos |
| Memory | mmap, munmap, brk, mprotect | Alocar memória virtual |
| File I/O | open, read, write, close, lseek, stat | Ler/escrever arquivos |
| Filesystem | mkdir, unlink, rename, chmod, chown | Manipular FS |
| Network | socket, bind, listen, accept, connect, send, recv | TCP/UDP |
| I/O multiplex | select, poll, epoll_*, kqueue (BSD), IOCP (Windows) | Gerenciar muitos FDs |
| Signals | kill, signal, sigaction | Inter-process signaling |
| Time | clock_gettime, nanosleep | Relógio, sleep |
| IPC | pipe, socketpair, shmget, mq_open | Inter-process communication |
Use strace -f em qualquer processo Linux pra ver as syscalls que ele faz. Faça isso uma vez com node script.js: você vai entender o que o runtime está realmente fazendo.
Tudo no Linux é arquivo: ou pelo menos é exposto via API de arquivo. Sockets, pipes, terminais, arquivos regulares, dispositivos, tudo é representado por um file descriptor (FD): um inteiro pequeno que indexa uma tabela por processo.
FDs especiais:
Quando você abre arquivo (open), o kernel retorna o FD numericamente menor disponível. Quando faz socket, idem. Quando faz pipe, retorna dois FDs (read end + write end).
Limite de FDs por processo: ulimit -n (default 1024 ou 65536, varia). Servidores de alta concorrência aumentam pra milhões. Cada socket aberto consome 1 FD.
Closing FDs é responsabilidade do processo. Não fechar = leak. Use try/finally (em qualquer linguagem) ou RAII (C++/Rust).
Tabela de FDs após fork: o filho herda cópia da tabela. Os mesmos FDs apontam pras mesmas entries no kernel, então pai e filho compartilham posição em arquivos abertos!
Imagine read(fd, buf, 1024):
Sleep até dado chegar. Simples, mas escala mal, uma thread por conexão pra um servidor web é caro.O_NONBLOCK): se não há dado, retorna imediatamente com EAGAIN/EWOULDBLOCK. Aplicação tem que pollar/voltar depois. Permite uma thread gerenciar muitos FDs.select/poll/epoll): thread bloqueia em muitos FDs ao mesmo tempo, acorda quando qualquer um tem dado pronto. epoll (Linux) é eficiente até 100k+ conexões, usado pelo libuv (Node), nginx, redis.io_uring): kernel faz a operação em background, acorda app quando termina. Mais eficiente mas mais complexo.Por que isso importa pra Node:
epoll/kqueue/IOCP (via libuv) pra esperar muitos FDs.Signal é uma notificação assíncrona enviada pelo kernel a um processo. Lista clássica:
SIGINT (Ctrl+C), interrupçãoSIGTERM, pedido educado pra terminar (default kill <pid>)SIGKILL (9), terminação forçada, não captávelSIGSEGV, segmentation fault (acesso a memória inválida)SIGCHLD, filho terminouSIGPIPE, escreveu em pipe sem leitorSIGUSR1, SIGUSR2, definidos pelo usuárioAplicações podem capturar signals (exceto SIGKILL e SIGSTOP) com signal() ou sigaction(). Em Node: process.on('SIGTERM', handler).
Padrão importante: graceful shutdown. Captura SIGTERM, fecha conexões abertas, espera in-flight requests terminarem, depois encerra. Kubernetes envia SIGTERM, espera terminationGracePeriodSeconds, depois SIGKILL.
Mecanismos pra processos se comunicarem:
| no shell): stream unidirecional, criada com pipe() ou ao spawnar com popen. Usado em Node via child_process./var/run/docker.sock), Postgres (default usa Unix socket pra conexões locais).shmget, mmap com MAP_SHARED): regiões de memória mapeadas em múltiplos processos. Mais rápido, mas exige sincronização manual.Cada arquivo tem owner (UID), group (GID) e bits de permissão:
chmod 755 file = rwxr-xr-x.Bits especiais:
sudo funciona internamente./tmp), só owner pode deletar arquivos.Princípio de menor privilégio: rode aplicações com usuário não-root (Docker USER appuser). Capabilities Linux (CAP_NET_BIND_SERVICE, etc.) permitem dar permissões granulares sem dar root inteiro.
Pra passar o Portão Conceitual, sem consultar:
SCHED_OTHER / SCHED_FIFO / SCHED_RR / SCHED_DEADLINE em Linux. Quando usar cada um.fork()./usr/bin/passwd).Implementar um mini-shell Unix em TypeScript.
Construa um REPL que aceite comandos e execute como um shell (bash-like). Suporte:
ls, cat foo.txt, node script.js, etc. Use child_process.spawn ou equivalente.cat foo.txt | grep bar | wc -l, encadeamento de processos.ls > out.txt, cat < in.txt, command 2> err.log.sleep 10 &, não bloqueia o prompt.cd <path>, exit, pwd, export VAR=value.SIGINT ao processo em foreground sem matar o shell.wait em filhos terminados.shelljs, execa com complex modes). Apenas node:child_process, node:readline, e Node API base.yargs-parser), mas o controle de processos tem que ser seu.strace no shell e analise).jobs, fg, bg, kill %1.~/.myshell_history).epoll esperando em N sockets é o fundamento do servidor Node.*.lock files) pra serializar escritas no .git/. Falhas em fork+exec são origem de "git stuck on lock" issues.epoll (Linux), kqueue (BSD/macOS), IOCP (Windows). Worker threads são threads kernel.strace: trace de syscalls de um processo.ltrace: trace de chamadas a libraries (libc).lsof -p <pid>: lista todos FDs abertos por um processo.htop, top: estado de processos, threads, scheduling.ps -ef, ps auxf: snapshot de processos.pidstat, vmstat, iostat: estatísticas finas./proc/<pid>/: filesystem virtual com info de cada processo (status, fd, maps, etc).bpftrace, eBPF: tracing avançado low-overhead.kernel/sched/ pra scheduler, fs/ pra filesystems.man 2 <syscall> (seção 2 = syscalls). Use sempre.Encerramento: após 01-02 você consegue raciocinar sobre runtime: por que o Node escala bem em I/O e mal em CPU-bound, por que Postgres usa multi-process em vez de multi-thread (até versão recente), por que Docker é "leve" comparado a VMs. Esse modelo mental é a base de toda discussão de operação em escala.
Destrava
01-02 é prereq dos seguintes módulos: