Strace v Linuxu: zgodovina, načrtovanje in uporaba

Strace v Linuxu: zgodovina, načrtovanje in uporaba

V operacijskih sistemih, podobnih Unixu, komunikacija programa z zunanjim svetom in operacijskim sistemom poteka prek majhnega nabora funkcij – sistemskih klicev. To pomeni, da je za namene odpravljanja napak lahko koristno vohuniti za sistemskimi klici, ki jih izvajajo procesi.

Pripomoček vam pomaga spremljati "intimno življenje" programov v Linuxu strace, ki je predmet tega članka. Primere uporabe vohunske opreme spremlja kratka zgodovina strace in opis zasnove takih programov.

Vsebina

Izvor vrste

Glavni vmesnik med programi in jedrom OS v Unixu so sistemski klici. sistemske klice, sistemske klice), interakcija programov z zunanjim svetom poteka izključno prek njih.

Toda v prvi javni različici Unixa (Različica 6 Unix, 1975) ni bilo priročnih načinov za sledenje obnašanju uporabniških procesov. Za rešitev te težave bo Bell Labs posodobil na naslednjo različico (Različica 7 Unix, 1979) je predlagal nov sistemski klic - ptrace.

ptrace je bil razvit predvsem za interaktivne razhroščevalnike, vendar do konca 80. let (v dobi komercialnega System V, izdaja 4) na tej podlagi so se pojavili ozko usmerjeni razhroščevalniki - sledilci sistemskih klicev - in postali široko uporabljani.

Prvič isto različico strace je leta 1992 objavil Paul Cronenburg na poštnem seznamu comp.sources.sun kot alternativo zaprtemu pripomočku trace od Sun. Tako klon kot izvirnik sta bila namenjena za SunOS, vendar do leta 1994 strace je bil prenesen v System V, Solaris in vedno bolj priljubljen Linux.

Danes strace podpira samo Linux in se zanaša nanj ptrace, poraščen s številnimi prizidki.

Sodoben (in zelo aktiven) vzdrževalec strace - Dmitrij Levin. Po njegovi zaslugi je pripomoček pridobil napredne funkcije, kot so vstavljanje napak v sistemske klice, podpora za široko paleto arhitektur in, kar je najpomembnejše, maskota. Neuradni viri trdijo, da je izbira padla na noja zaradi sozvočja med rusko besedo ostrich in angleško besedo strace.

Pomembno je tudi, da sistemski klic ptrace in sledilniki niso bili nikoli vključeni v POSIX, kljub dolgi zgodovini in izvajanju v Linuxu, FreeBSD, OpenBSD in tradicionalnem Unixu.

Naprava Strace na kratko: Piglet Trace

"Od tebe se ne pričakuje, da boš to razumel" (Dennis Ritchie, komentar v izvorni kodi Unixa različice 6)

Od zgodnjega otroštva ne prenesem črnih skrinjic: nisem se igral z igračami, ampak sem poskušal razumeti njihovo strukturo (odrasli so uporabili besedo "zlomil", vendar ne verjemite zlobnim jezikom). Morda mi je zato tako blizu neformalna kultura prvega Unixa in moderno odprtokodno gibanje.

Za namene tega članka je nesmiselno razstaviti izvorno kodo strace, ki je rasla skozi desetletja. A za bralce ne sme ostati skrivnosti. Zato bom za prikaz načela delovanja takšnih programov strace zagotovil kodo za miniaturni sledilnik - Sled prašička (ptr). Ne ve, kako narediti nič posebnega, glavna stvar pa so sistemski klici programa - izpiše:

$ 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 prepozna približno stotine sistemskih klicev Linuxa (glejte. mizo) in deluje samo na arhitekturi x86-64. To zadostuje za izobraževalne namene.

Poglejmo delo našega klona. V primeru Linuxa razhroščevalniki in sledilci uporabljajo, kot je omenjeno zgoraj, sistemski klic ptrace. Deluje tako, da v prvem argumentu posreduje identifikatorje ukazov, ki jih potrebujemo samo PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

Sledilnik se zažene v običajnem slogu Unixa: fork(2) zažene podrejeni proces, ki nato uporabi exec(3) zažene preučevani program. Edina subtilnost tukaj je izziv ptrace(PTRACE_TRACEME) pred exec: Podrejeni proces pričakuje, da ga bo nadrejeni proces spremljal:

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

Nadrejeni proces bi moral zdaj poklicati wait(2) v podrejenem procesu, torej se prepričajte, da je prišlo do preklopa v način sledenja:

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

Na tej točki so priprave končane in lahko nadaljujete neposredno s sledenjem sistemskih klicev v neskončni zanki.

Izziv ptrace(PTRACE_SYSCALL) zagotavlja, da naknadno wait nadrejeni bo dokončal pred izvedbo sistemskega klica ali takoj po njegovem zaključku. Med dvema klicema lahko izvajate poljubna dejanja: zamenjate klic z alternativnim, spremenite argumente ali vrnjeno vrednost.

Samo dvakrat moramo poklicati ukaz ptrace(PTRACE_GETREGS)pridobiti stanje registra rax pred klicem (številka sistemskega klica) in takoj za njim (vrnjena vrednost).

Pravzaprav cikel:

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

To je celoten sledilnik. Zdaj veste, kje začeti z naslednjim prenosom DTrace v sistemu Linux.

Osnove: izvajanje programa, ki izvaja strace

Kot prvi primer uporabe strace, morda je vredno navesti najpreprostejšo metodo - zagon zagnane aplikacije strace.

Da se ne poglobimo v neskončen seznam klicev tipičnega programa, pišemo minimalni program okoli 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;
}

Sestavimo program in poskrbimo, da deluje:

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

In končno, zaženimo ga pod nadzorom 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)                           = ?

Zelo "besedno" in premalo poučno. Tukaj sta dve težavi: izhod programa je pomešan z izhodom strace in obilico sistemskih klicev, ki nas ne zanimajo.

Standardni izhodni tok programa in izhod napak strace lahko ločite s stikalom -o, ki preusmeri seznam sistemskih klicev v datoteko argumentov.

Ostaja še reševanje problema "dodatnih" klicev. Predpostavimo, da nas zanimajo le klici write. Ključ -e vam omogoča, da določite izraze, po katerih bodo filtrirani sistemski klici. Najbolj priljubljena možnost stanja je seveda trace=*, s katerim lahko pustite samo klice, ki nas zanimajo.

Pri sočasni uporabi -o и -e dobili bomo:

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

Torej, vidite, je veliko lažje brati.

Odstranite lahko tudi sistemske klice, na primer tiste, povezane z dodeljevanjem in sproščanjem pomnilnika:

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

Upoštevajte ubežni klicaj na seznamu izključenih klicev: to zahteva ukazna lupina. shell).

V moji različici glibc sistemski klic prekine proces exit_group, ne tradicionalno _exit. To je težava pri delu s sistemskimi klici: vmesnik, s katerim dela programer, ni neposredno povezan s sistemskimi klici. Poleg tega se redno spreminja glede na izvedbo in platformo.

Osnove: vključitev v proces sproti

Sprva sistemski klic ptrace, na katerem je bil zgrajen strace, je bilo mogoče uporabiti samo, ko se program izvaja v posebnem načinu. Ta omejitev je morda zvenela razumno v dneh Unixa različice 6. Dandanes to ni več dovolj: včasih morate raziskati težave delujočega programa. Tipičen primer je proces, blokiran na ročaju ali v stanju mirovanja. Zato moderno strace se lahko sproti pridruži procesom.

Primer zamrzovanja programi:

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

Sestavimo program in poskrbimo, da je zamrznjen:

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

Zdaj pa se mu poskusimo pridružiti:

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

Program blokiran s klicem pause. Poglejmo, kako se odziva na signale:

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

Zagnali smo zamrznjeni program in se mu pridružili z uporabo strace. Dve stvari sta postali jasni: sistemski klic za zaustavitev ignorira signale brez obdelovalcev in, kar je še bolj zanimivo, strace spremlja ne samo sistemske klice, ampak tudi dohodne signale.

Primer: Sledenje podrejenim procesom

Delo s procesi prek klica fork - osnova vseh Unixov. Oglejmo si, kako strace deluje s procesnim drevesom na primeru preprostega »vzrejanja« programi:

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

Tu izvirni proces ustvari podrejeni proces, oba pišeta v standardni izhod:

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

Privzeto bomo videli samo sistemske klice nadrejenega procesa:

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

Zastavica vam pomaga slediti celotnemu drevesu procesa -f, ki strace spremlja sistemske klice v podrejenih procesih. To dodaja vsaki vrstici izhoda pid proces, ki naredi sistemski izhod:

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

V tem kontekstu je lahko koristno filtriranje po skupini sistemskih klicev:

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

Mimogrede, kateri sistemski klic se uporabi za ustvarjanje novega procesa?

Primer: poti datotek namesto ročajev

Poznavanje deskriptorjev datotek je vsekakor koristno, vendar lahko pridejo prav tudi imena določenih datotek, do katerih program dostopa.

naslednji Program zapiše vrstico v začasno datoteko:

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

Med običajnim klicem strace bo prikazal vrednost številke deskriptorja, posredovane sistemskemu klicu:

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

Z zastavo -y Pripomoček prikaže pot do datoteke, ki ji ustreza deskriptor:

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

Primer: Sledenje dostopu do datoteke

Druga uporabna funkcija: prikaz samo sistemskih klicev, povezanih z določeno datoteko. Naslednji Program doda vrstico poljubni datoteki, posredovani kot 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;
}

Privzeto strace prikazuje veliko nepotrebnih informacij. Zastava -P z argumentom povzroči, da strace natisne samo klice podane datoteke:

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

Primer: Večnitni programi

Uporabnost strace lahko pomaga tudi pri delu z večnitnimi program. Naslednji program piše v standardni izhod iz dveh tokov:

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

Seveda ga je treba prevesti s posebnim pozdravom povezovalcu - zastavico -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
$

Zastava -f, tako kot v primeru običajnih procesov, doda pid procesa na začetek vsake vrstice.

Seveda ne govorimo o identifikatorju niti v smislu implementacije standarda POSIX Threads, ampak o številki, ki jo uporablja razporejevalnik opravil v Linuxu. Z vidika slednjega ni procesov ali niti - obstajajo naloge, ki jih je treba porazdeliti med razpoložljiva jedra stroja.

Pri delu v več nitih postane sistemskih klicev preveč:

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

Smiselno se je omejiti samo na upravljanje procesov in sistemske klice 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 +++

Mimogrede, vprašanja. Kateri sistemski klic se uporabi za ustvarjanje nove niti? Kako se ta klic niti razlikuje od klica procesov?

Mojstrski razred: procesni sklad v času sistemskega klica

Eden od nedavno se je pojavil strace zmožnosti - prikaz sklada funkcijskih klicev v času sistemskega klica. Enostavno Primer:

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

Seveda postane rezultat programa zelo obsežen in poleg zastavice -k (prikaz sklada klicev), je smiselno sistemske klice filtrirati po imenu:

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

Mojstrski razred: vbrizgavanje napak

In še ena nova in zelo uporabna funkcija: vstavljanje napak. Tukaj Program, zapiše dve vrstici v izhodni tok:

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

Izsledimo oba pisalna klica:

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

Zdaj uporabljamo izraz injectda vnesete napako EBADF v vseh pisnih klicih:

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

Zanimivo, kakšne napake se vračajo Vsi izzivi write, vključno s klicem, skritim za napako. Napako je smiselno vrniti le pri prvem od klicev:

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

Ali drugo:

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

Vrsta napake ni potrebna:

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

V kombinaciji z drugimi zastavicami lahko "prekinete" dostop do določene datoteke. primer:

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

Poleg vbrizgavanja napak, eno lahko povzroča zakasnitve pri klicanju ali sprejemanju signalov.

spremna beseda

Uporabnost strace - preprosto in zanesljivo orodje. Toda poleg sistemskih klicev je mogoče odpraviti napake tudi v drugih vidikih delovanja programov in operacijskega sistema. Na primer, lahko sledi klicem v dinamično povezane knjižnice. ltrace, si lahko ogledajo delovanje operacijskega sistema SystemTap и ftrace, in vam omogoča, da poglobljeno raziščete delovanje programa perf. Kljub temu je strace - prva obrambna linija v primeru težav z lastnimi in tujimi programi in ga uporabljam vsaj nekajkrat na teden.

Skratka, če imate radi Unix, berite man 1 strace in vas prosimo, da pokukate v svoje programe!

Vir: www.habr.com

Dodaj komentar