我正在将以下简单的 shell 脚本传递给 LXC 容器上的 bash:
apt-get update
apt-get install postgresql -y
sudo -u postgres psql -c 'create database dvdrental;'
我用来运行它的实际命令是:
cat sample.sh | lxc-attach -n test-container -- /bin/bash
我这样做而不是将脚本上传到容器中并以这种方式执行的原因是,这只是我们正在构建的更复杂的应用程序的概念证明,该应用程序必须通过标准输入接收命令并在容器中运行它们。
它似乎运行良好,除了一件事。它psql
在 postgresql 仍在安装时转到命令,即,
[...]
Get:21 http://archive.ubuntu.com/ubuntu/ trusty/main ssl-cert all 1.0.33 [16.6 kB]
Get:22 http://archive.ubuntu.com/ubuntu/ trusty-updates/main postgresql-common all 154ubuntu1 [103 kB]
Get:23 http://archive.ubuntu.com/ubuntu/ trusty-updates/main postgresql-9.3 amd64 9.3.10-0ubuntu0.14.04 [2,669 kB]
Get:24 http://archive.ubuntu.com/ubuntu/ trusty-updates/main postgresql all 9.3+154ubuntu1 [5,038 B]
Fetched 5,834 kB in 28s (207 kB/s)
Preconfiguring packages ...
sudo -u postgres psql -c 'create database dvdrental;'
Selecting previously unselected package libroken18-heimdal:amd64.
(Reading database ... 14599 files and directories currently installed.)
Preparing to unpack .../libroken18-heimdal_1.6~git20131207+dfsg-1ubuntu1.1_amd64.deb ...
Unpacking libroken18-heimdal:amd64 (1.6~git20131207+dfsg-1ubuntu1.1) ...
Selecting previously unselected package libasn1-8-heimdal:amd64.
[...]
sudo -u postgres psql -c 'create database dvdrental;'
请注意输出中间的这一行。有趣的是,它总是在 apt-get 命令的下载部分完成后立即出现...
谁知道这可能是什么原因造成的?
答案1
噢,这是一个乐趣一。
简短的回答:发生这种情况是因为 apt(或它分叉的某个东西)在执行时正在读取 stdin,并且它会读取脚本的剩余行,因为这些行此时仍位于 stdin 中。简短的解决方法:将其放在</dev/null
行末apt-get install
,然后继续你的一天。
长答案(认真地说,这是一个大问题):从正在运行的进程的角度来看,stdin/stdout/stderr 没有什么特别之处。它们只是文件描述符,文件描述符在进程分叉时在进程之间共享。因此,发生的事情(或多或少)是:
终端中交互运行的 bash 副本会打开一个新进程
pipe
(2),然后派生一个新进程,该进程会关闭现有的 stdout,然后将 stdout 文件描述符 (1) 设置为管道的写入端(参见dup2
(2))。然后,该子进程exec
会cat sample.sh
读取文件并将其写入到它所要写入的文件中。想是标准输出(但实际上是管道的写入端)。在终端中交互运行的 bash 副本会分叉另一个新进程,这次会关闭现有的标准输入,然后使 stdin 文件描述符 (0) 成为前面讨论过的同一管道的读取端(再次调用
dup2
)。然后此进程将exec
成为您的lxc-attach
进程。如果没有任何事物干扰 stdin(在本例中没有),那么每一个从获得管道读取器端作为 stdin 的进程分叉出来的进程将还具有完全相同的文件描述符,连接到同一个管道,其中包含了
sample.sh
其标准输入的内容。 任何从该文件描述符读取的进程现在将消耗已读取的字节,并且从该文件描述符读取的其他进程将不会获得这些特定字节。请仔细注意这一点;您将再次看到此材料。当意大利式抽水马桶式管道盛宴远端的 bash 终于开始运行时,它将从管道(即其标准输入)读取“部分”数据(因为这是 bash 在不带参数且不将 tty 作为标准输入的情况下调用时所做的工作)。通过 的魔力
strace
,我刚刚确认 bash 确实每次读取一个字符的输入(而不是以 4k 个块的形式读取),因此每个不属于 bash 已执行或当前正在执行的命令的单个字符仍将位于管道(bash 已将其作为标准输入)中。当 bash 执行脚本中的第二条命令
apt-get install
tra la la 时,它会派生一个新进程。该进程继承了 bash 的所有文件描述符,包括(最重要的是)我们的好朋友标准输入管道。然后,这也发生在任何apt-get
分叉的进程上(我向你保证,分叉的进程相当多)。其中一个进程或其apt-get
本身决定读取 stdin,并将读取的内容写入 stdout(或可能是 stderr)。当
apt-get install
完成后,bash 通过再次读取 stdin 来找出下一步要执行的操作。因为有些别的已经从管道中读出所有内容,但是,什么也没有剩下,bash 认为“哦,好吧,我想我已经完成了”并退出。同样,管道是空的,因为其他东西已经将其读干,并且共享单个文件描述符的所有东西都共享其中的赏金。
解决“共享 stdin”问题的方法,毫不奇怪,就是停止像兄弟会派对上的水烟筒一样传递 stdin。由于你无法阻止fork
(2) 自动向每个人提供相同的文件描述符,因此你需要告诉 bash 给apt-get
(以及从那个甜蜜的管道中非法啜饮的任何其他东西)一些东西别的而不是继续吸食。最容易给出的是/dev/null
——那个永远忠实、永不满足的、所有“戴夫不在,伙计”乐趣的来源。这就是“输入重定向”的领域,这就是所做的</dev/null
——它说,“嘿,bash,在你exec
这样做之前apt-get
,用你从打开”获得的文件描述符替换掉 stdin(文件描述符 0)/dev/null
。
最后给读者做一个练习:尝试在命令</dev/zero
后面加上apt-get install
,并解释为什么会发生这种情况。