(一)什么是信号量
互斥相关概念
1、并发:
2、互斥
3、临界资源&临界区
4、原子性
(二)信号量的理解
(三)信号量的两种基本操作 P / V
(四)信号量的内核数据结构
(五)信号量的相关接口
1、创建信号量 - semget
2、释放信号量
(1)指令释放
(2)调用系统函数
3、PV操作 - semop
之前两个博客已经讲了System V IPC的两种形式:共享内存和消息队列,今天我们将System V IPC的最后一种方式 :信号量
(一)什么是信号量
System V 信号量是一种用于进程间通信和同步的机制,用于控制对共享资源的访问。它通常用于解决竞争条件和死锁等并发编程中的问题。
在正式学习信号量之前,我们需要先了解与互斥相关的四个概念,为后面多线程中信号量的学习作铺垫。
互斥相关概念
1、并发:
- 并发是指在系统能够同时执行多个任务或者处理多个操作,这个概念在操作系统中非常重要,特别是在多用户系统和服务器环境中。
- 并发主要在以下场景中应用:多任务处理、多线程、进程间通信、同步和互斥、并行处理以及事件驱动编程。
2、互斥
- 互斥是一种资源访问控制的机制,用于确保在任意时刻只有一个进程或线程能够访问某个共享资源,以防止数据竞争和不一致性,可以避免竞态条件和死锁等并发编程中常见的问题。
- 在并发编程中,互斥是确保共享资源在被一个进程或线程访问时处于被锁定状态,其他进程或线程需要等待锁释放后才能访问该资源的过程。
- 在Linux中,实现互斥的常见方式包括使用互斥锁(mutex)和信号量。
3、临界资源&临界区
- 临界资源指的是一种共享资源,它被多个线程或进程共同访问和修改。由于多个线程或进程可能同时尝试对该资源进行操作,因此需要采取措施来确保对该资源的访问是安全和有序的。
- 临界区是指访问临界资源的代码段或区域。在临界区内,对临界资源的访问需要通过同步机制来保证其一致性。
- 在并发程序中,为了保护临界资源,常常会使用互斥锁、信号量等同步机制来实现临界区的互斥访问。一旦一个线程或进程进入了临界区,其他线程或进程就不能进入,知道当前线程或进程离开临界区位置,这样可以避免数据竞争和不一致性。
4、原子性
- 原子性是指一个操作是不可分割的或者是不可中断的。在并发编程中,原子性是指一个操作在执行过程中不会被其他线程的操作所干扰,要么该操作执行完成,要么没有执行,不会出现部分执行的情况。
- 原子性通常用来描述对共享资源的访问或修改操作。如果一个操作是原子的,那么在多线程或多进程环境下,这个操作的执行过程中不会被其他线程或进程的操作所影响,可以确保数据的一致性和正确性。
(二)信号量的理解
将整个程序看作现实世界,人们看作执行流,电影院或手机等买票场所或方式看作临界区,而单场电影的电影票看作临界资源,电影院中单场电影余票的计数器就是信号量。
当电影票卖完时,计数器归零,其他想看电影的人也无法购票观看本场电影
所以有了下面这些情况:
- 当你购票成功后,计数器-1,你必然可以去看这场电影,而其他人无法和你抢,因为那个位置已经是你的了。
- 如果你买票完了,票卖完了,计数器为0,你就无法购票观看这场电影,也就没有一个座位是属于你的。
- 因为有了计数器,电影院有效划分了电影票这个临界资源的所属权限,从而保证了在电影放映时,绝对不会发生位置冲突等各种情况。
信号量的设计就是为了避免 因为多执行流对临界资源的并发访问,而导致程序出现问题。
而互斥怎么理解呢?
我们可以想象一个VIP放映室,每次只允许一个进入看电影,和普通电影院一样,这个VIP放映室也有自己的计数器,但计数器初始值为1,这也叫二元信号量,而上面普通电影院的计数器则叫做计数信号量。
(三)信号量的两种基本操作 P / V
P操作(也成为down操作或者wait操作)
- P操作用于获取(或者说获得)一个信号量,并且如果信号量的值大于0,就将它减一。如果信号量的值已经是0,那么P操作就会阻塞当前进程,直到信号量的值不再是0,然后再将其减一。
- 在二元信号量中,P操作相当于将信号量的减一,并且在信号量的值变为0时会阻塞。
V操作(也成为up操作或者signal操作)
- V操作用于释放(或者说放回)一个信号量,并将它的值加一。如果有其他线程因为等待信号量而阻塞,那么V操作就会唤醒其中一个被阻塞的进程。
- 在二元信号量中,V操作相当于将信号量的值加一,并且在信号量的值从0变为1时会唤醒等待的进程。
这两种操作通常用于控制对共享资源的访问每一集实现线程或进程之间的同步。在许多操作系统和编程语言中,都提供了对信号量的支持,并且通常也提供了用于执行P和V操作的相应函数或者方法。
(四)信号量的内核数据结构
下面来看看信号量的数据结构,使用man semctl命令查看
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Creation time/time of last
modification via semctl() */
unsigned long sem_nsems; /* No. of semaphores in set */
};
- struct ipc_perm sem_perm:ipc_perm结构体类型,用于描述信号量的权限。
- time _t sem_otime:表示信号量集上次操作的时间。
- time_t sem_ctime :表示信号量集上次改变(包括创建、删除、修改)的时间。
- unsigned long sem_nsems:表示信号量集中的信号量的数量。
根据System V IPC的基本规律,struct ipc_perm中肯定存储了信号的权限,内容如下
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
由此我们可以总结出,无论是共享内存、消息队列、信号量,它们的ipc_perm结构体中的内容都是一摸一样的,结构上的统一可以带来管理上的便利,具体为什么是一样的可以接着往下看。
(五)信号量的相关接口
1、创建信号量 - semget
semget函数用于创建或者获取一个System V 信号量集的函数,其原型为:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数:
- key_t key:用于唯一标识信号量集的键值,通常使用ftok函数来生成键值,也可以手动指定一个键值。
- int nsems:标识需要创建或者获取的信号量数量。如果是创建操作,需要指定这个数量,如果是获取操作,该参数可以设为0。
- int semflg:指定一些额外的标志位用于控制函数的行为。常见的标志包括IPC_CREAT和IPC_EXCL。
返回值 :
- semget函数返回值为一个非负整数,标识信号量集的标识符(成为信号量集ID),如果出现错误,则返回-1,并设置错误码。
我们可以创建一个简单的例子,并用指令ipcs -s查看创建的信号量集的信息:
#include<iostream>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
using namespace std;
int main()
{
int n =semget(ftok("./",668),1,IPC_CREAT | IPC_EXCL | 0666);
if(n == -1)
{
cerr<<"semget fail"<<endl;
exit(1);
}
return 0;
}
2、释放信号量
(1)指令释放
我们可以用指令ipcrm -s semid直接释放信号量集
(2)调用系统函数
我们可以在代码中利用semctl函数来释放信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
参数:
semid
:表示要操作的信号量集的标识符(即信号量集ID),由semget()
函数返回。semnum
:表示要操作的具体信号量在信号量集中的索引,从0开始计数。cmd
:表示要执行的操作命令,可以是下列值之一:GETVAL
:获取指定信号量的当前值。SETVAL
:设置指定信号量的值。GETPID
:获取上次执行semop()
操作的进程ID。GETNCNT
:获取当前正在等待资源的进程数。GETZCNT
:获取当前等待释放的资源的进程数。GETALL
:获取所有信号量的值。SETALL
:设置所有信号量的值。IPC_RMID
:从系统中删除信号量集。
- 最后一个参数根据cmd参数的不同而有所变化。例如,对于SETVAL和GETVAL命令,需要传递一个union semun类型的参数,定义如下:
union semun { int val; // SETVAL 的参数,用于设置信号量的值 struct semid_ds *buf; // IPC_STAT 和 IPC_SET 的参数,用于获取或设置信号量集的信息 unsigned short *array; // GETALL 和 SETALL 的参数,用于设置或获取所有信号量的值 struct seminfo *__buf; // Linux 特有的参数,用于获取额外信息 };
返回值:
- 如果执行失败返回-1,并设置错误码;否则根据cmd参数的不同返回不同的值。
3、PV操作 - semop
semop函数使用于执行信号量操作的函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
- int semid:信号量集的标识符,由semget函数返回
- struct sembuf *sops:一个指向sembuf结构体的制作真,每个sembuf结构描述一个信号量操作,定义如下:
-
struct sem_buf { unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ };
其中sem_num代表信号量在集合中的索引;sem_op表示要进行的操作,负数表示P操作,正数表示V操作,0表示检查信号量值;sem_flg 操作标志,可以为IPC_NOWAIT或者SEM_UNDO。
-
返回值:
- 操作成功返回0,否则返回-1,并设置错误码。
这就是System V 信号量部分的内容了。