目录
1.知识回顾
之前在OS55.【Linux】理解信号量(不是信号)文章介绍过信号量,也简单提到过System V有关于信号量的系统调用
这里摘一点重要的内容:
申请计数器成功,就表示具有访问资源的权限了
申请了计数器资源,当前可能尚未访问资源,即申请是资源的预订机制
计数器可以有效保证进入共享资源的执行流的数量
所以每一个执行流,想访问共享资源中的一部分的时候.不是直接访问,而是先申请计数器资源,类比看电影的先买票
计算机科学中,把这个"计数器"称为信号量
信号量本身也需要被保护,那么这把计数器的本质是什么?
信号量是保证PV操作原子性的一把计数器
1.申请信号量,本质是对计数器做减1操作,即P操作
2.释放资源,释放信号量,本质是对计数器进行加1操作,即V操作
3.申请和释放称为PV操作,且操作是原子的
由于System V信号量现在已经不常用了,本文介绍POSIX标准下的信号量
之前在https://zhangcoder.blog.csdn.net/article/details/158321006?spm=1011.2415.3001.5331文章写过基于阻塞队列的多生产者-多消费者模型的代码
cpp
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <queue>
#define PRODUCER_CNT 5
#define COMSUMER_CNT 5
template <class T>
class blocking_queue
{
public:
blocking_queue(size_t max_capacity=20)
:_max_capacity(max_capacity)
{
pthread_mutex_init(&_lock,nullptr);
pthread_cond_init(&_p_cond,nullptr);
pthread_cond_init(&_c_cond,nullptr);
}
T& pop()
{
pthread_mutex_lock(&_lock);
while (_q.empty())
pthread_cond_wait(&_c_cond,&_lock);
auto& obj=_q.front();
_q.pop();
pthread_cond_broadcast(&_p_cond);
pthread_mutex_unlock(&_lock);
return obj;
}
void push(const T& obj)
{
pthread_mutex_lock(&_lock);
while (_q.size()>=_max_capacity)
pthread_cond_wait(&_p_cond,&_lock);
_q.push(obj);
pthread_cond_broadcast(&_c_cond);
pthread_mutex_unlock(&_lock);
}
~blocking_queue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_p_cond);
pthread_cond_destroy(&_c_cond);
while(!_q.empty())
_q.pop();
}
private:
std::queue<T> _q;
size_t _max_capacity;
pthread_mutex_t _lock;
pthread_cond_t _p_cond;//生产者的条件变量
pthread_cond_t _c_cond;//消费者的条件变量
};
void* produce(void* args)
{
pthread_detach(pthread_self());
blocking_queue<int>* bq=static_cast<blocking_queue<int>*>(args);
for (;;)
{
int data=rand()%10;
bq->push(data);
printf("生产者生产了数据%d\n",data);
}
return nullptr;
}
void* consume(void* args)
{
pthread_detach(pthread_self());
blocking_queue<int>* bq=static_cast<blocking_queue<int>*>(args);
for (;;)
{
int data=bq->pop();
printf("消费者消费了数据%d\n",data);
}
return nullptr;
}
int main()
{
srand((unsigned int)time(NULL));
blocking_queue<int>* bq=new blocking_queue<int>();
pthread_t producer,consumer;
for (int i=0;i<PRODUCER_CNT;i++)
pthread_create(&producer,nullptr,produce,bq);
for (int i=0;i<COMSUMER_CNT;i++)
pthread_create(&consumer,nullptr,consume,bq);
for(;;){sleep(100);}
delete bq;
return 0;
}
注意到生产者和消费者只能有一个能访问_q队列,那么_q队列这个共享资源是当成整体使用的
也可以把_q队列这个共享资源拆成多份,每个线程只访问其中的一份,可以用信号量控制,本文后面将实现
2.在信号量的保护下访问临界资源
总共3步: 申请临界资源(P操作) → 访问资源 → 释放临界资源(V操作)
申请信号量如果成功,那么申请临界资源就成功(不需要手动判断临界资源,P操作内部自带计数和阻塞逻辑)
释放信号量如果成功,那么释放临界资源就成功
用伪代码表示:
cpp
//锁: 手动判断资源是否就绪
加锁();
while (资源不可用)
{
解锁();
放条件变量下的等待队列中休眠,等待被唤醒();
加锁();
}
使用资源();
解锁();
//信号量: 无需手动判断资源是否就绪
P操作();
使用资源();
V操作();
结论: PV操作之间不需要像锁那样判断资源是否就绪
3.POSIX信号量的系统调用
sem_init

cpp
int sem_init(sem_t *sem, int pshared, unsigned int value);
作用: 初始化信号量
参数sem: 信号量指针
参数pshared: 0表示线程间共享,非零表示进程间共享
参数value: 提供信号量的初始值,上面提过了信号量是保证PV操作原子性的一把计数器
sem_destroy

cpp
int sem_destroy(sem_t *sem);
作用: 销毁信号量
sem_wait

cpp
int sem_wait(sem_t *sem);
作用: 等待信号量,如果信号量的值不为0(当前有空闲的临界资源),就会将信号量的值减1,即P操作
如果当前没有有空闲的临界资源,那么阻塞等待
sem_trywait
作用: 等待信号量,但等待的逻辑和sem_wait不同,sem_trywait是非阻塞等待,即申请不到信号量就里面出错返回,不像sem_wait在那里一直等
sem_post

cpp
int sem_post(sem_t *sem);
作用: 发布信号量,表示申请到的临界资源使用完毕,可以归还该资源了,将信号量值加1,即V操作
4.glibc-2.43源码分析sem_t类型
sem_t实际上是联合体
sem_t实际上是一个联合体,在glibc-2.43的/sysdeps/unix/sysv/linux/bits/semaphore.h中定义了:
cpp
#ifndef _SEMAPHORE_H
# error "Never use <bits/semaphore.h> directly; include <semaphore.h> instead."
#endif
#include <bits/wordsize.h>
#if __WORDSIZE == 64
# define __SIZEOF_SEM_T 32
#else
# define __SIZEOF_SEM_T 16
#endif
/* Value returned if `sem_open' failed. */
#define SEM_FAILED ((sem_t *) 0)
typedef union
{
char __size[__SIZEOF_SEM_T];
long int __align;
} sem_t;
__WORDSIZE是字长,和系统架构有关
如果字长是64位,那么__size数组为32字节;
如果字长是32位,那么__size数组为16字节;
__align保证sem_t联合体对齐
POSIX信号量的系统调用把联合体当结构体使用
以sem_init为例说明,定义在/nptl/sem_init.c中:
sem_init和__new_sem_init是同一个函数
cpp
int
__new_sem_init (sem_t *sem, int pshared, unsigned int value)
{
ASSERT_PTHREAD_INTERNAL_SIZE (sem_t, struct new_sem);
/* Parameter sanity check. */
if (__glibc_unlikely (value > SEM_VALUE_MAX))
{
__set_errno (EINVAL);
return -1;
}
/* Map to the internal type. */
struct new_sem *isem = (struct new_sem *) sem;
/* Use the values the caller provided. */
#if USE_64B_ATOMICS_ON_SEM_T
isem->data = value;
#else
isem->value = value << SEM_VALUE_SHIFT;
/* pad is used as a mutex on pre-v9 sparc and ignored otherwise. */
isem->pad = 0;
isem->nwaiters = 0;
#endif
isem->private = (pshared == 0 ? FUTEX_PRIVATE : FUTEX_SHARED);
return 0;
}
versioned_symbol (libc, __new_sem_init, sem_init, GLIBC_2_34);
#if OTHER_SHLIB_COMPAT(libpthread, GLIBC_2_1, GLIBC_2_34)
compat_symbol (libpthread, __new_sem_init, sem_init, GLIBC_2_1);
#endif
sem_init先通过struct new_sem *isem = (struct new_sem *) sem,把联合体指针转为结构体指针,
再初始化各个字段: isem->data、isem->value、......
其中struct new_sem定义在/sysdeps/nptl/internaltypes.h中:
cpp
/* Semaphore variable structure. */
struct new_sem
{
#if USE_64B_ATOMICS_ON_SEM_T
/* The data field holds both value (in the least-significant 32 bits) and
nwaiters. */
# if __BYTE_ORDER == __LITTLE_ENDIAN
# define SEM_VALUE_OFFSET 0
# elif __BYTE_ORDER == __BIG_ENDIAN
# define SEM_VALUE_OFFSET 1
# else
# error Unsupported byte order.
# endif
# define SEM_NWAITERS_SHIFT 32
# define SEM_VALUE_MASK (~(unsigned int)0)
uint64_t data;
int private;
int pad;
#else
# define SEM_VALUE_SHIFT 1
# define SEM_NWAITERS_MASK ((unsigned int)1)
unsigned int value;
int private;
int pad;
unsigned int nwaiters;
#endif
};
sem_init把sem_init提供的信号量的值存到了struct new_sem的value字段
结论: POSIX信号量的系统调用把sem_t联合体当结构体使用,通过强制类型转换访问内部成员,使用联合体为了对外隐藏结构