在閱讀Hadoop源代碼過程中,在org.apache.hadoop.security.UnixUserGroupInformation類中,需要獲取到Unix系統的用戶名和所屬組的信息,就需要通過執行Shell命令得到相應的結果,這裏,通過閱讀Hadoop項目org.apache.hadoop.util包、org.apache.hadoop.fs.shell包、org.apache.hadoop.fs包中文件來了解,Hadoop基於Shell命令與底層Unix操作系統的交互,以及在MapReduce模型中通過命令行的方式提交管理計算任務的一些詳細情況。
首先看一下,與Unix系統命令行執行有關的一些類的繼承層次關係:
- ◦org.apache.hadoop.util.Shell
- ◦org.apache.hadoop.util.Shell.ShellCommandExecutor
- ◦org.apache.hadoop.fs.DF
- ◦org.apache.hadoop.fs.DU
- ◦org.apache.hadoop.fs.FileUtil.CygPathCommand
Shell命令最頂層的抽象類是org.apache.hadoop.util.Shell,它定義瞭如何在當前文件系統環境中,與底層的Unix系統通過命令行進行必要的交互。
從org.apache.hadoop.util.Shell類定義的屬性來看,可以分爲兩種類型的屬性,一種是static final的字符串命令,另一種是與實現命令的執行相關的屬性。第一種屬性(我把兩個static final的獲取命令的方式也列出,放到這裏的屬性的後面)如下所示:
- /** Unix命令whoami :執行命令得到當前用戶名 */
- public final static String USER_NAME_COMMAND = "whoami";
- /** Unix命令chmod :執行命令設置用戶操作權限 */
- public static final String SET_PERMISSION_COMMAND = "chmod";
- /** Unix命令chown :執行命令設置屬主 */
- public static final String SET_OWNER_COMMAND = "chown";
- /** Unix命令chgrp :執行命令設置組 */
- public static final String SET_GROUP_COMMAND = "chgrp";
- /** Unix命令bash -c groups :執行命令得到當前用戶所屬的組列表 */
- public static String[] getGROUPS_COMMAND() {
- return new String[]{"bash", "-c", "groups"};
- }
- /** Unix命令ls -ld :執行命令設置組,不支持Windows系統,但可以支持Cygwin */
- public static String[] getGET_PERMISSION_COMMAND() {
- return new String[] {(WINDOWS ? "ls" : "/bin/ls"), "-ld"};
- }
看到這些Unix的命令,應該非常熟悉。
第二種屬性,都屬於與如何實現定義的上述命令行的執行有關的,如下所示:
- private long interval; // 刷新間隔
- private long lastTime; // 最後執行命令的時間
- private Map<String, String> environment; // 命令行執行所需要的操作系統環境
- private File dir;
- private Process process; // 執行命令行的子進程
- private int exitCode; // 執行命令行完成後,退出狀態碼
dir屬性表示當前執行命令所在的工作目錄,environment屬性表示當前命令執行的環境,它們在Shell類中都提供了一個受保護的設置操作,可以在Shell抽象類的子類中根據需要設置不同工作目錄和環境,其中,dir默認爲系統“user.dir”變量值,environment使用系統默認的環境。
通過interval與lastTime屬性來檢查,是否有必要重新執行一次,如果是就執行,否則重置退出狀態碼exitCode爲0,正常退出,可以在Shell類的run方法中看到:
- protected void run() throws IOException {
- if (lastTime + interval > System.currentTimeMillis())
- return; // 不需要重新執行命令行,返回
- exitCode = 0;
- runCommand(); // 調用:需要重新執行命令行
- }
通過runCommand方法就可以執行指定的Shell命令,它是Shell類的核心。在分析runCommand方法之前,先了解一下與Shell命令執行相關的環境信息。
當在Windows系統下,打開一個cmd窗口的時候,執行set命令,就能看到當前系統的環境變量的信息,如下所示:
- ALLUSERSPROFILE=C:/Documents and Settings/All Users
- APPDATA=C:/Documents and Settings/Administrator/Application Data
- CLASSPATH=.;E:/Program Files/Java/jdk1.6.0_14/lib/tools.jar;E:/Program Files/Java/jdk1.6.0_14/lib/dt.jar;E:/Program Files/Java/jdk1.6.0_14/jre/lib/rt.jar;E:/Program Files/Java/jdk1.6.0_14/jre/lib/charsets.jar
- CLIENTNAME=Console
- CCommonProgramFiles=C:/Program Files/Common Files
- COMPUTERNAME=SYJ
- CComSpec=C:/WINDOWS/system32/cmd.exe
- DEVMGR_SHOW_NONPRESENT_DEVICES=1
- FP_NO_HOST_CHECK=NO
- HERITRIX_HOME=E:/MyEclipse/workspace/heritrix
- HOME=C:/Documents and Settings/Administrator
- HOMEDRIVE=C:
- HOMEPATH=/Documents and Settings/Administrator
- JAVA_HOME=E:/Program Files/Java/jdk1.6.0_14
- JSERV=D:/oracle/ora92/Apache/Jserv/conf
- LOGONSERVER=//SYJ
- NUMBER_OF_PROCESSORS=2
- NUTSUFFIX=1
- NUT_SUFFIXED_SEARCHING=1
- OS=Windows_NT
- Path=D:/oracle/ora92/bin;C:/Program Files/Oracle/jre/1.3.1/bin;C:/Program Files/Oracle/jre/1.1.8/bin;E:/Program Files/CollabNet Subversion Client;E:/Program Files/Java/jdk1.6.0_14/bin;C:/WINDOWS/system32;C:/WINDOWS;C:/WINDOWS/System32/Wbem;C:/Program Files/Microsoft SQL Server/80/Tools/Binn/;C:/Program Files/Microsoft SQL Server/90/Tools/binn/;C:/Program Files/MyEclipse 7.0M1/jre/bin;E:/Program Files/TortoiseSVN/bin;E:/PROGRA~1/F-Secure/SSHTRI~1;D:/Program Files/MySQL/MySQL Server 5.1/bin;F:/Program Files/Rational/common;C:/Program Files/StormII/Codec;C:/Program Files/StormII;C:/Program Files/SSH Communications Security/SSH Secure Shell;C:/Program Files/IDM Computer Solutions/UltraEdit/
- PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH
- PROCESSOR_ARCHITECTURE=x86
- PROCESSOR_IDENTIFIER=x86 Family 6 Model 23 Stepping 10, GenuineIntel
- PROCESSOR_LEVEL=6
- PROCESSOR_REVISION=170a
- ProgramFiles=C:/Program Files
- PROMPT=$P$G
- RATL_RTHOME=F:/Program Files/Rational/Rational Test
- SESSIONNAME=Console
- SystemDrive=C:
- SystemRoot=C:/WINDOWS
- TEMP=C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp
- TMP=C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp
- TMPDIR=C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp
- USERDOMAIN=SYJ
- USERNAME=Administrator
- USERPROFILE=C:/Documents and Settings/Administrator
- VBOX_INSTALL_PATH=E:/Program Files/Sun/xVM VirtualBox/
- VS90COMNTOOLS=d:/Microsoft Visual Studio 9.0/Common7/Tools/
- windir=C:/WINDOWS
- WV_GATEWAY_CFG=D:/oracle/ora92/Apache/modplsql/cfg/wdbsvr.app
這些環境變量的信息都是以鍵值對的形式出現的,當在操作系統上運行相關的應用程序的時候,其實就是在上述環境變量所設置的一個上下文中運行,環境是應用程序運行不可缺少的條件。你在Unix系統中執行env命令,同樣也能得到與上面類似的鍵值對的環境變量信息。
所以,org.apache.hadoop.util.Shell作爲Shell命令的抽象,一定要獲取到當前所在操作系統(Unix系統) 的環境變量,在這樣一個上下文中,才能如同從Unix系統中輸入執行Shell命令進行執行一樣。
在Java中,實現了一個java.lang.ProcessBuilder類,該類能夠創建一個操作系統的進程,通過爲該進程設置運行環境變量,從而啓動進行執行。默認情況下ProcessBuilder類已經實現了設置當前操作系統環境的功能,可以通過environment()方法查看它的實例所具有的環境信息,這等價於使用System.getenv()獲取到的環境變量,都是以鍵值對的形式存在於ProcessBuilder類實例的上下文中。
下面,分析Shell類實現執行命令的過程,實現方法爲runCommand,如下所示:
- private void runCommand() throws IOException {
- ProcessBuilder builder = new ProcessBuilder(getExecString()); // 方法getExecString()在該類中式抽象的,需要在實現類中實現,獲取到一個命令名稱及其參數,從而基於此 構造一個ProcessBuilder進程實例
- boolean completed = false; // 標識執行命令完成情況
- if (environment != null) {
- builder.environment().putAll(this.environment); // 如果有必要,設置命令行執行環境
- }
- if (dir != null) {
- builder.directory(this.dir); // 如果必要,設置命令行執行所在工作目錄
- }
- process = builder.start(); // 啓動ProcessBuilder builder進程,返回一個用來管理命令行執行情況的子進程process
- final BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); // 當builder進程啓動後,檢查提交的命令行是否合法,如果不合法或者執行出錯,將出錯信息寫入到緩衝流中,可以從其中解析讀取出來
- BufferedReader inReader = new BufferedReader(new InputStreamReader(process.getInputStream())); // 執行命令返回執行結果,通過process管理子線程來獲取執行流中的執行結果信息
- final StringBuffer errMsg = new StringBuffer(); // 存放執行命令出錯信息的String緩衝區
- // 定義解析線程,解析命令行執行出錯信息所在的流,解析完成後釋放流緩衝區
- Thread errThread = new Thread() {
- @Override
- public void run() {
- try {
- String line = errReader.readLine();
- while((line != null) && !isInterrupted()) {
- errMsg.append(line);
- errMsg.append(System.getProperty("line.separator"));
- line = errReader.readLine();
- }
- } catch(IOException ioe) {
- LOG.warn("Error reading the error stream", ioe);
- }
- }
- };
- try {
- errThread.start(); // 啓動線程,處理出錯信息
- } catch (IllegalStateException ise) { }
- try {
- parseExecResult(inReader); // 調用:解析執行命令返回的結果信息
- String line = inReader.readLine();
- while(line != null) {
- line = inReader.readLine();
- }
- exitCode = process.waitFor(); // 等待進程process處理完畢,置exitCode狀態碼
- try {
- errThread.join(); // 等待出錯信息處理線程執行完成
- } catch (InterruptedException ie) {
- LOG.warn("Interrupted while reading the error stream", ie);
- }
- completed = true; // 置命令行執行完成狀態
- if (exitCode != 0) {
- throw new ExitCodeException(exitCode, errMsg.toString());
- }
- } catch (InterruptedException ie) {
- throw new IOException(ie.toString());
- } finally {
- try {
- inReader.close(); // 最後,需要關閉流對象
- } catch (IOException ioe) {
- LOG.warn("Error while closing the input stream", ioe);
- }
- if (!completed) {
- errThread.interrupt(); // 可能處理錯誤信息的線程發生異常,未能置completed=true,最後終止要該線程
- }
- try {
- errReader.close(); // 關閉流對象
- } catch (IOException ioe) {
- LOG.warn("Error while closing the error stream", ioe);
- }
- process.destroy(); // 終止子進程process
- lastTime = System.currentTimeMillis(); // 設置當前時間爲該命令行執行的最後時間,爲了判斷一個命令是否需要被重新執行
- }
- }
上面已經做了詳細的註釋,基本上闡明瞭一個命令行的執行過程。
在類中,還提供了一個static方法execCommand,爲執行命令提供入口:
- public static String execCommand(String ... cmd) throws IOException {
- return execCommand(null, cmd);
- }
執行該方法,調用了另一個重載的execCommand方法,返回命令執行結果的信息。
注意,在Shell抽象類中並沒有實現該怎樣獲取一個命令名稱及其參數的方法,需要在實現類中給出,因此,在Shell類內部定義了一個靜態內部類ShellCommandExecutor,該類實現了獲取命令名稱及其參數的方法。在上面方法execCommand中,調用了一個重載的execCommand方法,該方法中通過實例化一個ShellCommandExecutor類,來提供獲取命令名稱及其參數,進而構造一個ProcessBuilder實例,創建一個操作系統線程來執行命令行。
? extends Shell
下面看實現Shell抽象類的一些子類的實現。
- ShellCommandExecutor類
ShellComandExecutor類的實現如下所示:
- public static class ShellCommandExecutor extends Shell {
- private String[] command; // 命令名稱及其參數
- private StringBuffer output; // 存放執行命令行返回結果的String緩衝區
- public ShellCommandExecutor(String[] execString) {
- command = execString.clone();
- }
- public ShellCommandExecutor(String[] execString, File dir) {
- this(execString);
- this.setWorkingDirectory(dir);
- }
- public ShellCommandExecutor(String[] execString, File dir, Map<String, String> env) {
- this(execString, dir);
- this.setEnvironment(env);
- }
- /** 繼承自Shell基類,執行命令行 */
- public void execute() throws IOException {
- this.run();
- }
- protected String[] getExecString() {
- return command; // 輸入的就是命令名稱+參數的格式,直接得到
- }
- /**
- * 解析命令行執行後的輸出結果流,存放到String緩衝區中
- */
- protected void parseExecResult(BufferedReader lines) throws IOException {
- output = new StringBuffer();
- char[] buf = new char[512];
- int nRead;
- while ( (nRead = lines.read(buf, 0, buf.length)) > 0 ) {
- output.append(buf, 0, nRead);
- }
- }
- DF類
org.apache.hadoop.fs.DF類實現了Unix系統中Shell命令df,用來獲取磁盤使用情況的統計數據。該Shell實現類中定義域df命令操作相關的內容,可以從屬性來看:
- public static final long DF_INTERVAL_DEFAULT = 3 * 1000; // 設置df命令刷新間隔爲3s
- private String dirPath; // 執行df命令所在工作目錄
- private String filesystem; // 磁盤設備名
- private long capacity; // 磁盤總容量
- private long used; // 磁盤使用量
- private long available; // 磁盤可用量
- private int percentUsed; // 磁盤使用率
- private String mount; // 磁盤掛載位置
只需要實現Shell類定義的getExecString與parseExecResult方法即可。比較簡單,getExecString方法實現如下:
- protected String[] getExecString() {
- return new String[] {"bash","-c","exec 'df' '-k' '" + dirPath + "' 2>/dev/null"};
- }
該方法返回的字符串數組,用來構造一個ProcessBuilder進程實例。
parseExecResult方法實現如下所示:
- protected void parseExecResult(BufferedReader lines) throws IOException {
- lines.readLine(); // 去掉流中的首行
- String line = lines.readLine();
- if (line == null) {
- throw new IOException( "Expecting a line not the end of stream" );
- }
- StringTokenizer tokens = new StringTokenizer(line, " /t/n/r/f%");
- this.filesystem = tokens.nextToken();
- if (!tokens.hasMoreTokens()) {
- line = lines.readLine();
- if (line == null) {
- throw new IOException( "Expecting a line not the end of stream" );
- }
- tokens = new StringTokenizer(line, " /t/n/r/f%");
- }
- /**
- * 下面處理並設置執行df -k命令的結果信息
- */
- this.capacity = Long.parseLong(tokens.nextToken()) * 1024;
- this.used = Long.parseLong(tokens.nextToken()) * 1024;
- this.available = Long.parseLong(tokens.nextToken()) * 1024;
- this.percentUsed = Integer.parseInt(tokens.nextToken());
- this.mount = tokens.nextToken();
- }
- DU類
DU類實現了Unix的du命令,顯示目錄或者文件大小的信息,具體實現可以參考org.apache.hadoop.fs.DU類,這裏跳過。
- CygPathCommand類
CygPathCommand類是org.apache.hadoop.fs.FileUtil類的一個內部靜態類,實現了Windows系統上模擬Unix系統的Cygwin系統的cygpath命令,這裏跳過。