进程间通信(3)——共享内存和信号量

时间:2021-10-29 14:44:36

【4】共享内存

(1)概述

  • 两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间,进程A可以即时看到进程B对共享内存中数据的更新,反之,进程B也可以即时看到进程A对共享内存中数据的更新。

  • 共享内存是存在于内核级别的一种资源

  • 在系统内核为一个进程分配内存地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存同时分配给不同的进程。共享内存机制就是通过该原理来实现的,共享内存机制只是提供数据的传送,如何控制服务器端和客户端的读和写操作互斥,这就需要一些其他的辅助工具,例如信号量的概念。

  • 共享内存可以说是Linux下最快速、最有效的进程间通信方式

  • 因为进程可以直接读写内存,而不需要任何数据的拷贝
  • 管道和消息队列等通信方式,需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件
  • 进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的
  • 共享内存的不足之处:由于多个进程对同一块内存区域具有访问的权限,各个进程间的同步问题尤为重要。必须控制同一时刻只有一个进程对共享内存区域写入数据,否则将造成数据的混乱。同步控制问题可以通过下一节介绍的信号量来解决。

(2)共享内存相关操作

用于Linux进程通信共享内存。共享内存函数由shmget、shmat、shmdt、shmctl四个函数组成。

shmat函数原型

  • shmat(把共享内存区对象映射到调用进程的地址空间)

  • 所需头文件

    #include <sys/types.h>
    #include <sys/shm.h>
  • 函数说明

    连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问
  • 函数原型

    void *shmat(int shmid, const void *shmaddr, int shmflg)
  • 函数传入值

    shmid   共享内存标识符
    shmaddr  指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
    shmflg   SHM_RDONLY:为只读模式,其他为读写模式
  • 函数返回值

    成功:附加好的共享内存地址
    出错:-1,错误原因存于errno中

shmget()

  • 创建或打开共享内存的函数

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    int shmget(key_t key,int size,int flag);

返回值:成功返回共享内存ID,出错返回-1

key:创建或打开的共享内存的键值

size:共享内存区域大小,只在创建一个新的共享内存时生效

flag:调用函数的操作类型,也可用于设置共享内存的访问权限,两者通过逻辑或表示

shmdt函数原型

  • shmdt(断开共享内存连接)

  • 所需头文件

    #include <sys/types.h>
    #include <sys/shm.h>
  • 函数说明

    与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存
  • 函数原型

    int shmdt(const void *shmaddr)
  • 函数传入值

    shmaddr:连接的共享内存的起始地址
  • 函数返回值

    成功:0
    出错:-1,错误原因存于error中

(3)使用shmget函数创建共享内存的例子,create_shm.c:

程序中在调用shmget函数时指定key参数值为IPC_PRIVATE,这个参数的意义是创建一个新的共享内存区,当创建成功后使用shell命令ipcs来显示目前系统下共享内存的状态。命令参数-m为只显示共享内存的状态。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#define BUFSZ 4096
int main(void)
{
        int shm_id;/*共享内存标识符*/
        shm_id = shmget(IPC_PRIVATE,BUFSZ,0666);
        /*创建共享内存*/
        if(shm_id < 0)
        {
                printf("shmget failed!\n");
                exit(1);/*shmget出错退出*/
        }
        printf("create a shared memory segment successfully:%d\n",shm_id);
        system("ipcs -m");/*调用ipcs命令查看IPC*/
        exit(0);
}

运行结果:

hyx@hyx-virtual-machine:~/test$ ./create_shm
create a shared memory segment successfully:2949133

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 65536      hyx        600        524288     2          目标       
0x00000000 1114113    hyx        600        524288     2          目标       
0x00000000 196610     hyx        600        524288     2          目标       
0x00000000 393219     hyx        600        524288     2          目标       
0x00000000 950276     hyx        600        524288     2          目标       
0x00000000 589829     hyx        600        524288     2          目标       
0x00000000 622598     hyx        600        16777216   2                       
0x00000000 655367     hyx        600        16777216   2          目标       
0x00000000 753672     hyx        600        524288     2          目标       
0x00000000 819209     hyx        600        2097152    2          目标       
0x00000000 1146890    hyx        600        1048576    2          目标       
0x00000000 1245195    hyx        600        524288     2          目标       
0x00000000 1933324    hyx        600        67108864   2          目标       
0x00000000 2949133    hyx        666        4096       0                       

执行ipcs,打印共享内存,信号量和消息队列的信息:

hyx@hyx-virtual-machine:~/test$ ipcs

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 65536      hyx        600        524288     2          目标       
0x00000000 1114113    hyx        600        524288     2          目标       
0x00000000 196610     hyx        600        524288     2          目标       
0x00000000 393219     hyx        600        524288     2          目标       
0x00000000 950276     hyx        600        524288     2          目标       
0x00000000 589829     hyx        600        524288     2          目标       
0x00000000 622598     hyx        600        16777216   2                       
0x00000000 655367     hyx        600        16777216   2          目标       
0x00000000 753672     hyx        600        524288     2          目标       
0x00000000 819209     hyx        600        2097152    2          目标       
0x00000000 1146890    hyx        600        1048576    2          目标       
0x00000000 1245195    hyx        600        524288     2          目标       
0x00000000 1933324    hyx        600        67108864   2          目标       
0x00000000 2949133    hyx        666        4096       0                       

--------- 信号量数组 -----------
键        semid      拥有者  权限     nsems     

--------- 消息队列 -----------
键        msqid      拥有者  权限     已用字节数 消息  

执行ipcs -m,只打印共享内存段信息:

hyx@hyx-virtual-machine:~/test$ ipcs -m

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     连接数  状态      
0x00000000 65536      hyx        600        524288     2          目标       
0x00000000 1114113    hyx        600        524288     2          目标       
0x00000000 196610     hyx        600        524288     2          目标       
0x00000000 393219     hyx        600        524288     2          目标       
0x00000000 950276     hyx        600        524288     2          目标       
0x00000000 589829     hyx        600        524288     2          目标       
0x00000000 622598     hyx        600        16777216   2                       
0x00000000 655367     hyx        600        16777216   2          目标       
0x00000000 753672     hyx        600        524288     2          目标       
0x00000000 819209     hyx        600        2097152    2          目标       
0x00000000 1146890    hyx        600        1048576    2          目标       
0x00000000 1245195    hyx        600        524288     2          目标       
0x00000000 1933324    hyx        600        67108864   2          目标       
0x00000000 2949133    hyx        666        4096       0                       

hyx@hyx-virtual-machine:~/test$ 

(4)使用共享内存进行通信的实例

共享内存ID以命令行参数的形式传递给进程。

write_shm.c是写共享内存,即分10次向共享内存中写入people结构体的成员数据,(姓名和年龄)数据的值由for循环自动生成。程序的最后使用shmdt将共享内存段从当前的进程空间中脱离掉。

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct
{
        char name[4];
        int age;
}people;
int main(int argc,char** argv)
{
        int shm_id,i;
        char temp;
        people *p_map;
        if(argc != 2)/*命令行参数错误*/
        {
                printf("USAGE:atshm < d=identifier>");/*打印帮助消息*/
                exit(1);
        }
        shm_id = atoi(argv[1]);/*得到要引入的共享内存段,atoi将字符转换成整型*/
        p_map = (people *)shmat(shm_id,NULL,0);/*shmat:把共享内存区对象映射到调用进程的地址空间*/
        temp = 'a';
        for(i = 0;i < 10;i++)
        {
                temp+=1;
                memcpy((*(p_map+i)).name,&temp,1);
                (*(p_map+i)).age = 20+i;
                /*memcpy:c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标des所指的内存地址的起始位置中,void *memcpy(void *dest, const void *src, size_t n);*/
        }
        if(shmdt(p_map)==-1)
        {
                perror("detach error!\n");
        }
        return 0;
}
                             

read_shm.c是读共享内存,即分10次从共享内存中读出people结构体的成员数据。程序的最后同样使用shmdt将共享内存段从当前的进程空间中脱离掉。

#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct
{
        char name[4];
        int age;
}people;
int main(int argc,char** argv)
{
        int shm_id,i;
        people *p_map;
        if(argc != 2)
        {
                printf("USAGE:atshm <idenfifier>");
                exit(1);
        }
        shm_id = atoi(argv[1]);/*得到要引入的共享内存段,atoi将字符转换成整型*/
        p_map = (people*)shmat(shm_id,NULL,0);/*shmat:把共享内存区对象映射到调用进程的地址空间*/
        for(i=0;i<10;i++)
        {
                printf("name:%s  ",(*(p_map+i)).name);
                printf("age %d\n",(*(p_map+i)).age);
        }
        if(shmdt(p_map)==-1)
        {
                perror("detach error!\n");

        }
       return 0;
}
        

运行结果:

hyx@hyx-virtual-machine:~/test$ ./write_shm 2949133
hyx@hyx-virtual-machine:~/test$ ./read_shm 2949133
name:b  age 20
name:c  age 21
name:d  age 22
name:e  age 23
name:f  age 24
name:g  age 25
name:h  age 26
name:i  age 27
name:j  age 28
name:k  age 29
hyx@hyx-virtual-machine:~/test$ 

【5】信号量

  • 信号量的原理是一种数据操作锁的概念,它本身不具备数据交换的功能,而是通过控制其他的通信资源(如文件、外部设备等)来实现进程间通信。信号量本身不具备数据传输的功能,其只是一种外部资源的标识。

  • 信号量,有时也被称为信号灯,是在多进程环境下使用的一种设施,它负责协调各个进程,以保证它们能够正确、合理的使用公共资源。信号量分为单值和多值两种,前者只能被一个进程获得,后者可以被若干个进程获得。

  • 以停车场为例:假设停车场只有三个车位,一开始三个车位都为空。这时如果同时来了第五辆车,看门人允许其中三辆直接进入,然后放下车栏,剩下的车则必须在入口等待,在此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车栏,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。

  • 车位:公共资源
  • 每辆车:一个进程
  • 看门人:信号量的作用
  • 抽象来讲,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的进程/线程(车辆)都会将该整数减1(通过它是为了使用公共资源),当该整数值为时,所有试图通过它的进程都处于等待状态。在信号量上我们定义两种操作:wait(等待)和releas(释放)。当一个进程调用wait操作时,它要么得到资源探后将信号量减1,要么一直等下去(指放入阻塞队列),直到信号量大于等于1时。release实际上是在信号量上执行加1操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了信号量守护的资源。