Strace en Linux: historia, diseño y uso

Strace en Linux: historia, diseño y uso

En los sistemas operativos tipo Unix, la comunicación de un programa con el mundo exterior y el sistema operativo se produce a través de un pequeño conjunto de funciones: llamadas al sistema. Esto significa que, con fines de depuración, puede resultar útil espiar las llamadas al sistema que ejecutan los procesos.

Una utilidad te ayuda a monitorear la “vida íntima” de los programas en Linux strace, que es el tema de este artículo. Ejemplos del uso de equipos de espionaje van acompañados de una breve historia. strace y una descripción del diseño de dichos programas.

contenido

Origen de las especies

La interfaz principal entre los programas y el kernel del sistema operativo en Unix son las llamadas al sistema. llamadas al sistema, llamadas al sistema), la interacción de los programas con el mundo exterior se produce exclusivamente a través de ellos.

Pero en la primera versión pública de Unix (Versión 6 Unix, 1975) no había formas convenientes de rastrear el comportamiento de los procesos de los usuarios. Para resolver este problema, Bell Labs actualizará a la siguiente versión (Versión 7 Unix, 1979) propuso una nueva llamada al sistema: ptrace.

ptrace se desarrolló principalmente para depuradores interactivos, pero a finales de los años 80 (en la era del comercio Versión 4 del sistema V) sobre esta base, aparecieron y se utilizaron ampliamente depuradores con un enfoque limitado (rastreadores de llamadas al sistema).

primero Paul Cronenburg publicó la misma versión de strace en la lista de correo comp.sources.sun en 1992 como alternativa a una utilidad cerrada. trace del sol. Tanto el clon como el original estaban destinados a SunOS, pero en 1994 strace fue portado a System V, Solaris y el cada vez más popular Linux.

Hoy en día strace solo soporta Linux y depende del mismo ptrace, cubierto de muchas extensiones.

Mantenedor moderno (y muy activo) strace - Дмитрий левин. Gracias a él, la utilidad adquirió funciones avanzadas como la inyección de errores en las llamadas al sistema, soporte para una amplia gama de arquitecturas y, lo más importante, mascota. Fuentes no oficiales afirman que la elección recayó en el avestruz debido a la consonancia entre la palabra rusa "avestruz" y la palabra inglesa "strace".

También es importante que la llamada al sistema ptrace y los rastreadores nunca se incluyeron en POSIX, a pesar de una larga historia e implementación en Linux, FreeBSD, OpenBSD y Unix tradicional.

Dispositivo Strace en pocas palabras: Piglet Trace

"No se espera que entiendas esto" (Dennis Ritchie, comentario en la versión 6 del código fuente de Unix)

Desde pequeño no soporto las cajas negras: no jugaba con juguetes, pero intentaba entender su estructura (los adultos usaban la palabra "roto", pero no creen en las malas lenguas). Quizás es por eso que la cultura informal del primer Unix y el movimiento moderno de código abierto me resultan tan cercanas.

A los efectos de este artículo, no es razonable desmontar el código fuente de strace, que ha crecido a lo largo de décadas. Pero no debería haber secretos para los lectores. Por lo tanto, para mostrar el principio de funcionamiento de dichos programas strace, proporcionaré el código para un rastreador en miniatura: Rastro de lechón (ptr). No sabe cómo hacer nada especial, pero lo principal son las llamadas al sistema del programa: genera:

$ gcc examples/piglet-trace.c -o ptr
$ ptr echo test > /dev/null
BRK(12) -> 94744690540544
ACCESS(21) -> 18446744073709551614
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694657216512
CLOSE(3) -> 0
ACCESS(21) -> 18446744073709551614
unknown(257) -> 3
READ(0) -> 832
FSTAT(5) -> 0
MMAP(9) -> 140694657208320
MMAP(9) -> 140694650953728
MPROTECT(10) -> 0
MMAP(9) -> 140694655045632
MMAP(9) -> 140694655070208
CLOSE(3) -> 0
unknown(158) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MPROTECT(10) -> 0
MUNMAP(11) -> 0
BRK(12) -> 94744690540544
BRK(12) -> 94744690675712
unknown(257) -> 3
FSTAT(5) -> 0
MMAP(9) -> 140694646390784
CLOSE(3) -> 0
FSTAT(5) -> 0
IOCTL(16) -> 18446744073709551591
WRITE(1) -> 5
CLOSE(3) -> 0
CLOSE(3) -> 0
unknown(231)
Tracee terminated

Piglet Trace reconoce alrededor de cientos de llamadas al sistema Linux (consulte. la mesa) y solo funciona en arquitectura x86-64. Esto es suficiente para fines educativos.

Veamos el trabajo de nuestro clon. En el caso de Linux, los depuradores y rastreadores utilizan, como se mencionó anteriormente, la llamada al sistema ptrace. Funciona pasando en el primer argumento los identificadores de comando, de los cuales solo necesitamos PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

El rastreador comienza en el estilo habitual de Unix: fork(2) lanza un proceso hijo, que a su vez utiliza exec(3) lanza el programa en estudio. La única sutileza aquí es el desafío. ptrace(PTRACE_TRACEME) antes exec: El proceso hijo espera que el proceso padre lo supervise:

pid_t child_pid = fork();
switch (child_pid) {
case -1:
    err(EXIT_FAILURE, "fork");
case 0:
    /* Child here */
    /* A traced mode has to be enabled. A parent will have to wait(2) for it
     * to happen. */
    ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    /* Replace itself with a program to be run. */
    execvp(argv[1], argv + 1);
    err(EXIT_FAILURE, "exec");
}

El proceso padre ahora debería llamar wait(2) en el proceso hijo, es decir, asegúrese de que se haya producido el cambio al modo de seguimiento:

/* Parent */

/* First we wait for the child to set the traced mode (see
 * ptrace(PTRACE_TRACEME) above) */
if (waitpid(child_pid, NULL, 0) == -1)
    err(EXIT_FAILURE, "traceme -> waitpid");

En este punto, los preparativos están completos y puede proceder directamente al seguimiento de las llamadas al sistema en un bucle sin fin.

Llamar ptrace(PTRACE_SYSCALL) garantiza que las posteriores wait parent se completará antes de que se ejecute la llamada al sistema o inmediatamente después de que se complete. Entre dos llamadas puedes realizar cualquier acción: reemplazar la llamada por una alternativa, cambiar los argumentos o el valor de retorno.

Solo necesitamos llamar el comando dos veces. ptrace(PTRACE_GETREGS)para obtener el estado de registro rax antes de la llamada (número de llamada del sistema) e inmediatamente después (valor de retorno).

En realidad, el ciclo:

/* A system call tracing loop, one interation per call. */
for (;;) {
    /* A non-portable structure defined for ptrace/GDB/strace usage mostly.
     * It allows to conveniently dump and access register state using
     * ptrace. */
    struct user_regs_struct registers;

    /* Enter syscall: continue execution until the next system call
     * beginning. Stop right before syscall.
     *
     * It's possible to change the system call number, system call
     * arguments, return value or even avoid executing the system call
     * completely. */
  if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
      err(EXIT_FAILURE, "enter_syscall");
  if (waitpid(child_pid, NULL, 0) == -1)
      err(EXIT_FAILURE, "enter_syscall -> waitpid");

  /* According to the x86-64 system call convention on Linux (see man 2
   * syscall) the number identifying a syscall should be put into the rax
   * general purpose register, with the rest of the arguments residing in
   * other general purpose registers (rdi,rsi, rdx, r10, r8, r9). */
  if (ptrace(PTRACE_GETREGS, child_pid, NULL, &registers) == -1)
      err(EXIT_FAILURE, "enter_syscall -> getregs");

  /* Note how orig_rax is used here. That's because on x86-64 rax is used
   * both for executing a syscall, and returning a value from it. To
   * differentiate between the cases both rax and orig_rax are updated on
   * syscall entry/exit, and only rax is updated on exit. */
  print_syscall_enter(registers.orig_rax);

  /* Exit syscall: execute of the syscall, and stop on system
   * call exit.
   *
   * More system call tinkering possible: change the return value, record
   * time it took to finish the system call, etc. */
  if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1)
      err(EXIT_FAILURE, "exit_syscall");
  if (waitpid(child_pid, NULL, 0) == -1)
      err(EXIT_FAILURE, "exit_syscall -> waitpid");

  /* Retrieve register state again as we want to inspect system call
   * return value. */
  if (ptrace(PTRACE_GETREGS, child_pid, NULL, &registers) == -1) {
      /* ESRCH is returned when a child terminates using a syscall and no
       * return value is possible, e.g. as a result of exit(2). */
      if (errno == ESRCH) {
          fprintf(stderr, "nTracee terminatedn");
          break;
      }
      err(EXIT_FAILURE, "exit_syscall -> getregs");
  }

  /* Done with this system call, let the next iteration handle the next
   * one */
  print_syscall_exit(registers.rax);
}

Ese es todo el rastreador. Ahora ya sabes por dónde empezar la próxima transferencia. DTrace en Linux.

Conceptos básicos: ejecutar un programa en ejecución strace

Como primer caso de uso strace, quizás valga la pena citar el método más simple: iniciar una aplicación en ejecución strace.

Para no ahondar en la interminable lista de llamadas de un programa típico, escribimos programa minimo vokrug write:

int main(int argc, char *argv[])
{
    char str[] = "write me to stdoutn";
    /* write(2) is a simple wrapper around a syscall so it should be easy to
     * find in the syscall trace. */
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Construyamos el programa y asegurémonos de que funcione:

$ gcc examples/write-simple.c -o write-simple
$ ./write-simple
write me to stdout

Y finalmente, ejecutémoslo bajo control de strace:

$ strace ./write-simple
pexecve("./write", ["./write"], 0x7ffebd6145b0 /* 71 vars */) = 0
brk(NULL)                               = 0x55ff5489e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=197410, ...}) = 0
mmap(NULL, 197410, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f7a2a633000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a631000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f7a2a04c000
mprotect(0x7f7a2a233000, 2097152, PROT_NONE) = 0
mmap(0x7f7a2a433000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f7a2a433000
mmap(0x7f7a2a439000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f7a2a439000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f7a2a6324c0) = 0
mprotect(0x7f7a2a433000, 16384, PROT_READ) = 0
mprotect(0x55ff52b52000, 4096, PROT_READ) = 0
mprotect(0x7f7a2a664000, 4096, PROT_READ) = 0
munmap(0x7f7a2a633000, 197410)          = 0
write(1, "write me to stdoutn", 20write me to stdout
)  = 20
exit_group(0)                           = ?

Muy “prolijo” y poco educativo. Hay dos problemas aquí: la salida del programa se mezcla con la salida strace y una gran cantidad de llamadas al sistema que no nos interesan.

Puede separar el flujo de salida estándar del programa y la salida de error de seguimiento utilizando el modificador -o, que redirige la lista de llamadas al sistema a un archivo de argumentos.

Queda por abordar el problema de las llamadas “extra”. Supongamos que solo nos interesan las llamadas. write... Llave -e le permite especificar expresiones por las cuales se filtrarán las llamadas al sistema. La opción de condición más popular es, naturalmente, trace=*, con el que podrás dejar sólo las llamadas que nos interesen.

Cuando se usa simultáneamente -o и -e obtendremos:

$ strace -e trace=write -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20
)  = 20
+++ exited with 0 +++

Entonces, verás, es mucho más fácil de leer.

También puede eliminar llamadas al sistema, por ejemplo aquellas relacionadas con la asignación y liberación de memoria:

$ strace -e trace=!brk,mmap,mprotect,munmap -owrite-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
execve("./write-simple", ["./write-simple"], 0x7ffe9972a498 /* 69 vars */) = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=124066, ...}) = 0
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1260342"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f00f0be74c0) = 0
write(1, "write me to stdoutn", 20)  = 20
exit_group(0)                           = ?
+++ exited with 0 +++

Tenga en cuenta el signo de exclamación escapado en la lista de llamadas excluidas: esto lo requiere el shell de comandos. shell).

En mi versión de glibc, una llamada al sistema finaliza el proceso exit_group, no tradicional _exit. Ésta es la dificultad de trabajar con llamadas al sistema: la interfaz con la que trabaja el programador no está directamente relacionada con las llamadas al sistema. Además, cambia periódicamente según la implementación y la plataforma.

Conceptos básicos: unirse al proceso sobre la marcha

Inicialmente, la llamada al sistema ptrace en el que se construyó strace, solo se puede utilizar cuando se ejecuta el programa en un modo especial. Esta limitación puede haber parecido razonable en los días de la Versión 6 de Unix. Hoy en día esto ya no es suficiente: a veces es necesario investigar los problemas de un programa en funcionamiento. Un ejemplo típico es un proceso bloqueado en un mango o en suspensión. Por lo tanto moderno strace puede unirse a procesos sobre la marcha.

Ejemplo de congelación programa:

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str[] = "write men";

    write(STDOUT_FILENO, str, sizeof(str));

    /* Sleep indefinitely or until a signal arrives */
    pause();

    write(STDOUT_FILENO, str, sizeof(str));

    return EXIT_SUCCESS;
}

Construyamos el programa y asegurémonos de que esté congelado:

$ gcc examples/write-sleep.c -o write-sleep
$ ./write-sleep
./write-sleep
write me
^C
$

Ahora intentemos unirnos a él:

$ ./write-sleep &
[1] 15329
write me
$ strace -p 15329
strace: Process 15329 attached
pause(
^Cstrace: Process 15329 detached
 <detached ...>

Programa bloqueado por llamada pause. Veamos cómo reacciona a las señales:

$ strace -o write-sleep.log -p 15329 &
strace: Process 15329 attached
$
$ kill -CONT 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause(
$
$ kill -TERM 15329
$ cat write-sleep.log
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
pause()                                 = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=14989, si_uid=1001} ---
+++ killed by SIGTERM +++

Lanzamos el programa congelado y nos unimos a él usando strace. Dos cosas quedaron claras: la llamada al sistema de pausa ignora las señales sin controladores y, lo que es más interesante, strace monitorea no solo las llamadas al sistema, sino también las señales entrantes.

Ejemplo: seguimiento de procesos secundarios

Trabajar con procesos a través de una llamada. fork - la base de todos los Unixes. Veamos cómo funciona strace con un árbol de procesos usando el ejemplo de un simple “breeding” programa:

int main(int argc, char *argv[])
{
    pid_t parent_pid = getpid();
    pid_t child_pid = fork();
    if (child_pid == 0) {
        /* A child is born! */
        child_pid = getpid();

        /* In the end of the day printf is just a call to write(2). */
        printf("child (self=%d)n", child_pid);
        exit(EXIT_SUCCESS);
    }

    printf("parent (self=%d, child=%d)n", parent_pid, child_pid);

    wait(NULL);

    exit(EXIT_SUCCESS);
}

Aquí el proceso original crea un proceso hijo, y ambos escriben en la salida estándar:

$ gcc examples/fork-write.c -o fork-write
$ ./fork-write
parent (self=11274, child=11275)
child (self=11275)

De forma predeterminada, solo veremos llamadas al sistema desde el proceso principal:

$ strace -e trace=write -ofork-write.log ./fork-write
child (self=22049)
parent (self=22048, child=22049)
$ cat fork-write.log
write(1, "parent (self=22048, child=22049)"..., 33) = 33
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22049, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

La bandera le ayuda a realizar un seguimiento de todo el árbol de procesos. -fcon la cual strace monitorea las llamadas al sistema en procesos secundarios. Esto se suma a cada línea de salida. pid proceso que genera una salida del sistema:

$ strace -f -e trace=write -ofork-write.log ./fork-write
parent (self=22710, child=22711)
child (self=22711)
$ cat fork-write.log
22710 write(1, "parent (self=22710, child=22711)"..., 33) = 33
22711 write(1, "child (self=22711)n", 19) = 19
22711 +++ exited with 0 +++
22710 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=22711, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
22710 +++ exited with 0 +++

En este contexto, puede resultar útil filtrar por grupo de llamadas al sistema:

$ strace -f -e trace=%process -ofork-write.log ./fork-write
parent (self=23610, child=23611)
child (self=23611)
$ cat fork-write.log
23610 execve("./fork-write", ["./fork-write"], 0x7fff696ff720 /* 63 vars */) = 0
23610 arch_prctl(ARCH_SET_FS, 0x7f3d03ba44c0) = 0
23610 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f3d03ba4790) = 23611
23610 wait4(-1,  <unfinished ...>
23611 exit_group(0)                     = ?
23611 +++ exited with 0 +++
23610 <... wait4 resumed> NULL, 0, NULL) = 23611
23610 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=23611, si_uid=1001, si_status=0, si_utime=0, si_stime=0} ---
23610 exit_group(0)                     = ?
23610 +++ exited with 0 +++

Por cierto, ¿qué llamada al sistema se utiliza para crear un nuevo proceso?

Ejemplo: rutas de archivos en lugar de identificadores

Conocer los descriptores de archivos es ciertamente útil, pero los nombres de los archivos específicos a los que accede un programa también pueden resultar útiles.

el próximo programa escribe la línea en el archivo temporal:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    char tmp_filename_template[] = "/tmp/output_fileXXXXXX";

    int out_fd = mkstemp(tmp_filename_template);
    if (out_fd == -1) {
        perror("mkstemp");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Durante una llamada normal strace mostrará el valor del número de descriptor pasado a la llamada al sistema:

$ strace -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3, "write me to a filen", 20)  = 20
+++ exited with 0 +++

con una bandera -y La utilidad muestra la ruta al archivo al que corresponde el descriptor:

$ strace -y -e trace=write -o write-tmp-file.log ./write-tmp-file
$ cat write-tmp-file.log
write(3</tmp/output_fileCf5MyW>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Ejemplo: seguimiento de acceso a archivos

Otra característica útil: muestra solo las llamadas al sistema asociadas con un archivo específico. Próximo programa agrega una línea a un archivo arbitrario pasado como argumento:

void do_write(int out_fd)
{
    char str[] = "write me to a filen";

    if (sizeof(str) != write(out_fd, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    /*
     * Path will be provided by the first program argument.
     *  */
    const char *path = argv[1];

    /*
     * Open an existing file for writing in append mode.
     *  */
    int out_fd = open(path, O_APPEND | O_WRONLY);
    if (out_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    do_write(out_fd);

    return EXIT_SUCCESS;
}

Por defecto strace muestra mucha información innecesaria. Bandera -P con un argumento hace que strace imprima solo llamadas al archivo especificado:

$ strace -y -P/tmp/test_file.log -o write-file.log ./write-file /tmp/test_file.log
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = 3</tmp/test_file.log>
write(3</tmp/test_file.log>, "write me to a filen", 20) = 20
+++ exited with 0 +++

Ejemplo: programas multiproceso

Utilidad strace También puede ayudar cuando se trabaja con subprocesos múltiples. el programa. El siguiente programa escribe en la salida estándar de dos flujos:

void *thread(void *arg)
{
    (void) arg;

    printf("Secondary thread: workingn");
    sleep(1);
    printf("Secondary thread: donen");

    return NULL;
}

int main(int argc, char *argv[])
{
    printf("Initial thread: launching a threadn");

    pthread_t thr;
    if (0 != pthread_create(&thr, NULL, thread, NULL)) {
        fprintf(stderr, "Initial thread: failed to create a thread");
        exit(EXIT_FAILURE);
    }

    printf("Initial thread: joining a threadn");
    if (0 != pthread_join(thr, NULL)) {
        fprintf(stderr, "Initial thread: failed to join a thread");
        exit(EXIT_FAILURE);
    };

    printf("Initial thread: done");

    exit(EXIT_SUCCESS);
}

Naturalmente, debe compilarse con un saludo especial al vinculador: la bandera -pthread:

$ gcc examples/thread-write.c -pthread -o thread-write
$ ./thread-write
/thread-write
Initial thread: launching a thread
Initial thread: joining a thread
Secondary thread: working
Secondary thread: done
Initial thread: done
$

Bandera -f, como en el caso de los procesos regulares, agregará el pid del proceso al comienzo de cada línea.

Naturalmente, no estamos hablando de un identificador de hilo en el sentido de la implementación del estándar POSIX Threads, sino del número utilizado por el programador de tareas en Linux. Desde el punto de vista de este último, no hay procesos ni subprocesos: hay tareas que deben distribuirse entre los núcleos disponibles de la máquina.

Cuando se trabaja en varios subprocesos, las llamadas al sistema se vuelven demasiadas:

$ strace -f -othread-write.log ./thread-write
$ wc -l thread-write.log
60 thread-write.log

Tiene sentido limitarse únicamente a la gestión de procesos y a las llamadas al sistema. write:

$ strace -f -e trace="%process,write" -othread-write.log ./thread-write
$ cat thread-write.log
18211 execve("./thread-write", ["./thread-write"], 0x7ffc6b8d58f0 /* 64 vars */) = 0
18211 arch_prctl(ARCH_SET_FS, 0x7f38ea3b7740) = 0
18211 write(1, "Initial thread: launching a thre"..., 35) = 35
18211 clone(child_stack=0x7f38e9ba2fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f38e9ba39d0, tls=0x7f38e9ba3700, child_tidptr=0x7f38e9ba39d0) = 18212
18211 write(1, "Initial thread: joining a thread"..., 33) = 33
18212 write(1, "Secondary thread: workingn", 26) = 26
18212 write(1, "Secondary thread: donen", 23) = 23
18212 exit(0)                           = ?
18212 +++ exited with 0 +++
18211 write(1, "Initial thread: done", 20) = 20
18211 exit_group(0)                     = ?
18211 +++ exited with 0 +++

Por cierto, preguntas. ¿Qué llamada al sistema se utiliza para crear un nuevo hilo? ¿En qué se diferencia esta convocatoria de subprocesos de la convocatoria de procesos?

Clase magistral: pila de procesos en el momento de una llamada al sistema

Uno de los recientemente aparecidos. strace capacidades: muestra la pila de llamadas a funciones en el momento de la llamada al sistema. Simple ejemplo:

void do_write(void)
{
    char str[] = "write me to stdoutn";
    if (sizeof(str) != write(STDOUT_FILENO, str, sizeof(str))){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    do_write();
    return EXIT_SUCCESS;
}

Naturalmente, la salida del programa se vuelve muy voluminosa y, además de la bandera -k (visualización de la pila de llamadas), tiene sentido filtrar las llamadas al sistema por nombre:

$ gcc examples/write-simple.c -o write-simple
$ strace -k -e trace=write -o write-simple.log ./write-simple
write me to stdout
$ cat write-simple.log
write(1, "write me to stdoutn", 20)  = 20
 > /lib/x86_64-linux-gnu/libc-2.27.so(__write+0x14) [0x110154]
 > /home/vkazanov/projects-my/strace-post/write-simple(do_write+0x50) [0x78a]
 > /home/vkazanov/projects-my/strace-post/write-simple(main+0x14) [0x7d1]
 > /lib/x86_64-linux-gnu/libc-2.27.so(__libc_start_main+0xe7) [0x21b97]
 > /home/vkazanov/projects-my/strace-post/write-simple(_start+0x2a) [0x65a]
+++ exited with 0 +++

Clase magistral: inyección de errores

Y una característica más nueva y muy útil: la inyección de errores. Aquí programa, escribiendo dos líneas en el flujo de salida:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void do_write(const char *str, ssize_t len)
{
    if (len != write(STDOUT_FILENO, str, (size_t)len)){
        perror("write");
        exit(EXIT_FAILURE);
    }
}

int main(int argc, char *argv[])
{
    (void) argc; (void) argv;

    char str1[] = "write me 1n";
    do_write(str1, sizeof(str1));

    char str2[] = "write me 2n";
    do_write(str2, sizeof(str2));

    return EXIT_SUCCESS;
}

Rastreemos ambas llamadas de escritura:

$ gcc examples/write-twice.c -o write-twice
$ ./write-twice
write me 1
write me 2
$ strace -e trace=write -owrite-twice.log ./write-twice
write me 1
write me 2
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = 12
+++ exited with 0 +++

Ahora usamos la expresión injectpara insertar un error EBADF en todas las llamadas de escritura:

$ strace -e trace=write -e inject=write:error=EBADF -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = -1 EBADF (Bad file descriptor) (INJECTED)
+++ exited with 1 +++

Es interesante qué errores se devuelven. todos desafíos write, incluida la llamada escondida detrás de perror. Sólo tiene sentido devolver un error para la primera de las llamadas:

$ strace -e trace=write -e inject=write:error=EBADF:when=1 -owrite-twice.log ./write-twice
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

O el segundo:

$ strace -e trace=write -e inject=write:error=EBADF:when=2 -owrite-twice.log ./write-twice
write me 1
write: Bad file descriptor
$ cat write-twice.log
write(1, "write me 1n", 12)          = 12
write(1, "write me 2n", 12)          = -1 EBADF (Bad file descriptor) (INJECTED)
write(3, "write: Bad file descriptorn", 27) = 27
+++ exited with 1 +++

No es necesario especificar el tipo de error:

$ strace -e trace=write -e fault=write:when=1 -owrite-twice.log ./write-twice
$ cat write-twice.log
write(1, "write me 1n", 12)          = -1 ENOSYS (Function not implemented) (INJECTED)
write(3, "write: Function not implementedn", 32) = 32
+++ exited with 1 +++

En combinación con otras banderas, puede "interrumpir" el acceso a un archivo específico. Ejemplo:

$ strace -y -P/tmp/test_file.log -e inject=file:error=ENOENT -o write-file.log ./write-file /tmp/test_file.log
open: No such file or directory
$ cat write-file.log
openat(AT_FDCWD, "/tmp/test_file.log", O_WRONLY|O_APPEND) = -1 ENOENT (No such file or directory) (INJECTED)
+++ exited with 1 +++

Además de la inyección de errores, uno puede introducir retrasos al realizar llamadas o recibir señales.

Epílogo

Utilidad strace - una herramienta sencilla y fiable. Pero además de las llamadas al sistema, se pueden depurar otros aspectos del funcionamiento de los programas y del sistema operativo. Por ejemplo, puede rastrear llamadas a bibliotecas vinculadas dinámicamente. traza, pueden investigar el funcionamiento del sistema operativo. toque del sistema и seguimientoy le permite investigar en profundidad el rendimiento del programa Perf. Sin embargo, es strace - la primera línea de defensa en caso de problemas con mis programas y los de otras personas, y lo uso al menos un par de veces a la semana.

En resumen, si te encanta Unix, lee man 1 strace ¡Y siéntete libre de echar un vistazo a tus programas!

Fuente: habr.com

Añadir un comentario