3、进程与线程管理:进程创建与销毁、线程同步机制(Mutex/Condvar)、优先级调度实战
好,咱们今天聊点硬核的。进程与线程管理,说白了就是QNX系统里最核心的“搬砖”逻辑。你想想看,车机上那么多模块——导航、仪表、娱乐、ADAS——它们怎么共存?怎么不打架?怎么保证关键任务不卡顿?嗯,答案就在这一章里。
我个人习惯把进程和线程比作“车间”和“工人”。进程是独立的车间,有自己的地盘(地址空间);线程是车间里的工人,共享工具和材料。QNX的微内核架构下,进程间通信(IPC)是轻量级的,但线程同步一旦搞砸,车机死机就是分分钟的事。
3.1 进程创建与销毁:别让子进程变成“孤儿”
在QNX里创建进程,最常用的就是 spawn() 和 fork()。但我得说句实话,fork() 在QNX里用得不多,因为微内核下进程开销本来就小,spawn() 更直接。
来看个实际例子:
#include <spawn.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
pid_t pid;
char *argv[] = {"/usr/bin/sensor_daemon", "-c", "/etc/sensor.conf", NULL};
// 创建传感器守护进程
pid = spawn(PROG_SENSOR, NULL, NULL, NULL, argv, NULL);
if (pid == -1) {
perror("spawn failed");
return -1;
}
printf("Sensor daemon started, PID: %d\n", pid);
// 等待子进程结束(非阻塞方式)
int status;
waitpid(pid, &status, WNOHANG);
return 0;
}
这里有个坑,我曾经踩过。你创建了子进程,但父进程挂了,子进程就成了“孤儿”。在QNX里,孤儿进程会被 init 接管,但资源可能泄漏。我的习惯是:父进程退出前,一定要清理子进程。要么发信号 SIGTERM,要么用 waitpid() 收尸。
kill -9,那会导致共享内存没释放、文件描述符没关闭。我建议用 SIGTERM 先通知,给进程3秒的清理时间,不行再 SIGKILL。
3.2 线程同步机制:Mutex 和 Condvar 的“黄金搭档”
线程同步,说白了就是防止多个工人抢同一把扳手。QNX提供了 pthread_mutex_t 和 pthread_cond_t,这俩是黄金搭档。
Mutex 负责互斥,Condvar 负责等待和通知。你想想看,如果只是用 Mutex,线程A占着资源不放,线程B只能死等,这叫“忙等”,浪费CPU。Condvar 就是用来解决这个问题的——线程B可以睡大觉,等线程A干完了再叫醒它。
看代码:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;
void* producer(void* arg) {
// 模拟数据生产
sleep(2);
pthread_mutex_lock(&mutex);
data_ready = 1;
printf("Producer: data is ready\n");
pthread_cond_signal(&cond); // 通知消费者
pthread_mutex_unlock(&mutex);
return NULL;
}
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (data_ready == 0) {
printf("Consumer: waiting for data...\n");
pthread_cond_wait(&cond, &mutex); // 自动释放锁,被唤醒后重新获取
}
printf("Consumer: got data, processing...\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
pthread_cond_wait() 必须在 while 循环里调用,而不是 if。为什么?因为存在“虚假唤醒”(spurious wakeup)。我曾经在项目中遇到过,消费者被唤醒了但数据还没准备好,结果读到了脏数据。用 while 循环重新检查条件,这是铁律。
另外,Mutex 有几种类型:PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_RECURSIVE、PTHREAD_MUTEX_ERRORCHECK。我个人习惯在车机项目里用 ERRORCHECK 类型,它能检测死锁——比如同一个线程重复加锁,它会返回 EDEADLK,而不是直接死锁。调试阶段特别有用。
3.3 优先级调度实战:别让低优先级任务“饿死”
QNX是实时操作系统,优先级调度是它的看家本领。默认是 FIFO调度策略,优先级高的线程先跑,直到它主动让出CPU或者被更高优先级的线程抢占。
但这里有个经典问题:优先级反转。低优先级任务拿着锁,高优先级任务等锁,结果中优先级任务抢了CPU,低优先级任务跑不了,高优先级任务也跑不了。嗯,我在做ADAS项目时遇到过,一个紧急制动模块被一个日志打印线程拖死,差点出事。
解决方案?QNX支持 优先级继承协议。你可以在创建Mutex时设置 PTHREAD_PRIO_INHERIT:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
这样,低优先级线程拿到锁后,会临时“继承”高优先级线程的优先级,直到释放锁。说白了,就是让低优先级任务赶紧跑完,别挡路。
- 高优先级(30-40):安全相关,如刹车、转向、碰撞检测
- 中优先级(20-29):交互相关,如HMI渲染、触摸响应
- 低优先级(10-19):后台任务,如日志上传、OTA下载
千万别把所有线程都设成高优先级,那跟没有优先级一样。
还有一个细节:优先级天花板协议。你可以给Mutex设置一个最高优先级,任何线程拿到这个锁,优先级都会被提升到这个天花板值。这比优先级继承更简单粗暴,但容易造成优先级“虚高”。我个人更推荐优先级继承,因为它更精准。
3.4 避坑指南:我踩过的那些坑
- 死锁:我曾经在项目中用两个Mutex,线程A锁了M1等M2,线程B锁了M2等M1。结果两个线程都卡死了。解决办法?固定加锁顺序,比如总是先锁M1再锁M2,或者用
pthread_mutex_trylock()加超时重试。 - 条件变量丢失唤醒:如果
pthread_cond_signal()在pthread_cond_wait()之前调用,消费者就永远等不到通知了。所以一定要在锁的保护下修改条件并发送信号。 - 线程栈溢出:QNX默认线程栈是8KB,但车机里有些模块(比如JSON解析)栈消耗大。我建议用
pthread_attr_setstacksize()显式设置,至少64KB起步。
好了,这一章的内容就这些。进程创建别留孤儿,线程同步用Mutex+Condvar,优先级调度注意反转。下一章咱们聊内存管理,那又是另一个大坑。嗯,先消化这些吧。