我将以下简单的 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 命令的下载部分完成后立即显示...
有谁知道这可能是什么原因造成的?
哦,这是一个有趣的。
简短的回答:它发生在那里是因为 apt(或它派生的东西)在其执行时正在读取标准输入,并且它读取脚本的其余行,因为那是当时仍在标准输入中的内容。简短的解决方法:放在行
</dev/null
尾apt-get install
,然后继续您的一天。长答案(说真的,这是一个大枪):从正在运行的进程的角度来看,stdin/stdout/stderr 没有什么特别之处。它们只是文件描述符,当它们被分叉时,文件描述符在进程之间共享。所以,发生的事情(或多或少)是:
在终端中以交互方式运行的 bash 副本打开一个新的
pipe
(2),然后分叉一个新进程,该进程关闭现有的标准输出,然后使标准输出文件描述符 (1) 成为管道的写入端(参见dup2
(2)) . 然后该子进程exec
scat sample.sh
读取文件并将其写入它认为是标准输出的内容(但实际上是管道的写入端)。在您的终端中以交互方式运行的 bash 副本分叉另一个新进程,这次关闭现有的stdin,然后使 stdin 文件描述符 (0) 成为前面讨论的同一管道的读取器端(再次调用
dup2
)。然后这个过程就是exec
你的lxc-attach
过程。如果在此过程中没有任何干扰标准输入(在这种特殊情况下不会),那么从将管道的读取器端作为标准输入的进程派生的每个进程也将具有完全相同的文件描述符,附加到与它的标准输入相同的管道,其中
sample.sh
填充了内容。 从该文件描述符读取的任何进程现在都将消耗读取的字节,并且从该文件描述符读取的任何其他进程都不会获得这些特定字节。请注意这一点;您将再次看到此材料。当你的意大利马桶式管道盛会远端的 bash 终于开始时,它会从管道中读取“一些”数据,这是它的标准输入(因为这就是 bash 所做的,当没有参数和没有参数调用时)一个 tty 作为标准输入)。通过 的魔力
strace
,我刚刚确认 bash 实际上一次读取一个字符(而不是读取它,比如 4k 块),所以每个不属于 bash 的命令的单个字符,或当前正在执行,仍将位于管道中,其中-bash-has-as-its-stdin。当 bash 执行脚本中的第二个命令
apt-get install
tra la la 时,它会派生一个新进程。它继承了 bash 的所有文件描述符,包括(最重要的是)我们的好朋友 pipe-which-is-stdin。这种情况也会发生在任何分叉的进程上apt-get
(我向你保证,很多)。其中之一或apt-get
它本身决定读取标准输入并将它读取的任何内容写入标准输出(或可能是标准错误)。完成
apt-get install
后,bash 通过再次读取 stdin 来找出接下来要执行的操作。但是,因为其他东西已经从管道中读取了所有内容,所以什么都没有了,并且 bash 表示“哦,好吧,我想我已经完成了”并退出。再一次,管道是空的,因为其他东西已经读干了它,并且共享单个文件描述符的所有内容都共享了它的赏金。不出所料,“共享标准输入”问题的解决方案是停止在兄弟会聚会上像烟枪一样传递标准输入。由于您无法阻止
fork
(2) 自动为每个人提供相同的文件描述符,因此您需要告诉 bash 给apt-get
(以及其他任何从该甜蜜、甜蜜的管道中非法啜饮的东西)其他东西来代替. 最容易给予的是/dev/null
——永远忠实、永远不完整,你所有“戴夫不在这里,伙计”的快乐源泉。那是“输入重定向”的域,这就是它的</dev/null
作用——它说,“嘿,bash,在你之前exec
,apt-get
用你从打开得到的文件描述符交换标准输入(文件描述符0)/dev/null
”。供读者完成的练习:尝试
</dev/zero
在apt-get install
命令之后放置,并解释为什么会发生这种情况。