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

知识点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()

  • 函数介绍

    cpp 复制代码
    #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()

  • 函数介绍

    cpp 复制代码
    #include <semaphore.h>
    int sem_wait(sem_t *sem); 

    函数功能:

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

    参数:

    信号量的地址。

    返回值:

    成功:0

    失败:-1

    cpp 复制代码
    int sem_trywait(sem_t *sem);*

    函数功能:

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

    参数:

    信号量的地址。

    返回值:

    成功:0

    失败:-1

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

  • 功能介绍

    cpp 复制代码
    #include <semaphore.h>
    int sem_post(sem_t *sem);

    函数功能:

    将信号量加一

    参数:

    信号量的地址

    返回值:

    成功:0

    失败:-1

4、销毁信号量

  • 功能介绍

    cpp 复制代码
    #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操作

代码演示

cpp 复制代码
#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原语步骤

整体代码演示:

cpp 复制代码
#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、验证后再完成互斥操作

代码实现

cpp 复制代码
#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【无名信号量 用于 有血缘关系的进程间同步】

代码演示

cpp 复制代码
#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、造成了死锁

结束

代码重在练习!

代码重在练习!

代码重在练习!

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

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

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

相关推荐
SakitamaX10 分钟前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.19 分钟前
Linux系统管理
linux·运维
孫治AllenSun30 分钟前
【DataX】生产环境搭建DataX集群案例
java·开发语言·jvm
阿龙的工作记录1 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手1 小时前
Linux的基本指令
linux·运维·服务器
c238561 小时前
把 C++ 内存分配拆透:new 与 malloc 的三层血缘
开发语言·c++·算法
moonsims2 小时前
星闪在跨域无人化系统作用
开发语言·php
aaaameliaaa2 小时前
指针之总结
c语言·笔记·算法
Iruoyaoxh3 小时前
栈和队列~
java·开发语言
agathakuan3 小时前
Wireshark 解密並導出TLS 1.2 / TLS 1.3 明文的方法(可控制 Client 端)
c语言·wireshark·ssl·openwrt