【操作系统 | 信号量:从生产者消费者问题到互斥与条件同步】

上一篇讨论临界区时,很多方案本质上都是忙等:拿不到锁就不断循环检查。它们能说明互斥为什么困难,却不适合等待时间未知的场景。因为一个线程什么也没做,却始终占着 CPU;更糟的是,在优先级调度下,它还可能妨碍真正持锁的线程继续运行。
这一篇从生产者—消费者问题出发,梳理信号量、互斥量、Futex 以及 Pthreads 条件变量之间的关系。重点不是记住某几个函数,而是分清两类完全不同的需求:
- 互斥:同一时刻只能有一个执行流修改共享数据;
- 同步:某件事必须等到某个条件成立后才能继续。
生产者和消费者同时需要这两件事,因此它是理解同步原语最经典的模型。
一、问题引入:为什么不能一直轮询加锁?
1. 优先级调度与饥饿
优先级调度的直觉很简单:优先级高的进程先运行。但如果系统总有高优先级任务到来,低优先级任务可能长期得不到 CPU,这种现象叫作饥饿(starvation)。
例子
优先级:谁的优先级高,执行谁
如果A一直运行,那么B始终得不到CPU资源,B可能出现"饿死"的问题

解决方法:例如Linux系统中,会规定 “高优先级的队列在某个时间端内只能调度x次”
操作系统通常不会让高优先级队列无限制地独占处理器。例如会结合时间片、老化(aging)或配额策略,让等待足够久的任务也能被调度。这里要记住:调度器解决的是“谁获得 CPU”的问题,而锁解决的是“谁能访问共享资源”的问题;两者一旦交织,就会产生更复杂的现象。
2. 优先级反转
设有三个线程:高优先级线程 H、中优先级线程 M、低优先级线程 L。
L先获得一把锁,正在访问临界区;H到来,也需要这把锁,因此被阻塞;M不需要锁,但优先级高于L,于是不断抢占L。
结果是:H 在等待 L 释放锁,可 L 又一直得不到运行机会。高优先级线程间接被中优先级线程拖住,这就是优先级反转(priority inversion)。
把忙等改为阻塞等待,可以避免 H 空转浪费 CPU;但它本身并不能彻底消除优先级反转。常见的进一步处理是优先级继承:当 H 等待 L 持有的锁时,临时把 H 的优先级提升给 L,让 L 尽快运行、释放锁,再恢复原优先级。
无论采用什么调度策略,等待线程都不应该无限制地轮询。较长的等待应当进入等待队列,主动让出 CPU;资源可用时,再由释放者或内核唤醒它。

这里的难点在于:检查条件、进入等待队列与唤醒操作必须正确配合,不能在中间留下“丢失唤醒”的窗口。
二、生产者—消费者:一个有界缓冲区里的两类约束
1. 有界缓冲区模型
生产者—消费者问题又称有界缓冲区(bounded-buffer)问题。共享缓冲区有固定容量 N:
- 生产者把数据放入缓冲区;缓冲区满时,生产者必须等待;
- 消费者从缓冲区取数据;缓冲区空时,消费者必须等待;
- 无论是放入还是取出,都要修改缓冲区及其计数器,因此不能并发进行。
用 count 表示当前已有的数据项数量,则它始终应满足:
0 <= count <= N
从这里已经能看出两类约束:
count == N时不能再生产、count == 0时不能再消费,这是同步约束;- 修改
count、读写缓冲区槽位必须串行,这是互斥约束。
2. 只有 sleep/wakeup 的朴素写法
下面看一个生产者消费者的伪代码示例,这是一个典型的模型:
#include <stdbool.h> // 引入布尔类型支持
/* 缓冲区 slot 槽的数量 */
#define N 100
/* 缓冲区数据的数量,使用 volatile 关键字防止编译器优化掉不必要的读取 */
volatile int count = 0;
// 模拟生成数据项的函数(示例)
int produce_item(void) {
// 这里应该实现生成数据项的逻辑,这里简单返回静态值作为示例
static int item = 0;
return ++item;
}
// 模拟消费数据项的函数(示例)
void consumer_item(int item) {
// 这里应该实现消费数据项的逻辑,这里仅打印作为示例
printf("Consumed: %d\n", item);
}
// 假设的插入数据项到缓冲区的函数(需具体实现)
void insert_item(int item) {
// 这里应实现将数据项放入缓冲区的逻辑
// 注意:实际实现中可能需要考虑缓冲区同步和互斥访问
}
// 假设的从缓冲区移除数据项的函数(需具体实现)
int remove_item(void) {
// 这里应实现从缓冲区取出数据项的逻辑
// 注意:实际实现中同样需要考虑同步和互斥
return 0; // 示例返回
}
// 模拟系统调用 sleep,这里使用忙等待(实际应使用系统提供的阻塞机制)
void sleep(void) {
// 这里仅为示例,实际应使用系统提供的 sleep 函数
// 在这里,我们简单地让出 CPU 时间片,但这不是真正的 sleep
// 实际应用中,应使用如 POSIX 的 sleep() 或 Windows 的 Sleep()
}
// 模拟系统调用 wakeup,这里假设有某种机制可以唤醒其他线程或进程
void wakeup(void (*func)(void)) {
// 这里仅为示例,实际应使用如 POSIX 的 pthread_cond_signal 或 Windows 的 SetEvent
// 这里我们不做任何操作,因为真正的唤醒机制依赖于操作系统
}
// 生产者
void producer(void) {
int item;
// 无限循环
while (true) {
// 生成下一项数据
item = produce_item();
// 如果缓冲区是满的,就阻塞(这里用忙等待模拟)
while (count == N) {
sleep(); // 在这等着。
}
// 把当前数据放在缓冲区中
insert_item(item);
// 增加缓冲区 count 的数量
count++;
// 如果缓冲区之前为空,唤醒消费者
if (count == 1) {
wakeup(consumer); // 假设可以唤醒消费者线程或进程
}
}
}
// 消费者
void consumer(void) {
int item;
// 无限循环
while (true) {
// 如果缓冲区是空的,就阻塞(这里用忙等待模拟)
while (count == 0) {
sleep(); // 假设这是真正的系统调用 sleep
}
// 从缓冲区中取出一个数据
item = remove_item();
// 将缓冲区的 count 数量减一
count--;
// 如果缓冲区之前是满的,唤醒生产者
if (count == N - 1) {
wakeup(producer); // 假设可以唤醒生产者线程或进程
}
// 模拟消费数
consumer_item(item);
}
}
// 注意:上述代码中的 sleep 和 wakeup 函数仅为示例,实际实现应依赖具体的操作系统提供的机制。
// 同时,缓冲区的管理(insert_item 和 remove_item)也需要实现具体的同步和互斥逻辑,以避免数据竞争。
它的问题不仅是 count++ 和 count-- 可能发生竞态,更致命的是唤醒丢失(lost wakeup)。考虑缓冲区为空时的如下交错:
- 消费者读取到
count == 0,准备执行sleep(); - 消费者恰好被切走,还没有真正睡眠;
- 生产者放入一个数据项,把
count改为 1,并执行wakeup(consumer); - 此刻消费者尚未在等待队列中,唤醒没有对象,信号被丢弃;(唤醒丢失了!)
- 消费者恢复执行,仍按照先前的判断进入睡眠。
之后如果生产者继续生产到缓冲区满,也会睡眠;消费者明明该消费却仍在睡,双方就可能永久等待。
问题的根源不是“调用 wakeup 的时机不够巧”,而是 if (count == 0) 与 sleep() 是两步可被打断的操作。
解决方案之一: 是引入唤醒等待标志(Wakeup Flag)。
- 给线程额外加一个唤醒标志,在单生产者、单消费者下可以缓解问题;但在多生产者、多消费者场景中很快又会陷入复杂的计数和时序处理。
- 更可靠的办法是使用把“资源计数”和“阻塞唤醒”一起定义好的同步原语:信号量。
三、信号量:把资源数量与等待关系交给原子操作
1. 什么是信号量
荷兰计算机科学家 Dijkstra 提出的信号量(semaphore),可以理解为一个带等待队列的非负计数器。它不是普通整型变量,因为对它的操作必须是原子的。
信号量最核心的两个操作是:
P/down/wait:申请一个资源;V/up/signal:归还或发布一个资源。
概念上,down(S) 的语义是:若 S 代表的资源还有余量,就消耗一个并继续;否则把当前线程加入 S 的等待队列并阻塞。up(S) 的语义是:发布一个资源;若有人等待,就唤醒一个合适的等待者。
down(S):原子地“检查资源 → 减少计数,或阻塞当前线程”
up(S) :原子地“增加计数 → 必要时唤醒一个等待者”
重点在“原子地”。如果检查 S、修改 S 和阻塞线程可以被其他线程插入,信号量仍会重新出现唤醒丢失或计数错误。
教材中常见两种理解:
- 整型信号量:只用一个整数描述资源数量;
- 记录型信号量:除计数外还维护等待队列,
P/V能真正实现阻塞与唤醒。
实际操作系统中的实现属于后一类思想。内部计数的具体正负表示法因实现而异,但对使用者而言只需记住:down 拿不到资源会等待,up 会使一个等待者有机会继续。
2. 用三个信号量解决生产者—消费者
设缓冲区容量为 N,需要三个信号量:
| 信号量 | 初值 | 含义 |
|---|---|---|
empty |
N |
尚未使用的空槽数量 |
full |
0 |
已经存放数据的满槽数量 |
mutex |
1 |
对缓冲区和共享状态的互斥访问权 |
完整的伪代码如下:
#include <stdio.h>
#include <sys/types.h>
#define FALSE 0
#define TRUE 1
/* 缓冲区 slot 槽的数量 */
#define N 100
struct queue{ pid_t pid;};
// typedef struct {
// int value;
// struct queue wq;
// } semaphore;
/* 信号量类型定义,通常在实际应用中会使用更复杂的结构体来确保原子操作 */
typedef int semaphore;
extern void down(semaphore *);
extern void up(semaphore *);
int produce_item(void) {
static int item = 0;
return ++item;
}
void consumer_item(int item) {
printf("Consumed: %d\n", item);
}
void insert_item(int item) {
}
int remove_item(void) {
return 0;
}
/* 标明缓存区有空位置的个数 */
semaphore empty = N; // 缓存区初始化时有N个空位
/* 标明缓存区已经放入资源的数量 */
semaphore full = 0; // 缓存区初始化时有0个可用资源
/* 控制区临界区互斥访问 */
semaphore mutex = 1; // 表示互斥锁
// 生产者
void producer(void ) {
int item;
while (TRUE) {
/* 生产一个数据项 */
item = produce_item();
/* 等待缓冲区有空槽 */
down(&empty); // 如果 empty 为 0(没有空位了),则生产者将阻塞,直到有消费者释放空槽
/* 进入临界区,保护对缓冲区的访问 */
down(&mutex); // 确保没有其他生产者或消费者同时访问缓冲区
/* 将数据项放入缓冲区 */
insert_item(item); // 假设这是将数据项放入缓冲区的函数
/* 离开临界区 */
up(&mutex); // 释放互斥锁
/* 增加缓冲区满槽的数量 */
up(&full); // 表示缓冲区中多了一个满槽
}
}
// 消费者
void consumer(void ) {
int item;
while (TRUE) {
/* 等待缓冲区有数据可消费 */
down(&full); // 如果 full 为 0,则消费者将阻塞,直到有生产者放入数据
/* 进入临界区 */
down(&mutex); // 确保没有其他生产者或消费者同时访问缓冲区
/* 从缓冲区取出数据项 */
item = remove_item(); // 假设这是从缓冲区取出数据项的函数
/* 离开临界区 */
up(&mutex); // 释放互斥锁
/* 增加缓冲区空槽的数量 */
up(&empty); // 表示缓冲区中多了一个空槽
/* 处理取出的数据项 */
consume_item(item); // 假设这是处理数据项的函数
}
}
/* 注意:
- down(&semaphore) 操作会检查信号量的值,如果为 0 则阻塞调用者,否则将其减 1。
- up(&semaphore) 操作会将信号量的值加 1,并可能唤醒一个或多个在该信号量上等待的进程。
- 这里假设 producer_item(), insert_item(), remove_item(), consume_item() 等函数
已经正确定义并实现了相应的功能。
*/
这段代码最值得理解的是每个信号量的职责:
empty/full是计数型同步信号量。它们回答“现在能不能生产/消费”;mutex是初值为 1 的二进制信号量。它回答“谁可以修改缓冲区”;up(&full)与up(&empty)分别是对消费者、生产者的“条件已经成立”的通知。
还要注意顺序:生产者必须先 down(empty),确认自己能占用一个槽,再进入临界区;消费者必须先 down(full),确认存在可取数据。不能反过来先拿 mutex 再等待 empty/full,否则一个生产者可能拿着 mutex 等空槽,而消费者因为拿不到 mutex 无法取走数据,形成死锁。
信号量的 down/up 在内核中需要借助硬件原子指令、短临界区自旋锁或关中断等机制保证原子性;在多核系统里,单纯关闭当前 CPU 的中断不足以阻止其他 CPU 修改同一个信号量,仍需要跨 CPU 的原子读—改—写操作。
3. 信号量也能表示 I/O 完成
信号量不只适用于缓冲区。假设某个 I/O 请求对应的信号量初值为 0:发起 I/O 的进程执行 down 后阻塞;设备完成 I/O 并触发中断后,中断处理程序执行 up;等待进程便能继续。对进程而言,它只是在等待一个信号量,设备中断的细节被同步原语隐藏起来了。
四、互斥量:只为“独占”而生
1. mutex 与信号量有什么不同?
**互斥量(mutex)**只有“已锁定”和“未锁定”两种逻辑状态,专门用来保护临界区。相比计数型信号量,它表达的意图更明确:谁成功加锁,谁应当在完成后解锁。
| 对比项 | 计数信号量 | 互斥量 |
|---|---|---|
| 核心含义 | 可用资源的数量或事件次数 | 临界区的所有权 |
| 取值 | 通常可大于 1 | 逻辑上只有锁定/解锁 |
| 解锁/归还者 | 不强制必须是申请者 | 通常要求持锁线程解锁 |
| 常见用途 | 空槽数、任务数、I/O 完成事件 | 保护共享数据结构 |
可以把生产者—消费者中的 mutex 看作二进制信号量,但在真实线程程序里,若只需要互斥,优先选择互斥量;若需要表达“有多少个资源”“发生了多少次事件”,信号量更贴切。
2. Futex:无竞争走用户态,有竞争才请内核帮忙
Futex 机制
- 为解决上述问题,Futex(快速用户空间互斥,Fast Userspace Mutexes)应运而生,它巧妙地融合了自旋锁与阻塞锁的优点。Futex允许进程在用户空间自旋一段时间(等待锁短暂释放),如果在此期间锁未释放,则进程将自动切换至阻塞状态,并通过内核机制进行高效的等待与唤醒。这种机制减少了不必要的内核上下文切换,同时避免了长时间自旋导致的CPU资源浪费。
Futex是Linux内核提供的一种高效同步机制,旨在实现类似于互斥锁的功能,同时显著减少因频繁陷入内核空间而导致的性能开销。
- 内核服务:这部分是futex机制在Linux内核中的实现,提供了一个高效的等待队列。当进程(或线程)尝试获取一个被其他进程持有的锁时,它可以通过futex系统调用将自己加入到等待队列中。此时,进程并不会立即陷入休眠状态,而是保持在一个“等待”状态,允许在用户空间进行自旋。如果自旋一段时间后锁仍未释放,进程才会真正陷入休眠,等待内核在锁被释放时将其唤醒。
- 用户空间库:为了简化futex的使用,Linux还提供了用户空间库,这些库封装了futex系统调用的细节,使开发者能够方便地在用户程序中实现同步机制。通过调用这些库函数,开发者可以轻松地创建锁、解锁以及等待锁等操作,而无需直接与系统调用接口打交道。
实际示例
- 举个例子,假设有两个进程A和B,它们都需要访问同一个共享资源。进程A首先获得了访问权并锁定了该资源。此时,如果进程B尝试访问该资源,它将通过futex系统调用将自己加入到等待队列中。如果进程A很快完成了对资源的访问并释放了锁,进程B可能会在用户空间通过自旋快速获得锁,从而避免了内核切换的开销。如果进程A的访问时间较长,进程B则会在自旋一段时间后陷入休眠,等待内核在锁被释放时将其唤醒。这样,futex就能够在保证同步正确性的同时,最大限度地提高系统的性能。
因值得注意的是,当没有锁竞争时(即所有尝试获取锁的操作都能立即成功),futex机制能够完全在用户空间内运作,无需内核介入,从而实现了极高的效率。这种设计使得futex成为高并发环境下现代同步机制的理想选择。
从这一层看,Futex 和前面的信号量没有矛盾:信号量强调抽象语义,Futex 提供高效的底层等待/唤醒支撑。常见线程库会利用 Futex 实现互斥锁、条件变量等更高层原语。
3. Pthreads 中的互斥锁
POSIX 线程库把互斥锁抽象为 pthread_mutex_t。典型生命周期是“初始化 → 加锁/解锁 → 销毁”:
#include <pthread.h>
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
/* 临界区:访问或修改共享数据 */
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
常用函数的含义如下:
pthread_mutex_lock:锁空闲则立即获得;锁被占用则阻塞,直到有机会获得锁;pthread_mutex_trylock:尝试获得锁;失败立即返回,例如EBUSY,不会阻塞;pthread_mutex_unlock:由持锁线程释放锁,并使等待线程重新有机会竞争;pthread_mutex_init/destroy:初始化和销毁动态初始化的互斥锁。
互斥锁只能解决“不要同时修改共享数据”,却不能直接表达“队列为空时消费者应该睡”“队列已满时生产者应该睡”(用于实现线程间的同步)。后者需要条件变量(condition variable)。
4. 条件变量:等待的不是锁,而是条件
Pthreads 条件变量类型为 pthread_cond_t。它总要和一个互斥锁共同使用:互斥锁保护共享状态,条件变量让线程在某个状态不成立时等待。
最重要的调用是:
pthread_cond_wait(&cond, &mutex);
它看似只是在等待 cond,实际上做了一个不可分割的动作:
- 当前线程已经持有
mutex; pthread_cond_wait把线程加入cond的等待队列,并原子地释放mutex;- 线程被唤醒后,不会立刻继续执行,而是先重新获得
mutex; - 重新获得锁后,函数才返回。
这正是它能避免“检查条件后再睡眠”出现丢失唤醒的原因。
条件变量还有两个常用通知函数:
pthread_cond_signal(&cond); // 至少唤醒一个等待者
pthread_cond_broadcast(&cond); // 唤醒所有等待者
被唤醒不等于条件一定为真。可能有多个等待者被唤醒,也可能在当前线程重新抢到互斥锁前,其他线程已经改变了共享状态;此外,条件变量允许虚假唤醒(spurious wakeup)。因此必须用 while 而不能用 if:
pthread_mutex_lock(&mutex);
while (!condition) {
pthread_cond_wait(&cond, &mutex);
}
/* 此时持有 mutex,且 condition 已被重新确认 */
pthread_mutex_unlock(&mutex);
5. 用 Pthreads 再写一次生产者—消费者
下面使用环形缓冲区、一个互斥锁和两个条件变量。not_full 表示“缓冲区尚未满”,not_empty 表示“缓冲区并非空”。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
typedef struct _node
{
int number;
struct _node *next;
} Node;
// 定义互斥锁与条件变量
pthread_mutex_t mutrx;
pthread_cond_t cond;
// 定义一个头插链表
Node *head = NULL;
void *th_do_fun(void *arg)
{
// 生产者
while (1)
{
pthread_mutex_lock(&mutrx);
Node *p = (Node *)malloc(sizeof(Node));
p->number = rand() % 100;
p->next = head;
head = p;
printf("[%ld]: do [%d] OK!\n", pthread_self(), head->number);
pthread_mutex_unlock(&mutrx);
pthread_cond_broadcast(&cond); // pthread_cond_signal(&cond); (也不是不行)
sleep(rand() % 3);
}
return NULL;
}
void *th_fk_fun(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutrx);
// if(head == NULL) // 这样写有bug
/*
为什么在上面使用if 有bug:
当任务队列为空, 所有的消费者线程都会被这个函数阻塞 pthread_cond_wait(&cond, &mutex);
也就是阻塞在代码的第9行
当生产者生产了1个节点, 调用 pthread_cond_broadcast(&cond); 唤醒了所有阻塞的线程
- 有一个消费者线程通过 pthread_cond_wait()加锁成功, 其余没有加锁成功的线程继续阻塞
- 加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁
- 没有加锁成功的线程解除阻塞继续抢这把锁, 另外一个子线程加锁成功
- 但是这个线程删除链表节点的时候链表已经为空了, 后边访问这个空节点的时候就会出现段错误
解决方案:
- 需要循环的对链表是否为空进行判断, 需要将if 该成 while
*/
while (!head)
{
// 条件变量: 阻塞线程 (因为没有了, 需要生产者生产!)
printf("阻塞(len == 0)\n");
// 任务队列, 也就是链表中已经没有节点可以消费了
// 消费者线程需要阻塞
// 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开
// 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁
// 这函数会自动将线程拥有的锁解开
pthread_cond_wait(&cond, &mutrx);
// 当消费者线程解除阻塞之后, 会自动将这把锁锁上
// 这时候当前这个线程又重新拥有了这把互斥锁
}
Node *p = head;
head = head->next;
printf("[%ld]: fk [%d] OK!\n", pthread_self(), p->number);
free(p);
pthread_mutex_unlock(&mutrx);
sleep(rand() % 2);
}
return NULL;
}
int main()
{
// 初始化
pthread_mutex_init(&mutrx, NULL);
pthread_cond_init(&cond, NULL);
// 定义生产者(do)与消费者(fk)线程
pthread_t do_arr[5], fk_arr[5];
for (int i = 0; i < 5; ++i)
{
pthread_create(&do_arr[i], NULL, th_do_fun, NULL);
printf("The do Man (%ld) Go !\n", do_arr[i]);
}
for (int i = 0; i < 5; ++i)
{
pthread_create(&fk_arr[i], NULL, th_fk_fun, NULL);
printf("The fk Man (%ld) Go !\n", fk_arr[i]);
}
// 释放
while (head)
{
Node *tmp = head;
head = head->next;
free(tmp);
}
for (int i = 0; i < 5; ++i)
{
pthread_join(do_arr[i], NULL);
pthread_join(fk_arr[i], NULL);
}
pthread_mutex_destroy(&mutrx);
pthread_cond_destroy(&cond);
return 0;
}
这里和信号量版本形成了清晰对应:
mutex仍负责保护buffer、in、out和count;while (count == N)对应“没有空槽就不能生产”;while (count == 0)对应“没有数据就不能消费”;not_full/not_empty不保存资源数量,它们只负责让等待者在条件变化时重新检查状态;count才是条件的真实依据,所以读它、改它和检查它都必须在同一把mutex的保护下完成。
如果有多个生产者或消费者,使用 signal 通常足够;在一次状态变化可能让多个等待者都重新具备运行条件,或实现上难以精确判断唤醒对象时,也可使用 broadcast。无论哪一种,等待端的 while 都不能省略。
五、总结:先分清“独占”还是“等条件”
从生产者—消费者问题可以看到,并发控制不是只加一把锁:
- 共享缓冲区的读写需要互斥;
- 空与满的边界需要同步;
sleep/wakeup若没有原子地协调检查与等待,就会产生唤醒丢失;- 信号量用
empty、full和mutex同时表达资源数量、先后关系和临界区保护; - 互斥锁专注于所有权;条件变量专注于“条件未满足就等待”;
- Futex 则体现了现代实现的取舍:无竞争时尽量停留在用户态,真正需要等待时再交给内核排队与唤醒。
面对一个并发问题,可以按下面的顺序判断:先找出共享数据与临界区;再问当前需要的是“只能一个人做”的互斥,还是“必须等某件事发生”的同步;最后选择合适的原语,并确认所有“检查—修改—等待/唤醒”的关键步骤没有可被插入的竞态窗口。
如果这篇文章对你有帮助,欢迎点赞、评论、关注、收藏。你们的支持是我前进的动力!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)