Hadoop-0.20.0源代碼分析(06)

在閱讀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系統命令行執行有關的一些類的繼承層次關係:

[java] view plaincopy
  1. ◦org.apache.hadoop.util.Shell  
  2.      ◦org.apache.hadoop.util.Shell.ShellCommandExecutor  
  3.      ◦org.apache.hadoop.fs.DF  
  4.      ◦org.apache.hadoop.fs.DU  
  5.      ◦org.apache.hadoop.fs.FileUtil.CygPathCommand  

Shell命令最頂層的抽象類是org.apache.hadoop.util.Shell,它定義瞭如何在當前文件系統環境中,與底層的Unix系統通過命令行進行必要的交互。

從org.apache.hadoop.util.Shell類定義的屬性來看,可以分爲兩種類型的屬性,一種是static final的字符串命令,另一種是與實現命令的執行相關的屬性。第一種屬性(我把兩個static final的獲取命令的方式也列出,放到這裏的屬性的後面)如下所示:

[java] view plaincopy
  1. /** Unix命令whoami :執行命令得到當前用戶名 */  
  2. public final static String USER_NAME_COMMAND = "whoami";  
  3.   
  4. /** Unix命令chmod :執行命令設置用戶操作權限 */  
  5. public static final String SET_PERMISSION_COMMAND = "chmod";  
  6.   
  7. /** Unix命令chown :執行命令設置屬主 */  
  8. public static final String SET_OWNER_COMMAND = "chown";  
  9.   
  10. /** Unix命令chgrp :執行命令設置組 */  
  11. public static final String SET_GROUP_COMMAND = "chgrp";  
  12.   
  13. /** Unix命令bash -c groups :執行命令得到當前用戶所屬的組列表 */  
  14. public static String[] getGROUPS_COMMAND() {  
  15.   return new String[]{"bash""-c""groups"};  
  16. }  
  17.   
  18. /** Unix命令ls -ld :執行命令設置組,不支持Windows系統,但可以支持Cygwin */  
  19. public static String[] getGET_PERMISSION_COMMAND() {  
  20.   return new String[] {(WINDOWS ? "ls" : "/bin/ls"), "-ld"};  
  21. }  

看到這些Unix的命令,應該非常熟悉。

第二種屬性,都屬於與如何實現定義的上述命令行的執行有關的,如下所示:

[java] view plaincopy
  1. private long    interval;   // 刷新間隔  
  2. private long    lastTime;   // 最後執行命令的時間  
  3. private Map<String, String> environment; // 命令行執行所需要的操作系統環境  
  4. private File dir;  
  5. private Process process; // 執行命令行的子進程  
  6. private int exitCode; // 執行命令行完成後,退出狀態碼  

dir屬性表示當前執行命令所在的工作目錄,environment屬性表示當前命令執行的環境,它們在Shell類中都提供了一個受保護的設置操作,可以在Shell抽象類的子類中根據需要設置不同工作目錄和環境,其中,dir默認爲系統“user.dir”變量值,environment使用系統默認的環境。

通過interval與lastTime屬性來檢查,是否有必要重新執行一次,如果是就執行,否則重置退出狀態碼exitCode爲0,正常退出,可以在Shell類的run方法中看到:

[java] view plaincopy
  1. protected void run() throws IOException {  
  2.   if (lastTime + interval > System.currentTimeMillis())  
  3.     return// 不需要重新執行命令行,返回  
  4.   exitCode = 0;   
  5.   runCommand(); // 調用:需要重新執行命令行  
  6. }  

通過runCommand方法就可以執行指定的Shell命令,它是Shell類的核心。在分析runCommand方法之前,先了解一下與Shell命令執行相關的環境信息。

當在Windows系統下,打開一個cmd窗口的時候,執行set命令,就能看到當前系統的環境變量的信息,如下所示:

[xhtml] view plaincopy
  1. ALLUSERSPROFILE=C:/Documents and Settings/All Users  
  2. APPDATA=C:/Documents and Settings/Administrator/Application Data  
  3. 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  
  4. CLIENTNAME=Console  
  5. CCommonProgramFiles=C:/Program Files/Common Files  
  6. COMPUTERNAME=SYJ  
  7. CComSpec=C:/WINDOWS/system32/cmd.exe  
  8. DEVMGR_SHOW_NONPRESENT_DEVICES=1  
  9. FP_NO_HOST_CHECK=NO  
  10. HERITRIX_HOME=E:/MyEclipse/workspace/heritrix  
  11. HOME=C:/Documents and Settings/Administrator  
  12. HOMEDRIVE=C:  
  13. HOMEPATH=/Documents and Settings/Administrator  
  14. JAVA_HOME=E:/Program Files/Java/jdk1.6.0_14  
  15. JSERV=D:/oracle/ora92/Apache/Jserv/conf  
  16. LOGONSERVER=//SYJ  
  17. NUMBER_OF_PROCESSORS=2  
  18. NUTSUFFIX=1  
  19. NUT_SUFFIXED_SEARCHING=1  
  20. OS=Windows_NT  
  21. 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/  
  22. PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH  
  23. PROCESSOR_ARCHITECTURE=x86  
  24. PROCESSOR_IDENTIFIER=x86 Family 6 Model 23 Stepping 10, GenuineIntel  
  25. PROCESSOR_LEVEL=6  
  26. PROCESSOR_REVISION=170a  
  27. ProgramFiles=C:/Program Files  
  28. PROMPT=$P$G  
  29. RATL_RTHOME=F:/Program Files/Rational/Rational Test  
  30. SESSIONNAME=Console  
  31. SystemDrive=C:  
  32. SystemRoot=C:/WINDOWS  
  33. TEMP=C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp  
  34. TMP=C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp  
  35. TMPDIR=C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp  
  36. USERDOMAIN=SYJ  
  37. USERNAME=Administrator  
  38. USERPROFILE=C:/Documents and Settings/Administrator  
  39. VBOX_INSTALL_PATH=E:/Program Files/Sun/xVM VirtualBox/  
  40. VS90COMNTOOLS=d:/Microsoft Visual Studio 9.0/Common7/Tools/  
  41. windir=C:/WINDOWS  
  42. 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,如下所示:

[java] view plaincopy
  1. private void runCommand() throws IOException {   
  2.   
  3.   ProcessBuilder builder = new ProcessBuilder(getExecString()); // 方法getExecString()在該類中式抽象的,需要在實現類中實現,獲取到一個命令名稱及其參數,從而基於此 構造一個ProcessBuilder進程實例  
  4.   boolean completed = false// 標識執行命令完成情況  
  5.     
  6.   if (environment != null) {  
  7.     builder.environment().putAll(this.environment); // 如果有必要,設置命令行執行環境  
  8.   }  
  9.   if (dir != null) {  
  10.     builder.directory(this.dir); // 如果必要,設置命令行執行所在工作目錄  
  11.   }  
  12.     
  13.   process = builder.start(); // 啓動ProcessBuilder builder進程,返回一個用來管理命令行執行情況的子進程process  
  14.   final BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); // 當builder進程啓動後,檢查提交的命令行是否合法,如果不合法或者執行出錯,將出錯信息寫入到緩衝流中,可以從其中解析讀取出來  
  15.   BufferedReader inReader = new BufferedReader(new InputStreamReader(process.getInputStream())); // 執行命令返回執行結果,通過process管理子線程來獲取執行流中的執行結果信息  
  16.   final StringBuffer errMsg = new StringBuffer(); // 存放執行命令出錯信息的String緩衝區  
  17.     
  18.   // 定義解析線程,解析命令行執行出錯信息所在的流,解析完成後釋放流緩衝區  
  19.   Thread errThread = new Thread() {  
  20.     @Override  
  21.     public void run() {  
  22.       try {  
  23.         String line = errReader.readLine();  
  24.         while((line != null) && !isInterrupted()) {  
  25.           errMsg.append(line);  
  26.           errMsg.append(System.getProperty("line.separator"));  
  27.           line = errReader.readLine();  
  28.         }  
  29.       } catch(IOException ioe) {  
  30.         LOG.warn("Error reading the error stream", ioe);  
  31.       }  
  32.     }  
  33.   };  
  34.   
  35.   try {  
  36.     errThread.start(); // 啓動線程,處理出錯信息  
  37.   } catch (IllegalStateException ise) { }  
  38.   
  39.   try {  
  40.     parseExecResult(inReader); // 調用:解析執行命令返回的結果信息  
  41.     String line = inReader.readLine();  
  42.     while(line != null) {   
  43.       line = inReader.readLine();  
  44.     }  
  45.     exitCode = process.waitFor(); // 等待進程process處理完畢,置exitCode狀態碼  
  46.     try {  
  47.       errThread.join(); // 等待出錯信息處理線程執行完成  
  48.     } catch (InterruptedException ie) {  
  49.       LOG.warn("Interrupted while reading the error stream", ie);  
  50.     }  
  51.     completed = true// 置命令行執行完成狀態  
  52.     if (exitCode != 0) {  
  53.       throw new ExitCodeException(exitCode, errMsg.toString());  
  54.     }  
  55.   } catch (InterruptedException ie) {  
  56.     throw new IOException(ie.toString());  
  57.   } finally {  
  58.     try {  
  59.       inReader.close(); // 最後,需要關閉流對象  
  60.     } catch (IOException ioe) {  
  61.       LOG.warn("Error while closing the input stream", ioe);  
  62.     }  
  63.     if (!completed) {  
  64.       errThread.interrupt(); // 可能處理錯誤信息的線程發生異常,未能置completed=true,最後終止要該線程  
  65.     }  
  66.     try {  
  67.       errReader.close(); // 關閉流對象  
  68.     } catch (IOException ioe) {  
  69.       LOG.warn("Error while closing the error stream", ioe);  
  70.     }  
  71.     process.destroy(); // 終止子進程process  
  72.     lastTime = System.currentTimeMillis(); // 設置當前時間爲該命令行執行的最後時間,爲了判斷一個命令是否需要被重新執行  
  73.   }  
  74. }  

上面已經做了詳細的註釋,基本上闡明瞭一個命令行的執行過程。

在類中,還提供了一個static方法execCommand,爲執行命令提供入口:

[java] view plaincopy
  1. public static String execCommand(String ... cmd) throws IOException {  
  2.   return execCommand(null, cmd);  
  3. }  

執行該方法,調用了另一個重載的execCommand方法,返回命令執行結果的信息。

注意,在Shell抽象類中並沒有實現該怎樣獲取一個命令名稱及其參數的方法,需要在實現類中給出,因此,在Shell類內部定義了一個靜態內部類ShellCommandExecutor,該類實現了獲取命令名稱及其參數的方法。在上面方法execCommand中,調用了一個重載的execCommand方法,該方法中通過實例化一個ShellCommandExecutor類,來提供獲取命令名稱及其參數,進而構造一個ProcessBuilder實例,創建一個操作系統線程來執行命令行。

 

? extends Shell 

 

下面看實現Shell抽象類的一些子類的實現。

  • ShellCommandExecutor類

ShellComandExecutor類的實現如下所示:

[java] view plaincopy
  1. public static class ShellCommandExecutor extends Shell {  
  2.     
  3.   private String[] command; // 命令名稱及其參數  
  4.   private StringBuffer output; // 存放執行命令行返回結果的String緩衝區  
  5.     
  6.   public ShellCommandExecutor(String[] execString) {  
  7.     command = execString.clone();  
  8.   }  
  9.   
  10.   public ShellCommandExecutor(String[] execString, File dir) {  
  11.     this(execString);  
  12.     this.setWorkingDirectory(dir);  
  13.   }  
  14.   
  15.   public ShellCommandExecutor(String[] execString, File dir, Map<String, String> env) {  
  16.     this(execString, dir);  
  17.     this.setEnvironment(env);  
  18.   }  
  19.     
  20.   /** 繼承自Shell基類,執行命令行 */  
  21.   public void execute() throws IOException {  
  22.     this.run();      
  23.   }  
  24.   
  25.   protected String[] getExecString() {  
  26.     return command; // 輸入的就是命令名稱+參數的格式,直接得到  
  27.   }  
  28.   
  29. /**   
  30.  * 解析命令行執行後的輸出結果流,存放到String緩衝區中 
  31.  */  
  32.   protected void parseExecResult(BufferedReader lines) throws IOException {  
  33.     output = new StringBuffer();  
  34.     char[] buf = new char[512];  
  35.     int nRead;  
  36.     while ( (nRead = lines.read(buf, 0, buf.length)) > 0 ) {  
  37.       output.append(buf, 0, nRead);  
  38.     }  
  39.   }  

  • DF類

org.apache.hadoop.fs.DF類實現了Unix系統中Shell命令df,用來獲取磁盤使用情況的統計數據。該Shell實現類中定義域df命令操作相關的內容,可以從屬性來看:

[java] view plaincopy
  1. public static final long DF_INTERVAL_DEFAULT = 3 * 1000// 設置df命令刷新間隔爲3s     
  2. private String  dirPath; // 執行df命令所在工作目錄  
  3. private String filesystem; // 磁盤設備名  
  4. private long capacity; // 磁盤總容量  
  5. private long used; // 磁盤使用量  
  6. private long available; // 磁盤可用量  
  7. private int percentUsed; // 磁盤使用率  
  8. private String mount; // 磁盤掛載位置  

只需要實現Shell類定義的getExecString與parseExecResult方法即可。比較簡單,getExecString方法實現如下:

[java] view plaincopy
  1. protected String[] getExecString() {  
  2.   return new String[] {"bash","-c","exec 'df' '-k' '" + dirPath  + "' 2>/dev/null"};  
  3. }  

該方法返回的字符串數組,用來構造一個ProcessBuilder進程實例。

parseExecResult方法實現如下所示:

[java] view plaincopy
  1. protected void parseExecResult(BufferedReader lines) throws IOException {  
  2.   lines.readLine();   // 去掉流中的首行    
  3.   String line = lines.readLine();  
  4.   if (line == null) {  
  5.     throw new IOException( "Expecting a line not the end of stream" );  
  6.   }  
  7.   StringTokenizer tokens = new StringTokenizer(line, " /t/n/r/f%");  
  8.     
  9.   this.filesystem = tokens.nextToken();  
  10.   if (!tokens.hasMoreTokens()) {              
  11.     line = lines.readLine();  
  12.     if (line == null) {  
  13.       throw new IOException( "Expecting a line not the end of stream" );  
  14.     }  
  15.     tokens = new StringTokenizer(line, " /t/n/r/f%");  
  16.   }  
  17. /**   
  18.  * 下面處理並設置執行df -k命令的結果信息 
  19.  */  
  20.   this.capacity = Long.parseLong(tokens.nextToken()) * 1024;  
  21.   this.used = Long.parseLong(tokens.nextToken()) * 1024;  
  22.   this.available = Long.parseLong(tokens.nextToken()) * 1024;  
  23.   this.percentUsed = Integer.parseInt(tokens.nextToken());  
  24.   this.mount = tokens.nextToken();  
  25. }  

  • DU類

DU類實現了Unix的du命令,顯示目錄或者文件大小的信息,具體實現可以參考org.apache.hadoop.fs.DU類,這裏跳過。

  • CygPathCommand類

CygPathCommand類是org.apache.hadoop.fs.FileUtil類的一個內部靜態類,實現了Windows系統上模擬Unix系統的Cygwin系統的cygpath命令,這裏跳過。


發佈了4 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章