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

Mui*_*uir 5 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);
}
Run Code Online (Sandbox Code Playgroud)

我编译并执行了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
Run Code Online (Sandbox Code Playgroud)

我正在运行Mac OS X 10.10.1(也在10.9.5上复制).

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

Mui*_*uir 6

Ken Thomases的回答可能是正确的,但我对一个更具体的答案感到好奇,因为我仍然发现单线程程序执行这样一个简单/常见操作后的慢速意外行为fork.在检查了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) {
Run Code Online (Sandbox Code Playgroud)

我证明了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);
}
Run Code Online (Sandbox Code Playgroud)

然后像这样运行:

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
Run Code Online (Sandbox Code Playgroud)


Ken*_*ses 3

你很幸运,没有遇到鼻恶魔

\n\n

POSIX 规定,只有异步信号安全函数在fork()调用函数之后和之前在子exec*()进程中调用才是合法的。从标准(强调添加):

\n\n
\n

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

\n\n

\xe2\x80\xa6

\n\n

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

\n\n

fork()在多线程世界中工作的普遍问题是如何处理所有线程。有两种选择。一是将所有线程复制到新进程中。这会导致程序员或实现处理在系统调用上挂起的线程,或者可能即将执行不应在新进程中执行的系统调用的线程。另一种选择是仅复制调用 的线程fork()。这造成了进程本地资源的状态通常保存在进程内存中的困难。如果未调用的线程fork()持有资源,则该资源永远不会在子进程中释放,因为其任务是释放该资源的线程在子进程中不存在。

\n\n

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

\n
\n\n

这里这里有异步信号安全函数的列表。对于任何其他功能,如果没有具体记录您要部署的平台上的实现添加了非标准安全保证,那么您必须认为它是不安全的,并且它在 to 的子端的fork()行为未定义。

\n