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_tpthread_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_NORMALPTHREAD_MUTEX_RECURSIVEPTHREAD_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,优先级调度注意反转。下一章咱们聊内存管理,那又是另一个大坑。嗯,先消化这些吧。