信号量机制
1、信号量机制的基本操作
信号量是一种用于线程或进程间同步的基本机制,它可以控制对临界资源的访问。常见的信号量操作包括以下几个基本操作:
-
创建信号量:通过系统调用或者库函数创建一个信号量。在创建时需要指定信号量的初始值,即可用资源的数量。
-
P(wait)操作:也称为申请资源操作。该操作会尝试获取一个可用资源,如果没有可用资源,则线程或进程会被阻塞,直到有可用资源时才能继续执行。
-
V(signal)操作:也称为释放资源操作。该操作会释放一个已占用的资源,并唤醒可能因为等待资源而被阻塞的线程或进程。
-
销毁信号量:通过系统调用或者库函数销毁一个信号量,在销毁前需要确保所有线程或进程都不再使用该信号量。
这些操作可以用来实现各种同步机制,例如互斥锁、条件变量等。在使用信号量时,需要注意避免死锁和竞态条件等并发问题,正确地使用P、V操作来保证线程或进程的正常执行。
2、C语言实现
A)创建信号量集
int semget(key_t key,int nsems,int semflg)
其中:key—创建信号量集关键字。
nsems—信号量集中信号量的数量。
semflg—指定选项及其权限位。
IPC_CREAT—创建新的信号量集
IPC_EXCEL—如果信号量集已经存在,则返回错误。
<XXX XXX XXX>--和文件、目录一样权限。
返回一个信号量集ID--semid
B) 获得一个已经存在的信号量集
int semget(key_t key,0,0)
key—含义同上。
C)等待、通知一个信号量集
int semop(int semid,struct sembuf *sops,unsigned nsops)
semid—由函数semget()产生的。
sops—描述信号量的操作:等待、通知等
struct sembuf{
short sem_num;/*semaphore number */
short sem_op; /*semaphore operation:-1 for waiting*/
short sem_flg; /* operation flag :0、IPC_NOWAIT */
}
nsops—指定信号量集中操作的信号量个数。
D) 控制信号量集的操作
int semctl(int semid,int semnum,int cmd,union semun arg)
semid—由semget()创建的信号量集标识。
semnum—信号量数量。
cmd—对信号量semid所进行的操作:SETALL—所有参数。
arg—对信号量集操作的初始数据。
arg的类型:
union semun{
int val; /* 仅用于参数SETVAL*/
struct semid_ds *buf; /*指向IPC_STAT和IPC_SET的semid_ds结构*/
ushort *array; /*用于GETALL和SETALL:指向一个初值的数*/
};
union semun arg;
生产者与消费者问题
生产者和消费者问题是一个经典的同步问题,用于描述多个线程(生产者和消费者)之间共享有限缓冲区的情况。生产者负责生产数据并放入缓冲区,消费者负责从缓冲区中取出数据进行消费。问题的目标是实现生产者和消费者之间的协调,确保生产者不会向满的缓冲区添加数据,消费者不会从空的缓冲区中取出数据。
1.问题描述
问题的描述如下:
-
缓冲区:有一个有限大小的缓冲区,可以存放一定数量的数据。缓冲区可以被生产者放入数据,也可以被消费者取出数据。
-
生产者:负责生成数据,并将其放入缓冲区。如果缓冲区已满,则生产者需要等待直到有空闲位置。
-
消费者:负责从缓冲区中取出数据进行消费。如果缓冲区为空,则消费者需要等待直到有数据可用。
-
同步:生产者和消费者之间需要进行同步,避免竞态条件和死锁等并发问题。
2.生产者消费者流程
解决生产者和消费者问题的方法有多种,常见的包括使用信号量、互斥锁和条件变量等同步机制。以下是其中一种基本的解决方案:
-
定义一个互斥锁(mutex):用于保护对缓冲区的访问,确保同时只有一个线程访问缓冲区。
-
定义两个信号量:一个表示空闲位置的信号量(empty),初始值为缓冲区的大小;另一个表示可用数据的信号量(full),初始值为0。
-
生产者流程:
- 等待空闲位置:如果empty为0,则等待直到有空闲位置。
- 获取互斥锁:保护对缓冲区的访问。
- 将数据放入缓冲区。
- 释放互斥锁。
- 增加可用数据的信号量full。
- 如果还有更多数据要生产,返回步骤1;否则结束。
-
消费者流程:
- 等待可用数据:如果full为0,则等待直到有可用数据。
- 获取互斥锁:保护对缓冲区的访问。
- 从缓冲区取出数据进行消费。
- 释放互斥锁。
- 减少可用数据的信号量full。
- 如果还有更多数据要消费,返回步骤3;否则结束。
3.利用AND信号量解决生产者-消费者问题
int in=0,out=0;
item buffer[n];
semaphore mutex=1,empty=n,full=0;
void proceducer() {
do {
producer an item nextp;
…
Swait(empty,mutex);buffer[in]= nextp;
in = (in+1) % n;
Ssignal(mutex,full);
}while(TRUE);
}
void proceducer() {
do {
Swait(full,mutex);
nextc=buffer[out];
out= (out+1) % n;
Ssignal(mutex,empth);
comsumer the item in nextc;
…
}while(TRUE); }
5.利用管程解决生产者-消费者问题
Monitor producerconsumer {
item buffer[N];
int in,out;
condition notfull,notempty;
int count;
public:
void put(item x) {
if (count>=N) cwait(notfull);
buffer[in] = x;
in = (in+1) % N;
count++;
csignal(notempty);
}
void get(item x) {
if (count<=0) cwait(notempty);
x = buffer[out];
out = (out+1) % N;
count--;
csignal(notfull);
}
{
in=0;out=0;count=0;
}
}PC;
void producer() {
item x;
while(TRUE) {
……
produce an item in nextp;
PC.put(x);
}
}
void comsumer() {
item x;
while(TRUE) {
PC.get(x);
consume the item in nextc;
……
}
}
void main() {
cobegin
proceducer(); consumer();
coend
}
具体实现
分别创建6个生产者及消费者进程,使用信号量机制实现生产者及消费者进程间的同步及互斥。每个生产者进程随机睡眠0~9秒模拟生产数据的过程,然后把自己的进程号写入共享存储区,每个消费者进程从中读取数据并输出并且同步输出自己的进程号。要求程序结果可完整演示生产者及消费者同步生产数据及接受数据的全过程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <signal.h>
#define SHM_SIZE 1024
#define NUM_PRODUCERS 6
#define NUM_CONSUMERS 6
static int shmid;
static int semid;
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
void handle_signal(int signal) {
if (shmid > 0) {
shmctl(shmid, IPC_RMID, 0);
}
if (semid > 0) {
semctl(semid, 0, IPC_RMID, 0);
}
exit(1);
}
int main() {
// 创建共享内存
key_t key = ftok("/tmp", 'z');
if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) < 0) {
perror("shmget");
exit(1);
}
// 连接共享内存
int *shared_data = shmat(shmid, NULL, 0);
if (shared_data == (int *) -1) {
perror("shmat");
exit(1);
}
// 创建信号量
if ((semid = semget(key, 1, IPC_CREAT | 0666)) == -1) {
perror("semget");
exit(1);
}
union semun su;
su.val = 1;
if (semctl(semid, 0, SETVAL, su) == -1) {
perror("semctl");
exit(1);
}
// 注册信号处理函数
signal(SIGINT, handle_signal);
// 创建生产者进程
for (int i = 0; i < NUM_PRODUCERS; i++) {
if (fork() == 0) {
srand(getpid());
while (1) {
sleep(rand() % 10); // 随机睡眠0~9秒
struct sembuf sb = {0, -1, 0}; // P操作
if (semop(semid, &sb, 1) != -1) {
// 生产数据
*shared_data = getpid();
printf("Producer %d produced data: %d\n", getpid(), *shared_data);
struct sembuf sb = {0, 1, 0}; // V操作
semop(semid, &sb, 1);
}
}
}
}
// 创建消费者进程
for (int i = 0; i < NUM_CONSUMERS; i++) {
if (fork() == 0) {
while (1) {
struct sembuf sb = {0, -1, 0}; // P操作
if (semop(semid, &sb, 1) != -1) {
// 消费数据
printf("Consumer %d consumed data: %d\n", getpid(), *shared_data);
struct sembuf sb = {0, 1, 0}; // V操作
semop(semid, &sb, 1);
}
}
}
}
while (1) {
pause(); // 等待信号
}
return 0;
}
总结
基本的生产者和消费者问题的解决方案包括使用互斥锁和条件变量或信号量的组合。生产者在向缓冲区添加数据之前需要获得互斥锁,然后检查是否有空闲位置,如果没有则等待条件变量或信号量。当有空闲位置时,生产者将数据放入缓冲区,并通过条件变量或信号量通知消费者。类似地,消费者在从缓冲区获取数据之前需要获得互斥锁,并检查是否有可用数据,如果没有则等待条件变量或信号量。当有可用数据时,消费者从缓冲区取出数据,并通过条件变量或信号量通知生产者。