lunes, 4 de marzo de 2013

Cazando zombies co GDB

Andaba eu pasando o rato hoxe por Devops Reactions cando topo con isto:

“Cando alguén intenta matar un proceso zombi”

E é que a quen non lle pasou que tentaba pechalos e era imposible :P, pero claro, que pasa se o proceso que os xerou nunca os recolle?, e se comezan a toma-los recursos do servidor e non queda máis remedio que reinicialo proceso, non é moi boa forma, non?

Pois xa que estivemos fozando co GDB para manipular un proceso e pechar os descriptores dos arquivos, ímos ver de eliminar os zombis :)


Ben, recapitulando, os procesos zombies foron creados por `fork()` e non foron recollidos polo proceso que os xerou, hai dúas formas de matalos, un é que o proceso os recolla (faga `wait()`) ou que ese proceso finalice, nese caso o proceso pai pasará a ser `init (pid 1)`, que os elimina cada pouco tempo.

Para as probas tiraremos deste programa, que xera 3 zombies:
#include <stdio.h>
#include <unistd.h>

#define ZOMBIE_NUMBER 3

int main(int argc, char **argv){
    
    int i;
    for (i = 0; i < ZOMBIE_NUMBER; i++){
        pid_t pid = fork();
        if (pid == 0){
            return 0;
        }
        else if (pid == -1){
            perror("fork");
            return 1;
        } 
    }

    while (1){
        printf(".");
        fflush(stdout);
        sleep(1);
    }

    return 0;
}

E xa está, compila sen cousas extrañas.
gcc zombie_spawner.c -o zombie_spawner

Como a idea é non reiniciar o server, teremos que facer `wait()`, ímos facer un script para automatizar o proceso, tomará como parámetro o PID do proceso a limpar, a partir de ahí xa podemos tomar os procesos zombies, por exemplo no meu caso o contexto é este:
$ ps aux|grep zombie
1000     25202  0.0  0.0   4044   324 pts/2    S+   01:43   0:00 ./zombie_spawner
1000     25203  0.0  0.0      0     0 pts/2    Z+   01:43   0:00 [zombie_spawner] <defunct>
1000     25204  0.0  0.0      0     0 pts/2    Z+   01:43   0:00 [zombie_spawner] <defunct>
1000     25205  0.0  0.0      0     0 pts/2    Z+   01:43   0:00 [zombie_spawner] <defunct>
1000     25208  0.0  0.0  14264   936 pts/4    S+   01:43   0:00 grep --color=auto zombie # Ignorade esta liña
$

Pero a idea é identificar os zombies programáticamente e de forma limpa.
Sabemos que o PID do proceso pai é 25202, para saber os procesos que descenden del:
$ ps h --ppid=25202
25203 pts/2    Z+     0:00 [zombie_spawner] <defunct>
25204 pts/2    Z+     0:00 [zombie_spawner] <defunct>
25205 pts/2    Z+     0:00 [zombie_spawner] <defunct>
$ 

E neste caso todos son zombis, pero por se se da o caso de que houbera un que non o fora podémos limpar a lista tirando de expresión regular, vexamos:

      Comezo da liña: ^
      Un número calquera de espazos en branco: \s*
25203 Un ou máis díxitos: \d+
      Un ou máis espazos en branco: \s+ 
pts/2 Un ou máis caracteres non espacios: [^ ]+
      Un ou máis caracteres en branco: \s+
Z     E polo fin unha Z: Z

A expresión regular é enton "^\s*\d+\s+[^ ]+\s+Z"

$ ps h --ppid=25202|grep -P '^\s*\d+\s+[^ ]+\s+Z' # O h elimina a cabeceira
25203 pts/2    Z+     0:00 [zombie_spawner] <defunct>
25204 pts/2    Z+     0:00 [zombie_spawner] <defunct>
25205 pts/2    Z+     0:00 [zombie_spawner] <defunct>
$

E esta parte xa está, con `awk` podemos separar a primeira columna.
$ ps h --ppid=25202|grep -P '^\s*\d+\s+[^ ]+\s+Z'|awk '{print $1;}'
25203
25204
25205
$

E o resto non ten chicha ningunha, é chamar a `tempfile` para xerar un arquivo temporal e meter dentro os `waitpid()` e un `detach` o final para deixar correr o proceso.
fname=`tempfile`

(for zpid in `ps h --ppid=$ppid|grep -P '^\s*\d+\s+[^ ]+\s+Z'|awk '{print $1;}'`;do
    echo "print waitpid($zpid, 0, 0)"
 done
 echo "detach") > $fname

E só queda darlle ese arquivo de órdes a GDB (podemos usar o /proc/$ppid/exe coma binario):
gdb -batch -p $ppid /proc/$ppid/exe -x $fname

Resultado:
$ ps h --ppid=25202|grep -P '^\s*\d+\s+[^ ]+\s+Z'|awk '{print $1;}'
  25203
  25204
  25205
$ sudo bash kill-zombie.sh 25202

  warning: not using untrusted file "/home/kenkeiras/.gdbinit"
  0x00007f71416c6bf0 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:82
  82 ../sysdeps/unix/syscall-template.S: Non hai tal ficheiro ou directorio.
   in ../sysdeps/unix/syscall-template.S
  $1 = 25203
  $2 = 25204
  $3 = 25205
$ ps h --ppid=25202|grep -P '^\s*\d+\s+[^ ]+\s+Z'|awk '{print $1;}'
$ # A lista, limpiña :)

E o script completo
#!/usr/bin/env bash

# Comprobamos se temos o parámetro
if [ -z "$1" ];then
    echo "$0 <zombie parent pid&gT;"
    exit 0
fi

# Gardámolo nunha variable cómoda
ppid=$1

# Xeramos un arquivo temporal
fname=`tempfile`

# Xeramos as instrucción para GDB
(for zpid in `ps h --ppid=$ppid|grep -P '^\s*\d+\s+[^ ]+\s+Z'|awk '{print $1;}'`;do
    echo "print waitpid($zpid, 0, 0)" # Limpar os procesos zombi
 done
 # E deixalo correr o acabar
 echo "detach") > $fname

# Executamos as órdes
gdb -batch -p $ppid /proc/$ppid/exe -x $fname

# E limpamos o arquivo
rm $fname

Espero que vos resulte útil, saúdos

No hay comentarios:

Publicar un comentario