程式扎記: [Perl 學習手冊] CH16 : 行程管理

標籤

2010年9月25日 星期六

[Perl 學習手冊] CH16 : 行程管理

前言 : 
現在我們來看看如何從 Perl 裡直接執行別的程式, 並藉此學習如何管理你的子程序. 和 Perl 的精神一樣, "辦法不只一種". 另外Perl 的可值性非常高, 某個功能在 Unix 是如此, 在 Windows 上也是這樣. 

system 函式 : 
要啟動子行程, 在 Perl 裡最簡單的做法就是利用 system 函式. 例如要在 Perl 裡面執行 Unix 的 date 命令, 看起來就像這樣 : 
system "date"; 

子行程會執行 date 命令, 並承接 Perl 的標準輸入, 標準輸出以及標準錯誤. 而通常提供給 system 函式的參數就等於在一般 shell 所鍵入的命令. 所以當你想用 "ls -l $HOME" 之類比較複雜的命令, 只要把它們全部放進參數就行了 : 
system 'ls -l $HOME'; 

請注意因為這裡 $HOME 是 shell 的變數, 為了避免與 Perl 的變數混淆, 所以我們不用雙引號, 而改用單引號. 不然要用雙引號也可以這麼寫 : 
system "ls -l \$HOME"; 

但是這種寫法不容易維護. 目前為止 date 命令只是輸出結果而以. 當子行程在執行時, Perl 會等待它結束. 不過你可以使用 shell 的功能來啟定背景行程 : 
system "long_running_command with parameters &"; 

接著 system 會啟動 shell 並且 long_running_command 將成為背景行程. 接著 Perl 注意到 shell 很快就結束了並繼續執行接下來的程式. 而在這個例子中long_running_command 其實是 Perl 行程的子行程, 只是 Perl 無法直接控制它也不知道它的存在. 在命令 "夠簡單" 的時候, system 會先在繼承而來的 PATH 變數裡搜尋可以執行的命令 ; 找到之後就會直接啟動它, 但是要是字串中出現奇怪的字符 (像是 shell 中介字符 : 錢號, 分號, 豎線等等) 這些麻煩事便會交給標準的 Bourne Shell (/bin/sh) 來處理. 這樣一來 shell 是子行程而所執行的命令則是孫行程. 

- 避免使用 Shell 
調用 system 函式時, 也可以指定一個以上的引數, 如此一來不管你所給的文字有多複雜, 都不會用到 shell, 作法如下 : 

  1. my $tarfile = "something*wicked.tar";  
  2. my @dirs = qw(fred|flintstone betty);  
  3. system "tar""cvf", $tarfile, @dirs;  
在這個例子頭一個參數 (此處為 "tar") 是命令名稱, 可以透過 PATH 變數以正常的方式找到. 接下來後面的參數會逐項傳遞給前面的命令. 即使引數裡出現對 shell 有意義的字符, shell 都沒有機會搞亂它們. 所以 tar 命令不多不少剛好會取得五個參數. system 函式的傳回值是根據子命令的結束狀態來決定. 而在 Unix 裡傳回值 0 代表一切正常, 而非零的值代表出了狀況 : 

請注意這和大多數運算符 "真為正常, 假為錯誤" 的一般標準剛好相反 ; 所以我們必須將真假值顛倒過來, 才能運用 "do this or die" 的典型寫法. 最簡單的方法就是在 system 函式前面加上一個驚嘆號 : 
!system "rm -rf files_to_delete" or die "Some errors occur"; 

另外在這個例子裡, 在錯誤訊息裡引用 $! 並不是很適當. 因為如果有錯誤發生, 照理來說多半是 rm 命令發生問題 ; 而這樣的問題並不是 Perl 內部與系統呼叫相關的問題, 所以沒辦法藉由 $! 得知. 

exec 函式 : 
到目前為止, 我們提過有關 system 的所有用法與含意, 除了一項不同外, 幾乎都可全部適用於 exec 函式 . system 函式會讓 Perl 建立子行程來執行命令 ; 而 exec 函式則會讓 Perl 行程去執行完所要求的命令. 舉例來說假設我們想要執行 /tmp 目錄裡面的 AutomationPM.jar 檔案, 以及傳給他 -c & 引數, 作法如下 : 
  1. #!/usr/bin/perl  
  2. chdir "./automation" or die "Can't chdir to ./automation directory: $!";  
  3. system "ls -l";  
  4. exec "java -jar AutomationPM.jar -c &";  
當程式執行到 exec 時, Perl 會找到 java 然後"跳進該程式裡". 在此之後Perl 行程就已經不存在了, 只剩下執行 java 命令的行程. 在 java 執行完畢之後就已經沒有 Perl 行程可以返回, 如果我們以命令列執行本程式, 就會直接回到命令提示符. 
這麼做的用處何在? 如果這個 Perl 程式的目的是建立某個特定的環境好讓其他程式執行的話, 那麼當其他程式執行時, 它的目的就算完成了. 如果我們不使用 exec 函式而使用 system 函式, 我們就會有一個 Perl 程式待在哪裡沒事做. 說了這麼多其實 exec 並不常用, 除非是跟 fork函式 一起使用. 當你不知道使用 exec 還是 system 就選擇 system 函式吧, 這樣幾乎你都不會遇到問題. 

環境變數 : 
啟動其他行程時, 你可能需要以某種方式來設定它的環境. 我們先前提過行程啟動時, 會從你的行程承接它的工作目錄. "環境變數" 則是另一種常見的組態. 最著名的一個環境變數就是 PATH. 當你鍵入像 rm john 這樣的命令時, 系統會在該目錄串列中依續尋找 rm 命令. Perl 會在需要時利用 PATH 來尋找程式, 以便執行. (當然如果你指定了命令的完整路徑名稱, 像是 /bin/echo , 這樣就沒必要在 PATH 裡搜尋.) 
在 Perl 中, 環境變數可透過雜湊 %ENV 取得, 其中每個雜湊鍵都代表一個環境變數. 在程式開始執行時, %ENV 會存放從父行程 (shell) 承接而來的設定值. 只要變更這個雜湊就會連帶變更到環境變數 ; 它會被新的行程所承接, 也可供 Perl 本身使用. 舉例來說, 假設你要執行系統裡的 make 工具程式, 並且想以自己的私有目錄做為尋找命令 (包括 make) 的頭一個目錄 ; 另外再假設你不想使用 IFS 環境變數, 以免 make 或其後的命令做出不正常的動作. 我們可以這麼寫 : 
  1. $ENV{'PATH'} = "/home/rootbetter/bin;$ENV{'PATH'}";  
  2. delete $ENV{'IFS'};  
  3. my $make_result = system "make";  
一般來說新啟動的行程會承接父行程的環境變數, 當前的工作目錄以及標準輸出輸入與錯誤串流. 更詳細的說明, 請參考系統上與程式設計相關的文件. 

用倒引號來擷取輸出結果 : 
使用 system 和 exec 時, 其所執行之命令的輸出結果送往 Perl 目前的標準輸出, 有些時候我們會需要將輸出結果擷取成字串, 以方便後續處理. 方法很簡單, 只要以倒引號來代替單引號或雙引號就行了 : 
my $now = `date`; # 擷取 date 的輸出結果 
print "Now is $now"; #已經包含換列字符 

一般來說 date 命令會產生長度約 30 個字符的字串, 當我們把 date 放在倒引號裡時, Perl 會執行這個命令並將輸出結果擷取成字串. 在這個例子字串會被賦值給 $now 變數. 
這就像是 Unix shell 的倒引號一樣, 但是 shell 還會作額外的處理 ; 它會將最後一個換列符號移除, 讓它成為易於其他東西結合的字串. Perl 不會做後續的處理, 它會直接使用所接受到的輸出. 要在 Perl 中取得相同的結果, 我們可以對所取得的字串進行一次 chomp 運算. 
倒引號裡面的值相當於 system 的單引數形式, 並且會以雙引號字串的方式進行解釋 ; 也就是說裡面的倒斜線規避序列與變數都會被適當的展開. 比方說若想取得一系列 Perl 函式的 Perl 說明文件, 可以重複執行 perldoc 命令, 每次執行不同的引數 : 
  1. #!/usr/bin/perl  
  2. my @functions = qw{ int rand sleep length hex eof not exit sqrt umask };  
  3. my %about;  
  4. foreach(@functions){  
  5.         $about{$_} = `perldoc -t -f $_`;  
  6. }  
倒引號要等效成單引號很麻煩 ; 它總是會展開 "變數參照" 與 "倒斜線項目". 此外也沒有簡單的方法可以對應到 system 的多引數版本 (也就是不使用 shell). 如果倒引號內的命令夠複雜的話, Unix 的 Bourne Shell 就會自動被用來解釋該命令. 
雖然倒引號很方便, 但是還是建議你 : 用不到命令的輸出值時, 請勿使用倒引號. 因為就算你不想要輸出結果, Perl 還是會費力擷取命令的結果. 所以在安全與效率的雙重考量下, 可以使用 system 為優先考量. 
如果想要用標準輸出來擷取標準錯誤輸出, 可以使用 shell 的 "將標準錯誤輸出合併至標準輸出" 功能 ; 此功能在一般的 Unix shell 下通常會寫成 2>&1, 如下所示 : 
my $output_with_errors = `command_line -argument 2>&1`; 

這樣做會讓標準錯誤輸出與標準輸出的訊息交錯在一起, 如果你想要將之分開來, 有很多難寫的方法可以使用. 另外我們使用倒引號的命令時多半不會使用到標準輸入, 所以受執行的命令不會卡在等待用戶的輸入. 但是如果 date 命令會詢問你欲使用哪個時區, 並等待用戶從 Console 輸入, 這樣由於使用者看不到提示字串 (被倒引號擷取走了), 就很像被卡住的情況. 因此請勿在倒引號使用會讀取標準輸入的命令. 如果你不太確定它是否會從標準輸入讀取資訊, 請將 /dev/null 重新導向到標準輸入 : 
my $result = `some_questionable_command arg arg argh  

這樣一來就算他要求輸入, 起碼也會馬上讀到檔案結尾 (end-of-file). 

- 在串列語境下使用倒引號 
要是某個命令的輸出結果有許多列, 那麼在純量語境使用倒引號時, 它會被當成一個很長的字串傳回, 其中包含換列字符. 但若是在串列語境下使用相同的倒引符, 則回傳回由各列輸出所組成的串列. 舉個例子, Unix 的 who 命令會將目前線上的使用者列出, 每個登入者一行資訊 : 
linux-tl0r:~ # who <執行 who 命令>
john tty7 Sep 9 10:28 (:0)
john pts/2 Sep 9 10:27 (:0.0)
root pts/1 Sep 10 09:24 (192.168.0.154)

但純量語境我們會一次得到所有的輸出, 但是在串列語境下, 則會自動拆成一列列的資料 : 
my @who_lines = `who`; 

@who_lines 裡會有數個被拆開的元素, 每一個都是以換列符號做為結尾, 我們可以對各個元素進行 chomp, 以去除換列符號. 或者使用 foreach 來逐項處理 : 
  1. my %ttys;  
  2. foreach(`who`) {  
  3.         my($user, $tty, $date) = /(\S+)\s+(\S+)\s+(.*)/;  
  4.         $ttys{$user} .= "$tty at $date\n";  
  5. }  
  6.   
  7. foreach(keys %ttys) {  
  8.         print "$_:\n$ttys{$_}";  
  9. }  
上面要注意的是, 我們使用了正規表示式比對 ; 由於不是使用 繫結運算符 (=~), 表示樣式比對是針對 $_ 進行的. 這樣剛好符合我們的需求, 因為資料就放在 $_ 裡. 並且陳序句是使用附加的方式加到某個雜湊鍵, 這是因為一個使用者可能會不止登入一次. 

將行程視為檔案代號 : 
到目前為止, 我們所看到的同步行程處理方式, 都是由 Perl 掌控全局 : 啟動一個命令, 然後等著它結束 (或許還會擷取其輸出). 不過 Perl 也可以啟動協同執行的子行程 ; 直到結束之前, 它們都會持續跟 Perl 交換資訊. 
要啟動一個協同執行的子行程時, 請將命令放在 open 呼叫的檔名部分, 並且在它的前面或後面加上豎線 (|). 因此這種做法也稱為 "導管式開啟" (piped open) : 
  1. open DATE, "date |" or die "Can't build piped with date: $!";  
  2. open MAIL, "|mail john" or die "Can't build piped with mail: $!";  
第一個例子的豎線符號在右邊, 表示命令執行時, 它的標準輸出會連接到提供程式讀取的檔案代號 DATE. 就像在 shell 裡用 date | your_program 這道命令. 而第二個例子豎線符號在左邊, 所以該命令的標準輸入會連結到供程式寫入的 MAIL 檔案代號, 就像在 shell 裡用 your_mail | mail john 這道命令. 不管是哪一個例子, 都會啟動命令並且讓它獨立於 Perl 行程外繼續執行. 如果無法建立子行程, open 就會發生錯誤. 如果命令本身不存在或沒有發生錯誤而正常結束, 這在開啟時 (通常) 不會有錯誤發生, 但是在關閉時卻會. 
因此想從 "以讀取模式開啟檔案代號" 中取得資料, 只要採用正常的檔案讀取就行了 : 
my $now = ; 

想傳資料給 mail 行程, 只要利用列印至檔案代號的 print 運算就行了 : 
print MAIL "Now is $now"; # 假設 $now 以換行符號結束 

如果行程連接到某個以讀取模式開啟的檔案代號, 然後它結束執行了, 檔案代號會回傳檔案結尾 ; 當你關閉某個會寫入資料到行程的檔案代號時, 該行程會讀到檔案結尾. 所以要結束電子郵件的寄送, 只要關閉這個檔案代號就行了 : 
  1. close MAIL;  
  2. die "mail: 結束狀態 ($?) 不為零!" if $?;  
關閉連接至行程的檔案代號, 會讓 Perl 等待該行程結束, 以取得它的結束狀態. 而結束狀態會存入 $? 變數, 它的值就跟 system 函式傳回的數值一樣 : 零表示成功, 非零值表示失敗. 每個新結束的行程都會覆寫掉前一個傳回值, 所以如果你需要這個值就記得把它存起來. 
這些行程間的同步就像 shell 中被管線連結的命令一樣. 如果你只想讀取的話, 除非相要在結果出現時立刻取得, 否則倒引號通常就能輕鬆達成目的. 舉例來說, Unix 的 find 命令可以依照檔案的屬性來找尋檔案的位置 ; 然而如果檔案很多的話, 這通常會花費不少時間. 雖然也可以把 find 命令放在倒引號內, 但是如果你想要在每找到一個檔案時, 就立即取得它的名稱的話, 可以參考下面的做法 : 
  1. open F, "find / -atime +90 -size +1000 -print|" or die "fork: $!";  
  2. while(){  
  3.         chomp;  
  4.         printf "%s size as %dK and last access time=%s\n", $_, (1023 + -s $_)/1024, -A $_;  
  5. }  
上面代碼會找尋離最近一次存取超過 90 天, 大小超過 1000 個區塊 的檔案. 

捲起袖子玩 fork : 
除了上述的高階介面, Perl 還對 Unix 以及其他系統上 "行程管理之用的低階系統呼叫" 提供了近乎直接的控制. 雖然這裡篇幅不足以詳述全部的細節, 我們還是可以知道一下如何實做這個命令 : 
system "date"; # 高階用法 
讓我們來看看同樣的事, 使用低階的系統呼叫該怎麼來做 : 
  1. defined(my $pid = fork) or die "無法 fork: $!";  
  2. unless($pid) {  
  3.         exec "date";  
  4.         die "Can't execute date:$!";  
  5. }  
  6. #Father process  
  7. waitpid($pid, 0);  
這裡檢查了 fork 的傳回值 ; 它只有在失敗的時候傳回 undef. 如果成功下一列就開始, 就會有兩個不同的行程在執行. 因為只有父行程的 $pid 不是零, 所以就只有子行程會執行 exec 函式. 父行程會略過該部分, 直接執行 waitpid 函式, 也就是在哪裡等待特定的子行程結束 (在這期間, 若是其他子行程結束執行, 會被忽略掉). 這裡的講解只是一部分, 如果要深入了解則可以參考 perldoc 線上文件. 

傳送及接收信號 : 
Unix 信號是傳送給行程的微小訊號. 信號沒辦法表達太多東西. 而 Unix 信號針對不同的情況會有不同的信號. 而不同的信號會有不同名稱 (像是 SIGINT 的意思就是 "中斷" interrupt signal) 以及相應的小整數 (範圍是 1 到16, 1 到 32 或是 1 到 64 依系統而定). 送出信號的時機通常是某些重大事件發生時, 例如在終端機上按下中斷字符 (同常是 Control + C) ; 它會對所有附接在該終端機上的行程, 送出一個 SIGINT. 信號可能是由系統自動送出的, 但也可以是由 Perl 行程送信號給別的行程, 但是得先知道目標行程的識別碼. 要取得它有點複雜, 不過讓我們先假設, 你已經知道程式要送 SIGINT 信號給行程 4201, 作法如下 : 
kill 2, 4021 or die "無法送 SIGNIT 信號給行程 4021: $!"; 

傳送訊號的函示取名為 kill, 是因為信號的主要目的之一就是中止跑了太久的行程, 而信號編號2 就是 SIGINT, 所以這裡的 2 也可以改成字串 'INT'. 如果該行程已經不存在, 傳回值將會是假值, 所以你可以利用這個技巧來判斷某個行程是否還存在. 編號為 0 的特殊訊號表示 "如果可以的話, 我想測試看看能不能送訊號過去 ; 不過我其實並不想這麼做, 所以別真的送訊號過去". 因此探測行程的程式可以寫成這樣 : 
  1. unless (kill 0, $pid) {  
  2.         warn "$pid 已經結束!";  
  3. }  
接收訊號比傳送訊號有趣一點. 你在何種情況會想要這麼做? 假設你有一支程式會在 /tmp 目錄產生檔案. 正常情況下, 程式結束前就會刪除這些檔案, 如果有人按下 Control + C 結果就會在 /tmp 裡留下垃圾, 而這不是很禮貌. 解決這個問題就是設置一個善後的信號處理常式 : 
  1. mkdir $temp_directory, 0700 or die "Error:$!";  
  2.   
  3. sub clean_up {  
  4.         unlink glob "$temp_directory/*";  
  5.         rmdir $temp_directory;  
  6. }  
  7.   
  8. sub my_int_handler {  
  9.         &clean_up;  
  10.         die "Interrupt!...\n";  
  11. }  
  12.   
  13. $SIG{'INT'} = 'my_int_handler';  
  14. .  
  15. .  
  16. # Normal app ending  
  17. &clean_up;  
對雜湊 %SIG 賦值時, 會啟動訊號處理常式 (直到冊銷為止). 雜湊鍵是信號名稱, 雜湊值則是去除 & 符號的賦常式名稱. 從這裡開始只要一有 SIGINT 進來, Perl 就會停止手邊正在執行的任何工作, 立刻跳到副常式裡. 然後結束程式. 但是假如信號處理常式沒有結束程式, 卻直接返回的話, 程式的執行會從先前中斷的地方繼續. 

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!