POSIX信号量
1. 基本概念
POSIX 信号量是一种用于进程间通信(IPC)和同步的机制。它是指遵循 POSIX 标准的信号量,允许多个进程或线程同时访问共享资源,并通过控制对该资源的访问来协调它们之间的活动。
POSIX 信号量有两个主要操作: P 操作(也称为 wait 操作)和 V 操作(也称为 signal 操作)。P 操作会在信号量值为正数时将其减 1,否则会阻塞进程。相反,V 操作会将信号量值加 1,并唤醒阻塞在该信号量上的任何进程。
使用 POSIX 信号量时,需要注意避免死锁,这种情况通常是由于多个进程或线程互相等待 对方释放某个资源所导致的。此外,也需要设置超时,以防止进程长时间阻塞。
POSIX 信号量分为两种:POSIX 有名信号量和POSIX 无名信号量。
2. 有名信号量
POSIX 有名信号量是一种在多个进程间共享的信号量。它通过一个名称来标识,因此多个进程可以通过该名称访问同一个信号量。这种信号量的优点在于它可以在不同的进程间共享,因此可以用来协调多个进程对共享资源的访问。
POSIX 有名信号量使用步骤:
- 通过函数
sem_open()
打开一个信号量。 - 通过函数
sem_wait()
和sem_post()
对信号量进行 P 操作和 V 操作。 - 通过函数
sem_close()
关闭信号量。 - 通过
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_CREAT
和 O_EXCL
标志来尝试创建一个新的信号量。如果 sem_open()
函数返回 SEM_FAILED
并且 errno
为 EEXIST
,表示存在同名的信号量。在这种情况下,可以再次调用 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 无名信号量是一种临时信号量,它只能在同一进程的不同线程之间共享。无名信号量无需通过文件系统的路径名来访问,因为它们是通过内存地址的方式进行访问的。
无名信号量与有名信号量相比,有以下几个主要特点:
- 只能在同一进程的不同线程之间共享,不能在多个进程之间共享。
- 无需通过文件系统的路径名来访问,而是通过内存地址的方式进行访问。
- 在进程结束时会自动销毁,不需要手动调用销毁函数。
无名信号量的主要优点在于其使用方便,可以避免文件系统的路径名的限制,灵活度更高。但是,由于它只能在同一进程的不同线程之间共享,所以不能用于解决多进程之间的同步问题。
使用无名信号量的步骤如下:
- 使用
sem_init()
函数创建无名信号量。 - 在需要进行同步操作时,调用
sem_wait()
或sem_trywait()
函数进行 P 操作。 - 在需要释放同步资源时,调用
sem_post()
函数进行 V 操作。 - 在进程结 束时,无需手动销毁无名信号量,它会在进程结束时自动销毁。
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 操作,从而保证了对共享资源的互斥访问。