Breve hist贸ria dos computadores modernos
Last updated
Last updated
Nesta d茅cada, os primeiros computadores considerados "modernos" eram capazes de executar programas escritos em cart玫es perfurados, ou punched cards.
Os computadores executavam um programa por vez e, por causa das velocidades modestas (cerca de 100 opera莽玫es por segundo), os programas demoravam dias ou semanas para serem executados at茅 ao fim.
As pessoas que escreviam programas em cart玫es perfurados tinham, ent茫o, de esperar em longas filas para que seus respectivos programas pudessem ser inseridos nos computadores.
Uma d茅cada depois, os computadores foram evolu铆dos para que pudessem enfileirar v谩rios programas de uma vez, assim as pessoas n茫o teriam que esperar para inserir os programas: bastava colocar todos eles e o computador fazia o resto do trabalho.
Apesar de termos resolvido a quest茫o das filas de pessoas que desenvolvem programas, ainda assim os programas eram executados em ordem, um de cada vez.
Para entendermos o problema existente nesta 茅poca, vamos lembrar que um computador moderno 茅 composto por:
CPU (unidade central de processamento)
Mem贸ria de trabalho (espa莽o tempor谩rio utilizado pela CPU para executar as tarefas)
I/O: onde input pode ser por ex. o teclado, e output pode ser a impressora ou o screen (tela)
Tomemos por cen谩rio 3 programas enfileirados: programa A, programa B e programa C. Enquanto o programa A utiliza o computador, os outros 2 ficam na fila 脿 espera.
Tudo bem at茅 aqui.
Mas vamos imaginar que, ap贸s utilizar a CPU para fazer seus c谩lculos, o programa A precisa imprimir o resultado na impressora. Esta sa铆da (I/O) leva seu tempo, e enquanto o programa A espera no I/O, a CPU n茫o est谩 sendo utilizada.
Esta n茫o-utiliza莽茫o da CPU 茅 por si s贸 um desperd铆cio de recursos. Por qu锚 n茫o deixar outro programa na fila (por ex. o programa B) utilizar a CPU enquanto o programa A fica 脿 espera de I/O? Seria um racioc铆nio sensato, correto?
Tamb茅m conhecida como a era dos transistores ou third-generation computers, nesta d茅cada come莽amos a ver computadores cada vez menores e mais r谩pidos.
O objetivo aqui 茅 justamente resolver o problema da ociosidade da CPU das d茅cadas anteriores. Enquanto um programa espera por I/O, outro programa pode utilizar a CPU, fazendo ent茫o com que o n煤mero de programas executados dentro de um intervalo de tempo aumente. A esta taxa chamamos de troughput, ou apenas tput.
Isto significa que dois programas podem concorrer a recursos f铆sicos do mesmo computador. Mas como isto 茅 feito na pr谩tica?
Os Monitors eram formas primitivas do que hoje conhecemos por Sistemas Operacionais. Eles eram capazes de controlar os programas enfileirados e garantir que cada um deles utilizasse uma quantia justa da CPU enquanto outros ficavam 脿 espera de I/O.
Tal t茅cnica se chama time-sharing multitasking.
Esta 茅 a d茅cada onde os Monitors foram evolu铆dos para Sistemas Operacionais (SO). Alguns rotulam como a era da explos茫o cambriana dos computadores, pois, a partir desta d茅cada, cada vez mais e mais SO's s茫o criados.
Dentre os SO's criados nesta 茅poca, vamos dar destaque ao UNIX, que ser谩 utilizado como base neste guia.
UNIX foi um sistema muito importante para entendermos onde estamos hoje. Muitas distribui莽玫es de SO baseadas em Linux e BSD utilizam a filosofia UNIX, e portanto s茫o chamados de sistemas UNIX-like.
SO's modernos baseados em UNIX utilizam a mesma t茅cnica primitiva de time-sharing multitasking observada nos Monitors dos anos 50/60.
Tendo por base a concorr锚ncia de recursos, e pelo fato da mem贸ria de trabalho do computador ser global, 茅 preciso garantir que dois processos concorrentes n茫o utilizem o mesmo espa莽o de mem贸ria, caso contr谩rio, tais programas entrariam numa condi莽茫o de conflito de race condition chamada data race.
Para evitar data race, os SO's ent茫o t锚m de garantir algumas propriedades enquanto programas concorrem aos recursos f铆sicos:
o programa precisa ser isolado, ou seja, ter seu espa莽o de mem贸ria reservado onde nenhum outro programa possa ver ou alterar
o programa pode eventualmente ter a necessidade de se comunicar com outros programas atrav茅s do envio de mensagens
para atingir tal comunica莽茫o, os programas precisam ter identificadores 煤nicos para serem reconhecidos no sistema operacional
Temos ent茫o o resumo das caracter铆sticas para um programa existir dentro de um sistema operacional moderno:
Isolamento
Comunica莽茫o por mensagens
Identificador 煤nico
Estas propriedades formam o conceito de Processo.
Os processos do SO s茫o uma representa莽茫o dos programas, com identificador 煤nico (PID) e canais de comunica莽茫o padr茫o para envio de mensagens, tamb茅m chamado de inter-process communication, ou IPC.
Vamos listar todos os processos no sistema operacional. No meu exemplo, estou dentro de um container Docker com Ubuntu (sim, depois teremos outro guia especial apenas pra falar sobre containers e Docker).
Utilizamos ent茫o o comando ps que faz parte do SO e lista todos os processos:
Agora, abrimos outro terminal e executamos o comando sleep 30
, que bloqueia o processo e fica 脿 espera do rel贸gio durante 30 segundos:
Terminal 1
Em outro terminal, verificamos todos os processos:
Terminal 2
Okay, vamos aos detalhes:
-e
茅 a op莽茫o para listar os processos utilizando a sintaxe standard do SO
-o pid,pcpu,comm
茅 a op莽茫o para mostrar as colunas PID, CPU e CMD
o processo de PID 1
茅 o programa bash aberto no primeiro terminal (TTY pts/0)
o processo 16 茅 o programa bash aberto no segundo terminal (TTY pts/1)
o processo 103 茅 o programa sleep que est谩 bloqueado a espera do rel贸gio terminar. Este programa est谩 rodando no primeiro terminal (TTY pts/0)
o processo 109 se refere ao pr贸prio programa ps
que mostrou a sa铆da e terminou. Lembrando que todos os comandos s茫o programas no SO.
Repara que o processo sleep, enquanto est谩 脿 espera de terminar o rel贸gio, n茫o utiliza quase nada da CPU. Esta opera莽茫o 茅 puramente I/O!
Agora, vamos simular um processo que fa莽a uso de CPU. No primeiro terminal, vamos utilizar um loop infinito (que em termos computacionais, utiliza um ciclo de CPU para cada intera莽茫o do loop):
Em um segundo terminal:
E, finalmente, no terceiro terminal:
Podemos notar que o processo 1 do primeiro terminal (pts/0) utiliza 57% da CPU, enquanto o processo 77 (sleep) do segundo terminal (pts/2) est谩 bloqueado no I/O sem utilizar a CPU.
Processos concorrentes!!!!!!1
Mas como o SO faz a gest茫o destes processos?
O sistema operacional traz um programa especial chamado escalonador, mais conhecido como OS Scheduler.
Este programa 茅 respons谩vel por fazer todo o trabalho de time-sharing multitasking, garantindo que os processos concorram com a CPU de forma justa.
Quando um processo fica a espera de I/O, outro processo pode utilizar a CPU. Entretanto, o SO nem sempre deixa um processo utilizar a CPU at茅 terminar seu trabalho. Na maioria das vezes, o escalonador interrompe o processo da CPU depois de um tempo, estipulado no pr贸prio escalonador.
Assim que um processo 茅 interrompido, ele 茅 colocado numa fila de espera juntamente com outros processos, abrindo oportunidade de preemp莽茫o para que o pr贸ximo processo na fila possa fazer uso da CPU.
Este trabalho feito pelo escalonador 茅 chamado de troca de contexto, ou context switch.
Os crit茅rios para interrup莽茫o podem variar, mas quando um SO traz escalonador que interrompe processos baseado no tempo de execu莽茫o com preemp莽茫o, 茅 comum chamarmos de escalonador preemptivo, ou preemptive scheduler.
Existe outra forma de fazer escalonamento, que 茅 baseada na transfer锚ncia de controle da troca de contexto do escalonador para o pr贸prio processo, ou seja, o processo recebe a responsabilidade de saber quando a troca de contexto deve acontecer. A este tipo de escalonamento, chamamos de escalonamento cooperativo, ou cooperative scheduler.
A maioria dos SO's modernos fazem uso do escalonamento preemptivo. Mas algumas implementa莽玫es chegaram a ter o modo cooperativo, como no caso do antigo MS-DOS.
Alguns runtimes de linguagens de programa莽茫o tamb茅m acabam por implementar um escalonamento cooperativo, sendo esta uma decis茫o de design de determinada linguagem de programa莽茫o.
Okay, vimos at茅 agora que os sistemas operacionais foram criados para que m煤ltiplos programas pudessem concorrer aos recursos f铆sicos do computador de forma justa, maximizando utiliza莽茫o dos recursos, evitando desperd铆cios e aumentando a taxa de conclus茫o dos programas (throughput).
Aprendemos tamb茅m que processos de SO's s茫o abstra莽玫es que encapsulam programas com um identificador e s茫o escalonados no SO para concorrerem 脿 CPU mediante troca de contexto.
Estas 3 d茅cadas foram um marco na tecnologia, pois os sistemas operacionais permitiram que diversas linguagens de programa莽茫o pudessem ser criadas, causando impacto direto na forma como diferentes empresas tomavam decis玫es estrat茅gicas.
Com isto, novas formas de criar programas foram desenvolvidas, dentre elas a capacidade de escrevermos programas onde determinados trechos de c贸digo pudessem concorrer separadamente na CPU.
Ou seja, enquanto o processo principal concorre 脿 CPU, um trecho dentro do mesmo processo pode tamb茅m concorrer de forma independente, assim um 煤nico processo poderia permitir que m煤ltiplos blocos de c贸digo utilizem a CPU de forma concorrente e justa.
Seria uma forma de dizer ao sistema operacional:
Hey, SO, sou o processo 42, mas tenho aqui um trecho de c贸digo numa estrutura que tem acesso 脿 minha mem贸ria, mas que esta estrutura quer tamb茅m ficar na fila de escalonamento para utiliza莽茫o da CPU.
Voc锚 poderia criar esta estrutura a铆 pra mim, ~senhor sistema operacional?
A esta estrutura chamamos de threads.
As threads podem ser vistas como unidades de concorr锚ncia assim como os processos, mas como estas vivem dentro de um processo, logo est茫o utilizando a mesma mem贸ria do processo, mas sem carregar todo o processo junto com elas.
Ou seja, threads s茫o mais leves que processos. Assim, 茅 poss铆vel escrever um programa com m煤ltiplas threads concorrentes, tamb茅m chamado de multi-threading.
Ent茫o quer dizer que uma thread, por ser mais leve, 茅 melhor que um processo?
N茫o necessariamente, pois duas threads diferentes do mesmo processo compartilham da mesma mem贸ria do processo principal. Ou seja, est茫o sujeitas a race condition! (mas vamos falar de race conditions e formas de mitiga-las mais pra frente).
Para exemplificar que threads s茫o criadas no sistema operacional, vamos expor a coluna TID
no output do comando ps
:
a coluna TID se refere a "thread ID"
no exemplo acima, TID = PID, pois cada processo tem uma thread principal. Os processos descritos acima n茫o est茫o criando novas threads.
Isto mostra que o sistema operacional traz estas duas unidades primitivas de concorr锚ncia para nossa utiliza莽茫o: processos e threads.
Gordon Moore constatou com estudos, por volta de 1970, que em termos f铆sicos, as CPU's iriam dobrar de performance a cada 18 meses. Ou seja, o n煤mero de ciclos que uma CPU faz por segundo iria dobrar a cada 1 ano e meio.
E isto foi uma verdade durante d茅cadas. A ind煤stria viu a explos茫o de sistemas criados por diversas linguagens de programa莽茫o onde, mesmo que o sistema fosse escrito de forma n茫o-perform谩tica, aquilo n茫o seria um problema para os programadores pois podia-se colocar "mais CPU" para suportar escala.
A partir dos anos 2000, os engenheiros de CPU's se depararam com um problema f铆sico. Com clocks de CPU's chegando a mais de 4GHz, se os ciclos continuassem aumentando, isto ocorreria uma gera莽茫o de energia enorme, podendo haver efeitos colaterais graves.
Isto colocou para tr谩s a Lei de Morre, mas desafiou a ind煤stria de CPU's a buscar uma solu莽茫o. Os engenheiros ent茫o chegaram a uma solu莽茫o onde, ao inv茅s de aumentar a velocidade da CPU, construir-se-iam mais n煤cleos utilizando a mesma velocidade.
Come莽ara ent茫o a era multi-core.
Meados dos anos 2000 marcou muito o conceito de multi-core. Era comum vermos CPU dual-core, mais tarde quad-core, 8-core e assim por diante. Hoje 茅 comum vermos CPU's com 8, 16, 32 ou at茅 mesmo 128-cores!
Com multi-core, vem ent茫o a mudan莽a de paradigma de desenvolvimento de sistemas. Para ter um m谩ximo proveito da CPU, precisamos escrever programas que saibam utilizar todos os n煤cleos (cores) da CPU de forma eficiente.
Os processos do SO, devido ao seu isolamento por defini莽茫o, s茫o escalonados de forma paralela na CPU, onde 2 processos distintos concorrem ao mesmo tempo em dois n煤cleos dentro da mesma CPU.
Mas com as threads, que compartilham mem贸ria do mesmo processo e est茫o sujeitas a race condition, o desafio para quem escreve programa multi-threading 茅 maior.
Vamos agora focar nos desafios e algumas solu莽玫es que a ind煤stria tem por padr茫o no que tange a multi-threading.
Como vimos anteriormente, os processos s茫o por natureza isolados e portanto n茫o est茫o sujeitos a race conditions, diferente das threads.
Mas em um cen谩rio onde h谩 risco de race condition em um sistema multi-threading, como evitar tal efeito colateral?
O sistema operacional fornece um mecanismo onde, caso uma thread v谩 utilizar CPU e precise garantir que n茫o h谩 data race, esta pode pedir um lock ao SO. Assim, outras threads do mesmo processo ficar茫o 脿 espera at茅 que o lock seja liberado.
Quando uma thread 茅 suspensa na troca de contexto, caso tenha um lock, este 茅 liberado.
Definir se uma thread vai ou n茫o utilizar um lock n茫o 茅 responsabilidade do SO, isto 茅 totalmente delegado para quem escrever o sistema, no caso a pessoa que escreve o c贸digo multi-threading.
Utilizar locks mitiga race conditions, mas traz alguns efeitos colaterais que precisam ser levados em conta:
se uma thread tem o lock e entretanto por algum motivo ela 茅 abruptamente terminada, outras threads que dependem do mesmo lock ficam bloqueadas, caindo em uma situa莽茫o de deadlock
a utiliza莽茫o de muitos locks afeta a performance do sistema. Muitos locks acabam por tornar o multi-threading redundante, pois como criar lock no SO tem seu custo, h谩 situa莽玫es em que seria melhor nem usar multi-threading de todo.
Apesar dos efeitos colaterais, a utiliza莽茫o de locks com prud锚ncia tem seu valor em diversas ocasi玫es, dependendo do tipo de problema.
Uma alternativa aos locks expl铆citos 茅 a utiliza莽茫o de um lock otimista (optimistic locking), que basicamente 茅 a cria莽茫o de duas ou mais vers玫es dos dados, que s茫o ent茫o comparados antes do update por 2 threads concorrentes. Desta forma, como n茫o h谩 lock expl铆cito, tamb茅m n茫o h谩 o risco de deadlocks.
Outra alternativa aos locks 茅 fazer com que a thread seja safe. Ou seja, que tenha seu pr贸prio espa莽o na mem贸ria tal como um processo. Mas teoricamente 茅 imposs铆vel uma vez que a thread compartilha o mesmo espa莽o f铆sico do processo.
Ent茫o, t茅cnicas de c贸pia dos dados para outro espa莽o dispon铆vel na mem贸ria aparecem. Mesmo dentro do mesmo processo, duas threads diferentes t锚m cada uma seu estado privado, e desta forma uma n茫o consegue modificar o estado da outra.
Com o estado interno isolado, em um sistema complexo, uma thread precisa se comunicar com outras, tal como os processos comunicam entre si utilizando IPC (veremos este tema de IPC mais tarde). Esta comunica莽茫o entre threads tamb茅m deve ser feita por envio de mensagens.
Estas 2 caracter铆sticas s茫o as principais que fazem com que tal thread seja denominada por ator. Com isto, modelo de atores 茅 uma alternativa a lock expl铆cito e lock otimista em sistemas multi-threading.
Nesta se莽茫o focamos bastante na concorr锚ncia pela CPU e na sua evolu莽茫o com t茅cnicas e funcionalidades como Threads, locks e modelo de atores.
Mas pouco falamos daqueles processos que, l谩 atr谩s nos anos 60, ficavam bloqueados 脿 espera de I/O, correto?
Ficar bloqueado a espera de I/O 茅 algo inevit谩vel, pois foge ao controle do programa e do sistema computacional. 脡 exatamente esperar pelo teclado, pelo screen, pelo rel贸gio, pela impressora ficar pronta, pelos dados chegarem na rede (sim, networking utiliza I/O), entre outros aspectos.
Mas, diferente da guinada que as CPU's tiveram para uma arquitetura multi-core, os mecanismos de armazenamento e rede tais como HD, SSH, largura de banda, fibra 贸tica, etc t锚m ficado cada vez mais r谩pidos e baratos.
Sistemas operacionais modernos fornecem funcionalidades onde podemos verificar se o recurso I/O est谩 dispon铆vel para leitura ou escrita.
Esta verifica莽茫o pode acontecer de forma ass铆ncrona, e assim a thread ou processo que vai utilizar I/O n茫o precisa ficar 脿 espera. O processo fica ent茫o dispon铆vel para outras opera莽玫es, e assim que o recurso ficar dispon铆vel, 茅 notificado e pode portanto ter o resultado do I/O.
Para que isto seja poss铆vel, 茅 necess谩ria a implementa莽茫o de um padr茫o de loop de eventos, e dentro desse loop s茫o feitas as tais verifica莽玫es de recursos I/O dispon铆veis com suas respectivas "threads interessadas".
Tal implementa莽茫o n茫o 茅 feita por todos os runtimes, mas nas 煤ltimas d茅cadas temos visto uma crescente nisto, principalmente depois do sucesso que o runtime NodeJS teve nesta 谩rea. Atualmente, outros runtimes e projetos come莽aram a ir pelo mesmo caminho de non-blocking I/O (ou I/O ass铆ncrono, que s茫o termos ligeiramente diferentes mas se convergem), tais como Java Loom, PHP Swoole e Ruby 3.
Vimos anteriormente que os sistemas operacionais t锚m, em sua maioria, um tipo de escalonamento preemptivo, e que a forma cooperativa (quando a pr贸pria thread ou processo controla a troca de contexto) pode ser utilizada em alguns casos .
Em arquitetura de I/O n茫o bloqueante, este modelo cooperativo pode ter suas vantagens, uma vez que a responsabilidade de trocar o contexto fica por conta do processo que est谩 a espera de I/O e 茅 notificado quando o recurso est谩 pronto.
Mas n茫o 茅 todo runtime que decide implementar modelo cooperativo de escalonamento. Por exemplo, o Ruby 3 traz uma interface de escalonador cooperativo, mas n茫o implementa dentro do runtime. Fica a cargo de quem desenvolve implementar um seguindo a interface de chamadas aos recursos de I/O do sistema operacional.
Esta jornada da breve hist贸ria dos computadores foi um pouco extensa, mas bastante resumida face 脿 quantidade de conte煤do que temos nesta 谩rea.
Vamos continuar com o guia e seu objetivo que 茅 continuar dissecando as partes fundamentais da Web.
Na pr贸xima se莽茫o abordaremos como diferentes processos se comunicam e quais as formas comuns de comunica莽茫o entre processos dentro de um sistema operacional.