进程是什么?进程是程序执行的一个实例,比如说,10个用户同时执行IE,那么就有10个独立的进程(尽管他们共享同一个可执行代码)。
进程不是什么?一个没有运行的程序不是一个进程。
进程的特点:每一个进程都有自己的独立的一块内存空间、一组资源系统。其内部数据和状态都是完全独立的。
怎么看待进程?进程的优点是提高CPU运行效率,在同一时间内执行多个程序,即并发执行。但是从严格上讲,也不是绝对的同一时刻执行多个程序,只不过CPU在执行时通过时间片等调度算法不同进程高速切换。进程类似于人类,是被产生的,有或长或短的有效生命,可以产生一个或多个子进程,最终都要消亡的。每个子进程都只有一个父进程。在这里顺带提下,Linux里通过调用fork()函数产生子进程。子进程在创建时,它几乎和父进程相同。它是从父进程的地址空间copy过来的。尽管它们可以共享有程序代码的页,但是它们各自有独立的数据空间。对子进程内存的修改不会影响父进程,反之亦然。
什么是守护进程?在系统的引导的时候会开启很多服务,这些服务就叫做守护进程,也叫后台服务程序,它的生命周期较长,在系统关闭时终止。这个在linux中经常提到init进程,超级守护进程。
Java 进程的建立方法
在 JDK 中,与进程有直接关系的类为 Java.lang.Process,它是一个抽象类。在 JDK 中也提供了一个实现该抽象类的 ProcessImpl 类,如果用户创建了一个进程,那么肯定会伴随着一个新的 ProcessImpl 实例。同时和进程创建密切相关的还有 ProcessBuilder,它是在 JDK1.5 中才开始出现的,相对于 Process 类来说,提供了便捷的配置新建进程的环境,目录以及是否合并错误流和输出流的方式。
Java.lang.Runtime.exec 方法和 Java.lang.ProcessBuilder.start 方法都可以创建一个本地的进程,然后返回代表这个进程的 Java.lang.Process 引用。
Runtime.exec 方法建立一个本地进程
该方法在 JDK1.5 中,可以接受 6 种不同形式的参数传入。
Process exec(String command) Process exec(String [] cmdarray) Process exec(String [] cmdarrag, String [] envp) Process exec(String [] cmdarrag, String [] envp, File dir) Process exec(String cmd, String [] envp) Process exec(String command, String [] envp, File dir)
他们主要的不同在于传入命令参数的形式,提供的环境变量以及定义执行目录。
ProcessBuilder.start 方法来建立一个本地的进程
如果希望在新创建的进程中使用当前的目录和环境变量,则不需要任何配置,直接将命令行和参数传入 ProcessBuilder 中,然后调用 start 方法,就可以获得进程的引用。
Process p = new ProcessBuilder("command", "param").start();
也可以先配置环境变量和工作目录,然后创建进程。
ProcessBuilder pb = new ProcessBuilder("command", "param1", "param2"); Map<String, String> env = pb.environment(); env.put("VAR", "Value"); pb.directory("Dir"); Process p = pb.start();
可以预先配置 ProcessBuilder 的属性是通过 ProcessBuilder 创建进程的最大优点。而且可以在后面的使用中随着需要去改变代码中 pb 变量的属性。如果后续代码修改了其属性,那么会影响到修改后用 start 方法创建的进程,对修改之前创建的进程实例没有影响。
JVM 对进程的实现
在 JDK 的代码中,只提供了 ProcessImpl 类来实现 Process 抽象类。其中引用了 native 的 create, close, waitfor, destory 和 exitValue 方法。在 Java 中,native 方法是依赖于操作系统平台的本地方法,它的实现是用 C/C++ 等类似的底层语言实现。我们可以在 JVM 的源代码中找到对应的本地方法,然后对其进行分析。JVM 对进程的实现相对比较简单,以 Windows 下的 JVM 为例。在 JVM 中,将 Java 中调用方法时的传入的参数传递给操作系统对应的方法来实现相应的功能。如表 1
表 1. JDK 中 native 方法与 Windows API 的对应关系
JDK 中调用的 native 方法名 | 对应调用的 Windows API |
---|---|
create | CreateProcess,CreatePipe |
close | CloseHandle |
waitfor | WaitForMultipleObjects |
destroy | TerminateProcess |
exitValue | GetExitCodeProcess |
以 create 方法为例,我们看一下它是如何和系统 API 进行连接的。
在 ProcessImple 类中,存在 native 的 create 方法,其参数如下:
private native long create(String cmdstr, String envblock, String dir, boolean redirectErrorStream, FileDescriptor in_fd, FileDescriptor out_fd, FileDescriptor err_fd) throws IOException;
在 JVM 中对应的本地方法如代码清单 1 所示 。
清单 1
JNIEXPORT jlong JNICALL Java_Java_lang_ProcessImpl_create(JNIEnv *env, jobject process, jstring cmd, jstring envBlock, jstring dir, jboolean redirectErrorStream, jobject in_fd, jobject out_fd, jobject err_fd) { /* 设置内部变量值 */ …… /* 建立输入、输出以及错误流管道 */ if (!(CreatePipe(&inRead, &inWrite, &sa, PIPE_SIZE) && CreatePipe(&outRead, &outWrite, &sa, PIPE_SIZE) && CreatePipe(&errRead, &errWrite, &sa, PIPE_SIZE))) { throwIOException(env, "CreatePipe failed"); goto Catch; } /* 进行参数格式的转换 */ pcmd = (LPTSTR) JNU_GetStringPlatformChars(env, cmd, NULL); …… /* 调用系统提供的方法,建立一个 Windows 的进程 */ ret = CreateProcess( 0, /* executable name */ pcmd, /* command line */ 0, /* process security attribute */ 0, /* thread security attribute */ TRUE, /* inherits system handles */ processFlag, /* selected based on exe type */ penvBlock, /* environment block */ pdir, /* change to the new current directory */ &si, /* (in) startup information */ &pi); /* (out) process information */ … /* 拿到新进程的句柄 */ ret = (jlong)pi.hProcess; … /* 最后返回该句柄 */ return ret; }
可以看到在创建一个进程的时候,调用 Windows 提供的 CreatePipe 方法建立输入,输出和错误管道,同时将用户通过 Java 传入的参数转换为操作系统可以识别的 C 语言的格式,然后调用 Windows 提供的创建系统进程的方式,创建一个进程,同时在 JAVA 虚拟机中保存了这个进程对应的句柄,然后返回给了 ProcessImpl 类,但是该类将返回句柄进行了隐藏。也正是 Java 跨平台的特性体现,JVM 尽可能的将和操作系统相关的实现细节进行了封装,并隐藏了起来。
同样,在用户调用 close、waitfor、destory 以及 exitValue 方法以后, JVM 会首先取得之前保存的该进程在操作系统中的句柄,然后通过调用操作系统提供的接口对该进程进行操作。通过这种方式来实现对进程的操作。
在其它平台下也是用类似的方式实现的,不同的是调用的对应平台的 API 会有所不同。
Java 进程与操作系统进程
通过上面对 Java 进程的分析,其实它在实现上就是创建了操作系统的一个进程,也就是每个 JVM 中创建的进程都对应了操作系统中的一个进程。但是,Java 为了给用户更好的更方便的使用,向用户屏蔽了一些与平台相关的信息,这为用户需要使用的时候,带来了些许不便。
在使用 C/C++ 创建系统进程的时候,是可以获得进程的 PID 值的,可以直接通过该 PID 去操作相应进程。但是在 JAVA 中,用户只能通过实例的引用去进行操作,当该引用丢失或者无法取得的时候,就无法了解任何该进程的信息。
当然,Java 进程在使用的时候还有些要注意的事情:
- Java 提供的输入输出的管道容量是十分有限的,如果不及时读取会导致进程挂起甚至引起死锁。
- 当创建进程去执行 Windows 下的系统命令时,如:dir、copy 等。需要运行 windows 的命令解释器,command.exe/cmd.exe,这依赖于 windows 的版本,这样才可以运行系统的命令。
- 对于 Shell 中的管道 ‘ | ’命令,各平台下的重定向命令符 ‘ > ’,都无法通过命令参数直接传入进行实现,而需要在 Java 代码中做一些处理,如定义新的流来存储标准输出,等等问题。
总之,Java 中对操作系统的进程进行了封装,屏蔽了操作系统进程相关的信息。同时,在使用 Java 提供创建进程运行本地命令的时候,需要小心使用。
一般而言,使用进程是为了执行某项任务,而现代操作系统对于执行任务的计算资源的配置调度一般是以线程为对象(早期的类 Unix 系统因为不支持线程,所以进程也是调度单位,但那是比较轻量级的进程,在此不做深入讨论)。创建一个进程,操作系统实际上还是会为此创建相应的线程以运行一系列指令。特别地,当一个任务比较庞大复杂,可能需要创建多个线程以实现逻辑上并发执行的时候,线程的作用更为明显。因而我们有必要深入了解 Java 中的线程,以避免可能出现的问题。本文下面的内容即是呈现 Java 线程的创建方式以及它与操作系统线程的联系与区别。