OS79.【Linux】POSIX信号量 目录1.知识回顾2.在信号量的保护下访问临界资源3.POSIX信号量的系统调用sem_initsem_destroysem_waitsem_trywaitsem_post4.glibc-2.43源码分析sem_t类型sem_t实际上是联合体POSIX信号量的系统调用把联合体当结构体使用1.知识回顾之前在OS55.【Linux】理解信号量(不是信号)文章介绍过信号量,也简单提到过System V有关于信号量的系统调用这里摘一点重要的内容:1. 申请计数器成功,就表示具有访问资源的权限了2. 申请了计数器资源,当前可能尚未访问资源,即申请是资源的预订机制3. 计数器可以有效保证进入共享资源的执行流的数量4. 所以每一个执行流,想访问共享资源中的一部分的时候.不是直接访问,而是先申请计数器资源,类比看电影的先买票计算机科学中,把这个计数器称为信号量信号量本身也需要被保护,那么这把计数器的本质是什么?信号量是保证PV操作原子性的一把计数器1.申请信号量,本质是对计数器做减1操作,即P操作2.释放资源,释放信号量,本质是对计数器进行加1操作,即V操作3.申请和释放称为PV操作,且操作是原子的由于System V信号量现在已经不常用了,本文介绍POSIX标准下的信号量之前在https://zhangcoder.blog.csdn.net/article/details/158321006?spm1011.2415.3001.5331文章写过基于阻塞队列的多生产者-多消费者模型的代码#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_capacity20) :_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::queueT _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_queueint* bqstatic_castblocking_queueint*(args); for (;;) { int datarand()%10; bq-push(data); printf(生产者生产了数据%d\n,data); } return nullptr; } void* consume(void* args) { pthread_detach(pthread_self()); blocking_queueint* bqstatic_castblocking_queueint*(args); for (;;) { int databq-pop(); printf(消费者消费了数据%d\n,data); } return nullptr; } int main() { srand((unsigned int)time(NULL)); blocking_queueint* bqnew blocking_queueint(); pthread_t producer,consumer; for (int i0;iPRODUCER_CNT;i) pthread_create(producer,nullptr,produce,bq); for (int i0;iCOMSUMER_CNT;i) pthread_create(consumer,nullptr,consume,bq); for(;;){sleep(100);} delete bq; return 0; }注意到生产者和消费者只能有一个能访问_q队列,那么_q队列这个共享资源是当成整体使用的也可以把_q队列这个共享资源拆成多份,每个线程只访问其中的一份,可以用信号量控制,本文后面将实现2.在信号量的保护下访问临界资源总共3步: 申请临界资源(P操作) → 访问资源 → 释放临界资源(V操作)申请信号量如果成功,那么申请临界资源就成功(不需要手动判断临界资源,P操作内部自带计数和阻塞逻辑)释放信号量如果成功,那么释放临界资源就成功用伪代码表示://锁: 手动判断资源是否就绪 加锁(); while (资源不可用) { 解锁(); 放条件变量下的等待队列中休眠,等待被唤醒(); 加锁(); } 使用资源(); 解锁(); //信号量: 无需手动判断资源是否就绪 P操作(); 使用资源(); V操作();结论:PV操作之间不需要像锁那样判断资源是否就绪3.POSIX信号量的系统调用sem_initint sem_init(sem_t *sem, int pshared, unsigned int value);作用: 初始化信号量参数sem: 信号量指针参数pshared: 0表示线程间共享,非零表示进程间共享参数value: 提供信号量的初始值,上面提过了信号量是保证PV操作原子性的一把计数器sem_destroyint sem_destroy(sem_t *sem);作用: 销毁信号量sem_waitint sem_wait(sem_t *sem);作用: 等待信号量,如果信号量的值不为0(当前有空闲的临界资源),就会将信号量的值减1,即P操作如果当前没有有空闲的临界资源,那么阻塞等待sem_trywait作用: 等待信号量,但等待的逻辑和sem_wait不同,sem_trywait是非阻塞等待,即申请不到信号量就里面出错返回,不像sem_wait在那里一直等sem_postint 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中定义了:#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是同一个函数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); #endifsem_init先通过struct new_sem *isem (struct new_sem *) sem,把联合体指针转为结构体指针,再初始化各个字段: isem-data、isem-value、......其中struct new_sem定义在/sysdeps/nptl/internaltypes.h中:/* 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联合体当结构体使用,通过强制类型转换访问内部成员,使用联合体为了对外隐藏结构