#include #define KEY 4
#define SIZE 4096*3
int main(int argc, char *argv[])
{
int shmid = 0, ret = 0;
char *shmaddr = 0;
struct shmid_ds buf;
shmid = shmget(KEY, SIZE, IPC_CREAT | SHM_R);
printf("id: %d\n", shmid);
shmaddr = (char *)shmat(shmid, NULL, 0);
*(shmaddr + 4095*3) = 'c';
shmdt(shmaddr);
}
使用ipcs –m 查看建立的共享内存对象:
要理解IPC共享内存和文件映射的实现机制,先要理解什么是共享内存,共享内存实现的基本原理是什么。
(五)共享内存实现基本原理
CPU要访问某块内存,必须要获得内存的物理地址。
CPU集成有寻址硬件,会根据机器语言指令中提供的地址,执行地址转换,获得的物理地址。
CPU有两种转换模式:1.实模式 2.保护模式。
实模式下的物理地址 = 线性地址。
保护模式下的物理地址 = 线性地址通过分页机制转化为物理地址。
启动保护模式:把CPU控制寄存器CR0中的最高位置1。
CPU保护模式寻址方式:
图1 分页机制寻址
说明:CR3控制寄存器的值是物理地址。由于寻找的是页框的物理地址,所以CR3,页目录和页表中存储的物理地址后12位都为0。也就是这12位的空间不存储物理地址,而是用于访问控制(可读/可写/CPU特权级别),指示对应的页是否存在等作用。
在转换过程中,出现以下情况之一将会引起也异常:
涉及的页目录表内的表项或页表内的表项中的P=0,即涉及到的页不在内存;
违反也保护属性的规定而对页进行访问。
注意:从2.6.11版本开始,采用了四级分页模型,但基本原理是一样的。
每个进程的CR3的值是不同,进程切换时保存或者恢复CR3的值。只要设置了CR3的值CPU将自动进行寻址。
要将某个物理页加入进程的地址空间,要做的事情就是将物理页的物理地址填写进程页目录表内的表项和页表内的表项。
共享内存实现原理就是:将相同的物理页加入不同进程的地址空间。
显然进程中要加入一块物理页,就必须对应一块线性空间,于是就要先申请一块线性空间,然后根据这块线性空间填写页目录表内的表项和页表内的表项。(这里可以说明为什么不同进程中的不同线性地址可以对应相同的物理地址)。
由于linux会先分配线性空间,页表的修改会推后进行。Linux通过vm_area_struct对象实现线性区,当产生缺页异常时,会根据vm_area_struct对象来修改页表,然后重新执行产生缺页异常的代码。
总的来说,内核实际要做的事情是很多的,但是,内核也提供了很多接口,所以我们要做的事情还是比较少的。
以上就是共享内存实现的基本原理,下面分析一下IPC共享内存和内存映射实现机制。实际上IPC共享内存的实现是基于内存映射,原因是:内存映射提供了一些接口,基于内存映射来实现IPC共享内存可以复用这些代码。但是,两者最终的实现原理还是修改页表。
(六)IPC共享内存实现机制
先看一下内核是如何组织共享内存对象的,如图2所示:
图2 共享内存对象组织
其中struct shmid_kernel就是一个共享内存对象,使用id radix tree来组织所有的共享内存对象。使用id号查找一个共享内存对象。
我们现在最关心的问题是:如何根据struct shmid_kernel结构获得对象所拥有的物理内存。
是根据file->f_path.dentry->d_inode
struct address_space *mapping = inode->i_mapping
mapping存储着共享内存拥有的物理页,如图3所示:
图3 共享内存物理页存储方式
其中page_tree用于存储物理页,每个节点的值类型是struct page *
每个共享内存对象对应一个inode对象,这个对象是被多个进程共享,也就是说,进程是通过inode对象获得物理页。这里借用了文件映射的框架,i_mmaping对象也就是文件的页高速缓存。
进程映射共享内存区域的过程:
1.需要申请一块线性地址空间,也就是生成一个vm_area_struct对象,并将对象加入到自己的地址空间,当此时并不修改进程页表,而是把struct file对象加入到vm_area_struct对象中,执行以下代码:
vma->vm_file = file;
get_file(file);
error = file->f_op->mmap(file, vma);
注意:这里的file是根据shm_file生成的一个新的对象,相当于shm_file的复制。
2. 当第一次访问共享内存块时,由于相应的页表项还未填写,将产生缺页异常,内核根据产生异常的线性地址找到对应的vm_area_struct对象,最后将执行以下函数:
static int shmem_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
struct inode *inode = vma->vm_file->f_path.dentry->d_inode;
int error;
int ret;
if (((loff_t)vmf->pgoff << PAGE_CACHE_SHIFT) >= i_size_read(inode))
return VM_FAULT_SIGBUS;
error = shmem_getpage(inode, vmf->pgoff, &vmf->page, SGP_CACHE, &ret);
if (error)
return ((error == -ENOMEM) ? VM_FAULT_OOM : VM_FAULT_SIGBUS);
return ret | VM_FAULT_LOCKED;
}
本函数的功能是:根据产生缺页异常的线性地址找到对应的物理页,并将这个物理页加入页表。
以上说明的也就是内存映射实现机制,现在做个总结:
调用do_mmap()函数生成并注册一个线性对象,同时也注册缺页异常回掉函数。
当第一次访问共享内存空间时,产生缺页异常,将会调用先前注册的回调函数,在回调函数中将相应的物理页加入进程的页表。
(七)文件映射的实现机制
一个磁盘文件唯一对应一个磁盘inode,每个磁盘inode也唯一对应一个内核inode,每个内核inode都包含一个页高速缓存对象struct address_space i_mmap,建立一个文件映射的步骤如下:
第一步和IPC共享内存一样还是生成并注册一个线性对象,同时也注册缺页异常回掉函数。
第二步当产生缺页异常时会执行先前注册的回调函数,在回调函数中将请求的物理页的物理地址写入进程页表项,并重新执行产生缺页异常的代码。
下面以EXT3文件系统为例子:
注册回调函数:
int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
{
struct address_space *mapping = file->f_mapping;
if (!mapping->a_ops->readpage)
return -ENOEXEC;
file_accessed(file);
vma->vm_ops = &generic_file_vm_ops;
vma->vm_flags |= VM_CAN_NONLINEAR;
return 0;
}
const struct vm_operations_struct generic_file_vm_ops = {
.fault = filemap_fault,
};
filemap_fault()回调函数被多种文件系统使用。
在函数内总是从struct address_space *mapping = file->f_mapping;获得物理页。
从以下函数看出file->f_mapping指向inode的i_mapping对象。
struct file *alloc_file(struct path *path, fmode_t mode,
const struct file_operations *fop)
{
。。。。。。。。。。。。。。
file->f_path = *path;
file->f_mapping = path->dentry->d_inode->i_mapping;
。。。。。。。。。。。。。。。
}
每个磁盘文件都唯一对应一个内核inode对象,如果多个进程同时打开同一个磁盘文件,inode对象将被多个进程共享。每个文件的inode包含了一个address_space结构,通过inode->i_mapping来访问。address_space结构中维护了一棵radix树,用于磁盘高速缓存的内存页面就挂在这棵树上。打开这个文件的每个进程都共用同一份页高速缓存。
如果是私有映射类型则相应的页表项会设置写时复制,也就是,当进程试图修改一个私有映射内存的页时,内核将会产生缺页异常,内核就把该页框进行复制,并在进程页表中用复制的页来替换原来的页框,显然这个新的页框已经不在页高速缓存中了,对页框的内容进行修改将不会写回文件,其它进程将无法共享这个页框。
pdflush内核线程用于刷新脏页。
注意:如果还没修改私有映射内存的页,也就是还未进行写时复制,则对应的页框还是页高速缓存中的页,如果其他进程修改了这个页框的数据,本进程还是可以读取修改后的数据。