Strace în Linux: istorie, design și utilizare

Strace în Linux: istorie, design și utilizare

În sistemele de operare asemănătoare Unix, comunicarea unui program cu lumea exterioară și sistemul de operare are loc printr-un set mic de funcții - apeluri de sistem. Aceasta înseamnă că, în scopuri de depanare, poate fi util să spionați apelurile de sistem executate de procese.

Un utilitar vă ajută să monitorizați „viața intimă” a programelor pe Linux strace, care face obiectul acestui articol. Exemplele de utilizare a echipamentelor de spionaj sunt însoțite de un scurt istoric strace și o descriere a designului unor astfel de programe.

Conținut

Originea speciilor

Interfața principală dintre programe și nucleul sistemului de operare în Unix este apelurile de sistem. apeluri de sistem, syscall-uri), interacțiunea programelor cu lumea exterioară are loc exclusiv prin intermediul acestora.

Dar în prima versiune publică a Unix (Versiunea 6 Unix, 1975) nu existau căi convenabile de a urmări comportamentul proceselor utilizatorului. Pentru a rezolva această problemă, Bell Labs va actualiza la următoarea versiune (Versiunea 7 Unix, 1979) a propus un nou apel de sistem - ptrace.

ptrace a fost dezvoltat în primul rând pentru depanatoare interactive, dar până la sfârșitul anilor 80 (în era comercială System V Versiunea 4) pe această bază, au apărut și au devenit utilizate pe scară largă aplicații de depanare concentrate îngust — urmăritori de apeluri de sistem.

în primul rând aceeași versiune a strace a fost publicată de Paul Cronenburg pe lista de corespondență comp.sources.sun în 1992, ca alternativă la utilitatea închisă trace de la Soare. Atât clona, ​​cât și originalul au fost destinate SunOS, dar până în 1994 strace a fost portat pe System V, Solaris și Linux din ce în ce mai popular.

Astăzi, strace acceptă doar Linux și se bazează pe același ptrace, copleșit cu multe extensii.

Menținător modern (și foarte activ). strace - Dmitri Levin. Datorită lui, utilitarul a dobândit funcții avansate, cum ar fi injectarea de erori în apelurile de sistem, suport pentru o gamă largă de arhitecturi și, cel mai important, mascotă. Surse neoficiale susțin că alegerea a căzut asupra struțului din cauza consonanței dintre cuvântul rus „struț” și cuvântul englezesc „strace”.

De asemenea, este important ca apelul de sistem ptrace și trasoarele nu au fost niciodată incluse în POSIX, în ciuda unei istorii lungi și a implementării în Linux, FreeBSD, OpenBSD și Unix tradițional.

Dispozitivul Strace pe scurt: Piglet Trace

„Nu trebuie să înțelegeți asta” (Dennis Ritchie, comentariu în codul sursă Unix Versiunea 6)

Încă din copilărie, nu suport cutiile negre: nu m-am jucat cu jucăriile, ci am încercat să le înțeleg structura (adulții au folosit cuvântul „spărut”, dar nu cred în limbile rele). Poate de aceea cultura informală a primului Unix și mișcarea open-source modernă este atât de aproape de mine.

În sensul acestui articol, este nerezonabil să dezasamblați codul sursă al strace, care a crescut de-a lungul deceniilor. Dar nu ar trebui să rămână secrete pentru cititori. Prin urmare, pentru a arăta principiul de funcționare a unor astfel de programe strace, voi furniza codul pentru un trasor miniatural - Urmă de purcel (ptr). Nu știe să facă nimic special, dar principalul lucru este apelurile de sistem ale programului - iese:

$ 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 recunoaște aproximativ sute de apeluri de sistem Linux (vezi. tabel) și funcționează numai pe arhitectura x86-64. Acest lucru este suficient pentru scopuri educaționale.

Să ne uităm la munca clonei noastre. În cazul Linux, depanatorii și trasoarele folosesc, așa cum sa menționat mai sus, apelul de sistem ptrace. Funcționează trecând în primul argument identificatorii de comandă, de care avem nevoie doar PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Tracerul începe în stilul obișnuit Unix: fork(2) lansează un proces copil, care la rândul său utilizează exec(3) lansează programul în studiu. Singura subtilitate aici este provocarea ptrace(PTRACE_TRACEME) înainte exec: Procesul copil se așteaptă ca procesul părinte să îl monitorizeze:

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");
}

Procesul părinte ar trebui să sune acum wait(2) în procesul copil, adică asigurați-vă că a avut loc trecerea la modul de urmărire:

/* 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");

În acest moment, pregătirile sunt finalizate și puteți trece direct la urmărirea apelurilor de sistem într-o buclă nesfârșită.

apel ptrace(PTRACE_SYSCALL) garantează că ulterior wait părinte va finaliza fie înainte ca apelul de sistem să fie executat, fie imediat după finalizarea acestuia. Între două apeluri puteți efectua orice acțiune: înlocuiți apelul cu unul alternativ, schimbați argumentele sau valoarea returnată.

Trebuie doar să apelăm comanda de două ori ptrace(PTRACE_GETREGS)pentru a obține starea de registru rax înainte de apel (număr de apel de sistem) și imediat după (valoare de returnare).

De fapt, ciclul:

/* 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);
}

Acesta este întregul trasor. Acum știi de unde să începi următoarea portare DTrace pe Linux.

Elemente de bază: rularea unui program care rulează strace

Ca prim caz de utilizare strace, poate că merită citată cea mai simplă metodă - lansarea unei aplicații care rulează strace.

Pentru a nu pătrunde în lista nesfârșită de apeluri a unui program tipic, scriem program minim вокруг 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;
}

Să construim programul și să ne asigurăm că funcționează:

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

Și, în sfârșit, să-l rulăm sub control 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)                           = ?

Foarte „probă” și nu foarte educațional. Există două probleme aici: ieșirea programului este amestecată cu ieșirea strace și o abundență de apeluri de sistem care nu ne interesează.

Puteți separa fluxul de ieșire standard al programului și ieșirea erorii de strace folosind comutatorul -o, care redirecționează lista de apeluri de sistem către un fișier argument.

Rămâne să ne ocupăm de problema apelurilor „în plus”. Să presupunem că ne interesează doar apelurile write. Cheie -e vă permite să specificați expresii după care apelurile de sistem vor fi filtrate. Cea mai populară opțiune de afecțiune este, desigur, trace=*, cu care poți lăsa doar apelurile care ne interesează.

Când se utilizează simultan -o и -e vom obține:

$ 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 +++

Deci, vezi tu, este mult mai ușor de citit.

De asemenea, puteți elimina apelurile de sistem, de exemplu cele legate de alocarea și eliberarea memoriei:

$ 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 +++

Notați semnul exclamării escape din lista apelurilor excluse: acest lucru este cerut de shell-ul de comandă. coajă).

În versiunea mea de glibc, un apel de sistem încheie procesul exit_group, nu tradițional _exit. Aceasta este dificultatea de a lucra cu apeluri de sistem: interfața cu care lucrează programatorul nu este direct legată de apelurile de sistem. Mai mult, se schimbă regulat în funcție de implementare și platformă.

Elemente de bază: alăturarea procesului din mers

Inițial, apelul de sistem ptrace pe care a fost construit strace, ar putea fi folosit numai atunci când rulați programul într-un mod special. Este posibil ca această limitare să fi sunat rezonabil în zilele versiunii 6 Unix. În zilele noastre, acest lucru nu mai este suficient: uneori trebuie să investigați problemele unui program de lucru. Un exemplu tipic este un proces blocat pe un mâner sau dormit. Prin urmare modern strace se poate alătura proceselor din mers.

Exemplu de congelare programe:

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;
}

Să construim programul și să ne asigurăm că este înghețat:

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

Acum să încercăm să ne alăturăm:

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

Program blocat prin apel pause. Să vedem cum reacționează ea la semnale:

$ 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 +++

Am lansat programul înghețat și ne-am alăturat utilizând strace. Două lucruri au devenit clare: apelul sistem de pauză ignoră semnalele fără handler și, mai interesant, strace monitorizează nu numai apelurile de sistem, ci și semnalele primite.

Exemplu: Urmărirea proceselor secundare

Lucrul cu procese printr-un apel fork - baza tuturor Unix-urilor. Să vedem cum funcționează strace cu un arbore de proces folosind exemplul unei „cresări” simple programe:

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);
}

Aici, procesul inițial creează un proces copil, ambele scriind la ieșirea standard:

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

În mod implicit, vom vedea numai apelurile de sistem de la procesul părinte:

$ 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 +++

Steagul vă ajută să urmăriți întregul arbore de proces -f, care strace monitorizează apelurile de sistem în procesele copil. Acest lucru se adaugă la fiecare linie de ieșire pid proces care face ieșirea unui sistem:

$ 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 +++

În acest context, filtrarea după grup de apeluri de sistem poate fi utilă:

$ 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 +++

Apropo, ce apel de sistem este folosit pentru a crea un nou proces?

Exemplu: căi de fișiere în loc de mânere

Cunoașterea descriptorilor de fișiere este cu siguranță utilă, dar numele fișierelor specifice pe care le accesează un program pot fi, de asemenea, utile.

următoare program scrie linia în fișierul temporar:

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;
}

În timpul unui apel normal strace va afișa valoarea numărului de descriptor transmis apelului de sistem:

$ 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 +++

Cu un steag -y Utilitarul arată calea către fișierul căruia îi corespunde descriptorul:

$ 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 +++

Exemplu: Urmărirea accesului la fișiere

O altă caracteristică utilă: afișați numai apelurile de sistem asociate cu un anumit fișier. Următorul program adaugă o linie la un fișier arbitrar transmis ca argument:

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;
}

În mod implicit strace afișează o mulțime de informații inutile. Steag -P cu un argument determină ca strace să imprime numai apelurile către fișierul specificat:

$ 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 +++

Exemplu: Programe cu mai multe fire

Utilitate strace poate ajuta, de asemenea, atunci când lucrați cu mai multe fire programul. Următorul program scrie la ieșirea standard din două fluxuri:

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);
}

Desigur, trebuie să fie compilat cu un salut special pentru linker - steag-ul -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
$

pavilion -f, ca și în cazul proceselor obișnuite, va adăuga pid-ul procesului la începutul fiecărei linii.

Desigur, nu vorbim despre un identificator de thread în sensul implementării standardului POSIX Threads, ci despre numărul folosit de task scheduler în Linux. Din punctul de vedere al acestuia din urmă, nu există procese sau fire - există sarcini care trebuie distribuite între nucleele disponibile ale mașinii.

Când lucrați în mai multe fire, apelurile de sistem devin prea multe:

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

Este logic să vă limitați doar la gestionarea proceselor și la apelurile de sistem 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 +++

Apropo, întrebări. Ce apel de sistem este folosit pentru a crea un fir nou? Cum diferă acest apel pentru fire de execuție de apelul pentru procese?

Master class: stiva de procese în momentul unui apel de sistem

Una dintre cele apărute recent strace capabilități - afișarea stivei de apeluri de funcții în momentul apelului de sistem. Simplu exemplu:

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;
}

Desigur, ieșirea programului devine foarte voluminoasă și, pe lângă steag -k (afișare stivă de apeluri), este logic să filtrați apelurile de sistem după nume:

$ 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 +++

Clasa de master: injectare de eroare

Și încă o caracteristică nouă și foarte utilă: injectarea de erori. Aici program, scriind două linii în fluxul de ieșire:

#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;
}

Să urmărim ambele apeluri de scriere:

$ 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 +++

Acum folosim expresia injectpentru a introduce o eroare EBADF în toate apelurile de scriere:

$ 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 +++

Este interesant ce erori sunt returnate toate provocări write, inclusiv apelul ascuns după eroare. Are sens doar să returnați o eroare pentru primul dintre apeluri:

$ 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 +++

Sau al doilea:

$ 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 +++

Nu este necesar să specificați tipul de eroare:

$ 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 +++

În combinație cu alte steaguri, puteți „întrerupe” accesul la un anumit fișier. Exemplu:

$ 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 +++

Pe lângă injecția de eroare, Se poate introduceți întârzieri la efectuarea apelurilor sau la primirea semnalelor.

postfață

Utilitate strace - un instrument simplu și de încredere. Dar, pe lângă apelurile de sistem, pot fi depanate și alte aspecte ale funcționării programelor și ale sistemului de operare. De exemplu, poate urmări apelurile către biblioteci conectate dinamic. ltrace, ei pot analiza funcționarea sistemului de operare SystemTap и ftrace, și vă permite să investigați în profunzime performanța programului perfect. Cu toate acestea, este strace - prima linie de apărare în caz de probleme cu programele mele și ale altora și o folosesc de cel puțin câteva ori pe săptămână.

Pe scurt, dacă îți place Unix, citește man 1 strace și nu ezitați să aruncați o privire la programele dvs.!

Sursa: www.habr.com

Adauga un comentariu