O que é um spinlock no Linux?

29

Eu gostaria de saber mais sobre os spinlocks do Linux em detalhes; alguém poderia me explicar?

    
por Sen 22.12.2010 / 14:53

5 respostas

30

Um bloqueio de rotação é uma forma de proteger um recurso compartilhado de ser modificado por dois ou mais processos simultaneamente. O primeiro processo que tenta modificar o recurso "adquire" o bloqueio e continua seu caminho, fazendo o que precisava com o recurso. Quaisquer outros processos que subseqüentemente tentam adquirir o bloqueio são interrompidos; Dizem que eles "giram no lugar" esperando que a fechadura seja liberada pelo primeiro processo, portanto, o nome "spin lock".

O kernel do Linux usa bloqueios de giro para muitas coisas, como ao enviar dados para um periférico específico. A maioria dos periféricos de hardware não é projetada para lidar com várias atualizações de estado simultâneas. Se duas modificações diferentes tiverem que acontecer, uma tem que seguir estritamente a outra, elas não podem se sobrepor. Um bloqueio de giro fornece a proteção necessária, garantindo que as modificações ocorram uma de cada vez.

Os bloqueios de rotação são um problema porque a rotação impede que o núcleo da CPU do encadeamento faça qualquer outro trabalho. Embora o kernel do Linux forneça serviços multitarefa para programas de espaço do usuário executados sob ele, esse recurso multitarefa de propósito geral não se estende ao código do kernel.

Esta situação está mudando e tem sido a maior parte da existência do Linux. Através do Linux 2.0, o kernel era quase exclusivamente um programa de tarefa única: sempre que a CPU executava o código do kernel, apenas um núcleo da CPU era usado, porque havia um único bloqueio de rotação protegendo todos os recursos compartilhados, chamado Big Kernel Lock (BKL ). Começando com o Linux 2.2, a BKL está sendo lentamente dividida em muitos bloqueios independentes, cada um protegendo uma classe de recurso mais focada. Hoje, com o kernel 2.6, o BKL ainda existe, mas é usado apenas por códigos realmente antigos que não podem ser facilmente movidos para um bloqueio mais granular. Agora é bem possível que uma caixa multicore tenha cada CPU executando código de kernel útil.

Há um limite para a utilidade de dividir o BKL porque o kernel do Linux não possui multitarefa geral. Se um núcleo da CPU ficar bloqueado girando em um bloqueio de rotação do kernel, ele não poderá ser submetido novamente, para fazer outra coisa até que o bloqueio seja liberado. Ele apenas fica e gira até que a trava seja liberada.

Os bloqueios de giro podem efetivamente transformar uma caixa de 16 núcleos de monstro em uma caixa de núcleo único, se a carga de trabalho for tal que cada núcleo esteja sempre à espera de um único bloqueio de giro. Este é o principal limite para a escalabilidade do kernel do Linux: dobrar os núcleos da CPU de 2 para 4 provavelmente irá quase dobrar a velocidade de uma caixa Linux, mas duplicá-la de 16 para 32 provavelmente não, com a maioria das cargas de trabalho. >     

por 22.12.2010 / 17:21
10

Um bloqueio de giro é quando um processo pesquisa continuamente por um bloqueio a ser removido. É considerado ruim porque o processo está consumindo ciclos (geralmente) desnecessariamente. Não é específico do Linux, mas um padrão geral de programação. E embora seja geralmente considerada uma prática ruim, é, de fato, a solução correta; há casos em que o custo de usar o planejador é maior (em termos de ciclos de CPU) do que o custo dos poucos ciclos que o spinlock deve durar.

Exemplo de um spinlock:

#!/bin/sh
#wait for some program to clear a lock before doing stuff
while [ -f /var/run/example.lock ]; do
  sleep 1
done
#do stuff

Existe frequentemente uma maneira de evitar um bloqueio de rotação. Para este exemplo em particular, existe uma ferramenta do Linux chamada inotifywait (normalmente não é instalada por padrão). Se fosse escrito em C, você usaria simplesmente a API inotify que o Linux fornece.

O mesmo exemplo, usando inotifywait, mostra como realizar a mesma coisa sem um bloqueio de rotação:

#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff
    
por 22.12.2010 / 15:08
6

Quando um thread tenta adquirir um bloqueio, três coisas podem acontecer se ele falhar, ele pode tentar e bloquear, ele pode tentar e continuar, ele pode tentar, em seguida, ir dormir dizendo ao SO para despertar quando algum evento acontecer .

Agora, tente e continue usando muito menos tempo do que tentar e bloquear. Digamos que no momento em que "tentar e continuar" tomará uma unidade de tempo e uma "tentativa e bloqueio" levará cem.

Agora, vamos assumir que, em média, um thread levará 4 unidades de tempo mantendo a trava. É um desperdício esperar 100 unidades. Então, você escreve um loop de "try and continue". Na quarta tentativa você geralmente irá adquirir o bloqueio. Este é um bloqueio de giro. Chama-se isso porque o fio continua girando no lugar até conseguir o bloqueio.

Uma medida de segurança adicional é limitar o número de vezes que o loop é executado. Então, no exemplo, você faz um loop for, por exemplo, seis vezes, se falhar, você "tenta bloquear".

Se você souber que um segmento sempre manterá a fechadura para duzentas unidades, você estará desperdiçando o tempo do computador para cada tentativa e continuará.

Então, no final, um bloqueio de giro pode ser muito eficiente ou um desperdício. É um desperdício quando o tempo "típico" para manter um bloqueio é maior que o tempo que leva para "tentar bloquear". É eficiente quando o tempo típico para manter um bloqueio é muito menor do que o tempo para 'tentar bloquear'.

Ps: o livro para ler nos tópicos é "A Thread Primer", se você ainda puder encontrá-lo.

    
por 23.12.2010 / 00:19
5

Um bloqueio é uma maneira de sincronizar duas ou mais tarefas (processos, threads). Especificamente, quando ambas as tarefas precisam de acesso intermitente a um recurso que só pode ser usado por uma tarefa por vez, é uma maneira de as tarefas organizarem o não uso do recurso ao mesmo tempo. Para acessar o recurso, uma tarefa deve executar as seguintes etapas:

take the lock
use the resource
release the lock

Fazer um bloqueio não é possível se outra tarefa já o tiver feito. (Pense no bloqueio como um objeto de token físico. O objeto está em uma gaveta ou alguém o tem na mão. Somente a pessoa que segura o objeto pode acessar o recurso.) Portanto, “pegar a fechadura” significa “esperar até ninguém mais tem a fechadura, então pegue-a ”.

De um ponto de vista de alto nível, existem duas maneiras principais de implementar bloqueios: spinlocks e condições. Com spinlocks , usar o bloqueio significa apenas “girar” (ou seja, não fazer nada em um loop) até que ninguém mais tenha o bloqueio. Com as condições, se uma tarefa tentar pegar o bloqueio, mas estiver bloqueada porque outra tarefa o mantém, o recém-chegado entrará em uma fila de espera; a operação de liberação sinaliza para qualquer tarefa em espera que o bloqueio esteja disponível agora.

(Essas explicações não são suficientes para permitir que você implemente um bloqueio, porque eu não disse nada sobre atomicidade. Mas a atomicidade não é importante aqui.)

Spinlocks são obviamente um desperdício: a tarefa de espera continua verificando se a trava foi tirada. Então, por que e quando é usado? Spinlocks geralmente são muito baratos para serem obtidos no caso em que a trava não é mantida. Isso torna atraente quando a chance de o bloqueio ser mantido é pequena. Além disso, os spinlocks só são viáveis se a obtenção do bloqueio não for demorada. Portanto, os spinlocks tendem a ser usados em situações em que eles permanecerão retidos por um tempo muito curto, de modo que a maioria das tentativas é esperada na primeira tentativa, e aqueles que precisam de uma espera não esperam muito.

Há uma boa explicação sobre spinlocks e outros mecanismos de concorrência do kernel do Linux em Drivers de Dispositivos Linux , capítulo 5.

    
por 22.12.2010 / 21:54
2

Um spinlock é um bloqueio que opera desabilitando o agendador e possivelmente interrompe (variante irqsave) naquele núcleo em particular no qual o bloqueio é adquirido. É diferente de um mutex, pois desabilita o agendamento, de modo que somente o seu thread pode ser executado enquanto o spinlock é mantido. Um mutex permite que outros encadeamentos de prioridade mais alta sejam programados enquanto ele é mantido, mas não permite que eles executem simultaneamente a seção protegida. Como os spinlocks desativam a multitarefa, você não pode executar um spinlock e, em seguida, chamar outro código que tentará adquirir um mutex. Seu código dentro da seção de spinlock nunca deve dormir (o código normalmente dorme quando encontra um mutex bloqueado ou um semáforo vazio).

Outra diferença com um mutex é que os threads geralmente fazem fila para um mutex, portanto um mutex abaixo tem uma fila. Considerando que spinlock apenas garante que nenhum outro segmento será executado, mesmo que seja necessário. Portanto, você nunca deve manter um spinlock ao chamar funções fora do seu arquivo que você não tem certeza de que não conseguirá dormir.

Quando quiser compartilhar seu spinlock com uma interrupção, você deve usar a variante irqsave. Isso não apenas desabilitará o agendador, mas também desabilitará as interrupções. Faz sentido certo? O Spinlock funciona garantindo que nada mais rode. Se você não quiser que uma interrupção seja executada, desative-a e prossiga com segurança na seção crítica.

Na máquina multicore, um spinlock irá girar esperando por outro núcleo que mantenha a trava para liberá-lo. Essa fiação só acontece em máquinas multicore, porque em máquinas de núcleo único isso não pode acontecer (ou você segura o spinlock e continua ou você nunca roda até que o bloqueio seja liberado).

Spinlock não é um desperdício quando faz sentido. Para seções críticas muito pequenas, seria um desperdício alocar uma fila de tarefas mutex, em vez de simplesmente suspender o planejador por alguns microssegundos necessários para concluir o trabalho importante. Se você precisar dormir ou segurar a trava em uma operação do io (que pode dormir), use um mutex. Certamente nunca bloqueie um spinlock e tente liberá-lo dentro de uma interrupção. Enquanto isso vai funcionar, será como a porcaria arduino de while (flagnotset); Nesse caso, use um semáforo.

Pegue um spinlock quando precisar de exclusão mútua simples para blocos de transações de memória. Pegue um mutex quando desejar que vários encadeamentos parem logo antes de um bloqueio mutex e, em seguida, o encadeamento de prioridade mais alta seja escolhido para continuar quando o mutex ficar livre e quando você bloquear e liberar no mesmo encadeamento. Pegue um semáforo quando você pretende publicá-lo em um segmento ou uma interrupção e levá-lo em outro segmento. São três maneiras ligeiramente diferentes de garantir a exclusão mútua e são usadas para propósitos ligeiramente diferentes.

    
por 14.12.2017 / 03:14