4. 进程管理API:spawn()、fork()、exec() 函数详解、进程退出与等待、进程属性设置
好,咱们进入第四章。这一章聊的是进程管理,说白了就是怎么创建进程、怎么结束进程、怎么控制进程的行为。在QNX里,进程管理跟Linux有些相似,但细节上差别不小。我刚开始接触QNX时,就因为在fork()上踩了坑,折腾了一整天。
嗯,咱们一个一个来。
4.1 创建进程:spawn() vs fork() vs exec()
QNX提供了三种创建进程的方式。你可能会问:为什么需要三个?其实它们各有各的用武之地。
4.1.1 spawn() —— 我推荐的首选
我个人习惯,在QNX里创建新进程,首选spawn()。为什么?因为它一步到位,直接把新程序加载运行,不需要中间步骤。
#include <spawn.h>
#include <sys/neutrino.h>
pid_t pid;
int ret;
char *argv[] = {"/usr/bin/myapp", "-c", "/etc/myapp.conf", NULL};
char *envp[] = {"PATH=/usr/bin", "HOME=/root", NULL};
// 创建新进程,直接运行 myapp
ret = spawn("/usr/bin/myapp", 0, NULL, NULL, argv, envp);
if (ret == -1) {
// 处理错误
perror("spawn failed");
}
你看,代码很简洁。spawn()会创建一个新进程,然后立即加载并执行指定的程序。这在嵌入式系统里特别实用——你想想看,很多时候我们就是需要启动一个独立的任务,比如一个网络服务、一个传感器采集程序。
我的经验:spawn()在QNX Neutrino里是POSIX标准的,跨平台性好。我在一个车载项目中,需要同时启动12个传感器处理进程,用的就是spawn()。配合进程组管理,非常干净。
4.1.2 fork() —— 小心使用
fork()在Linux里很常见,但在QNX里,我建议你谨慎使用。为什么?因为QNX是微内核架构,fork()的开销比Linux大得多。
#include <unistd.h>
#include <sys/neutrino.h>
pid_t pid;
pid = fork();
if (pid == -1) {
// fork 失败
perror("fork failed");
} else if (pid == 0) {
// 子进程
printf("I am child, PID = %d\n", getpid());
// 通常紧接着调用 exec()
execl("/usr/bin/myapp", "myapp", NULL);
// 如果 execl 返回,说明出错了
_exit(1);
} else {
// 父进程
printf("I am parent, child PID = %d\n", pid);
}
fork()会复制当前进程的完整地址空间。在QNX里,这意味着要复制消息队列、通道连接、内存映射等资源。我曾经在一个项目中,用fork()创建了50个进程,结果系统内存直接爆了。
避坑指南:我曾经在QNX里用fork()创建子进程,然后子进程里又打开了文件描述符。结果父进程的文件描述符也被复制了,导致资源泄漏。记住:fork()复制的是整个进程状态,包括锁、文件描述符、定时器等。如果你只需要运行一个新程序,用spawn()更安全。
4.1.3 exec() 家族 —— 替换当前进程
exec()不创建新进程,它是在当前进程里加载新程序。说白了,就是把当前进程的代码段、数据段全部替换掉。
#include <unistd.h>
// 常见的 exec 变体
execl("/usr/bin/myapp", "myapp", "-v", NULL);
execv("/usr/bin/myapp", argv);
execle("/usr/bin/myapp", "myapp", NULL, envp);
execve("/usr/bin/myapp", argv, envp);
execlp("myapp", "myapp", NULL); // 在 PATH 中搜索
execvp("myapp", argv); // 在 PATH 中搜索
exec()通常和fork()配合使用:先fork()出一个子进程,然后在子进程里调用exec()加载新程序。但在QNX里,我建议直接用spawn()替代这个组合。
| 函数 | 特点 | QNX推荐度 |
|---|---|---|
| spawn() | 一步创建+加载,资源开销小 | ⭐⭐⭐⭐⭐ |
| fork() | 复制进程,开销大,易出错 | ⭐⭐ |
| exec() | 替换当前进程,常与fork配合 | ⭐⭐⭐ |
4.2 进程退出与等待
进程创建完了,总得知道它什么时候结束、结束得怎么样吧?这就是进程退出和等待机制要解决的问题。
4.2.1 进程退出方式
进程退出有三种方式:
- 正常退出:main()函数返回,或者调用exit()、_exit()
- 异常退出:收到信号,比如SIGKILL、SIGSEGV
- 主动退出:调用abort()
#include <stdlib.h>
#include <unistd.h>
// 方式1:main 返回
int main() {
// ... 业务逻辑 ...
return 0; // 正常退出,返回0表示成功
}
// 方式2:调用 exit()
void some_function() {
// ... 处理 ...
exit(EXIT_SUCCESS); // 会执行 atexit 注册的函数
}
// 方式3:调用 _exit()
void error_handler() {
_exit(EXIT_FAILURE); // 立即退出,不执行清理函数
}
这里有个细节:exit()会调用所有通过atexit()注册的清理函数,然后刷新缓冲区,最后才进入内核。而_exit()是直接进入内核,不做任何清理。我在调试一个内存泄漏问题时,就发现是exit()里的清理函数没执行对,换成_exit()反而暴露了问题。
4.2.2 等待子进程:wait() 和 waitpid()
父进程需要知道子进程什么时候结束。用wait()或waitpid()来等待。
#include <sys/wait.h>
#include <unistd.h>
pid_t child_pid;
int status;
// 等待任意子进程
child_pid = wait(&status);
if (child_pid == -1) {
perror("wait failed");
} else {
if (WIFEXITED(status)) {
printf("Child %d exited with code %d\n",
child_pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child %d killed by signal %d\n",
child_pid, WTERMSIG(status));
}
}
// 等待特定子进程,非阻塞模式
pid_t target_pid = 1234;
child_pid = waitpid(target_pid, &status, WNOHANG);
if (child_pid == 0) {
// 子进程还在运行
printf("Child still running\n");
} else if (child_pid == target_pid) {
// 子进程已结束
printf("Child finished\n");
}
关键点:在QNX里,如果父进程不调用wait(),子进程结束后会变成僵尸进程。僵尸进程不占用CPU,但会占用进程表项。进程表满了,新进程就创建不了。我曾经在一个长期运行的服务器程序里忘了wait(),结果运行一周后,系统提示"Resource temporarily unavailable"——进程表满了。
4.2.3 等待特定事件:SyncMutexEventWait()
QNX还提供了一种更灵活的等待机制,用于等待进程状态变化。
#include <sys/neutrino.h>
#include <sys/wait.h>
pid_t pid;
int status;
struct sigevent event;
// 设置事件通知
SIGEV_SIGNAL_INIT(&event, SIGCHLD);
// 等待子进程退出
pid = waitpid(-1, &status, 0);
// 或者使用同步消息
int coid = ConnectAttach(0, pid, 0, 0, 0);
// ... 使用 MsgSend 等函数进行同步
4.3 进程属性设置
进程创建后,我们可以调整它的各种属性。这就像给进程"定制"运行环境。
4.3.1 进程优先级
QNX是实时操作系统,优先级管理是核心。我习惯用ThreadCtl()来设置线程优先级,但进程级别的优先级设置也很重要。
#include <sys/neutrino.h>
#include <sched.h>
struct sched_param param;
int policy;
// 获取当前调度策略和优先级
policy = SCHED_RR; // 轮转调度
param.sched_priority = 20; // 优先级范围 1-255
// 设置进程的调度策略和优先级
if (sched_setscheduler(0, policy, ¶m) == -1) {
perror("sched_setscheduler failed");
}
// 或者设置特定进程
pid_t target_pid = 1234;
if (sched_setscheduler(target_pid, policy, ¶m) == -1) {
perror("sched_setscheduler failed");
}
优先级设置要小心。我曾经在一个项目中,把一个关键进程的优先级设成了250,结果它抢占了所有CPU时间,其他进程(包括网络栈)都得不到调度,系统直接"假死"了。
我的教训:优先级不是越高越好。在QNX里,优先级255是最高,但通常留给中断处理。普通应用进程用10-30就足够了。如果你需要实时性,考虑用SCHED_FIFO或SCHED_RR,配合合理的优先级。
4.3.2 进程亲和性
在多核系统里,我们可以把进程绑定到特定CPU上运行。
#include <sys/neutrino.h>
#include <sys/cpuset.h>
cpuset_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset); // 绑定到 CPU 0
CPU_SET(2, &cpuset); // 也绑定到 CPU 2
// 设置进程的 CPU 亲和性
if (ThreadCtl(_NTO_TCTL_RUNMASK, &cpuset) == -1) {
perror("ThreadCtl failed");
}
// 获取当前进程的 CPU 亲和性
cpuset_t current_cpuset;
if (ThreadCtl(_NTO_TCTL_RUNMASK_GET_0, ¤t_cpuset) == -1) {
perror("ThreadCtl get failed");
}
嗯,这里要注意:ThreadCtl()虽然名字带"Thread",但它也可以控制进程级别的属性。在QNX里,进程和线程的关系很紧密,很多控制接口是共用的。
4.3.3 进程内存限制
嵌入式系统内存有限,我们需要限制进程能使用的内存量。
#include <sys/resource.h>
struct rlimit rlim;
// 设置进程的虚拟内存上限为 64MB
rlim.rlim_cur = 64 * 1024 * 1024; // 软限制
rlim.rlim_max = 128 * 1024 * 1024; // 硬限制
if (setrlimit(RLIMIT_AS, &rlim) == -1) {
perror("setrlimit failed");
}
// 设置栈大小限制为 1MB
rlim.rlim_cur = 1024 * 1024;
rlim.rlim_max = 1024 * 1024;
if (setrlimit(RLIMIT_STACK, &rlim) == -1) {
perror("setrlimit stack failed");
}
我曾经在一个摄像头处理项目里,有个进程因为内存泄漏,慢慢吃掉了所有可用内存。后来加了内存限制,进程超过64MB就直接被杀死,系统其他部分还能正常运行。这就是"隔离"的思想。
4.3.4 进程名称和调试
调试时,给进程起个有意义的名字会方便很多。
#include <sys/procfs.h>
// 设置进程名称(用于调试和日志)
char proc_name[32];
snprintf(proc_name, sizeof(proc_name), "sensor_reader_%d", sensor_id);
if (ThreadCtl(_NTO_TCTL_NAME, proc_name) == -1) {
perror("ThreadCtl name failed");
}
// 获取进程名称
char name_buf[32];
if (ThreadCtl(_NTO_TCTL_NAME_GET, name_buf) == -1) {
perror("ThreadCtl get name failed");
}
printf("Current process name: %s\n", name_buf);
实用技巧:我在调试多进程系统时,会给每个进程设置一个唯一的名字。这样在pidin命令输出里,一眼就能看出哪个进程在干什么。比如"cam_capture_0"、"cam_capture_1",比默认的"myapp"清晰多了。
4.4 实战:一个完整的进程管理示例
最后,咱们看一个完整的例子。这个例子创建子进程,设置属性,等待结束,并处理退出状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/neutrino.h>
#include <sys/resource.h>
#include <spawn.h>
int main() {
pid_t pid;
int status;
struct rlimit rlim;
// 1. 设置父进程属性
// 设置进程名称
ThreadCtl(_NTO_TCTL_NAME, "parent_monitor");
// 设置优先级
struct sched_param param;
param.sched_priority = 15;
sched_setscheduler(0, SCHED_RR, ¶m);
// 2. 创建子进程
char *argv[] = {"/usr/bin/child_worker", "-t", "30", NULL};
char *envp[] = {"WORKER_ID=1", NULL};
pid = spawn("/usr/bin/child_worker", 0, NULL, NULL, argv, envp);
if (pid == -1) {
perror("spawn failed");
return 1;
}
printf("Created child process: %d\n", pid);
// 3. 等待子进程结束
pid_t ended_pid = waitpid(pid, &status, 0);
if (ended_pid == -1) {
perror("waitpid failed");
return 1;
}
// 4. 分析退出状态
if (WIFEXITED(status)) {
printf("Child %d exited normally with code %d\n",
ended_pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child %d was killed by signal %d\n",
ended_pid, WTERMSIG(status));
} else {
printf("Child %d exited abnormally\n", ended_pid);
}
return 0;
}
这个例子涵盖了本章的主要内容:spawn()创建进程、设置进程属性、等待进程结束、处理退出状态。你可以在自己的项目里参考这个结构。
好了,第四章就到这里。进程管理是嵌入式系统的基础,掌握好了,后面的IPC和资源管理才能用得顺手。下一章我们聊消息传递——QNX最核心的IPC机制。