Почему многие системные вызовы (getpid) перехватываются только один раз с помощью strace?

Я много раз вызывал getpid() в программе (для проверки эффективности системных вызовов), однако, когда я использую strace для получения трассировки, только один getpid() звонок перехвачен.

Код прост:

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

void print_usage(){
    printf("Usage: program count\n");
    exit(-1);
}

int main(int argc, char** argv){
    if(argc != 2)
        print_usage();
    int cnt = atoi(argv[1]);
    int i = 0;
    while(i++<cnt)
        getpid();
    return 0;
}

Я использовал gdb и получил это:

(gdb) disasse
Dump of assembler code for function getpid:
0xb76faac0 <getpid+0>:  mov    %gs:0x4c,%edx
0xb76faac7 <getpid+7>:  cmp    $0x0,%edx
0xb76faaca <getpid+10>: mov    %edx,%eax
0xb76faacc <getpid+12>: jle    0xb76faad0 <getpid+16>
0xb76faace <getpid+14>: repz ret 
0xb76faad0 <getpid+16>: jne    0xb76faadc <getpid+28>
0xb76faad2 <getpid+18>: mov    %gs:0x48,%eax
0xb76faad8 <getpid+24>: test   %eax,%eax
0xb76faada <getpid+26>: jne    0xb76faace <getpid+14>
0xb76faadc <getpid+28>: mov    $0x14,%eax
0xb76faae1 <getpid+33>: call   *%gs:0x10
0xb76faae8 <getpid+40>: test   %edx,%edx
0xb76faaea <getpid+42>: mov    %eax,%ecx
0xb76faaec <getpid+44>: jne    0xb76faace <getpid+14>
0xb76faaee <getpid+46>: mov    %ecx,%gs:0x48
0xb76faaf5 <getpid+53>: ret  

Я не совсем понимаю ассемблерный код. Также было бы полезно, если бы кто-нибудь мог дать подробное объяснение по этому поводу. По моим наблюдениям, вызов *%gs:0x10 (, который переходит в vdso) не выполняется, за исключением первого вызова getpid(), что может быть причиной того, что последующие getpid( ) вызовы не перехватываются. Но я не знаю почему.

Ядро Linux: 2.6.24-29 gcc (GCC) 4.2.4 libc 2.7,

Спасибо!


person Infinite    schedule 23.04.2011    source источник


Ответы (3)


Glibc кэширует результат, так как он не может измениться между вызовами. Например, см. исходный код здесь.

Таким образом, настоящий системный вызов выполняется только один раз. Остальные вызовы просто читаются из кеша. (Код не очень прост, потому что он заботится о том, чтобы делать правильные вещи с потоками.)

person Mat    schedule 23.04.2011
comment
Здорово. И у меня есть еще один вопрос; это может быть не актуально. Мне интересно, не вызывает ли даже первый вызов getpid() переключение режима пользователь-ядро, используя vdso, как для gettimeofday? - person Infinite; 23.04.2011
comment
Другой вопрос: как я могу сделать один шаг для вызова *%gs:0x10 с помощью gdb? - person Infinite; 23.04.2011
comment
Я не уверен, что на это есть единственный ответ. системные вызовы обрабатываются по-разному в зависимости от платформы (т. е. даже 32-разрядная версия x86 и 64-разрядная версия x86_64 имеют разные механизмы системных вызовов). Но, возможно, я ошибаюсь - вам, вероятно, следует опубликовать отдельный вопрос для этого и указать, какие архитектуры вас интересуют, и какие системные вызовы, если есть некоторые, которые вас особенно интересуют. (я не вообще не очень хорошо знаю gdb) - person Mat; 23.04.2011
comment
@SIFE: не то, чтобы я знал, и я не вижу причин, по которым они могли бы предоставить эту функцию - PID процесса никогда не меняется. - person Mat; 08.02.2013
comment
@Mat Но это не отвечает на мой вопрос. - person SIFE; 08.02.2013
comment
@SIFE: Как я уже сказал, не думаю, что есть. - person Mat; 09.02.2013

glibc кэширует значение pid. При первом вызове getpid он запрашивает у ядра pid, в следующий раз он просто возвращает значение, полученное при первом системном вызове getpid.

glibc-код:

pid_t
__getpid (void)
{
#ifdef NOT_IN_libc
  INTERNAL_SYSCALL_DECL (err);
  pid_t result = INTERNAL_SYSCALL (getpid, err, 0);
#else
  pid_t result = THREAD_GETMEM (THREAD_SELF, pid);
  if (__builtin_expect (result <= 0, 0))
    result = really_getpid (result);
#endif
  return result;
}

Если вы хотите протестировать накладные расходы на системные вызовы, для этого часто используется gettimeofday() - работа, проделанная ядром, очень мала, и ни компилятор, ни библиотека C не могут оптимизировать вызовы к нему.

person nos    schedule 23.04.2011
comment
Вы не можете использовать функцию gettimeofday() для измерения накладных расходов системного вызова, поскольку gettimeofday не является стандартным системным вызовом в Linux: он оптимизирован с помощью механизма vDSO. На моем ноутбуке стандартный системный вызов длится около 225 нс, тогда как gettimeofday длится всего 20 нс. - person pdagog; 09.03.2018

В настоящее время, с введением pid_namespaces и многочисленными ошибками, обнаруженными в приложениях при получении сигнала или при создании дочерних процессов путем вызова syscall() вместо fork(), vfork() и clone (), pid больше не кэшируется в GLIBC. Это указано в руководстве:

Начиная с glibc версии 2.3.4 и вплоть до версии 2.24,
функция оболочки glibc для getpid() кэшировала PID с целью
избежать дополнительных системных вызовов, когда процесс вызывает getpid()
неоднократно. Обычно это кэширование было невидимым, но его правильная
работа зависела от поддержки в функциях-оболочках для fork(2),
vfork(2) и clone(2): если приложение обходит glibc
оболочки для этих системных вызовов с помощью syscall(2), то вызов
getpid() в дочернем элементе вернет неверное значение (точнее,
он вернет PID родительского обработать). Вдобавок
были случаи, когда getpid() мог вернуть неверное значение
даже при вызове clone(2) через функцию-оболочку glibc.
(Обсуждение одного из таких случаев см. ОШИБКИ в clone(2).)
Кроме того, сложность кода кэширования была
источником нескольких ошибок в glibc на протяжении многих лет.

person Rachid K.    schedule 26.12.2020