第3章:QNX进程与线程管理

大家好,我是你们的老朋友。今天我们来聊聊QNX里最核心的东西——进程与线程管理。说实话,这部分内容我做了这么多年嵌入式,每次带新人时都会反复讲。为什么?因为搞不懂进程线程,你写的代码在QNX上跑起来,轻则卡顿,重则死机,甚至把整个系统拖垮。

嗯,咱们直接进入正题。

3.1 进程的创建与销毁

在QNX里,进程的创建和销毁,说白了就是让程序“活起来”和“死掉”的过程。我个人习惯用fork()exec()的组合,但QNX也提供了posix_spawn()这种更现代的方式。

3.1.1 使用fork()创建进程

fork()这个函数,你想想看,它就像细胞分裂。调用一次,返回两次。父进程返回子进程的PID,子进程返回0。我在项目中遇到过一个问题:新手容易忘记检查返回值,结果父进程和子进程都去干同一件事,导致资源冲突。

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    
    if (pid == -1) {
        // 创建失败,这种情况我遇到过,通常是系统资源不足
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("我是子进程,PID: %d\n", getpid());
        // 这里可以执行具体任务
        return 0;
    } else {
        // 父进程
        printf("我是父进程,子进程PID: %d\n", pid);
        wait(NULL); // 等待子进程结束
    }
    
    return 0;
}

注意:我曾经见过一个案例,有人忘记在子进程里调用exit()return,结果子进程继续执行父进程的代码,导致逻辑混乱。记住:fork之后,子进程和父进程是独立的,该退就退。

3.1.2 使用posix_spawn()创建进程

说实话,我更喜欢posix_spawn()。为什么?因为它一步到位,不用先fork再exec。在汽车电子这种对实时性要求高的场景下,少一次上下文切换就是赚到。

#include <stdio.h>
#include <spawn.h>
#include <sys/wait.h>

int main() {
    pid_t pid;
    char *argv[] = {"/usr/bin/myapp", "-c", "config.ini", NULL};
    
    // 创建进程并执行myapp
    int ret = posix_spawn(&pid, "/usr/bin/myapp", NULL, NULL, argv, NULL);
    
    if (ret != 0) {
        perror("posix_spawn failed");
        return 1;
    }
    
    printf("子进程已启动,PID: %d\n", pid);
    waitpid(pid, NULL, 0);
    
    return 0;
}

小技巧:如果你需要设置子进程的优先级或调度策略,可以在posix_spawn()的第四个参数(posix_spawnattr_t)里配置。我一般会先初始化属性,再设置调度参数。

3.1.3 进程的销毁

进程销毁,说白了就是让进程“体面地退出”。正常退出用exit()return,异常退出用abort()。但要注意,在QNX里,进程退出时会自动释放所有资源,包括打开的文件描述符、共享内存等。不过,如果你用了动态分配的内存,最好还是手动释放。

我曾经遇到过一个坑:子进程异常退出时,父进程没有调用wait(),结果子进程变成了僵尸进程。僵尸进程虽然不占CPU,但会占用进程表项,积累多了系统就创建不了新进程了。

3.2 线程的同步与互斥

多线程编程,说白了就是让多个线程协同工作。但线程之间共享全局变量,如果不加控制,就会出现数据竞争。嗯,这里就要用到互斥锁(Mutex)和条件变量(Condvar)了。

3.2.1 互斥锁(Mutex)

互斥锁,我习惯叫它“锁”。它的作用就是保证同一时间只有一个线程访问共享资源。你想想看,就像厕所的门锁,一个人进去了,其他人就得等着。

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;

void* thread_func(void* arg) {
    for (int i = 0; i < 1000; i++) {
        pthread_mutex_lock(&mutex);
        shared_counter++;  // 临界区
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    
    pthread_create(&t1, NULL, thread_func, NULL);
    pthread_create(&t2, NULL, thread_func, NULL);
    
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    
    printf("最终值: %d (期望: 2000)\n", shared_counter);
    
    return 0;
}

重点:锁的粒度要适中。锁得太细,性能差;锁得太粗,并发度低。我在项目中一般遵循“最小临界区”原则,只锁真正需要保护的代码。

3.2.2 条件变量(Condvar)

条件变量,说白了就是让线程“等消息”。一个线程等条件满足,另一个线程发信号通知。我经常用它来实现生产者-消费者模式。

#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(1);
    
    pthread_mutex_lock(&mutex);
    data_ready = 1;
    printf("生产者: 数据准备好了\n");
    pthread_cond_signal(&cond);  // 通知消费者
    pthread_mutex_unlock(&mutex);
    
    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (data_ready == 0) {
        printf("消费者: 等待数据...\n");
        pthread_cond_wait(&cond, &mutex);  // 等待条件满足
    }
    printf("消费者: 收到数据,开始处理\n");
    pthread_mutex_unlock(&mutex);
    
    return NULL;
}

注意:我曾经犯过一个错误:在pthread_cond_wait()之前没有检查条件,结果线程永远等在那里。记住:一定要用while循环检查条件,而不是if。因为pthread_cond_wait()可能被虚假唤醒。

3.3 优先级调度策略

QNX是实时操作系统,优先级调度是它的看家本领。说白了,就是决定哪个线程先运行。QNX支持多种调度策略,但最常用的是FIFO和Round Robin。

3.3.1 调度策略概述

调度策略 说明 适用场景
SCHED_FIFO 先进先出,同优先级线程按队列执行 实时性要求高,任务执行时间短
SCHED_RR 时间片轮转,同优先级线程轮流执行 多个同优先级任务需要公平调度
SCHED_OTHER 普通分时调度 非实时任务,后台处理

我个人习惯,在汽车电子领域,大部分控制任务用SCHED_FIFO。为什么?因为控制任务通常执行时间短,而且需要尽快响应。你想想看,刹车控制线程如果被其他线程打断,那后果不堪设想。

3.3.2 设置线程优先级

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

void* high_priority_task(void* arg) {
    // 高优先级任务
    printf("高优先级任务运行中...\n");
    return NULL;
}

int main() {
    pthread_t thread;
    struct sched_param param;
    int policy = SCHED_FIFO;
    
    // 设置优先级为80(范围0-255,数字越大优先级越高)
    param.sched_priority = 80;
    
    // 创建线程并设置调度参数
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setschedpolicy(&attr, policy);
    pthread_attr_setschedparam(&attr, &param);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    
    pthread_create(&thread, &attr, high_priority_task, NULL);
    pthread_join(thread, NULL);
    
    pthread_attr_destroy(&attr);
    
    return 0;
}

避坑指南:我曾经在项目中把优先级设得太高,结果导致低优先级的看门狗线程得不到CPU时间,系统频繁复位。记住:优先级不是越高越好,要合理分配。一般建议:控制任务80-100,通信任务60-80,后台任务40-60。

3.3.3 优先级反转问题

优先级反转,说白了就是高优先级线程被低优先级线程阻塞。举个例子:高优先级线程A要访问共享资源,但这个资源被低优先级线程B锁着,而B又被中等优先级线程C抢占了CPU。结果A只能等B,B只能等C,优先级完全颠倒了。

QNX提供了优先级继承协议来解决这个问题。当高优先级线程被低优先级线程阻塞时,低优先级线程会临时继承高优先级,直到释放锁。我建议你在使用互斥锁时,用pthread_mutexattr_setprotocol()设置优先级继承属性。

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

嗯,这部分内容就到这里。进程线程管理是QNX开发的基石,搞懂了这些,后面的消息传递、资源管理就水到渠成了。下一章我们聊聊QNX的消息传递机制,那可是QNX的独门绝技。

公众号:蓝海资料掘金营,微信deep3321