Почему tzset () работает намного медленнее после разветвления в Mac OS X?

Вызов tzset() после разветвления кажется очень медленным. Я вижу медлительность только в том случае, если сначала вызываю tzset() в родительском процессе перед разветвлением. Моя переменная среды TZ не установлена. Я dtruss запустил свою тестовую программу, и она показала, что дочерний процесс читает /etc/localtime при каждом tzset() вызове, в то время как родительский процесс читает его только один раз. Этот доступ к файлу кажется источником медлительности, но я не мог определить, почему он обращается к нему каждый раз в дочернем процессе.

Вот моя тестовая программа foo.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>

void check(char *msg);

int main(int argc, char **argv) {
  check("before");

  pid_t c = fork();
  if (c == 0) {
    check("fork");
    exit(0);
  }

  wait(NULL);

  check("after");
}

void check(char *msg) {
  struct timeval tv;

  gettimeofday(&tv, NULL);
  time_t start = tv.tv_sec;
  suseconds_t mstart = tv.tv_usec;

  for (int i = 0; i < 10000; i++) {
    tzset();
  }

  gettimeofday(&tv, NULL);
  double delta = (double)(tv.tv_sec - start);
  delta += (double)(tv.tv_usec - mstart)/1000000.0;

  printf("%s took: %fs\n", msg, delta);
}

Я скомпилировал и выполнил foo.c вот так:

[muir@muir-work-mb scratch]$ clang -o foo foo.c
[muir@muir-work-mb scratch]$ env -i ./foo
before took: 0.002135s
fork took: 1.122254s
after took: 0.001120s

Я использую Mac OS X 10.10.1 (также воспроизводится на 10.9.5).

Первоначально я заметил медлительность через рубин (Time # localtime slow в дочернем процессе).


person Muir    schedule 13.01.2015    source источник
comment
Незначительное: рекомендовать difftime(tv.tv_sec, start), а не (double)(tv.tv_sec - start). (double) в delta += (double)... не нужен.   -  person chux - Reinstate Monica    schedule 14.01.2015
comment
Я думаю, что основная причина низкой производительности заключается в том, что он на самом деле проверяет, не изменился ли файл часового пояса при каждом вызове localtime - он выполняет действия Mac, так как системные настройки могут измениться из-под него. Плохое поведение вилки может быть побочным эффектом используемого механизма уведомления об изменении файла, который не работает должным образом в fork() - это всего лишь предположение, основанное на прогоне кода через инструменты и некотором поиске в Google.   -  person Petesh    schedule 14.01.2015
comment
Мне понравилась теория уведомлений, поэтому я немного исследовал и опубликовал ответ ниже.   -  person Muir    schedule 14.01.2015


Ответы (2)


Ответ Кена Томаса может быть правильным, но мне было любопытно получить более конкретный ответ, потому что я все еще нахожу неожиданное поведение медлительности для однопоточной программы, выполняющей такую ​​простую / обычную операцию после forking. После изучения http://opensource.apple.com/source/Libc/Libc-997.1.1/stdtime/FreeBSD/localtime.c (не уверен на 100%, что это правильный источник), думаю, у меня есть ответ.

Код использует пассивные уведомления, чтобы определить, изменился ли часовой пояс (в отличие от stating /etc/localtime каждый раз). Похоже, что зарегистрированный токен уведомления становится недействительным в дочернем процессе после forking. Кроме того, код обрабатывает ошибку из-за использования недопустимого токена как положительное уведомление о том, что часовой пояс изменился, и каждый раз переходит к чтению /etc/localtime. Полагаю, такое неопределенное поведение может возникнуть после forking? Было бы неплохо, если бы библиотека заметила ошибку и перерегистрировалась для получения уведомления.

Вот фрагмент кода из localtime.c, который смешивает значение ошибки со значением статуса:

nstat = notify_check(p->token, &ncheck);
if (nstat || ncheck) {

Я продемонстрировал, что токен регистрации становится недействительным после вилки с помощью этой программы:

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

void bail(char *msg) {
  printf("Error: %s\n", msg);
  exit(1);
}

int main(int argc, char **argv) {
  int token, something_changed, ret;

  notify_register_check("com.apple.system.timezone", &token);

  ret = notify_check(token, &something_changed);
  if (ret)
    bail("notify_check #1 failed");
  if (!something_changed)
    bail("expected change on first call");

  ret = notify_check(token, &something_changed);
  if (ret)
    bail("notify_check #2 failed");
  if (something_changed)
    bail("expected no change");

  pid_t c = fork();
  if (c == 0) {
    ret = notify_check(token, &something_changed);
    if (ret) {
      if (ret == NOTIFY_STATUS_INVALID_TOKEN)
        printf("ret is invalid token\n");

      if (!notify_is_valid_token(token))
        printf("token is not valid\n");

      bail("notify_check in fork failed");
    }

    if (something_changed)
      bail("expected not changed");

    exit(0);
  }

  wait(NULL);
}

И запустил это так:

muir-mb:projects muir$ clang -o notify_test notify_test.c 
muir-mb:projects muir$ ./notify_test 
ret is invalid token
token is not valid
Error: notify_check in fork failed
person Muir    schedule 14.01.2015

Вам повезло, что вы не сталкивались с носовыми демонами!

POSIX утверждает, что только функции, безопасные для асинхронных сигналов, разрешены для вызова в дочернем процессе после fork() и перед вызовом exec*() функции. Из стандарта (выделено мной):

… Дочерний процесс может выполнять только безопасные для асинхронных сигналов операции до тех пор, пока не будет вызвана одна из exec функций.

Программисты POSIX называют fork() по двум причинам. Одна из причин - создать новый поток управления в той же программе (что изначально было возможно в POSIX только путем создания нового процесса); другой - создать новый процесс, выполняющий другую программу. В последнем случае за вызовом fork() вскоре следует вызов одной из функций exec.

Общая проблема, связанная с работой fork() в многопоточном мире, заключается в том, что делать со всеми потоками. Есть две альтернативы. Один из них - скопировать все потоки в новый процесс. Это заставляет программиста или реализацию иметь дело с потоками, которые приостановлены при системных вызовах или которые могут собираться выполнить системные вызовы, которые не должны выполняться в новом процессе. Другой вариант - скопировать только поток, вызывающий fork(). Это создает трудность, заключающуюся в том, что состояние локальных ресурсов процесса обычно хранится в памяти процесса. Если поток, который не вызывает fork(), содержит ресурс, этот ресурс никогда не освобождается в дочернем процессе, потому что поток, задачей которого является освобождение ресурса, не существует в дочернем процессе.

Когда программист пишет многопоточную программу, первое описанное использование fork(), создание новых потоков в той же программе, обеспечивается функцией pthread_create(). Таким образом, функция fork() используется только для запуска новых программ, а эффекты вызова функций, которым требуются определенные ресурсы между вызовом fork() и вызовом exec функции, не определены.

здесь и здесь. Для любой другой функции, если специально не задокументировано, что реализации на платформах, на которых вы развертываете, добавляют нестандартную гарантию безопасности, тогда вы должны считать ее небезопасной, а ее поведение на дочерней стороне fork() неопределенным.

person Ken Thomases    schedule 13.01.2015
comment
... дочерний процесс может выполнять только безопасные для асинхронного сигнала операции до тех пор, пока не будет вызвана одна из функций exec, которая, по-видимому, относится к тому, что происходит, когда многопоточный процесс вызывает fork (). Так что я не думаю, что это относится к вышеупомянутому вопросу. - person psanford; 14.01.2015
comment
Это обоснование, но я не считаю, что это правило условно. Поскольку любая программа может стать многопоточной, системные библиотеки должны это учитывать. Они могут регистрировать pthread_atfork() обратные вызовы, реализация которых предполагает, что программа может быть многопоточной во время вилки. Поэтому они могут временно перевести общее состояние в безопасное для вилки состояние в обратном вызове подготовки, восстановить их в родительском обратном вызове, но оставить их такими же в дочернем обратном вызове (потому что восстанавливать их там не требуется). - person Ken Thomases; 14.01.2015
comment
Я считаю, что это слишком широкое чтение. Ни APUE, ни среда программирования Linux, ни Butenhof ничего не упоминают об этом ограничении за пределами многопоточной программы, что было бы довольно явным упущением, если бы это было правдой. Стандарт четко изолирует это, если многопоточный процесс вызывает fork (), как указывает @psanford - я думаю, что системная библиотека с поддержкой потоков, которая не оставила дочерний fork безопасным в однопоточном процессе, как вы description будет просто нарушен, так как в этом случае действительно нет проблем, если они не будут созданы. - person Crowman; 14.01.2015
comment
Из pthread_atfork() справочной страницы OS X : на дочерней стороне fork () разрешены только безопасные для асинхронных сигналов функции. Точно так же см. мой ответ на связанный вопрос. Я получил свою интерпретацию стандарта от Apple. Кроме того, в стандарте четко указано, что единственное допустимое использование fork() - это запуск новых программ. - person Ken Thomases; 14.01.2015
comment
Возвращаясь к вашему последнему предложению, здесь говорится только о том, что сразу после того, как программист пишет многопоточную программу, первое описанное использование fork(), создание новых потоков в той же программе, обеспечивается функцией pthread_create(), ограничивая этот комментарий до многопоточности. многопоточные программы. Если вы используете сторонние фреймворки, такие как Cocoa, то, очевидно, это честная игра, но ни одна нормальная ISO или базовая библиотека UNIX не должна случайным образом создавать потоки, кроме полностью прозрачных, или вы могли бы никогда использовать такую ​​функцию, как malloc(), поскольку POSIX не требует, чтобы он был потокобезопасным. - person Crowman; 14.01.2015
comment
В этом параграфе говорится, что fork() не является способом достижения (примитивной) многопоточности, как описано двумя параграфами ранее. Теперь для этого есть pthread_create(). Но это предложение не предполагает, что последующее применимо только к многопоточным программам. Он говорит, что осталось только одно использование fork(), учитывая, что другое использование исключено: для запуска новых программ. Это верно как для однопоточных программ, так и для многопоточных. OP делает то, что стандарт описывает как старый способ многопоточности. Как вы думаете, о чем он говорит, если не об этом? - person Ken Thomases; 14.01.2015
comment
Просто, когда [вы] пишете многопоточную программу, вы используете pthread_create() для создания нового потока управления, а не fork(), поскольку, вероятно, именно поэтому вы решили написать такую ​​программу. Для других случаев ранее говорилось, что есть две причины, по которым программисты POSIX вызывают fork(), а не были две причины ... но теперь они всегда используют pthreads для одной из них. Эффективный отказ от использования fork() для этой цели был бы очень существенным изменением, которое, будем надеяться, будет выделено очень четко, а не сведено к двусмысленному обсуждению в разделе обоснования. - person Crowman; 14.01.2015