3. 进程与线程基础:进程与线程的区别、QNX中的进程管理、线程创建与销毁
好,咱们今天聊聊进程和线程。这个话题,说实话,是嵌入式多核编程的基石。很多新手容易把这两个概念搞混,或者觉得它们差不多。其实差别大了去了。
我刚开始接触QNX那会儿,也犯过糊涂。记得有一次调试一个多任务系统,一个线程崩了,结果整个进程都挂了,连带其他几个线程一起遭殃。当时我就想,这要是进程隔离的,哪会出这种问题?嗯,从那以后,我对进程和线程的理解就深刻多了。
3.1 进程与线程的区别
说白了,进程是资源分配的最小单位,线程是CPU调度的最小单位。你想想看,一个进程就像是一个独立的工厂,有自己的厂房、设备、原材料(也就是内存空间、文件描述符、信号处理等资源)。而线程呢,就是工厂里的工人,共享工厂的一切资源,但各自干各自的活。
我习惯用一张表来对比它们,清晰明了:
| 对比项 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立,互不干扰 | 共享同一进程的地址空间 |
| 资源开销 | 大(创建、切换、销毁都重) | 小(轻量级) |
| 通信方式 | IPC(管道、消息队列、共享内存等) | 直接读写全局变量(需同步) |
| 健壮性 | 一个进程挂了,不影响其他进程 | 一个线程挂了,整个进程跟着完蛋 |
| 适用场景 | 需要强隔离、高可靠性的模块 | 需要频繁协作、共享数据的任务 |
这里有个关键点:进程切换的开销远大于线程切换。为什么?因为进程切换要换地址空间,TLB(快表)得刷新,缓存也得重新热起来。线程切换呢?就在同一个地址空间里,轻快得多。
核心结论: 在QNX这种微内核系统里,进程是保护边界,线程是执行单元。我个人的经验是:能用线程解决的,尽量别上进程。但涉及安全关键模块,必须用进程隔离。
3.2 QNX中的进程管理
QNX的进程管理,跟Linux有相似之处,但也有自己的脾气。它用的是proc和procnto这两个内核模块来管理进程。创建进程最常用的就是fork()和spawn()。
fork()大家应该不陌生,它会把当前进程复制一份。子进程拿到的是父进程的完整副本,包括内存、文件描述符、信号处理等等。但QNX里我更推荐用spawn(),因为它更灵活,可以直接指定新进程的路径、参数、环境变量,甚至能控制继承哪些资源。
举个例子:
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid;
char *argv[] = {"/bin/ls", "-l", NULL};
char *envp[] = {NULL};
// 创建一个新进程,执行 ls -l
pid = spawn("ls", 0, NULL, NULL, argv, envp);
if (pid == -1) {
perror("spawn failed");
return EXIT_FAILURE;
}
printf("子进程 PID: %d\n", pid);
return EXIT_SUCCESS;
}
这段代码很简单,但背后的事情不少。spawn()会加载/bin/ls这个可执行文件,给它分配独立的地址空间,然后开始执行。父进程和子进程从此各走各的路。
我的小技巧: 在QNX里,如果你需要频繁创建销毁进程,可以考虑用posix_spawn()系列函数,它们更符合POSIX标准,移植性更好。我在一个车载项目中就用它来管理多个传感器数据采集进程,稳定得很。
进程退出也很简单,调用exit()或者从main()返回就行。父进程可以用waitpid()来等待子进程结束,并获取它的退出状态。这里有个坑:如果父进程不调用waitpid(),子进程结束时会变成僵尸进程,占用系统资源。嗯,这个要小心。
3.3 线程创建与销毁
线程在QNX里是用pthread库来管理的。创建线程用pthread_create(),销毁线程用pthread_exit()或pthread_cancel()。咱们直接看代码:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// 线程函数
void *thread_func(void *arg) {
int thread_id = *(int *)arg;
printf("线程 %d 开始运行\n", thread_id);
// 模拟一些工作
for (int i = 0; i < 5; i++) {
printf("线程 %d: 第 %d 次迭代\n", thread_id, i);
sleep(1);
}
printf("线程 %d 结束\n", thread_id);
return NULL;
}
int main() {
pthread_t thread1, thread2;
int id1 = 1, id2 = 2;
// 创建两个线程
if (pthread_create(&thread1, NULL, thread_func, &id1) != 0) {
perror("创建线程1失败");
return EXIT_FAILURE;
}
if (pthread_create(&thread2, NULL, thread_func, &id2) != 0) {
perror("创建线程2失败");
return EXIT_FAILURE;
}
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("主线程:所有子线程已完成\n");
return EXIT_SUCCESS;
}
这段代码创建了两个线程,它们共享进程的全局变量和堆内存。注意看,id1和id2是局部变量,但通过指针传给了线程函数。这里要小心:如果主线程提前退出了,这些局部变量就失效了,线程再访问就会出问题。我建议用动态分配的内存或者全局变量来传参。
警告: 线程创建时,默认的栈大小是8KB(QNX默认值)。如果你的线程函数里用了大量局部变量或者深度递归,栈可能会溢出。我曾经在一个项目中遇到过,线程莫名其妙地崩溃,查了半天才发现是栈不够用。解决办法是用pthread_attr_setstacksize()设置更大的栈。
线程的销毁有两种方式:
- 自然退出: 线程函数执行完毕,返回
NULL或者调用pthread_exit()。 - 强制取消: 其他线程调用
pthread_cancel(),但要注意,这可能导致资源泄漏(比如锁没释放)。
我个人习惯是:尽量让线程自然退出。如果非要强制取消,我会在取消点(比如pthread_testcancel())做清理工作。嗯,这样安全得多。
避坑指南: 我曾经在一个多线程数据处理系统中,用pthread_cancel()粗暴地终止了一个正在写文件的线程。结果文件只写了一半,下次启动时读文件直接崩溃。从那以后,我改用事件标志+条件变量的方式,让线程自己检测退出信号,优雅地释放资源后再退出。
最后,提一下线程的分离状态。如果你不关心线程的退出状态,可以用pthread_detach()把线程设为分离状态。这样线程结束后会自动回收资源,不需要pthread_join()。但要注意:分离后的线程不能再被join,否则会出错。
好了,进程和线程的基础就聊到这儿。下一节咱们会深入QNX的调度策略,看看怎么让这些线程在多核上跑得又快又稳。