信号量(Semaphore)是操作系统中用于管理并发进程的一种同步机制。它们用于控制对共享资源的访问,以避免竞争条件和数据不一致的问题。信号量主要分为两种类型:计数信号量(Counting Semaphore)和二进制信号量(Binary Semaphore),又称互斥量(Mutex)。
计数信号量是一个整数值,可以用于控制对多个资源的访问。它的初始值表示可用资源的数量。计数信号量支持两种基本操作:
二进制信号量只有0和1两个值,常用于实现互斥访问。它也支持P和V操作,但由于值只能为0或1,因此操作更简单:
#include
提供了System V IPC(进程间通信)机制的通用接口,包括消息队列、信号量和共享内存。
主要用于生成IPC键值(ftok
函数)和定义IPC相关的常量。
#include
提供了System V共享内存的接口,包括创建、连接、分离和控制共享内存段的函数。
常用函数包括 shmget
、shmat
、shmdt
、shmctl
等。
#include
定义了许多数据类型,用于其他系统调用接口。例如,pid_t
(进程ID)、key_t
(IPC键值)、size_t
(对象大小)等。
提供了一些POSIX标准定义的类型,用于与系统调用进行交互。
#include
提供了System V信号量的接口,包括创建、操作和控制信号量集的函数。
常用函数包括 semget
、semop
、semctl
等。
以下例程来自b站公开课,C++中高级程序员实战(码农联盟)
这段代码演示了如何使用信号量对共享内存进行加锁,以确保对共享内存的访问是线程安全的。下面是对这段代码的详细分析
这里头文件里存放了以下函数声明,csemp 类的声明和定义,用于信号量操作。
#include "_public.h"
struct stgirl // 超女结构体。
{
int no; // 编号。
char name[51]; // 姓名,注意,不能用string。
};
int main(int argc, char *argv[])
{
if (argc != 3) {
cout << "Using:./demo no name\n";
return -1;
}
相关的如 shmget() 函数的介绍放在我另外一个笔记中:Linux 共享内存
int shmid = shmget(0x5005, sizeof(stgirl), 0640 | IPC_CREAT);
if (shmid == -1) {
cout << "shmget(0x5005) failed.\n";
return -1;
}
cout << "shmid=" << shmid << endl;
补充:
组合使用权限标志
- 在上述示例中,IPC_CREAT 常常与权限标志组合使用,如 0644 或 0666。这些权限标志类似于文件权限,定义了对 IPC 资源的读写权限。
- 0644:所有者读写权限,其他用户只读权限。
- 0666:所有用户都具有读写权限。
0640 权限表示:
- 所有者(Owner):读(r)+ 写(w),即6。
- 所属组(Group):读(r),即4。
- 其他用户(Others):没有权限(0)。
- 这个权限设置保证了对IPC资源的合理访问控制,防止未授权用户对资源进行读写操作。
stgirl *ptr = (stgirl *)shmat(shmid, 0, 0);
if (ptr == (void *)-1) {
cout << "shmat() failed\n";
return -1;
}
csemp mutex;
if (mutex.init(0x5005) == false) {
cout << "mutex.init(0x5005) failed.\n";
return -1;
}
cout << "申请加锁...\n";
mutex.wait(); // 申请加锁。
cout << "申请加锁成功。\n";
cout << "原值:no=" << ptr->no << ",name=" << ptr->name << endl; // 显示共享内存中的原值。
ptr->no = atoi(argv[1]); // 对超女结构体的no成员赋值。
strcpy(ptr->name, argv[2]); // 对超女结构体的name成员赋值。
cout << "新值:no=" << ptr->no << ",name=" << ptr->name << endl; // 显示共享内存中的当前值。
sleep(10);
其中 wait() 是 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
if (m_semid==-1) return false;
struct sembuf sem_b;
sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
sem_b.sem_op = value; // P操作的value必须小于0。
sem_b.sem_flg = m_sem_flg;
if (semop(m_semid,&sem_b,1) == -1) { perror("p semop()"); return false; }
return true;
}
atoi 函数
atoi(ASCII to Integer)函数是C标准库中的一个函数,用于将C风格的字符串转换为整数。它的声明在头文件 中。
在 main 函数中,命令行参数会被存储在 argv 数组中:
argv[0] 存储程序的名称,即 ./demo。
argv[1] 存储第一个参数,即 “12345”。
argv[2] 存储第二个参数,即 “John”。
atoi(argv[1]); 的作用是将字符串 “12345” 转换为整数 12345。
在这段代码中,atoi(argv[1]) 的具体作用如下:
mutex.post(); // 解锁。
cout << "解锁。\n";
// 查看信号量 :ipcs -s // 删除信号量 :ipcrm sem 信号量id
// 查看共享内存:ipcs -m // 删除共享内存:ipcrm -m 共享内存id
// 第4步:把共享内存从当前进程中分离。
shmdt(ptr);
// 第5步:删除共享内存。
if (shmctl(shmid, IPC_RMID, 0) == -1) {
cout << "shmctl failed\n";
return -1;
}
}
mutex.post() 表示对信号量进行V操作,解锁对共享资源的访问。具体来说,当一个进程完成对共享资源的操作后,它会调用 mutex.post() 来释放信号量,使得其他等待该信号量的进程可以继续执行。
mutex是我们之前用类创建的对象,它去调用post()函数
post函数
// 信号量的V操作(把信号量的值减value)。
bool csemp::post(short value)
{
if (m_semid==-1) return false;
struct sembuf sem_b;
sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
sem_b.sem_op = value; // V操作的value必须大于0。
sem_b.sem_flg = m_sem_flg;
if (semop(m_semid,&sem_b,1) == -1) { perror("V semop()"); return false; }
return true;
}
_public.h
#ifndef __PUBLIC_HH
#define __PUBLIC_HH 1
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
// 信号量。
class csemp
{
private:
union semun // 用于信号量操作的共同体。 操作信号量需要这样的数据结构
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int m_semid; // 信号量id(描述符)。
// 如果把sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,
// 在全部修改过信号量的进程(正常或异常)终止后,操作系统将把信号量恢复为初始值。
// 如果信号量用于互斥锁,设置为SEM_UNDO。
// 如果信号量用于生产消费者模型,设置为0。
short m_sem_flg;
csemp(const csemp &) = delete; // 禁用拷贝构造函数。
csemp &operator=(const csemp &) = delete; // 禁用赋值函数。
public:
csemp():m_semid(-1){}
// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool init(key_t key,unsigned short value=1,short sem_flg=SEM_UNDO);
bool wait(short value=-1);// 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool post(short value=1); // 信号量的V操作。
int getvalue(); // 获取信号量的值,成功返回信号量的值,失败返回-1。
bool destroy(); // 销毁信号量。
~csemp();
};
#endif
_public.cpp
#include "_public.h"
// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key,unsigned short value,short sem_flg)
{
if (m_semid!=-1) return false; // 如果已经初始化了,不必再次初始化。
m_sem_flg=sem_flg;
// 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT)
// 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,
// 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。
// 信号量的初始化分三个步骤:
// 1)获取信号量,如果成功,函数返回。
// 2)如果失败,则创建信号量。
// 3) 设置信号量的初始值。
// 获取信号量。
if ( (m_semid=semget(key,1,0666)) == -1)
{
// 如果信号量不存在,创建它。
if (errno==ENOENT)
{
// 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
if ( (m_semid=semget(key,1,0666|IPC_CREAT|IPC_EXCL)) == -1)
{
if (errno==EEXIST) // 如果错误代码是信号量已存在,则再次获取信号量。
{
if ( (m_semid=semget(key,1,0666)) == -1)
{
perror("init 1 semget()"); return false;
}
return true;
}
else // 如果是其它错误,返回失败。
{
perror("init 2 semget()"); return false;
}
}
// 信号量创建成功后,还需要把它初始化成value。
union semun sem_union;
sem_union.val = value; // 设置信号量的初始值。
if (semctl(m_semid,0,SETVAL,sem_union) < 0)
{
perror("init semctl()"); return false;
}
}
else
{ perror("init 3 semget()"); return false; }
}
return true;
}
// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
if (m_semid==-1) return false;
struct sembuf sem_b;
sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
sem_b.sem_op = value; // P操作的value必须小于0。
sem_b.sem_flg = m_sem_flg;
if (semop(m_semid,&sem_b,1) == -1) { perror("p semop()"); return false; }
return true;
}
// 信号量的V操作(把信号量的值减value)。
bool csemp::post(short value)
{
if (m_semid==-1) return false;
struct sembuf sem_b;
sem_b.sem_num = 0; // 信号量编号,0代表第一个信号量。
sem_b.sem_op = value; // V操作的value必须大于0。
sem_b.sem_flg = m_sem_flg;
if (semop(m_semid,&sem_b,1) == -1) { perror("V semop()"); return false; }
return true;
}
// 获取信号量的值,成功返回信号量的值,失败返回-1。
int csemp::getvalue()
{
return semctl(m_semid,0,GETVAL);
}
// 销毁信号量。
bool csemp::destroy()
{
if (m_semid==-1) return false;
if (semctl(m_semid,0,IPC_RMID) == -1) { perror("destroy semctl()"); return false; }
return true;
}
csemp::~csemp()
{
}
demo3.cpp
// demo3.cpp,本程序演示用信号量给共享内存加锁。
#include "_public.h"
struct stgirl // 超女结构体。
{
int no; // 编号。
char name[51]; // 姓名,注意,不能用string。
};
int main(int argc,char *argv[])
{
if (argc!=3) { cout << "Using:./demo no name\n"; return -1; }
// 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
int shmid=shmget(0x5005, sizeof(stgirl), 0640|IPC_CREAT);
if ( shmid ==-1 )
{
cout << "shmget(0x5005) failed.\n"; return -1;
}
cout << "shmid=" << shmid << endl;
// 第2步:把共享内存连接到当前进程的地址空间。
stgirl *ptr=(stgirl *)shmat(shmid,0,0);
if ( ptr==(void *)-1 )
{
cout << "shmat() failed\n"; return -1;
}
// 创建、初始化二元信号量。
csemp mutex;
if (mutex.init(0x5005)==false)
{
cout << "mutex.init(0x5005) failed.\n"; return -1;
}
cout << "申请加锁...\n";
mutex.wait(); // 申请加锁。
cout << "申请加锁成功。\n";
// 第3步:使用共享内存,对共享内存进行读/写。
cout << "原值:no=" << ptr->no << ",name=" << ptr->name << endl; // 显示共享内存中的原值。
ptr->no=atoi(argv[1]); // 对超女结构体的no成员赋值。
strcpy(ptr->name,argv[2]); // 对超女结构体的name成员赋值。
cout << "新值:no=" << ptr->no << ",name=" << ptr->name << endl; // 显示共享内存中的当前值。
sleep(10);
mutex.post(); // 解锁。
cout << "解锁。\n";
// 查看信号量 :ipcs -s // 删除信号量 :ipcrm sem 信号量id
// 查看共享内存:ipcs -m // 删除共享内存:ipcrm -m 共享内存id
// 第4步:把共享内存从当前进程中分离。
shmdt(ptr);
// 第5步:删除共享内存。
if (shmctl(shmid,IPC_RMID,0)==-1)
{
cout << "shmctl failed\n"; return -1;
}
}