Linux进程间通信-共享内存 &信号量

进程间通信-共享内存 & 信号量


一.共享内存

1. 简单介绍
        (1)共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间的数据传递不再涉及内核,即进程不再通过执行进入内核的系统调用来传递彼此的数据。
        (2)共享内存的生命周期随内核。
        (3)注意: 共享内存未提供任何保护资源,即共享内存自身没有同步与互斥机制,但它是临界资源,所以我们需要利用其它机制来保证数据的正确性,Linux下就可以用信号量达到同步的目的。
        (4) linux共享内存有两种方式(本文主要介绍shmget方式)
                1)mmap方式,适用于父子进程之间,创建的内存非常大时;
                2)shmget方式,适用于同一台电脑上不同进程之间,创建的内存相对较小。
        (5)进程间利用共享内存实现消息队列的基本原理如下图


2. 相关函数介绍
(1)shmget函数
        1)函数原型:
        2)函数功能:创建共享内存
        3)参数:
            key:共享内存段名字
            size:共享内存大小
            shmflg:九个权限标志构成,用法与创建文件时用的mode一致
        4)返回值:成功返回一个非负整数,即该共享内存段标识码;失败返回-1
(2)shmat函数
        1)函数原型:
        2)函数功能:将共享内存段连接到进程地址空间
        3)参数:
            shmid:共享内存标识码
            shmaddr:指定连接的地址
            shmflg:两个可能取值SHM_RND和SHM_RDONLY
        4)返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
        5)说明:
            shmaddr为NULL时,核心自动选择一个地址;
            shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址;
            shmaddr不为NULL时,且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍,相应公式为:shmadrr - (shmadrr % SHMLBA);
            shmflg = SHM_RDONLY,表示连接操作用来只读共享内存

(3)shmdt函数
        1)函数原型:
        2)函数功能:将共享内存段与当前进程脱离
        3)参数:
            shmadrr:由shmat函数返回的指针
        4)返回值:成功返回0;失败返回-1
        5)注意: 将共享内存段与当前进程脱离不等于删除共享内存段


(4)shmctl函数
        1)函数原型:
        2)函数功能:用于控制共享内存
        3)参数:
            shmid:由shmget函数返回的共享内存标识码
            cmd:将要采取的动作,有三个可取值
            buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
        4)返回值:成功返回0;失败返回-1

3. 共享内存实现进程间的单向通信

(1)创建一个comm.c文件用于实现创建、销毁共享内存,向共享内存里发消息收消息等函数,comm.h文件为它相应的头文件等内容
comm.h
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATHNAME "."
#define PROJ_ID 0x6666   
comm.c
#include "comm.h"                                                                                                                    
static int _CreateShm(int size, int flags)
{
    key_t key = ftok(PATHNAME, PROJ_ID);
    if(key < 0)
    {       
        perror("ftok");
        return -1;
    }
    int shmid;//共享内存标识码(非负整数)
    if((shmid = shmget(key, size, flags)) < 0)//创建共享内存失败时返回-1
    {
        perror("shmget");
        return -2;
    }
    return shmid;//将创建的共享内存的标识码返回,以实现后续代码
}

int CreateShm(int size)//创建一个新的共享内存
{
    return _CreateShm(size, IPC_CREAT | IPC_EXCL | 0666);//已有该共享内存,返回-1;否则创建再返回标识码
}

int GetShm(int size)//获得共享内存标识码
{
    return _CreateShm(size, IPC_CREAT);//已有该共享内存,返回标识码即可;没有则创建则创建
}

int DestroyShm(int shmid)
{
    if((shmctl(shmid, IPC_RMID, NULL)) < 0)
    {
        perror("shmctl");
        return -1;
    }
    return 0;
}       

(2) 创建一个server.c文件,用于创建、销毁共享内存,接收消息,实现代码如下:
include "comm.c"                                                                                                            
#include<unistd.h>

int main()
{   
    int shmid = CreateShm(4096);//创建一个共享内存
    char* addr = shmat(shmid, NULL, 0);//将共享内存段连接到进程的地址空间,成功返回指向共享内存的第一个字节
    sleep(6);//为了能够等到client进程向共享内存放消息,具体等几秒自己把握,如不等,接收的消息会少一点
    int i = 0;
    while(i++ < 26)
    {   
        printf("client# %s\n",addr);
        sleep(1);//这里不等的话,26次循环很快就执行完了,你还没等到client向共享内存里放消息,它就运行完了
    }   
    shmdt(addr);//将共享内存段与当前进程脱离
    sleep(2);
    DestroyShm(shmid);
    return 0;
}
(3)创建一个client.c文件,用于向共享内存里发消息
#include "comm.c"                                                                                                                    
#include<unistd.h>

int main()
{
    int shmid = GetShm(4096);//获得一个共享内存的标识码
    sleep(1);
    char* addr = shmat(shmid, NULL, 0); 
    sleep(2);
    char x = 'A';
    for(x='A'; x<='Z'; x++)//依次发送字母A-Z
    {   
        addr[x-'A'] = x;
        addr[x-'A'+1] = '\0';
        sleep(1);
    }   
    shmdt(addr);
    sleep(2);
    return 0;
}


(4)运行结果如下:
        同样在运行时,要先运行server.c文件,因为它要先创建消息队列,才能实现进程间通信。可以看到,当server打印26次消息后,就会结束此次通信。我们的预想是,server接到client发送的A-Z的26条消息,并依次打印,但在代码中sleep的时间不同,会有不一样的结果,这个得根据自身在运行时的速度以及等待的时间决定。

4. 删除消息队列IPC资源
        之前在另一文章我提到过一句话:IPC资源在用完后必须删除。如以上的代码,若是正常跑完IPC资源会被代码删除,若是我们用ctrl+c终止进程,则代码不能删除IPC资源。这样在下次运行代码时,就会出现以下问题:
        这个问题产生的原因就是IPC资源未删除,所以我们要学两条命令用来删除消息队列的IPC资源:

        删除后,该IPC资源就没有了

二. 信号量
     信号量 主要用于同步与互斥。本质上是一个计数器,里面记录了临界资源的数目。信号量的生命周期也随内核。

1. 进程互斥
(1)由于各进程要求共享资源,而且有些资源需要互斥使用。因此各进程间竞争使用这些资源,进程的这种关系即为进程的互斥;
(2)系统中某些资源一次只能让一个进程使用,这样的资源叫做临界资源或互斥资源;
(3)在进程中涉及到互斥资源的程序段叫做临界区。

2. 进程同步
        进程同步是指多个进程需要相互配合共同完成同一项任务

3. 信号量和P、V原语


(1)信号量和P、V原语由迪杰斯特拉(Dijkstra)提出,信号量值为1的为二元信号量,又称为互斥锁。
(2)信号量
        同步:P、V在不同进程中
        互斥:P、V在同一进程中
(3)信号量值含义:
        S>0:S表示可用资源的数目
        S=0:表示无可用资源,无等待进程
        S<0:ISI表示等待队列中的进程数
(4)信号量结构体伪代码
        信号量本质上其实是一个计数器(整型变量),它维护等待队列。
struct semaphore                                      
{
    int value;
    pointer_PCB queue;
};

(5)P原语
//减1操作                                                                                                                                             
P(s)
{
    s.value = s.value--;
    if(s.value < 0)
    {   
        该进程状态置为等待状态
        将该进程的PCB插入到相应的等待队列s.queue队尾
    }   
}

(6)V原语
//加1操作                                                                                                                                             
V(s)
{
    s.value = s.value++;
    if(s.value >= 0)
    {
        唤醒相应等待队列s.queue中等待的一个进程
        改变其状态为就绪态
        并将其插入到就绪队列
    }
}

注意:P、V原语都是原子操作

4. 信号量集相关函数
        信号量是以多个即集申请的,而不是单个申请。维护一种临界资源需要一个信号量,所以维护多种临界资源就需要多个信号量。多种信号量组成一个信号量集。信号量集可以看做是计数器的个数,信号量值可看作计数器的个数。信号量集是以数组形式进行组织的,以下标来提取各个信号,数组元素表示信号量的值即临界资源的数目。

(1)semget函数
        1)函数原型
        2)函数功能:创建和访问一个信号量集
        3)参数
            key:信号集的名字
            nsems:信号集中信号量的个数
            semflg:九个权限标志构成,用法与创建文件的mode模式标志一致
        4)返回值:成功返回一个非负整数即该信号集的标识码;失败返回-1

(2)semctl函数
        1)函数原型
        2)函数功能:控制信号量集
        3)参数:        
            semid:由semget函数返回的信号量集标识码
            semnum:信号量集中信号量的序号
            cmd:将要采取的动作(有三个可取值)
        4)返回值:成功返回0,失败返回-1

(3)semop函数
        1)函数原型
        2)函数功能:创建和访问一个信号量集
        3)参数
            semid:由semget函数返回的信号量的标识码
            sops:是个指向一个结构数值的指针
            nsops:信号量的个数
        4)返回值:成功返回0;失败返回-1
        5)说明:
struct sembuf
{
    short sem_num;//信号量的编号
    short sem_op;//信号量一次PV操作时加减的数值,一般只会用到两个值:
    //一个是“-1”,即P操作,等待信号量变得可用
    //另一个是“+1”,即V操作,发出信号量已经变得可用
    short sem_flg;//默认设为0,另外两个取值是IPC_NOWAIT或SEM_UNDO                                                                     };

5. 程序实现信号量的作用(采用二元信号量来测试)
        创建一个comm.c文件,以封装信号量相关操作的函数,代码如下
//实现信号量的相关操作的函数                                                                                                         
#include "comm.h"

static int _CreateSem(int nsems, int flags)//创建一个信号量
{
    key_t key = ftok(PATHNAME, PROJ_ID);//产生key值
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    int semid = semget(key, nsems, flags);//创建一个信号量
    if(semid < 0)
    {
        perror("semget");
        return -2;
    }
    return semid;
}

int CreateSem(int nsems)//获得一个新的信号量集
{
    return _CreateSem(nsems, IPC_CREAT | IPC_EXCL | 0666);
}

int GetSem(int nsems)//获得一个信号量集的标识码
{
    return _CreateSem(nsems, IPC_CREAT);
}

int InitSem(int semid, int semnum, int initval)//对信号量集进行初始化
{
    union semun _un;
    _un.val = initval;
    if(semctl(semid, semnum, SETVAL, _un ) < 0)//设置信号量集中信号量的计数值
    {
        perror("semctl");
        return -1;
    }
    return 0;
}

static int SemPV(int semid, int who, int op)//PV操作实现
{
    struct sembuf _sf;
    _sf.sem_num = who;//通过信号量的编号确定对哪个信号量进行操作
    _sf.sem_op = op;//信号量一次PV操作时加减的数值
    _sf.sem_flg = 0;
    if(semop(semid, &_sf, 1) < 0)
    {
        perror("semop");
        return -1;
    }
    return 0;
}

int P(int semid, int who)//对信号量进行P操作                                                                                         
{
    return SemPV(semid, who, -1);//减1操作
}

int V(int semid, int who)//对信号量进行V操作
{
    return SemPV(semid, who, +1);//即加1操作
}

int DestroySem(int semid)//销毁信号量集
{
    if(semctl(semid, 0, IPC_RMID) < 0)//删除信号量集中序号为0的信号量
    {
        perror("semctl");
        return -1;
    }
}                           

        它对应的头文件comm.h,代码如下:
#pragma once

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <wait.h>                                                                                                                    
#include <unistd.h>

#define PATHNAME "."
#define PROJ_ID 0x6666

union semun
{
    int val;//SETVAL用的值
    struct semid_ds* buf;//IPC_STAT、IPC_SET用的
    unsigned short* array;//GETALL、SETALL用的数组值
    struct seminfo* _buf;//为IPC_INFO提供的缓存
};

        测试代码如下:
#include "comm.c"                                                                                                                    
int main()
{
    pid_t id = fork();
    if(id == 0)//child
    {
        while(1)
        {
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");
            fflush(stdout);
            usleep(321456);
        }
    }
    else//father
    {
        while(1)
        {
            printf("B");
            fflush(stdout);
            usleep(223456);
            printf("B ");
            fflush(stdout);
            usleep(121456);
        }
        wait(NULL);//不关心子进程的退出状态
    }
    return 0;
}             

        运行结果如下:
        可以看到,程序输出的结果很乱。因为程序中的父子进程都要向显示器打印数据,所以此时显示器就是一个临界资源。我们希望父子进程互斥使用它,即输出AA、BB而不会出现AB混合的情况,这种情况是父子进程在竞争的使用它,我们不知道它在什么时刻就会被会切换进程,造成这样的输出结果。所以我们要让它们互斥的访问以输出我们想要的结果, 这时就可以用到信号量以实现互斥与同步,所以我们修改代码如下:
#include "comm.c"

int main()
{
    int semid = CreateSem(1);//申请信号量为1的信号量集                                                                               
    InitSem(semid, 0 , 1);//将信号量的计数值初始化为1

    pid_t id = fork();
    if(id == 0)//child
    {
        int _semid = GetSem(0);
        while(1)
        {
            P(_semid, 0);
            printf("A");
            fflush(stdout);
            usleep(123456);
            printf("A ");
            fflush(stdout);
            usleep(321456);
            V(_semid, 0);
        }
    }
    else//father
    {
        while(1)
        {
            P(semid, 0);
            printf("B");
            fflush(stdout);
            usleep(223456);
            printf("B ");
            fflush(stdout);
            usleep(121456);
            V(semid, 0);
        }
        wait(NULL);//不关心子进程的退出状态
    }
    DestroySem(semid);
    return 0;
}                                

        此时,运行结果如下:
        其实,我们只是在原有代码的基础上,对父子进程的每一次打印加了一个信号量的PV操作。当子进程开始打印时先进行P操作,将资源减去1,此时没有资源了,父进程只能等待,知道子进程打印一次之后进行V操作,此时资源数目为1。父进程也是同样的原理,我们不能控制让谁申请到临界资源,但是我们可以保证在当前进程使用资源时,不被其他进程切换进来,从而造成的数据不正确。

6. 信号量资源的释放

        以上的代码,因为我们设置的是死循环,所以代码不能执行到删除创建的信号量资源就被我们终止。这样在下次运行时会出现以下情况:
        所以我们要学两条命令手动删除信号量资源:
         再说一遍:IPC资源必须删除,否则不会自动清除,除非重启,所以System V IPC资源随内核。


猜你喜欢

转载自blog.csdn.net/Lycorisradiata__/article/details/80050848
今日推荐