问题描述
Erlang的port相当于系统的IO,打开了Erlang世界通往外界的通道,可以很方便的执行外部程序。 但是open_port的性能对整个系统来讲非常的重要,我就带领大家看看open_port影响性能的因素。首先**open_port的文档: {spawn, Command} Starts an external program. Command is the name of the external program which will be run. Command runs outside the Erlang work space unless an Erlang driver with the name Command is found. If found, that driver will be started. A driver runs in the Erlang workspace, which means that it is linked with the Erlang runtime **. When starting external programs on Solaris, the ** call vfork is used in preference to fork for performance reasons, although it has a history of being less robust. If there are problems with using vfork, setting the environment variable ERL_NO_VFORK to any value will cause fork to be used instead. For external programs, the PATH is searched (or an equivalent method is used to find programs, depending on operating **). This is done by invoking the shell och certain platforms. The first space separated token of the command will be considered as the name of the executable (or driver). This (among other things) makes this option unsuitable for running programs having spaces in file or directory names. Use {spawn_executable, Command} instead if spaces in executable file names is desired.open_port一个外部程序的时候流程大概是这样的:beam.smp先vfork, 子进程调用child_setup程序,做进一步的清理**作。 清理完成后才真正exec我们的外部程序。再来**open_port实现的代码:// sys.c:L135202static ErlDrvData spawn_start(ErlDrvPort port_num, char* name, SysDriverOpts* opts)03{04...05#if !DISABLE_VFORK06 int no_vfork;07 size_t no_vfork_sz = sizeof(no_vfork);08 09 no_vfork = (erts_sys_getenv("ERL_NO_VFORK",10 (char *) &no_vfork,11 &no_vfork_sz) >= 0);12#endif13...14else { /* Use vfork() */15 char **cs_argv= erts_alloc(ERTS_ALC_T_TMP,(CS_ARGV_NO_OF_ARGS + 1)*16 sizeof(char *));17 char fd_close_range; /* 44 bytes are enough to */18 char dup2_op; /* hold any "%d:%d" string */19 /* on a 64-bit machine. */20 21 /* Setup argv[] for the child setup program (implemented in 22 erl_child_setup.c) */23 i = 0;24 if (opts->use_stdio) {25 if (opts->read_write & DO_READ){26 /* stdout for process */27 sprintf(&dup2_op, "%d:%d", ifd, 1);28 if(opts->redir_stderr)29 /* stderr for process */30 sprintf(&dup2_op, "%d:%d", ifd, 2);31 }32 if (opts->read_write & DO_WRITE)33 /* stdin for process */34 sprintf(&dup2_op, "%d:%d", ofd, 0);35 } else { /* ** will fail if ofd == 4 (unlikely..) */36 if (opts->read_write & DO_READ)37 sprintf(&dup2_op, "%d:%d", ifd, 4);38 if (opts->read_write & DO_WRITE)39 sprintf(&dup2_op, "%d:%d", ofd, 3);40 }41 for (; i use_stdio ? 3 : 5, max_files-1);44 45 cs_argv = child_setup_prog;46 cs_argv = opts->wd ? opts->wd : ".";47 cs_argv = erts_sched_bind_atvfork_child(unbind);48 cs_argv = fd_close_range;49 for (i = 0; i spawn_type == ERTS_SPAWN_EXECUTABLE) {52 int num = 0;53 int j = 0;54 if (opts->argv != NULL) {55 for(; opts->argv != NULL; ++num)56 ;57 }58 cs_argv = erts_realloc(ERTS_ALC_T_TMP,cs_argv, (CS_ARGV_NO_OF_ARGS + 1 + num + 1) * sizeof(char *));59 cs_argv = "-";60 cs_argv = cmd_line;61 if (opts->argv != NULL) {62 for (;opts->argv != NULL; ++j) {63 if (opts->argv == erts_default_arg0) {64 cs_argv = cmd_line;65 } else {66 cs_argv = opts->argv;67 }68 }69 }70 cs_argv = NULL;71 } else {72 cs_argv = cmd_line; /* Command */73 cs_argv = NULL;74 }75 DEBUGF(("Using vforkn"));76 pid = vfork();77 78 if (pid == 0) {79 /* The child! */80 81 /* Observe! 82 * OTP-4389: The child setup program (implemented in 83 * erl_child_setup.c) will perform the necessary setup of the 84 * child before it execs to the user program. This because 85 * vfork() only allow an *immediate* execve() or _exit() in the 86 * child. 87 */88 execve(child_setup_prog, cs_argv, new_environ);89 _exit(1);90 }91 erts_free(ERTS_ALC_T_TMP,cs_argv);92...93}在支持vfork的系统下,比如说linux,除非禁止,默认会采用vfork来执行child_setup来调用外部程序。**vfork的文档: vfork() differs from fork() in that the parent is suspended until the child makes a call to execve(2) or _exit(2). The child shares all memory with its parent, including the stack, until execve() is issued by the child. The child must not return from the current function or call exit(), but may call _exit().vfork的时候beam.smp整个进程会被阻塞,所以这里是个很重要的性能影响点。我们再**erl_child_setup.c的代码:// erl_child_setup.c:11102// 1. 取消绑定03if (strcmp("false", argv) != 0)04 if (erts_unbind_from_cpu_str(argv) != 0)05 return 1;06// 2. 复制句柄07 for (i = 0; i 6 _ = ,7 Port = open_port({spawn, "/bin/cat"}, ),8 port_close(Port),9 ok.我们再准备个stap脚本,用来分析这些行为和性能数字:$ cat demo.stp02global t0, t1, t203 04probe process("beam.smp").function("spawn_start") {05 printf("spawn %sn", user_string($name))06 t0 = gettimeofday_us()07}08 09probe process("beam.smp").statement("*@sys.c:1607") {10 t1 = gettimeofday_ns()11}12 13probe process("beam.smp").statement("*@sys.c:1627") {14 printf("vfork take %d nsn", gettimeofday_ns() - t1);15}16 17probe process("child_setup").function("main") {18 t2 = gettimeofday_us()19}20 21probe process("child_setup").statement("*@erl_child_setup.c:111") {22 t3 = gettimeofday_us()23 printf("spawn take %d us, child_setup take %d usn", t3 - t0, t3 - t2)24}25 26probe syscall.execve {27 printf("%s, arg %sn", name, argstr)28}29 30probe syscall.fork {31 printf("%s, arg %sn", name, argstr)32}33 34probe begin {35 println(")");我们在一个终端下运行stap脚本观察行为:$ erlc demo.erl02$ PATH=otp/bin/x86_64-unknown-linux-gnu/:$PATH sudo stap demo.stp03)04fork, arg05execve, arg otp/bin/erl06fork, arg07fork, arg08fork, arg09execve, arg /bin/sed "s/.*\///"10execve, arg /home/chuba/otp/bin/x86_64-unknown-linux-gnu/erlexec11execve, arg /home/chuba/otp/bin/x86_64-unknown-linux-gnu/beam.smp "--" "-root" "/home/chuba/otp" "-progname" "erl" "--" "-home" "/home/chuba" "--"12clone, arg .13..14clone, arg CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID15spawn inet_gethost 416fork, arg17execve, arg /home/chuba/otp/bin/x86_64-unknown-linux-gnu/child_setup "FFFF" "." "exec inet_gethost 4 " "3:327679" "8:1" "9:0" "-"18vfork take 8487 ns19spawn take 173707 us, child_setup take 94535 us20execve, arg /bin/sh "-c" "exec inet_gethost 4 "21execve, arg /home/chuba/otp/bin/x86_64-unknown-linux-gnu/inet_gethost "4"22fork, arg23clone, arg CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID24clone, arg CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID25spawn /bin/cat26fork, arg27execve, arg /home/chuba/otp/bin/x86_64-unknown-linux-gnu/child_setup "FFFF" "." "exec /bin/cat" "3:327679" "2312:1" "2313:0" "-"28vfork take 5298 ns29spawn take 180974 us, child_setup take 101646 us30execve, arg /bin/sh "-c" "exec /bin/cat"31execve, arg /bin/cat32spawn /bin/cat33fork, arg34execve, arg /home/chuba/otp/bin/x86_64-unknown-linux-gnu/child_setup "FFFF" "." "exec /bin/cat" "3:327679" "3080:1" "3081:0" "-"35vfork take 8929 ns36spawn take 169569 us, child_setup take 90163 us37execve, arg /bin/sh "-c" "exec /bin/cat"38execve, arg /bin/cat39...在另外一个终端下运行我们的**案例:$ otp/bin/erl2Erlang R14B04 (erts-5.8.5) 1 3 4Eshell V5.8.5 (abort with ^G)51> demo:start().6ok72> demo:start().8ok93>我们可以看到二次执行的开销差不多:vfork take 8929 nsspawn take 169569 us, child_setup take 90163 us从实验得来的数字来看:vfork需要阻塞beam.smp 8个us时间,而整个spawn下来要169ms, 其中 child_setup关闭句柄等等花了90ms, 数字无情的告诉我们这些性能杀手不容忽视。解决方案:1. 改用fork避免阻塞beam.smp, erl -env ERL_NO_VFORK 12. 减少文件句柄,如果确实需要大量的open_port让另外一个专注的节点来做。祝玩得开心!