linux的信号量初识

Linux下的信号量(Semaphore)深度解析

在多线程或多进程并发编程的领域中,确保对共享资源的安全访问和协调不同执行单元的同步至关重要。信号量(Semaphore)作为经典的同步原语之一,在 Linux 系统中扮演着核心角色。本文将深入探讨 Linux 环境下 POSIX 信号量的概念、工作原理、API 使用、示例代码、流程图及注意事项。

1. 什么是信号量?

信号量是由荷兰计算机科学家艾兹格·迪科斯彻(Edsger Dijkstra)在 1965 年左右提出的一个同步机制。本质上,信号量是一个非负整数计数器,它被用于控制对一组共享资源的访问。它主要支持两种原子操作:

  1. P 操作 (Proberen - 测试/尝试): 也称为 wait(), down(), acquire()。此操作会检查信号量的值。
    • 如果信号量的值大于 0,则将其减 1,进程/线程继续执行。
    • 如果信号量的值等于 0,则进程/线程会被阻塞(放入等待队列),直到信号量的值变为大于 0。
  2. V 操作 (Verhogen - 增加): 也称为 signal(), up(), post(), release()。此操作会将信号量的值加 1。
    • 如果此时有其他进程/线程因等待该信号量而被阻塞,则系统会唤醒其中一个(或多个,取决于实现)等待的进程/线程。

核心思想: 信号量的值代表了当前可用资源的数量。当一个进程/线程需要使用资源时,它执行 P 操作;当它释放资源时,执行 V 操作。

类比:

  • 计数信号量 (Counting Semaphore): 想象一个有 N 个停车位的停车场。信号量的初始值是 N。每当一辆车进入,信号量减 1 (P 操作)。当车位满 (信号量为 0) 时,新来的车必须等待。每当一辆车离开,信号量加 1 (V 操作),并可能通知等待的车辆有空位了。
  • 二值信号量 (Binary Semaphore): 停车场只有一个车位 (N=1)。信号量的值只能是 0 或 1。这常被用作互斥锁 (Mutex),确保同一时间只有一个进程/线程能访问某个临界区。

2. Linux 中的信号量类型

Linux 主要支持两种信号量实现:

  1. System V 信号量: 这是较老的一套 IPC (Inter-Process Communication) 机制的一部分(还包括 System V 消息队列和共享内存)。它功能强大但 API 相对复杂,信号量通常是内核持久的,需要显式删除。相关函数有 semget(), semop(), semctl()
  2. POSIX 信号量: 这是 POSIX 标准定义的一套接口,通常更推荐在新代码中使用。它提供了更简洁、更易于使用的 API。POSIX 信号量可以是命名信号量(可在不相关的进程间共享,通过名字访问,如 /mysemaphore)或未命名信号量(通常在同一进程的线程间或父子进程间共享,存在于内存中)。

本文将重点关注更常用且推荐的 POSIX 未命名信号量。

3. POSIX 信号量核心 API (C/C++)

使用 POSIX 信号量需要包含头文件 <semaphore.h>

3.1 sem_init() - 初始化未命名信号量

#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned int value);

功能: 初始化位于 sem 指向地址的未命名信号量。

参数:

  • sem_t *sem: 指向要初始化的信号量对象的指针。sem_t 是信号量类型。
  • int pshared: 控制信号量的共享范围。
    • 0: 信号量在当前进程的线程间共享。信号量对象 sem 应位于所有线程都能访问的内存区域(如全局变量、堆内存)。
    • 0: 信号量在进程间共享。信号量对象 sem 必须位于共享内存区域(例如使用 mmap 创建的共享内存段)。
  • unsigned int value: 信号量的初始值。对于二值信号量(用作锁),通常初始化为 1;对于计数信号量,根据可用资源数量初始化。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno。常见的 errno 包括 EINVAL (value 超过 SEM_VALUE_MAX),ENOSYS (不支持进程间共享)。

3.2 sem_destroy() - 销毁未命名信号量

#include <semaphore.h>int sem_destroy(sem_t *sem);

功能: 销毁由 sem_init() 初始化的未命名信号量 sem。销毁一个正在被其他线程等待的信号量会导致未定义行为。只有在确认没有线程再使用该信号量后才能销毁。

参数:

  • sem_t *sem: 指向要销毁的信号量对象的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno (如 EINVAL 表示 sem 不是一个有效的信号量)。

3.3 sem_wait() - 等待(P 操作/减 1)

#include <semaphore.h>int sem_wait(sem_t *sem);

功能: 对信号量 sem 执行 P 操作(尝试减 1)。

  • 如果信号量的值大于 0,则原子地将其减 1,函数立即返回。
  • 如果信号量的值等于 0,则调用线程/进程将被阻塞,直到信号量的值大于 0(通常是另一个线程/进程调用 sem_post() 之后)或收到一个信号。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno
    • EINVAL: sem 不是一个有效的信号量。
    • EINTR: 操作被信号中断。应用程序通常需要检查 errno 并重新尝试 sem_wait()

3.4 sem_trywait() - 非阻塞等待

#include <semaphore.h>int sem_trywait(sem_t *sem);

功能: sem_wait() 的非阻塞版本。

  • 如果信号量的值大于 0,则原子地将其减 1,函数立即返回 0。
  • 如果信号量的值等于 0,则函数立即返回 -1,并将 errno 设置为 EAGAIN,调用线程不会被阻塞。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。

返回值:

  • 成功 (信号量减 1): 返回 0。
  • 失败: 返回 -1,并设置 errno
    • EAGAIN: 信号量当前为 0,无法立即减 1。
    • EINVAL: sem 不是一个有效的信号量。

3.5 sem_timedwait() - 带超时的等待

#include <semaphore.h>
#include <time.h>int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

功能: 类似 sem_wait(),但带有超时限制。

  • 如果信号量的值大于 0,则原子地将其减 1,函数立即返回 0。
  • 如果信号量的值等于 0,则线程阻塞等待,但如果在 abs_timeout 指定的绝对时间(基于 CLOCK_REALTIME)到达之前信号量仍未增加,则函数返回错误。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。
  • const struct timespec *abs_timeout: 指向一个 timespec 结构体,指定了阻塞等待的绝对超时时间点。struct timespec { time_t tv_sec; long tv_nsec; };

返回值:

  • 成功 (信号量减 1): 返回 0。
  • 失败: 返回 -1,并设置 errno
    • ETIMEDOUT: 在超时时间到达前未能成功将信号量减 1。
    • EINVAL: sem 无效或 abs_timeout 无效。
    • EINTR: 操作被信号中断。

3.6 sem_post() - 释放(V 操作/加 1)

#include <semaphore.h>int sem_post(sem_t *sem);

功能: 对信号量 sem 执行 V 操作(原子地将其值加 1)。如果有任何线程/进程因此信号量而被阻塞,则其中一个会被唤醒。

参数:

  • sem_t *sem: 指向要操作的信号量对象的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno
    • EINVAL: sem 不是一个有效的信号量。
    • EOVERFLOW: 信号量的值增加将超过 SEM_VALUE_MAX

3.7 sem_getvalue() - 获取信号量当前值

#include <semaphore.h>int sem_getvalue(sem_t *sem, int *sval);

功能: 获取信号量 sem 的当前值,并将其存储在 sval 指向的整数中。注意:获取到的值可能在函数返回后立即就过时了(因为其他线程可能同时修改了信号量),主要用于调试或特定场景。

参数:

  • sem_t *sem: 指向要查询的信号量对象的指针。
  • int *sval: 指向用于存储信号量当前值的整数的指针。

返回值:

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置 errno (如 EINVAL)。

4. 工作流程图 (sem_wait 和 sem_post)

graph TDsubgraph Thread A (Calls sem_wait)A1(Start sem_wait(sem)) --> A2{Check sem value > 0?};A2 -- Yes --> A3[Decrement sem value];A3 --> A4[Proceed];A2 -- No --> A5[Block Thread A];endsubgraph Thread B (Calls sem_post)B1(Start sem_post(sem)) --> B2[Increment sem value];B2 --> B3{Any threads blocked on sem?};B3 -- Yes --> B4[Wake up one blocked thread (e.g., Thread A)];B3 -- No --> B5[Return];B4 --> B5;endA5 --> B4;  // Woken up by Thread B's postB4 -..-> A2; // Woken Thread A re-evaluates condition

流程图解释:

sem_wait 流程 (Thread A):

  1. 线程 A 调用 sem_wait
  2. 检查信号量的值是否大于 0。
    • 是: 信号量减 1,线程 A 继续执行。
    • 否: 线程 A 被阻塞,进入等待状态。

sem_post 流程 (Thread B):

  1. 线程 B 调用 sem_post
  2. 信号量的值加 1。
  3. 检查是否有其他线程(如线程 A)正因该信号量而被阻塞。
    • 是: 唤醒其中一个被阻塞的线程。被唤醒的线程会回到 sem_wait 的检查点,此时信号量值已大于 0,它将成功减 1 并继续执行。
    • 否: 直接返回。

5. C/C++ 测试用例:使用信号量保护临界区

这个例子演示了如何使用二值信号量(初始化为 1)来实现类似互斥锁的功能,保护一个共享计数器,防止多个线程同时修改导致竞态条件。

#include <iostream>
#include <vector>
#include <thread>
#include <semaphore.h> // For POSIX semaphores
#include <unistd.h>    // For usleep// Global shared resource
int shared_counter = 0;// Global semaphore (acting as a mutex)
sem_t mutex_semaphore;// Number of threads and increments per thread
const int NUM_THREADS = 5;
const int INCREMENTS_PER_THREAD = 100000;// Thread function
void worker_thread(int id) {for (int i = 0; i < INCREMENTS_PER_THREAD; ++i) {// --- Enter Critical Section ---if (sem_wait(&mutex_semaphore) == -1) { // P operation (wait)perror("sem_wait failed");return; // Exit thread on error}// --- Critical Section Start ---// Access shared resourceint temp = shared_counter;// Simulate some work inside the critical section// usleep(1); // Optional small delay to increase chance of race condition without semaphoreshared_counter = temp + 1;// --- Critical Section End ---if (sem_post(&mutex_semaphore) == -1) { // V operation (post)perror("sem_post failed");// Handle error if necessary, though less critical than wait failure}// --- Exit Critical Section ---}std::cout << "Thread " << id << " finished." << std::endl;
}int main() {// Initialize the semaphore// pshared = 0: shared between threads of the same process// value = 1: initial value, acting as a binary semaphore (mutex)if (sem_init(&mutex_semaphore, 0, 1) == -1) {perror("sem_init failed");return 1;}std::cout << "Starting " << NUM_THREADS << " threads, each incrementing counter "<< INCREMENTS_PER_THREAD << " times." << std::endl;std::vector<std::thread> threads;for (int i = 0; i < NUM_THREADS; ++i) {threads.emplace_back(worker_thread, i);}// Wait for all threads to completefor (auto& t : threads) {t.join();}// Destroy the semaphoreif (sem_destroy(&mutex_semaphore) == -1) {perror("sem_destroy failed");// Continue cleanup if possible}std::cout << "All threads finished." << std::endl;std::cout << "Expected final counter value: " << NUM_THREADS * INCREMENTS_PER_THREAD << std::endl;std::cout << "Actual final counter value:   " << shared_counter << std::endl;// Check if the result is correctif (shared_counter == NUM_THREADS * INCREMENTS_PER_THREAD) {std::cout << "Result is correct!" << std::endl;} else {std::cout << "Error: Race condition likely occurred!" << std::endl;}return 0;
}

编译与运行:

# Compile using g++ (or gcc if it were pure C)
# Link with pthread library for std::thread and potentially needed by semaphore implementation
g++ semaphore_example.cpp -o semaphore_example -pthread# Run the executable
./semaphore_example

预期输出:
程序会创建多个线程,每个线程对共享计数器执行大量递增操作。由于信号量的保护,最终的 shared_counter 值应该等于 NUM_THREADS * INCREMENTS_PER_THREAD。如果没有信号量保护(注释掉 sem_waitsem_post),最终结果几乎肯定会小于预期值,因为会发生竞态条件。

6. 信号量的主要应用场景

  1. 互斥访问 (Mutual Exclusion): 使用初始值为 1 的二值信号量来保护临界区,确保同一时间只有一个线程/进程能访问共享资源或执行某段代码,功能类似互斥锁(Mutex)。
  2. 资源计数: 使用初始值为 N 的计数信号量来管理 N 个相同的资源(如数据库连接池中的连接、线程池中的工作线程等)。需要资源的线程执行 P 操作,释放资源的线程执行 V 操作。
  3. 同步 (Synchronization): 协调不同线程/进程的执行顺序。例如,一个线程(生产者)产生数据后执行 V 操作,另一个线程(消费者)在执行 P 操作时等待,直到有数据可用。

7. 注意事项与最佳实践

  1. 成对使用 sem_waitsem_post: 在保护临界区的场景下,每个 sem_wait 都必须有对应的 sem_post。忘记 sem_post 会导致资源永久锁定(死锁的一种形式),而错误地多调用 sem_post 会破坏互斥性。
  2. 初始化与销毁: 确保在使用前正确调用 sem_init 初始化信号量,并在不再需要时调用 sem_destroy 销毁它。对于进程间共享的信号量,销毁逻辑需要特别注意。
  3. 错误检查: 务必检查 sem_init, sem_wait, sem_trywait, sem_timedwait, sem_post, sem_destroy 等函数的返回值,并在失败时根据 errno 进行适当的错误处理。
  4. 处理 EINTR: sem_waitsem_timedwait 可能会被信号中断(返回 -1 且 errnoEINTR)。健壮的程序应该捕获这种情况并通常重新尝试等待操作。
  5. 死锁 (Deadlock): 当多个线程/进程相互等待对方持有的信号量时,会发生死锁。设计锁的获取顺序是避免死锁的关键策略之一。例如,总是按相同的固定顺序获取多个信号量。
  6. 避免在信号处理函数中使用 sem_wait: 信号处理函数的执行环境受限。在信号处理函数中调用可能阻塞的函数(如 sem_wait)通常是不安全的,可能导致

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/79113.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《Android 应用开发基础教程》——第十一章:Android 中的图片加载与缓存(Glide 使用详解)

目录 第十一章&#xff1a;Android 中的图片加载与缓存&#xff08;Glide 使用详解&#xff09; &#x1f539; 11.1 Glide 简介 &#x1f538; 11.2 添加 Glide 依赖 &#x1f538; 11.3 基本用法 ✦ 加载网络图片到 ImageView&#xff1a; ✦ 加载本地资源 / 文件 / UR…

AE模板 300个故障干扰损坏字幕条标题动画视频转场预设

这个AE模板提供了300个故障干扰损坏字幕条标题动画视频转场预设&#xff0c;让您的视频具有炫酷的故障效果。无论是预告片、宣传片还是其他类型的视频&#xff0c;这个模板都能带给您令人惊叹的故障运动标题效果。该模板无需任何外置插件或脚本&#xff0c;只需一键点击即可应用…

在 Python 中,以双下划线开头和结尾的函数(如 `__str__`、`__sub__` 等)

在 Python 中&#xff0c;以双下划线开头和结尾的函数&#xff08;如 __str__、__sub__ 等&#xff09;被称为特殊方法&#xff08;Special Methods&#xff09;或魔术方法&#xff08;Magic Methods&#xff09;。它们确实是 Python 内置的&#xff0c;用于定义类的行为&#…

git问题记录-如何切换历史提交分支,且保留本地修改

问题记录 我在本地编写了代码&#xff0c;突然想查看之前提交的代码&#xff0c;并且想保留当前所在分支所做的修改 通过git stash对本地的代码进行暂存 使用git checkout <commit-hash>切换到之前的提交记录。 查看完之后我想切换回来&#xff0c;恢复暂存的本地代码…

Github开通第三方平台OAuth登录及Java对接步骤

调研起因&#xff1a; 准备搞AI Agent海外项目&#xff0c;有相当一部分用户群体是程序员&#xff0c;所以当然要接入Github这个全球最大的同性交友网站了&#xff0c;让用户使用Github账号一键完成注册或登录。 本教程基于Web H5界面进行对接&#xff0c;同时也提供了spring-…

期刊、出版社、索引数据库

image 1、研究人员向期刊或者会议投稿&#xff0c;交注册费和相应的审稿费等相关费用[1]&#xff1b; 2、会议组织者和期刊联系出版社&#xff0c;交出版费用&#xff1b; 3、出版社将论文更新到自己的数据库中&#xff0c;然后将数据库卖给全世界各大高校或企业&#xff1b; 4…

Transformer 模型及深度学习技术应用

近年来&#xff0c;随着卷积神经网络&#xff08;CNN&#xff09;等深度学习技术的飞速发展&#xff0c;人工智能迎来了第三次发展浪潮&#xff0c;AI技术在各行各业中的应用日益广泛。 注意力机制&#xff1a;理解其在现代深度学习中的关键作用&#xff1b; Transformer模型…

zynq7035的arm一秒钟最多可以支持触发多少次中断

一、概述 1.关于zynq7035的ARM处理器一秒能够支持多少次中断触发&#xff0c;需要综合来考虑。需要确定ARM处理器的参数&#xff0c;目前zynq7000系列&#xff0c;使用的双核Cortex-A9处理器。其中主频大概在500MHZ~1GHZ左右&#xff0c;不同的用户配置的主频可能稍微有差别。 …

数据结构与算法:图论——最短路径

最短路径 先给出一些leetcode算法题&#xff0c;以后遇见了相关题目再往上增加 最短路径的4个常用算法是Floyd、Bellman-Ford、SPFA、Dijkstra。不同应用场景下&#xff0c;应有选择地使用它们&#xff1a; 图的规模小&#xff0c;用Floyd。若边的权值有负数&#xff0c;需要…

[android]MT6835 Android 关闭selinux方法

Selinux SELinux is an optional feature of the Linux kernel that provides support to enforce access control security policies to enforce MAC. It is based on the LSM framework. Working with SELinux on Android – LineageOS Android 关闭selinux MT6835 Android…

【Linux网络编程】http协议的状态码,常见请求方法以及cookie-session

本文专栏&#xff1a;Linux网络编程 目录 一&#xff0c;状态码 重定向状态码 1&#xff0c;永久重定向&#xff08;301 Moved Permanently&#xff09; 2&#xff0c;临时重定向&#xff08;302 Found&#xff09; 二&#xff0c;常见请求方法 1&#xff0c;HTTP常见Hea…

当神经网络突破摩尔定律:探索大模型时代的算力新纪元

当摩尔定律熄灭后&#xff1a;AI算力革命如何重塑技术文明的底层逻辑 一、摩尔定律的黄昏&#xff1a;物理极限与经济理性的双重困境 当英特尔在1965年提出摩尔定律时&#xff0c;没有人预料到这个每18-24个月将芯片晶体管数量翻倍的预言会成为现代计算文明的基石。半个世纪以…

位运算题目:寻找重复数

文章目录 题目标题和出处难度题目描述要求示例数据范围进阶 前言解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 解法三思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;寻找重复数 出处&#xff1a;287. 寻找重复数 难度 6 级 题目描述 要…

Elasticsearch:没有 “AG” 的 RAG?

作者&#xff1a;来自 Elastic Gustavo Llermaly 了解如何利用语义搜索和 ELSER 构建一个强大且视觉上吸引人的问答体验&#xff0c;而无需使用 LLMs。 想要获得 Elastic 认证&#xff1f;查看下一期 Elasticsearch Engineer 培训的时间&#xff01; Elasticsearch 拥有众多新…

linux下安装ollama网不好怎么办?

文章目录 前言kkgithub下载脚本,而不是直接运行修改脚本修改权限还是不行?前言 今天想在linux上面更新一下ollama,于是去到官网: https://ollama.com/download/linux linux下安装ollama还是挺简单的: curl -fsSL https://ollama.com/install.sh | sh我也是特别嗨皮地就…

相机-IMU联合标定:相机-IMU外参标定

文章目录 📚简介🚀标定工具kalibr🚀标定数据录制🚀相机-IMU外参标定📚简介 在 VINS(视觉惯性导航系统) 中,相机-IMU外参标定 是确保多传感器数据时空统一的核心环节,其作用可概括为以下关键点: 坐标系对齐(空间同步),外参误差会导致视觉特征点投影与IMU预积…

基于 Java 的实现前端组装查询语句,后端直接执行查询方案,涵盖前端和后端的设计思路

1. 前端设计 前端负责根据用户输入或交互条件,动态生成查询参数,并通过 HTTP 请求发送到后端。 前端逻辑: 提供用户界面(如表单、筛选器等),让用户选择查询条件。将用户选择的条件组装成 JSON 格式的查询参数。发送 HTTP 请求(如 POST 或 GET)到后端。示例: 假设用…

[STM32] 4-2 USART与串口通信(2)

文章目录 前言4-2 USART与串口通信(2)数据发送过程双缓冲与连续发送数据发送过程中的问题 数据接收过程TXE标志位&#xff08;发送数据寄存器空&#xff09;TC标志位&#xff08;发送完成标志位&#xff09;单个数据的发送数据的连续发送 接收过程中遇到的问题问题描述&#xf…

Qt多线程TCP服务器实现指南

在Qt中实现多线程TCP服务器可以通过为每个客户端连接分配独立的线程来处理&#xff0c;以提高并发性能。以下是一个分步实现的示例&#xff1a; 1. 自定义工作线程类&#xff08;处理客户端通信&#xff09; // workerthread.h #include <QObject> #include <QTcpSo…

详细介绍Python-pandas-DataFrame全部 *功能* 函数

Python-pandas-DataFrame全部 功能 函数 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是pandas的使用语法。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性。【帮帮志系列文章】&#xff1a;每个知识点…