在多任务操作系统中,进程间同步和共享资源的管理是至关重要的。信号量(Semaphore)作为一种经典的同步机制,为控制对共享资源的访问提供了一种有效的解决方案,尤其是在避免竞争条件的发生时。


一、什么是信号量集?

信号量集(Semaphore Set) 是操作系统中的一种数据结构,它由多个信号量(semaphore)组成。每个信号量集可以包含一个或多个信号量,每个信号量独立用于控制不同的资源或管理不同进程的同步问题。信号量集为开发者提供了一种在同一操作中同时管理多个信号量的机制,简化了多资源同步的操作流程。

在 Unix 和类 Unix 系统中,信号量集通常用于进程间通信(IPC,Inter-Process Communication)中。与其他 IPC 机制相比,信号量适用于需要频繁同步或控制对共享资源访问的场景,如生产者-消费者模型或资源池管理。

信号量集在创建时,由操作系统分配一个唯一的标识符(ID),称为 m_semid。该 ID 是后续对信号量集进行所有操作的核心。类似于文件描述符标识一个文件,信号量集的 ID 唯一标识一个信号量集。通过这个唯一的 ID,进程可以对信号量集执行各种操作,如创建、初始化、P(等待)、V(信号)操作,以及删除信号量集。

拥有这个唯一 ID,多个进程可以同步地操作同一个信号量集,保证共享资源的正确使用和同步操作的有序执行。


二、信号量集的基本操作

信号量集的基本操作包括创建、初始化、P(等待)操作、V(信号)操作和删除信号量集。下面将依次介绍这些操作,并结合实际代码示例展示如何在 C++ 中使用信号量集。

2.1 创建信号量集

要创建一个信号量集,首先需要使用 semget 系统调用。这个函数返回一个唯一的信号量集 ID,用于后续的操作。

1
2
3
4
5
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • key:用于标识信号量集的键值。可以通过 ftok 函数生成,或直接使用整数值。
  • nsems:信号量集中的信号量个数。
  • semflg:标志位,指定创建方式。例如 IPC_CREAT 表示如果信号量集不存在则创建。

例如,创建一个包含 1 个信号量的信号量集:

1
2
3
4
5
6
key_t key = ftok("pathname", 'A'); // 生成键值
int semid = semget(key, 1, 0666 | IPC_CREAT); // 创建信号量集
if (semid == -1) {
    perror("semget failed");
    exit(1);
}

在此例中,semget 返回信号量集的唯一 ID semid,用于后续的操作。

2.2 初始化信号量

创建信号量集之后,通常需要初始化信号量的值。我们可以使用 semctl 系统调用来初始化信号量的值。

1
2
3
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
  • semid:信号量集的 ID。
  • semnum:信号量的编号(从 0 开始)。
  • cmd:执行的操作命令,如 SETVAL 设置信号量值。

例如,将信号量的值初始化为 1,表示资源是可用的:

1
2
3
4
if (semctl(semid, 0, SETVAL, 1) == -1) {
    perror("semctl SETVAL failed");
    exit(1);
}

2.3 P(等待)操作和 V(信号)操作

信号量的核心操作是 P 和 V 操作。P 操作会减少信号量的值,表示占用资源;V 操作增加信号量的值,表示释放资源。多个进程之间通过 P 和 V 操作实现对共享资源的同步访问。

1
2
3
4
5
struct sembuf {
    unsigned short sem_num; // 信号量编号
    short sem_op;           // 操作类型:P操作为-1,V操作为+1
    short sem_flg;          // 操作标志
};

P 操作通过将信号量的值减少 1 来尝试占用资源。如果信号量的值为 0,则进程阻塞,等待资源释放。

1
2
3
4
5
struct sembuf p = {0, -1, 0}; // 第一个信号量,P操作
if (semop(semid, &p, 1) == -1) {
    perror("P operation failed");
    exit(1);
}

V 操作通过将信号量的值增加 1 来释放资源,唤醒等待的进程。

1
2
3
4
5
struct sembuf v = {0, 1, 0}; // 第一个信号量,V操作
if (semop(semid, &v, 1) == -1) {
    perror("V operation failed");
    exit(1);
}

通过这种方式,我们可以控制多个进程对同一资源的并发访问。

2.4 删除信号量集

当信号量集不再需要时,可以使用 semctl 来删除它,释放系统资源。

1
2
3
4
if (semctl(semid, 0, IPC_RMID) == -1) {
    perror("semctl IPC_RMID failed");
    exit(1);
}

删除信号量集可以防止系统中遗留的信号量集占用资源。应确保在所有进程都不再使用该信号量集时再执行删除操作。


三、实际示例:生产者-消费者问题

生产者-消费者问题是经典的同步问题之一,下面的例子展示了如何使用信号量集解决这一问题。生产者将数据放入共享缓冲区,消费者从缓冲区读取数据,信号量用于确保缓冲区不会被同时读取或写入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;

void producer(int semid) {
    struct sembuf p = {0, -1, 0};
    struct sembuf v = {0, 1, 0};

    for (int i = 0; i < 20; ++i) {
        semop(semid, &p, 1); // 等待缓冲区有空位
        buffer[in] = i;
        printf("Produced: %d\n", i);
        in = (in + 1) % BUFFER_SIZE;
        semop(semid, &v, 1); // 通知消费者
        sleep(1);
    }
}

void consumer(int semid) {
    struct sembuf p = {0, -1, 0};
    struct sembuf v = {0, 1, 0};

    for (int i = 0; i < 20; ++i) {
        semop(semid, &p, 1); // 等待数据可用
        int item = buffer[out];
        printf("Consumed: %d\n", item);
        out = (out + 1) % BUFFER_SIZE;
        semop(semid, &v, 1); // 通知生产者
        sleep(2);
    }
}

int main() {
    key_t key = ftok("semfile", 'a');
    int semid = semget(key, 1, 0666 | IPC_CREAT);
    semctl(semid, 0, SETVAL, 1);

    if (fork() == 0) {
        producer(semid);
    } else {
        consumer(semid);
        wait(NULL);
        semctl(semid, 0, IPC_RMID); // 删除信号量集
    }

    return 0;
}

四、总结

信号量集是 Linux 系统中常用的同步机制之一,广泛应用于进程间通信和共享资源的管理。通过 semgetsemctlsemop 等系统调用,开发者可以灵活地创建、初始化、操作和删除信号量集。在实际开发中,信号量集能够确保多个进程对共享资源的有序访问,避免竞争条件和死锁等问题。