Todas as CPUs modernas têm capacidade para interromper as instruções da máquina em execução no momento. Eles economizam estado suficiente (geralmente, mas nem sempre, na pilha) para tornar possível retomar a execução mais tarde, como se nada tivesse acontecido (a instrução interrompida será reiniciada do zero, geralmente). Então eles começam a executar um manipulador de interrupção , que é apenas mais código de máquina, mas colocado em um local especial para que a CPU saiba onde está com antecedência. Manipuladores de interrupção sempre fazem parte do kernel do sistema operacional: o componente que é executado com o maior privilégio e é responsável por supervisionar a execução de todos os outros componentes. 1,2
As interrupções podem ser síncronas , o que significa que elas são acionadas pela própria CPU como uma resposta direta a algo que a instrução atualmente em execução fez, ou assíncronas , o que significa que elas ocorrem em um momento imprevisível por causa de um evento externo, como dados chegando na rede porta. Algumas pessoas reservam o termo "interrupção" para interrupções assíncronas e chamam interrupções síncronas de "traps", "falhas" ou "exceções", mas todas essas palavras têm outros significados, por isso vou usar a "interrupção síncrona".
Agora, os sistemas operacionais mais modernos têm uma noção de processos . No mais básico, esse é um mecanismo pelo qual o computador pode executar mais de um programa ao mesmo tempo, mas também é um aspecto essencial de como os sistemas operacionais configuram a proteção de memória , que é um recurso da maioria (mas, infelizmente, ainda não todos ) CPUs modernas. Combina com a memória virtual, que é a capacidade de alterar o mapeamento entre endereços de memória e locais reais na RAM. A proteção de memória permite que o sistema operacional forneça a cada processo seu próprio pedaço privado de RAM, que somente ele pode acessar. Ele também permite que o sistema operacional (agindo em nome de algum processo) designe regiões da RAM como somente leitura, executável, compartilhada entre um grupo de processos cooperativos etc. Também haverá um pedaço de memória acessível apenas pelo núcleo. 3
Enquanto cada processo acessa a memória apenas da maneira que a CPU está configurada para permitir, a proteção da memória é invisível. Quando um processo quebra as regras, a CPU gera uma interrupção síncrona, pedindo ao kernel para resolver as coisas. Ocorre regularmente que o processo realmente não violou as regras, apenas o kernel precisa fazer algum trabalho antes que o processo possa continuar. Por exemplo, se uma página da memória de um processo precisar ser "despejada" no arquivo de permuta para liberar espaço na RAM para outra coisa, o kernel marcará essa página como inacessível. Na próxima vez que o processo tentar usá-lo, a CPU gerará uma interrupção na proteção de memória; o kernel recuperará a página do swap, colocará de volta onde estava, marcará como acessível novamente e retomará a execução.
Mas suponha que o processo realmente tenha infringido as regras. Ele tentou acessar uma página que nunca teve nenhuma RAM mapeada para ele ou tentou executar uma página marcada como não contendo código de máquina ou qualquer outra coisa. A família de sistemas operacionais geralmente conhecida como "Unix" usa sinais para lidar com essa situação. 4 Os sinais são semelhantes às interrupções, mas são gerados pelo kernel e colocados em campo por processos, em vez de serem gerados pelo hardware e em campo pelo kernel. Processos podem definir manipuladores de sinalem seu próprio código e diga ao kernel onde eles estão. Esses manipuladores de sinal serão executados, interrompendo o fluxo normal de controle, quando necessário. Todos os sinais têm um número e dois nomes, um dos quais é um acrônimo enigmático e o outro uma frase um pouco menos enigmática. O sinal gerado quando um processo quebra as regras de proteção de memória é (por convenção) o número 11 e seus nomes são SIGSEGV
"Falha na segmentação". 5,6
Uma diferença importante entre sinais e interrupções é que existe um comportamento padrão para cada sinal. Se o sistema operacional falhar na definição de manipuladores para todas as interrupções, isso é um bug no sistema operacional e o computador inteiro falhará quando a CPU tentar invocar um manipulador ausente. Mas os processos não têm obrigação de definir manipuladores de sinais para todos os sinais. Se o kernel gerar um sinal para um processo, e esse sinal tiver sido deixado em seu comportamento padrão, o kernel apenas seguirá em frente e fará o que for o padrão e não incomodará o processo. O comportamento padrão da maioria dos sinais é "não fazer nada" ou "encerrar esse processo e talvez também produzir um dump principal". SIGSEGV
é um dos últimos.
Então, para recapitular, temos um processo que violou as regras de proteção de memória. A CPU suspendeu o processo e gerou uma interrupção síncrona. O kernel colocou em campo essa interrupção e gerou um SIGSEGV
sinal para o processo. Vamos supor que o processo não configurou um manipulador de sinal para SIGSEGV
, portanto, o kernel executa o comportamento padrão, que é finalizar o processo. Isso tem os mesmos efeitos que a _exit
chamada do sistema: os arquivos abertos são fechados, a memória é desalocada, etc.
Até esse momento, nada imprimia nenhuma mensagem que um humano pudesse ver, e o shell (ou, de maneira mais geral, o processo pai do processo que acabou de terminar) não estava envolvido. SIGSEGV
vai para o processo que violou as regras, não seu pai. A próxima etapa da sequência, no entanto, é notificar o processo pai de que seu filho foi encerrado. Isso pode acontecer de várias maneiras diferentes, das quais a mais simples é quando o pai já está esperando por esta notificação, usando uma das wait
chamadas de sistema ( wait
, waitpid
, wait4
, etc). Nesse caso, o kernel fará com que a chamada do sistema retorne e forneça ao processo pai um número de código chamado status de saída. 7 O status de saída informa ao pai por que o processo filho foi encerrado; nesse caso, aprenderá que a criança foi encerrada devido ao comportamento padrão de um SIGSEGV
sinal.
O processo pai pode então relatar o evento a um humano imprimindo uma mensagem; programas shell quase sempre fazem isso. Você crsh
não inclui código para fazer isso, mas acontece assim mesmo, porque a rotina da biblioteca C system
executa um shell com todos os recursos /bin/sh
, "sob o capô". crsh
é o avô nesse cenário; a notificação do processo pai é preenchida por /bin/sh
, que imprime sua mensagem usual. Em seguida, /bin/sh
ele sai, pois não tem mais nada a fazer, e a implementação da biblioteca C system
recebe essa notificação de saída. Você pode ver essa notificação de saída em seu código, inspecionando o valor de retorno desystem
; mas não lhe dirá que o processo do neto morreu em um segfault, porque foi consumido pelo processo intermediário do shell.
Notas de rodapé
Alguns sistemas operacionais não implementam drivers de dispositivo como parte do kernel; no entanto, todos os manipuladores de interrupção ainda precisam fazer parte do kernel, assim como o código que configura a proteção de memória, porque o hardware não permite nada além do kernel fazer essas coisas.
Pode haver um programa chamado "hypervisor" ou "gerenciador de máquina virtual" que seja ainda mais privilegiado que o kernel, mas, para fins desta resposta, pode ser considerado parte do hardware .
O kernel é um programa , mas é não um processo; é mais como uma biblioteca. Todos os processos executam partes do código do kernel, de tempos em tempos, além de seu próprio código. Pode haver vários "threads do kernel" que executam apenas o código do kernel, mas eles não nos interessam aqui.
O único sistema operacional com o qual você provavelmente precisará lidar mais que não pode ser considerado uma implementação do Unix é, obviamente, o Windows. Não usa sinais nesta situação. (Na verdade, ele não possui sinais; no Windows, a <signal.h>
interface é completamente falsificada pela biblioteca C.) Ela usa algo chamado " manipulação de exceção estruturada ".
Algumas violações de proteção de memória são geradas SIGBUS
("Erro de barramento") em vez de SIGSEGV
. A linha entre os dois é subespecificada e varia de sistema para sistema. Se você escreveu um programa que define um manipulador SIGSEGV
, provavelmente é uma boa ideia definir o mesmo manipulador para SIGBUS
.
"Falha de segmentação" foi o nome da interrupção gerada por violações da proteção de memória por um dos computadores que executaram o Unix original , provavelmente o PDP-11 . " Segmentação " é um tipo de proteção de memória, mas atualmente o termo " falha de segmentação " refere-se genericamente a qualquer tipo de violação de proteção de memória.
Todas as outras maneiras pelas quais o processo pai pode ser notificado sobre a conclusão de um filho terminam com o pai chamando wait
e recebendo um status de saída. É que algo mais acontece primeiro.
crsh
é uma ótima idéia para esse tipo de experimentação. Obrigado por nos informar sobre a idéia e por trás dela.