Se uma exceção de CPU no kernel, como acesso ruim à memória ou opcode inválido, acontecer no contexto de manutenção de um processo de usuário (como syscall ou page-in), ou em um kthread
processo, então, até que panic_on_oops
seja definido, informações úteis serão despejadas e a tarefa simplesmente morrerá. Sem pânico. Às vezes, o sistema permanece absolutamente utilizável. O suficiente para o usuário tentar sincronizar seus discos, encerrar programas graciosamente e, de outras maneiras, se preparar para a reinicialização de emergência.
Mas infelizmente, se a exceção acontece em contexto atômico (como interrupção ou softirq), a ação tomada é sempre Kernel Panic (com descrição "Fatal exception in interrupt"
) — independentemente de quaisquer configurações ou configurações de tempo de compilação. É triste. Por que não é possível simular um retorno de interrupção e manter o sistema funcionando na esperança de que algumas partes ainda funcionem? Obrigado.
Eu sei que posso colocar um infinito mdelay()
no caminho do código de "exceção em interrupção" em vez de panic()
, para apenas paralisar a CPU local. Mas geralmente não há muita coisa que possa ser feita depois que isso acontece... Mesmo que haja centenas de CPUs na máquina, todas elas logo travam. Então não é muito útil.
Vou descrever o comportamento da CPU de forma genérica. Isso explica princípios gerais, mas os detalhes e a terminologia podem variar muito entre arquiteturas de CPU.
Quando uma CPU está executando código, certas coisas ruins podem acontecer que impedem que o código seja executado normalmente, como uma tentativa de executar um opcode de instrução inválido ou uma tentativa de acessar um endereço de memória não mapeado. Chamarei essas falhas de . Quando uma falha acontece, a CPU invoca um manipulador de falhas. Isso significa que a CPU salva o conteúdo de alguns registradores e define esses registradores para novos valores. Em particular, a CPU salva o contador de programa (PC) em algum lugar e salta (ou seja, define o PC) para algum endereço fixo que é o manipulador de falhas configurado. Antes de começar a executar o manipulador de falhas, a CPU também altera o modo da CPU para um modo de tratamento de falhas (que tem privilégios de nível de kernel e, na verdade, pode ser apenas o modo kernel, dependendo da arquitetura da CPU).
Se a falha ocorrer enquanto a CPU estiver em um modo não privilegiado, então (assumindo um sistema operacional corretamente projetado) o sistema ainda estará em um estado conhecido, exceto para o contexto do usuário (o processo) que estava em execução no momento. O manipulador de falhas pode examinar as informações que tem sobre o processo e decidir o que fazer. Isso inclui eventos que não são erros de forma alguma, como carregar uma página de um arquivo mapeado na memória na RAM. Mesmo que o evento não seja um que o kernel possa manipular, as consequências dos erros são limitadas ao processo, então o processo tem permissão para definir manipuladores de sinal para tais eventos. Um manipulador de sinal significa que o kernel transferirá o controle para o processo, mas pulará para algum endereço fixo que foi configurado pelo processo. É muito complicado manipular tais eventos em um processo, e a maioria não o faz, mas o processo pode assumir a responsabilidade e manipular o evento da maneira que quiser.
Se a falha ocorrer enquanto a CPU estiver no modo kernel, o que o manipulador de falhas pode fazer? Algum código que pode corromper a memória do kernel tentou fazer algo impossível. Não há como saber o quão ruins são as consequências. Talvez tudo estivesse bem e então o código tentou executar uma parte opcional que estava desabilitada e, portanto, tentou pular para o endereço armazenado em um ponteiro nulo. Ou talvez o código tenha executado um loop que corrompeu a memória e acabou de chegar ao fim de um bloco mapeado. Neste ponto, a coisa mais segura é fazer o mínimo possível e simplesmente parar de fazer qualquer coisa (e, em particular, não gravar no disco, para evitar o risco de corromper o armazenamento). Isso é chamado de pânico no design do sistema operacional: algo inesperado aconteceu do qual não há como se recuperar com segurança, então o sistema operacional tenta causar o mínimo de dano possível fazendo o mínimo possível, ponto final.
O Linux é projetado sob a suposição de que qualquer código que foi aceito no kernel foi escrito com muito, muito cuidado, e então uma falha no modo kernel é geralmente provável que seja relativamente benigna. A menos que a configuração sysctl
kernel.panic_on_oops
esteja habilitada, o kernel responderá a uma falha encerrando apenas a tarefa atual, esperando que outras tarefas não sejam afetadas. Isso pressupõe que a tarefa permaneceu principalmente independente de outras tarefas, em particular que não corrompeu nenhuma memória usada por outras tarefas, e não contém nenhum bloqueio também usado por outras tarefas. Neste contexto, “tarefa” se refere aproximadamente a um thread do kernel. Esse thread do kernel pode ser associado a um thread do usuário, nesse caso o thread do usuário também é morto, pois o kernel não pode mais lidar com ele.Mas e se a falha acontecer quando não há contexto de tarefa? Então não há esperança de confinar o impacto da falha. Você pode ver a definição do kernel Linux de “nenhum contexto de tarefa” na função
kernel_should_crash
, que é invocada quando uma falha acontece:p->pid == 0
, significa que o kernel está no agendador e não há recuperação de uma falha do agendador — o agendador é responsável por decidir o que fazer a seguir, mas ele simplesmente falhou em decidir o que fazer a seguir, então não há como saber o que fazer a seguir.Agora, neste ponto, tenho certeza de que você ainda está sentindo que isso não responde realmente à pergunta. Por que o kernel não deixa você simplesmente assumir que está tudo bem e continuar com quaisquer estruturas de dados corrompidas que ele ainda tenha? E em qualquer caso, um programador habilidoso com conhecimento do código e acesso a um depurador pode decidir que sim, desde que não toquemos nessa parte da memória, o sistema ainda está bem... Mas qual parte? E oh, "é claro" que o sistema precisa liberar esse bloqueio (caso contrário, ele entrará em um loop infinito na próxima vez que o mesmo periférico disparar uma interrupção). Não, esse bloqueio: é um ponteiro para a memória que acabou de ser liberada (e talvez tenha sido reutilizada por outro thread em execução em outro núcleo). Há alguma memória do kernel que agora está inacessível, mas uma pena, é só por um tempinho até que o usuário decida reinicializar. Ah, e "é claro" que precisamos desmascarar as interrupções antes de fazer qualquer outra coisa. Ah, mas antes disso precisamos ter certeza de que outra CPU não pegue imediatamente a interrupção já pendente do mesmo periférico assim que essa interrupção for desmascarada. Ah, e...
O ponto é que uma maneira válida de lidar com uma falha durante uma interrupção é extremamente dependente do que o manipulador de interrupção estava fazendo e da natureza da falha. Se você puder escrever um código que seja robusto o suficiente para que as falhas sejam tratadas de forma confiável, você pode escrever um código que não acione falhas em primeiro lugar — é muito mais fácil!
Um problema adicional com o tratamento de exceções aninhadas (interrupções ou falhas), como uma falha durante um manipulador de interrupção, é que dependendo da arquitetura da CPU e do estado do sistema quando a falha ocorreu, algumas informações sobre a primeira exceção podem ter sido perdidas. O tratamento de exceções aninhadas requer algum tipo de pilha para armazenar informações sobre cada exceção sucessiva. Em algumas arquiteturas, isso depende inteiramente do manipulador de exceções: ele precisa detectar exceções aninhadas e salvar informações onde quiser. Existem arquiteturas em que a própria CPU manipula uma pilha, mas mesmo nessas, a pilha pode ficar cheia (especialmente se um bug em um manipulador de exceções fizer com que ele não retorne como esperado).
É extremamente complicado lidar com uma falha no mesmo nível de privilégio que acionou a falha. O Linux já está no lado YOLO disso. Sistemas mais robustos fazem menos coisas no modo kernel . Mesmo no Linux (e em todos os kernels, na verdade), os manipuladores de interrupção devem fazer o mínimo possível por muitas razões de qualquer maneira. (A razão motriz geralmente é a simultaneidade: enquanto um manipulador de interrupção está em execução, há muitas coisas que não podem acontecer.) O manipulador de interrupção deve ser pequeno e é conhecido por ser um código especialmente delicado, então deve ser escrito com cuidado especial. Se até isso for bugado, não há mais esperança.
Os contextos de execução são diferentes.
"Modo de usuário", onde os programas de alguém rodam, tem todos os tipos de restrições que o isolam do Mundo Real. O sistema ainda tem código que ele sabe que não foi mexido pelo usuário, e pode ser confiável para lidar com falhas de nível de usuário.
"Kernel mode", onde manipuladores de interrupção e outras tarefas do sistema são executados, não tem restrições. Todo o hardware/software é vulnerável a programas com bugs no kernel mode.
Quando ocorre uma falha no modo kernel, ou uma interrupção em um manipulador de interrupção, não se pode confiar em mais nada do sistema. O sistema reinicia para atualizar o software potencialmente corrompido.