Strace በሊኑክስ፡ ታሪክ፣ ዲዛይን እና አጠቃቀም

Strace በሊኑክስ፡ ታሪክ፣ ዲዛይን እና አጠቃቀም

በዩኒክስ መሰል ኦፕሬቲንግ ሲስተሞች ውስጥ የፕሮግራሙ ግንኙነት ከውጭው ዓለም እና ከስርዓተ ክወናው ጋር ያለው ግንኙነት በትንሽ የተግባር ስብስብ ነው - የስርዓት ጥሪዎች። ይህ ማለት ለማረም ዓላማዎች በሂደቶች የሚፈጸሙ የስርዓት ጥሪዎችን ለመሰለል ጠቃሚ ሊሆን ይችላል.

መገልገያ በሊኑክስ ላይ ያሉትን የፕሮግራሞችን "የቅርብ ህይወት" ለመከታተል ይረዳዎታል straceየዚህ ጽሑፍ ርዕሰ ጉዳይ የሆነው። የስለላ መሳሪያዎች አጠቃቀም ምሳሌዎች ከአጭር ታሪክ ጋር ተያይዘዋል። strace እና የእንደዚህ አይነት ፕሮግራሞች ንድፍ መግለጫ.

ይዘቶች

የዝርያዎች አመጣጥ

በዩኒክስ ውስጥ በፕሮግራሞች እና በ OS kernel መካከል ያለው ዋና በይነገጽ የስርዓት ጥሪዎች ነው። የስርዓት ጥሪዎች, ሲስካሎች), የፕሮግራሞች መስተጋብር ከውጭው ዓለም ጋር ብቻ ነው የሚከሰተው.

ግን በመጀመሪያው ህዝባዊ የዩኒክስ ስሪት (እ.ኤ.አ.)ስሪት 6 ዩኒክስ, 1975) የተጠቃሚ ሂደቶችን ባህሪ ለመከታተል ምንም ምቹ መንገዶች አልነበሩም. ይህንን ችግር ለመፍታት ቤል ቤተሙከራ ወደሚቀጥለው ስሪት ይዘምናል (ስሪት 7 ዩኒክስ1979) አዲስ የስርዓት ጥሪ አቅርቧል - ptrace.

ptrace በዋነኛነት በይነተገናኝ አራሚዎች የተሰራ ነበር፣ነገር ግን በ80ዎቹ መገባደጃ ላይ (በንግዱ ዘመን የስርዓት V መለቀቅ 4) በዚህ መሠረት በጠባብ ላይ ያተኮሩ አራሚዎች - የስርዓት ጥሪ መከታተያ - ታይተው በሰፊው ጥቅም ላይ ውለዋል.

የመጀመሪያው በ 1992 የተዘጋ አገልግሎትን እንደ አማራጭ በ comp.sources.sun የመልዕክት ዝርዝር ላይ በፖል ክሮንበርግ ታትሟል ። trace ከፀሐይ. ሁለቱም ክሎኑ እና ዋናው የታሰቡት ለ SunOS ነው፣ ግን በ1994 strace ወደ ሲስተም V፣ Solaris እና ከጊዜ ወደ ጊዜ ታዋቂው ሊኑክስ ተላልፏል።

ዛሬ strace ሊኑክስን ብቻ ነው የሚደግፈው እና በተመሳሳይ ላይ ይተማመናል። ptrace, በብዙ ማራዘሚያዎች የተትረፈረፈ.

ዘመናዊ (እና በጣም ንቁ) ጠባቂ strace - ዲሚትሪ ሌቪን. ለእሱ ምስጋና ይግባው ፣ መገልገያው እንደ የስርዓት ጥሪዎች ውስጥ የስህተት መርፌ ፣ ለብዙ የስነ-ህንፃዎች ድጋፍ እና ከሁሉም በላይ ፣ የላቁ ባህሪያትን አግኝቷል። ማስኮት. ኦፊሴላዊ ያልሆኑ ምንጮች ምርጫው በሰጎን ላይ የወደቀው በሩሲያኛ "ሰጎን" እና በእንግሊዘኛ "strace" መካከል ባለው ስምምነት ምክንያት እንደሆነ ይናገራሉ.

በሊኑክስ፣ ፍሪቢኤስዲ፣ ኦፕንቢኤስዲ እና ባህላዊ ዩኒክስ ረጅም ታሪክ እና አተገባበር ቢኖርም የ ptrace ስርዓት ጥሪ እና መከታተያዎች በPOSIX ውስጥ መካተታቸው አስፈላጊ ነው።

የስትራይስ መሳሪያ በአጭሩ፡ Piglet Trace

"ይህን እንድትረዱት አይጠበቅባችሁም" (ዴኒስ ሪቺ፣ በስሪት 6 የዩኒክስ ምንጭ ኮድ አስተያየት)

ከልጅነቴ ጀምሮ, ጥቁር ሳጥኖችን መቋቋም አልችልም: በአሻንጉሊት አልተጫወትኩም, ነገር ግን አወቃቀራቸውን ለመረዳት ሞከርኩ (አዋቂዎች "የተሰበረ" የሚለውን ቃል ይጠቀማሉ, ነገር ግን ክፉ ልሳኖችን አያምኑም). ምናልባትም የመጀመርያው ዩኒክስ መደበኛ ያልሆነ ባህል እና የዘመናዊው ክፍት ምንጭ እንቅስቃሴ ለእኔ ቅርብ የሆነው ለዚህ ነው።

ለዚህ ጽሑፍ ዓላማዎች ለብዙ አሥርተ ዓመታት ያደገውን የዝርፊያ ምንጭ ኮድ መበተን ምክንያታዊ አይደለም. ግን ለአንባቢዎች ምንም ሚስጥሮች ሊኖሩ አይገባም. ስለዚህ የእንደዚህ ዓይነቶቹን የጭረት ፕሮግራሞች አሠራር መርህ ለማሳየት ለአነስተኛ መፈለጊያ ኮዱን አቀርባለሁ - Piglet Trace (ptr) ምንም ልዩ ነገር እንዴት እንደሚሰራ አያውቅም ፣ ግን ዋናው ነገር የፕሮግራሙ የስርዓት ጥሪዎች ነው - እሱ ያወጣል-

$ 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 በመቶዎች የሚቆጠሩ የሊኑክስ ስርዓት ጥሪዎችን ያውቃል (ተመልከት. ሰንጠረዥ) እና በ x86-64 አርክቴክቸር ላይ ብቻ ይሰራል። ይህ ለትምህርት ዓላማዎች በቂ ነው.

የኛን ክሎሎን ስራ እንይ። በሊኑክስ ጉዳይ ላይ አራሚዎች እና ዱካዎች ከላይ እንደተጠቀሰው የፕትሬስ ሲስተም ጥሪን ይጠቀማሉ። የሚሠራው በመጀመሪያው ክርክር ውስጥ የትእዛዝ መለያዎችን በማለፍ ነው, ይህም እኛ ብቻ ያስፈልገናል PTRACE_TRACEME, PTRACE_SYSCALL и PTRACE_GETREGS.

ጠቋሚው በተለመደው የዩኒክስ ዘይቤ ይጀምራል፡- fork(2) የልጅ ሂደትን ይጀምራል, እሱም በተራው ይጠቀማል exec(3) በጥናት ላይ ያለውን ፕሮግራም ይጀምራል. እዚህ ያለው ብቸኛው ረቂቅ ተግዳሮት ነው። ptrace(PTRACE_TRACEME) በፊት execየልጁ ሂደት የወላጅ ሂደት እንዲከታተለው ይጠብቃል፡-

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

የወላጅ ሂደት አሁን መደወል አለበት። wait(2) በልጁ ሂደት ውስጥ ማለትም ወደ መከታተያ ሁነታ መቀየር መከሰቱን ያረጋግጡ፡

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

በዚህ ጊዜ ዝግጅቶቹ ተጠናቀዋል እና ማለቂያ በሌለው ዑደት ውስጥ በቀጥታ ወደ የስርዓት ጥሪዎች መከታተያ መቀጠል ይችላሉ።

ግጥሚያ ptrace(PTRACE_SYSCALL) ከዚያ በኋላ ዋስትና ይሰጣል wait ወላጅ የስርዓት ጥሪው ከመደረጉ በፊት ወይም ወዲያውኑ ከተጠናቀቀ በኋላ ያጠናቅቃል። በሁለት ጥሪዎች መካከል ማንኛውንም ድርጊት ማከናወን ይችላሉ-ጥሪውን በተለዋጭ መተካት, ክርክሮችን ወይም የመመለሻ እሴቱን ይለውጡ.

ትዕዛዙን ሁለት ጊዜ መደወል ብቻ ያስፈልገናል ptrace(PTRACE_GETREGS)የመመዝገቢያ ሁኔታን ለማግኘት rax ከጥሪው በፊት (የስርዓት ጥሪ ቁጥር) እና ወዲያውኑ በኋላ (የመመለሻ እሴት).

በእውነቱ ዑደቱ;

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

ያ ነው ሙሉው መከታተያ። አሁን የሚቀጥለውን ማስተላለፍ የት እንደሚጀመር ያውቃሉ DTrace በሊኑክስ ላይ.

መሰረታዊ: የፕሮግራም ሩጫ strace ማስኬድ

እንደ መጀመሪያ አጠቃቀም ሁኔታ strace, ምናልባት ቀላሉ ዘዴን መጥቀስ ተገቢ ነው - ትግበራ ማስጀመር strace.

ወደ መደበኛው ፕሮግራም ማለቂያ በሌለው የጥሪ ዝርዝር ውስጥ ላለመግባት ፣ እንጽፋለን። ዝቅተኛው ፕሮግራም አካባቢ 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;
}

ፕሮግራሙን እንገንባ እና እንደሚሰራ እናረጋግጥ፡-

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

እና በመጨረሻ፣ በስትሬስ ቁጥጥር ስር እናስኬደው፡-

$ 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)                           = ?

በጣም “ቃላት” እና በጣም ትምህርታዊ አይደለም። እዚህ ሁለት ችግሮች አሉ-የፕሮግራሙ ውፅዓት ከውጤቱ ጋር ተቀላቅሏል strace እና እኛን የማይፈልጉ የተትረፈረፈ የስርዓት ጥሪዎች።

የፕሮግራሙን መደበኛ የውጤት ዥረት እና የስስት ስህተት ውፅዓት በ -o ማብሪያ / ማጥፊያ በመጠቀም መለየት ይችላሉ ፣ ይህም የስርዓት ጥሪዎችን ዝርዝር ወደ ክርክር ፋይል ያዞራል።

የ "ተጨማሪ" ጥሪዎችን ችግር ለመቋቋም ይቀራል. ለጥሪዎች ብቻ ፍላጎት እንዳለን እናስብ write. ቁልፍ -e የስርዓት ጥሪዎች የሚጣሩበትን መግለጫዎች እንዲገልጹ ያስችልዎታል። በጣም ታዋቂው ሁኔታ አማራጭ, በተፈጥሮ, trace=*እኛን የሚስቡን ጥሪዎች ብቻ መተው የሚችሉበት።

በአንድ ጊዜ ጥቅም ላይ ሲውል -o и -e እኛ እናገኛለን:

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

ስለዚህ፣ አየህ፣ ለማንበብ በጣም ቀላል ነው።

እንዲሁም የስርዓት ጥሪዎችን ማስወገድ ይችላሉ፣ ለምሳሌ ከማህደረ ትውስታ ምደባ እና ነጻ ማድረግ ጋር የተያያዙ፡

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

በተገለሉ ጥሪዎች ዝርዝር ውስጥ ያመለጠውን የቃለ አጋኖ ምልክት ልብ ይበሉ፡ ይህ በትእዛዝ ሼል ያስፈልጋል። ቀለህ).

በእኔ የglibc ስሪት ውስጥ የስርዓት ጥሪ ሂደቱን ያቋርጣል exit_group, ባህላዊ አይደለም _exit. ይህ ከስርዓት ጥሪዎች ጋር የመሥራት ችግር ነው፡ ፕሮግራመር የሚሰራበት በይነገጽ ከስርዓት ጥሪዎች ጋር በቀጥታ የተገናኘ አይደለም። ከዚህም በላይ በአፈፃፀሙ እና በመድረክ ላይ በመመስረት በየጊዜው ይለወጣል.

መሰረታዊ: ሂደቱን በበረራ ላይ መቀላቀል

መጀመሪያ ላይ, የተገነባበት የፕላስ ሲስተም ጥሪ strace, ፕሮግራሙን በልዩ ሁነታ ሲሰራ ብቻ መጠቀም ይቻላል. ይህ ገደብ በስሪት 6 ዩኒክስ ዘመን ምክንያታዊ መስሎ ሊሆን ይችላል። በአሁኑ ጊዜ ይህ በቂ አይደለም: አንዳንድ ጊዜ የሥራ ፕሮግራም ችግሮችን መመርመር ያስፈልግዎታል. የተለመደው ምሳሌ በእጀታ ወይም በእንቅልፍ ላይ የታገደ ሂደት ነው። ስለዚህ ዘመናዊ strace በበረራ ላይ ሂደቶችን መቀላቀል ይችላል.

የማቀዝቀዝ ምሳሌ ፕሮግራሞች:

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

ፕሮግራሙን እንገንባ እና የቀዘቀዘ መሆኑን እናረጋግጥ፡-

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

አሁን እሱን ለመቀላቀል እንሞክር፡-

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

ፕሮግራም በጥሪ ታግዷል pause. ለምልክቶቹ እንዴት ምላሽ እንደምትሰጥ እንይ፡-

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

የቀዘቀዘውን ፕሮግራም አስጀመርን እና ተጠቅመን ተቀላቅለናል። strace. ሁለት ነገሮች ግልጽ ሆኑ፡ ለአፍታ ማቆም የስርዓት ጥሪ ያለ ተቆጣጣሪዎች ምልክቶችን ችላ ይላል እና ከሁሉም በላይ ደግሞ የስርዓት ጥሪዎችን ብቻ ሳይሆን ገቢ ምልክቶችንም ይቆጣጠራል።

ምሳሌ፡ የልጅ ሂደቶችን መከታተል

ከሂደቶች ጋር በጥሪ መስራት fork - የሁሉም ዩኒክስ መሠረት። የቀላል “እርባታ” ምሳሌን በመጠቀም ክር ከሂደት ዛፍ ጋር እንዴት እንደሚሰራ እንመልከት ። ፕሮግራሞች:

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

እዚህ ዋናው ሂደት የልጅ ሂደትን ይፈጥራል፣ ሁለቱም ወደ መደበኛ ውፅዓት ይፃፉ።

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

በነባሪ የስርዓት ጥሪዎችን ከወላጅ ሂደት ብቻ ነው የምናየው፡-

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

ባንዲራ ሙሉውን የሂደቱን ዛፍ ለመከታተል ይረዳዎታል -f፣ የትኛው strace በልጆች ሂደቶች ውስጥ የስርዓት ጥሪዎችን ይቆጣጠራል. ይህ በእያንዳንዱ የውጤት መስመር ላይ ይጨምራል pid የስርዓት ውፅዓት ሂደት;

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

በዚህ አውድ፣ በስርዓት ጥሪዎች ቡድን ማጣራት ጠቃሚ ሊሆን ይችላል፡-

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

በነገራችን ላይ አዲስ ሂደት ለመፍጠር ምን ዓይነት የስርዓት ጥሪ ጥቅም ላይ ይውላል?

ምሳሌ፡ ከመያዣዎች ይልቅ ዱካዎችን ፋይል ያድርጉ

የፋይል ገላጭዎችን ማወቅ በእርግጥ ጠቃሚ ነው, ነገር ግን አንድ ፕሮግራም የሚደርስባቸው የተወሰኑ ፋይሎች ስሞችም ጠቃሚ ሊሆኑ ይችላሉ.

ቀጣይ ፕሮግራሙ መስመሩን ወደ ጊዜያዊው ፋይል ይጽፋል፡-

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

በመደበኛ ጥሪ ወቅት strace ወደ ስርዓቱ ጥሪ የተላለፈውን ገላጭ ቁጥር ዋጋ ያሳያል፡-

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

ከባንዲራ ጋር -y መገልገያው ገላጭ ወደሚገኝበት ፋይል የሚወስደውን መንገድ ያሳያል፡-

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

ምሳሌ፡ የፋይል መዳረሻ መከታተያ

ሌላ ጠቃሚ ባህሪ፡ ከአንድ የተወሰነ ፋይል ጋር የተጎዳኙ የስርዓት ጥሪዎችን ብቻ አሳይ። ቀጥሎ ፕሮግራሙ መስመርን እንደ ክርክር ከተላለፈ የዘፈቀደ ፋይል ጋር አያይዘዋል፡-

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

በነባሪ strace ብዙ አላስፈላጊ መረጃዎችን ያሳያል። ባንዲራ -P ከክርክር ጋር ወደተገለጸው ፋይል የሚደረጉ ጥሪዎች ብቻ እንዲታተም ድርድር ያስከትላል፡-

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

ምሳሌ፡- ባለብዙ ክርድድ ፕሮግራሞች

መገልገያ strace ከበርካታ ክር ጋር ሲሰራ ሊረዳ ይችላል ፕሮግራሙ. የሚከተለው ፕሮግራም ከሁለት ዥረቶች ወደ መደበኛ ውፅዓት ይጽፋል፡

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

በተፈጥሮ፣ ለአገናኛው ልዩ ሰላምታ --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
$

ሰንደቅ -f, እንደ መደበኛ ሂደቶች, በእያንዳንዱ መስመር መጀመሪያ ላይ የሂደቱን ፒድ ይጨምራል.

በተፈጥሮ እኛ የምንናገረው በ POSIX Threads መስፈርት አተገባበር ስሜት ውስጥ ስለ ክር መለያ አይደለም ፣ ግን በሊኑክስ ውስጥ ባለው የተግባር መርሐግብር ስለተጠቀመው ቁጥር ነው። ከኋለኛው አንፃር ፣ ምንም ሂደቶች ወይም ክሮች የሉም - በማሽኑ ውስጥ በሚገኙት ማዕከሎች መካከል መሰራጨት የሚያስፈልጋቸው ተግባራት አሉ።

በበርካታ ክሮች ውስጥ ሲሰሩ የስርዓት ጥሪዎች በጣም ብዙ ይሆናሉ፡

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

በሂደት አስተዳደር እና በስርዓት ጥሪዎች ብቻ እራስዎን መገደብ ምክንያታዊ ነው። 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 +++

በነገራችን ላይ ጥያቄዎች. አዲስ ክር ለመፍጠር ምን የስርዓት ጥሪ ጥቅም ላይ ይውላል? ይህ የክሮች ጥሪ ከሂደቶች ጥሪ እንዴት ይለያል?

ማስተር ክፍል፡ በስርዓት ጥሪ ጊዜ የሂደት ቁልል

አንዱ በቅርቡ ታየ strace ችሎታዎች - በስርዓቱ ጥሪ ጊዜ የተግባር ጥሪዎችን ቁልል ማሳየት. ቀላል ምሳሌ:

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

በተፈጥሮ ፣ የፕሮግራሙ ውፅዓት በጣም ብዙ ይሆናል ፣ እና ከባንዲራ በተጨማሪ -k (የጥሪ ቁልል ማሳያ)፣ የስርዓት ጥሪዎችን በስም ማጣራቱ ጠቃሚ ነው፡-

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

ማስተር ክፍል፡ የስህተት መርፌ

እና አንድ ተጨማሪ አዲስ እና በጣም ጠቃሚ ባህሪ: የስህተት መርፌ. እዚህ ፕሮግራሙሁለት መስመሮችን ወደ የውጤት ዥረቱ በመጻፍ፡-

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

ሁለቱንም ጥሪዎች እንጽፋለን፡-

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

አሁን አገላለጹን እንጠቀማለን injectስህተት ለማስገባት EBADF በሁሉም ጥሪዎች ይጻፉ:

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

ስህተቶቹ የሚመለሱት ነገር ትኩረት የሚስብ ነው። ሁሉም ችግሮች writeከሽብር ጀርባ የተደበቀውን ጥሪ ጨምሮ። ለመጀመሪያዎቹ ጥሪዎች ስህተት መመለስ ብቻ ምክንያታዊ ነው፡-

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

ወይም ሁለተኛው:

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

የስህተት አይነትን መግለጽ አስፈላጊ አይደለም:

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

ከሌሎች ባንዲራዎች ጋር በማጣመር የአንድ የተወሰነ ፋይል መዳረሻን "መስበር" ይችላሉ። ለምሳሌ:

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

ከስህተት መርፌ በተጨማሪ ፣ ይችላል ጥሪዎችን ሲያደርጉ ወይም ምልክቶችን ሲቀበሉ መዘግየቶችን ያስተዋውቁ።

ከቃል በኋላ

መገልገያ strace - ቀላል እና አስተማማኝ መሳሪያ. ነገር ግን ከስርዓት ጥሪዎች በተጨማሪ የፕሮግራሞች እና የስርዓተ ክወናው ሌሎች ገጽታዎች ማረም ይቻላል. ለምሳሌ፣ ወደ ተለዋዋጭ የተገናኙ ቤተ-መጻሕፍት ጥሪዎችን መከታተል ይችላል። ltrace, የስርዓተ ክወናውን አሠራር መመልከት ይችላሉ SystemTap и ስንጥቅ, እና የፕሮግራሙን አፈፃፀም በጥልቀት ለመመርመር ያስችልዎታል ፐርፍ. ቢሆንም ግን ነው። strace - በራሴ እና በሌሎች ሰዎች ፕሮግራሞች ላይ ችግር ሲፈጠር የመጀመሪያው የመከላከያ መስመር, እና ቢያንስ በሳምንት ሁለት ጊዜ እጠቀማለሁ.

ባጭሩ ዩኒክስን የምትወድ ከሆነ አንብብ man 1 strace እና ፕሮግራሞችዎን ለማየት ነፃነት ይሰማዎ!

ምንጭ: hab.com

አስተያየት ያክሉ