Skip to main content

POSIX信号量

1. 基本概念

POSIX 信号量是一种用于进程间通信(IPC)和同步的机制。它是指遵循 POSIX 标准的信号量,允许多个进程或线程同时访问共享资源,并通过控制对该资源的访问来协调它们之间的活动。

POSIX 信号量有两个主要操作: P 操作(也称为 wait 操作)和 V 操作(也称为 signal 操作)。P 操作会在信号量值为正数时将其减 1,否则会阻塞进程。相反,V 操作会将信号量值加 1,并唤醒阻塞在该信号量上的任何进程。

使用 POSIX 信号量时,需要注意避免死锁,这种情况通常是由于多个进程或线程互相等待对方释放某个资源所导致的。此外,也需要设置超时,以防止进程长时间阻塞。

POSIX 信号量分为两种:POSIX 有名信号量和POSIX 无名信号量。

2. 有名信号量

POSIX 有名信号量是一种在多个进程间共享的信号量。它通过一个名称来标识,因此多个进程可以通过该名称访问同一个信号量。这种信号量的优点在于它可以在不同的进程间共享,因此可以用来协调多个进程对共享资源的访问。

POSIX 有名信号量使用步骤:

  1. 通过函数 sem_open() 打开一个信号量。
  2. 通过函数 sem_wait()sem_post() 对信号量进行 P 操作和 V 操作。
  3. 通过函数 sem_close() 关闭信号量。
  4. 通过 sem_unlink() 函数来删除信号量,以释放系统资源。

3. 有名信号量API

3.1 打开或创建有名信号量

sem_open() 是 Linux 中用于打开一个有名信号量的函数。该函数原型为:

sem_t *sem_open(const char *name, int oflag);  									// 打开POSIX有名信号量
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); // 可创建或打开POSIX有名信号量

该函数需要四个参数:

  • name:指向信号量名称的指针。该名称可以是一个以斜杠(/)开头的字符串,或者是以字符点(.)开头的本地名称。
  • oflag:指定打开信号量的方式。可以指定 O_CREAT 标志来创建一个新的信号量,或者指定 O_EXCL 标志来确保信号量不存在。
  • mode:指定信号量的权限。可以使用相关的宏来指定读写权限。
  • value:指定信号量的初始值。

该函数返回一个指向打开的信号量的指针。如果打开失败,则返回一个空指针。例如,可以使用下面的代码来打开一个新的信号量:

sem_t *sem = sem_open("/my_sem", O_CREAT | O_EXCL, 0666, 1);
if (sem == SEM_FAILED) {
if (errno == EEXIST) {
// 同名的信号量已经存在,打开已存在的信号量
sem = sem_open("/my_sem", O_RDWR);
if (sem == SEM_FAILED) {
// 打开失败,输出错误信息并退出
perror("sem_open() fail");
exit(EXIT_FAILURE);
}
} else {
// 打开失败,输出错误信息并退出
perror("sem_open");
exit(EXIT_FAILURE);
}
}

在上面的代码中,首先通过指定 O_CREATO_EXCL 标志来尝试创建一个新的信号量。如果 sem_open() 函数返回 SEM_FAILED 并且 errnoEEXIST,表示存在同名的信号量。在这种情况下,可以再次调用 sem_open() 函数,并只指定 O_RDWR 标志来打开已经存在的信号量。如果打开失败,则输出错误信息并终止程序的执行。

3.2 PV操作

PV操作函数原型为:

int sem_wait(sem_t *sem);	// P操作
int sem_post(sem_t *sem); // V操作

sem_wait() 函数和 sem_post() 函数分别用于实现 P 操作和 V 操作。

  • sem_wait() 函数:该函数用于实现 P 操作,即对信号量进行减一操作。如果信号量的值为 0,则会被阻塞,直到信号量的值大于 0 时才会继续执行。
  • sem_post() 函数:该函数用于实现 V 操作,即对信号量进行加一操作。如果当前有线程因为信号量的值为 0 而被阻塞,则会唤醒一个被阻塞的线程继续执行。
  • sem:它们的参数都是一个指向信号量名称的指针。

3.3 关闭信号量

sem_close() 函数用于关闭信号量,该函数原型为:

int sem_close(sem_t *sem);	// 关闭信号量

该函数需要一个参数:

  • sem:指向要关闭的信号量的指针。

如果关闭成功,则该函数返回 0。否则,返回一个非 0 值。

注意:使用 sem_close() 函数关闭信号量后,信号量并不会被立即删除,只是断开了对信号量的引用。

3.4 删除信号量

sem_unlink() 函数用于删除信号量。函数原型为:

int sem_unlink(const char *name);	// 删除信号量

该函数需要一个参数:

  • name:指向信号量名字的指针。

如果删除成功,则该函数返回 0。否则,返回一个非 0 值。

注意:只有当信号量的所有引用都被关闭后,信号量才会执行删除操作。

3.5 示例代码

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
sem_t *sem = sem_open("/my_sem", O_CREAT | O_EXCL, 0666, 1);
if (sem == SEM_FAILED) {
if (errno == EEXIST) {
// 同名的信号量已经存在,打开已存在的信号量
sem = sem_open("/my_sem", O_RDWR);
if (sem == SEM_FAILED){
// 打开失败,输出错误信息并退出
perror("sem_open() fail");
exit(EXIT_FAILURE);
}
} else {
// 打开失败,输出错误信息并退出
perror("sem_open() fail");
exit(EXIT_FAILURE);
}
}

// P操作
sem_wait(sem);
// 临界区代码
printf("critical section\n");
// V操作
sem_post(sem);

// 关闭信号量
int ret = sem_close(sem);
if (ret == -1) {
perror("sem_close() fail");
exit(EXIT_FAILURE);
}

// 删除信号量
ret = sem_unlink("/my_sem");
if (ret == -1) {
perror("sem_unlink() fail");
exit(EXIT_FAILURE);
}

return 0;
}

4. 无名信号量

POSIX 无名信号量是一种临时信号量,它只能在同一进程的不同线程之间共享。无名信号量无需通过文件系统的路径名来访问,因为它们是通过内存地址的方式进行访问的。

无名信号量与有名信号量相比,有以下几个主要特点:

  • 只能在同一进程的不同线程之间共享,不能在多个进程之间共享。
  • 无需通过文件系统的路径名来访问,而是通过内存地址的方式进行访问。
  • 在进程结束时会自动销毁,不需要手动调用销毁函数。

无名信号量的主要优点在于其使用方便,可以避免文件系统的路径名的限制,灵活度更高。但是,由于它只能在同一进程的不同线程之间共享,所以不能用于解决多进程之间的同步问题。

使用无名信号量的步骤如下:

  1. 使用 sem_init() 函数创建无名信号量。
  2. 在需要进行同步操作时,调用 sem_wait()sem_trywait() 函数进行 P 操作。
  3. 在需要释放同步资源时,调用 sem_post() 函数进行 V 操作。
  4. 在进程结束时,无需手动销毁无名信号量,它会在进程结束时自动销毁。

5. 无名信号量API

5.1 创建无名信号量

在 Linux 系统中,可以使用 sem_init() 函数来创建一个 POSIX 无名信号量。该函数的原型为:

int sem_init(sem_t *sem, int pshared, unsigned int value);

该函数需要三个参数:

  • sem:指向要创建的无名信号量的指针。
  • pshared:一个整数值,用来指定信号量的范围。如果该值为 0,则表示信号量仅能在同一进程的不同线程之间共享;如果该值为非 0,则表示信号量可以在多个进程之间共享。
  • value:一个无符号整数值,表示信号量的初始值。

如果创建成功,则该函数返回 0。否则,返回一个非 0 值。

例如,以下是使用 sem_init 创建信号量的示例代码,用于演示无名信号量的范围:

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

int main(int argc, char *argv[])
{
// 声明无名信号量
sem_t sem;

// 创建无名信号量,并初始化为 1
int ret = sem_init(&sem, 0, 1);
if (ret != 0){
perror("sem_init() fail");
exit(EXIT_FAILURE);
}

// 检查无名信号量的范围
if (sem.__align == 0){
printf("semaphore is local to process\n");
}else{
printf("semaphore is shared between processes\n");
}

return 0;
}

注意:如果无名信号量的 __align 成员为 0,则表示该信号量仅能在同一进程的不同线程之间共享;如果该成员为非 0,则表示该信号量可以在多个进程之间共享。

5.2 P 操作

sem_wait() 是一个用于信号量的 P 操作的函数。它会获取信号量并将信号量的值减 1。该函数的原型为:

int sem_wait(sem_t *sem);

该函数需要一个参数:

  • sem:指向要操作的信号量的指针。

如果 P 操作成功,则该函数返回 0。否则,返回一个非 0 值。

另一个与 P 操作类似的操作是 sem_trywait,该函数会尝试获取信号量,如果信号量被占用,则不会阻塞,而是立即返回。该函数的原型为:

int sem_trywait(sem_t *sem);

该函数需要一个参数:

  • sem:指向要操作的信号量的指针。

如果 sem_trywait 操作成功,则该函数返回 0。如果信号量被占用,则会立即返回一个非 0 值。

例如,以下是使用 sem_wait() 和 sem_trywait() 函数对信号量进行 P 操作的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

int main(int argc, char *argv[])
{
// 声明无名信号量
sem_t sem;

// 创建无名信号量,并初始化为 1
int ret = sem_init(&sem, 0, 1);
if (ret != 0){
perror("sem_init() fail");
exit(EXIT_FAILURE);
}

// 调用 sem_wait() 函数对信号量进行 P 操作
ret = sem_wait(&sem);
if (ret != 0){
perror("sem_wait() fail");
exit(EXIT_FAILURE);
}

printf("sem_wait() success\n");

// 调用 sem_trywait() 函数对信号量进行 P 操作
ret = sem_trywait(&sem);
if (ret != 0){
perror("sem_trywait() fail");
exit(EXIT_FAILURE);
}

printf("sem_trywait() success\n");

return 0;
}

在上面的代码中,我们先创建了一个无名信号量,并初始化为 1。然后,我们调用 sem_wait() 和 sem_trywait() 函数对信号量进行 P操作。

在第一次调用 sem_wait() 时,由于信号量的值为 1,所以 P 操作会成功,线程不会阻塞,并输出 "sem_wait() success"。

在第二次调用 sem_trywait() 时,由于信号量的值已经为 0,所以 sem_trywait() 不会阻塞线程,直接返回错误。因此,该函数会输出 "sem_trywait() fail"。

5.3 V 操作

sem_post() 是一个用于信号量的 V 操作的函数。它会将信号量的值加 1,并唤醒正在等待信号量的线程。该函数的原型为:

int sem_post(sem_t *sem);

该函数需要一个参数:

  • sem:指向要操作的信号量的指针。

如果 sem_post 操作成功,则该函数返回 0。否则,返回一个非 0 值。

例如,以下是使用 sem_post() 函数对信号量进行 V 操作的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

int main(int argc, char *argv[])
{
// 声明无名信号量
sem_t sem;

// 创建无名信号量,并初始化为 1

int ret = sem_init(&sem, 0, 1);
if (ret != 0){
perror("sem_init() fail");
exit(EXIT_FAILURE);
}

// 调用 sem_post() 函数对信号量进行 V 操作
ret = sem_post(&sem);
if (ret != 0){
perror("sem_post() fail");
exit(EXIT_FAILURE);
}

printf("sem_post() success\n");

return 0;
}

在上面的代码中,我们先创建了一个无名信号量,并初始化为 1。然后,我们调用 sem_post() 函数对信号量进行 V 操作,将信号量的值加 1。

注意:使用 sem_post() 函数进行 V 操作时,要确保信号量的值不会超过它的初始值。否则,会导致信号量值超出范围,导致程序错误。

5.4 销毁无名信号量

在 Linux 系统中,可以使用 sem_destroy() 函数来销毁一个 POSIX 无名信号量。该函数的原型为:

int sem_destroy(sem_t *sem);

该函数需要一个参数:

  • sem:指向要销毁的无名信号量的指针。

如果销毁成功,则该函数返回 0。否则,返回一个非 0 值。

注意:由于无名信号量的生命周期与进程相同,在进程结束时会自动销毁,不需要手动调用销毁函数。

5.5 示例代码

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

int main(int argc, char *argv[])
{
// 声明无名信号量
sem_t sem;

// 创建无名信号量,并初始化为 1
int ret = sem_init(&sem, 0, 1);
if (ret != 0){
perror("sem_init() fail");
exit(EXIT_FAILURE);
}

// 调用 sem_wait() 函数对信号量进行 P 操作
ret = sem_wait(&sem);
if (ret != 0){
perror("sem_wait() fail");
exit(EXIT_FAILURE);
}

printf("sem_wait() success\n");

// 调用 sem_post() 函数对信号量进行 V 操作
ret = sem_post(&sem);
if (ret != 0){
perror("sem_post() fail");
exit(EXIT_FAILURE);
}

printf("sem_post() success\n");

return 0;
}

在上面的代码中,我们首先声明了一个无名信号量,然后使用 sem_init() 函数创建了一个无名信号量,并将其初始化为 1。接下来,我们调用 sem_wait() 函数对信号量进行 P 操作,将信号量的值减 1。最后,我们调用 sem_post() 函数对信号量进行 V 操作,将信号量的值加 1。

该示例代码只是用来演示信号量的基本操作,实际应用中,信号量通常用于控制多个线程的同步和互斥。例如,我们可以使用信号量来控制多个线程对共享资源的访问,以避免资源竞争的问题。

5.6 多个线程示例代码

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>

// 全局变量,表示信号量的值
int value = 0;

// 声明信号量
sem_t sem;

// 线程1入口函数
void *thread1_func(void *arg)
{
// 使用 sem_wait() 函数对信号量进行 P 操作
sem_wait(&sem);

// 访问共享资源
value++;
printf("thread1: value = %d\n", value);

// 使用 sem_post() 函数对信号量进行 V 操作
sem_post(&sem);

pthread_exit(NULL);
}

// 线程2入口函数
void *thread2_func(void *arg)
{
// 使用 sem_wait() 函数对信号量进行 P 操作
sem_wait(&sem);

// 访问共享资源
value++;
printf("thread2: value = %d\n", value);

// 使用 sem_post() 函数对信号量进行 V 操作
sem_post(&sem);

pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
// 创建无名信号量,并初始化为 1
int ret = sem_init(&sem, 0, 1);
if (ret != 0){
perror("sem_init() fail");
exit(EXIT_FAILURE);
}

// 声明两个线程
pthread_t thread1, thread2;

// 创建线程1
ret = pthread_create(&thread1, NULL, thread1_func, NULL);
if (ret != 0){
perror("pthread_create() fail");
exit(EXIT_FAILURE);
}

// 创建线程2
ret = pthread_create(&thread2, NULL, thread2_func, NULL);
if (ret != 0){
perror("pthread_create() fail");
exit(EXIT_FAILURE);
}

// 等待线程1结束
ret = pthread_join(thread1, NULL);
if (ret != 0){
perror("pthread_join() fail");
exit(EXIT_FAILURE);
}

// 等待线程2结束
ret = pthread_join(thread2, NULL);
if (ret != 0){
perror("pthread_join() fail");
exit(EXIT_FAILURE);
}

pthread_exit(NULL);
}

在上面的代码中,我们创建了一个无名信号量,并在两个线程的入口函数中分别调用 sem_wait() 和 sem_post() 函数对信号量进行 P 和 V 操作,从而保证了对共享资源的互斥访问。