Estamos compilando um aplicativo C / C ++ incorporado que é implantado em um dispositivo protegido em um ambiente bombardeado com radiação ionizante . Estamos usando o GCC e a compilação cruzada para o ARM. Quando implantado, nosso aplicativo gera alguns dados incorretos e trava com mais frequência do que gostaríamos. O hardware foi projetado para esse ambiente e nosso aplicativo é executado nesta plataforma há vários anos.
Existem alterações que podemos fazer em nosso código ou melhorias em tempo de compilação que podem ser feitas para identificar / corrigir erros de software e corrupção de memória causados por distúrbios de um único evento ? Algum outro desenvolvedor teve sucesso em reduzir os efeitos nocivos de erros de software em um aplicativo de longa duração?
Respostas:
Trabalhando por cerca de 4-5 anos no desenvolvimento de software / firmware e testes ambientais de satélites miniaturizados *, gostaria de compartilhar minha experiência aqui.
* (os satélites miniaturizados são muito mais propensos a perturbações em eventos únicos do que os satélites maiores, devido aos seus tamanhos relativamente pequenos e limitados para seus componentes eletrônicos )
Agora, essa situação é normalmente tratada no nível de hardware e software. Aqui, como você pede, vou compartilhar o que podemos fazer no nível do software.
... finalidade de recuperação . Forneça a capacidade de atualizar / recompilar / atualizar novamente seu software / firmware em ambiente real. Esse é um recurso quase obrigatório para qualquer software / firmware em ambiente altamente ionizado. Sem isso, você pode ter software / hardware redundante quantos quiser, mas a certa altura, todos eles vão explodir. Então, prepare esse recurso!
... versão mínima de trabalho ... Tenha em seu código cópias responsivas e múltiplas, versão mínima do software / firmware. É como o modo de segurança no Windows. Em vez de ter apenas uma versão totalmente funcional do seu software, tenha várias cópias da versão mínima do seu software / firmware. A cópia mínima geralmente terá muito menos tamanho que a cópia completa e quase sempre terá apenas os dois ou três recursos a seguir:
... copiar ... em algum lugar ... Tem software / firmware redundante em algum lugar.
Você poderia, com ou sem hardware redundante, tentar ter software / firmware redundante no seu ARM uC. Isso normalmente é feito com dois ou mais softwares / firmware idênticos em endereços separados, que enviam pulsação um para o outro - mas apenas um estará ativo por vez. Se um ou mais software / firmware não responder, mude para o outro software / firmware. O benefício de usar essa abordagem é que podemos ter substituição funcional imediatamente após a ocorrência de um erro - sem nenhum contato com qualquer sistema / parte externa responsável por detectar e reparar o erro (no caso de satélite, geralmente é o Mission Control Center ( MCC)).
Estritamente falando, sem hardware redundante, a desvantagem de fazer isso é que você realmente não pode eliminar todos os pontos únicos de falhas. No mínimo, você ainda terá um único ponto de falha, que é o próprio switch (ou geralmente o início do código). No entanto, para um dispositivo limitado pelo tamanho em um ambiente altamente ionizado (como satélites pico / femto), a redução do ponto único de falha para um ponto ainda vale a pena considerar sem hardware adicional. Em algum momento, o trecho de código para a troca certamente seria muito menor que o código para todo o programa - reduzindo significativamente o risco de obter um Evento Único nele.
Mas se você não estiver fazendo isso, deverá ter pelo menos uma cópia em seu sistema externo que possa entrar em contato com o dispositivo e atualizar o software / firmware (no caso do satélite, é novamente o centro de controle da missão).
... situação incorreta detectável. O erro deve ser detectável , geralmente pelo circuito de correção / detecção de erros de hardware ou por um pequeno pedaço de código para correção / detecção de erros. É melhor colocar esse código pequeno, múltiplo e independente do software / firmware principal. Sua principal tarefa é apenas para verificar / corrigir. Se o circuito / firmware do hardware for confiável(como é mais resistente à radiação do que os demais - ou possui vários circuitos / lógicas), considere fazer a correção de erros com ela. Mas se não for, é melhor fazê-lo como detecção de erro. A correção pode ser por sistema / dispositivo externo. Para a correção de erros, você pode considerar utilizar um algoritmo básico de correção de erros, como Hamming / Golay23, porque eles podem ser implementados com mais facilidade, tanto no circuito / software. Mas, em última análise, depende da capacidade da sua equipe. Para detecção de erros, normalmente o CRC é usado.
... hardware de suporte à recuperação Agora, trata-se do aspecto mais difícil sobre esse assunto. Por fim, a recuperação exige que o hardware responsável pela recuperação seja pelo menos funcional. Se o hardware estiver permanentemente quebrado (normalmente ocorre após a dose ionizante total atingir determinado nível), então (infelizmente) não há como o software ajudar na recuperação. Assim, o hardware é justamente a preocupação de maior importância para um dispositivo exposto a um alto nível de radiação (como satélite).
Além da sugestão de antecipar o erro de firmware acima devido à perturbação de evento único, também gostaria de sugerir que você tenha:
Algoritmo de detecção e / ou correção de erros no protocolo de comunicação entre subsistemas. Este é outro quase necessário para evitar sinais incompletos / errados recebidos de outro sistema
Filtre sua leitura do ADC. Você não usar o ADC leitura diretamente. Filtre-o por filtro mediano, filtro médio ou qualquer outro filtro - nunca confie no valor de leitura única. Prove mais, não menos - razoavelmente.
fonte
A NASA tem um artigo sobre software reforçado por radiação . Descreve três tarefas principais:
Observe que a taxa de varredura de memória deve ser frequente o suficiente para que erros de vários bits raramente ocorram, pois a maioria da memória ECC pode se recuperar de erros de um bit, e não de vários bits.
A recuperação robusta de erros inclui transferência de fluxo de controle (normalmente reiniciando um processo em um ponto anterior ao erro), liberação de recursos e restauração de dados.
Sua principal recomendação para restauração de dados é evitar a necessidade, pois os dados intermediários são tratados como temporários, para que a reinicialização antes do erro também reverta os dados para um estado confiável. Isso soa semelhante ao conceito de "transações" nos bancos de dados.
Eles discutem técnicas particularmente adequadas para linguagens orientadas a objetos, como C ++. Por exemplo
E, por acaso, a NASA usou C ++ para grandes projetos, como o Mars Rover .
Eles evitaram certos recursos do C ++ que poderiam criar problemas:
new
edelete
)new
para evitar a possibilidade de corrupção de heap do sistema).fonte
Aqui estão alguns pensamentos e idéias:
Use a ROM de maneira mais criativa.
Armazene o que puder na ROM. Em vez de calcular coisas, armazene tabelas de consulta na ROM. (Verifique se o compilador está exibindo suas tabelas de consulta na seção somente leitura! Imprima os endereços de memória em tempo de execução para verificar!) Armazene sua tabela de vetor de interrupção na ROM. Obviamente, execute alguns testes para ver o quão confiável sua ROM é comparada à sua RAM.
Use sua melhor RAM para a pilha.
Os SEUs na pilha são provavelmente a fonte mais provável de falhas, porque é onde coisas como variáveis de índice, variáveis de status, endereços de retorno e ponteiros de vários tipos normalmente vivem.
Implemente rotinas de timer-tick e watchdog.
Você pode executar uma rotina de "verificação de integridade" a cada marca de timer, bem como uma rotina de vigilância para lidar com o bloqueio do sistema. Seu código principal também pode incrementar periodicamente um contador para indicar progresso, e a rotina de verificação de integridade pode garantir que isso ocorra.
Implemente códigos de correção de erros no software.
Você pode adicionar redundância aos seus dados para poder detectar e / ou corrigir erros. Isso aumentará o tempo de processamento, potencialmente deixando o processador exposto à radiação por mais tempo, aumentando assim a chance de erros; portanto, você deve considerar o trade-off.
Lembre-se dos caches.
Verifique os tamanhos dos caches da sua CPU. Os dados que você acessou ou modificou recentemente provavelmente estarão em um cache. Eu acredito que você pode desativar pelo menos alguns dos caches (com um grande custo de desempenho); você deve tentar isso para ver como os caches são suscetíveis aos SEUs. Se os caches forem mais difíceis que a RAM, você poderá ler e reescrever regularmente dados críticos para garantir que eles permaneçam no cache e traga a RAM de volta à linha.
Use manipuladores de falhas de página de maneira inteligente.
Se você marcar uma página de memória como não presente, a CPU emitirá uma falha de página quando você tentar acessá-la. Você pode criar um manipulador de falhas de página que faça alguma verificação antes de atender à solicitação de leitura. (Os sistemas operacionais de PC usam isso para carregar de forma transparente as páginas que foram trocadas para o disco.)
Use a linguagem assembly para coisas críticas (que podem ser tudo).
Com a linguagem assembly, você sabe o que há nos registros e o que há na RAM; você sabe quais tabelas de RAM especiais a CPU está usando e pode projetar coisas de maneira indireta para manter seu risco baixo.
Usar
objdump
para realmente examinar a linguagem assembly gerada e descobrir quanto código cada uma de suas rotinas ocupa.Se você estiver usando um grande sistema operacional como o Linux, estará pedindo problemas; há tanta complexidade e tantas coisas para dar errado.
Lembre-se de que é um jogo de probabilidades.
Um comentarista disse
Embora isso seja verdade, as chances de erros nos (digamos) 100 bytes de código e dados necessários para que uma rotina de verificação funcione corretamente são muito menores do que as chances de erros em outros lugares. Se sua ROM é bastante confiável e quase todo o código / dados está realmente na ROM, então suas chances são ainda melhores.
Use hardware redundante.
Use 2 ou mais configurações de hardware idênticas com código idêntico. Se os resultados diferirem, uma redefinição deve ser acionada. Com 3 ou mais dispositivos, você pode usar um sistema de "votação" para tentar identificar qual deles foi comprometido.
fonte
Você também pode estar interessado na rica literatura sobre o assunto da tolerância a falhas algorítmicas. Isso inclui a atribuição antiga: escreva uma classificação que classifique corretamente sua entrada quando um número constante de comparações falhará (ou, a versão um pouco mais ruim, quando o número assintótico de comparações com falha for escalonado como
log(n)
nasn
comparações).Um lugar para começar a ler é o artigo de Huang e Abraham, de 1984, " Tolerância a falhas baseada em algoritmos para operações matriciais ". A ideia deles é vagamente semelhante à computação criptografada homomórfica (mas não é a mesma, pois eles estão tentando detectar / corrigir erros no nível da operação).
Um descendente mais recente desse artigo é Bosilca, Delmas, Dongarra e " tolerância a falhas baseada em algoritmo aplicada à computação de alto desempenho ".
fonte
Escrever código para ambientes radioativos não é realmente diferente de escrever código para qualquer aplicativo de missão crítica.
Além do que já foi mencionado, aqui estão algumas dicas diversas:
IMPORTANTE: Você deve garantir a integridade dos registros internos do MCU. Todos os registros de controle e status dos periféricos de hardware graváveis podem estar localizados na memória RAM e, portanto, são vulneráveis.
Para se proteger contra a corrupção de registros, escolha um microcontrolador com recursos incorporados de "gravação única" dos registros. Além disso, você precisa armazenar valores padrão de todos os registros de hardware no NVM e copiar esses valores para seus registros em intervalos regulares. Você pode garantir a integridade de variáveis importantes da mesma maneira.
Nota: sempre use programação defensiva. Isso significa que você precisa configurar todos os registros no MCU e não apenas os usados pelo aplicativo. Você não quer que algum periférico aleatório de hardware acorde de repente.
Existem todos os tipos de métodos para verificar se há erros na RAM ou NVM: somas de verificação, "padrões de caminhada", ECC de software etc. etc. Atualmente, a melhor solução é não usar nenhum deles, mas usar um MCU com ECC embutido e verificações semelhantes. Como fazer isso no software é complexo, a verificação de erros por si só pode, portanto, introduzir bugs e problemas inesperados.
Compreender e adotar o conceito de programação defensiva. Isso significa que seu programa precisa lidar com todos os casos possíveis, mesmo aqueles que não podem ocorrer na teoria. Exemplos .
O firmware de missão crítica de alta qualidade detecta o máximo de erros possível e os ignora de maneira segura.
IMPORTANTE: Não implemente confiança nos valores padrão das variáveis de duração do armazenamento estático. Ou seja, não confie no conteúdo padrão do
.data
ou.bss
. Pode haver qualquer quantidade de tempo entre o ponto de inicialização e o ponto em que a variável é realmente usada; pode haver bastante tempo para a RAM ser corrompida. Em vez disso, escreva o programa para que todas essas variáveis sejam definidas no NVM em tempo de execução, imediatamente antes do momento em que essa variável for usada pela primeira vez.Na prática, isso significa que se uma variável for declarada no escopo do arquivo ou como
static
, você nunca deve usar=
-la para inicializá-la (ou pode, mas é inútil, porque você não pode confiar no valor de qualquer maneira). Sempre defina-o em tempo de execução, imediatamente antes do uso. Se for possível atualizar repetidamente essas variáveis do NVM, faça-o.Da mesma forma em C ++, não confie em construtores para variáveis de duração de armazenamento estático. Peça ao construtor que chame uma rotina pública de "configuração", que você também pode chamar mais tarde em tempo de execução, diretamente do aplicativo de chamada.
Se possível, remova o código de inicialização "copiar" que inicializa
.data
e.bss
(e chama os construtores C ++) completamente, para que você obtenha erros de vinculador se escrever código contando com isso. Muitos compiladores têm a opção de pular isso, geralmente chamado de "inicialização mínima / rápida" ou similar.Isso significa que todas as bibliotecas externas precisam ser verificadas para que não contenham essa confiança.
Implemente e defina um estado seguro para o programa, para onde você reverterá em caso de erros críticos.
fonte
TRUE
igual para0xffffffff
depois usarPOPCNT
com um limite.%01010101010101010101010101010101
, XOR então POPCNT?.text
seção, alterando um código operacional ou similar.Pode ser possível usar C para escrever programas que se comportam de maneira robusta nesses ambientes, mas apenas se a maioria das formas de otimização do compilador estiver desabilitada. Os compiladores de otimização foram projetados para substituir muitos padrões de codificação aparentemente redundantes por "mais eficientes" e podem não ter idéia de que o motivo pelo qual o programador está testando
x==42
quando o compilador sabe que não há comox
ter mais alguma coisa, porque o programador deseja impedir a execução de determinado códigox
mantendo outro valor - mesmo nos casos em que a única maneira de manter esse valor seria se o sistema recebesse algum tipo de falha elétrica.Declarar variáveis como
volatile
muitas vezes é útil, mas pode não ser uma panacéia. De particular importância, observe que a codificação segura geralmente requer que operações perigosas tenham intertravamentos de hardware que exigem várias etapas para serem ativados e que o código seja escrito usando o padrão:Se um compilador converter o código de maneira relativamente literal e se todas as verificações do estado do sistema forem repetidas após o mesmo
prepare_for_activation()
, o sistema poderá ser robusto contra praticamente qualquer evento de falha única plausível, mesmo aqueles que corromperiam arbitrariamente o contador e a pilha do programa. Se ocorrer uma falha logo após uma chamadaprepare_for_activation()
, isso implicaria que a ativação seria apropriada (já que não há outro motivoprepare_for_activation()
que teria sido chamado antes da falha). Se a falha fizer com que o código chegue de formaprepare_for_activation()
inadequada, mas não houver eventos subsequentes de falha, não haveria maneira de o código chegar posteriormentetrigger_activation()
sem passar pela verificação de validação ou chamando cancel_preparations primeiro [se a pilha falha, a execução pode prosseguir para um local logo antestrigger_activation()
após o contexto que chamouprepare_for_activation()
retornos, mas a chamada paracancel_preparations()
teria ocorrido entre as chamadas paraprepare_for_activation()
etrigger_activation()
, tornando a última chamada inofensiva.Esse código pode ser seguro no C tradicional, mas não nos compiladores C modernos. Esses compiladores podem ser muito perigosos nesse tipo de ambiente, porque eles se esforçam para incluir apenas códigos que serão relevantes em situações que poderiam ocorrer por meio de algum mecanismo bem definido e cujas conseqüências resultantes também seriam bem definidas. Código cujo objetivo seria detectar e limpar após falhas pode, em alguns casos, acabar piorando as coisas. Se o compilador determinar que a tentativa de recuperação, em alguns casos, invocará um comportamento indefinido, poderá inferir que as condições que exigiriam essa recuperação nesses casos não podem ocorrer, eliminando o código que seria verificado por eles.
fonte
-O0
ou um switch equivalente? O GCC fará muitas coisas estranhas se você der permissão , mas se você pedir para não fazê-lo, também poderá ser bastante literal.-O2
.-O0
é uma má ideia é porque emite muito mais instruções inúteis. Exemplo: uma chamada não embutida contém instruções para salvar registros, fazer a chamada, restaurar registros. Tudo isso pode falhar. Uma instrução que não está lá não pode falhar.-O0
é uma má idéia: ela tende a armazenar variáveis na memória em vez de em um registro. Agora não é certo que a memória seja mais suscetível aos SEU, mas os dados em voo são mais suscetíveis que os dados em repouso. A movimentação inútil de dados deve ser evitada e-O2
ajuda lá.v1=v2+0xCAFEBABE
e todas as atualizações para as duas variáveis são feitas ... #Este é um assunto extremamente amplo. Basicamente, você não pode realmente se recuperar da corrupção de memória, mas pode pelo menos tentar falhar imediatamente . Aqui estão algumas técnicas que você pode usar:
dados constantes de soma de verificação . Se você tiver quaisquer dados de configuração que permaneçam constantes por um longo período de tempo (incluindo os registros de hardware configurados), calcule sua soma de verificação na inicialização e verifique-a periodicamente. Quando você vê uma incompatibilidade, é hora de reinicializar ou redefinir.
armazene variáveis com redundância . Se você tem uma variável importante
x
, escrever o seu valor emx1
,x2
ex3
e lê-lo como(x1 == x2) ? x2 : x3
.implementar o monitoramento do fluxo do programa . XOR um sinalizador global com um valor exclusivo em funções / ramificações importantes chamadas do loop principal. A execução do programa em um ambiente livre de radiação com cobertura de teste de quase 100% deve fornecer a lista de valores aceitáveis da bandeira no final do ciclo. Redefina se você encontrar desvios.
monitorar o ponteiro da pilha . No início do loop principal, compare o ponteiro da pilha com o valor esperado. Redefinir no desvio.
fonte
O que poderia ajudá-lo é um cão de guarda . Os cães de guarda foram usados extensivamente na computação industrial nos anos 80. As falhas de hardware eram muito mais comuns na época - outra resposta também se refere a esse período.
Um cão de guarda é um recurso combinado de hardware / software. O hardware é um contador simples que diminui de um número (digamos 1023) para zero. TTL ou outra lógica pode ser usada.
O software foi projetado para que uma rotina monitore a operação correta de todos os sistemas essenciais. Se essa rotina for concluída corretamente = encontrar o computador funcionando corretamente, o contador voltará para 1023.
O design geral é para que, em circunstâncias normais, o software impeça que o contador de hardware atinja zero. Caso o contador atinja zero, o hardware do contador executa sua tarefa única e redefine o sistema inteiro. De uma perspectiva de contador, zero é igual a 1024 e o contador continua em contagem regressiva novamente.
Esse watchdog garante que o computador conectado seja reiniciado em muitos casos de falha. Devo admitir que não estou familiarizado com hardware capaz de executar essa função nos computadores de hoje. As interfaces para o hardware externo agora são muito mais complexas do que costumavam ser.
Uma desvantagem inerente do watchdog é que o sistema não está disponível desde o momento em que falha até que o contador do watchdog atinja zero + tempo de reinicialização. Embora esse tempo seja geralmente muito menor do que qualquer intervenção externa ou humana, o equipamento suportado precisará poder continuar sem o controle do computador durante esse período.
fonte
Esta resposta pressupõe que você esteja preocupado em ter um sistema que funcione corretamente, além de ter um sistema de custo mínimo ou rápido; a maioria das pessoas brincando com coisas radioativas valoriza a correção / a segurança sobre a velocidade / custo
Várias pessoas sugeriram mudanças de hardware que você pode fazer (ótimo - já há muitas coisas boas aqui nas respostas e eu não pretendo repetir tudo), e outras sugeriram redundância (ótima em princípio), mas acho que não alguém sugeriu como essa redundância pode funcionar na prática. Como você falha? Como você sabe quando algo "deu errado"? Muitas tecnologias funcionam com base em que tudo funcionará, e o fracasso é, portanto, uma coisa complicada de se lidar. No entanto, algumas tecnologias de computação distribuída projetadas para escala esperam falhas (afinal, com escala suficiente, a falha de um nó de muitos é inevitável com qualquer MTBF para um único nó); você pode aproveitar isso para o seu ambiente.
Aqui estão algumas idéias:
Certifique-se de que todo o seu hardware seja replicado
n
vezes (onden
é maior que 2 e, de preferência, ímpar) e que cada elemento de hardware possa se comunicar um com o outro. A Ethernet é uma maneira óbvia de fazer isso, mas existem muitas outras rotas muito mais simples que dariam melhor proteção (por exemplo, CAN). Minimize os componentes comuns (até fontes de alimentação). Isso pode significar amostragem de entradas ADC em vários locais, por exemplo.Verifique se o estado do seu aplicativo está em um único local, por exemplo, em uma máquina de estados finitos. Isso pode ser totalmente baseado em RAM, embora não exclua o armazenamento estável. Assim, ele será armazenado em vários locais.
Adote um protocolo de quorum para alterações de estado. Veja RAFT, por exemplo. Enquanto você trabalha em C ++, existem bibliotecas conhecidas para isso. Mudanças no FSM somente seriam feitas quando a maioria dos nós concordasse. Use uma biblioteca boa conhecida para a pilha de protocolos e o quorum, em vez de rolar uma, ou todo o seu bom trabalho de redundância será desperdiçado quando o protocolo de quorum for interrompido.
Verifique se você soma a verificação (por exemplo, CRC / SHA) ao seu FSM e armazena o CRC / SHA no próprio FSM (além de transmitir na mensagem e somar as próprias mensagens). Faça com que os nós verifiquem seu FSM regularmente com relação a essa soma de verificação, mensagens recebidas da soma de verificação e verifique se a soma de verificação corresponde à soma de verificação do quorum.
Crie o maior número possível de verificações internas em seu sistema, fazendo com que os nós que detectam sua própria falha sejam reinicializados (isso é melhor do que continuar trabalhando metade, desde que você tenha nós suficientes). Tente deixá-los remover-se do quorum de maneira limpa durante a reinicialização, caso não voltem a aparecer. Na reinicialização, faça com que eles somam a imagem do software (e qualquer outra coisa que eles carregam) e façam um teste de RAM completo antes de se reintroduzirem no quorum.
Use o hardware para apoiá-lo, mas faça-o com cuidado. Você pode obter RAM de ECC, por exemplo, e ler / gravar regularmente nela para corrigir erros de ECC (e entrar em pânico se o erro for incorreto). No entanto (a partir da memória), a RAM estática é muito mais tolerante à radiação ionizante do que a DRAM em primeiro lugar; portanto, pode ser melhor usar a DRAM estática. Veja o primeiro ponto em 'coisas que eu não faria' também.
Digamos que você tenha 1% de chance de falha de qualquer nó em um dia e vamos fingir que você pode tornar as falhas totalmente independentes. Com 5 nós, você precisará de três falhas no prazo de um dia, o que representa uma chance de 0,001%. Com mais, bem, você entendeu a idéia.
Coisas que eu não faria:
Subestime o valor de não ter o problema para começar. A menos que o peso seja uma preocupação, um grande bloco de metal em torno do seu dispositivo será uma solução muito mais barata e mais confiável do que uma equipe de programadores pode criar. O acoplamento óptico de entradas de EMI é um problema, etc. Seja como for, tente ao fornecer seus componentes para obter os que são classificados como melhores contra radiação ionizante.
Role seus próprios algoritmos . As pessoas já fizeram isso antes. Use o trabalho deles. Tolerância a falhas e algoritmos distribuídos são difíceis. Use o trabalho de outras pessoas sempre que possível.
Use configurações complicadas do compilador na esperança ingênua de detectar mais falhas. Se você tiver sorte, poderá detectar mais falhas. O mais provável é que você use um caminho de código no compilador que tenha sido menos testado, principalmente se você o tiver rolado.
Use técnicas que não foram testadas em seu ambiente. A maioria das pessoas que cria software de alta disponibilidade precisa simular modos de falha para verificar se o seu HA funciona corretamente e perder muitos modos de falha como resultado. Você está na posição 'feliz' de ter falhas frequentes sob demanda. Portanto, teste cada técnica e garanta que sua aplicação real melhore o MTBF em uma quantidade que exceda a complexidade para introduzi-lo (com a complexidade vem os bugs). Aplique isso especialmente aos meus algoritmos de quorum, etc.
fonte
Como você solicita especificamente soluções de software e usa C ++, por que não usar a sobrecarga do operador para criar seus próprios tipos de dados seguros? Por exemplo:
Em vez de usar
uint32_t
(edouble
,int64_t
etc), fazer o seu próprioSAFE_uint32_t
que contém um múltiplo (mínimo de 3) de uint32_t. Sobrecarregar todas as operações que você deseja (* + - / << >> = ==! = Etc) para executar e fazer com que as operações sobrecarregadas sejam executadas independentemente em cada valor interno, ou seja, não faça uma vez e copie o resultado. Antes e depois, verifique se todos os valores internos correspondem. Se os valores não corresponderem, você poderá atualizar o incorreto para o valor com o mais comum. Se não houver um valor mais comum, você poderá notificar com segurança que há um erro.Dessa forma, não importa se a corrupção ocorre na ALU, nos registros, na RAM ou em um barramento, você ainda terá várias tentativas e uma chance muito boa de detectar erros. Observe, no entanto, que isso funciona apenas para as variáveis que você pode substituir - seu ponteiro de pilha, por exemplo, ainda estará suscetível.
Uma história paralela: encontrei um problema semelhante, também em um chip ARM antigo. Acabou sendo uma cadeia de ferramentas que usava uma versão antiga do GCC que, juntamente com o chip específico que usamos, acionava um bug em certos casos extremos que (às vezes) corrompiam valores sendo passados para as funções. Certifique-se de que seu dispositivo não tenha problemas antes de culpá-lo pela atividade de rádio e, sim, às vezes é um bug do compilador =)
fonte
Isenção de responsabilidade: não sou profissional de radioatividade nem trabalhei para esse tipo de aplicação. Mas trabalhei com erros leves e redundância no arquivamento de longo prazo de dados críticos, que estão um pouco vinculados (mesmo problema, objetivos diferentes).
O principal problema com a radioatividade, na minha opinião, é que a radioatividade pode alternar bits, portanto a radioatividade pode / irá alterar qualquer memória digital . Esses erros são geralmente chamados de erros leves , podridão de bits etc.
A questão é: como calcular de forma confiável quando sua memória não é confiável?
Para reduzir significativamente a taxa de erros de software (às custas da sobrecarga computacional, pois serão principalmente soluções baseadas em software), você pode:
confiar no bom e velho esquema de redundância e, mais especificamente, nos códigos de correção de erros mais eficientes (mesma finalidade, mas algoritmos mais inteligentes, para que você possa recuperar mais bits com menos redundância). Isso às vezes (incorretamente) também é chamado de soma de verificação. Com esse tipo de solução, você terá que armazenar o estado completo do seu programa a qualquer momento em uma variável / classe mestre (ou uma estrutura?), Calcular um ECC e verificar se o ECC está correto antes de fazer qualquer coisa, e se não, repare os campos. Essa solução, no entanto, não garante que seu software possa funcionar (simplesmente funcionará corretamente quando puder, ou parará de funcionar, se não, porque o ECC pode informar se algo está errado) e, nesse caso, você pode parar o software para que você não obtenha resultados falsos).
ou você pode usar estruturas de dados algorítmicas resilientes, que garante, até certo ponto, que seu programa ainda fornecerá resultados corretos, mesmo na presença de erros de software. Esses algoritmos podem ser vistos como uma mistura de estruturas algorítmicas comuns com esquemas de ECC nativamente misturados, mas isso é muito mais resistente do que isso, porque o esquema de resiliência está fortemente ligado à estrutura, para que você não precise codificar procedimentos adicionais para verificar o ECC, e geralmente eles são muito mais rápidos. Essas estruturas fornecem uma maneira de garantir que seu programa funcione sob qualquer condição, até o limite teórico de erros leves. Você também pode misturar essas estruturas resilientes com o esquema de redundância / ECC para obter segurança adicional (ou codificar suas estruturas de dados mais importantes como resilientes e o restante, os dados dispensáveis que você pode recompilar das principais estruturas de dados,
Se você estiver interessado em estruturas de dados resilientes (que é um campo novo, mas interessante, em engenharia de algoritmos e redundância), recomendamos que você leia os seguintes documentos:
Introdução de estruturas de dados de algoritmos resilientes por Giuseppe F.Italiano, Universidade de Roma "Tor Vergata"
Christiano, P., Demaine, ED; Kishore, S. (2011). Estruturas de dados tolerantes a falhas e sem perdas com sobrecarga aditiva. Em Algoritmos e Estruturas de Dados (pp. 243-254). Springer Berlin Heidelberg.
Ferraro-Petrillo, U., Grandoni, F., & Italiano, GF (2013). Estruturas de dados resilientes a falhas de memória: um estudo experimental de dicionários. Journal of Experimental Algorithmics (JEA), 18, 1-6.
Italiano, GF (2010). Algoritmos e estruturas de dados resilientes. Em Algoritmos e Complexidade (pp. 13-24). Springer Berlin Heidelberg.
Se você estiver interessado em saber mais sobre o campo de estruturas de dados resilientes, pode fazer o checkout dos trabalhos de Giuseppe F. Italiano (e trabalhar o seu caminho através das referências) e o modelo de falha de RAM (introduzido em Finocchi et al. 2005; Finocchi e Italiano 2008).
/ EDIT: Ilustrei a prevenção / recuperação de erros de software principalmente para memória RAM e armazenamento de dados, mas não falei sobre erros de computação (CPU) . Outras respostas já apontaram para o uso de transações atômicas, como nos bancos de dados, por isso proponho outro esquema mais simples: redundância e voto majoritário .
A idéia é que você simplesmente faça x vezes o mesmo cálculo para cada cálculo que precisa e armazene o resultado em x variáveis diferentes (com x> = 3). Você pode comparar suas variáveis x :
Esse esquema de redundância é muito rápido comparado ao ECC (praticamente O (1)) e fornece um sinal claro quando você precisa de segurança . O voto da maioria também é (quase) garantido para nunca produzir resultados corrompidos e também para recuperar-se de pequenos erros de computação , porque a probabilidade de que x computações a mesma saída é infinitesimal (porque há uma quantidade enorme de saídas possíveis, é quase impossível obter aleatoriamente 3 vezes o mesmo, ainda menos chances se x> 3).
Portanto, com o voto da maioria, você está protegido contra saída corrompida e com redundância x == 3, você pode recuperar 1 erro (com x == 4, serão 2 erros recuperáveis etc.) - a equação exata é
nb_error_recoverable == (x-2)
onde x é o número de repetições de cálculo porque você precisa de pelo menos 2 cálculos concordantes para se recuperar usando o voto da maioria).A desvantagem é que você precisa calcular x vezes em vez de uma vez, para ter um custo de computação adicional, mas a complexidade linear é tão assintoticamente que você não perde muito pelos benefícios que obtém. Uma maneira rápida de votar por maioria é calcular o modo em uma matriz, mas você também pode usar um filtro mediano.
Além disso, se você deseja ter certeza de que os cálculos são conduzidos corretamente, se você pode criar seu próprio hardware, pode construir seu dispositivo com x CPUs e conectar o sistema para que os cálculos sejam duplicados automaticamente nas x CPUs com uma votação majoritária. mecanicamente no final (usando portas AND / OR, por exemplo). Isso geralmente é implementado em aviões e dispositivos de missão crítica (consulte redundância modular tripla ). Dessa forma, você não teria nenhuma sobrecarga computacional (já que os cálculos adicionais serão feitos em paralelo) e você terá outra camada de proteção contra erros de software (uma vez que a duplicação do cálculo e a votação majoritária serão gerenciados diretamente pelo hardware e não pelo software - que pode ser corrompido mais facilmente, pois um programa é simplesmente bits armazenados na memória ...).
fonte
Um ponto que ninguém parece ter mencionado. Você diz que está desenvolvendo no GCC e fazendo compilação cruzada no ARM. Como você sabe que não possui um código que faça suposições sobre RAM livre, tamanho inteiro, tamanho do ponteiro, quanto tempo leva para realizar uma determinada operação, quanto tempo o sistema funcionará continuamente ou várias coisas assim? Este é um problema muito comum.
A resposta é geralmente teste de unidade automatizado. Escreva chicotes de teste que exercitam o código no sistema de desenvolvimento e execute os mesmos chicotes de teste no sistema de destino. Procure diferenças!
Verifique também se há erratas no seu dispositivo incorporado. Você pode achar que há algo sobre "não faça isso porque ele falhará; portanto, habilite essa opção do compilador e o compilador funcionará com isso".
Em resumo, sua fonte mais provável de falha são os erros no seu código. Até você ter certeza de que esse não é o caso, não se preocupe (ainda) com modos de falha mais esotéricos.
fonte
Você deseja mais de 3 máquinas escravas com um mestre fora do ambiente de radiação. Todas as E / S passam pelo mestre que contém um mecanismo de votação e / ou nova tentativa. Os escravos devem ter um watchdog de hardware cada um e a chamada para batê-los deve ser cercada por CRCs ou similares para reduzir a probabilidade de choques involuntários. O bumping deve ser controlado pelo mestre, para que a conexão perdida com o mestre seja igual à reinicialização em alguns segundos.
Uma vantagem dessa solução é que você pode usar a mesma API para o mestre e para os escravos, para que a redundância se torne um recurso transparente.
Editar: A partir dos comentários, sinto a necessidade de esclarecer a "ideia da CRC". A possibilidade do escravo esbarrar em seu próprio cão de guarda é quase zero se você cercar o solavanco com CRC ou digerir verificações em dados aleatórios do mestre. Esses dados aleatórios são enviados apenas pelo mestre quando o escravo sob escrutínio está alinhado com os outros. Os dados aleatórios e CRC / resumo são limpos imediatamente após cada aumento. A frequência de resposta do mestre-escravo deve ser mais que o dobro do tempo limite do watchdog. Os dados enviados pelo mestre são gerados exclusivamente todas as vezes.
fonte
Que tal executar muitas instâncias do seu aplicativo. Se as falhas ocorrerem devido a alterações aleatórias nos bits de memória, é provável que algumas das instâncias de seu aplicativo passem e produzam resultados precisos. Provavelmente é bastante fácil (para alguém com experiência estatística) calcular quantas instâncias você precisa, dada a probabilidade de um flop de bits, para obter o menor erro geral que desejar.
fonte
O que você pergunta é um tópico bastante complexo - não é fácil de responder. Outras respostas estão ok, mas cobriram apenas uma pequena parte de todas as coisas que você precisa fazer.
Como visto nos comentários , não é possível corrigir 100% dos problemas de hardware; no entanto, é possível com alta probabilidade reduzi-los ou capturá-los usando várias técnicas.
Se eu fosse você, criaria o software do mais alto nível de integridade de segurança (SIL-4). Obtenha o documento IEC 61513 (para a indústria nuclear) e siga-o.
fonte
Alguém mencionou o uso de chips mais lentos para impedir que os íons lançassem bits tão facilmente. De maneira semelhante, talvez use um processador / ram especializado que realmente use vários bits para armazenar um único bit. Assim, fornecendo uma tolerância a falhas de hardware, porque seria muito improvável que todos os bits fossem invertidos. Então 1 = 1111, mas precisaria ser atingido 4 vezes para realmente virar. (4 pode ser um número ruim, pois se 2 bits forem invertidos, isso já é ambíguo). Portanto, se você optar por 8, obtém 8 vezes menos memória RAM e um pouco mais de tempo de acesso, mas uma representação de dados muito mais confiável. Você provavelmente poderia fazer isso tanto no nível do software com um compilador especializado (alocar x quanto mais espaço para tudo) ou na implementação da linguagem (escrever wrappers para estruturas de dados que alocam as coisas dessa maneira).
fonte
Talvez ajude a saber se isso significa que o hardware seja "projetado para este ambiente". Como isso corrige e / ou indica a presença de erros do SEU?
Em um projeto relacionado à exploração espacial, tínhamos um MCU personalizado, que geraria uma exceção / interrupção nos erros do SEU, mas com algum atraso, ou seja, alguns ciclos podem passar / as instruções serão executadas após o insn que causou a exceção do SEU.
Particularmente vulnerável era o cache de dados; portanto, um manipulador invalidaria a linha de cache incorreta e reiniciaria o programa. Somente que, devido à natureza imprecisa da exceção, a sequência de insns encabeçada pela exceção que gera insn pode não ser reinicializável.
Identificamos as sequências perigosas (não reinicializáveis) (como
lw $3, 0x0($2)
, seguidas por um insn, que modifica$2
e não depende de dados$3
), e eu fiz modificações no GCC, para que essas sequências não ocorram (por exemplo, como último recurso, separando o dois insns por anop
).Apenas algo a considerar ...
fonte
Se o seu hardware falhar, você poderá usar o armazenamento mecânico para recuperá-lo. Se sua base de código for pequena e tiver algum espaço físico, você poderá usar um armazenamento de dados mecânico.
Haverá uma superfície de material que não será afetada pela radiação. Várias engrenagens estarão lá. Um leitor mecânico funcionará em todas as marchas e será flexível para subir e descer. Para baixo significa que é 0 e para cima significa que é 1. De 0 e 1, você pode gerar sua base de código.
fonte
Use um planejador cíclico . Isso permite adicionar tempos de manutenção regulares para verificar a correção dos dados críticos. O problema mais frequentemente encontrado é a corrupção da pilha. Se o seu software for cíclico, você poderá reinicializar a pilha entre os ciclos. Não reutilize as pilhas para chamadas de interrupção, configure uma pilha separada de cada chamada de interrupção importante.
Semelhante ao conceito Watchdog são os cronômetros de prazo. Inicie um temporizador de hardware antes de chamar uma função. Se a função não retornar antes que o cronômetro do prazo seja interrompido, recarregue a pilha e tente novamente. Se ainda assim falhar após 3/5 tentativas, você precisará recarregar a partir da ROM.
Divida seu software em partes e isole essas partes para usar áreas de memória e tempos de execução separados (especialmente em um ambiente de controle). Exemplo: aquisição de sinal, dados de posse, algoritmo principal e implementação / transmissão de resultados. Isso significa que uma falha em uma parte não causará falhas no restante do programa. Portanto, enquanto reparamos a aquisição do sinal, o restante das tarefas continua com dados obsoletos.
Tudo precisa de CRCs. Se você executar fora da RAM, mesmo o seu .text precisará de um CRC. Verifique os CRCs regularmente se estiver usando um planejador cíclico. Alguns compiladores (não o GCC) podem gerar CRCs para cada seção e alguns processadores têm hardware dedicado para fazer cálculos de CRC, mas acho que isso ficaria fora do escopo da sua pergunta. A verificação de CRCs também solicita que o controlador ECC na memória repare erros de bit único antes que se torne um problema.
fonte
Em primeiro lugar, projete seu aplicativo para evitar falhas . Certifique-se de que, como parte da operação de fluxo normal, ele espere redefinir (dependendo da sua aplicação e do tipo de falha, suave ou dura). É difícil ficar perfeito: operações críticas que exigem algum grau de transacionalidade podem precisar ser verificadas e ajustadas em um nível de montagem, para que uma interrupção em um ponto-chave não possa resultar em comandos externos inconsistentes. Falha rapidamente assim que qualquer corrupção irrecuperável da memória ou desvio de fluxo de controle é detectado. Falhas de log, se possível.
Em segundo lugar, sempre que possível, corrija a corrupção e continue . Isso significa soma de verificação e correção de tabelas constantes (e código de programa, se você puder) com frequência; talvez antes de cada operação principal ou em uma interrupção cronometrada e armazenando variáveis em estruturas que se autocorrigam (novamente antes de cada operação principal ou em uma interrupção cronometrada, faça uma votação majoritária de 3 e corrija se houver um único desvio). Correções de log, se possível.
Terceiro, falha no teste . Configure um ambiente de teste repetitivo que inverta os bits na memória aleatoriamente. Isso permitirá que você replique situações de corrupção e ajude a projetar seu aplicativo em torno delas.
fonte
Dados os comentários de supercat, as tendências dos compiladores modernos e outras coisas, eu ficaria tentado a voltar aos dias antigos e escrever todo o código em alocações de montagem e memória estática em todos os lugares. Para esse tipo de confiabilidade total, acho que a montagem não incorre mais em uma grande diferença percentual do custo.
fonte
Aqui estão muitas respostas, mas tentarei resumir minhas idéias sobre isso.
Algo trava ou não funciona corretamente pode ser resultado de seus próprios erros - então deve ser facilmente corrigido quando você localizar o problema. Mas há também a possibilidade de falhas de hardware - e isso é difícil, se não impossível, de corrigir em geral.
Eu recomendaria primeiro tentar capturar a situação problemática registrando (pilha, registradores, chamadas de função) - registrando-as em algum lugar no arquivo ou transmitindo-as de alguma maneira diretamente ("oh não - estou travando").
A recuperação dessa situação de erro é reinicializada (se o software ainda estiver ativo e funcionando) ou redefinida por hardware (por exemplo, hw watchdogs). Mais fácil de começar do primeiro.
Se o problema estiver relacionado ao hardware - o registro deve ajudá-lo a identificar em qual problema de chamada de função ocorre e que pode fornecer informações internas sobre o que não está funcionando e onde.
Além disso, se o código for relativamente complexo - faz sentido "dividi-lo e conquistá-lo" - o que significa que você remove / desativa algumas chamadas de função nas quais suspeita de problemas - normalmente desabilitando metade do código e habilitando outra metade - você pode obter "funciona" / tipo de decisão "não funciona", após o qual você pode se concentrar em outra metade do código. (Onde está o problema)
Se o problema ocorrer após algum tempo - pode haver suspeita de estouro de pilha - é melhor monitorar os registros de ponto de pilha - se eles crescerem constantemente.
E se você conseguir minimizar completamente o seu código até o tipo de aplicativo "olá mundo" - e ainda falhar aleatoriamente - são esperados problemas de hardware - e é preciso haver "atualização de hardware" - o que significa inventar tais cpu / ram / ... - combinação de hardware que toleraria melhor a radiação.
O mais importante é provavelmente como você recupera seus logs se a máquina totalmente parada / zerada / não funcionar - provavelmente a primeira coisa que o boottap deve fazer - é voltar para casa se houver uma situação problemática.
Se for possível no seu ambiente também transmitir um sinal e receber resposta - você pode tentar criar algum tipo de ambiente de depuração remota on-line, mas deve ter pelo menos a mídia de comunicação funcionando e algum processador / alguma memória RAM no estado de funcionamento. E por depuração remota, quero dizer o tipo de abordagem GDB / gdb stub ou a sua própria implementação do que você precisa recuperar do seu aplicativo (por exemplo, baixar arquivos de log, baixar pilha de chamadas, baixar ram, reiniciar)
fonte
Eu realmente li muitas ótimas respostas!
Aqui estão meus 2 centavos: construa um modelo estatístico da anormalidade da memória / registro, escrevendo um software para verificar a memória ou realizar comparações frequentes de registros. Além disso, crie um emulador no estilo de uma máquina virtual onde você pode experimentar o problema. Eu acho que se você variar o tamanho da junção, a frequência do relógio, o fornecedor, a caixa, etc., observará um comportamento diferente.
Até a memória do nosso PC de mesa apresenta uma certa taxa de falhas, o que, no entanto, não prejudica o dia a dia do trabalho.
fonte