linux 进程间的通信(五) 共享内存-2

 
共享内存(Shared Memory)

共享内存区域是被多个进程共享的一部分物理内存。如果多个进程都把该内存区域映射到自己的虚拟地址空间,则这些进程就都可以直接访问该 共享内存区域,从而 可以通过该区域进行通信。 共享内存是进程间共享数据的一种最快的方法,一个进程向 共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中 的内容。这块共享虚拟内存的页面,出现在每一个共享该页面的进程的页表中。但是它不需要在所有进程的虚拟内存中都有相同的虚拟地址。

 

linux 进程间的通信(五) 共享内存-2_第1张图片



共享内存映射图

象所有的 System V IPC对象一样,对于 共享内存对象的访问由key控制,并要进行访问权限检查。内存共享之后,对进程如何使用这块内存就不再做检查。它们必须依赖于其它机 制,比如System V的信号灯来同步对于 共享内存区域的访问。

每一个新创建的 共享内存对象都用一个shmid_kernel数据结构来表达。系统中所有的shmid_kernel数据结构都保存在shm_segs向 量表中,该向量表的每一个元素都是一个指向shmid_kernel数据结构的指针。shm_segs向量表的定义如下:
struct shmid_kernel *shm_segs[SHMMNI];

SHMMNI为128,表示系统中最多可以有128个 共享内存对象。

数据结构shmid_kernel的定义如下:
struct shmid_kernel
{   
struct shmid_ds u;         /* the following are private */
unsigned long shm_npages;  /* size of segment (pages) */
unsigned long *shm_pages;  /* array of ptrs to frames -> SHMMAX */
struct vm_area_struct *attaches;  /* descriptors for attaches */
};

其中:shm_pages是该 共享内存对象的页表,每个 共享内存对象一个,它描述了如何把该 共享内存区域映射到进程的地址空间的信息。
shm_npages是该 共享内存区域的大小,以页为单位。
shmid_ds是一个数据结构,它描述了这个 共享内存区的认证信息,字节大小,最后一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次 对它操作的进程,当前有多少个进程在使用它等信息。其定义如下:
struct shmid_ds {
struct ipc_perm shm_perm;   /* operation perms */
int shm_segsz;              /* size of segment (bytes) */
__kernel_time_t shm_atime;  /* last attach time */
__kernel_time_t shm_dtime;  /* last detach time */
__kernel_time_t shm_ctime;  /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch;   /* no. of current attaches */
unsigned short shm_unused;   /* compatibility */
void *shm_unused2;           /* ditto - used by DIPC */
void *shm_unused3;           /* unused */
};

attaches描述被共享的物理内存对象所映射的各进程的虚拟内存区域。每一个希望共享这块内存的进程都必须通过系统调用将其粘附(attach)到它 的虚拟内存中。这一过程将为该进程创建了一个新的描述这块 共享内存的vm_area_struct数据结构。进程可以选择 共享内存在它的虚拟地址空间的位 置,也可以让 Linux为它选择一块足够的空闲区域。

这个新的vm_area_struct结构除了要连接到进程的内存结构mm中以外,还被连接到 共享内存数据结构shmid_kernel的一个链表中,该 结构中的attaches指针指向该链表。vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和 vm_prev_shared,用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。其实,虚拟内存并没有在粘附的时候 创建,而要等到第一个进程试图访问它的时候才创建。


linux 进程间的通信(五) 共享内存-2_第2张图片
图 System V IPC 机制 - 共享内存

Linux
共享内存提供了四种操作。
1. 共享内存对象的创建或获得。与其它两种IPC机制一样,进程在使用 共享内存区域以前,必须通过系统调用sys_ipc (call值为SHMGET)创建一个键值为key的 共享内存对象,或获得已经存在的键值为key的某 共享内存对象的引用标识符。以后对 共享内存对象的访 问都通过该引用标识符进行。对 共享内存对象的创建或获得由函数sys_shmget完成,其定义如下:
int sys_shmget (key_t key, int size, int shmflg)

这里key是表示该 共享内存对象的键值,size是该 共享内存区域的大小(以字节为单位),shmflg是标志(对该 共享内存对象的特殊要求)。

它所做的工作如下:
1) 如果key == IPC_PRIVATE,则创建一个新的 共享内存对象。
* 算出size对应的页数,检查其合法性。
* 搜索向量表shm_segs,为新创建的 共享内存对象找一个空位置。
* 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区域大小不包括真正的 共享内存区,实际上,要等到第一个进程试图访问它的时候 才真正创建 共享内存区。
* 根据该 共享内存区所占用的页数,为其申请一块空间用于建立页表(每页4个字节),将页表清0。
* 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的空位置。
* 返回该 共享内存对象的引用标识符。

2) 在向量表shm_segs中查找键值为key的 共享内存对象,结果有三:
* 如果没有找到,而且在操作标志shmflg中没有指明要创建新 共享内存,则错误返回,否则创建一个新的 共享内存对象。
* 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么错误返回。
* 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对象的引用标识符。

共享内存对象的创建者可以控制对于这块内存的访问权限和它的key是公开还是私有。如果有足够的权限,它也可以把 共享内存锁定在物理内存中。
参见include/ linux/shm.h

2. 粘附。在创建或获得某个 共享内存区域的引用标识符后,还必须将 共享内存区域映射(粘附)到进程的虚拟地址空间,然后才能使用该 共享内存区域。系统调用 sys_ipc(call值为SHMAT)用于 共享内存区到进程虚拟地址空间的映射,而真正完成粘附动作的是函数sys_shmat,其定义如下:
int sys_shmat (int shmid, char *shmaddr, int shmflg, ulong *raddr)

其中:shmid是 共享内存对象的引用标识符;
shmaddr该 共享内存区域在进程的虚拟地址空间对应的虚拟地址;
shmflg是映射标志;
raddr是实际映射的虚拟空间地址。

该函数所做的工作如下:
1) 根据shmid找到 共享内存对象。
2) 如果shmaddr为0,即用户没有指定该 共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则,就 用shmaddr作为映射的虚拟地址。
3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小—3G,不能太接近堆栈栈顶)。
4) 认证检查。
5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。
6) 检查该内存区域,将其加入到进程的mm结构和该 共享内存对象的vm_area_struct队列中。

共享内存的粘附只是创建一个vm_area_struct数据结构,并将其加入到相应的队列中,此时并没有创建真正的 共享内存页。

当进程第一次访问共享虚拟内存的某页时,因为所有的 共享内存页还都没有分配,所以会发生一个page fault异常。当 Linux处理这个page fault的时候,它找到发生异常的虚拟地址所在的vm_area_struct数据结构。在该数据结构中包含有这类共享虚拟内存的一组处理例程,其中的 nopage操作用来处理虚拟页对应的物理页不存在的情况。对 共享内存,该操作是shm_nopage(定义在ipc/shm.c中)。该操作在描述这个 共享内存的shmid_kernel数据结构的页表shm_pages中查找发生page fault异常的虚拟地址所对应的页表条目,看共享页是否存在(页表条目为0,表示共享页是第一次使用)。如果不存在,它就分配一个物理页,并为它创建一 个页表条目。这个条目不但进入当前进程的页表,同时也存到shmid_kernel数据结构的页表shm_pages中。

当下一个进程试图访问这块内存并得到一个page fault的时候,经过同样的路径,也会走到函数shm_nopage。此时,该函数查看shmid_kernel数据结构的页表shm_pages时, 发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问 共享内存页的进程使得这一页被创建, 而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。

3. 分离。当进程不再需要共享虚拟内存的时候,它们与之分离(detach)。只要仍旧有其它进程在使用这块内存,这种分离就只会影响当前的进程,而不会影响 其它进程。当前进程的vm_area_struct数据结构被从shmid_ds中删除,并被释放。当前进程的页表也被更新, 共享内存对应的虚拟内存页被 标记为无效。当共享这块内存的最后一个进程与之分离时, 共享内存页被释放,同时,这块 共享内存的shmid_kernel数据结构也被释放。

系统调用sys_ipc(call值为SHMDT)用于 共享内存区与进程虚拟地址空间的分离,而真正完成分离动作的是函数sys_shmdt,其定义如 下:
int sys_shmdt (char *shmaddr)

其中shmaddr是进程要分离的共享页的开始虚拟地址。

该函数搜索进程的内存结构中的所有vm_area_struct数据结构,找到地址shmaddr对应的一个,调用函数do_munmap将其释放。

在函数do_munmap中,将要释放的vm_area_struct数据结构从进程的虚拟内存中摘下,清除它在进程页表中对应的页表项(可能占多个页表 项),调用该 共享内存数据结构vm_area_struct的操作例程中的close操作(此处为shm_close)做进一步的处理。

在函数shm_close(定义在ipc/shm.c中)中,找到该 共享内存对象在向量表shm_segs中的索引,从而找到该 共享内存对象,将该共享内 存在当前进程中对应的vm_area_struct数据结构从对象的 共享内存区域链表(由vm_next_share和vm_pprev_share指针 连接)中摘下。如果目前与该 共享内存对象粘附的进程数变成了0,则释放 共享内存页,释放 共享内存页表,释放该对象的shmid_kernel数据结构,将 向量表shm_segs中该 共享内存对象所占用的项改为IPC_UNUSED。

如果共享的虚拟内存没有被锁定在物理内存中,分离会更加复杂。因为在这种情况下, 共享内存的页可能在系统大量使用内存的时候被交换到系统的交换磁盘。为了 避免这种情况,可以通过下面的控制操作,将某 共享内存页锁定在物理内存不允许向外交换。 共享内存的换出和换入,已在第3章中讨论。

4. 控制。 Linux共享内存上实现的第四种操作是 共享内存的控制(call值为SHMCTL的sys_ipc调用),它由函数sys_shmctl实现。 控制操作包括获得 共享内存对象的状态,设置 共享内存对象的参数(如uid、gid、mode、ctime等),将 共享内存对象在内存中锁定和释放(在对象 的mode上增加或去除SHM_LOCKED标志),释放 共享内存对象资源等。

共享内存提供了一种快速灵活的机制,它允许进程之间直接共享大量的数据,而无须使用拷贝或系统调用。 共享内存的主要局限性是它不能提供同步,如果两个进程 企图修改相同的 共享内存区域,由于内核不能串行化这些动作,因此写的数据可能任意地互相混合。所以使用 共享内存的进程必须设计它们自己的同步协议,如用信 号灯等。

以下是使用共享内存机制进行进程间通信的基本操作:

需要包含的头文件:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

1.创建共享内存

 int shmget(key_t key,int size,int shmflg);

参数说明:

key:用来表示新建或者已经存在的共享内存去的关键字。

size:创建共享内存的大小。

shmflg:可以指定的特殊标志。IPC_CREATE,IPC_EXCL以及低九位的权限。

eg:

int shmid;

shmid=shmget(IPC_PRIVATE,4096,IPC_CREATE|IPC_EXCL|0660);

if(shmid==-1)

perror("shmget()");

 

2.连接共享内存

char *shmat(int shmid,char *shmaddr,int shmflg);

参数说明:

shmid:共享内存的关键字

shmaddr:指定共享内存出现在进程内存地址的什么位置,通常我们让内核自己决定一个合适的地址位置,用的时候设为0。

shmflg:制定特殊的标志位。

eg:

int shmid;

char *shmp;

shmp=shmat(shmid,0,0);

if(shmp==(char *)(-1))

perror("shmat()\n");

3.使用共享内存

在使用共享内存是需要注意的是,为防止内存访问冲突,我们一般与信号量结合使用。

4.分离共享内存:当程序不再需要共享内后,我们需要将共享内存分离以便对其进行释放,分离共享内存的函数原形如下:

int shmdt(char *shmaddr);

5.

释放共享内存

int shmctl(int shmid,int cmd,struct shmid_ds *buf);

 

/*********************程序相关信息*********************
程序编号:012
程序编写起始日期:2008.11.1
程序编写完成日期:2008.11.1
程序修改日期:                                   修改备注:
程序目的:学习linux消息队列通信
所用主要函数:msgget(),msgsnd(),msgrcv(),msgctl()
程序存疑:
程序完成地点: 宿舍内
*********************程序相关信息*********************/
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
    int pid,msqid;//后者为消息队列识别代号
    struct msgbuf
    {
        long mtype;//消息类型
        char mtext[20];//消息内容
    }send_buf,receive_buf;
    if((msqid=msgget(IPC_PRIVATE,0700))<0)//建立消息队列
    {
        printf("msgget建立消息队列失败。\n");
        exit(1);
    }
    else
        printf("msgget建立消息队列成功,该消息队列识别代号为%d。\n",msqid);
    if((pid=fork())<0)
    {
        printf("fork()函数调用失败!\n");
        exit(2);
    }
    else if(pid>0)//父进程,发送消息到消息队列
    {
        send_buf.mtype=1;
        strcpy(send_buf.mtext,"My test information");
        printf("发送到消息队列的信息内容为:%s\n",send_buf.mtext);
        if(msgsnd(msqid,&send_buf,20,IPC_NOWAIT)<0)//发送send_buf中的信息到msqid对应的消息队列
        {
            printf("msgsnd消息发送失败。\n");
            exit(3);
        }
        else
            printf("msgsnd消息发送成功。\n");
        sleep(2);
        exit(0);
    }
    else//子进程,从消息队列中接收消息]
    {
        sleep(2);//等待父进程发送消息完成
        int infolen;//读到的信息数据长度
        if((infolen=msgrcv(msqid,&receive_buf,20,0,IPC_NOWAIT))<0)//自消息队列接收信息
        {
            printf("msgrcv读取信息错误。\n");
            exit(4);
        }
        else
            printf("msgrcv读取信息成功。\n");
        printf("自消息队列读取到的内容为%s,共读取%d个字节。\n",receive_buf.mtext,infolen);
        if((msgctl(msqid,IPC_RMID,NULL))<0)//删除msqid对应的消息队列
        {
            printf("msgctl函数调用出现错误。\n");
            exit(5);
        }
        else
        {
            printf("识别代号为%d的消息队列已经被成功删除。\n",msqid);
            exit(0);
        }
    }
}
/*********************程序运行结果*********************
[root@localhost temp]# ./msg
msgget建立消息队列成功,该消息队列识别代号为98304。
发送到消息队列的信息内容为:My test information
msgsnd消息发送成功。
msgrcv读取信息成功。
自消息队列读取到的内容为My test information,共读取20个字节。
识别代号为98304的消息队列已经被成功删除。
***********************************************************/

/*********************程序相关信息*********************
程序编号:013
程序编写起始日期:2008.11.1
程序编写完成日期:2008.11.1
程序修改日期:                                   修改备注:
程序目的:学习linux共享内存
所用主要函数:shmget(),shmat(),shmctl(),shmdt()
程序存疑:
程序完成地点: 宿舍内
*********************程序相关信息*********************/
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
int main()
{
    int pid,shmid;//后者为共享内存识别代号
    char *write_address;
    char *read_address;
    struct shmid_ds dsbuf;
    if((shmid=shmget(IPC_PRIVATE,32,0))<0)//分配共享内存
    {
        printf("shmid共享内存分配出现错误。\n");
        exit(1);
    }
    else
        printf("shmid共享内存分配成功,共享内存识别代号为:%d。\n",shmid);
    if((pid=fork())<0)
    {
        printf("fork函数调用出现错误!\n");
        exit(2);
    }
    else if(pid>0)//父进程,向共享内存中写入数据
    {
        printf("父进程的ID是:%d\n",getpid());
        write_address=(char *)shmat(shmid,NULL,0);//连接共享内存
        if((int)write_address==-1)
        {
            printf("shmat连接共享内存错误。\n");
            exit(3);
        }
        else
        {
            printf("shmat连接共享内存成功。\n");
            strcpy(write_address,"我是写入共享内存的测试数据");//将数据写入共享内存
            printf("写入共享内存的信息为“%s”。\n",write_address);
            if((shmdt((void *)write_address))<0)//断开与共享内存的连接
                printf("shmdt共享内存断开错误。\n");
            else
                printf("shmdt共享内存断开成功。\n");
            sleep(2);
            return;
        }
    }
    else//子进程,从共享内存中读取数据
    {
        sleep(2);//等待父进程写入共享内存完毕
        printf("子进程ID是:%d\n",getpid());
        if((shmctl(shmid,IPC_STAT,&dsbuf))<0)
        {
            printf("shmctl获取共享内存数据结构出现错误。\n");
            exit(4);
        }
        else
        {
            printf("shmctl获取共享内存数据结构成功。\n建立这个共享内存的进程ID是:%d\n",dsbuf.shm_cpid);
            printf("该共享内存的大小为:%d\n",dsbuf.shm_segsz);
            if((read_address=(char *)shmat(shmid,0,0))<0)//连接共享内存
            {
                printf("shmat连接共享内存出现错误。\n");
                exit(5);
            }
            else
            {
                printf("自共享内存中读取的信息为:“%s”。\n",read_address);
                printf("最后一个操作该共享内存的进程ID是:%d\n",dsbuf.shm_lpid);
                if((shmdt((void *)read_address))<0)//断开与共享内存的连接
                {
                    printf("shmdt共享内存断开错误。\n");
                    exit(6);
                }
                else
                    printf("shmdt共享内存断开成功。\n");
                if(shmctl(shmid,IPC_RMID,NULL)<0)//删除共享内存及其数据结构
                {
                    printf("shmctl删除共享内存及其数据结构出现错误。\n");
                    exit(7);
                }
                else
                    printf("shmctl删除共享内存及其数据结构成功。\n");
                exit(0);
            }
        }   
    }
}
/*********************程序运行结果*********************
[root@localhost temp]# ./shm
shmid共享内存分配成功,共享内存识别代号为:1703947。
父进程的ID是:7647
shmat连接共享内存成功。
写入共享内存的信息为“我是写入共享内存的测试数据”。
shmdt共享内存断开成功。
子进程ID是:7648
shmctl获取共享内存数据结构成功。
建立这个共享内存的进程ID是:7647
该共享内存的大小为:32
自共享内存中读取的信息为:“我是写入共享内存的测试数据”。
最后一个操作该共享内存的进程ID是:7647
shmdt共享内存断开成功。
shmctl删除共享内存及其数据结构成功。
***********************************************************/

你可能感兴趣的:(数据结构,linux,struct,测试,null,System)