Chinaunix首页 | 论坛 | 博客
  • 博客访问: 68877
  • 博文数量: 52
  • 博客积分: 2065
  • 博客等级: 大尉
  • 技术积分: 530
  • 用 户 组: 普通用户
  • 注册时间: 2009-11-03 13:59
文章存档

2011年(1)

2010年(49)

2009年(2)

我的朋友

分类: LINUX

2010-03-20 00:32:12

linux共享内存的设计
 posix的共享内存是通过用户空间挂在的tmpfs文件系统实现的,而system V的共享内存是由内核本身的tmpfs实现的,这里可以看出,二者其实是用同一种机制实现的,不同的是用户接口不同,posix旨在提供一套统一的可用接口而不是实现,因此posix的上层根本就不会在意其机制是内核实现的还是用户空间实现的,因此用文件系统实现再好不过了,在linux上它实际上是通过用户空间挂载的tmpfs实现,system v的共享内存就不一样,它严格由操作系统内核提供接口和实现,因此linux中有关于共享内存的shmget/at等系统调用,虽然它们的底层实现一致,但是system v的共享内存作为内核提供的ipc之一,由内核直接支持,而posix就不一定了,在某些系统上由内核直接实现,而在另一些系统比如linux上由用户空间实现,posix的共享内存严格依赖用户空间tmpfs的挂载,在shm_open中有判断:
int shm_open (const char *name, int oflag, mode_t mode)
{
size_t namelen;
char *fname;
int fd;
__libc_once (once, where_is_shmfs); //找到tmpfs的挂载点
if (mountpoint.dir == NULL)
{
__set_errno (ENOSYS); //如果没有挂载tmpfs,那么就说明它还有被实现,返回错误码
return -1;
}
while (name[0] == '/')
++name;
if (name[0] == '\0')
{
__set_errno (EINVAL);
return -1;
}
namelen = strlen (name);
fname = (char *) alloca (mountpoint.dirlen + namelen + 1);
__mempcpy (__mempcpy (fname, mountpoint.dir, mountpoint.dirlen), name, namelen + 1);
fd = open (fname, oflag | O_NOFOLLOW, mode);
...
return fd;
}
posix的共享内存机制实际上在库过程中以及用户空间的其他部分被展示为完全的文件系统的调用过程,在调用完shm_open之后,需要调用mmap来将tmpfs的文件映射到地址空间,接着就可以操作这个文件了,需要注意的是,别的进程也可以操作这个文件,因此这个文件其实就是共享内存。反观system v的共享内存,内核直接实现了shmget/at系统调用,虽然最终也是靠tmpfs来实现的,但是接口设计上和posix完全不同,posix旨在提供所有系统都一致的接口,而system v只在于实现自己的逻辑,共享内存其实只是sysv中ipc的一部分,最终的管理数据结构也是ipc的而不是共享内存的,比如newseg(ipc/shm.c文件中)函数中的shm_addid函数调用的是ipc_addid(在文件ipc/util.c中),我们看一下这个newseg函数:
static int newseg (key_t key, int shmflg, size_t size)
{
...
} else {
sprintf (name, "SYSV%08x", key);
file = shmem_file_setup(name, size, VM_ACCOUNT); //最终落实到tmpfs
}
...
id = shm_addid(shp); //加入到ipc统一的管理数据结构中
...//设置shp的各个字段的值
return shp->id;
}
注意shmem_file_setup这个调用只是使用了tmpfs的一个实例,从它的实现可以看出有这样一句:file->f_vfsmnt = mntget(shm_mnt);使用了shm_mnt,那么这个shm_mnt是什么时候初始化的呢?在init_tmpfs中有:
shm_mnt = do_kern_mount(tmpfs_fs_type.name, MS_NOUSER, tmpfs_fs_type.name, NULL);
从linux的Document中可以看出,内核中起码要有一个tmpfs的实例,就是shm_mnt,这就是专门为system v的共享内存准备的,和是否编译TMPFS无关,虽然posix也使用了tpfs,但是却不是和内核实现的system v共享内存使用的一样的实例,后者的mnt实例是在用户执行mount tmpfs时创建的,实例虽然不一样但操作还是一样的,就像类实例虽不同,但是类是同一个类,数据不同但是代码相同。具体的tmpfs的file_operations等等就不赘述了。所以可以看出,不管是sysyem v的还是posix的共享内存,在计算机重新启动之后都会不复存在,因为数据室存在物理存储器或者交换分区的,它们不同的是,posix是以用户空间文件实现的,并且用户空间操作的是文件描述符,文件描述符是属于task_struct的,因此进程退出之后共享内存将递减共享计数,如果为0则销毁,因此posix的共享内存虽然使用open来创建或打开共享内存并没有用close而是用unlink来关闭的,作用就是在计数为0的时候销毁它,看看system v的共享内存,虽然也是由文件实现的,但是文件并没有加入到进程的打开文件表,而是作为进程的一种内禀属性被使用的,因此在进程退出时并不关闭文件,只要机器不重启或者该共享内存不显式销毁,那么共享内存将一直存在,进程和文件其实是互不相关的两个概念,只是进程打开的文件才和进程相关,在进程结束的时候只是能保证与之相关的文件被关闭,而别的文件比如sysv的共享内存使用的文件结构没有和进程关联,因此不会被关闭。其实close并不操作什么,而是仅仅刷新缓冲区,然后递减文件的使用者计数:
int filp_close(struct file *filp, fl_owner_t id)
{
int retval = 0;
if (!file_count(filp)) {
printk(KERN_ERR "VFS: Close: file count is 0\n");
return 0;
}
if (filp->f_op && filp->f_op->flush)
retval = filp->f_op->flush(filp);
dnotify_flush(filp, id);
locks_remove_posix(filp, id);
fput(filp); //linux的懒惰特性决定了这个fput操作才是最重要的。
return retval;
}
 
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dog250/archive/2010/02/09/5303709.aspx

2:Linux共享内存编程

引自:http://blog.sina.com.cn/s/blog_494e45fe0100h2x3.html

三、posix共享内存函数

posix共享内存区涉及两个步骤:

1、指定一个名字参数调用shm_open,以创建一个新的共享内存区对象或打开一个以存在的共享内存区对象。

2、调用mmap把这个共享内存区映射到调用进程的地址空间。传递给shm_open的名字参数随后由希望共享该内存区的任何其他进程使用。

 

5

名称:

shm_open

功能:

打开或创建一个共享内存区

头文件:

#include

函数原形:

int shm_open(const char *name,int oflag,mode_t mode);

参数:

name    共享内存区的名字

cflag    标志位

mode    权限位

返回值:

成功返回0,出错返回-1

      

 

 

 

 

 

 oflag参数必须含有O_RDONLYO_RDWR标志,还可以指定如下标志:O_CREAT,O_EXCLO_TRUNC.

       mode参数指定权限位,它指定O_CREAT标志的前提下使用。

shm_open的返回值是一个整数描述字,它随后用作mmap的第五个参数。

 

6

名称:

shm_unlink

功能:

删除一个共享内存区

头文件:

#include

函数原形:

int shm_unlink(const char *name);

参数:

name    共享内存区的名字

返回值:

成功返回0,出错返回-1

     

 

 

 

  shm_unlink函数删除一个共享内存区对象的名字,删除一个名字仅仅防止后续的open,mq_opensem_open调用取得成功。

 

下面是创建一个共享内存区的例子:

 

#include

#include

#include

 

int main(int argc,char **argv)

{

int shm_id;

 

if(argc!=2)

{

    printf(“usage:shm_open \n”);

    exit(1);

}

shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);

printf(“shmid:%d\n”,shm_id);

shm_unlink(argv[1]);

}

       下面是运行结果,注意编译程序我们要加上“-lrt”参数。

#cc –lrt –o shm_open shm_open.c

#./shm_open test

shm_id:3

 

 

四、ftruncatefstat函数

普通文件或共享内存区对象的大小都可以通过调用ftruncate修改。

7

名称:

ftruncate

功能:

调整文件或共享内存区大小

头文件:

#include

函数原形:

int ftruncate(int fd,off_t length);

参数:

fd          描述符

length       大小

返回值:

成功返回0,出错返回-1

 

 

 

 

 

 

 

 

 

 

 

当打开一个已存在的共享内存区对象时,我们可调用fstat来获取有关该对象的信息。

 

8

名称:

fstat

功能:

获得文件或共享内存区的信息

头文件:

#include

#include

#include

函数原形:

int stat(const char *file_name,struct stat *buf);

参数:

file_name          文件名

buf               stat结构

返回值:

成功返回0,出错返回-1

      

 

 

 

 

 

对于普通文件stat结构可以获得12个以上的成员信息,然而当fd指代一个共享内存区对象时,只有四个成员含有信息。

struct stat{

mode_t st_mode;

uid_t st_uid;

gid_t st_gid;

off_t st_size;

};

 

 

#include

#include

#include

#include

#include

#include

 

int main(int argc,char **argv)

{
    int shm_id;

struct stat buf;

 

if(argc!=2)

{

    printf(“usage:shm_open \n”);

    exit(1);

}

shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);

ftruncate(shm_id,100);

fstat(shm_id,&buf);

printf(“uid_t:%d\n”,buf.st_uid);

printf(“git_t:%d\n”,buf.st_gid);

printf(“size :%d\n”,buf.st_size);

}

下面是运行结果:

#cc –lrt –o shm_show shm_show.c

#./shm_show test

uid_t:0

git_t:0

size:100

 

五、共享内存区的写入和读出

       上面我们介绍了mmap函数,下面我们就可以通过这些函数,把进程映射到共享内存区。

然后我们就可以通过共享内存区进行进程间通信了。

       下面是共享内存区写入的例子:

 

 

#include

#include

#include

#include

 

int main(int argc,char **argv)

{

int shm_id;

struct stat buf;

char *ptr;

 

if(argc!=2)

{

    printf(“usage:shm_open \n”);

    exit(1);

}

shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);

ftruncate(shm_id,100);

fstat(shm_id,&buf);

ptr=mmap(NULL,buf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);

strcpy(ptr,”hello linux”);

printf(“%s\n”,ptr);

shm_unlink(argv[1]);

}

 

下面是运行结果:

#cc –lrt –o shm_write shm_write.c

#./shm_write test

hello linux

 

六、程序例子

       下面是利用pisix共享内存区实现进程间通信的例子:服务器进程读出共享内存区内容,然后清空。客户进程向共享内存区写入数据。直到用户输入“q”程序结束。程序用posix信号量实现互斥。

 

 

#include

#include

#include

#include

#include

 

int main(int argc,char **argv)

{

int shm_id;

char *ptr;

sem_t *sem;

 

if(argc!=2)

{

    printf(“usage:shm_open \n”);

    exit(1);

}

shm_id=shm_open(argv[1],O_RDWR|O_CREAT,0644);

ftruncate(shm_id,100);

sem=sem_open(argv[1],O_CREAD,0644,1);

ptr=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);

strcpy(ptr,”\0”);

while(1)

{

    if((strcmp(ptr,”\0”))==0)

        continue;

        else

        {

             if((strcmp(ptr,”q\n”))==0)

                 break;

             sem_wait(sem);

             printf(“server:%s”,ptr);

             strcpy(ptr,”\0”);

             sem_pose(sem);

         }

         sem_unlink(argv[1]);

         shm_unlink(argv[1]);

     }

}

 

 

 

 

 

#include

#include

#include

#include

#include

#include

 

int main(int argc,char **argv)

{

int shm_id;

char *ptr;

sem_t *sem;

 

if(argc!=2)

{

    printf(“usage:shm_open \n”);

    exit(1);

}

shm_id=shm_open(argv[1],0);

ptr=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,shm_id,0);

while(1)

{

         sem_wait(sem);

         fgets(ptr,10,stdin);

         printf(“user:%s”,ptr);

         if((strcmp(ptr,”q\n”))==0)

             exit(0);

         sem_pose(sem);

         sleep(1);

     }

     exit(0);

}

 

 

#cc –lrt –o server server.c

#cc –lrt –o user user.c

#./server test&

#./user test

输入:abc

user:abc

server:abc

输入:123

user:123

server:123

输入:q

user:q

4:Linux环境进程间通信 共享内存(上)
   共享内存能说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A能即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都能。
采用共享内存通信的一个显而易见的好处是效率高,因为进程能直接读写内存,而不必所有数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,本文将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。
一、内核怎样确保各个进程寻址到同一个共享内存区域的内存页面
1、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,他指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构及一个偏移量来区分的。
2、文件和address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space和一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,非常容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。
3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。
4、对于共享内存映射情况,缺页异常处理程式首先在swap cache中寻找目标页(符合address_space及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程式将分配新的物理页面,并把他插入到page cache中。进程最终将更新进程页表。
注:对于映射普通文件情况(非共享映射),缺页异常处理程式首先会在page cache中根据address_space及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程式会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。
5、所有进程在映射同一个共享内存区域时,情况都相同,在建立线性地址和物理地址之间的映射之后,不论进程各自的返回地址怎么,实际访问的必然是同一个共享内存区域对应的物理页面。
注:一个共享内存区域能看作是特别文件系统shm中的一个文件,shm的安装点在交换区上。
上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。




回页首
二、mmap()及其相关系统调用
mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程能向访问普通内存相同对文件进行访问,不必再调用read(),write()等操作。
注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。他本身提供了不同于一般对普通文件的访问方式,进程能像读写内存相同对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
1、mmap()系统调用形式如下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd能指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,非常显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,他从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再周详介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。
2、系统调用mmap()用于共享内存的两种方式:
(1)使用普通文件提供的内存映射:适用于所有进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:
        fd=open(name, flag, mode);
if(fd
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。
(2)使用特别文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特别的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就能通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程独立维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程一起维护。
对于具有亲缘关系的进程实现共享内存最佳的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。
3、系统调用munmap()
int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
4、系统调用msync()
int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改动并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。能通过调用msync()实现磁盘上文件内容和共享内存区的内容一致。




回页首
三、mmap()范例
下面将给出使用mmap()的两个范例:范例1给出两个进程通过映射普通文件实现共享内存通信;范例2给出父子进程通过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。
范例1:两个进程通过映射普通文件实现共享内存通信
范例1包含两个子程式:map_normalfile1.c及map_normalfile2.c。编译两个程式,可执行文件分别为map_normalfile1及map_normalfile2。两个程式通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。
下面是两个程式代码:
/*-------------map_normalfile1.c-----------*/
#include
#include
#include
#include
typedef struct{
        char name[4];
        int  age;
}people;
main(int argc, char** argv) // map a normal file as shared mem:
{
        int fd,i;
        people *p_map;
        char temp;
       
        fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
        lseek(fd,sizeof(people)*5-1,SEEK_SET);
        write(fd,"",1);
       
        p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 );
        close( fd );
        temp = ’a’;
        for(i=0; i
#include
#include
#include
typedef struct{
        char name[4];
        int  age;
}people;
main(int argc, char** argv)        // map a normal file as shared mem:
{
        int fd,i;
        people *p_map;
        fd=open( argv[1],O_CREAT|O_RDWR,00777 );
        p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
        for(i = 0;i
map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。map_normfile1首先打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。
map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。
分别把两个程式编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程式输出结果如下:
initialize over
umap ok
在map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果):
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;
在map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:
name: b        age 20;        name: c        age 21;        name: d        age 22;        name: e        age 23;        name: f        age 24;
name:        age 0;        name:        age 0;        name:        age 0;        name:        age 0;        name:        age 0;
从程式的运行结果中能得出的结论
1、 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改动文件的大小;
2、 能用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截短为5个people结构大小,而在map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10个people结构的值,后面将给出周详讨论。
注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程能对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;不过,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。
3、 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。
范例2:父子进程通过匿名映射实现共享内存
#include
#include
#include
#include
typedef struct{
        char name[4];
        int  age;
}people;
main(int argc, char** argv)
{
        int i;
        people *p_map;
        char temp;
        p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
        if(fork() == 0)
        {
                sleep(2);
                for(i = 0;i
考察程式的输出结果,体会父子进程匿名共享内存:
child read: the 1 people’s age is 20
child read: the 2 people’s age is 21
child read: the 3 people’s age is 22
child read: the 4 people’s age is 23
child read: the 5 people’s age is 24
parent read: the first people,s age is 100
umap
umap ok




回页首
四、对mmap()返回地址的访问
前面对范例运行结构的讨论中已提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:


注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:
#include
#include
#include
#include
typedef struct{
        char name[4];
        int  age;
}people;
main(int argc, char** argv)
{
        int fd,i;
        int pagesize,offset;
        people *p_map;
       
        pagesize = sysconf(_SC_PAGESIZE);
        printf("pagesize is %d\n",pagesize);
        fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
        lseek(fd,pagesize*2-100,SEEK_SET);
        write(fd,"",1);
        offset = 0;        //此处offset = 0编译成版本1;offset = pagesize编译成版本2
        p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);
        close(fd);
       
        for(i = 1; i
如程式中所注释的那样,把程式编译成两个版本,两个版本主要体目前文件被映射部分的大小不同。文件的大小介于一个页面和两个页面之间(大小为:pagesize*2-99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(大小为:pagesize-99)。程式中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize*3的字节数。
版本1的输出结果如下:
pagesize is 4096
access page 1 over
access page 1 edge over, now begin to access page 2
access page 2 over
access page 2 over
access page 2 edge over, now begin to access page 3
Bus error                //被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个页面
版本2的输出结果如下:
pagesize is 4096
access page 1 over
access page 1 edge over, now begin to access page 2
Bus error                //被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个页面
结论:采用系统调用mmap()实现进程间通信是非常方便的,在应用层上接口非常简洁。内部实现机制区涉及到了linux存储管理及文件系统等方面的内容,能参考一下相关重要数据结构来加深理解。在本专题的后面部分,将介绍系统v共享内存的实现。
Linux环境进程间通信 共享内存(下)
   在共享内存(上)中,主要围绕着系统调用mmap()进行讨论的,本部分将讨论系统V共享内存,并通过实验结果对比来阐述两者的异同。系统V共享内存指的是把所有共享数据放在共享内存区域(IPC shared memory region),所有想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。
系统调用mmap()通过映射一个普通文件实现共享内存。系统V则是通过映射特别文件系统shm中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特别文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的),后面还将阐述。
1、系统V共享内存原理
进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在确保shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构注同时,还将在特别文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于所有一个进程(所有进程都能访问该共享内存区)。所有这一切都是系统调用shmget完成的。
注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,他是存储管理和文件系统结合起来的桥梁,定义如下:
struct shmid_kernel /* private to the kernel */
{       
        struct kern_ipc_perm        shm_perm;
        struct file *                shm_file;
        int                        id;
        unsigned long                shm_nattch;
        unsigned long                shm_segsz;
        time_t                        shm_atim;
        time_t                        shm_dtim;
        time_t                        shm_ctim;
        pid_t                        shm_cprid;
        pid_t                        shm_lprid;
};
该结构中最重要的一个域应该是shm_file,他存储了将被映射文件的地址。每个共享内存区对象都对应特别文件系统shm中的一个文件,一般情况下,特别文件系统shm中的文件是不能用read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。
这里我们采用[1]中的图表给出和系统V共享内存相关数据结构:


正如消息队列和信号灯相同,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该非常熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就和shm文件系统中的文件对应起来。
在创建了一个共享内存区域后,还要将他映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已创建了文件系统shm中的一个同名文件和共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理和mmap()大同小异。




回页首
2、系统V共享内存API
对于系统V共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。
#include
#include
shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就能方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。这里我们不对这些系统调用作具体的介绍,读者可参考相应的手册页面,后面的范例中将给出他们的调用方法。
注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改动进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。




回页首
3、系统V共享内存限制
在/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,能手工对其调整,但不推荐这样做。
在[2]中,给出了这些限制的测试方法,不再赘述。




回页首
4、系统V共享内存范例
本部分将给出系统V共享内存API的使用方法,并对比分析系统V共享内存机制和mmap()映射普通文件实现共享内存之间的差异,首先给出两个进程通过系统V共享内存通信的范例:
/***** testwrite.c *******/
#include
#include
#include
#include
typedef struct{
        char name[4];
        int age;
} people;
main(int argc, char** argv)
{
        int shm_id,i;
        key_t key;
        char temp;
        people *p_map;
        char* name = "/dev/shm/myshm2";
        key = ftok(name,0);
        if(key==-1)
                perror("ftok error");
        shm_id=shmget(key,4096,IPC_CREAT);       
        if(shm_id==-1)
        {
                perror("shmget error");
                return;
        }
        p_map=(people*)shmat(shm_id,NULL,0);
        temp=’a’;
        for(i = 0;i
#include
#include
#include
typedef struct{
        char name[4];
        int age;
} people;
main(int argc, char** argv)
{
        int shm_id,i;
        key_t key;
        people *p_map;
        char* name = "/dev/shm/myshm2";
        key = ftok(name,0);
        if(key == -1)
                perror("ftok error");
        shm_id = shmget(key,4096,IPC_CREAT);       
        if(shm_id == -1)
        {
                perror("shmget error");
                return;
        }
        p_map = (people*)shmat(shm_id,NULL,0);
        for(i = 0;i
testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程式编译为testwrite及testread,先后执行./testwrite及./testread
则./testread输出结果如下:
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;
通过对试验结果分析,对比系统V和mmap()映射普通文件实现共享内存通信,能得出如下结论:
1、 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信能指定何时将数据写入磁盘文件中。注:前面讲到,系统V共享内存机制实际是通过映射特别文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。
2、 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的所有改写操作都将一直保留。
3、 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。注:这里没有给出shmctl的使用范例,原理和消息队列大同小异。




回页首
结论:
共享内存允许两个或多个进程共享一给定的存储区,因为数据不必来回复制,所以是最快的一种进程间通信机制。共享内存能通过mmap()映射普通文件(特别情况下还能采用匿名映射)机制实现,也能通过系统V共享内存机制实现。应用接口和原理非常简单,内部机制复杂。为了实现更安全通信,往往还和信号灯等同步机制一起使用。
共享内存涉及到了存储管理及文件系统等方面的知识,深入理解其内部机制有一定的难度,关键还要紧紧抓住内核使用的重要数据结构。系统V共享内存是以文件的形式组织在特别文件系统shm中的。通过shmget能创建或获得共享内存的标识符。取得共享内存标识符后,要通过shmat将这个内存区映射到本进程的虚拟地址空间。
参考资料
[1] Understanding the Linux Kernel, 2nd Edition, By Daniel P. Bovet, Marco Cesati , 对各主题阐述得重点突出,脉络清晰。
[2] UNIX网络编程第二卷:进程间通信,作者:W.Richard Stevens,译者:杨继张,清华大学出版社。对mmap()有周详阐述。
[3] Linux内核原始码情景分析(上),毛德操、胡希明著,浙江大学出版社,给出了mmap()相关的原始码分析。
[4]shmget、shmat、shmctl、shmdt手册


本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u2/60641/showart_495405.html
 
 
阅读(2248) | 评论(0) | 转发(0) |
0

上一篇:linux共享内存

下一篇:Socket编程知识必学

给主人留下些什么吧!~~