如何在macOS上异步运行Bash脚本并获取其退出码?
针对Launch Daemon的Bash脚本异步执行方案
问题1:异步等待子进程结束,避免阻塞父进程
在macOS环境下,推荐使用GCD(Grand Central Dispatch)的dispatch_source实现异步子进程监控,代码简洁且适配launch daemon场景,还支持随时取消监控以满足daemon退出需求。
实现代码
#include <dispatch/dispatch.h> #include <sys/wait.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> // 封装回调参数的结构体 typedef struct { void (*callback)(int exitCode, const void* param); const void* param; } CallbackData; void RunExternalScript(const char* pScriptFilePath, void (*pfnCallback)(int exitCode, const void* pParam), const void* pParam) { pid_t pidChild = fork(); if (pidChild == 0) { // 子进程:修正execl参数格式,第一个参数为程序路径,第二个为argv[0],最终以NULL结尾 execl(pScriptFilePath, pScriptFilePath, (const char*)NULL); // 执行失败时输出日志,用_exit而非exit避免触发父进程的atexit钩子 log("Error %d running script: %s", errno, pScriptFilePath); _exit(255); // 约定255为脚本启动失败的退出码 } else if (pidChild != -1) { // 父进程:创建GCD进程监控源 CallbackData* data = malloc(sizeof(CallbackData)); data->callback = pfnCallback; data->param = pParam; dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pidChild, DISPATCH_PROC_EXIT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); if (source) { // 设置子进程退出时的回调逻辑 dispatch_source_set_event_handler(source, ^{ int exitCode = -1; pid_t pid = dispatch_source_get_handle(source); int status; // 非阻塞获取子进程退出状态 if (waitpid(pid, &status, WNOHANG) == pid) { if (WIFEXITED(status)) { exitCode = WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { // 子进程被信号终止,用负信号值标记退出码 exitCode = -WTERMSIG(status); } } // 调用用户传入的回调 data->callback(exitCode, data->param); // 清理资源 dispatch_source_cancel(source); free(data); }); // 设置监控源取消时的清理逻辑 dispatch_source_set_cancel_handler(source, ^{ dispatch_release(source); }); // 启动监控源 dispatch_resume(source); } else { log("Failed to create dispatch source for pid %d", pidChild); free(data); pfnCallback(-1, pParam); } } else { log("Error forking process: %s", pScriptFilePath); pfnCallback(-1, pParam); } }
核心要点
- 用
dispatch_source_create监听DISPATCH_PROC_EXIT事件,子进程退出时自动触发回调 - 通过
waitpid配合WNOHANG非阻塞获取退出状态,避免阻塞GCD队列 - 支持调用
dispatch_source_cancel随时终止监控,适配daemon退出场景 - 区分正常退出和信号终止两种情况,返回对应退出码
问题2:父进程如何获知子进程启动Bash脚本失败
子进程中execl调用失败无法直接通知父进程,可通过约定退出码或管道传递错误码两种方式实现:
方式1:约定退出码(简易版)
子进程在execl失败时返回约定的退出码(比如255),父进程在回调中判断该值即可确定启动失败:
// 子进程失败时 _exit(255); // 父进程回调中判断 if (exitCode == 255) { // 脚本启动失败 }
方式2:管道传递具体错误码(进阶版)
如果需要获取启动失败的具体errno(比如ENOENT表示脚本不存在、EACCES表示权限不足),可以用管道传递:
#include <dispatch/dispatch.h> #include <sys/wait.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> typedef struct { void (*callback)(int exitCode, int execErrno, const void* param); const void* param; int pipefd; // 管道读端 } CallbackData; void RunExternalScript(const char* pScriptFilePath, void (*pfnCallback)(int exitCode, int execErrno, const void* pParam), const void* pParam) { int pipefd[2]; if (pipe(pipefd) == -1) { log("Error creating pipe: %s", pScriptFilePath); pfnCallback(-1, errno, pParam); return; } pid_t pidChild = fork(); if (pidChild == 0) { close(pipefd[0]); // 子进程关闭读端 execl(pScriptFilePath, pScriptFilePath, (const char*)NULL); // 执行失败,将errno写入管道 int err = errno; write(pipefd[1], &err, sizeof(err)); close(pipefd[1]); _exit(255); } else if (pidChild != -1) { close(pipefd[1]); // 父进程关闭写端 CallbackData* data = malloc(sizeof(CallbackData)); data->callback = pfnCallback; data->param = pParam; data->pipefd = pipefd[0]; dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pidChild, DISPATCH_PROC_EXIT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); if (source) { dispatch_source_set_event_handler(source, ^{ int exitCode = -1; int execErrno = 0; pid_t pid = dispatch_source_get_handle(source); int status; if (waitpid(pid, &status, WNOHANG) == pid) { if (WIFEXITED(status)) { exitCode = WEXITSTATUS(status); if (exitCode == 255) { // 读取子进程写入的错误码 read(data->pipefd, &execErrno, sizeof(execErrno)); } } else if (WIFSIGNALED(status)) { exitCode = -WTERMSIG(status); } } data->callback(exitCode, execErrno, data->param); close(data->pipefd); dispatch_source_cancel(source); free(data); }); dispatch_source_set_cancel_handler(source, ^{ close(data->pipefd); dispatch_release(source); free(data); }); dispatch_resume(source); } else { log("Failed to create dispatch source for pid %d", pidChild); close(pipefd[0]); free(data); pfnCallback(-1, errno, pParam); } } else { log("Error forking process: %s", pScriptFilePath); close(pipefd[0]); close(pipefd[1]); pfnCallback(-1, errno, pParam); } }
核心要点
- 子进程通过管道将
execl失败的errno传递给父进程 - 父进程在回调中读取管道内容,获取具体错误原因
内容的提问来源于stack exchange,提问作者c00000fd
相关产品推荐
相关产品推荐

