1. 为什么要发明信号量?
这种多进程争抢访问的共享资源(如共享内存、打印机),被称为 临界资源 (Critical Resource) 。访问这些资源的代码段,叫 临界区 (Critical Section)。
我们面临的问题是:原子性 (Atomicity)。
- 你在 C++ 里写
count++,汇编层面其实是 3 条指令(读入寄存器、加1、写回内存)。 - 如果进程 A 执行了一半被切走了,进程 B 来了,数据就会乱套。
信号量 就是为了解决这个问题而生的。它本质上是一个内核中的计数器 ,但它的增减操作是原子的(要么全做完,要么不做,不会被打断)。
2. 核心原理:PV 原语
这是荷兰计算机科学家 Dijkstra(迪杰斯特拉)提出的概念,是所有并发编程的基石。
假设我们需要一把"锁"(互斥量),信号量的初始值设为 1。
- P 操作 (Proberen, 测试/申请):
-
- 逻辑:
sem--(计数器减 1)。 - 判断:
- 逻辑:
-
-
- 如果减完后值 >= 0:申请资源成功,进程继续执行(拿到锁了)。
- 如果减完后值 < 0(或者减之前是0):资源没了,进程挂起阻塞,放入等待队列。
-
- V 操作 (Verhogen, 增加/释放):
-
- 逻辑:
sem++(计数器加 1)。 - 动作:
- 逻辑:
-
-
- 如果加完后值 <=0:说明等待队列里还有人,唤醒一个等待的进程。
- 进程继续执行。
-
3. 复杂的 System V 接口
Linux 的 System V 信号量设计得比较复杂(它设计的初衷是让你可以一次操作一组信号量),所以接口参数很多。我们要学会把复杂变简单。
我们要用的三个核心函数:
A. semget**------ 创建/获取**
int semget(key_t key, int nsems, int semflg);
key:和共享内存一样,用ftok生成。nsems:你要申请几个信号量? 通常我们只需要 1 个(作为互斥锁)。semflg:IPC_CREAT | 0666等。
B. semctl**------ 控制/初始化**
int semctl(int semid, int semnum, int cmd, ...);
semnum:操作第几个信号量?(下标从 0 开始)。cmd:
-
- SETVAL:设置信号量的初始值(比如设为 1)。
- IPC_RMID:删除信号量集。
C. semop**------ 核心 PV 操作**
int semop(int semid, struct sembuf *sops, size_t nsops);
这是最难用的函数,我们需要定义一个结构体:
struct sembuf {
unsigned short sem_num; // 操作第几个信号量 (0)
short sem_op; // -1 是 P操作,+1 是 V操作
short sem_flg; // 通常设为 0,或 SEM_UNDO
};
SEM_UNDO 是个很重要的标志:如果进程崩溃了没来得及释放锁,操作系统会自动帮你"撤销"之前的 P 操作,防止死锁。
4.信号量实现进程通信(代码)
common
cpp
#pragma once
#include <iostream>
#include <string>
#include<cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/sem.h>
// 生成key的路径和ID
const std::string PATH_NAME = ".";
const int PROJ_ID = 6666;
// 设置共享内存的大小
const int MEM_SIZE = 4096;
// 用于同步的命名管道
const std::string FIFO_NAME = "./my_pipe";
// 获取唯一的key
key_t GetKey()
{
key_t key = ftok(PATH_NAME.c_str(), PROJ_ID);
if (key < 0)
{
std::cout << "创建共享key获取失败" << std::endl;
exit(1);
}
return key;
}
//信号量创建
int CreateSem(int nsems)
{
int key=GetKey();
int sem=semget(key,nsems,IPC_CREAT|0666);
if(sem<0)
{
std::cout<<"信号量创建失败"<<std::endl;
exit(1);
}
return sem;
}
//信号量获取
int GetSem()
{
int key=GetKey();
int sem=semget(key,0,0);
if(sem<0)
{
std::cout<<"信号量获取失败"<<std::endl;
exit(1);
}
return sem;
}
//信号量初始化
void InitSem(int semid, int which, int value)
{
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
union semun se;
se.val=value;
semctl(semid,which,SETVAL,se);
}
//申请资源
void P(int semid, int which)
{
//int semop(int semid, struct sembuf *sops, size_t nsops);
sembuf se;
se.sem_num=which;
se.sem_op=-1;
se.sem_flg=0;
semop(semid,&se,1);
}
//释放资源
void V(int semid, int which)
{
//int semop(int semid, struct sembuf *sops, size_t nsops);
sembuf se;
se.sem_num=which;
se.sem_op=1;
se.sem_flg=0;
semop(semid,&se,1);
}
//信号量删除
void DleSem(int semid)
{
//int semop(int semid, struct sembuf *sops, size_t nsops);
semctl(semid,0,IPC_RMID);
}
server.cc
cpp
#include "common.hpp"
// 创建共享内存,读取数据
class Init
{
public:
Init()
{
// 共享内存
key_t k = GetKey();
std::cout << "server key: " << std::hex << k << std::dec << std::endl;
// 不存在就创建,存在就报错
_shmid = shmget(k, MEM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid < 0)
{
perror("创建共享内存失败");
exit(2);
}
std::cout << "创建共享内存成功:" << _shmid << std::endl;
// 挂载
_start = (char *)shmat(_shmid, nullptr, 0);
if (_start == (void *)(-1))
{
perror("挂载失败");
exit(3);
}
// 创建信号量 (申请1个)
_semid = CreateSem(1);
if (_semid < 0)
{
perror("信号量创建失败");
exit(4);
}
// 3. 初始化信号量为 0
// 含义:目前没有资源(数据),消费者必须等
InitSem(_semid, 0, 0);
}
~Init()
{
// 去关联
shmdt(_start);
// 删除共享内存
shmctl(_shmid, IPC_RMID, nullptr);
// 删除信号量
DleSem(_semid);
std::cout << "资源清理完毕...." << std::endl;
}
public:
int _shmid;
int _semid;
char *_start;
};
int main()
{
Init init;
std::cout << "server ready...." << std::endl;
while (true)
{
P(init._semid, 0);
// 收到信号
std::cout << "客户端说:" << init._start << std::endl;
if (strcmp(init._start, "quit") == 0)
{
break;
}
}
return 0;
}
client.cc
cpp
#include"common.hpp"
int main()
{
//获取key
key_t k=GetKey();
//获取共享内存ID
int shmid=shmget(k,MEM_SIZE,IPC_CREAT);
if(shmid<0)
{
perror("共享内存获取失败");
return 1;
}
//挂接
char* start=(char*)shmat(shmid,nullptr,0);
if(start==(char*)(-1))
{
perror("共享内存挂接失败");
return 2;
}
int semid = GetSem();
//开始写入数据
while(true)
{
std::cout<<">";
std::string buffer;
std::getline(std::cin,buffer);
//直接把数据写到共享内存
snprintf(start,MEM_SIZE,"%s",buffer.c_str());
//通知server
V(semid,0);
if(buffer=="quit")
{
break;
}
}
//去关联,但是不需要删除共享内存
shmdt(start);
return 0;
}