Como os sinais funcionam internamente?

28

Em geral, para matar processos, geramos sinais como SIGKILL , SIGTSTP etc.

Mas como se sabe quem ordenou aquele sinal em particular, quem o enviou para um processo específico e, em geral, como os sinais realizam suas operações? Como os sinais funcionam internamente?

    
por Varun Chhangani 19.06.2013 / 22:05

3 respostas

32

A vista de 50.000 pés é a seguinte:

  1. Um sinal é gerado pelo kernel internamente (por exemplo, SIGSEGV quando um endereço inválido é acessado, ou SIGQUIT quando você aperta Ctrl + \ ), ou por um programa usando o kill syscall (ou vários relacionados).

  2. Se é por um dos syscalls, o kernel confirma que o processo de chamada tem privilégios suficientes para enviar o sinal. Se não, um erro é retornado (e o sinal não acontece).

  3. Se for um dos dois sinais especiais, o kernel age incondicionalmente nele, sem nenhuma entrada do processo de destino. Os dois sinais especiais são SIGKILL e SIGSTOP. Todas as coisas abaixo sobre ações padrão, sinais de bloqueio, etc., são irrelevantes para esses dois.

  4. Em seguida, o kernel descobre o que fazer com o sinal:

    1. Para cada processo, há uma ação associada a cada sinal. Há um monte de padrões, e os programas podem definir diferentes usando sigaction , signal , etc. Estes incluem coisas como "ignorá-lo completamente", "matar o processo", "matar o processo com um dump de núcleo", "pare o processo", etc.

    2. Os programas também podem desativar a entrega de sinais ("bloqueados"), com base em sinal-por-sinal. Então o sinal permanece pendente até ser desbloqueado.

    3. Os programas podem solicitar que, em vez de o kernel tomar alguma ação, ele entregue o sinal ao processo de forma síncrona (com sigwait , et. al. ou signalfd ) ou de forma assíncrona (interrompendo o que for processo está fazendo e chamando uma função especificada).

Existe um segundo conjunto de sinais chamados "sinais em tempo real", que não têm significado específico, e também permitem que múltiplos sinais sejam enfileirados (os sinais normais enfileiram apenas um de cada quando o sinal é bloqueado). Eles são usados em programas multiencadeados para que os encadeamentos se comuniquem entre si. Vários são usados na implementação de threads POSIX da glibc, por exemplo. Eles também podem ser usados para se comunicar entre diferentes processos (por exemplo, você pode usar vários sinais em tempo real para que um programa fooctl envie uma mensagem para o daemon foo).

Para uma visualização de 50.000 pés, tente man 7 signal e também a documentação interna do kernel (ou fonte).

    
por 19.06.2013 / 23:10
21

A implementação de sinal é muito complexa e é específica do kernel. Em outras palavras, kernels diferentes implementarão sinais de maneira diferente. Uma explicação simplificada é a seguinte:

A CPU, baseada em um valor de registro especial, tem um endereço na memória onde espera encontrar uma "tabela de descritores de interrupções" que na verdade é uma tabela de vetores. Existe um vetor para cada exceção possível, como divisão por zero ou trap, como INT 3 (debug). Quando a CPU encontra a exceção, salva os sinalizadores e o ponteiro de instrução atual na pilha e, em seguida, salta para o endereço especificado pelo vetor relevante. No Linux, esse vetor sempre aponta para o kernel, onde existe um manipulador de exceções. A CPU está pronta e o kernel do Linux assume o controle.

Note que você também pode acionar uma exceção do software. Por exemplo, o usuário pressiona CTRL - C , então esta chamada vai para o kernel que chama seu próprio handler de exceção. Em geral, há maneiras diferentes de chegar ao manipulador, mas, independentemente da mesma coisa básica, o contexto é salvo na pilha e o manipulador de exceções do kernel é saltado para.

O manipulador de exceções decide qual segmento deve receber o sinal. Se algo como divisão por zero ocorreu, então é fácil: o segmento que causou a exceção recebe o sinal, mas para outros tipos de sinais, a decisão pode ser muito complexa e em alguns casos incomuns um thread mais ou menos aleatório pode obter o sinal.

Para enviar o sinal, o que o kernel faz é primeiro definir um valor indicando o tipo de sinal, SIGHUP ou o que for. Isso é apenas um inteiro. Todo processo tem uma área de memória "sinal pendente" onde esse valor é armazenado. Então o kernel cria uma estrutura de dados com a informação do sinal. Essa estrutura inclui um sinal "disposição" que pode ser padrão, ignorar ou manipular. O kernel chama sua própria função do_signal() . A próxima fase começa.

do_signal() primeiro decide se ele irá lidar com o sinal. Por exemplo, se é um kill , então do_signal() apenas mata o processo, fim da história. Caso contrário, olha para a disposição. Se a disposição for padrão, do_signal() manipulará o sinal de acordo com uma política padrão que depende do sinal. Se a disposição for manipular, significa que há uma função no programa do usuário que é projetada para manipular o sinal em questão e o ponteiro para essa função estará na estrutura de dados mencionada anteriormente. Nesse caso, do_signal () chama outra função de kernel, handle_signal() , que passa pelo processo de alternar de volta para o modo de usuário e chamar essa função. Os detalhes dessa entrega são extremamente complexos. Este código em seu programa geralmente é vinculado automaticamente ao seu programa quando você usa as funções em signal.h .

Examinando adequadamente o valor do sinal pendente, o kernel pode determinar se o processo está lidando com todos os sinais e tomará a ação apropriada se não estiver, o que pode estar colocando o processo em suspensão ou matando-o ou outra ação, dependendo do sinal.

    
por 16.09.2014 / 19:28
14

Embora esta questão tenha sido respondida, deixe-me publicar um fluxo detalhado de eventos no kernel do Linux.
Isso é copiado inteiramente de postagens do Linux: Linux Signals - Internals no blog "posts do Linux" em sklinuxblog.blogspot.in.

Programa C do espaço do usuário de sinal

Vamos começar escrevendo um simples programa C de espaço de usuário de sinal:

#include<signal.h>
#include<stdio.h>

/* Handler function */
void handler(int sig) {
    printf("Receive signal: %u\n", sig);
};

int main(void) {
    struct sigaction sig_a;

    /* Initialize the signal handler structure */
    sig_a.sa_handler = handler;
    sigemptyset(&sig_a.sa_mask);
    sig_a.sa_flags = 0;

    /* Assign a new handler function to the SIGINT signal */
    sigaction(SIGINT, &sig_a, NULL);

    /* Block and wait until a signal arrives */
    while (1) {
            sigsuspend(&sig_a.sa_mask);
            printf("loop\n");
    }
    return 0;
};

Este código atribui um novo manipulador para o sinal SIGINT. O SIGINT pode ser enviado para o processo em execução usando a combinação de teclas Ctrl + C . Quando a tecla Ctrl + C é pressionada, o sinal assíncrono SIGINT é enviado para a tarefa. Também é equivalente a enviar o comando kill -INT <pid> em outro terminal.

Se você fizer um kill -l (que é um minúsculo L , que significa "lista"), você conhecerá os vários sinais que podem ser enviados para um processo em execução.

[root@linux ~]# kill -l
 1) SIGHUP        2) SIGINT        3) SIGQUIT       4) SIGILL        5) SIGTRAP
 6) SIGABRT       7) SIGBUS        8) SIGFPE        9) SIGKILL      10) SIGUSR1
11) SIGSEGV      12) SIGUSR2      13) SIGPIPE      14) SIGALRM      15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD      18) SIGCONT      19) SIGSTOP      20) SIGTSTP
21) SIGTTIN      22) SIGTTOU      23) SIGURG       24) SIGXCPU      25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF      28) SIGWINCH     29) SIGIO        30) SIGPWR
31) SIGSYS       34) SIGRTMIN     35) SIGRTMIN+1   36) SIGRTMIN+2   37) SIGRTMIN+3
38) SIGRTMIN+4   39) SIGRTMIN+5   40) SIGRTMIN+6   41) SIGRTMIN+7   42) SIGRTMIN+8
43) SIGRTMIN+9   44) SIGRTMIN+10  45) SIGRTMIN+11  46) SIGRTMIN+12  47) SIGRTMIN+13
48) SIGRTMIN+14  49) SIGRTMIN+15  50) SIGRTMAX-14  51) SIGRTMAX-13  52) SIGRTMAX-12
53) SIGRTMAX-11  54) SIGRTMAX-10  55) SIGRTMAX-9   56) SIGRTMAX-8   57) SIGRTMAX-7
58) SIGRTMAX-6   59) SIGRTMAX-5   60) SIGRTMAX-4   61) SIGRTMAX-3   62) SIGRTMAX-2
63) SIGRTMAX-1   64) SIGRTMAX

A seguinte combinação de teclas também pode ser usada para enviar sinais específicos:

  • Ctrl + C - envia o SIGINT, cuja ação padrão é encerrar o aplicativo.
  • Ctrl + \ - envia o SIGQUIT, cuja ação padrão é terminar o núcleo de despejo do aplicativo.
  • Ctrl + Z - envia o SIGSTOP que suspende o programa.

Se você compilar e executar o programa C acima, você obterá a seguinte saída:

[root@linux signal]# ./a.out
Receive signal: 2
loop
Receive signal: 2
loop
^CReceive signal: 2
loop

Mesmo com Ctrl + C ou kill -2 <pid> , o processo não será finalizado. Em vez disso, ele executará o manipulador de sinal e retornará.

Como o sinal é enviado para o processo

Se nós vermos os internos do sinal enviando para um processo e colocarmos o Jprobe com dump_stack na função __send_signal , nós veremos o seguinte rastreamento de chamada:

May  5 16:18:37 linux kernel: dump_stack+0x19/0x1b
May  5 16:18:37 linux kernel: my_handler+0x29/0x30 (probe)
May  5 16:18:37 linux kernel: complete_signal+0x205/0x250
May  5 16:18:37 linux kernel: __send_signal+0x194/0x4b0
May  5 16:18:37 linux kernel: send_signal+0x3e/0x80
May  5 16:18:37 linux kernel: do_send_sig_info+0x52/0xa0
May  5 16:18:37 linux kernel: group_send_sig_info+0x46/0x50
May  5 16:18:37 linux kernel: __kill_pgrp_info+0x4d/0x80
May  5 16:18:37 linux kernel: kill_pgrp+0x35/0x50
May  5 16:18:37 linux kernel: n_tty_receive_char+0x42b/0xe30
May  5 16:18:37 linux kernel:  ? ftrace_ops_list_func+0x106/0x120
May  5 16:18:37 linux kernel: n_tty_receive_buf+0x1ac/0x470
May  5 16:18:37 linux kernel: flush_to_ldisc+0x109/0x160
May  5 16:18:37 linux kernel: process_one_work+0x17b/0x460
May  5 16:18:37 linux kernel: worker_thread+0x11b/0x400
May  5 16:18:37 linux kernel: rescuer_thread+0x400/0x400
May  5 16:18:37 linux kernel:  kthread+0xcf/0xe0
May  5 16:18:37 linux kernel:  kthread_create_on_node+0x140/0x140
May  5 16:18:37 linux kernel:  ret_from_fork+0x7c/0xb0
May  5 16:18:37 linux kernel: ? kthread_create_on_node+0x140/0x140

Portanto, a principal função para enviar o sinal é como:

First shell send the Ctrl+C signal using n_tty_receive_char
n_tty_receive_char()
isig()
kill_pgrp()
__kill_pgrp_info()
group_send_sig_info() -- for each PID in group call this function
do_send_sig_info()
send_signal()
__send_signal() -- allocates a signal structure and add to task pending signals
complete_signal()
signal_wake_up()
signal_wake_up_state()  -- sets TIF_SIGPENDING in the task_struct flags. Then it wake up the thread to which signal was delivered.

Agora tudo está configurado e as alterações necessárias são feitas na task_struct do processo.

Manipulação de sinal

O sinal é verificado / tratado por um processo quando retorna da chamada do sistema ou se o retorno da interrupção é feito. O retorno da chamada do sistema está presente no arquivo entry_64.S .

A função int_signal é chamada de entry_64.S , que chama a função do_notify_resume() .

Vamos verificar a função do_notify_resume() . Esta função verifica se temos o sinalizador TIF_SIGPENDING definido no task_struct :

 /* deal with pending signal delivery */
 if (thread_info_flags & _TIF_SIGPENDING)
  do_signal(regs);
do_signal calls handle_signal to call the signal specific handler
Signals are actually run in user mode in function:
__setup_rt_frame -- this sets up the instruction pointer to handler: regs->ip = (unsigned long) ksig->ka.sa.sa_handler;

Chamadas e sinais do SISTEMA

syscalls "lentos", e. bloqueio de leitura / gravação, colocar processos em estado de espera: TASK_INTERRUPTIBLE ou TASK_UNINTERRUPTIBLE .

Uma tarefa no estado TASK_INTERRUPTIBLE será alterada para o estado TASK_RUNNING por um sinal. TASK_RUNNING significa que um processo pode ser agendado.

Se executado, seu manipulador de sinal será executado antes da conclusão do syscall “lento”. O syscall não é concluído por padrão.

Se SA_RESTART sinalizador for definido, syscall será reiniciado depois que o manipulador de sinal terminar.

Referências

por 12.07.2016 / 10:08