前情提要
昨天同事问到,如果jar中包含通过system.in获取标准输入的代码时,这个jar包是否可以配合nohup使用?这个问题第一感觉是可以,因为nohup让程序可以永久的执行下去,即便关闭终端,jar依然可以运行,只是无法获取标准输入了。然而仔细研究下去,发现真实情况和预想的有些出入。
测试代码
首先写一个java程序来作为我们实验的素材:
package my.in;
import java.io.IOException;
public class InTest {
public static void main(String[] args) throws IOException {
for(;;) {
System.out.print("Enter a Char:");
char i = (char) System.in.read();
System.out.println("your char is :"+i);
}
}
}
我们从终端读取输入并输出。打个JAR包来试验一下:
$ java -jar java-in-test.jar
Enter a Char:a
your char is :a
运行情况良好,然后加入nohup:
$ nohup java -jar java-in-test.jar
nohup: 忽略输入并把输出追加到"nohup.out"
看起来好像没有问题。然而通过ps命令查看后,并没有这个进程,查看nohup.out时发现抛出了异常信息:
$ more nohup.out
Enter a Char:Exception in thread “main” java.io.IOException: 错误的文件描述符
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:255)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
at java.io.BufferedInputStream.read(BufferedInputStream.java:265)
at my.in.InTest.main(InTest.java:10)
实际情况与之前预想的并不一样。
nohup详解
在man中,nohup解释如下:
nohup - 使程序运行时不挂起, 不向 tty 输出信息
然而,在实际使用时,nohup其实做了三件事。阅读nohup源码可知:
ignoring_input = isatty (STDIN_FILENO);
redirecting_stdout = isatty (STDOUT_FILENO);
stdout_is_closed = (!redirecting_stdout && errno == EBADF);
redirecting_stderr = isatty (STDERR_FILENO);
首先,代码判断了标准输入、标准输出、标准错误输出是否为终端。如果标准输入是终端,则将其重定向为 /dev/null,并且文件打开方式置为只写,这样如果试图从中读取时会引发错误。这也就解释了之前我们执行nohup java -jar java-in-test.jar命令时抛出了异常信息。
/* If standard input is a tty, replace it with /dev/null if possible.
Note that it is deliberately opened for *writing*,
to ensure any read evokes an error. */
if (ignoring_input)
{
if (fd_reopen (STDIN_FILENO, "/dev/null", O_WRONLY, 0) < 0)
error (exit_internal_failure, errno,
_("failed to render standard input unusable"));
if (!redirecting_stdout && !redirecting_stderr)
error (0, 0, _("ignoring input"));
}
如果标准输出和标准错误输出是终端,则将其重定向到当前目录下的nohup.out文件,如果文件打开失败,则重定向到HOME目录下的nohup.out文件。标准错误输出重定向到同一个文件中。
/* If standard output is a tty, redirect it (appending) to a file.
First try nohup.out, then $HOME/nohup.out. If standard error is
a tty and standard output is closed, open nohup.out or
$HOME/nohup.out without redirecting anything. */
if (redirecting_stdout || (redirecting_stderr && stdout_is_closed))
{
char *in_home = NULL;
char const *file = "nohup.out";
int flags = O_CREAT | O_WRONLY | O_APPEND;
mode_t mode = S_IRUSR | S_IWUSR;
mode_t umask_value = umask (~mode);
out_fd = (redirecting_stdout
? fd_reopen (STDOUT_FILENO, file, flags, mode)
: open (file, flags, mode));
if (out_fd < 0)
{
int saved_errno = errno;
char const *home = getenv ("HOME");
if (home)
{
in_home = file_name_concat (home, file, NULL);
out_fd = (redirecting_stdout
? fd_reopen (STDOUT_FILENO, in_home, flags, mode)
: open (in_home, flags, mode));
}
if (out_fd < 0)
{
int saved_errno2 = errno;
error (0, saved_errno, _("failed to open %s"), quoteaf (file));
if (in_home)
error (0, saved_errno2, _("failed to open %s"),
quoteaf (in_home));
return exit_internal_failure;
}
file = in_home;
}
umask (umask_value);
error (0, 0,
_(ignoring_input
? N_("ignoring input and appending output to %s")
: N_("appending output to %s")),
quoteaf (file));
free (in_home);
}
/* If standard error is a tty, redirect it. */
if (redirecting_stderr)
{
/* Save a copy of stderr before redirecting, so we can use the original
if execve fails. It's no big deal if this dup fails. It might
not change anything, and at worst, it'll lead to suppression of
the post-failed-execve diagnostic. */
saved_stderr_fd = fcntl (STDERR_FILENO, F_DUPFD_CLOEXEC,
STDERR_FILENO + 1);
if (!redirecting_stdout)
error (0, 0,
_(ignoring_input
? N_("ignoring input and redirecting stderr to stdout")
: N_("redirecting stderr to stdout")));
if (dup2 (out_fd, STDERR_FILENO) < 0)
error (exit_internal_failure, errno,
_("failed to redirect standard error"));
if (stdout_is_closed)
close (out_fd);
}
同时,nohup命令会阻止SIGHUP信号。
signal (SIGHUP, SIG_IGN);
&详解
在命令最后添加&时,命令会转入后台运行。查看任务控制源码如下:
/* If the last arg on the line is '&', then start this job in the
background. Else, fg the job. */
for (t = list; t && t->next; t = t->next)
;
fg_bit = (t && t->word->word[0] == '&' && t->word->word[1] == '\0') == 0;
return (fg_bg (list, fg_bit));
转入后台的任务会阻断标准输入,如果程序试图读取,会被挂起。注意,后台任务的标准输出不会阻断。
$ java -jar java-in-test.jar &
[2] 3690
$ Enter a Char:
[2] + 3690 suspended (tty input) java -jar java-in-test.jar
$ jobs -l
[1] - 7610 suspended (tty output) man
[2] + 3690 suspended (tty input) java -jar java-in-test.jar
通过命令fg %job_number可将任务调到前台运行,在本例中,job_number是2。注意此处不要和进程号3690混淆。
$ fg %2
关闭当前shell,任务即被注销。
将nohup和&结合起来使用,可将任务转入后台永久运行,不受SIGHUP信号的影响,即关闭终端程序依然可以运行。但是在本例中,运行会抛出异常,因为nohup重定向了标准输入。如果有类似的需求,可以通过创建socket连接、显式重定向输入为文件等方式来实现。
$ nohup java -jar java-in-test.jar &
[1] 6308
nohup: 忽略输入并把输出追加到"nohup.out"
$
[1] + 6308 exit 1 nohup java -jar java-in-test.jar
$ more nohup.out
Enter a Char:Exception in thread “main” java.io.IOException: 错误的文件描述符
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:255)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
at java.io.BufferedInputStream.read(BufferedInputStream.java:265)
at my.in.InTest.main(InTest.java:11)
需注意,关闭终端后,任务无法再通过fg调出。