Linux 深入浅出信号量:从线程到进程的同步与互斥实战指南

news2025/4/20 3:42:56

知识点1【信号量概述】

信号量是广泛用于进程和线程间的同步和互斥。信号量的本质 是一个非负的整数计数器,它被用来控制对公共资源的访问

当信号量值大于0的时候,可以访问,否则将阻塞。

PV原语对信号量的操作,一次P操作使信号量减一,一次V操作使信号量加一。

信号量的类型sem_t

信号量用于互斥:不管多少个任务互斥,只需要一个信号量,信号量应初始化为1

先P操作,再V操作

大家看上面这张图,若任务A抢到该任务量,信号量被初始化为1,由于先P(减一),导致其他线程(进程)被阻塞,实现互斥的功能,然后执行任务A,V操作(加一),其他任务抢锁,循环上面的过程。

信号量用于同步:有多少个任务,就需要多少个信号量,最先执行的任务对应的信号量为1,其他信号量全部为0

下面介绍一下流程

每个任务先P自己,然后V下一个要执行的任务的信号量

详细介绍:

如图,我们先将sem1初始化1,任务A执行P操作,其他任务被阻塞,执行任务A函数体,任务A结束后,执行要执行任务sem2的V操作,又由于sem1的值为0,即使有循环,也不需要担心A任务继续执行

知识点2【信号量的API】

1、初始化信号量sem_init()

  • 函数介绍

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

    函数功能:

    创建一个信号量并初始化它的值。一个无名信号在被使用前必须先初始化

    参数:

    sen:信号量的地址

    pshared:

    等于0,信号量在线程间共享

    非0:信号量在进程间共享

    value:信号量的初始值

    返回值:

    成功:0

    失败:-1

2、信号量减一 P操作 sem_wait()

  • 函数介绍

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

    函数功能:

    将信号量减一。如果信号量为0,则阻塞,大于0则可以减一

    参数:

    信号量的地址。

    返回值:

    成功:0

    失败:-1

    int sem_trywait(sem_t *sem);*
    

    函数功能:

    尝试将信号量减一,如果信号量的值为0,不阻塞,立即返回,大于0可以加一

    参数:

    信号量的地址。

    返回值:

    成功:0

    失败:-1

3、信号量加一 V操作 sem_post()

  • 功能介绍

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

    函数功能:

    将信号量加一

    参数:

    信号量的地址

    返回值:

    成功:0

    失败:-1

4、销毁信号量

  • 功能介绍

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

    函数功能:

    销毁信号量

    参数:

    信号量的地址

    返回值:

    成功:0

    失败:-1

知识点3【信号量用于线程的互斥】

代码步骤

1、创建 初始化 阻塞回收线程 3个

2、线程函数创建void *名(void *arg)

封装一个函数my_printf()

这里用的函数实现的功能都是一样的,可以用同一个函数,但是为了提高观看的直观性,我们分成了3个进程函数

在这里我们运行一下验证函数功能

3、全局创建,初始化,销毁信号量 1个

4、在线程函数中执行PV操作

代码演示

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

//封装my_printf() 函数
void my_printf(char *);
//线程函数声明
void *my_fun01(void *arg);
void *my_fun02(void *arg);
void *my_fun03(void *arg);

//全局创建信号量
sem_t sem;

int main(int argc, char const *argv[])
{
    srand(time(NULL));

    //创建线程 3 个
    pthread_t tid1,tid2,tid3;

    //信号量初始化
    sem_init(&sem,0,1);//初始化信号地址,线程信号量,初始化值1

    //初始化线程
    pthread_create(&tid1,NULL,my_fun01,(void *)"pthread A  ");
    pthread_create(&tid2,NULL,my_fun02,(void *)"pthread B  ");
    pthread_create(&tid3,NULL,my_fun03,(void *)"pthread C  ");

    //销毁线程
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);

    //摧毁信号量
    sem_destroy(&sem);

    return 0;
}
//my_printf() 函数实现
void my_printf(char *arr)
{
    while(*arr != 0)
    {
        printf("%c",*arr);
        fflush(stdout);
        usleep(1000 * 100);
        arr++;
    }
}
//线程函数实现
void *my_fun01(void *arg)
{
    while(1)
    {   
        //p操作
        sem_wait(&sem);

        //函数体
        my_printf((char *)arg);

        //v操作
        sem_post(&sem);
        
        //关索后休眠,防止重复抢锁
        //重要!!!!!
        usleep(1000 * 1000 *(rand()%4 + 1));
    }
    return NULL;
}
void *my_fun02(void *arg)
{
    while(1)
    {   
        //p操作
        sem_wait(&sem);

        //函数体
        my_printf((char *)arg);

        //v操作
        sem_post(&sem);
        
        //关索后休眠,防止重复抢锁
        usleep(1000 * 100 *(rand()%4 + 1));
    }
    return NULL;
}
void *my_fun03(void *arg)
{
    while(1)
    {   
        //p操作
        sem_wait(&sem);

        //函数体
        my_printf((char *)arg);

        //v操作
        sem_post(&sem);
        
        //关索后休眠,防止重复抢锁
        usleep(1000 * 100 *(rand()%4 + 1));
    }
    return NULL;
}

代码运行结果

这个是执行完1,2步骤后函数功能验证运行结果:

完整代码的运行结

知识点4【信号量用于线程的同步】

同步操作我们只需要改一下上述代码 但是为了让大家更好地理解 我将扔把全部代码发出

执行顺序:进程A,C,B

这里我先标出不同点地方:

1、信号量的个数

2、信号量的初始化和销毁

3、线程函数中PV原语步骤

整体代码演示:

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

//封装my_printf() 函数
void my_printf(char *);
//线程函数声明
void *my_fun01(void *arg);
void *my_fun02(void *arg);
void *my_fun03(void *arg);

//全局创建信号量 同步 三个进程创建三个信号量
sem_t sem1,sem2,sem3;

int main(int argc, char const *argv[])
{
    srand(time(NULL));

    //创建线程 3 个
    pthread_t tid1,tid2,tid3;

    //信号量初始化
    sem_init(&sem1,0,1);//初始化信号地址,线程信号量,初始化值1
    sem_init(&sem2,0,0);//初始化信号地址,线程信号量,初始化值0
    sem_init(&sem3,0,0);//初始化信号地址,线程信号量,初始化值0

    //初始化线程
    pthread_create(&tid1,NULL,my_fun01,(void *)"pthread A  ");
    pthread_create(&tid2,NULL,my_fun02,(void *)"pthread B  ");
    pthread_create(&tid3,NULL,my_fun03,(void *)"pthread C  ");

    //销毁线程
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    pthread_join(tid3,NULL);

    //摧毁信号量
    sem_destroy(&sem1);
    sem_destroy(&sem2);
    sem_destroy(&sem3);

    return 0;
}
//my_printf() 函数实现
void my_printf(char *arr)
{
    while(*arr != 0)
    {
        printf("%c",*arr);
        fflush(stdout);
        usleep(1000 * 100);
        arr++;
    }
}
//线程函数实现
void *my_fun01(void *arg)
{
    while(1)
    {   
        //p操作
        sem_wait(&sem1);

        //函数体
        my_printf((char *)arg);

        //v操作
        sem_post(&sem3);
        
        //关索后休眠,防止重复抢锁
        //重要!!!!!
        usleep(1000 * 1000 *(rand()%2 + 1));
    }
    return NULL;
}
void *my_fun02(void *arg)
{
    while(1)
    {   
        //p操作
        sem_wait(&sem2);

        //函数体
        my_printf((char *)arg);

        //v操作
        sem_post(&sem1);
        
        //关索后休眠,防止重复抢锁
        usleep(1000 * 100 *(rand()%4 + 1));
    }
    return NULL;
}
void *my_fun03(void *arg)
{
    while(1)
    {   
        //p操作
        sem_wait(&sem3);

        //函数体
        my_printf((char *)arg);

        //v操作
        sem_post(&sem2);
        
        //关索后休眠,防止重复抢锁
        usleep(1000 * 100 *(rand()%4 + 1));
    }
    return NULL;
}

代码运行结果:

知识点5【无名信号量 用于 有血缘关系的进程间互斥】

互斥仍只需要一个信号量

有血缘关系的进程 说明 需要fork 创建子进程

现在只有一个问题 无名信号量是什么?

现在我们想一下 如果子进程1中,我们对一个变量的值进行修改,子进程2 中的值会改变吗?

答案是不会的,那我们该如何实现互斥和同步呢?

这里只需要找到子进程间能够互相识别的部分即可。这里利用我们 之前讲的进程间的共享内存中的磁盘映射mmap。

好了,现在思路有了 我们来写一下代码实现的步骤

代码实现步骤

1、进程的创建 父进程负责管理子进程的空间,子进程负责操作

2、父进程进行信号量磁盘映射,这里我们使用匿名映射

创建子进程,会不会重复映射呢?这个大家放心是不会的,因为系统会识别,父进程映射成功,子进程映射会失败的

3、进程中先实现功能基本输出,并进行验证

4、验证后再完成互斥操作

代码实现

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>

#define NUM 2
//打包my_printf函数
void my_printf(char *arr);
int main(int argc, char const *argv[])
{
    //创建一个数组用来存储 子进程的id 在本项目中不需要 目的是帮助大家回忆
    pid_t arr[NUM] = {0};
    //映射mmap信号量
    sem_t *sem = (sem_t *)mmap(NULL,sizeof(sem_t),PROT_WRITE|PROT_READ,MAP_SHARED | MAP_ANONYMOUS,-1,0);
    
    //信号量的初始化
    sem_init(sem,1,1);

    //父进程创建两个子进程
    int i = 0;
    for (;i < NUM; i++)
    {
        arr[i] = fork();
        if(arr[i] == -1)
        {
            perror("fork");
            _exit(-1);
        }
        else if(arr[i] == 0)
        {
            break;
        }
    }
    //子进程1 打印world
    if(i == 0)
    {
        //P操作
        sem_wait(sem);

        //函数体
        my_printf("world");

        //V操作
        sem_post(sem);

        _exit(-1);
    }

    //子进程2 打印hello
    else if(i == 1)
    {
        //P操作
        sem_wait(sem);

        //函数体
        my_printf("hello");

        //V操作
        sem_post(sem);

        _exit(-1);
    }

    //父进程 回收空间waitpid
    while(1)
    {
        int ret = waitpid(-1,NULL,WNOHANG);
        if(ret < 0)
        {
            break;
        }
    }
    //销毁信号量
    sem_destroy(sem);

    return 0;
}
void my_printf(char *arr)
{
    while(*arr != 0)
    {
        printf("%c",*arr);
        fflush(stdout);
        usleep(1000 * 200);//0.2s打印一次
        arr++;
    }
    return;
}

这里我们补充一个小点:

MAP_ANONYMOUS是匿名的意思,如果用了这个在文件描述符必须写-1

代码运行结果

1、没有实现互斥的情况

2、实现互斥的情况

知识点5【无名信号量 用于 有血缘关系的进程间同步】

代码演示

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>

#define NUM 2
//打包my_printf函数
void my_printf(char *arr);
int main(int argc, char const *argv[])
{
    //创建一个数组用来存储 子进程的id 在本项目中不需要 目的是帮助大家回忆
    pid_t arr[NUM] = {0};

    //映射mmap信号量 由于有两个子进程,并且要完成同步操作,因此我们需要完成两次映射磁盘
    sem_t *sem1 = (sem_t *)mmap(NULL,sizeof(sem_t),PROT_WRITE|PROT_READ,MAP_SHARED | MAP_ANONYMOUS,-1,0);
    sem_t *sem2 = (sem_t *)mmap(NULL,sizeof(sem_t),PROT_WRITE|PROT_READ,MAP_SHARED | MAP_ANONYMOUS,-1,0);
    //我们这里实现先遍历 world 再遍历 hello

    //信号量的初始化
    sem_init(sem1,1,1);
    sem_init(sem2,1,0);

    //父进程创建两个子进程
    int i = 0;
    for (;i < NUM; i++)
    {
        arr[i] = fork();
        if(arr[i] == -1)
        {
            perror("fork");
            _exit(-1);
        }
        else if(arr[i] == 0)
        {
            break;
        }
    }
    //子进程1 打印world
    if(i == 0)
    {
        //P操作
        sem_wait(sem1);

        //函数体
        my_printf("world");

        //V操作
        sem_post(sem2);

        _exit(-1);
    }

    //子进程2 打印hello
    else if(i == 1)
    {
        //P操作
        sem_wait(sem2);

        //函数体
        my_printf("hello");

        //V操作
        sem_post(sem1);

        _exit(-1);
    }

    //父进程 回收空间waitpid
    while(1)
    {
        int ret = waitpid(-1,NULL,WNOHANG);
        if(ret < 0)
        {
            break;
        }
    }
    //销毁信号量
    sem_destroy(sem1);
    sem_destroy(sem2);

    return 0;
}
void my_printf(char *arr)
{
    while(*arr != 0)
    {
        printf("%c",*arr);
        fflush(stdout);
        usleep(1000 * 200);//0.2s打印一次
        arr++;
    }
    return;
}

代码运行结果

下面将标出不同的地方:

代码中遇到的问题:

1、子进程的创建步骤 有些模糊

逻辑,循环中,应是只有子进程才会break,父进程要一直运行循环,不能是因为是break退出。

2、造成了死锁

结束

代码重在练习!

代码重在练习!

代码重在练习!

今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!

今天的内容,中有遗漏了信号量用于无血缘关系的进程的互斥与同步

是因为我在写的过程中遇到了一些问题,我解决后将进行补充。

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

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

相关文章

Oracle数据库数据编程SQL<9.1 数据库逻辑备份和迁移exp和imp之导出、导入>

EXP (Export) 和 IMP (Import) 是 Oracle 提供的传统数据导出导入工具,用于数据库逻辑备份和迁移。尽管在较新版本中已被 Data Pump (EXPDP/IMPDP) 取代,但在某些场景下仍然有用。 目录 一、EXP 导出工具 1. 基本语法 2. 常用参数说明 3. 导出模式 3.1 表模式导出 3.2 用…

DotnetCore开源库SampleAdmin源码编译

1.报错: System.Net.Sockets.SocketException HResult0x80004005 Message由于目标计算机积极拒绝&#xff0c;无法连接。 SourceSystem.Net.Sockets StackTrace: 在 System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, C…

.Net 9 webapi使用Docker部署到Linux

参考文章连接&#xff1a; https://www.cnblogs.com/kong-ming/p/16278109.html .Net 6.0 WebApi 使用Docker部署到Linux系统CentOS 7 - 长白山 - 博客园 项目需要跨平台部署&#xff0c;所以就研究了一下菜鸟如何入门Net跨平台部署&#xff0c;演示使用的是Net 9 webAPi Li…

PyTorch 根据官网命令行无法安装 GPU 版本 解决办法

最近遇到一个问题&#xff0c;PyTorch 官网给出了 GPU 版本的安装命令&#xff0c;但安装成功后查看版本&#xff0c;仍然是 torch 2.6.0cpu 1. 清理现有 PyTorch 安装 经过探索发现&#xff0c;需要同时卸载 conda 和 pip 安装的 torch。 conda remove pytorch torchvision …

PHP防火墙代码,防火墙,网站防火墙,WAF防火墙,PHP防火墙大全

PHP防火墙代码,防火墙,网站防火墙,WAF防火墙,PHP防火墙大全 资源宝整理分享&#xff1a;https://www.htple.net PHP防火墙&#xff08;作者&#xff1a;悠悠楠杉&#xff09; 验证测试&#xff0c;链接后面加上?verify_cs1后可以自行测试 <?php //复制保存zzwaf.php$we…

使用 Vitis Model Composer 生成 FPGA IP 核

本文将逐步介绍如何使用 Vitis Model Composer 生成 FPGA IP 核&#xff0c;从建模到部署。 在当今快节奏的世界里&#xff0c;技术正以前所未有的速度发展&#xff0c;FPGA 设计也不例外。高级工具层出不穷&#xff0c;加速着开发进程。传统上&#xff0c;FPGA 设计需要使用硬…

BERT、T5、ViT 和 GPT-3 架构概述及代表性应用

BERT、T5、ViT 和 GPT-3 架构概述 1. BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09; 架构特点 基于 Transformer 编码器&#xff1a;BERT 使用多层双向 Transformer 编码器&#xff0c;能够同时捕捉输入序列中每个词的左右上下文信息…

倚光科技:以创新之光,雕琢全球领先光学设计公司

在光学技术飞速发展的当下&#xff0c;每一次突破都可能为众多领域带来变革性的影响。而倚光&#xff08;深圳&#xff09;科技有限公司&#xff0c;作为光学设计公司的一颗璀璨之星&#xff0c;正以其卓越的创新能力和深厚的技术底蕴&#xff0c;引领着光学设计行业的发展潮流…

数据结构(六)——红黑树及模拟实现

目录 前言 红黑树的概念及性质 红黑树的效率 红黑树的结构 红黑树的插入 变色不旋转 单旋变色 双旋变色 插入代码如下所示&#xff1a; 红黑树的查找 红黑树的验证 红黑树代码如下所示&#xff1a; 小结 前言 在前面的文章我们介绍了AVL这一棵完全二叉搜索树&…

解决 Vue 中 input 输入框被赋值后,无法再修改和编辑的问题

目录 需求&#xff1a; 出现 BUG&#xff1a; Bug 代码复现 解决问题&#xff1a; 解决方法1&#xff1a; 解决方法2 关于 $set() 的补充&#xff1a; 需求&#xff1a; 前段时间&#xff0c;接到了一个需求&#xff1a;在选择框中选中某个下拉菜单时&#xff0c;对应的…

【差分隐私相关概念】瑞丽差分隐私(RDP)-瑞丽散度约束了贝叶斯因子后验变化

分步解释和答案&#xff1a; 在Rnyi差分隐私&#xff08;RDP&#xff09;框架中&#xff0c;通过贝叶斯因子和Rnyi散度的关系可以推导出关于后验变化的概率保证。以下是关键步骤的详细解释&#xff1a; 1. 贝叶斯因子的定义与分解 设相邻数据集 D D D 和 D ′ D D′&#x…

前端快速入门——JavaScript函数、DOM

1.JavaScript函数 函数是一段可重复使用的代码块&#xff0c;它接受输入(参数)、执行特定任务&#xff0c;并返回输出。 <scricpt>function add(a,b){return ab;}let cadd(5,10);console.log(c); </script>2.JavaScript事件 JavaScript绑定事件的方法&#xff1…

10【模块学习】LCD1602(二):6路温度显示+实时时钟

项目&#xff1a;6路温度显示实时时钟 1、6路温度显示①TempMenu.c文件的代码②TempMenu.h文件的代码③main.c文件的代码④Timer.c文件的代码⑤Delay.c文件的代码⑥Key.c文件的代码 2、实时时钟显示①BeiJingTime.c文件的代码②BeiJingTime.h文件的代码③main.c文件的代码如下④…

PDF处理控件Aspose.PDF指南:使用 C# 从 PDF 文档中删除页面

需要从 PDF 文档中删除特定页面&#xff1f;本快速指南将向您展示如何仅用几行代码删除不需要的页面。无论您是清理报告、跳过空白页&#xff0c;还是在共享前自定义文档&#xff0c;C# 都能让 PDF 操作变得简单高效。学习如何以编程方式从 PDF 文档中选择和删除特定页面&#…

如何在不同版本的 Elasticsearch 之间以及集群之间迁移数据

作者&#xff1a;来自 Elastic Kofi Bartlett 当你想要升级一个 Elasticsearch 集群时&#xff0c;有时候创建一个新的独立集群并将数据从旧集群迁移到新集群会更容易一些。这让用户能够在不冒任何停机或数据丢失风险的情况下&#xff0c;在新集群上使用所有应用程序测试其所有…

Day08【基于预训练模型分词器实现交互型文本匹配】

基于预训练模型分词器实现交互型文本匹配 目标数据准备参数配置数据处理模型构建主程序测试与评估总结 目标 本文基于预训练模型bert分词器BertTokenizer&#xff0c;将输入的文本以文本对的形式&#xff0c;送入到分词器中得到文本对的词嵌入向量&#xff0c;之后经过若干网络…

npm和npx的作用和区别

npx 和 npm 是 Node.js 生态系统中两个常用的工具&#xff0c;它们有不同的作用和使用场景。 1. npm&#xff08;Node Package Manager&#xff09; 作用&#xff1a; npm 是 Node.js 的包管理工具&#xff0c;主要用于&#xff1a; 安装、卸载、更新项目依赖&#xff08;包&a…

C++学习之金融类安全传输平台项目git

目录 1.知识点概述 2.版本控制工具作用 3.git和SVN 4.git介绍 5.git安装 6.工作区 暂存区 版本库概念 7.本地文件添加到暂存区和提交到版本库 8.文件的修改和还原 9.查看提交的历史版本信息 10.版本差异比较 11.删除文件 12.本地版本管理设置忽略目录 13.远程git仓…

CCF CSP 第36次(2024.12)(1_移动_C++)

CCF CSP 第36次&#xff08;2024.12&#xff09;&#xff08;1_移动_C&#xff09; 解题思路&#xff1a;思路一&#xff1a; 代码实现代码实现&#xff08;思路一&#xff09;&#xff1a; 时间限制&#xff1a; 1.0 秒 空间限制&#xff1a; 512 MiB 原题链接 解题思路&…

7.thinkphp的路由

一&#xff0e;路由简介 1. 路由的作用就是让URL地址更加的规范和优雅&#xff0c;或者说更加简洁&#xff1b; 2. 设置路由对URL的检测、验证等一系列操作提供了极大的便利性&#xff1b; 3. 路由是默认开启的&#xff0c;如果想要关闭路由&#xff0c;在config/app.php配置…