您的位置:首页 > 运维架构 > Linux

进程通信之内存地址映射与共享,同时如何在Linux0.11下实现共享内存

2012-10-27 05:38 597 查看
Mr.Peach这一周经历了许多磨难,所以才在deadline前2天写了这篇博文,关于进程通信时,共享内存的相关理解。

这一切都要从哈工大的第五次操作系统实验说起,它的任务就是将第四次的消费者生产者问题改造改造,进程间的通信方式从文件转为内存。

内存管理有分段式管理,页式管理,也有段式地址,段基址+段偏移得线性地址,线性地址可以得到页目录号,页号,页内偏移地址,页号加上页偏移得物理地址。Linux就是这么解析一个逻辑地址的。

那站在unix系统角度来看内存共享,就要从它的系统调用说起了,和以前一样,我们需要10个sizeof(int)大小的区域来作为 producer 和 consumer 两个进程的共享空间让他们进行情感交流。按常理,就有以下几步了:

打开共享内存区域:相应调用是   id = shmget(KEY, BUFFER_SIZE, IPC_CREAT);  这里 shmget会创建一块新的IPC对象的共享内存,KEY为唯一标定IPC对象的标记,这样在不同进程间,可以通过唯一的KEY来指定相同的共享内存块,创建空间BUFFER_SIZE即为 10个int变量的大小,创建方式为IPC_CREAT,无则创建,有则打开。与以前的文件共享方式无异。id
为共享内存标识符,我们可以通过它来完成以后的映射。
KEY的生成,可以通过调用 ftok() 实现。  like: key_t KEY = ftok("/", 'A');   第一个参数是该进程有权限访问的目录节点,第二个参数是一个介于0-255的数,可以简单用ascii来代替。通过这两个参数,能生成唯一一个KEY用于标记共享内存区域。
将共享内存区映射到本进程内存空间,任何想用共享内存区域的进程都需要进行映射。 使用 head = shmat("id", NULL, 0); 来进行映射。通过id确定哪块儿共享内存,NULL意味针对共享内存首址,我们希望系统能够自动给出,因此我们不指定,给null,0意味对于此空间,进程可读可写。head 是 一个 int* ,映射成功后,head即是共享内存的首址,通过它加上偏移,就能方便只有的读写共享内存区域。such as: head[3]=4;// 共享内存的第四个位置值为4.

这几个函数代替原先文本共享时,文件读写的几个函数即可。关于生产,消费的流程,还是以前的思路。挺简单无聊的。。。。。

另外需要注意的是, 运行有共享内存的进程时,需要有root权限,可通过 sudo ./producer &                   sudo ./consumer.......不然会报segmentation fault

至于Linux0.11下共享内存系统调用的实现。需要理解下。

以前实现信号量时,我们限定了可用信号量的上限,这里也是这样的,共享内存区域相当于一个页面,我们得限定上限有几个空闲页面可作为共享内存区域。

对于每个共享内存区域,都有一个唯一的上面谈到的key,有是否可用的信号,当然还有它的物理地址了,毕竟它需要将共享的东西放在真实地址上,所以要给出实际物理地址的首地址。至于共享区域的长度限制,大家如果看书的话,知道凡是申请共享内存区域,系统都会给出4KB的空间来,不管你实际申请多少。因为那是页面的大小哇,怎么能随便变呢。因此,没有变量记录长度限制。

这样,就可以在unistd.h中确定shm_t结构体的具体内容了。

现在聊聊怎么在kernel中实现shm的两个调用 shmget() shmat().  直观的,需要一个物理页,需要一个线性地址,然后把他们映射起来。之后把段偏移地址作为shmat的返回值就行了。

首先需要一个空白物理页,这个物理空白页的地址是存在 sem_t->addr中的。以为最后映射时使用。可以 
temp = get_free_page();  shm[i]->addr = temp;


其次需要一个线性地址,这里从数据段中偷一块儿出来。
static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
{
/*其中text_size是代码段长度,从可执行文件的头部取出,page为参数和环境页*/
unsigned long code_limit,data_limit,code_base,data_base;
int i;

code_limit = text_size+PAGE_SIZE -1; code_limit &= 0xFFFFF000;
//code_limit为代码段限长=text_size对应的页数(向上取整)
data_limit = 0x4000000; //数据段限长64MB
code_base = get_base(current->ldt[1]); data_base = code_base;

//数据段基址=代码段基址
set_base(current->ldt[1],code_base); set_limit(current->ldt[1],code_limit);
set_base(current->ldt[2],data_base); set_limit(current->ldt[2],data_limit);
__asm__("pushl $0x17\n\tpop %%fs":: );
data_base += data_limit; //从数据段的末尾开始

for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {  //向前处理
data_base -= PAGE_SIZE;  //一次处理一页
if (page[i]) put_page(page[i],data_base); //建立线性地址到物理页的映射
}
return data_limit;  //返回段界限
}
这是修改段标识的函数,可以看出来,数据段与代码段基址相同,其次数据段长度是 0x4000000.  我们可以从下往上,抽取一部分地址作为共享内存区域的线性地址空间,一个页面大小是一个 PAGE_SIZE, 如果我们在unistd中定义了MAX个共享内存区域,那可以将 di = 0x4000000 - MAX*PAGE_SIZE 作为段偏移地址。而基址是 data_base = get_base(current->ldt[2],那共享内存区域的线性地址就是 data_base
+ di。

做线性地址与物理页的映射。
put_page(shm[i]->addr, data_base+di);//将空白物理页映射到线性地址。把di返回。进程就能通过di来读写共享内存区域了。


所以shmget基本就是初始化 shm_t 结构体的,shmat就是做各种地址相关操作的。  按上面思路实现就好了。debug时可以通过多打印地址来跟踪。

有时我希望与我交流更多的是各式各样的人和地域风情的事儿,而不是coding,但也仅是有时。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: