現在我們來看看如何從 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, 作法如下 :
- my $tarfile = "something*wicked.tar";
- my @dirs = qw(fred|flintstone
betty); - system "tar", "cvf", $tarfile, @dirs;
請注意這和大多數運算符 "真為正常, 假為錯誤" 的一般標準剛好相反 ; 所以我們必須將真假值顛倒過來, 才能運用 "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 & 引數, 作法如下 :
- #!/usr/bin/perl
- chdir "./automation" or die "Can't chdir to ./automation directory: $!";
- system "ls -l";
- exec "java -jar AutomationPM.jar -c &";
這麼做的用處何在? 如果這個 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 或其後的命令做出不正常的動作. 我們可以這麼寫 :
- $ENV{'PATH'} = "/home/rootbetter/bin;$ENV{'PATH'}";
- delete $ENV{'IFS'};
- 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 命令, 每次執行不同的引數 :
- #!/usr/bin/perl
- my @functions = qw{ int rand sleep length hex eof not exit sqrt umask };
- my %about;
- foreach(@functions){
- $about{$_} = `perldoc -t -f $_`;
- }
雖然倒引號很方便, 但是還是建議你 : 用不到命令的輸出值時, 請勿使用倒引號. 因為就算你不想要輸出結果, 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 命令會將目前線上的使用者列出, 每個登入者一行資訊 :
但純量語境我們會一次得到所有的輸出, 但是在串列語境下, 則會自動拆成一列列的資料 :
my @who_lines = `who`;
@who_lines 裡會有數個被拆開的元素, 每一個都是以換列符號做為結尾, 我們可以對各個元素進行 chomp, 以去除換列符號. 或者使用 foreach 來逐項處理 :
- my %ttys;
- foreach(`who`) {
- my($user, $tty, $date) = /(\S+)\s+(\S+)\s+(.*)/;
- $ttys{$user} .= "$tty at $date\n";
- }
- foreach(keys %ttys) {
- print "$_:\n$ttys{$_}";
- }
將行程視為檔案代號 :
到目前為止, 我們所看到的同步行程處理方式, 都是由 Perl 掌控全局 : 啟動一個命令, 然後等著它結束 (或許還會擷取其輸出). 不過 Perl 也可以啟動協同執行的子行程 ; 直到結束之前, 它們都會持續跟 Perl 交換資訊.
要啟動一個協同執行的子行程時, 請將命令放在 open 呼叫的檔名部分, 並且在它的前面或後面加上豎線 (|). 因此這種做法也稱為 "導管式開啟" (piped open) :
- open DATE, "date |" or die "Can't build piped with date: $!";
- open MAIL, "|mail john" or die "Can't build piped with mail: $!";
因此想從 "以讀取模式開啟檔案代號" 中取得資料, 只要採用正常的檔案讀取就行了 :
my $now =
想傳資料給 mail 行程, 只要利用列印至檔案代號的 print 運算就行了 :
print MAIL "Now is $now"; # 假設 $now 以換行符號結束
如果行程連接到某個以讀取模式開啟的檔案代號, 然後它結束執行了, 檔案代號會回傳檔案結尾 ; 當你關閉某個會寫入資料到行程的檔案代號時, 該行程會讀到檔案結尾. 所以要結束電子郵件的寄送, 只要關閉這個檔案代號就行了 :
- close MAIL;
- die "mail: 結束狀態 ($?) 不為零!" if $?;
這些行程間的同步就像 shell 中被管線連結的命令一樣. 如果你只想讀取的話, 除非相要在結果出現時立刻取得, 否則倒引號通常就能輕鬆達成目的. 舉例來說, Unix 的 find 命令可以依照檔案的屬性來找尋檔案的位置 ; 然而如果檔案很多的話, 這通常會花費不少時間. 雖然也可以把 find 命令放在倒引號內, 但是如果你想要在每找到一個檔案時, 就立即取得它的名稱的話, 可以參考下面的做法 :
- open F, "find / -atime +90 -size +1000 -print|" or die "fork: $!";
- while(
){ - chomp;
- printf "%s size as %dK and last access time=%s\n", $_, (1023 + -s $_)/1024, -A $_;
- }
捲起袖子玩 fork :
除了上述的高階介面, Perl 還對 Unix 以及其他系統上 "行程管理之用的低階系統呼叫" 提供了近乎直接的控制. 雖然這裡篇幅不足以詳述全部的細節, 我們還是可以知道一下如何實做這個命令 :
system "date"; # 高階用法
讓我們來看看同樣的事, 使用低階的系統呼叫該怎麼來做 :
- defined(my $pid = fork) or die "無法 fork: $!";
- unless($pid) {
- exec "date";
- die "Can't execute date:$!";
- }
- #Father process
- waitpid($pid, 0);
傳送及接收信號 :
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 的特殊訊號表示 "如果可以的話, 我想測試看看能不能送訊號過去 ; 不過我其實並不想這麼做, 所以別真的送訊號過去". 因此探測行程的程式可以寫成這樣 :
- unless (kill 0, $pid) {
- warn "$pid 已經結束!";
- }
- mkdir $temp_directory, 0700 or die "Error:$!";
- sub clean_up {
- unlink glob "$temp_directory/*";
- rmdir $temp_directory;
- }
- sub my_int_handler {
- &clean_up;
- die "Interrupt!...\n";
- }
- $SIG{'INT'} = 'my_int_handler';
- .
- .
- # Normal app ending
- &clean_up;
沒有留言:
張貼留言