0.restrict:
c99中新增加了一个类型定义,就是restrict。
概括的说,关键字restrict只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码。
一、条件变量
1.核心特性
- 必须与互斥锁配合:避免 “条件检查” 和 “休眠” 之间的竞态;
- 无锁等待:等待时释放互斥锁,不占用 CPU;
- 唤醒机制:支持单线程唤醒(
signal)、多线程唤醒(broadcast); - 虚假唤醒:线程可能无原因被唤醒,需循环检查条件。
2.核心接口
| 接口 | 作用 | 关键说明 |
|---|---|---|
pthread_cond_init(&cond, NULL) | 初始化条件变量 | NULL = 默认属性,全局变量可静态初始化 PTHREAD_COND_INITIALIZER |
pthread_cond_wait(&cond, &mutex) | 等待条件满足 | 原子操作:释放 mutex → 休眠 → 被唤醒后重新加 mutex |
pthread_cond_signal(&cond) | 唤醒一个等待线程 | 从等待队列中选一个线程唤醒,不确定哪一个 |
pthread_cond_broadcast(&cond) | 唤醒所有等待线程 | 适用于多个线程等待同一条件(如生产者 - 消费者) |
pthread_cond_destroy(&cond) | 销毁条件变量 | 需确保无线程等待,否则未定义行为 |
为什么必须原子?如果 “释放锁” 和 “休眠” 拆分,可能导致:线程释放锁后、休眠前,其他线程发送唤醒信号 → 信号丢失,线程永久休眠。
3.底层原理(基于futex)
typedef union {
struct {
int __lock; // 条件变量内部锁
unsigned int __futex; // futex 等待队列(核心)
int __total_seq; // 唤醒序列号(避免信号丢失)
int __wakeup_seq; // 已唤醒序列号
int __mutex; // 绑定的互斥锁地址
// 其他调试/统计字段
} __data;
char __size[__SIZEOF_PTHREAD_COND_T];
long int __align;
} pthread_cond_t;
核心执行流程(pthread_cond_wait)
- 加内部锁:保护条件变量的等待队列;
- 记录唤醒序列号:避免信号丢失;
- 释放外部互斥锁:原子操作,由 futex 保证;
- futex 阻塞:调用
futex(FUTEX_WAIT)进入内核休眠; - 被唤醒后:
- 重新加外部互斥锁;
- 检查序列号,确认是有效唤醒(非虚假唤醒);
- 释放内部锁,函数返回。
核心执行流程(pthread_cond_signal)
- 加内部锁;
- 更新唤醒序列号;
- futex 唤醒:调用
futex(FUTEX_WAKE)唤醒一个等待线程; - 释放内部锁。
4.常见坑
4.1虚假唤醒(最常见)
- 现象:线程被
pthread_cond_wait唤醒,但条件并未满足; - 原因:内核调度、信号中断等导致无意义唤醒;
- 解决:用
while循环检查条件(而非if): -
// 错误:if 检查(无法处理虚假唤醒) if (q->count == 0) pthread_cond_wait(...); // 正确:while 检查(唤醒后重新验证条件) while (q->count == 0) pthread_cond_wait(...);
4.2未加锁直接等待 / 唤醒
- 现象:程序崩溃或未定义行为;
- 原因:
pthread_cond_wait要求必须持有传入的互斥锁; - 解决:等待前必须
pthread_mutex_lock,唤醒可在锁内 / 锁外(建议锁内)。
4.3唤醒信号丢失
- 现象:线程永久休眠;
- 原因:释放锁后、休眠前,其他线程发送唤醒信号;
- 解决:依赖
pthread_cond_wait的原子性(释放锁 + 休眠是不可拆分的操作)。
4.4销毁有等待线程的条件变量
- 现象:程序崩溃或资源泄漏;
- 解决:确保所有线程退出等待后,再调用
pthread_cond_destroy。
二、信号量
信号量(Semaphore)是 Linux 中最通用的同步原语,是一个计数器:申请资源时计数器 - 1(P 操作),释放资源时计数器 + 1(V 操作);计数器≤0 时,申请资源的线程会阻塞,直到计数器> 0。
| 类型 | 核心结构体 | 适用场景 | 底层依赖 |
|---|---|---|---|
| 内核态信号量 | struct semaphore | 内核驱动、进程 / 线程同步 | 内核等待队列 + 自旋锁 |
| 用户态信号量 | sem_t(POSIX 信号量) | 用户态进程 / 线程同步 | futex(同互斥锁 / 条件变量) |
1.内核信号量(struct semaphore)
内核态信号量是 Linux 内核核心同步机制,用于内核态代码(如驱动、进程调度)的同步,不能直接在用户态使用。
1.1核心结构体
struct semaphore {
raw_spinlock_t lock; // 保护计数器的自旋锁
unsigned int count; // 资源计数器:>0=可用资源数,≤0=等待线程数
struct list_head wait_list;// 等待队列:存放阻塞的线程
};
1.2核心接口
// 初始化信号量:count为初始资源数
#define sema_init(sem, val) do { ... } while (0)
// P操作:申请资源(count-1),count≤0则阻塞
void down(struct semaphore *sem);
// 非阻塞P操作:申请失败立即返回非0
int down_trylock(struct semaphore *sem);
// 可中断P操作:阻塞时可被信号中断
int down_interruptible(struct semaphore *sem);
// V操作:释放资源(count+1),唤醒等待队列中的线程
void up(struct semaphore *sem);
1.3底层流程(伪代码)
void down(struct semaphore *sem) {
// 1. 加自旋锁保护计数器
spin_lock(&sem->lock);
// 2. 计数器>0:直接占用资源(count-1)
if (sem->count > 0) {
sem->count--;
spin_unlock(&sem->lock);
return;
}
// 3. 计数器≤0:加入等待队列,阻塞
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
spin_unlock(&sem->lock);
}
void up(struct semaphore *sem) {
// 1. 加自旋锁保护计数器
spin_lock(&sem->lock);
// 2. 计数器+1
sem->count++;
// 3. 有等待线程:唤醒队列中的一个线程
if (list_empty(&sem->wait_list) == false) {
wake_up_process(list_first_entry(&sem->wait_list, struct task_struct, wait_list));
}
spin_unlock(&sem->lock);
}
1.4核心特性
- 可重入性:若计数器初始值 > 1,同一线程可多次执行
down(直到 count=0); - 阻塞方式:线程进入内核休眠(TASK_UNINTERRUPTIBLE/TASK_INTERRUPTIBLE),不占用 CPU;
- 适用场景:内核驱动、进程间同步(如内核态共享资源)。
2.用户态信号量(sem_t,POSIX 信号量)
用户态信号量分两种:无名信号量(线程间同步)、有名信号量(进程间同步),底层均基于 futex 实现。
2.1. 核心接口(<semaphore.h>)
| 接口 | 作用 | 关键说明 |
|---|---|---|
sem_init(&sem, pshared, value) | 初始化信号量 | pshared=0:线程间同步;pshared=1:进程间同步;value:初始计数器 |
sem_wait(&sem) | P 操作(阻塞) | 计数器 - 1,≤0 则阻塞 |
sem_trywait(&sem) | P 操作(非阻塞) | 失败立即返回 - 1 |
sem_post(&sem) | V 操作 | 计数器 + 1,唤醒等待线程 |
sem_destroy(&sem) | 销毁无名信号量 | 仅用于线程间信号量 |
sem_open(name, oflag, mode, value) | 创建 / 打开有名信号量 | 进程间同步,需指定名称(如 "/my_sem") |
sem_close(&sem) | 关闭有名信号量 | 释放文件描述符 |
sem_unlink(name) | 删除有名信号量 | 移除文件系统中的信号量名称 |
2.2示例(生产者-消费者模型)
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define BUFFER_SIZE 5
#define PRODUCER_CNT 2
#define CONSUMER_CNT 2
// 共享缓冲区
int buffer[BUFFER_SIZE];
int in = 0, out = 0;
// 信号量定义
sem_t empty; // 空缓冲区数量(初始=BUFFER_SIZE)
sem_t full; // 满缓冲区数量(初始=0)
pthread_mutex_t mutex; // 保护缓冲区操作的互斥锁
// 生产者线程
void *producer(void *arg) {
int tid = *(int*)arg;
for (int i = 0; i < 10; i++) {
// P操作:申请空缓冲区(empty-1)
sem_wait(&empty);
// 加锁保护缓冲区
pthread_mutex_lock(&mutex);
// 生产数据
buffer[in] = i;
printf("生产者%d:生产%d,放入位置%d\n", tid, i, in);
in = (in + 1) % BUFFER_SIZE;
// 解锁
pthread_mutex_unlock(&mutex);
// V操作:释放满缓冲区(full+1)
sem_post(&full);
sleep(1); // 模拟生产耗时
}
free(arg);
return NULL;
}
// 消费者线程
void *consumer(void *arg) {
int tid = *(int*)arg;
for (int i = 0; i < 10; i++) {
// P操作:申请满缓冲区(full-1)
sem_wait(&full);
// 加锁保护缓冲区
pthread_mutex_lock(&mutex);
// 消费数据
int val = buffer[out];
printf("消费者%d:从位置%d消费%d\n", tid, out, val);
out = (out + 1) % BUFFER_SIZE;
// 解锁
pthread_mutex_unlock(&mutex);
// V操作:释放空缓冲区(empty+1)
sem_post(&empty);
sleep(2); // 模拟消费耗时
}
free(arg);
return NULL;
}
int main() {
pthread_t prod_tids[PRODUCER_CNT], cons_tids[CONSUMER_CNT];
// 初始化信号量
sem_init(&empty, 0, BUFFER_SIZE); // 初始空缓冲区=5
sem_init(&full, 0, 0); // 初始满缓冲区=0
pthread_mutex_init(&mutex, NULL);
// 创建生产者线程
for (int i = 0; i < PRODUCER_CNT; i++) {
int *tid = malloc(sizeof(int));
*tid = i + 1;
pthread_create(&prod_tids[i], NULL, producer, tid);
}
// 创建消费者线程
for (int i = 0; i < CONSUMER_CNT; i++) {
int *tid = malloc(sizeof(int));
*tid = i + 1;
pthread_create(&cons_tids[i], NULL, consumer, tid);
}
// 等待线程退出
for (int i = 0; i < PRODUCER_CNT; i++) {
pthread_join(prod_tids[i], NULL);
}
for (int i = 0; i < CONSUMER_CNT; i++) {
pthread_join(cons_tids[i], NULL);
}
// 销毁资源
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);
return 0;
}
2.3有名信号量
#include <stdio.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SEM_NAME "/my_sem"
int main() {
// 创建有名信号量(初始值=1)
sem_t *sem = sem_open(SEM_NAME, O_CREAT | O_EXCL, 0644, 1);
if (sem == SEM_FAILED) {
perror("sem_open failed");
return -1;
}
// 子进程
pid_t pid = fork();
if (pid == 0) {
// 子进程P操作
sem_wait(sem);
printf("子进程:占用信号量\n");
sleep(3);
sem_post(sem);
printf("子进程:释放信号量\n");
sem_close(sem);
return 0;
}
// 父进程
sleep(1); // 让子进程先执行
sem_wait(sem);
printf("父进程:占用信号量\n");
sem_post(sem);
printf("父进程:释放信号量\n");
// 清理资源
sem_close(sem);
sem_unlink(SEM_NAME); // 删除有名信号量
return 0;
}
2.4对比
| 特性 | 信号量(sem_t) | 互斥锁(pthread_mutex_t) | 条件变量(pthread_cond_t) |
|---|---|---|---|
| 核心功能 | 资源计数(互斥 + 同步) | 互斥访问(二进制信号量) | 等待 - 唤醒(同步) |
| 计数器 | 可设任意非负值 | 二进制(0/1) | 无计数器 |
| 适用场景 | 多资源同步(如缓冲区) | 单资源互斥 | 条件等待(如队列空 / 满) |
| 所有权 | 无(任意线程可释放) | 有(谁加锁谁解锁) | 无(配合互斥锁使用) |
| 底层实现 | futex | futex | futex |
关键区别
- 信号量是 “无所有权” 的:线程 A 执行
sem_wait,线程 B 可执行sem_post; - 互斥锁是 “有所有权” 的:必须由加锁的线程解锁,否则返回
EPERM; - 条件变量需绑定互斥锁:仅用于 “等待条件”,不能单独实现互斥。
2.5底层原理
typedef union {
struct {
unsigned int __value; // 信号量计数器
int __futex; // futex 等待队列(核心)
// 其他字段:进程间同步标识、锁等
} __data;
char __size[__SIZEOF_SEM_T];
long int __align;
} sem_t;
核心执行流程(sem_wait/sem_post)
- sem_wait(P 操作):
- 用户态原子减计数器:
__value--; - 若
__value ≥ 0:直接返回(快速路径); - 若
__value < 0:调用futex(FUTEX_WAIT)进入内核休眠;
- 用户态原子减计数器:
- sem_post(V 操作):
- 用户态原子加计数器:
__value++; - 若
__value ≤ 0(有等待线程):调用futex(FUTEX_WAKE)唤醒一个等待线程。
- 用户态原子加计数器:
2.6模拟 “负数计数”
static noinline void __down(struct semaphore *sem) {
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list); // 加入等待队列
waiter.task = current;
waiter.up = false;
// 核心:循环等待,直到被唤醒
for (;;) {
if (waiter.up) // 被唤醒(有资源了)
break;
// 将线程设为休眠状态,让出CPU
set_task_state(current, TASK_UNINTERRUPTIBLE);
spin_unlock_irq(&sem->lock);
schedule(); // 调度其他线程执行
spin_lock_irq(&sem->lock);
}
list_del(&waiter.list); // 移出等待队列
// 唤醒后:count 逻辑上 +1(实际是其他线程执行 up 操作时加的)
}
3.进程间同步—— 信号量+共享内存
3.1有名信号量
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
// 定义共享内存和信号量名称
#define SHM_NAME "/my_shm"
#define SEM_EMPTY "/sem_empty" // 空缓冲区信号量
#define SEM_FULL "/sem_full" // 满缓冲区信号量
#define SEM_MUTEX "/sem_mutex" // 互斥信号量
#define BUFFER_SIZE 5
// 共享缓冲区结构体(需与消费者一致)
typedef struct {
int data[BUFFER_SIZE];
int in;
int out;
} SharedBuffer;
int main() {
// 1. 创建/打开共享内存(大小为SharedBuffer)
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0644);
ftruncate(shm_fd, sizeof(SharedBuffer));
SharedBuffer *buf = mmap(NULL, sizeof(SharedBuffer), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
buf->in = 0;
buf->out = 0;
// 2. 创建/打开有名信号量
sem_t *sem_empty = sem_open(SEM_EMPTY, O_CREAT | O_EXCL, 0644, BUFFER_SIZE);
sem_t *sem_full = sem_open(SEM_FULL, O_CREAT | O_EXCL, 0644, 0);
sem_t *sem_mutex = sem_open(SEM_MUTEX, O_CREAT | O_EXCL, 0644, 1);
// 3. 生产数据(循环5次)
for (int i = 0; i < 10; i++) {
// P操作:申请空缓冲区
sem_wait(sem_empty);
// P操作:申请互斥锁(保护缓冲区)
sem_wait(sem_mutex);
// 写入共享缓冲区
buf->data[buf->in] = i;
printf("生产者:写入数据 %d 到位置 %d\n", i, buf->in);
buf->in = (buf->in + 1) % BUFFER_SIZE;
// V操作:释放互斥锁
sem_post(sem_mutex);
// V操作:释放满缓冲区
sem_post(sem_full);
sleep(1); // 模拟生产耗时
}
// 4. 清理资源
sem_close(sem_empty);
sem_close(sem_full);
sem_close(sem_mutex);
munmap(buf, sizeof(SharedBuffer));
close(shm_fd);
// 注意:sem_unlink 一般由最后退出的进程执行
// sem_unlink(SEM_EMPTY);
// sem_unlink(SEM_FULL);
// sem_unlink(SEM_MUTEX);
// shm_unlink(SHM_NAME);
return 0;
}
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/mman.h>
#define SHM_NAME "/my_shm"
#define SEM_EMPTY "/sem_empty"
#define SEM_FULL "/sem_full"
#define SEM_MUTEX "/sem_mutex"
#define BUFFER_SIZE 5
typedef struct {
int data[BUFFER_SIZE];
int in;
int out;
} SharedBuffer;
int main() {
// 1. 打开共享内存
int shm_fd = shm_open(SHM_NAME, O_RDWR, 0644);
SharedBuffer *buf = mmap(NULL, sizeof(SharedBuffer), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
// 2. 打开已存在的有名信号量(无需O_CREAT)
sem_t *sem_empty = sem_open(SEM_EMPTY, 0);
sem_t *sem_full = sem_open(SEM_FULL, 0);
sem_t *sem_mutex = sem_open(SEM_MUTEX, 0);
// 3. 消费数据(循环5次)
for (int i = 0; i < 10; i++) {
// P操作:申请满缓冲区
sem_wait(sem_full);
// P操作:申请互斥锁
sem_wait(sem_mutex);
// 读取共享缓冲区
int val = buf->data[buf->out];
printf("消费者:从位置 %d 读取数据 %d\n", buf->out, val);
buf->out = (buf->out + 1) % BUFFER_SIZE;
// V操作:释放互斥锁
sem_post(sem_mutex);
// V操作:释放空缓冲区
sem_post(sem_empty);
sleep(2); // 模拟消费耗时
}
// 4. 清理资源(最后退出的进程执行unlink)
sem_close(sem_empty);
sem_close(sem_full);
sem_close(sem_mutex);
munmap(buf, sizeof(SharedBuffer));
close(shm_fd);
// 释放内核资源
sem_unlink(SEM_EMPTY);
sem_unlink(SEM_FULL);
sem_unlink(SEM_MUTEX);
shm_unlink(SHM_NAME);
return 0;
}
3.2无名信号量
sem_init的时候,pshared参数一定要为1。如果为0表示线程间同步,会卡死,1表示进程间同步。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
// 共享内存中存储信号量和共享数据
typedef struct {
sem_t sem; // 无名信号量
int shared_data; // 共享数据
} SharedData;
int main() {
// 1. 创建共享内存(匿名映射,父子进程共享)
SharedData *shm = mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
// 2. 初始化无名信号量:pshared=1(进程间共享),初始值=1
sem_init(&shm->sem, 1, 1);
shm->shared_data = 0;
// 3. 创建子进程
pid_t pid = fork();
if (pid == 0) {
// 子进程:修改共享数据(5次)
for (int i = 0; i < 5; i++) {
sem_wait(&shm->sem); // P操作:申请资源
shm->shared_data += 10;
printf("子进程:修改后 shared_data = %d\n", shm->shared_data);
sem_post(&shm->sem); // V操作:释放资源
sleep(1);
}
munmap(shm, sizeof(SharedData));
exit(0);
} else if (pid > 0) {
// 父进程:读取共享数据(5次)
for (int i = 0; i < 5; i++) {
sem_wait(&shm->sem); // P操作:申请资源
printf("父进程:读取 shared_data = %d\n", shm->shared_data);
sem_post(&shm->sem); // V操作:释放资源
sleep(1);
}
// 等待子进程退出
wait(NULL);
// 清理资源
sem_destroy(&shm->sem);
munmap(shm, sizeof(SharedData));
}
return 0;
}
三、扩展
1.加入等待队列后,等到资源释放出来时,等待队列里的多个等待任务会按顺序去获取信号量资源吗?
这个问题触及了信号量等待队列的调度公平性核心—— 结论先明确:默认情况下,等待队列中的线程并非严格按 “先入先出(FIFO)” 顺序获取资源,而是受内核调度策略、等待类型(排他 / 非排他)、CPU 抢占等因素影响。
semaphore_waiter 是信号量专属的等待节点(区别于通用的 wait_queue_entry_t),本质是对通用等待队列节点的封装,定义如下(简化版):
struct semaphore_waiter {
struct list_head list; // 链接到 sem->wait_list 的链表节点
struct task_struct *task; // 等待的线程
bool up; // 标记是否被唤醒(up=true 表示可获取资源)
};
1. 链表层面:等待线程按 FIFO 入队
当多个线程因 sem->count=0 调用 down 时:
- 线程 A → 加入等待队列尾部 → 队列:[A]
- 线程 B → 加入等待队列尾部 → 队列:[A→B]
- 线程 C → 加入等待队列尾部 → 队列:[A→B→C]从链表结构上,节点的排列顺序是严格 FIFO 的(先到的在队头,后到的在队尾)。
2. 资源获取层面:并非严格 FIFO
当资源释放(调用 up)时,内核会执行以下逻辑:
// up 操作的核心唤醒逻辑(简化版)
if (!list_empty(&sem->wait_list)) {
// 1. 取出队列头部的第一个等待节点(线程A)
struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, ...);
// 2. 从队列中删除该节点
list_del(&waiter->list);
// 3. 标记为“可唤醒”
waiter->up = true;
// 4. 唤醒线程A(设置为 TASK_RUNNING)
wake_up_process(waiter->task);
}
从代码上看,内核确实优先唤醒队列头部的线程(FIFO),但这只是 “唤醒顺序”,而非 “资源获取顺序”—— 关键差异在:
(1)唤醒 ≠ 立即获取资源
- 线程 A 被
wake_up_process()标记为TASK_RUNNING后,只是 “进入 CPU 就绪队列”,并非立即执行; - 如果此时有更高优先级的线程在运行,或 CPU 核心被占满,线程 A 可能延后执行;
- 而后续被唤醒的线程(如线程 B),若刚好抢占到 CPU,可能 “插队” 先获取资源(但信号量本身是互斥的,最终只有一个线程能拿到)。
(2)排他等待的 “批量唤醒”(惊群优化)
Linux 信号量的等待是排他等待(WQ_FLAG_EXCLUSIVE):
- 调用
up时,内核默认只唤醒队列头部的一个线程(而非所有),避免 “惊群”(多个线程被唤醒但只有一个能拿到资源,其余重新休眠); - 这个 “只唤醒队头” 的逻辑,让信号量的等待队列尽可能接近 FIFO,但仍受调度器影响。
(3)内核调度策略的干扰
- 若等待线程的调度策略是
SCHED_RR(轮转)或SCHED_FIFO(实时 FIFO),高优先级的实时线程会优先抢占 CPU,即使它在等待队列中排后面; - 普通线程(
SCHED_OTHER)则受动态优先级、CPU 亲和性等影响,不一定按唤醒顺序执行。

1万+

被折叠的 条评论
为什么被折叠?



