1)Linux和所有的UNIX操作系统都允许通过共享内存在应用程序之间共享存储空间.

2)有两类基本的API函数用于在进程间共享内存:System v和POSIX.  (当然,还有mmap,属于POSIX的)

3)这两类函数上使用相同的原则,核心思想就是任何要被共享的内存都必须经过显示的分配.

4)因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率.

5)内核没有对访问共享内存进行同步,所以必须提供自己的同步措施,比如数据在写入之前,不允许其它进程对其进行读写.可以用wait来解决这个问题.

 

二)POSIX共享内存API

1)函数shm_open和shm_unlink非常类似于为普通文件所提供的open和unlink系统调用.

  2)如果要编写一个可移植的程序,那么shm_open和shm_unlink是最好的选择.

  3)shm_open:创建一个新的共享区域或者附加在已有的共享区域上.区域被其名字标识,函数返回各文件的描述符.

  4)shm_unlink:类似于unlink系统调用对文件进行操作,直到所有的进程不再引用该内存区后才对其进行释放.

  5)mmap:用于将一个文件映射到某一内存区中,其中也使用了shm_open函数返回的文件描述符.

  6)munmap:用于释放mmap所映射的内存区域.

  7)msync:同步存取一个映射区域并将高速缓存的数据回写到物理内存中,以便其他进程可以监听这些改变.

 



#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>

void error_out(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}

int main (int argc, char *argv[])
{
int r;
const char *memname = "/mymem";
const size_t region_size = sysconf(_SC_PAGE_SIZE);
int fd = shm_open(memname, O_CREAT|O_TRUNC|O_RDWR, 0666);
if (fd == -1)
error_out("shm_open");
r = ftruncate(fd, region_size);
if (r != 0)
error_out("ftruncate");
void *ptr = mmap(0, region_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
error_out("MMAP");
close(fd);
pid_t pid = fork();
if (pid == 0)
{
u_long *d = (u_long *)ptr;
*d = 0xdeadbeef;
exit(0);
}
else
{
int status;
waitpid(pid, &status, 0);
printf("child wrote %#lx\n", *(u_long *)ptr);
}
sleep(50);
r = munmap(ptr, region_size);
if (r != 0)
error_out("munmap");
r = shm_unlink(memname);
if (r != 0)
error_out("shm_unlink");
return 0;
}


编译:

  gcc -o postix-shm postix-shm.c -lrt

  ./postix-shm

  child wrote 0xdeadbeef

  等50秒后,程序退出.

 

-l表示链接指定库

rt应该是库名  

POSIX.1b Realtime Extensions library

 

程序分析:

  1)程序执行shm_open函数创建了共享内存区域,此时会在/dev/shm/创建mymem文件.

  2)通过ftruncate函数改变shm_open创建共享内存的大小为页大小(sysconf(_SC_PAGE_SIZE)),如果不执行ftruncate函数的话,会报Bus error的错误. (其实大小指定成多少都可以,1024也行,2048也行(page size的倍数?),但是一定要用ftruncate来将文件改成指定的大小,后面mmap要用的)



函数说明:ftruncate()会将参数fd指定的文件大小改为参数length指定的大小。参数fd为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件件大小比参数length大,则超过的部分会被删去
返 回 值:0、-1
错误原因:errno
EBADF 参数fd文件描述词为无效的或该文件已关闭
EINVAL 参数fd为一socket并非文件,或是该文件并非以写入模式打开


 

  3)通过mmap函数将创建的mymem文件映射到内存.

  4)通过fork派生出子进程,而共享区域映射通过fork调用而被继承.

  5)程序通过wait系统调用来保持父进程与子进程的同步.

  6)在非父子进程也可以通过共享内存区域的方式进行通讯.

 

Linux共享内存的实现依赖于共享内存文件系统,该文件系统通常装载在/dev/shm,在调用shm_open系统函数的时候,会在/dev/shm/目录下生成mymem文件.

而后程序调用shm_unlink删除mymem,这里如果卸载掉/dev/shm挂载点会怎么样呢?



查看分区信息

  df -h

  Filesystem Size Used Avail Use% Mounted on

  /dev/sda1 19G 973M 17G 6% /

  tmpfs 253M 0 253M 0% /lib/init/rw

  udev 10M 88K 10M 1% /dev

  tmpfs 253M 0 253M 0% /dev/shm

  卸载/dev/shm

  umount /dev/shm/

  ./posix-shm &

  child wrote 0xdeadbeef

  [1] 15476

  ls -l /dev/shm/mymem

  -rw-r--r-- 1 root root 4096 2010-10-26 14:25 /dev/shm/mymem

  我们看到shm_open只是在/dev/shm下创建文件.而不管/dev/shm是否是用tmpfs类型挂载的分区.

如果删除/dev/shm呢?

  rmdir /dev/shm

  再次执行posix-shm

  ./posix-shm &

  child wrote 0xdeadbeef

  此时程序找不到/dev/shm,而在/dev/目录下建立共享内存文件

  ls -l /dev/mymem

  -rw-r--r-- 1 root root 4096 2010-10-26 14:29 /dev/mymem


 

三)System V共享内存 API

1)System V API广泛应用于X windows系统及其扩展版本中,许多X应用程序也使用它.

  2)shmget:创建一个新的共享区域或者附加在已有的共享区域上(同shm_open).

  3)shmat:用于将一个文件映射到内存区域中(同mmap).

  4)shmdt:用于释放所映射的内存区域(同munmap)

  5)shmctl:对于多个用户,断开其对共享区域的连接(同shm_unlink)

 



#include <stdio.h>

  #include <string.h>

  #include <stdlib.h>

  #include <unistd.h>

  #include <sys/ipc.h>

  #include <sys/shm.h>

  #include <sys/wait.h>

  void error_out(const char *msg)

  {

  perror(msg);

  exit(EXIT_FAILURE);

  }

  int main (int argc, char *argv[])

  {

  key_t mykey = 12345678;

  const size_t region_size = sysconf(_SC_PAGE_SIZE);

  int smid = shmget(mykey, region_size, IPC_CREAT|0666);

  if(smid == -1)

  error_out("shmget");

  void *ptr;

  ptr = shmat(smid, NULL, 0);

  if (ptr == (void *) -1)

  error_out("shmat");

  pid_t pid = fork();

  if (pid == 0){

  u_long *d = (u_long *)ptr;

  *d = 0xdeadbeef;

  exit(0);

  }

  else{

  int status;

  waitpid(pid, &status, 0);

  printf("child wrote %#lx/n", *(u_long *)ptr);

  }

  sleep(30);

  int r = shmdt(ptr);

  if (r == -1)

  error_out("shmdt");

  r = shmctl(smid, IPC_RMID, NULL);

  if (r == -1)

  error_out("shmdt");

  return 0;

  }


gcc sysv-shm.c -o sysv-shm -lrt

  ./sysv-shm

  child wrote 0xdeadbeef

 

 

 程序分析:

  1)shmget函数使用的key_t变量在功能上等价于shm_open使用的文件名,由shmget返回的smid在功能上等价于shm_open返回的文件描述符.

  2)不同于POSIX API所创建的内存区,System V API创建的内存区在任何文件系统中都是不可见的.

  3)可以用ipcs管理System V API共享内存.

 

 

共享内存的内部实现

 

参考 ​​javascript:void(0)​

 

每一个新创建的共享内存对象都用一个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;        

        unsigned long shm_npages; 

        unsigned long *shm_pages;   

        struct vm_area_struct *attaches; 

    };

(A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE)

 

shmid_ds是一个数据结构,它描述了这个共享内存区的认证信息,字节大小,最后一次粘附时间、分离时间、改变时间,创建该共享区域的进程,最后一次对它操作的进程,当前有多少个进程在使用它等信息。

 

创建时可以指定共享内存在它的虚拟地址空间的位置,也可以让Linux自己为它选择一块足够的空闲区域。

如下: 

system v和posix的共享内存对比 & 共享内存位置_#include

 

不管上面哪种IPC(System V或者Posix),在Linux上面都是Linux提供的系统调用来实现的。

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,则总是会创建一个新的共享内存对象。

 但是  (The name choice IPC_PRIVATE was perhaps unfortunate, IPC_NEW would more clearly show its function)

    * 算出size要占用的页数,检查其合法性。

    * 申请一块内存用于建立shmid_kernel数据结构,注意这里申请的内存区域大小不包括真正的共享内存区,实际上,要等到第一个进程试图访问它的时候才真正创建共享内存区。

    * 根据该共享内存区所占用的页数,为其申请一块空间用于建立页表(每页4个字节),将页表清0。

    * 搜索向量表shm_segs,为新创建的共享内存对象找一个空位置。

    * 填写shmid_kernel数据结构,将其加入到向量表shm_segs中为其找到的空位置。

    * 返回该共享内存对象的引用标识符。

 

    2) 在向量表shm_segs中查找键值为key的共享内存对象,结果有三:

    * 如果没有找到,而且在操作标志shmflg中没有指明要创建新共享内存,则错误返回,否则创建一个新的共享内存对象。

    * 如果找到了,但该次操作要求必须创建一个键值为key的新对象,那么错误返回。

    * 否则,合法性、认证检查,如有错,则错误返回;否则,返回该内存对象的引用标识符。

 

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

    参见include/linux/shm.h

 

 

2. 关联。

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

 

其定义如下:   

       #include <sys/types.h>

       #include <sys/shm.h>

       void *shmat(int shmid, const void *shmaddr, int shmflg);

 

 

其中:

     shmid是shmget返回的共享内存对象的引用标识符;

    shmaddr用来指定该共享内存区域在进程的虚拟地址空间对应的虚拟地址;

    shmflg是映射标志;

    返回的是在进程中的虚拟地址

 

    该函数所做的工作如下:

    1) 根据shmid找到共享内存对象。

    2) 如果shmaddr为0,即用户没有指定该共享内存区域在它的虚拟空间中的位置,则由系统在进程的虚拟地址空间中为其找一块区域(从1G开始);否则,就用shmaddr作为映射的虚拟地址。(具体的位置,后面再讨论。也要结合进程空间分布来看。)

  (If  shmaddr  is NULL, the system chooses a suitable (unused) address a他 which to attach the segment)

    3) 检查虚拟地址的合法性(不能超过进程的最大虚拟空间大小—3G,不能太接近堆栈栈顶)。

    4) 认证检查。

    5) 申请一块内存用于建立数据结构vm_area_struct,填写该结构。

    6) 检查该内存区域,将其加入到进程的mm结构和该共享内存对象的vm_area_struct队列中。

 

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

 

非常重要,shmat的时候也没有创建真正的共享内存页。

 

当进程第一次访问共享虚拟内存的某页时,因为所有的共享内存页还都没有分配,所以会发生一个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时,发现共享页已经存在,它只需把这里的页表项填到进程页表的相应位置即可,而不需要重新创建物理页。所以,是第一个访问共享内存页的进程使得这一页被创建,而随后访问它的其它进程仅把此页加到它们的虚拟地址空间。

 

(注:我的理解:因为共享内存访问,都是对于内存位置的实际访问。所以在访问的时候,直接去访问进程空间中的那部分地址,是通过页表来映射的,如果页表没有这个位置的映射,就会触发page fault异常,交给系统处理;而系统就会调用到上面的流程)(具体的,还要学习中断与异常等内容)

 

 

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

 

关于 vm_area_struct



这个新的vm_area_struct结构是维系共享内存和使用它的进程之间的关系的,所以除了要关联进程信息外,还要指明这个共享内存数据结构shmid_kernel所在位置; 另外,便于管理这些经常变化的vm_area_struct,所以采取了链表形式组织这些数据结构,链表由attaches指向,同时 vm_area_struct数据结构中专门提供了两个指针:vm_next_shared和 vm_prev_shared,用于连接该共享区域在使用它的各进程中所对应的vm_area_struct数据结构。


 

分离时,物理内存与交换磁盘

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

 

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()");


 

连接共享内存



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

参数说明

shmid:共享内存的关键字

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

shmflg:制定特殊的标志位。
第三个参数如果在flag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写的方式连接此段

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);


例子:



int r = shmdt(ptr);

  if (r == -1)

  error_out("shmdt");

  r = shmctl(smid, IPC_RMID, NULL);

  if (r == -1)

  error_out("shmdt");


 

关于 shmctl

shmctl函数原型


shmctl(共享内存管理)



所需头文件



#include <sys/types.h>

#include <sys/shm.h>



函数说明



完成对共享内存的控制



函数原型



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



函数传入值



shmid



共享内存标识符



cmd



IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中



IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内



IPC_RMID:删除这片共享内存



buf



共享内存管理结构体。具体说明参见共享内存内核结构定义部分



函数返回值



成功:0



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



错误代码



EACCESS:参数cmd为IPC_STAT,确无权限读取该共享内存

EFAULT:参数buf指向无效的内存地址

EIDRM:标识符为shmid的共享内存已被删除

EINVAL:无效的参数cmd或shmid

EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行


 

注意,其中除了删除的 IPC_RMID,还有 IPC_STAT和IPC_SET,这时候会用到第三个参数 buf,来复制或者设置 shmid_ds结构。

而这个结构如下:

struct shmid_ds{

      struct ipc_perm shm_perm;/* 操作权限*/

       int shm_segsz;                    /*段的大小(以字节为单位)*/

      time_t shm_atime;          /*最后一个进程附加到该段的时间*/

       time_t shm_dtime;          /*最后一个进程离开该段的时间*/

      time_t shm_ctime;          /*最后一个进程修改该段的时间*/

      unsigned short shm_cpid;   /*创建该段进程的pid*/

       unsigned short shm_lpid;   /*在该段上操作的最后1个进程的pid*/

       short shm_nattch;          /*当前附加到该段的进程的个数*/

/*下面是私有的*/

        unsigned short shm_npages;  /*段的大小(以页为单位)*/

      unsigned long *shm_pages;   /*指向frames->SHMMAX的指针数组*/

      struct vm_area_struct *attaches; /*对共享段的描述*/

};