在本章中,我们将介绍以下配方:
- 产生一个新进程
- 将进程输出和错误流重定向到文件
- 更改子流程的工作目录
- 为子流程设置环境变量
- 运行 Shell 脚本
- 获取当前 JVM 的进程信息
- 获取衍生进程的进程信息
- 管理衍生流程
- 枚举系统中的活动进程
- 使用管道连接多个进程
- 管理子流程
您多久编写一次生成新进程的代码?不经常。然而,可能存在需要编写此类代码的情况。在这种情况下,您必须求助于使用第三方 API,如 ApacheCommonsExec 等。为什么会这样?JavaAPI 还不够吗?不,不是;至少要等到 Java9。现在,使用 Java9 和更高版本,我们在流程 API 中添加了更多的特性。
在 Java7 之前,重定向输入、输出和错误流并不简单。Java7 引入了新的 API,允许将输入、输出和错误重定向到其他进程(管道)、文件或标准输入/输出。然后,在 Java8 中,又引入了一些 API。在 Java 9 中,现在有用于以下领域的新 API:
- 获取流程信息,如进程 ID(PID)、启动流程的用户、流程运行时间等
- 枚举系统中运行的进程
- 管理子流程并通过向上导航流程层次结构来访问流程树
在本章中,我们将介绍一些配方,这些配方将帮助您探索流程 API 中的所有新内容,您还将了解自Runtime.getRuntime().exec()
时代以来所引入的变化。你们都知道使用它是一种犯罪。
所有这些秘籍只能在 Linux 平台上执行,因为我们将在从 Java 代码生成新进程时使用 Linux 特定的命令。 在 Linux 上有两种执行脚本run.sh
的方法:
sh run.sh
chmod +x run.sh && ./run.sh
Windows 10 上的用户不必担心,因为 Microsoft 已经发布了 Windows Linux 子系统,它允许您在 Windows 上运行自己喜欢的 Linux 发行版,如 Ubuntu、OpenSuse 等。有关更多详细信息,请查看此链接。
在这个配方中,我们将看到如何使用ProcessBuilder
生成一个新流程。我们还将看到如何利用输入、输出和错误流。这应该是一个非常简单和常见的配方。然而,介绍这一点的目的是使本章更加完整,而不仅仅是关注 Java9 特性。
Linux 中有一个名为free
的命令,显示可用 RAM 的数量以及系统正在使用的 RAM 数量。它接受一个选项-m
,以兆字节为单位显示输出。因此,仅运行free -m
就提供了以下输出:
我们将在 Java 程序中运行前面的代码。
遵循以下步骤:
- 通过提供所需的命令及其选项,创建
ProcessBuilder
的实例:
ProcessBuilder pBuilder = new ProcessBuilder("free", "-m");
指定命令和选项的另一种方法如下:
pBuilder.command("free", "-m");
- 为进程构建器和其他属性(如执行目录和环境变量)设置输入和输出流。之后,在
ProcessBuilder
实例上调用start()
生成流程并获取对Process
对象的引用:
Process p = pBuilder.inheritIO().start();
inheritIO()
函数将派生子进程的标准 I/O 设置为与当前 Java 进程的标准 I/O 相同。
- 然后,我们等待流程完成,或等待一秒钟(以较早者为准),如以下代码所示:
if(p.waitFor(1, TimeUnit.SECONDS)){
System.out.println("process completed successfully");
}else{
System.out.println("waiting time elapsed, process did
not complete");
System.out.println("destroying process forcibly");
p.destroyForcibly();
}
如果这没有在指定的时间内完成,那么我们通过调用destroyForcibly()
方法终止该进程。
- 使用以下命令编译并运行代码:
$ javac -d mods --module-source-path src
$(find src -name *.java)
$ java -p mods -m process/com.packt.process.NewProcessDemo
- 我们得到的结果如下:
此配方的代码可在Chapter08/1_spawn_new_process
中找到。
有两种方式让ProcessBuilder
知道要运行哪个命令:
- 在创建
ProcessBuilder
对象时,将命令及其选项传递给构造函数 - 将命令及其选项作为参数传递给
ProcessBuilder
对象的command()
方法
在生成流程之前,我们可以执行以下操作:
- 我们可以使用
directory()
方法更改执行目录。 - 我们可以将输入流、输出流和错误流重定向到文件或另一个进程。
- 我们可以为子流程提供所需的环境变量。
在本章中,我们将在各自的食谱中看到所有这些活动。
当调用start()
方法并且调用者以Process
类的实例的形式获取对此子流程的引用时,将生成一个新流程。使用这个Process
对象,我们可以做很多事情,比如:
- 获取有关进程的信息,包括其 PID
- 获取输出和错误流
- 检查流程是否完成
- 破坏过程
- 将流程完成后要执行的任务关联起来
- 检查该进程生成的子进程
- 查找进程的父进程(如果存在)
在我们的食谱中,我们需要waitFor
一秒钟,或者完成整个过程(以先发生的为准)。如果流程已经完成,waitFor
返回true
;否则返回false
。如果流程没有完成,我们可以通过调用Process
对象上的destroyForcibly()
方法来终止流程。
在这个配方中,我们将看到如何处理由 Java 代码生成的进程的输出和错误流。我们将把生成的进程产生的输出或错误写入一个文件。
在这个配方中,我们将使用iostat
命令。此命令用于报告不同设备和分区的 CPU 和 I/O 统计信息。让我们运行命令,看看它报告了什么:
$ iostat
在一些 Linux 发行版中,例如 Ubuntu,默认情况下不安装iostat
。您可以通过运行sudo apt-get install sysstat
来安装该实用程序。
上述命令的输出如下所示:
遵循以下步骤:
- 通过指定要执行的命令来创建新的
ProcessBuilder
对象:
ProcessBuilder pb = new ProcessBuilder("iostat");
- 将输出和错误流分别重定向到文件的输出和错误:
pb.redirectError(new File("error"))
.redirectOutput(new File("output"));
- 启动流程并等待其完成:
Process p = pb.start();
int exitValue = p.waitFor();
- 读取输出文件的内容:
Files.lines(Paths.get("output"))
.forEach(l -> System.out.println(l));
- 读取错误文件的内容。仅当命令中存在错误时,才会创建此命令:
Files.lines(Paths.get("error"))
.forEach(l -> System.out.println(l));
步骤 4 和 5 供我们参考。这与ProcessBuilder
或产生的过程无关。使用这两行代码,我们可以检查进程写入输出和错误文件的内容。
完整代码可在Chapter08/2_redirect_to_file
找到。
- 使用以下命令编译代码:
$ javac -d mods --module-source-path src $(find src -name
*.java)
- 使用以下命令运行代码:
$ java -p mods -m process/com.packt.process.RedirectFileDemo
我们将获得以下输出:
我们可以看到,当命令成功执行时,错误文件中没有任何内容。
您可以向ProcessBuilder
提供错误的命令,然后看到错误被写入错误文件,而输出文件中没有任何内容。您可以按如下方式更改ProcessBuilder
实例创建:
ProcessBuilder pb = new ProcessBuilder("iostat", "-Z");
使用前面的“操作方法”中给出的命令编译并运行。
您将看到错误文件中报告了一个错误,但输出文件中没有报告任何错误:
通常,您希望在路径的上下文中执行进程,例如列出目录中的文件。为此,我们必须告诉ProcessBuilder
在给定位置的上下文中启动流程。我们可以使用directory()
方法来实现这一点。这种方法有两个目的:
- 当我们不传递任何参数时,它返回当前的执行目录。
- 当我们传递参数时,它将当前执行目录设置为传递的值。
在这个配方中,我们将看到如何执行
tree
递归遍历当前目录中所有目录并以树的形式打印的命令。
通常,tree
命令不是预先安装的,因此您必须安装包含该命令的软件包。要在基于 Ubuntu/Debian 的系统上安装,请运行以下命令:
$ sudo apt-get install tree
要在支持yum
软件包管理器的 Linux 上安装,请运行以下命令:
$ yum install tree
要验证您的安装,只需运行tree
命令,您应该能够看到打印的当前目录结构。对我来说,是这样的:
tree
命令支持多个选项。这是供你探索的。
遵循以下步骤:
- 创建一个新的
ProcessBuilder
对象:
ProcessBuilder pb = new ProcessBuilder();
- 将命令设置为
tree
,输出和错误与当前 Java 进程相同:
pb.command("tree").inheritIO();
- 将目录设置为所需的任何目录。我将其设置为根文件夹:
pb.directory(new File("/root"));
- 启动进程并等待其退出:
Process p = pb.start();
int exitValue = p.waitFor();
- 使用以下命令编译并运行:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.ChangeWorkDirectoryDemo
- 输出将是目录的递归内容,在
ProcessBuilder
对象的directory()
方法中指定,以树状格式打印。
完整代码可在Chapter08/3_change_work_directory
找到。
directory()
方法接受Process
的工作目录路径。路径被指定为File
的实例。
环境变量与编程语言中的任何其他变量一样。它们有一个名称并具有一些值,这些值可以变化。Linux/Windows 命令或 Shell/batch 脚本使用这些命令来执行不同的操作。这些被称为环境变量,因为它们存在于正在执行的进程/命令/脚本的环境中。通常,流程从父流程继承环境变量。
它们在不同的操作系统中以不同的方式访问。在 Windows 中,它们作为%ENVIRONMENT_VARIABLE_NAME%
访问,在基于 Unix 的操作系统中,它们作为$ENVIRONMENT_VARIABLE_NAME
访问。
在基于 Unix 的系统中,您可以使用printenv
命令打印进程可用的所有环境变量,在基于 Windows 的系统中,您可以使用SET
命令。
在此配方中,我们将向子流程传递一些环境变量,并使用printenv
命令打印所有可用的环境变量。
遵循以下步骤:
- 创建
ProcessBuilder
的实例:
ProcessBuilder pb = new ProcessBuilder();
- 将命令设置为
printenv
,输出和错误流与当前 Java 进程相同:
pb.command("printenv").inheritIO();
- 为环境变量
COOKBOOK_VAR1
提供值First variable
、COOKBOOK_VAR2
提供值Second variable
、COOKBOOK_VAR3
提供值Third variable
:
Map<String, String> environment = pb.environment();
environment.put("COOKBOOK_VAR1", "First variable");
environment.put("COOKBOOK_VAR2", "Second variable");
environment.put("COOKBOOK_VAR3", "Third variable");
- 启动流程并等待其完成:
Process p = pb.start();
int exitValue = p.waitFor();
此配方的完整代码可在Chapter08/4_environment_variables
中找到。
- 使用以下命令编译并运行代码:
$ javac -d mods --module-source-path src $(find src -name
*.java)
$ java -p mods -m
process/com.packt.process.EnvironmentVariableDemo
您得到的输出如下所示:
您可以看到在其他变量中打印的三个变量。
当您在ProcessBuilder
实例上调用environment()
方法时,它会复制当前流程的环境变量,将它们填充到HashMap
实例中,并将其返回给调用方代码。
加载环境变量的所有工作都由包私有的最终类ProcessEnvironment
完成,它实际上扩展了HashMap
。
然后,我们使用这个映射来填充我们自己的环境变量,但我们不需要将映射设置回ProcessBuilder
,因为我们将有一个对映射对象的引用,而不是一个副本。对映射对象所做的任何更改都将反映在ProcessBuilder
实例所持有的实际映射对象中。
我们通常在文件中收集一组用于执行操作的命令,在 Unix 世界中称为shell 脚本,在 Windows 中称为批处理文件。这些文件中的命令是按顺序执行的,但脚本中有条件块或循环时除外。
这些 Shell 脚本由执行它们的 Shell 进行评估。可用的不同类型的外壳有bash
、csh
、ksh
等。bash
外壳是最常用的外壳。
在这个方法中,我们将编写一个简单的 Shell 脚本,然后使用ProcessBuilder
和Process
对象从 Java 代码中调用相同的脚本。
首先,让我们编写 Shell 脚本。此脚本执行以下操作:
- 打印环境变量
MY_VARIABLE
的值 - 执行
tree
命令 - 执行
iostat
命令
让我们创建一个名为script.sh
的 Shell 脚本文件,其中包含以下命令:
echo $MY_VARIABLE;
echo "Running tree command";
tree;
echo "Running iostat command"
iostat;
您可以将script.sh
放在您的主文件夹中;就是在/home/<username>
中。现在,让我们看看如何从 Java 执行此操作。
遵循以下步骤:
- 创建
ProcessBuilder
的新实例:
ProcessBuilder pb = new ProcessBuilder();
- 将执行目录设置为指向 Shell 脚本文件的目录:
pb.directory(new File("/root"));
请注意,创建File
对象时传递的上一条路径将取决于脚本script.sh
的放置位置。在我们的案例中,我们将其放置在/root
中。您可能已经在/home/yourname
中复制了脚本,因此File
对象将被创建为newFile("/home/yourname")
。
- 设置 Shell 脚本将使用的环境变量:
Map<String, String> environment = pb.environment();
environment.put("MY_VARIABLE", "Set by Java process");
- 设置要执行的命令,以及要传递给该命令的参数。另外,将进程的输出流和错误流设置为与当前 Java 进程的输出流和错误流相同:
pb.command("/bin/bash", "script.sh").inheritIO();
- 启动流程并等待其完全执行:
Process p = pb.start();
int exitValue = p.waitFor();
您可以从Chapter08/5_running_shell_script
获取完整的代码。
可以使用以下命令编译和运行代码:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.RunningShellScriptDemo
我们得到的结果如下:
你必须在食谱中记下两件事:
- 将进程的工作目录更改为 Shell 脚本的位置。
- 使用
/bin/bash
执行 Shell 脚本。
如果没有记下步骤 1,则必须使用 Shell 脚本文件的绝对路径。然而,在这个配方中,我们确实做到了这一点,因此我们只为/bin/bash
命令使用 Shell 脚本名称。
步骤 2 基本上是您希望如何执行 Shell 脚本。这样做的方法是将 Shell 脚本传递给解释器,解释器将解释并执行脚本。以下代码行就是这样做的:
pb.command("/bin/bash", "script.sh")
正在运行的进程具有一组与其关联的属性,例如:
- PID:唯一标识流程
- 所有者:启动流程的用户名称
- 命令:流程下运行的命令
- CPU 时间:表示进程处于活动状态的时间
- 启动时间:表示流程启动的时间
这些是我们通常感兴趣的几个属性。也许我们也会对 CPU 使用或内存使用感兴趣。现在,在 Java9 之前,从 Java 内部获取这些信息是不可能的。然而,在 Java9 中,引入了一组新的 API,这使我们能够获得有关流程的基本信息。
在这个配方中,我们将看到如何获取当前 Java 进程的进程信息;也就是说,执行代码的进程。
遵循以下步骤:
- 创建一个简单类,使用
ProcessHandle.current()
获取当前 Java 进程的ProcessHandle
:
ProcessHandle handle = ProcessHandle.current();
- 我们添加了一些代码,这将为代码增加一些运行时间:
for ( int i = 0 ; i < 100; i++){
Thread.sleep(1000);
}
- 在
ProcessHandle
实例上使用info()
方法得到ProcessHandle.Info
实例:
ProcessHandle.Info info = handle.info();
- 使用
ProcessHandle.Info
实例获取界面提供的所有信息:
System.out.println("Command line: " +
info.commandLine().get());
System.out.println("Command: " + info.command().get());
System.out.println("Arguments: " +
String.join(" ", info.arguments().get()));
System.out.println("User: " + info.user().get());
System.out.println("Start: " + info.startInstant().get());
System.out.println("Total CPU Duration: " +
info.totalCpuDuration().get().toMillis() +"ms");
- 使用
ProcessHandle
的pid()
方法获取当前 Java 进程的进程 ID:
System.out.println("PID: " + handle.pid());
- 我们还将使用代码即将结束的时间打印结束时间。这将使我们了解流程的执行时间:
Instant end = Instant.now();
System.out.println("End: " + end);
您可以从Chapter08/6_current_process_info
获取完整的代码。
使用以下命令编译并运行代码:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.CurrentProcessInfoDemo
您看到的输出如下所示:
程序完成执行需要一些时间。要进行的一项观察是,即使程序运行了大约两分钟,总 CPU 持续时间也是 350 毫秒。 这是 CPU 忙碌的时间段。
为了更好地控制本机进程并获取其信息,Java API 中添加了一个名为ProcessHandle
的新接口。使用ProcessHandle
可以控制流程的执行,也可以获取流程的一些信息。该接口有另一个名为ProcessHandle.Info
的内部接口。此接口提供 API 以获取有关流程的信息。
有多种方法可以为流程获取ProcessHandle
对象。其中一些方法如下:
ProcessHandle.current()
:用于获取当前 Java 进程的ProcessHandle
实例。Process.toHandle()
:用于获取给定Process
对象的ProcessHandle
。ProcessHandle.of(pid)
:用于获取给定 PID 标识的流程的ProcessHandle
。
在我们的食谱中,我们使用第一种方法,即使用ProcessHandle.current()
。这为我们提供了当前 Java 进程的句柄。在ProcessHandle
实例上调用info()
方法将为我们提供一个ProcessHandle.Info
接口实现的实例,我们可以利用它来获取流程信息,如配方代码所示。
ProcessHandle
和ProcessHandle.Info
是接口。 JDK 提供 Oracle JDK 或 Open JDK,将为这些接口提供实现。 Oracle JDK 有一个名为ProcessHandleImpl
的类,它实现了ProcessHandle
,而ProcessHandleImpl
中的另一个内部类称为Info
,它实现了ProcessHandle.Info
接口。 因此,每当您调用上述方法之一来获取ProcessHandle
对象时,都会返回一个ProcessHandleImpl
的实例。
Process
类也是如此。 它是一个抽象类,Oracle JDK 提供了一个名为ProcessImpl
的实现,它实现了Process
类中的抽象方法。
在本章的所有秘籍中,任何对ProcessHandle
实例或ProcessHandle
对象的提及都将引用ProcessHandleImpl
或您正在使用的 JDK 提供的任何其他实现类的实例或对象。
此外,任何对ProcessHandle.Info
实例或ProcessHandle.Info
对象的提及都将引用ProcessHandleImpl.Info
或您正在使用的 JDK 提供的任何其他实现类的实例或对象。
在前面的配方中,我们看到了如何获取当前 Java 进程的进程信息。在本食谱中,我们将了解如何获取 Java 代码生成的进程的进程信息;也就是说,通过当前的 Java 进程。除了ProcessHandle
实例的实现方式外,使用的 API 将与我们在前面的配方中看到的相同。
在此配方中,我们将使用 Unix 命令sleep
,该命令用于暂停执行一段时间(以秒为单位)。
遵循以下步骤:
- 从 Java 代码生成一个新进程,该进程运行
sleep
命令:
ProcessBuilder pBuilder = new ProcessBuilder("sleep", "20");
Process p = pBuilder.inheritIO().start();
- 获取此衍生进程的
ProcessHandle
实例:
ProcessHandle handle = p.toHandle();
- 等待生成的进程完成执行:
int exitValue = p.waitFor();
- 使用
ProcessHandle
获取ProcessHandle.Info
实例,并使用其 API 获取所需信息。或者,我们甚至可以使用Process
类中的info()
方法直接使用Process
对象来获取ProcessHandle.Info
:
ProcessHandle.Info info = handle.info();
System.out.println("Command line: " +
info.commandLine().get());
System.out.println("Command: " + info.command().get());
System.out.println("Arguments: " + String.join(" ",
info.arguments().get()));
System.out.println("User: " + info.user().get());
System.out.println("Start: " + info.startInstant().get());
System.out.println("Total CPU time(ms): " +
info.totalCpuDuration().get().toMillis());
System.out.println("PID: " + handle.pid());
您可以从Chapter08/7_spawned_process_info
获取完整的代码。
使用以下命令编译并运行代码:
$ javac -d mods --module-source-path src $(find src -name *.java)
$ java -p mods -m process/com.packt.process.SpawnedProcessInfoDemo
或者,Chapter08/7_spawned_process_info
中有一个run.sh
脚本,您可以在任何基于 Unix 的系统上以/bin/bash run.sh
的形式运行该脚本。
您看到的输出如下所示:
有几种方法,例如destroy()
、destroyForcibly()
(在 Java 8 中添加)、isAlive()
(在 Java 8 中添加)和supportsNormalTermination()
(在 Java 9 中添加),可以用来控制生成的进程。这些方法在Process
对象和ProcessHandle
对象上都可用。在这里,控制只是检查进程是否处于活动状态,如果处于活动状态,则销毁进程。
在此配方中,我们将生成一个长时间运行的流程,并执行以下操作:
- 检查它是否活泼
- 检查是否能正常停止;也就是说,根据平台的不同,必须使用销毁或强制销毁来停止该过程
- 停止这个过程
- 从 Java 代码生成一个新进程,该进程运行
sleep
命令一分钟或 60 秒:
ProcessBuilder pBuilder = new ProcessBuilder("sleep", "60");
Process p = pBuilder.inheritIO().start();
- 等待,比如说,10 秒钟:
p.waitFor(10, TimeUnit.SECONDS);
- 检查进程是否处于活动状态:
boolean isAlive = p.isAlive();
System.out.println("Process alive? " + isAlive);
- 检查过程是否能正常停止:
boolean normalTermination = p.supportsNormalTermination();
System.out.println("Normal Termination? " + normalTermination);
- 停止进程并检查其活力:
p.destroy();
isAlive = p.isAlive();
System.out.println("Process alive? " + isAlive);
您可以从Chapter08/8_manage_spawned_process
获取完整的代码。
我们提供了一个名为run.sh
的实用程序脚本,您可以使用它编译和运行代码-sh run.sh
。
我们得到的结果如下:
如果我们在 Windows 上运行该程序,supportsNormalTermination()
返回false
,但在 Unix 上supportsNormalTermination()
返回true
(如前面的输出所示)。
在 Windows 中,您可以打开 Windows 任务管理器来查看当前活动的进程,在 Linux 中,您可以使用带有各种选项的ps
命令来查看进程以及其他详细信息,如用户、花费的时间、命令等。
在 Java9 中,添加了一个名为ProcessHandle
的新 API,用于控制和获取有关进程的信息。API 的方法之一是allProcesses()
,它返回当前进程可见的所有进程的快照。在本食谱中,我们将了解该方法是如何工作的,以及我们可以从 API 中提取哪些信息。
遵循以下步骤:
- 使用
ProcessHandle
界面上的allProcesses()
方法获取当前活动进程的流:
Stream<ProcessHandle> liveProcesses =
ProcessHandle.allProcesses();
- 使用
forEach()
在流上迭代,并传递 Lambda 表达式以打印可用的详细信息:
liveProcesses.forEach(ph -> {
ProcessHandle.Info phInfo = ph.info();
System.out.println(phInfo.command().orElse("") +" " +
phInfo.user().orElse(""));
});
您可以从Chapter08/9_enumerate_all_processes
获取完整的代码。
我们提供了一个名为run.sh
的实用程序脚本,您可以使用它编译和运行代码-sh run.sh
。
我们得到的结果如下:
在前面的输出中,我们正在打印命令名以及进程的用户。我们展示了输出的一小部分。
在 Unix 中,通常使用|
符号将一组命令通过管道连接在一起,以创建活动管道,其中命令的输入是前一个命令的输出。这样,我们可以处理输入以获得所需的输出。
常见的情况是,您希望在日志文件中搜索某个内容或模式,或在日志文件中搜索某个文本。在这种情况下,您可以创建一个管道,通过一系列命令传递所需的日志文件数据,即,cat
、grep
、wc -l
等等。
在本配方中,我们将使用 UCI 机器学习库中的鸢尾花数据集,该数据集位于这里。创建一个管道,我们将计算每种花的出现次数。
我们已经下载了鸢尾花数据集 ),可在本书代码下载的Chapter08/10_connecting_process_pipe/iris.data
处找到。
如果您碰巧看到Iris
数据,您将看到以下格式的 150 行:
4.7,3.2,1.3,0.2,Iris-setosa
这里有多个属性用逗号(,
分隔,属性如下:
- 萼片长度(厘米)
- 萼片宽度(厘米)
- 花瓣长度(厘米)
- 花瓣宽度(厘米)
- 类别:
- 山鸢尾
- 花色鸢尾
- 弗吉尼亚鸢尾
在这个食谱中,我们将找到每一类花的总数,即刚毛、杂色和弗吉尼亚。
我们将通过以下命令使用管道(使用基于 Unix 的操作系统):
$ cat iris.data.txt | cut -d',' -f5 | uniq -c
我们得到的结果如下:
50 Iris-setosa
50 Iris-versicolor
50 Iris-virginica
1
末尾的 1 表示文件末尾的新行。因此,每个班级有 50 朵鲜花。让我们仔细分析上面的 Shell 命令管道,了解它们各自的功能:
cat
:此命令读取作为参数给出的文件。cut
:使用-d
选项中给出的字符分割每一行,并返回-f
选项标识的列中的值。uniq
:返回给定值的唯一列表,当使用-c
选项时,返回每个唯一值在列表中出现的次数。
- 创建一个
ProcessBuilder
对象列表,它将保存参与我们管道的ProcessBuilder
实例。另外,将管道中最后一个进程的输出重定向到当前 Java 进程的标准输出:
List<ProcessBuilder> pipeline = List.of(
new ProcessBuilder("cat", "iris.data.txt"),
new ProcessBuilder("cut", "-d", ",", "-f", "5"),
new ProcessBuilder("uniq", "-c")
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
);
- 使用
ProcessBuilder
的startPipeline()
方法,通过ProcessBuilder
对象列表启动管线。它将返回一个Process
对象列表,每个对象代表列表中的一个ProcessBuilder
对象:
List<Process> processes = ProcessBuilder.startPipeline(pipeline);
- 获取列表中的最后一个流程并
waitFor
完成:
int exitValue = processes.get(processes.size() - 1).waitFor();
您可以从Chapter08/10_connecting_process_pipe
获取完整的代码。
我们提供了一个名为run.sh
的实用程序脚本,您可以使用它编译和运行代码-sh run.sh
。
我们得到的结果如下:
startPipeline()
方法为列表中的每个ProcessBuilder
对象启动Process
。除了第一个和最后一个进程外,它使用ProcessBuilder.Redirect.PIPE
将一个进程的输出重定向到另一个进程的输入。如果您为任何中间流程提供了redirectOutput
而不是ProcessBuilder.Redirect.PIPE
,则会抛出错误;类似于以下内容:
Exception in thread "main" java.lang.IllegalArgumentException: builder redirectOutput() must be PIPE except for the last builder: INHERIT.
它指出,除最后一个构建器外,任何构建器都应该将其输出重定向到下一个进程。同样适用于redirectInput
。
当一个流程启动另一个流程时,启动的流程将成为启动流程的子流程。启动的流程反过来可以启动另一个流程,并且这个链可以继续。这将生成一个进程树。通常,我们必须处理一个有缺陷的子流程,可能想要杀死该子流程,或者我们可能想要知道启动的子流程,可能想要获得关于它们的一些信息。
在 Java9 中,Process
类中添加了两个新的 API—children()
和descendants()
。children()
API 提供当前流程的直接子流程快照列表,descendants()
API 提供当前流程递归children()
的流程快照;也就是说,它们在每个子进程上递归调用children()
。
在这个配方中,我们将查看children()
和descendants()
API,并查看我们可以从流程快照中收集哪些信息。
让我们创建一个简单的 Shell 脚本,我们将在配方中使用它。此脚本可在Chapter08/11_managing_sub_process/script.sh
找到:
echo "Running tree command";
tree;
sleep 60;
echo "Running iostat command";
iostat;
在前面的脚本中,我们正在运行命令tree
和iostat
,两个命令之间有一分钟的睡眠时间。如果您想了解这些命令,请参考本章的“运行 Shell 脚本”配方。从 bashshell 中执行 sleep 命令时,每次调用它时都会创建一个新的子进程。
例如,我们将创建 10 个ProcessBuilder
实例来运行前面的 Shell 脚本并同时启动它们。
- 我们将创建 10 个
ProcessBuilder
实例来运行我们的 Shell 脚本(可在Chapter08/11_managing_sub_process/script.sh
上找到)。我们不关心它的输出,所以让我们通过将输出重定向到名为ProcessHandle.Redirect.DISCARD
的预定义重定向来丢弃命令的输出:
for ( int i = 0; i < 10; i++){
new ProcessBuilder("/bin/bash", "script.sh")
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.start();
}
- 获取当前进程的句柄:
ProcessHandle currentProcess = ProcessHandle.current();
- 使用当前进程使用
children()
API 获取其子进程,并迭代每个子进程以打印其信息。一旦我们有了ProcessHandle
的实例,我们可以做很多事情,比如销毁流程,获取流程信息,等等:
System.out.println("Obtaining children");
currentProcess.children().forEach(pHandle -> {
System.out.println(pHandle.info());
});
- 使用当前流程,通过使用
descendants()
API 获取其子流程的所有子流程,并迭代每个子流程以打印其信息:
currentProcess.descendants().forEach(pHandle -> {
System.out.println(pHandle.info());
});
您可以从Chapter08/11_managing_sub_process
获取完整的代码。
我们提供了一个名为run.sh
的实用程序脚本,您可以使用它编译和运行代码-sh run.sh
。
我们得到的结果如下:
APIchildren()
和descendants()
为每个进程返回ProcessHandler
的Stream
,这些进程可以是当前进程的直接子进程,也可以是当前进程的后代。使用ProcessHandler
实例,我们可以执行以下操作:
- 获取流程信息
- 检查进程的状态
- 停止这个过程