为什么在Mac OS X上分叉后tzset()会慢很多?

时间:2015-01-13 22:03:36

标签: c macos fork libc

分叉后调用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上复制)。

我最初注意到ruby的缓慢(在子进程中Time#localtime slow)。

2 个答案:

答案 0 :(得分:6)

Ken Thomases的回答可能是正确的,但我对更具体的答案感到好奇,因为我仍然发现单线程程序在fork之后执行这样一个简单/常见操作时出现的慢速意外行为ING。在检查http://opensource.apple.com/source/Libc/Libc-997.1.1/stdtime/FreeBSD/localtime.c(不是100%确定这是正确的来源)之后,我想我有一个答案。

代码使用被动通知来确定时区是否已更改(而不是每次都stat /etc/localtime)。在fork之后,似乎已注册的通知令牌在子进程中变为无效。此外,代码将错误视为使用无效令牌作为时区已更改的肯定通知,并且每次都继续阅读/etc/localtime。我想这是fork之后你可以得到的那种未定义的行为?如果图书馆注意到错误并重新注册了通知,那将是很好的。

以下是localtime.c的代码片段,它将错误值与状态值混合在一起:

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

我证明使用此程序后fork注册令牌无效:

#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

答案 1 :(得分:3)

你很幸运,你没有遇到nasal demons

POSIX指出,在fork()之后和调用exec*()函数之前,只有异步信号安全函数才能在子进程中调用。从standard(强调添加):

  

...子进程可能只执行异步信号安全操作,直到调用其中一个exec函数为止。

     

...

     

POSIX程序员调用fork()有两个原因。一个原因是   在同一个程序中创建一个新的控制线程(这是   最初只能通过创建新流程在POSIX中实现);该   另一种是创建一个运行不同程序的新进程。在里面   在后一种情况下,调用fork()之后很快就会调用其中一个   exec函数。

     

使fork()在多线程世界中工作的一般问题   是如何处理所有线程的。有两种选择。一   是将所有线程复制到新进程中。这导致了   程序员或实现来处理被挂起的线程   在系统调用或可能即将执行系统调用   不应该在新进程中执行。另一种选择是   仅复制调用fork()的线程。这造成了困难   流程本地资源的状态通常在进行中   记忆。如果未调用fork()的线程拥有资源,那么   资源永远不会在子进程中释放,因为该线程   在孩子身上不存在释放资源的工作   过程

     

当程序员编写多线程程序时,第一个   描述了使用fork(),在同一个程序中创建新线程,是   由pthread_create()函数提供。因此fork()功能   仅用于运行新程序,以及调用函数的效果   在fork()的呼叫和呼叫之间需要某些资源   exec函数未定义

列出了异步信号安全功能herehere。对于任何其他功能,如果没有具体记录您要部署的平台上的实现添加了非标准的安全保证,那么您必须将其视为不安全及其在fork()的子端的行为未定义。