Como posso matar um processo e ter certeza de que o PID não foi reutilizado

39

Suponha, por exemplo, que você tenha um script de shell semelhante a:

longrunningthing &
p=$!
echo Killing longrunningthing on PID $p in 24 hours
sleep 86400
echo Time up!
kill $p

Deve fazer o truque, não é? Exceto que o processo pode ter terminado cedo e seu PID pode ter sido reciclado, o que significa que algum trabalho inocente consegue uma bomba em sua fila de sinal. Na prática isso possivelmente importa, mas ainda assim está me preocupando. Fazer o hacking para cair morto por si mesmo ou manter / remover seu PID no FS faria, mas estou pensando na situação genérica aqui.

    
por FJL 25.01.2015 / 19:22

9 respostas

28

Melhor seria usar o comando timeout se você o tiver feito, o que significa:

timeout 86400 cmd

A implementação atual (8.23) do GNU funciona pelo menos usando alarm() ou equivalente enquanto aguarda o processo filho. Ele não parece estar protegendo contra o SIGALRM sendo entregue entre waitpid() retornando e timeout saindo (efetivamente cancelando esse alarme ). Durante essa pequena janela, timeout pode até escrever mensagens no stderr (por exemplo, se a criança despejar um núcleo), o que aumentaria ainda mais a janela de corrida (indefinidamente se stderr é um tubo cheio, por exemplo).

Eu pessoalmente posso viver com essa limitação (que provavelmente será corrigida em uma versão futura). timeout também tomará cuidado extra para informar o status de saída correto, lidar com outros casos de canto (como SIGALRM bloqueado / ignorado na inicialização, lidar com outros sinais ...) melhor do que você provavelmente conseguiria fazer à mão.

Como uma aproximação, você pode escrever em perl como:

perl -MPOSIX -e '
  $p = fork();
  die "fork: $!\n" unless defined($p);
  if ($p) {
    $SIG{ALRM} = sub {
      kill "TERM", $p;
      exit 124;
    };
    alarm(86400);
    wait;
    exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
  } else {exec @ARGV}' cmd

Há um comando timelimit no link (precede o GNU timeout em alguns meses).

 timelimit -t 86400 cmd

Esse usa um mecanismo alarm() -like, mas instala um manipulador em SIGCHLD (ignorando filhos interrompidos) para detectar a criança morrendo. Ele também cancela o alarme antes de executar waitpid() (isso não cancela a entrega de SIGALRM se ele estava pendente, mas a maneira como está escrito, não consigo ver como é um problema) e mata antes chamando waitpid() (então não pode matar um pid reutilizado).

netpipes também tem um comando timelimit . Aquele que antecede todos os outros por décadas, leva ainda uma outra abordagem, mas não funciona corretamente para comandos interrompidos e retorna um status de saída 1 no tempo limite.

Como uma resposta mais direta à sua pergunta, você pode fazer algo como:

if [ "$(ps -o ppid= -p "$p")" -eq "$$" ]; then
  kill "$p"
fi

Isto é, verifique se o processo ainda é um filho nosso. Novamente, há uma pequena janela de corrida (entre ps recuperando o status desse processo e kill matando) durante a qual o processo pode morrer e seu pid ser reutilizado por outro processo.

Com alguns shells ( zsh , bash , mksh ), você pode passar especificações do job em vez de pids.

cmd &
sleep 86400
kill %
wait "$!" # to retrieve the exit status

Isso só funciona se você gerar apenas um trabalho em segundo plano (caso contrário, obter o jobpec correto nem sempre é possível de forma confiável).

Se isso for um problema, basta iniciar uma nova instância do shell:

bash -c '"$@" & sleep 86400; kill %; wait "$!"' sh cmd

Isso funciona porque o shell remove o trabalho da tabela de tarefas quando a criança está morrendo. Aqui, não deve haver nenhuma janela de corrida, pois quando o shell chamar kill() , o sinal SIGCHLD não foi tratado e o pid não pode ser reutilizado (já que não foi aguardado), ou foi manipulado e o trabalho foi removido da tabela de processos (e kill reportaria um erro). bash kill pelo menos bloqueia SIGCHLD antes de acessar sua tabela de trabalhos para expandir o % e desbloqueá-lo após o kill() .

Outra opção para evitar que o processo sleep permaneça, mesmo depois que cmd morreu, com bash ou ksh93 é usar um canal com read -t em vez de sleep :

{
  {
    cmd 4>&1 >&3 3>&- &
    printf '%d\n.' "$!"
  } | {
    read p
    read -t 86400 || kill "$p"
  }
} 3>&1

Esse ainda tem condições de corrida e você perde o status de saída do comando. Também assume que cmd não fecha seu fd4.

Você poderia tentar implementar uma solução sem raça em perl como:

perl -MPOSIX -e '
   $p = fork();
   die "fork: $!\n" unless defined($p);
   if ($p) {
     $SIG{CHLD} = sub {
       $ss = POSIX::SigSet->new(SIGALRM); $oss = POSIX::SigSet->new;
       sigprocmask(SIG_BLOCK, $ss, $oss);
       waitpid($p,WNOHANG);
       exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
           unless $? == -1;
       sigprocmask(SIG_UNBLOCK, $oss);
     };
     $SIG{ALRM} = sub {
       kill "TERM", $p;
       exit 124;
     };
     alarm(86400);
     pause while 1;
   } else {exec @ARGV}' cmd args...

(embora seja necessário melhorar para lidar com outros tipos de casos de canto).

Outro método sem raça poderia estar usando grupos de processos:

set -m
((sleep 86400; kill 0) & exec cmd)

No entanto, observe que o uso de grupos de processos pode ter efeitos colaterais se houver E / S em um dispositivo de terminal envolvido. Ele tem o benefício adicional de matar todos os outros processos extras gerados por cmd .

    
por 25.01.2015 / 22:56
27

Em geral, você não pode. Todas as respostas dadas até agora são heurísticas de buggy. Há apenas um caso em que você pode usar com segurança o pid para enviar sinais: quando o processo de destino é um filho direto do processo que enviará o sinal, e o pai ainda não o esperou. Neste caso, mesmo que tenha saído, o pid é reservado (isto é o que é um "processo zumbi") até que o pai o aguarde. Eu não estou ciente de qualquer maneira de fazer isso de forma limpa com o shell.

Uma maneira segura alternativa de eliminar processos é iniciá-los com um conjunto tty de controle para um pseudo-terminal do qual você possui o lado mestre. Você pode então enviar sinais através do terminal, por ex. escrevendo o caractere para SIGTERM ou SIGQUIT sobre o pty.

No entanto, outra maneira que é mais conveniente com o script é usar uma sessão denominada screen e enviar comandos para a sessão de tela para finalizá-la. Esse processo ocorre em um soquete de pipe ou unix chamado de acordo com a sessão de tela, que não será reutilizado automaticamente se você escolher um nome exclusivo seguro.

    
por 26.01.2015 / 04:01
10
  1. Ao iniciar o processo, salve seu horário de início:

    longrunningthing &
    p=$!
    stime=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    echo "Killing longrunningthing on PID $p in 24 hours"
    sleep 86400
    echo Time up!
    
  2. Antes de tentar matar o processo, pare (isso não é realmente essencial, mas é uma maneira de evitar condições de corrida: se você parar o processo, o pid não poderá ser reutilizado)

    kill -s STOP "$p"
    
  3. Verifique se o processo com esse PID tem o mesmo horário de início e, em caso afirmativo, mate-o, caso contrário, deixe o processo continuar:

    cur=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    if [ "$cur" = "$stime" ]
    then
        # Okay, we can kill that process
        kill "$p"
    else
        # PID was reused. Better unblock the process!
        echo "long running task already completed!"
        kill -s CONT "$p"
    fi
    

Isso funciona porque pode haver apenas um processo com a mesma hora de início de PID e em um determinado sistema operacional.

Parar o processo durante a verificação faz com que as condições de corrida não sejam um problema. Obviamente, isso tem o problema de que, algum processo aleatório pode ser interrompido por alguns milissegundos. Dependendo do tipo de processo, isso pode ou não ser um problema.

Pessoalmente, eu simplesmente usaria o python e o psutil que manipula automaticamente a reutilização do PID:

import time

import psutil

# note: it would be better if you were able to avoid using
#       shell=True here.
proc = psutil.Process('longrunningtask', shell=True)
time.sleep(86400)

# PID reuse handled by the library, no need to worry.
proc.terminate()   # or: proc.kill()
    
por 26.01.2015 / 13:24
7

Em um sistema Linux, você pode assegurar que um pid não será reutilizado, mantendo vivo o namespace pid. Isso pode ser feito através do arquivo /proc/$pid/ns/pid .

  • man namespaces -

    Bind mounting (see mount(2)) one of the files in this directory to somewhere else in the filesystem keeps the corresponding namespace of the process specified by pid alive even if all processes currently in the namespace terminate.

    Opening one of the files in this directory (or a file that is bind mounted to one of these files) returns a file handle for the corresponding namespace of the process specified by pid. As long as this file descriptor remains open, the namespace will remain alive, even if all processes in the namespace terminate. The file descriptor can be passed to setns(2).

  •   
  Você pode isolar um grupo de processos - basicamente qualquer número de processos - colocando o namespace em init .

  • man pid_namespaces -

    The first process created in a new namespace (i.e., the process created using clone(2) with the CLONE_NEWPID flag, or the first child created by a process after a call to unshare(2) using the CLONE_NEWPID flag) has the PID 1, and is the init process for the namespace (see init(1)). A child process that is orphaned within the namespace will be reparented to this process rather than init(1) (unless one of the ancestors of the child in the same PID namespace employed the prctl(2) PR_SET_CHILD_SUBREAPER command to mark itself as the reaper of orphaned descendant processes).

    If the init process of a PID namespace terminates, the kernel terminates all of the processes in the namespace via a SIGKILL signal. This behavior reflects the fact that the init process is essential for the correct operation of a PID namespace.

  •   
  O pacote util-linux fornece muitas ferramentas úteis para manipular namespaces. Por exemplo, há unshare , no entanto, se você ainda não tiver organizado seus direitos em um namespace de usuário, ele exigirá direitos de superusuário:
unshare -fp sh -c 'n=
    echo "PID = $$"
    until   [ "$((n+=1))" -gt 5 ]
    do      while   sleep 1
            do      date
            done    >>log 2>/dev/null   &
    done;   sleep 5' >log
cat log; sleep 2
echo 2 secs later...
tail -n1 log

Se você não tiver organizado um namespace de usuário, ainda assim poderá executar com segurança comandos arbitrários descartando privilégios imediatamente. O comando runuser é outro binário (não definido) fornecido pelo pacote util-linux e sua incorporação pode parecer:

sudo unshare -fp runuser -u "$USER" -- sh -c '...'

... e assim por diante.

No exemplo acima, dois switches são passados para unshare(1) the --fork flag, o que faz com que invoked sh -c processe o primeiro filho criado e garanta seu init status e o --pid flag que instrui unshare(1) para criar um namespace pid.

O processo sh -c gera cinco shells filhos com background - cada um um loop while de inifinite que continuará anexando a saída de date ao final de log enquanto o valor de sleep 1 retornar verdadeiro. Depois de gerar esses processos, sh chama sleep por mais 5 segundos e termina.

Vale a pena notar que, se o sinal -f não fosse usado, nenhum dos loops while em segundo plano seria encerrado, mas com ele ...

OUTPUT:

PID = 1
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:45 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:46 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:47 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
Mon Jan 26 19:17:48 PST 2015
2 secs later...
Mon Jan 26 19:17:48 PST 2015
    
por 27.01.2015 / 05:21
5

Considere fazer com que seu longrunningthing se comporte um pouco melhor, um pouco mais parecido com um daemon. Por exemplo, você pode criar um pidfile que permitirá pelo menos algum controle limitado do processo. Existem várias maneiras de fazer isso sem modificar o binário original, tudo envolvendo um wrapper. Por exemplo:

  1. um script simples de wrapper que iniciará a tarefa desejada em segundo plano (com redirecionamento de saída opcional), grava o PID desse processo em um arquivo, aguarde o término do processo (usando wait ) e remova o arquivo. Se durante a espera, o processo é morto, p. Ex. por algo como

    kill $(cat pidfile)
    

    o wrapper irá apenas garantir que o pidfile seja removido.

  2. um wrapper de monitor, que irá colocar o seu próprio PID em algum lugar e capturar (e responder a) sinais enviados para ele. Exemplo simples:

    #!/bin/bash
    p=0
    trap killit USR1

    killit () {
        printf "USR1 caught, killing %s\n" "$p"
        kill -9 $p
    }

    printf "monitor $$ is waiting\n"
    therealstuff &
    p=%1
    wait $p
    printf "monitor exiting\n"

Agora, como @R .. e @ StéphaneChazelas apontaram, essas abordagens geralmente têm uma condição de corrida em algum lugar ou impõem uma restrição no número de processos que você pode gerar. Além disso, ele não manipula os casos, em que longrunningthing pode separar e os filhos são separados (o que provavelmente não é o problema na pergunta original).

Com kernels Linux recentes (leia alguns anos), isso pode ser bem tratado usando cgroups , ou seja, o freezer - que, suponho, é o que alguns sistemas modernos de Linux usam.

    
por 25.01.2015 / 23:10
1

Se você está rodando no Linux (e alguns outros * nixes), você pode verificar se o processo que você pretende matar ainda é usado e que a linha de comando corresponde ao seu longo processo. Algo como:

echo Time up!
grep -q longrunningthing /proc/$p/cmdline 2>/dev/null
if [ $? -eq 0 ]
then
  kill $p
fi

Uma alternativa pode ser verificar por quanto tempo o processo que você pretende matar está sendo executado, com algo como ps -p $p -o etime= . Você poderia fazer isso sozinho extraindo esta informação de /proc/$p/stat , mas isso seria complicado (o tempo é medido em momentos, e você terá que usar o tempo de atividade do sistema em /proc/stat também).

De qualquer forma, você geralmente não pode garantir que o processo não seja substituído após sua verificação e antes de que você o mata.

    
por 25.01.2015 / 21:14
-1

Esta é realmente uma boa pergunta.

A maneira de determinar a singularidade do processo é olhar para (a) onde está na memória; e (b) o que essa memória contém. Para ser específico, queremos saber onde na memória está o texto do programa para a chamada inicial, porque sabemos que a área de texto de cada thread ocupará um local diferente na memória. Se o processo morrer e outro for lançado com o mesmo pid, o texto do programa para o novo processo não ocupará o mesmo lugar na memória e não conterá as mesmas informações.

Portanto, imediatamente após o lançamento do seu processo, faça md5sum /proc/[pid]/maps e salve o resultado. Mais tarde, quando você quiser matar o processo, faça outro md5sum e compare-o. Se corresponder, mate o pid. Se não, não faça.

para ver isso por si mesmo, lançar duas conchas bash idênticas. Examine o /proc/[pid]/maps para eles e você descobrirá que eles são diferentes. Por quê? Porque mesmo sendo o mesmo programa, eles ocupam locais diferentes na memória e os endereços de sua pilha são diferentes. Então, se o seu processo morrer e seu PID for reutilizado, mesmo que o mesmo comando seja reiniciado com os mesmos argumentos , o arquivo "maps" será diferente e você saberá que não está lidando com o processo original.

Veja: página proc man para detalhes.

Observe que o arquivo /proc/[pid]/stat já contém todas as informações mencionadas por outros anunciantes em suas respostas: idade do processo, pai pid, etc. Esse arquivo contém informações estáticas e dinâmicas, portanto, se você preferir use este arquivo como base de comparação, depois de lançar seu longrunningthing , você precisará extrair os seguintes campos estáticos do arquivo stat e salvá-los para comparação depois:

pid, nome do arquivo, pid do pai, id do grupo de processos, terminal de controle, processo de tempo iniciado após a inicialização do sistema, tamanho do conjunto residente, o endereço do início da pilha,

juntos, os acima identificam exclusivamente o processo, e isso representa outro caminho a percorrer. Na verdade, você poderia se safar com nada mais do que "pid" e "processo de tempo iniciado após a inicialização do sistema" com alto grau de confiança. Basta extrair esses campos do arquivo stat e salvá-lo em algum lugar ao iniciar seu processo. Mais tarde, antes de matá-lo, extraia-o novamente e compare. Se eles coincidirem, então você está certo de que está vendo o processo original.

    
por 27.01.2015 / 01:21
-1

Outra maneira seria verificar a idade do processo antes de matá-lo. Dessa forma, você pode ter certeza de que não está eliminando um processo que não é gerado em menos de 24 horas. Você pode adicionar uma condição if com base nisso antes de matar o processo.

if [[ $(ps -p $p -o etime=) =~ 1-. ]] ; then
    kill $p
fi

Esta condição if verificará se a ID do processo $p é menor que 24 horas (86400 segundos).

P.S: - O comando ps -p $p -o etime= terá o formato <no.of days>-HH:MM:SS

    
por 26.01.2015 / 05:23
-3

O que eu faço é, depois de ter matado o processo, fazer de novo. Toda vez que eu faço isso, a resposta volta, "nenhum tal processo"

allenb   12084  5473  0 08:12 pts/4    00:00:00 man man
allenb@allenb-P7812 ~ $ kill -9 12084
allenb@allenb-P7812 ~ $ kill -9 12084
bash: kill: (12084) - No such process
allenb@allenb-P7812 ~ $ 

Não poderia ser mais simples e estou fazendo isso há anos sem problemas.

    
por 29.01.2015 / 22:15

Tags