当前位置:首页 >> 信息与通信 >>

Linux环境进程间通信(五):


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<0) ...
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 <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> 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<10; i++) { temp += 1; memcpy( ( *(p_map+i) ).name, &temp,2 ); ( *(p_map+i) ).age = 20+i; } printf(" initialize over \n "); sleep(10); munmap( p_map, sizeof(people)*10 ); printf( "umap ok \n" ); } /*-------------map_normalfile2.c-----------*/ #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> 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<10;i++) { printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );

} munmap( p_map,sizeof(people)*10 ); }
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 age name: g age age

20; name: c age 21; name: d age 22; name: e age 23; name: f 24; 25; name: h age 26; name: I age 27; name: j age 28; name: k 29;

在 map_normalfile1 输出 umap ok 后,运行 map_normalfile2 则输出如下结果:

name: b age age name: age age

20; name: c age 21; name: d age 22; name: e age 23; name: f 24; 0; name: age 0; name: age 0; name: age 0; name: 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 <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> 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_WRI TE,MAP_SHARED|MAP_ANONYMOUS,-1,0); if(fork() == 0) { sleep(2); for(i = 0;i<5;i++) printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age); (*p_map).age = 100; munmap(p_map,sizeof(people)*10); //实际上,进程终止时, 会自动解除映射。 exit(); } temp = 'a'; for(i = 0;i<5;i++) { temp += 1; memcpy((*(p_map+i)).name, &temp,2); (*(p_map+i)).age=20+i; } sleep(5); printf( "parent read: the first people,s age is %d\n",(*p_map).age ); printf("umap\n");

munmap( p_map,sizeof(people)*10 ); printf( "umap ok\n" ); }
考察程序的输出结果,体会父子进程匿名共享内存:

child child child child child

read: read: read: read: read:

the the the the the

1 2 3 4 5

people's people's people's people's people's

age age age age age

is is is is is

20 21 22 23 24

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

注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分, 一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:

#include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> 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,offs et); close(fd); for(i = 1; i<10; i++) { (*(p_map+pagesize/sizeof(people)*i-2)).age = 100; printf("access page %d over\n",i); (*(p_map+pagesize/sizeof(people)*i-1)).age = 100; printf("access page %d edge over, now begin to access page %d\n",i, i+1); (*(p_map+pagesize/sizeof(people)*i)).age = 100; printf("access page %d over\n",i+1); } munmap(p_map,sizeof(people)*10); }
如程序中所注释的那样,把程序编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件 的大小介于一个页面与两个页面之间(大小为: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 共享内存的实现。 在共享内存(上)中,主要围绕着系统调用 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 struct file * int unsigned long unsigned long time_t time_t time_t pid_t shm_perm; shm_file; id; shm_nattch; shm_segsz; shm_atim; shm_dtim; shm_ctim; 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 e name: g age 20; age 23; age 25; name: c name: f age 21; name: d age 22; name:

age 24; age 26; name: I age 27; name:

name: h

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()相关的源代 码分析。

共享内存(Shared Memory)

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

图 共享内存映射图

象所有的 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 数据结构.其实,虚拟内 存并没有在粘附的时候 创建,而要等到第一个进程试图访问它的时候才创建.

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


相关文章:
linux环境进程间通信(全)
[1]); 5 / 65 LINUX 环境进程间通信 sleep(10); exit(); } else if(pid>0) { sleep(1); //等待子进程完成关闭读端的操作 close(pipe_fd[0]);/...
linux环境进程间通信(五)共享内存
Linux 环境进程间通信(五): 共享内存 环境进程间通信( 共享内存共享内存可以说是最有用的进程间通信方式,也是最快的 IPC 形式。两个不同进程 A、B 共享内存的...
实验五 进程间通信(二)
操作系统 实验内容: 实验五 进程间通信(二) 11gb 软件 2 班工程技术学院 贺红艳 成绩: 专业班级: 一、实验目的 1、掌握 linux 系统中进程通信的基本原理。 ...
实验五 进程间通信
实验五 进程间通信 UNIX/LINUX 系统的进程间通信机构(IPC)允许在任意进程间大批量地交换数据。本实验的目的 是了解和熟悉 LINUX 支持的信号机制、管道机制、消息...
Linux环境进程间通信
开放源码的程序都是经过 无数人检验地,本文将以 linux-kernel-2.6.5 为例对 pipe 的工作机制进行阐述。 周欣 张博 二、 进程间通信的分类大型程序大多会涉及...
实验五 Linux进程间通信
实验五 Linux 进程间通信 姓名 学号 1. 实验目的 1)熟悉在 C 语言源程序中使用 Linux 所提供的系统调用界面的方法; 2)掌握 Linux 中子进程的创建方法以及调度...
Linux环境进程间通信
Linux环境进程间通信Linux环境进程间通信隐藏>> 在信号(上)中,讨论了 linux 信号...4. 5. 6. 7. struct sigpending pending: struct sigpending{ struct ...
实验五_Linux进程间通信
实验五 实验五 Linux 进程间通信 1. 实验目的 1)熟悉在 C 语言源程序中使用 Linux 所提供的系统调用界面的方法; 2)掌握 Linux 中子进程的创建方法以及调度执行...
实验五 进程间通信实验(二)
实验五 进程间通信实验(二)实验目的: 1. 通过基础实验,基本掌握无名管道、有...? 常用信号说明:通过 kill –l 命令可以查看到 Linux 支持的信号列表。 实验...
Linux 环境进程间通信(六)套接字
Linux 环境进程间通信(六)套接字_计算机软件及应用_IT/计算机_专业资料。Linux 环境进程间通信套接字今日推荐 89份文档 爆笑大撞脸 ...
更多相关标签: