往期地址:
- 操作系统系列一 —— 操作系统概述
- 操作系统系列二 —— 进程
- 操作系统系列三 —— 编译与链接关系
- 操作系统系列四 —— 栈与函数调用关系
- 操作系统系列五 —— 目标文件详解
- 操作系统系列六 —— 详细解释【静态链接】
- 操作系统系列七 —— 装载
- 操作系统系列八 ——动态链接
- 操作系统系列九 ——系统调用和API
- 从编译角度看c和c++混合编译
- stripped文件描述以及gdb反汇编工具使用
本期主题:
- 操作系统的信号量简单讲解以及例子
- 踩坑分析
目录
- 1. 操作系统的信号量是什么?
- 2. 规范操作方式
- 3. 踩坑分析
1. 操作系统的信号量是什么?
可以看看我原来写的这篇文章—— UNIX环境编程——信号量与互斥量对比 ,不仅对操作系统的信号量进行了讲解,还对比了信号量和互斥量。这里可以再简单总结一下:
- 信号量的作用
在操作系统中,信号量是一种用于控制
对共享资源访问的同步机制
。它通常用于解决多个进程或线程之间的竞争条件,以及实现互斥访问和同步操作。
信号量具有一个整数值,表示可用资源的数量或者某种状态。根据信号量的值,进程或线程可以执行不同的操作,包括等待(阻塞)、唤醒、以及修改信号量的值等。
- 信号量类型
常见的信号量有两种类型:二进制信号量和计数信号量。
- 二进制信号量: 也称为互斥锁,它的取值范围只能是 0 或 1。它用于实现互斥访问,只允许一个进程或线程访问资源,其他进程或线程必须等待。
- 计数信号量: 它的取值范围可以是任意非负整数。它用于表示可用资源的数量,例如缓冲区中剩余的可用空间数量,或者允许的并发访问的最大数量。
- 信号量的操作
信号量通常提供以下两种操作:
- P(wait)操作: 用于尝试获取资源。如果资源可用,则执行成功并将信号量的值减一;否则进入等待状态,直到资源可用。
- V(post)操作: 用于释放资源。执行成功后,将信号量的值加一,表示释放了一个资源。
信号量的正确使用可以确保对共享资源的安全访问,并解决进程间的竞争条件。因此,信号量是操作系统中重要的同步机制之一。
2. 规范操作方式
信号量的规范操作方式包括以下几个步骤:
-
初始化信号量: 在使用信号量之前,首先需要对其进行初始化。初始化操作包括设置信号量的初始值,通常使用 sem_init() 函数来完成。在 POSIX 标准中,通常将信号量初始化为 1(二进制信号量)或者一个正整数(计数信号量)。
-
等待操作(P 操作): 在需要访问共享资源之前,先执行等待操作(也称为 P 操作)。等待操作会尝试获取资源,如果资源不可用,则阻塞当前进程或线程,直到资源可用。等待操作通常使用 sem_wait() 函数来完成。
-
访问共享资源: 在成功执行等待操作后,进程或线程可以安全地访问共享资源,进行读取、写入或其他操作。
-
释放操作(V 操作): 在完成对共享资源的访问后,需要执行释放操作(也称为 V 操作)。释放操作会增加信号量的计数,表示释放了一个资源。释放操作通常使用 sem_post() 函数来完成。
-
销毁信号量: 在不再需要使用信号量时,需要对其进行销毁以释放资源。销毁操作通常使用 sem_destroy() 函数来完成。
看一个示例代码,代码实现的功能是:
- 创建一个post进程和wait进程
- post进程负责把一个全局变量cnt++,并释放信号量,这里的全局变量相当于一个共享区域
- wait进程负责等待信号 ,并打印信号量
- 预期是能看到post打印一个,wait打印一个
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define TEST_TIME 10
int cnt = 0;
sem_t semaphore;
// Post 线程函数
void *post_thread(void *arg) {
int test_cnt = 0;
do {
cnt++;
sem_post(&semaphore); // 发送信号量
printf("Send cnt value: %d\n", cnt);
sleep(1);
test_cnt++;
} while(test_cnt < TEST_TIME);
return NULL;
}
// Wait 线程函数
void *wait_thread(void *arg) {
int test_cnt = 0;
do {
sem_wait(&semaphore); // 等待信号量
printf("Received cnt value: %d\n", cnt); // 打印接收到的值
test_cnt++;
// usleep(1500000);
sleep(1);
} while (test_cnt < TEST_TIME);
return NULL;
}
int main(void) {
pthread_t post_tid, wait_tid; // 定义线程 ID
int value_to_post = 42; // 要 post 的值
// 初始化信号量,第二个参数 0 表示初始计数为 0
sem_init(&semaphore, 0, 0);
// 创建 post 线程,并传递 value_to_post
if (pthread_create(&post_tid, NULL, post_thread, NULL) != 0) {
perror("pthread_create");
return EXIT_FAILURE;
}
// 创建 wait 线程
if (pthread_create(&wait_tid, NULL, wait_thread, NULL) != 0) {
perror("pthread_create");
return EXIT_FAILURE;
}
// 等待线程结束
pthread_join(post_tid, NULL);
pthread_join(wait_tid, NULL);
// 销毁信号量
sem_destroy(&semaphore);
return EXIT_SUCCESS;
}
测试结果符合预期:
3. 踩坑分析
在工作中遇到过一种情况,等待信号量的进程执行时间过长了,导致下一次post信号的时候,并不是处于wait状态,这样就会导致程序逻辑错误,例如我们把上面代码中的wait进程,sleep时间从1s增加到1.5s,那么就会有下面这种情况,post进程的值和wait进程的值并不能对应上:
总结:
-
在常规的信号量使用中,释放信号量的操作通常应该在至少有一个等待信号量的进程执行等待操作之后进行。这确保了信号量的正确性和互斥性。
-
在典型的生产者-消费者问题中,生产者在将数据放入缓冲区之前执行 P 操作以等待空闲缓冲区,然后执行 V 操作以释放空闲缓冲区;消费者在获取数据之前执行 P 操作以等待可用数据,然后执行 V 操作以释放缓冲区。这样的操作顺序保证了生产者和消费者之间的正确同步。
-
在一些情况下,可能存在信号量的释放操作先于等待操作的情况,这样的设计通常需要额外的逻辑来确保正确性,比如在资源的生命周期结束时释放信号量,而在等待资源时检查资源是否可用。但在大多数情况下,应遵循释放操作应在等待操作之后执行的原则,以确保程序的正确性和可靠性。