Linux环境进程间通信——共享内存

原文链接

    原文链接:http://www.ibm.com/developerworks/cn/linux/l-ipc/part5/index1.html


概述

    Android系统中大量使用了mmap实现的共享内存,所以这里需要介绍一下LInux进程间通信机制——共享内存。共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
    采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝。而共享内存则只需要拷贝两次数据:[1] 一次从输入文件到共享内存区 [2] 从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而且保持共享内存区域,直到通信完毕为止。这样,数据内存一直保持在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

内核怎样保证每个进程寻址到同一个共享内存区域的内存页面

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()系统调用并不是完全为了用于共享内存而设计的。它本身提供了一种不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件进行操作。)

1. mmap()系统调用形式

    void* mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
  1. 参数fd为即将映射到进程空间的文件描述符,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。
  2. len映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
  3. prot参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)、PROT_NONE(不可访问)。
  4. flags由以下几个常量值指定:MAP_SHARED,MAP_PRIVATE,MAP_FIXED。其中,MAP_SHARED,MAP_PRIVATE必选其一。
  5. offset参数一般设置为0,表示从文件头开始映射。
  6. 参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。
  7. 函数的返回值为最后文件映射到进程空间的地址,进程可以直接操作起始地址为该值的有效地址。

2. 系统调用mmap()用于共享内存的两种方式

    1. 使用普通文件提供的内存映射,适用于任何进程之间。此时,需要打开或创建一个文件,然后再调用mmap()。典型的调用代码如下:
fd = open(name, flag, mode);
if (fd < 0) {
    return -1;
}
ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHAREDM, fd, 0);

    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()范例

    下面给出使用两个进程通过映射普通文件实现共享内存通信的示例代码。示例包括两个子程序:map_normalfile1.c和map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个文件通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile1试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile2把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。
    map_normalfile1.c代码如下:
#include 
#include 
#include 
#include 
#include 
#include 

typedef struct {
    char name[4];
    int age;
} people;

int main(int argc, char** argv)
{
    int fd, i;
    people *p_map;
    char temp;

    fd = open(argv[1], O_CREAT|O_RDWR|O_TRUNC, 0777);
    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(" Unmap is ok \n");

    return 0;
}
    map_normalfile2.c代码如下:
#include 
#include 
#include 
#include 
#include 

typedef struct {
    char name[4];
    int age;
} people;

int main(int argc, char** argv)
{
    int fd, i;
    people *p_map;
    fd = open(argv[1], O_CREAT|O_RDWR, 0777);
    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);

    return 0;
}
    map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。map_normfile1首先打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。
    map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。
    编译执行结果如下图所示:

Linux环境进程间通信——共享内存_第1张图片

    从运行结果可以得出的结论:
  1. 最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小。
  2. 可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截断为5个people结构大小,而在mmap_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10个people结构的值,后面将给出详细讨论。
  3. 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()或者msync()后,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

对mmap()返回地址的访问

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

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

typedef struct {
    char name[4];
    int age;
}people;

int 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, 0777);
    lseek(fd, pagesize * 2 - 100, SEEK_SET);
    write(fd, "", 1);

    // 版本1:offset = 0
    // 版本2:offset = pagesize
    offset = 0;
    
    p_map = (people*)mmap(NULL, pagesize * 3, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset);
    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);

    return 0;
}
    如代码注释的那样,把程序根据offset编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize * 2 - 99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(pagesize - 99)。程序中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize * 3的字节数。
    版本1的输出结果如下:
Linux环境进程间通信——共享内存_第3张图片

    版本2的输出结果如下:
Linux环境进程间通信——共享内存_第4张图片

    结论:采用系统调用mmap()实现进程间通信是很方便的,在应用层上接口非常简洁。

你可能感兴趣的:(Linux内核)