Skip to content

Erlang 工具run_erl使用介绍和原理

Published:

Erlang提供了run_erl工具,可以用来启动erlang;同时也提供了to_erl工具,用来连接由run_erl启动的erlang进程。

用AI翻译了官网手册介绍,原文地址(https://www.erlang.org/doc/apps/erts/run_erl_cmd.html):

run_erl 程序是特定于 Unix 系统的工具。它用于重定向标准输入和标准输出流,以便记录所有输出。此外,它还允许 to_erl 程序连接到 Erlang 控制台,从而可以远程监视和调试嵌入式系统。

有关详细的使用信息,请参阅系统文档中的《嵌入式系统用户指南》。

运行方式:run_erl [-daemon] pipe_dir/ log_dir "exec command arg1 arg2 ..."

参数说明:
-daemon —— 强烈推荐使用此选项。它使 run_erl 在后台运行,完全脱离任何控制终端,并立即返回给调用方。如果不使用该选项,则需要使用多种 shell 技巧来完全将 run_erl 与启动它的终端分离。该选项必须作为 run_erl 命令行上的第一个参数。

pipe_dir —— 命名管道的目录,通常是 /tmp/。必须以 /(斜杠)结尾,例如 /tmp/epipes/,而不能是 /tmp/epipes。

log_dir —— 日志文件目录,包括:

一个日志文件 run_erl.log,用于记录 run_erl 本身的进度和警告信息。

最多五个日志文件(默认每个最大 100 KB),记录标准输入和标准输出的内容。(日志数量和大小可以通过环境变量修改,详见下文“环境变量”部分。)

当日志文件满后,run_erl 会删除最旧的日志文件,并重新使用它。

"exec command arg1 arg2 ..." —— 用于指定要执行的程序的字符串,通常第二个字段是类似 erl 的命令名称。
``

按手册提示,我们先创建目录,然后启动run_erl。

```bash
$mkdir -p /tmp/erlang_pipe /tmp/erlang_log
$run_erl -daemon /tmp/erlang_pipe/ /tmp/erlang_log/ "exec erl"

输入ps aux | grep erl,看看有没有启动成功。

$ps aux | grep erl
root     18002  0.0  0.0  14140   688 ?        S    15:40   0:00 run_erl -daemon /tmp/erlang_pipe/ /tmp/erlang_log/ exec erl
root     18003  0.3  0.9 2207008 18352 pts/2   Ssl+ 15:40   0:00 /usr/local/lib/erlang/erts-10.7.2.19/bin/beam.smp -- -root /usr/local/lib/erlang -progname erl -- -home /root --

以上所示,启动成功。

查看/tmp/erlang_pipe/

$ll /tmp/erlang_pipe/
total 0
prw------- 1 root root 0 Mar 27 15:45 erlang.pipe.1.r
prw------- 1 root root 0 Mar 27 15:45 erlang.pipe.1.w

从文件名字来看,应该是管道1负责读,管道2负责写。prw-------中的p说明是命名管道(pipe)

再看看/tmp/erlang_log/

$ll /tmp/erlang_log/
total 8
-rw-r--r-- 1 root root 195 Mar 27 15:45 erlang.log.1
-rw-r--r-- 1 root root 236 Mar 27 15:45 run_erl.log

如手册所说,erlang.log.1为erlang日志,run_erl.lgo为工具日志。

现在尝试用to_erl连接这个erlang控制台。

先看官网说明,原文地址(https://www.erlang.org/doc/system/embedded#to_erl)

该程序用于连接(attach)到一个由 run_erl 启动的正在运行的 Erlang 运行时系统。

用法:to_erl [pipe_name | pipe_dir]
其中,pipe_name 默认为 /tmp/erlang.pipe.N。

说明:
to_erl 允许你连接到 run_erl 启动的 Erlang 进程,方便进行交互、监控和调试。

断开连接(但不退出 Erlang 系统),请按 Ctrl+D。

按手册指示,输入to_erl /tmp/erlang_pipe/erlang.pipe.1

$to_erl /tmp/erlang_pipe/erlang.pipe.1
Attaching to /tmp/erlang_pipe/erlang.pipe.1 (^D to exit)

1> 

连接成功,这里要注意是是按Ctrl+D退出to_erl,否则会退出Erlang。

看完run_erl和to_erl的用法后,我们看看与平时erlang放后台运行的方式有什么不同?并且为什么需要run_erl这种工具?

erl使用参数-detached,可以将erlang放到后台运行,输入erl -name [email protected] -setcookie mycookie -detached。 然后可通过remsh模式远程登录节点,如erl -name [email protected] -setcookie mycookie -remsh [email protected]

以这种方式启动的erlang控制台,只能够通过远程连接节点的方式进入控制台。假设出现类似system_limit之类的问题,这种方式会连接失败,详见[Erlang 源码阅读笔记:端口数(port_limit)是如何作用的]这篇文章。而run_erl方式启动的,通过to_erl依然能连接上。为什么to_erl还能连接上,我们来看看run_erl的实现原理。

run_erl所在源码文件erts/etc/unix/run_erl.c

/* 
 * Module: run_erl.c
 * 
 * This module implements a reader/writer process that opens two specified 
 * FIFOs, one for reading and one for writing; reads from the read FIFO
 * and writes to stdout and the write FIFO.
 *
  ________                            _________ 
 |        |--<-- pipe.r (fifo1) --<--|         |
 | to_erl |                          | run_erl | (parent)
 |________|-->-- pipe.w (fifo2) -->--|_________|
                                          ^ master pty
                                          |
                                          | slave pty
                                      ____V____ 
                                     |         |
                                     |  "erl"  | (child)
                                     |_________|
*/

从源码中的注释,可以知道run_erl的工作流程,我们看看源码细节。

int main(int argc, char **argv)
{
  int childpid;
  int sfd = -1;
  int fd;
  char *p, *ptyslave=NULL;
  int i = 1;
  int off_argv;
  int calculated_pipename = 0;
  int highest_pipe_num = 0;
  int sleepy_child = 0;
//...省略部分代码...
  /*
   * Open master pseudo-terminal
   */

  if ((mfd = open_pty_master(&ptyslave, &sfd)) < 0) {	//创建pty, mfd为主设备文件描述符, sfd为从设备文件描述符, ptyslave为终端名称,如/dev/pts/2
    ERRNO_ERR0(LOG_ERR,"Could not open pty master");
    exit(1);
  }

  /* 
   * Now create a child process
   */

  if ((childpid = fork()) < 0) { // fork一个子进程,用来运行erlang
    ERRNO_ERR0(LOG_ERR,"Cannot fork");
    exit(1);
  }
  if (childpid == 0) {	// childpid 等于 0,即新开的子进程
      if (sleepy_child)
          sleep(1);

    /* Child */	// 注释也标明是子进程
    sf_close(mfd);
//......
#if defined(HAVE_OPENPTY) && defined(TIOCSCTTY)
    else {
        /* sfd is from open_pty_master 
         * openpty -> fork -> login_tty (forkpty)
         * 
         * It would be preferable to implement a portable 
         * forkpty instead of open_pty_master / open_pty_slave
         */
        /* login_tty(sfd);  <- FAIL */
        ioctl(sfd, TIOCSCTTY, (char *)NULL); // 将终端输绑定sfd,那么终端的输出输入都由sfd负责
    }
#endif
    }
//......
    if (dup(sfd) != 0 || dup(sfd) != 1 || dup(sfd) != 2) {
      status("Cannot dup\n");
    }
    sf_close(sfd);
	// 执行command,运行erl
    exec_shell(argv+off_argv); /* exec_shell expects argv[2] to be */
                        /* the command name, so we have to */
                        /* adjust. */
} else {
    /* Parent */	//父进程,即run_erl进程
    /* Ignore the SIGPIPE signal, write() will return errno=EPIPE */
    struct sigaction sig_act;
    sigemptyset(&sig_act.sa_mask);
    sig_act.sa_flags = 0;
    sig_act.sa_handler = SIG_IGN;
    sigaction(SIGPIPE, &sig_act, (struct sigaction *)NULL);

    sigemptyset(&sig_act.sa_mask);
    sig_act.sa_flags = SA_NOCLDSTOP;
    sig_act.sa_handler = catch_sigchild;
    sigaction(SIGCHLD, &sig_act, (struct sigaction *)NULL);

    /*
     * read and write: enter the workloop
     */

    pass_on(childpid);//监听键盘输入
  }
  return 0;
} /* main() */

具体看看open_pty_master的实现

/* open_pty_master()
 * Find a master device, open and return fd and slave device name.
 */

#ifdef HAVE_WORKING_POSIX_OPENPT
   /*
    * Use openpty() on OpenBSD even if we have posix_openpt()
    * as there is a race when read from master pty returns 0
    * if child has not yet opened slave pty.
    * (maybe other BSD's have the same problem?)
    */
#  if !(defined(__OpenBSD__) && defined(HAVE_OPENPTY))
#    define TRY_POSIX_OPENPT
#  endif
#endif

static int open_pty_master(char **ptyslave, int *sfdp)
{
  int mfd;
//......
  {
      static char slave[SLAVE_SIZE];
#  undef SLAVE_SIZE
      if (openpty(&mfd, sfdp, slave, NULL, NULL) == 0) {//调用系统提供的openpty方法, 分别返回主设备的文件描述符,从设备的文件描述符
          *ptyslave = slave;	// 终端名字,如/dev/pts/2
          return mfd;		// 设备的文件描述符
      }
  }
/* pass_on()
 * Is the work loop of the logger. Selects on the pipe to the to_erl
 * program erlang. If input arrives from to_erl it is passed on to
 * erlang.
 */
static void pass_on(pid_t childpid)
{
    int len;
    fd_set readfds;
    fd_set writefds;
    fd_set* writefds_ptr;
    struct timeval timeout;
    time_t last_activity;
    char buf[BUFSIZ];
    char log_alive_buffer[ALIVE_BUFFSIZ+1];
    int lognum;
    int rfd, wfd=0, lfd=0;
    int maxfd;
    int ready;
    int got_some = 0; /* from to_erl */
//...省略部分代码...

 /*
         * Write any pending output first.
         */
        if (FD_ISSET(wfd, &writefds)) {
            int written;
            char* buf = outbuf_first();	// 这里缓存了erlang的返回数据,现在先取出,然后往wfd写入,发给to_erl

            len = outbuf_size();
            written = sf_write(wfd, buf, len);	// 往wfd写入,发给to_erl
            if (written < 0 && errno == EAGAIN) {
                /*
                 * Nothing was written - this is really strange because
                 * select() told us we could write. Ignore.
                 */
            } else if (written < 0) {
                /*
                 * A write error. Assume that to_erl has terminated.
                 */
                clear_outbuf();
                sf_close(wfd);
                wfd = 0;
            } else {
                /* Delete the written part (or all) from the buffer. */
                outbuf_delete(written);
            }
        }
        /*
         * Read master pty and write to FIFO.	// 从mfd读出erlang返回数据然后往wfd写入,to_erl再读取
         */
        if (FD_ISSET(mfd, &readfds)) {
#ifdef DEBUG
            status("Pty master read; ");
#endif      
            if ((len = sf_read(mfd, buf, BUFSIZ)) <= 0) {	// 从mfd读取数据,即上面通过open_pty_master返回的master pty 设备文件描述符
                int saved_errno = errno;
                sf_close(rfd);
                if(wfd) sf_close(wfd);
                sf_close(mfd);
                unlink(fifo1);
                unlink(fifo2);
                if (len < 0) {
                    errno = saved_errno;
                    if(errno == EIO)
                        ERROR0(LOG_ERR,"Erlang closed the connection.");
                    else
                        ERRNO_ERR0(LOG_ERR,"Error in reading from terminal");
                    exit(1);
                }
                exit(0);
            }
            
            write_to_log(&lfd, &lognum, buf, len);
            
            /*
             * Save in the output queue.
             */
            
            if (wfd) {
                outbuf_append(buf, len);	// 将mfd返回的数据缓存起来,下次再发送
            }
        }
//...省略部分代码...
       /*
         * Read from FIFO, write to master pty	// 从rfd里读取数据,然后往mfd写入
         */
        if (FD_ISSET(rfd, &readfds)) {
#ifdef DEBUG
            status("FIFO read; ");
#endif
            if ((len = sf_read(rfd, buf, BUFSIZ)) < 0) {	// 从rfd读取to_erl写入的指令,保存在buf
                sf_close(rfd);
                if(wfd) sf_close(wfd);
                sf_close(mfd);
                unlink(fifo1);
                unlink(fifo2);
                ERRNO_ERR0(LOG_ERR,"Error in reading from FIFO.");
                exit(1);
            }
//...省略部分代码...

                /* Write the message */
#ifdef DEBUG
                status("Pty master write; ");
#endif
                len = extract_ctrl_seq(buf, len);

                if(len==1 && buf[0] == '\003') {
                    kill(childpid,SIGINT);
                }
                else if (len>0 && write_all(mfd, buf, len) != len) { // 往mfd写入数据,发送给erlang
                    ERRNO_ERR0(LOG_ERR,"Error in writing to terminal.");
                    sf_close(rfd);
                    if(wfd) sf_close(wfd);
                    sf_close(mfd);
                    exit(1);
                }
            }

通过源码,我们总结一下,run_erl调用系统提供的openpty函数,分别生成mfd和sfd,两个文件描述符。然后fork一个子进程,子进程的输入输出由sfd负责,而父进程则对mfd进行读写。 mfd和sfd会自动同步数据。to_erl与run_erl通过rfd和wfd两个文件描述符进行交互,然后run_erl将to_erl的输入写入mfd,然后将mfd的数据返回给to_erl。

再重温一下源码开头的注释所示的流程图。

  ________                            _________
 |        |--<-- pipe.r (fifo1) --<--|         |
 | to_erl |                          | run_erl | (parent)
 |________|-->-- pipe.w (fifo2) -->--|_________|
                                          ^ master pty
                                          |
                                          | slave pty
                                      ____V____
                                     |         |
                                     |  "erl"  | (child)
                                     |_________|

我们回顾一下上面的两个问题?

run_erl与平时erlang放后台运行的方式有什么不同?

run_erl是使用pty建立erlang控制台,模拟终端操作,通过pty可以与erlang控制台交互。

而使用erl -detached是直接以后台进程形式运行,只能通过远程节点的方式进入控制台。

类似的工具有screen,为什么还需要run_erl这种工具?

像erlang这种系统,要考虑嵌入式系统环境,嵌入式系统为了稳定安全工作,可能没有安装太多的三方工具,甚至不需要联网,就是一个简单的本地部署。

因此将run_erl这种工具直接集成到erlang里,可以在类似的系统环境里提供更可靠的维护方式。

补充: 从run_erl的源码中,我们可以看出run_erl不局限于只运行erlang。是一个独立的工具。

尝试运行python,输入run_erl -daemon /tmp/erlang_pipe/ /tmp/erlang_log/ "python"

$run_erl -daemon /tmp/erlang_pipe/ /tmp/erlang_log/ "python" 

再输入to_erl /tmp/erlang_pipe/erlang.pipe.3

$to_erl /tmp/erlang_pipe/erlang.pipe.3
Attaching to /tmp/erlang_pipe/erlang.pipe.3 (^D to exit)
>>> print("hello")
hello
>>>

成功进入python控制台。


Previous Post
PTY介绍和应用实践
Next Post
Erlang 源码阅读笔记:端口数(port_limit)设置