Interrupção de chamadas do sistema quando um sinal é capturado

29

Ao ler as páginas de manual nas chamadas read()e write(), parece que essas chamadas são interrompidas por sinais, independentemente de terem ou não de bloquear.

Em particular, suponha

  • um processo estabelece um manipulador para algum sinal.
  • um dispositivo é aberto (por exemplo, um terminal) com o O_NONBLOCK não configurado (ou seja, operando no modo de bloqueio)
  • o processo faz uma read()chamada de sistema para ler do dispositivo e, como resultado, executa um caminho de controle do kernel no espaço do kernel.
  • enquanto o precess está executando seu read()no espaço do kernel, o sinal para o qual o manipulador foi instalado anteriormente é entregue a esse processo e seu manipulador de sinal é invocado.

Lendo as páginas de manual e as seções apropriadas no SUSv3 'System Interfaces volume (XSH)' , verifica-se que:

Eu. Se a read()for interrompido por um sinal antes de ler quaisquer dados (ou seja, ele teve que bloquear porque não havia dados disponíveis), ele retornará -1 com errnodefinido como [EINTR].

ii. Se a read()for interrompido por um sinal após a leitura bem-sucedida de alguns dados (ou seja, foi possível iniciar o atendimento da solicitação imediatamente), ele retornará o número de bytes lidos.

Pergunta A): Estou correto ao assumir que, em ambos os casos (bloco / sem bloco), a entrega e o manuseio do sinal não são totalmente transparentes para o read()?

Caso i. parece compreensível, pois o bloqueio read()normalmente colocaria o processo no TASK_INTERRUPTIBLEestado, de modo que, quando um sinal é entregue, o núcleo coloca o processo no TASK_RUNNINGestado.

No entanto, quando read()não precisa bloquear (caso ii.) E está processando a solicitação no espaço do kernel, eu pensaria que a chegada de um sinal e seu manuseio seriam transparentes, assim como a chegada e o manuseio adequado de um HW interrupção seria. Em particular, eu teria assumido que, após a entrega do sinal, o processo seria temporariamente colocado no modo de usuário para executar seu manipulador de sinal, do qual retornaria eventualmente para terminar o processamento da interrupção read()(no espaço do kernel) para que a read()execução fosse executada. curso até a conclusão, após o qual o processo retorna ao ponto logo após a chamada para read()(no espaço do usuário), com todos os bytes disponíveis lidos como resultado.

Mas ii. parece implicar que o read()item seja interrompido, já que os dados estão disponíveis imediatamente, mas ele retorna apenas alguns dos dados (em vez de todos).

Isso me leva à minha segunda (e final) pergunta:

Pergunta B): Se minha suposição em A) está correta, por que a read()interrupção é interrompida, mesmo que não precise ser bloqueada porque há dados disponíveis para satisfazer a solicitação imediatamente? Em outras palavras, por que o read()não é retomado após a execução do manipulador de sinal, resultando no retorno de todos os dados disponíveis (que estavam disponíveis, afinal)?

darbehdar
fonte

Respostas:

29

Resumo: você está certo de que receber um sinal não é transparente, nem no caso i (interrompido sem ter lido nada) nem no caso ii (interrompido após uma leitura parcial). Caso contrário, seria necessário fazer alterações fundamentais na arquitetura do sistema operacional e na arquitetura dos aplicativos.

A visão de implementação do SO

Considere o que acontece se uma chamada do sistema for interrompida por um sinal. O manipulador de sinal executará o código do modo de usuário. Mas o manipulador syscall é o código do kernel e não confia em nenhum código no modo de usuário. Então, vamos explorar as opções para o manipulador syscall:

  • Encerre a chamada do sistema; relate quanto foi feito no código do usuário. Depende do código do aplicativo reiniciar a chamada do sistema de alguma forma, se desejado. É assim que o unix funciona.
  • Salve o estado da chamada do sistema e permita que o código do usuário continue a chamada. Isso é problemático por vários motivos:
    • Enquanto o código do usuário estiver em execução, algo poderá invalidar o estado salvo. Por exemplo, se estiver lendo um arquivo, o arquivo poderá estar truncado. Portanto, o código do kernel precisaria de muita lógica para lidar com esses casos.
    • O estado salvo não pode manter nenhum bloqueio, porque não há garantia de que o código do usuário continue a syscall e, em seguida, o bloqueio será mantido para sempre.
    • O kernel deve expor novas interfaces para retomar ou cancelar syscalls em andamento, além da interface normal para iniciar um syscall. Isso é muita complicação para um caso raro.
    • O estado salvo precisaria usar recursos (memória, pelo menos); esses recursos precisariam ser alocados e mantidos pelo kernel, mas contados na distribuição do processo. Isso não é intransponível, mas é uma complicação.
      • Observe que o manipulador de sinal pode fazer chamadas do sistema que são interrompidas; portanto, você não pode apenas ter uma atribuição estática de recursos que cubra todos os syscalls possíveis.
      • E se os recursos não puderem ser alocados? Então o syscall teria que falhar de qualquer maneira. O que significa que o aplicativo precisaria ter código para lidar com esse caso, portanto, esse design não simplificaria o código do aplicativo.
  • Permaneça em andamento (mas suspenso), crie um novo thread para o manipulador de sinais. Isso, novamente, é problemático:
    • As implementações iniciais do unix tinham um único encadeamento por processo.
    • O manipulador de sinais correria o risco de ultrapassar os sapatos do syscall. Este é um problema de qualquer maneira, mas no design atual do unix, ele está contido.
    • Os recursos precisariam ser alocados para o novo thread; Veja acima.

A principal diferença com uma interrupção é que o código de interrupção é confiável e altamente restrito. Geralmente, não é permitido alocar recursos, ou executar para sempre, trancar bloqueios e não liberá-los, ou fazer qualquer outro tipo de coisa desagradável; Como o manipulador de interrupção é escrito pelo próprio implementador do SO, ele sabe que não fará nada de errado. Por outro lado, o código do aplicativo pode fazer qualquer coisa.

A visualização do design do aplicativo

Quando um aplicativo é interrompido no meio de uma chamada do sistema, o syscall deve continuar até a conclusão? Nem sempre. Por exemplo, considere um programa como um shell que está lendo uma linha do terminal e o usuário pressiona Ctrl+C, acionando o SIGINT. A leitura não deve ser concluída; é disso que se trata o sinal. Observe que este exemplo mostra que o readsyscall deve ser interrompível mesmo que nenhum byte tenha sido lido ainda.

Portanto, deve haver uma maneira de o aplicativo dizer ao kernel para cancelar a chamada do sistema. Sob o design unix, isso acontece automaticamente: o sinal faz o syscall retornar. Outros projetos exigiriam uma maneira de o aplicativo retomar ou cancelar o syscall quando necessário.

A readchamada do sistema é do jeito que é, porque é o primitivo que faz sentido, dado o design geral do sistema operacional. O que isso significa é, aproximadamente, “leia o máximo que puder, até um limite (o tamanho do buffer), mas pare se algo mais acontecer”. Para realmente ler um buffer completo, é necessário executar readum loop até que o máximo de bytes possível seja lido; Esta é uma função de nível superior fread(3). Ao contrário de read(2)qual é uma chamada do sistema, freadé uma função de biblioteca, implementada no espaço do usuário read. É adequado para um aplicativo que lê um arquivo ou morre tentando; não é adequado para um intérprete de linha de comando ou para um programa em rede que deve limitar as conexões de maneira limpa, nem para um programa em rede que tenha conexões simultâneas e não use threads.

O exemplo de leitura em um loop é fornecido na Linux System Programming de Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Ele cuida case ie case iie muito mais.

Gilles 'SO- parar de ser mau'
fonte
Muito obrigado a Gilles por uma resposta muito concisa e clara que corrobora visões semelhantes apresentadas em um artigo sobre a filosofia de design do UNIX. Parece muito convincente para mim que o comportamento interrupção syscall tem a ver com o philosopy projeto UNIX ao invés de limitações técnicas ou impedimentos
darbehdar
@darbehdar São todos os três: filosofia de design unix (aqui principalmente que os processos são menos confiáveis ​​que o kernel e podem executar código arbitrário, também que processos e threads não são criados implicitamente), restrições técnicas (na alocação de recursos) e design de aplicativos (existem são casos em que o sinal deve cancelar o syscall).
Gilles 'SO- stop be evil'
2

Para responder à pergunta A :

Sim, a entrega e o manuseio do sinal não são totalmente transparentes para o read().

A read()execução no meio do caminho pode estar ocupando alguns recursos enquanto é interrompida pelo sinal. E o manipulador de sinal do sinal também pode chamar outro read()(ou qualquer outro syscalls seguros de sinal assíncrono ). Portanto, a read()interrupção do sinal deve ser interrompida primeiro para liberar os recursos que usa, caso contrário, os read()chamados do manipulador de sinal acessarão os mesmos recursos e causarão problemas de reentrada.

Como as chamadas do sistema que não read()podem ser chamadas pelo manipulador de sinal e também podem ocupar um conjunto idêntico de recursos, como o read()fazem. Para evitar problemas de reentrada acima, o design mais simples e seguro é interromper a interrupção read()toda vez que um sinal ocorre durante sua execução.

Justin
fonte