Como implementar seções críticas no ARM Cortex A9


15

Estou portando algum código legado de um núcleo ARM926 para o CortexA9. Esse código é baremetal e não inclui um SO ou bibliotecas padrão, todas personalizadas. Estou com uma falha que parece estar relacionada a uma condição de corrida que deve ser evitada pelo corte crítico do código.

Quero alguns comentários sobre minha abordagem para ver se minhas seções críticas podem não ser implementadas corretamente para esta CPU. Estou usando o GCC. Suspeito que haja algum erro sutil.

Além disso, existe uma biblioteca de código-fonte aberto que tenha esses tipos de primitivos para o ARM (ou mesmo uma boa biblioteca leve de spinlock / seméforo)?

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "orr r1, %[key], #0xC0\n\t"\
    "msr cpsr_c, r1\n\t" : [key]"=r"(key_) :: "r1", "cc" );

#define ARM_INT_UNLOCK(key_) asm volatile ("MSR cpsr_c,%0" : : "r" (key_))

O código é usado da seguinte maneira:

/* lock interrupts */
ARM_INT_KEY_TYPE key;
ARM_INT_LOCK(key);

<access registers, shared globals, etc...>

ARM_INT_UNLOCK(key);

A idéia da "chave" é permitir seções críticas aninhadas, e elas são usadas no início e no final das funções para criar funções reentrantes.

Obrigado!


1
consulte infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/… não faça isso no asm btw incorporado. faça com que ela funcione como o artigo.
Jason Hu

Não sei nada sobre o ARM, mas espero que, para o mutex (ou qualquer função de sincronização entre processos ou processos), você deva usar o clobber "memory" para garantir que a) todos os valores de memória atualmente armazenados em cache nos registros sejam liberados volte para a memória antes de executar o asm eb) quaisquer valores na memória acessados ​​após o asm ser recarregado. Observe que a realização de uma chamada (como recomenda o HuStmpHrrr) deve implicitamente executar esse aviso para você.

Além disso, enquanto eu ainda não falo ARM, suas restrições para 'key_' não parecem corretas. Como você diz que isso deve ser usado para reentrada, declarar como "= r" no bloqueio parece suspeito. '=' significa que você pretende substituí-lo e o valor existente não é importante. Parece mais provável que você pretenda usar '+' para indicar sua intenção de atualizar o valor existente. E novamente para desbloquear, listá-lo como uma entrada indica ao gcc que você não pretende alterá-lo, mas se não me engano, você o faz (altere). Eu acho que isso também deve ser listado como uma saída '+'.

1
+1 para codificar na montagem para um núcleo de alta especificação. Enfim, isso pode estar relacionado aos modos de privilégio?
Dzarda

Tenho certeza de que você precisará usá-lo ldrexe strexfazê-lo corretamente. Aqui está uma página da Web que mostra como usar ldrexe streximplementar um spinlock.

Respostas:


14

A parte mais difícil de lidar com uma seção crítica sem um sistema operacional não é criar o mutex, mas sim descobrir o que deve acontecer se o código quiser usar um recurso que não está disponível no momento. As instruções de carregamento exclusivo e de armazenamento condicional exclusivo facilitam bastante a criação de uma função "swap" que, dada um ponteiro para um número inteiro, armazena atomicamente um novo valor, mas retorna o que o número inteiro apontado continha:

int32_t atomic_swap(int32_t *dest, int32_t new_value)
{
  int32_t old_value;
  do
  {
    old_value = __LDREXW(&dest);
  } while(__STREXW(new_value,&dest);
  return old_value;
}

Dada uma função como a acima, é possível inserir facilmente um mutex por meio de algo como

if (atomic_swap(&mutex, 1)==0)
{
   ... do stuff in mutex ... ;
   mutex = 0; // Leave mutex
}
else
{ 
  ... couldn't get mutex...
}

Na ausência de um sistema operacional, a principal dificuldade geralmente está no código "não foi possível obter o mutex". Se ocorrer uma interrupção quando um recurso protegido por mutex estiver ocupado, pode ser necessário que o código de tratamento de interrupções defina um sinalizador e salve algumas informações para indicar o que ele queria fazer e, em seguida, tenha qualquer código principal que adquira o O mutex verifica sempre que ele libera o mutex para ver se uma interrupção deseja fazer alguma coisa enquanto o mutex foi mantido e, se houver, executa a ação em nome da interrupção.

Embora seja possível evitar problemas com interrupções que desejam usar recursos protegidos por mutex simplesmente desativando interrupções (e, de fato, desativar interrupções pode eliminar a necessidade de qualquer outro tipo de mutex), em geral é desejável evitar a desativação de interrupções por mais tempo do que o necessário.

Um compromisso útil pode ser usar um sinalizador como descrito acima, mas ter o código da linha principal que liberará as interrupções de desativação do mutex e verificar o sinalizador mencionado antes de fazê-lo (reative as interrupções após o lançamento do mutex). Essa abordagem não exige que as interrupções sejam desativadas por muito tempo, mas evitará a possibilidade de que, se o código da linha principal testar a sinalização da interrupção após o lançamento do mutex, exista um risco de que entre o momento em que a sinalize e a hora em que a sinalize age sobre ele, pode ser impedido por outro código que adquire e libera o mutex e age sobre o sinalizador de interrupção; se o código da linha principal não testar o sinalizador da interrupção após o lançamento do mutex,

De qualquer forma, o mais importante será ter um meio pelo qual o código que tenta usar um recurso protegido por mutex quando não estiver disponível terá um meio de repetir sua tentativa assim que o recurso for liberado.


7

Essa é uma maneira pesada de fazer seções críticas; desativar interrupções. Pode não funcionar se o seu sistema tiver / lida com falhas de dados. Também aumentará a latência de interrupção. O irqflags.h do Linux possui algumas macros que lidam com isso. O cpsieecpsid instruções podem ser úteis; No entanto, eles não salvam o estado e não permitem o aninhamento. cpsnão usa um registro.

Para a série Cortex-A , eles ldrex/strexsão mais eficientes e podem trabalhar para formar um mutex para a seção crítica ou podem ser usados ​​com algoritmos sem travas para se livrar da seção crítica.

Em certo sentido, o ldrex/strexparece um ARMv5 swp. No entanto, eles são muito mais complexos de implementar na prática. Você precisa de um cache de trabalho e a memória de destino ldrex/strexprecisa estar no cache. A documentação do ARM ldrex/strexé bastante nebulosa, pois eles querem mecanismos para trabalhar em CPUs que não sejam o Cortex-A. No entanto, para o Cortex-A, o mecanismo para manter o cache da CPU local sincronizado com outras CPUs é o mesmo usado para implementar as ldrex/strexinstruções. Para a série Cortex-A, a reserva granual (tamanho deldrex/strex memória reservada) é igual a uma linha de cache; você também precisará alinhar a memória à linha de cache se pretender modificar vários valores, como em uma lista duplamente vinculada.

Suspeito que haja algum erro sutil.

mrs %[key], cpsr
orr r1, %[key], #0xC0  ; context switch here?
msr cpsr_c, r1

Você precisa garantir que a sequência nunca possa ser antecipada . Caso contrário, você poderá obter duas variáveis- chave com as interrupções ativadas e a liberação do bloqueio estará incorreta. Você pode usar a swpinstrução com a memória principal para garantir consistência no ARMv5, mas esta instrução foi preterida no Cortex-A em favor deldrex/strex , pois funciona melhor em sistemas com várias CPUs.

Tudo isso depende de que tipo de agendamento seu sistema possui. Parece que você só tem linhas principais e interrupções. Geralmente, você precisa que as primitivas da seção crítica tenham alguns ganchos no planejador, dependendo de quais níveis (sistema / espaço do usuário / etc) você deseja que a seção crítica trabalhe.

Além disso, existe uma biblioteca de código-fonte aberto que tenha esses tipos de primitivos para o ARM (ou mesmo uma boa biblioteca leve de spinlock / seméforo)?

É difícil escrever de maneira portátil. Ou seja, essas bibliotecas podem existir para determinadas versões de CPUs ARM e para sistemas operacionais específicos.


2

Vejo vários problemas em potencial com essas seções críticas. Existem advertências e soluções para tudo isso, mas como um resumo:

  • Não há nada que impeça o compilador de mover o código por essas macros, para otimização ou outros motivos aleatórios.
  • Eles salvam e restauram algumas partes do estado do processador que o compilador espera que o assembly embutido deixe em paz (a menos que seja dito o contrário).
  • Não há nada impedindo que uma interrupção ocorra no meio da sequência e alterando o estado entre a leitura e a gravação.

Primeiro, você definitivamente precisa de algumas barreiras de memória do compilador . O GCC os implementa como clobbers . Basicamente, essa é uma maneira de informar ao compilador "Não, não é possível mover os acessos à memória por essa parte do assembly embutido, pois isso pode afetar o resultado dos acessos à memória". Especificamente, você precisa dos dois "memory"e dos "cc"clobbers, nas macros inicial e final. Isso impedirá que outras coisas (como chamadas de função) também sejam reordenadas em relação ao assembly embutido, porque o compilador sabe que pode ter acesso à memória. Eu vi o GCC para ARM manter o estado nos registros de código de condição no conjunto em linha com os "memory"clobbers, então você definitivamente precisa do"cc" clobber.

Em segundo lugar, essas seções críticas estão economizando e restaurando muito mais do que apenas se as interrupções estão ativadas. Especificamente, eles estão salvando e restaurando a maior parte do CPSR (Registro atual do status do programa) (o link é para o Cortex-R4 porque não consegui encontrar um bom diagrama para um A9, mas deve ser idêntico). Existem restrições sutis torno das quais partes do estado podem realmente ser modificadas, mas é mais do que necessário aqui.

Entre outras coisas, isso inclui os códigos de condição (onde os resultados de instruções como cmpsão armazenados para que instruções condicionais subsequentes possam atuar no resultado). O compilador definitivamente ficará confuso com isso. Isso é facilmente solucionável usando o"cc" triturador como mencionado acima. No entanto, isso fará com que o código falhe sempre, portanto, não parece com o que você está tendo problemas. Um pouco de uma bomba-relógio, porém, nessa modificação aleatória de outro código pode fazer com que o compilador faça algo um pouco diferente que será quebrado por isso.

Isso também tentará salvar / restaurar os bits de TI, que são usados ​​para implementar a execução condicional do Thumb . Observe que, se você nunca executar o código Thumb, isso não importa. Eu nunca descobri como o assembly embutido do GCC lida com os bits de TI, além de concluir que não, o que significa que o compilador nunca deve colocar o assembly embutido em um bloco de TI e sempre espera que o assembly termine fora de um bloco de TI. Eu nunca vi o GCC gerar código violando essas suposições e fiz um assembly interno bastante intrincado com otimização pesada, por isso tenho certeza de que eles são válidos. Isso significa que provavelmente não tentará alterar os bits de TI; nesse caso, está tudo bem. A tentativa de modificar esses bits é classificada como "arquitetonicamente imprevisível", para que ele possa fazer todos os tipos de coisas ruins, mas provavelmente não fará nada.

A última categoria de bits que serão salvos / restaurados (além dos que realmente desativam interrupções) são os bits de modo. Provavelmente, isso não muda, portanto, provavelmente não importa, mas se você tiver algum código que mude deliberadamente os modos, essas seções de interrupção podem causar problemas. Mudar entre o modo privilegiado e o usuário é o único caso que eu esperaria.

Terceiro, nada impede que uma interrupção altere outras partes do CPSR entre o MRSe o MSRin ARM_INT_LOCK. Quaisquer alterações podem ser substituídas. Na maioria dos sistemas razoáveis, as interrupções assíncronas não alteram o estado do código em que são interrompidas (incluindo o CPSR). Se o fizerem, fica muito difícil argumentar sobre o que o código fará. No entanto, é possível (a alteração do bit de desativação do FIQ me parece mais provável), portanto, você deve considerar se o seu sistema faz isso.

Aqui está como eu os implementaria de maneira a abordar todos os possíveis problemas que eu apontei:

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "ands %[key], %[key], #0xC0\n\t"\
    "cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
    "tst %[key], #0x40\n\t"\
    "beq 0f\n\t"\
    "cpsie f\n\t"\
    "0: tst %[key], #0x80\n\t"\
    "beq 1f\n\t"\
    "cpsie i\n\t"
    "1:\n\t" :: [key]"r" (key_) : "memory", "cc")

Certifique-se de compilar, -mcpu=cortex-a9porque pelo menos algumas versões do GCC (como a minha) são padronizadas para uma CPU ARM mais antiga que não suporta cpsiee cpsid.

Eu usei em andsvez de apenas andno, ARM_INT_LOCKpor isso é uma instrução de 16 bits se for usada no código Thumb. De "cc"qualquer forma, o clobber é necessário, por isso é estritamente um benefício de desempenho / tamanho do código.

0e 1são etiquetas locais , para referência.

Eles devem ser utilizados da mesma maneira que suas versões. O ARM_INT_LOCKé tão rápido / pequeno quanto o original. Infelizmente, não consegui encontrar uma maneira de ARM_INT_UNLOCKagir com segurança em qualquer lugar com poucas instruções.

Se o seu sistema tiver restrições quando IRQs e FIQs estiverem desativados, isso poderá ser simplificado. Por exemplo, se eles estiverem sempre desativados juntos, você poderá combinar um cbz+ cpsie ifcomo este:

#define ARM_INT_UNLOCK(key_) asm volatile (\
    "cbz %[key], 0f\n\t"\
    "cpsie if\n\t"\
    "0:\n\t" :: [key]"r" (key_) : "memory", "cc")

Como alternativa, se você não se importa com FIQs, é semelhante a simplesmente deixar de ativar / desativar completamente.

Se você souber que nada mais altera nenhum dos outros bits de estado no CPSR entre o bloqueio e o desbloqueio, também poderá usar continue com algo muito semelhante ao seu código original, exceto com ambos "memory"e "cc"clobbers nos dois ARM_INT_LOCKeARM_INT_UNLOCK


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.