Cocoaプログラミング:pipeでプロセス間通信を試す
最近MacBook Airを買ったのですが、せっかくMacを買ったのでCocoaをやらねばということで、Cocoaプログラミングをちまちまと勉強しています。手始めに詳解 Objective-C 2.0を読んだのですが、プロセス間通信のフレームワークの解説が少なかったので、少し勉強してみました。
pipeを試す
pipeを使うためのクラスであるNSPipeを試してみたいと思います。
まずは、Cでひとりpipeをread/writeするコードです。
#import <stdlib.h> #import <stdio.h> #import <string.h> #import <unistd.h> static void pipe_test(void) { const char message[] = "hello"; char buffer[1024]; int pipes[2]; if (pipe(pipes) != 0){ goto err; } printf("read_fd = %d write_fd = %d\n", pipes[0], pipes[1]); // write data write(pipes[1], message, sizeof(message)); // read data read(pipes[0], buffer, sizeof(message)); printf("reslut => %s\n", buffer); err: return; }
Xcodeでの実行すると下記のようになります。
read_fd = 3 write_fd = 4 reslut => hello
pipe()によりread/write用のfdが割り当てられますが、それぞれ3,4となっています。UNIXの場合、標準入力/標準出力/標準エラー出力がそれぞれ0,1,2となるので、期待通りですね。
これをNSPipeクラスを使えば、以下のように書き替えることができます。
#import <Foundation/Foundation.h> #import <stdlib.h> static void nspipe_test(void) { const char message[] = "hello"; @autoreleasepool { NSPipe *pipe = [[NSPipe alloc] init]; printf("read_fd = %d write_fd = %d\n", [[pipe fileHandleForReading] fileDescriptor], [[pipe fileHandleForWriting] fileDescriptor]); // write data NSData *inData = [[NSData alloc] init]; inData = [inData initWithBytes:message length:sizeof(message)]; [[pipe fileHandleForWriting] writeData:inData]; // read data NSData *outData = [[pipe fileHandleForReading] readDataOfLength:sizeof(message)]; printf("result => %s\n", [[[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding] UTF8String]); } return; }
結果はCの例と同じです。
read_fd = 3 write_fd = 4 reslut => hello
NSPipeクラスには上記の通りデータ入出力用のメソッドが用意されていますので、NSPipeオブジェクトを操作するだけよいことが分かります。
また、読み出し側の実装は[pipe fileHandleForReading] readDataToEndOfFile]でもよいですが、その場合EOFを検出するまでブロッキングされます。具体的には、[pipe fileHandleForWriting] closeFile]のようにwrite側がクローズされるまでブロックされます。
pipeでプロセス間通信を行う
fork/exec()による実行結果をpipeでつなぐことで、2プロセス間のデータ通信を行うコードを書いてみます。
具体的には、write側のプロセス "ps -a" の結果を、read側のプロセス ”cat" で出力するコードです。
まずはCの例です。
static void pipe_test2(void) { int pipes[2]; if (pipe(pipes) != 0){ goto err; } int ret = fork(); // child process if (ret == 0){ char* cmd[] = {"cat", 0}}; printf("child process start\n"); close(0); int new_fd = dup(pipes[0]); printf("#c read_fd = %d write_fd = %d new_fd = %d\n", pipes[0], pipes[1], new_fd); close(pipes[0]); close(pipes[1]); execvp(cmd[0], cmd); } // parent process else { char* cmd[] = {"ps", "-a" ,0}; printf("parent process start\n"); close(1); int new_fd = dup(pipes[1]); printf("#p read_fd = %d write_fd = %d new_fd = %d\n", pipes[0], pipes[1], new_fd); close(pipes[0]); close(pipes[1]); execvp(*cmd[0], cmd); } err: return; }
実行結果は以下の通りとなりました。
Xcodeで実行すると、write側のプロセス(親プロセス)の "ps -a" の結果が、read側のプロセス(子プロセス)に渡って出力されます。
ただし、なぜかGDBでbreakがかかるしオプション "-a" も伝わってないですが・・・(もし分かる方がいらっしゃれば教えて下さい><)
[Switching to process 918 thread 0x0] #child read_fd = 3 write_fd = 4 new_fd = 0 #parent read_fd = 3 write_fd = 4 new_fd = 1 (gdb) c Continuing. PID TTY TIME CMD 152 ttys000 0:00.00 (login) 153 ttys000 0:00.00 -bash Program ended with exit code: 0
これをNSPipe/NSTaskを使って書き直してみます。
static void nspipe_test3(void) { @autoreleasepool { NSPipe *pipe = [[NSPipe alloc] init]; NSTask *task1 = [[NSTask alloc] init]; [task1 setLaunchPath:@"/bin/cat"]; [task1 setStandardInput:pipe]; [task1 launch]; NSTask *task2 = [[NSTask alloc] init]; [task2 setLaunchPath:@"/bin/ps"]; [task2 setArguments:[NSArray arrayWithObjects:@"-a", nil]]; [task2 setStandardOutput:pipe]; [task2 launch]; [task1 waitUntilExit]; [task2 waitUntilExit]; } return; }
NSTaskオブジェクトがfork/execの実行を肩代わりしてくれるので、実装としてもすごく簡潔になりました(^^)
実行結果は下記の通りです。
[Switching to process 817 thread 0x0] PID TTY TIME CMD 152 ttys000 0:00.03 login -pfl uents /bin/bash -c exec -la bash /bin/bash 153 ttys000 0:00.09 -bash Program ended with exit code: 0
Cocoaで外部プログラムを実行する時は、forkするよりもNSTaskを使う方が、はるかに楽できそうです。