4. 进程与线程管理:进程创建与终止、线程同步与调度

好,咱们今天聊聊QNX里最核心的东西——进程和线程。说实话,我刚开始接触QNX那会儿,也被它这套机制绕得有点晕。但后来发现,只要你理解了它的设计哲学,其实特别清晰。

4.1 进程创建:spawn vs fork

在QNX里创建进程,主要有两条路:spawn()fork()。很多人问我,到底该用哪个?

先说说 fork()。它会把当前进程完整复制一份。子进程从fork返回的地方继续跑。嗯,这里有个坑——我早期做项目时,用fork创建了一个大进程,结果内存直接爆了。为什么?因为fork会复制整个地址空间,包括那些锁、文件描述符,全给你复制一遍。

注意:在QNX的实时场景下,我建议少用fork。它开销大,而且容易出问题。特别是你手上有多个线程时,fork出来的子进程只有调用线程,其他线程都消失了——这很容易导致死锁。

再说 spawn()。这是QNX推荐的方式。它直接加载一个新程序,不复制父进程的内存。说白了,就是干净利落地启动一个新任务。

// spawn示例:启动一个子进程
#include <spawn.h>
#include <stdio.h>

int main() {
    pid_t pid;
    char *argv[] = {"/usr/bin/myapp", "-v", NULL};
    
    // 我个人习惯用spawn,干净
    int ret = spawn("/usr/bin/myapp", 0, NULL, NULL, argv, NULL);
    if (ret == -1) {
        perror("spawn failed");
        return 1;
    }
    
    printf("子进程PID: %d\n", ret);
    return 0;
}
我的经验:在嵌入式系统里,我几乎只用spawn。fork只在极少数需要共享内存的场景下才用。你想想看,spawn启动快、内存开销小,多省心。

4.2 线程创建与管理:pthread

线程这块,QNX用的是标准的POSIX线程——pthread。如果你在Linux上写过线程,那基本无缝切换。

创建线程很简单,但管理起来有讲究。我见过不少新手,线程一多就乱套了。

#include <pthread.h>
#include <stdio.h>

void* worker_thread(void* arg) {
    int id = *(int*)arg;
    printf("线程 %d 开始工作\n", id);
    // 干活...
    return NULL;
}

int main() {
    pthread_t threads[4];
    int ids[4];
    
    for (int i = 0; i < 4; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, worker_thread, &ids[i]);
    }
    
    // 等待所有线程结束
    for (int i = 0; i < 4; i++) {
        pthread_join(threads[i], NULL);
    }
    
    return 0;
}

这里有个细节:pthread_create 的第四个参数是void*,你可以传任何东西。但我建议你传一个结构体,而不是单个整数。为什么?因为传整数时,如果线程启动慢了,主线程可能已经把那个整数改了——我曾经被这个坑过。

4.3 线程同步:互斥锁、条件变量、信号量

多线程最头疼的就是同步。QNX提供了三种主要工具:互斥锁、条件变量、信号量。

互斥锁(Mutex)

互斥锁是最基础的。说白了,就是一把锁,谁拿到谁干活。

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* safe_worker(void* arg) {
    pthread_mutex_lock(&lock);
    // 临界区代码
    printf("线程安全操作\n");
    pthread_mutex_unlock(&lock);
    return NULL;
}
避坑指南:我曾经在中断服务程序里用互斥锁,结果系统直接死锁了。记住:中断里绝对不能加锁!中断上下文不能睡眠。

条件变量

条件变量用来等待某个条件成立。比如生产者-消费者模式,消费者等数据来了再处理。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;

void* producer(void* arg) {
    // 生产数据
    pthread_mutex_lock(&mutex);
    data_ready = 1;
    pthread_cond_signal(&cond);  // 通知消费者
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!data_ready) {
        pthread_cond_wait(&cond, &mutex);  // 等待条件
    }
    // 处理数据
    pthread_mutex_unlock(&mutex);
    return NULL;
}

注意那个while循环——这是必须的。为什么?因为pthread_cond_wait可能被虚假唤醒。嗯,这是POSIX标准规定的,不是QNX的bug。

信号量

信号量适合控制资源数量。比如限制同时访问某个设备的线程数。

#include <semaphore.h>

sem_t sem;

void* limited_worker(void* arg) {
    sem_wait(&sem);  // 获取信号量
    // 访问有限资源
    printf("访问资源\n");
    sem_post(&sem);  // 释放信号量
    return NULL;
}

int main() {
    sem_init(&sem, 0, 3);  // 最多3个线程同时访问
    // 创建线程...
}
我的选择:简单互斥用mutex,等待条件用condvar,控制并发数用semaphore。别混用,也别滥用。我见过有人用信号量实现互斥锁,那效率低得吓人。

4.4 线程优先级与调度策略

QNX是实时系统,优先级和调度策略是它的灵魂。你想想看,如果所有线程优先级一样,那跟普通Linux有什么区别?

QNX支持三种调度策略:

策略 说明 适用场景
SCHED_FIFO 先进先出,直到主动让出CPU 实时任务,需要确定性的响应时间
SCHED_RR 时间片轮转,同优先级轮流执行 多个同等重要的实时任务
SCHED_OTHER 普通分时调度 非实时任务,后台处理
#include <pthread.h>
#include <sched.h>

void set_realtime_priority(pthread_t thread, int priority) {
    struct sched_param param;
    param.sched_priority = priority;
    
    // 设置为FIFO实时调度
    int ret = pthread_setschedparam(thread, SCHED_FIFO, &param);
    if (ret != 0) {
        printf("设置优先级失败: %d\n", ret);
    }
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, worker, NULL);
    
    // 设置优先级为80(范围0-255,越高越优先)
    set_realtime_priority(thread, 80);
    
    pthread_join(thread, NULL);
    return 0;
}
重要提醒:优先级不是越高越好。我见过一个项目,把所有线程都设成最高优先级,结果系统调度器忙不过来了。记住:只有真正需要实时响应的线程才设高优先级。其他线程用SCHED_OTHER就行。

还有个容易忽略的点:优先级反转。当高优先级线程等待低优先级线程持有的锁时,就会发生。QNX提供了优先级继承协议来解决这个问题——在创建互斥锁时设置PTHREAD_PRIO_INHERIT属性。

pthread_mutexattr_t attr;
pthread_mutex_t mutex;

pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&mutex, &attr);

嗯,这个设置我建议在所有实时线程共享的互斥锁上都加上。虽然有一点点性能开销,但能避免很多诡异的死锁问题。

好了,进程线程这块就聊到这儿。记住:在QNX里,线程是调度的基本单位,进程是资源分配的基本单位。搞清楚了这一点,后面就好办了。