做mac應(yīng)用開發(fā)與IOS一個很大的不同楣嘁,是多進(jìn)程,一個應(yīng)用中存在多個的進(jìn)程篙梢。很多時候我們都有監(jiān)控進(jìn)程的需求淳衙,下面就介紹多種OSX監(jiān)控進(jìn)程的方式,總有一種適合你。
簡介
做一段時間的Mac OS X開發(fā)之后,你將不可避免的遇到需要創(chuàng)建協(xié)作進(jìn)程的情況,例如:
- 在寫一個應(yīng)用程序中肚豺,你可能會想要將一些代碼封裝到一個獨(dú)立的輔助進(jìn)程中去〗缋梗或許你想要將一些不可靠的代碼放到一個獨(dú)立的進(jìn)程中去吸申,這樣,這個進(jìn)程崩潰了也不會影響主程序的運(yùn)行享甸。又或者你可能想要訪問某些不是線程安全的API截碴,而不會鎖定應(yīng)用程序的用戶界面。
- 您可能正在編寫一套合作應(yīng)用程序蛉威。也許你正在編寫一個文字處理器日丹,并且想要調(diào)用一個單獨(dú)的公式編輯器服務(wù)。
- 如果您正在編寫一個守護(hù)進(jìn)程蚯嫌,則可能需要與可訪問每個用戶狀態(tài)的各種代理程序進(jìn)行交互哲虾。
一旦你有多個進(jìn)程,就不可避免的遇到進(jìn)程生命周期的問題:也就是說择示,一個進(jìn)程需要知道另一個進(jìn)程是否在運(yùn)行束凑。這篇文檔描述了多種可以在進(jìn)程啟動或者終止時通知你的方法。它分為兩個主要部分栅盲。監(jiān)控一個你自己啟動的進(jìn)程汪诉,監(jiān)控一個不是你自己啟動的進(jìn)程。最后谈秫,進(jìn)程的序列號中包含了一些本文中討論的進(jìn)程序列號的基礎(chǔ)API的重要信息扒寄。
首先,讓我們談?wù)撘粋€提供了大量關(guān)鍵優(yōu)勢的替代方法拟烫。
重要提示:所有本文中討論的技術(shù)都會在事件發(fā)生變化時通知你旗们。通過輪詢進(jìn)程列表可以獲得相同的信息,但輪詢通常是一個壞主意(它消耗CPU時間构灸,減少電池壽命,增加你設(shè)置進(jìn)程的工作量,并且還會增加響應(yīng)事件的延遲喜颁。)
面向服務(wù)的替代方案
監(jiān)控進(jìn)程生命周期的最常見原因之一是該進(jìn)程為您提供一些服務(wù)稠氮。例如,一個電影轉(zhuǎn)碼應(yīng)用程序半开,該程序通常會將實(shí)際的轉(zhuǎn)碼工作放到一個子進(jìn)程中去執(zhí)行隔披,主進(jìn)程負(fù)責(zé)監(jiān)控子進(jìn)程的工作狀態(tài),一旦子進(jìn)程意外退出了寂拆,主進(jìn)程可以重新啟動它奢米。
你可以通過重新構(gòu)思你的方法來避免這個需求。與其明確的管理你的輔助進(jìn)程狀態(tài)纠永,還不如將其重新定義為應(yīng)用程序所需要的服務(wù)鬓长,然后通過launchd來管理該服務(wù),它將負(fù)責(zé)啟動和終止提供該服務(wù)的進(jìn)程的所有細(xì)節(jié)尝江。
關(guān)于面向服務(wù)的更全面的討論涉波,可以閱讀launchd相關(guān)的文檔。
監(jiān)控自己啟動的進(jìn)程
有許多種不同的方式監(jiān)控自己啟動的進(jìn)程炭序,每種技術(shù)都各有利弊啤覆,閱讀下面的內(nèi)容,以選擇一個最適合自己情況的惭聂。
NSTask
NSTask可以輕松的啟動一個幫助進(jìn)程并等待它結(jié)束窗声。你可以同步等待(使用-[NSTask waitUntilExit]方法),也可以注冊一個通知辜纲,接收NSTaskDidTerminateNotification通知笨觅。代碼清單1展示了同步方式,代碼清單2展示了異步的方式侨歉。
清單1:同步使用NSTask
- (IBAction)testNSTaskSync:(id)sender
{
NSTask * syncTask;
syncTask = [NSTask
launchedTaskWithLaunchPath:@"/bin/sleep"
arguments:[NSArray arrayWithObject:@"1"]
];
[syncTask waitUntilExit];
}
清單2:異步使用NSTask
- (IBAction)testNSTaskAsync:(id)sender{
task = [[NSTask alloc] init];
[task setLaunchPath:@"/bin/sleep"];
[task setArguments:[NSArray arrayWithObject:@"1"]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskExited:) name:NSTaskDidTerminateNotification object:task ];
[task launch];
//在下面的-taskExited:中繼續(xù)執(zhí)行屋摇。
}
- (void)taskExited:(NSNotification *)note{
// 收到通知!
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSTaskDidTerminateNotification object:task ];
[task release];
task = nil;
}
進(jìn)程死亡事件
如果您使用基于序列號的API啟動應(yīng)用程序,則可以通過注冊kAEApplicationDiedApple事件來得知其終止幽邓。
重要提示:此事件僅適用于您啟動的應(yīng)用程序炮温。
清單3顯示了如何注冊和處理應(yīng)用程序死亡事件。
清單3:使用進(jìn)程死亡事件
- (IBAction)testApplicationDied:(id)sender
{
NSURL * url;
static BOOL sHaveInstalledAppDiedHandler;
if ( ! sHaveInstalledAppDiedHandler ) {
(void) AEInstallEventHandler(
kCoreEventClass,
kAEApplicationDied,
(AEEventHandlerUPP) AppDiedHandler,
(SRefCon) self,
false
);
sHaveInstalledAppDiedHandler = YES;
}
url = [NSURL fileURLWithPath:@"/Applications/TextEdit.app"];
(void) LSOpenCFURLRef( (CFURLRef) url, NULL);
// Execution continues in AppDiedHandler, below.
}
static OSErr AppDiedHandler(
const AppleEvent * theAppleEvent,
AppleEvent * reply,
SRefCon handlerRefcon
)
{
SInt32 errFromEvent;
ProcessSerialNumber psn;
DescType junkType;
Size junkSize;
(void) AEGetParamPtr(
theAppleEvent,
keyErrorNumber,
typeSInt32,
&junkType,
&errFromEvent,
sizeof(errFromEvent),
&junkSize
);
(void) AEGetParamPtr(
theAppleEvent,
keyProcessSerialNumber,
typeProcessSerialNumber,
&junkType,
&psn,
sizeof(psn),
&junkSize
);
// You've been notified!
NSLog(
@"died %lu.%lu %d",
(unsigned long) psn.highLongOfPSN,
(unsigned long) psn.lowLongOfPSN,
(int) errFromEvent
);
return noErr;
}
重要提示:進(jìn)程死亡事件基于序列號牵舵,這是一個具有一些重要后果的事實(shí)柒啤。有關(guān)詳細(xì)信息,請參閱過程序列號畸颅。
UNIX方式
Mac OS X的BSD子系統(tǒng)有兩個開啟新進(jìn)程的基本API:
fork和exec—這種技術(shù)起源于第一個UNIX系統(tǒng)担巩,使用fork創(chuàng)建一個進(jìn)程,是對當(dāng)前進(jìn)程的精確克隆没炒,而exec(實(shí)際上是一個基于exec的例程簇)會使當(dāng)前進(jìn)程去啟動運(yùn)行一個新的可執(zhí)行文件涛癌。
posix spawn—這個API就像fork和exec的組合。它是在Mac OS X 10.5中引入的。
在上述兩種情況下拳话,生成的進(jìn)程都是當(dāng)前進(jìn)程的子進(jìn)程先匪。有兩種傳統(tǒng)的UNIX方法能知道子進(jìn)程的死亡:同步方法:使用一系列的等待例程(典型的:waitpid)
異步方法:通過SIGCHLD信號(SIGCHLD,在一個進(jìn)程終止或者停止時弃衍,將SIGCHLD信號發(fā)送給其父進(jìn)程呀非,按系統(tǒng)默認(rèn)將忽略此信號,如果父進(jìn)程希望被告知其子系統(tǒng)的這種狀態(tài)镜盯,則應(yīng)捕捉此信號岸裙。)
在許多情況下同步等待是比較適用的方法,例如速缆,如果父進(jìn)程在子進(jìn)程完成之前無法進(jìn)行降允,理所應(yīng)當(dāng)?shù)膽?yīng)該同步等待。清單4顯示了如何fork激涤,然后exec 拟糕,再等待的示例。
清單4:Fork, exec, wait
extern char **environ;
- (IBAction)testWaitPID:(id)sender
{
pid_t pid;
char * args[3] = { "/bin/sleep", "1", NULL };
pid_t waitResult;
int status;
// I used fork/exec rather than posix_spawn because I would like this
// code to be compatible with 10.4.x.
pid = fork();
switch (pid) {
case 0:
// child
(void) execve(args[0], args, environ);
_exit(EXIT_FAILURE);
break;
case -1:
// error
break;
default:
// parent
break;
}
if (pid >= 0) {
do {
waitResult = waitpid(pid, &status, 0);
} while ( (waitResult == -1) && (errno == EINTR) );
}
}
另一方面倦踢,有些情況同步等待是一個非常糟糕的主意送滞。例如,如果您正在應(yīng)用程序的主線程上面運(yùn)行辱挥,并且子進(jìn)程可能會執(zhí)行一個耗時操作犁嗅,則不希望阻塞應(yīng)用程序的用戶界面等待該子進(jìn)程退出。
在這種情況下晤碘,您可以通過監(jiān)聽SIGCHLD信號異步等待褂微。
重要提示:如果你使用監(jiān)聽SIGCHLD信號異步等待的方式,你仍然需要通過調(diào)用等待例程來獲取子進(jìn)程园爷,否則將會導(dǎo)致僵尸進(jìn)程宠蚂。
由于與信號處理程序相關(guān)聯(lián)的環(huán)境很復(fù)雜,可能監(jiān)聽信號會很棘手童社。具體來說求厕,如果你使用了一個信號處理程序(signal或sigaction,那么你必須非常小心你在該處理程序中所做的工作扰楼。很少的函數(shù)能被信號處理程序安全的調(diào)用呀癣,例如,使用malloc分配內(nèi)存空間就是不安全的弦赖。
內(nèi)唄信號處理程序安全調(diào)用的函數(shù)(async-signal safe函數(shù))列在sigaction手冊頁上项栏。
在大部分情況下,你必須采用額外的手段蹬竖,將傳入信號重定向到更加合理的環(huán)境中沼沈。有兩種標(biāo)準(zhǔn)的做法:
- sockets(套接字)—在此技術(shù)中流酬,您將創(chuàng)建一個UNIX域套接字對,并使用CFSocket將一端添加到您的循環(huán)中列另。當(dāng)信號到達(dá)時康吵,信號處理器將一個虛擬消息寫入套接字。這將喚醒循環(huán)访递,并允許您在安全的環(huán)境中處理信號。要查看此技術(shù)的演示同辣,請查看示例代碼“CFLocalServer”中的InstallSignalToSocket例程拷姿。
- kqueues —kqueue機(jī)制允許您收聽信號而不安裝任何信號處理程序。所以你可以創(chuàng)建一個kqueue旱函,指示它來監(jiān)聽SIGCHLD信號响巢,然后將其包裝在一個CFFileDescriptor中并將其添加到你的runloop中。當(dāng)信號到達(dá)時棒妨,與CFFileDescriptor關(guān)聯(lián)的回調(diào)例程運(yùn)行踪古,您可以在安全的環(huán)境中處理信號。要查看此技術(shù)的演示券腔,請查看示例代碼“PreLoginAgents”中的InstallHandleSIGTERMFromRunLoop例程伏穆。
重要提示: kqueue技術(shù)需要Mac OS X 10.5或更高版本,因?yàn)樗褂肅FFileDescriptor纷纫。
UNIX替代方案
處理SIGCHLD信號有許多陷阱枕扫。上一節(jié)描述了最深刻的一部分,但還有其他部分辱魁。當(dāng)你在寫library code的時候烟瞧,使用SIGCHLD是一件非常棘手的事情,因?yàn)镾IGCHLD由主程序本身控制染簇,你的library code不能要求其被設(shè)置為某種方式参滴。
有多種方法來避免SIGCHLD的這種混亂,一種方式就是創(chuàng)建一個域套接字對锻弓,并且為了使子進(jìn)程具有引用一端的唯一描述符砾赔,父進(jìn)程具有另一端的描述符。當(dāng)子進(jìn)程中止的時候弥咪,系統(tǒng)關(guān)閉子進(jìn)程的描述符过蹂,這導(dǎo)致套接字的另一端指向文件的結(jié)尾(這意味著它變成可讀的了,但是當(dāng)你嘗試讀取的時候聚至,返回的是0).當(dāng)父進(jìn)程監(jiān)控到文件結(jié)束的狀態(tài)酷勺,就可以獲取子進(jìn)程信息。清單5展示了這種方案的示例扳躬。
清單5:使用套接字監(jiān)測子進(jìn)程的中止
- (IBAction)testSocketPair:(id)sender
{
int fds[2];
int remoteSocket;
int localSocket;
CFSocketContext context = { 0, self, NULL, NULL, NULL };
CFRunLoopSourceRef rls;
char * args[3] = { "/bin/sleep", "1", NULL } ;
// Create a socket pair and wrap the local end up in a CFSocket.
(void) socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
remoteSocket = fds[0];
localSocket = fds[1];
socket = CFSocketCreateWithNative(
NULL,
localSocket,
kCFSocketDataCallBack,
SocketClosedSocketCallBack,
&context
);
CFSocketSetSocketFlags(
socket,
kCFSocketAutomaticallyReenableReadCallBack | kCFSocketCloseOnInvalidate
);
// Add the CFSocket to our runloop.
rls = CFSocketCreateRunLoopSource(NULL, socket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
CFRelease(rls);
// fork and exec the child process.
childPID = fork();
switch (childPID) {
case 0:
// child
(void) execve(args[0], args, environ);
_exit(EXIT_FAILURE);
break;
case -1:
// error
break;
default:
// parent
break;
}
// Close our reference to the remote socket. The only reference remaining
// is the one in the child. When that dies, the socket will become readable.
(void) close(remoteSocket);
// Execution continues in SocketClosedSocketCallBack, below.
}
static void SocketClosedSocketCallBack(
CFSocketRef s,
CFSocketCallBackType type,
CFDataRef address,
const void * data,
void * info
)
{
int waitResult;
int status;
// Reap the child.
do {
waitResult = waitpid( ((AppDelegate *) info)->childPID, &status, 0);
} while ( (waitResult == -1) && (errno == EINTR) );
// You've been notified!
}
監(jiān)控任一進(jìn)程
如果要監(jiān)控一個非自己啟動的進(jìn)程脆诉,則可選用的方案比較少甚亭。但是,這些可以的方案就可以滿足我們大部分的需求击胜。此外亏狰,你要根據(jù)自己的情況選擇正確的API,閱讀以下部分以讓你了解哪個API是適合你的偶摔。
NSWorkspace
NSWorkspace提供了一個非常簡單的方式來監(jiān)控進(jìn)程的啟動和退出暇唾。
要注冊這些通知,您必須:
1辰斋、獲得NSWorkspace的自定義通知中心 策州,調(diào)用-[NSWorkspace notificationCenter]
2、添加NSWorkspaceDidLaunchApplicationNotification和NSWorkspaceDidTerminateApplicationNotification事件的觀察者
當(dāng)收到通知的時候宫仗,user info字典包含了受影響進(jìn)程的信息够挂。NSWorkspace.h頭文件中列出了user info字典的key,以"NSApplicationPath"開頭藕夫。
清單6顯示了如何使用NSWorkspace獲取應(yīng)用程序啟動和終止的示例孽糖。
清單6:使用NSWorkspace獲取應(yīng)用程序啟動和終止
- (IBAction)testNSWorkspace:(id)sender
{
NSNotificationCenter * center;
NSLog(@"-[AppDelegate testNSWorkspace:]");
// Get the custom notification center.
center = [[NSWorkspace sharedWorkspace] notificationCenter];
// Install the notifications.
[center addObserver:self
selector:@selector(appLaunched:)
name:NSWorkspaceDidLaunchApplicationNotification
object:nil
];
[center addObserver:self
selector:@selector(appTerminated:)
name:NSWorkspaceDidTerminateApplicationNotification
object:nil
];
// Execution continues in -appLaunched: and -appTerminated:, below.
}
- (void)appLaunched:(NSNotification *)note
{
NSLog(@"launched %@\n", [[note userInfo] objectForKey:@"NSApplicationName"]);
// You've been notified!
}
- (void)appTerminated:(NSNotification *)note
{
NSLog(@"terminated %@\n", [[note userInfo] objectForKey:@"NSApplicationName"]);
// You've been notified!
}
重要提示: NSWorkspace基于序列號,這是一個具有一些重要后果的事實(shí)毅贮。有關(guān)詳細(xì)信息办悟,請參閱過程序列號
Carbon Event Manager
Carbon Event Manager 發(fā)送大量的與進(jìn)程管理相關(guān)的事件,具體來說嫩码,當(dāng)應(yīng)用程序啟動的時候誉尖,發(fā)送kEventAppLaunched事件,當(dāng)應(yīng)用程序中止時铸题,發(fā)送kEventAppTerminated事件铡恕,你可以像任何其他Carbon事件一樣注冊這些事件。清單7顯示了一個例子丢间。
調(diào)用事件處理程序時探熔,kEventParamProcessID參數(shù)將包含受影響的進(jìn)程的ProcesSerialNumber。
重要提示:當(dāng)您的應(yīng)用程序收到該kEventAppTerminated事件時烘挫,終止應(yīng)用程序可能已經(jīng)退出诀艰。因此,您無法獲取有關(guān)該應(yīng)用程序的信息GetProcessInformation饮六。如果您需要有關(guān)終止應(yīng)用程序的信息其垄,則必須提前緩存。
清單7:使用Carbon事件來獲取應(yīng)用程序啟動和終止
- (IBAction)testCarbonEvents:(id)sender
{
static EventHandlerRef sCarbonEventsRef = NULL;
static const EventTypeSpec kEvents[] = {
{ kEventClassApplication, kEventAppLaunched },
{ kEventClassApplication, kEventAppTerminated }
};
if (sCarbonEventsRef == NULL) {
(void) InstallEventHandler(
GetApplicationEventTarget(),
(EventHandlerUPP) CarbonEventHandler,
GetEventTypeCount(kEvents),
kEvents,
self,
&sCarbonEventsRef
);
}
// Execution continues in CarbonEventHandler, below.
}
static OSStatus CarbonEventHandler(
EventHandlerCallRef inHandlerCallRef,
EventRef inEvent,
void * inUserData
)
{
ProcessSerialNumber psn;
(void) GetEventParameter(
inEvent,
kEventParamProcessID,
typeProcessSerialNumber,
NULL,
sizeof(psn),
NULL,
&psn
);
switch ( GetEventKind(inEvent) ) {
case kEventAppLaunched:
NSLog(
@"launched %u.%u",
(unsigned int) psn.highLongOfPSN,
(unsigned int) psn.lowLongOfPSN
);
// You've been notified!
break;
case kEventAppTerminated:
NSLog(
@"terminated %u.%u",
(unsigned int) psn.highLongOfPSN,
(unsigned int) psn.lowLongOfPSN
);
// You've been notified!
break;
default:
assert(false);
}
return noErr;
}
kqueues
NSWorkspace和Carbon事件只能在單個GUI登錄上下文中工作卤橄。如果您正在編寫一個不在GUI登錄上下文中運(yùn)行的程序(也許是守護(hù)程序)绿满,或者您需要監(jiān)視與運(yùn)行時不同的上下文中的進(jìn)程,則需要考慮替代方法窟扑。 kqueue NOTE_EXIT
事件是一個不錯的選擇喇颁。您可以使用它來檢測進(jìn)程何時退出漏健,無論它運(yùn)行的是哪個上下文。與NSWorkspace和Carbon事件不同橘霎,您必須準(zhǔn)確指定要監(jiān)視的進(jìn)程; 否則任何進(jìn)程的中止都無法得到通知蔫浆。
清單8是一個簡單的例子,說明如何使用kqueue來監(jiān)視特定進(jìn)程的終止姐叁。
清單8:使用kqueue監(jiān)視特定進(jìn)程
static pid_t gTargetPID = -1;
// We assume that some other code sets up gTargetPID.
- (IBAction)testNoteExit:(id)sender
{
FILE * f;
int kq;
struct kevent changes;
CFFileDescriptorContext context = { 0, self, NULL, NULL, NULL };
CFRunLoopSourceRef rls;
// Create the kqueue and set it up to watch for SIGCHLD. Use the
// new-in-10.5 EV_RECEIPT flag to ensure that we get what we expect.
kq = kqueue();
EV_SET(&changes, gTargetPID, EVFILT_PROC, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL);
(void) kevent(kq, &changes, 1, &changes, 1, NULL);
// Wrap the kqueue in a CFFileDescriptor (new in Mac OS X 10.5!). Then
// create a run-loop source from the CFFileDescriptor and add that to the
// runloop.
noteExitKQueueRef = CFFileDescriptorCreate(NULL, kq, true, NoteExitKQueueCallback, &context);
rls = CFFileDescriptorCreateRunLoopSource(NULL, noteExitKQueueRef, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
CFRelease(rls);
CFFileDescriptorEnableCallBacks(noteExitKQueueRef, kCFFileDescriptorReadCallBack);
// Execution continues in NoteExitKQueueCallback, below.
}
static void NoteExitKQueueCallback(
CFFileDescriptorRef f,
CFOptionFlags callBackTypes,
void * info
)
{
struct kevent event;
(void) kevent( CFFileDescriptorGetNativeDescriptor(f), NULL, 0, &event, 1, NULL);
NSLog(@"terminated %d", (int) (pid_t) event.ident);
// You've been notified!
}
進(jìn)程序列號(Process Serial Numbers)
Mac OS X具有許多用于進(jìn)程管理的高級API瓦盛,可以按進(jìn)程序列號(ProcessSerialNumber)進(jìn)行處理。這些包括啟動服務(wù)外潜,進(jìn)程管理器和NSWorkspace谭溉。這些API都有三個重要的功能:
它們在單個GUI登錄會話的上下文中工作。例如橡卤,如果您使用NSWorkspace來觀察正在啟動和終止的應(yīng)用程序,那么只會在同一個GUI登錄會話中運(yùn)行的應(yīng)用程序被通知损搬。
他們只看到連接到窗口服務(wù)器的進(jìn)程碧库。
例如,如果您使用NSTask來運(yùn)行BSD命令行工具巧勤,如find嵌灰,那么基于NSWorkspace的觀察者將不會被通知該工具的啟動或終止。它們通常不能在GUI登錄上下文之外運(yùn)行的進(jìn)程(例如颅悉,守護(hù)程序)使用