注意:没有父子关系的进程之间进行共享内存,shmget()的第一个参数key不要用IPC_PRIVATE,否则无法共享。
1. 首先查看如何获得空闲页面:
在kernel/fork.c文件中有:
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page();
函数get_free_page()用来获得一个空闲物理页面,在mm/memory.c文件中:
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t"
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"
"sall $12,%%ecx\n\t"
"addl %2,%%ecx\n\t"
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"
"leal 4092(%%edx),%%edi\n\t"
"rep ; stosl\n\t"
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1)
);
return __res;
}
get_free_page函数就是在mem_map位图中寻找值为0的项(空闲页面),该函数返回的是该页面的起始物理地址。
2.地址映射
mm/memory.c中的do_no_page(unsigned long address),该函数用来处理线性地址address对应的物理页面无效的情况(即缺页中断),do_no_page函数中调用一个重要的函数get_empty_page(address)
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
address &= 0xfffff000;
tmp = address - current->start_code;
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
if (share_page(tmp))
return;
if (!(page = get_free_page()))
oom();
/* remember that 1 block is used for header */
block = 1 + tmp/BLOCK_SIZE;
for (i=0 ; i<4 ; block++,i++)
nr[i] = bmap(current->executable,block);
bread_page(page,current->executable->i_dev,nr);
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
if (put_page(page,address))
return;
free_page(page);
oom();
}
其中putpage用来建立物理地址和线性地址的映射
3.寻找空闲虚拟地址
有了空闲物理页面,也有了建立线性地址和物理页面的映射,但要完成本实验还需要能获得一段空闲的虚拟地址空闲。要从数据段中划出一段空间,首先需要了解进程数据段空间的分布,而这个分布显然是由exec系统调用决定的,所以要详细看一看exec的核心代码,do_execve(在文件fs/exec.c中)。在函数do_execve()中,修改数据段(当然是修改LDT)的地方是change_ldt,函数change_ldt实现如下:
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对应的页数(向上取整)
code_limit = text_size+PAGE_SIZE -1;
code_limit &= 0xFFFFF000;
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);
/* make sure fs points to the NEW data segment */
__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;//返回段界限
}
4.终端运行两个程序
Linux的shell有后台运行程序的功能。只要在命令的最后输入一个&,命令就会进入后台运行,前台马上回到提示符,进而能运行下一个命令
以下为具体实现
首先添加与信号量有关的结构体及本次实验要使用的系统函数
在linux-0.11/include/unistd.h中添加
#define NR_SHM 64
struct shmid_ds
{
int key;
int size;
unsigned long page;
int attached;
};
int shmget(int key,int size,int shmflag);
void * shmat(int shmid,const void * shmaddr,int shmflag);
int,shmdt(int shmid);
int shmctl(int shmid,int shmcmd,struct shmid_ds * buf);
系统调用过程与上次实验完全相同,不再赘述
其中shm.c的实现如下:
#define __LIBRARY__
#include <unistd.h>
#include <stdarg.h>
#include <errno.h>
#include <linux/sched.h>
#include <asm/segment.h>
#include <string.h>
#include <linux/kernel.h>
#define LOW_MEM 0x100000
static struct shmid_ds shm_list[NR_SHM]={{0,0,0,0}};
extern void add_mem_count(long addr);
extern void remove_page(long addr);
int sys_shmget(int key,int size,int shmflag)
{
int i;
int return_key=-1;
unsigned long page;
if(size>PAGE_SIZE)
{
return -EINVAL;
}
for(i=0;i<NR_SHM;i++)
{
if(shm_list[i].key==key)
{
return i;
}
}
for(i=0;i<NR_SHM;i++)
{
if(shm_list[i].key==0)
{
return_key=i;
break;
}
}
//_get_free_page() 是页面分配器提供给调用者的最底层的内存分配函数。它分配连续的物理内存。返回的是物理内存地址。
page=get_free_page();
printk("shmget get memory's address is 0x%08x\n",page);
if(page==0)
{
return -ENOMEM;
}
shm_list[return_key].key=key;
shm_list[return_key].size=size;
shm_list[return_key].page=page;
shm_list[return_key].attached=0;
return return_key;
}
void * sys_shmat(int shmid,const void *shmaddr, int shmflag)
{
unsigned long data_base;
unsigned long brk;
unsigned long page;
if(shm_list[shmid].key<=0 || shm_list[shmid].page==0)
{
return (void*)-EINVAL;
}
//get_base函数的作用就是取出段选择符指向的段描述符中的段基址。
data_base=get_base(current->ldt[2]);
printk("current's data_base = 0x%08x,new page = 0x%08x\n",data_base,shm_list[shmid].page);
brk=current->brk+data_base;
current->brk +=PAGE_SIZE;
// put_page用来完成物理页面与一个线性地址页面的挂接,从而将一个
// 线性地址空间内的页面落实到物理地址空间内,copy_page_tables函数
// 只是为一个进程提供了在线性地址空间的一个页表及1024页内存,而当时
// 并未将其对应的物理内存上。put_page函数则负责为copy_page_tables开
// 的“空头支票”买单。
// page为物理地址,address为线性地址
//unsigned long put_page(unsigned long page,unsigned long address)
page=put_page(shm_list[shmid].page,brk);
if(page==0)
{
return (void*)-ENOMEM;
}
//add_mem_count(page);
shm_list[shmid].attached++;
//printk("current->brk=0x%08x,shmat return 0x%08x,put_page return 0x%08x\n",current-brk,brk,page);
return (void *)(brk - data_base);
}
int sys_shmdt(int shmid)
{
unsigned long data_base;
if(shm_list[shmid].key<=0 && shm_list[shmid].page==0)
{
return -EINVAL;
}
shm_list[shmid].attached--;
data_base=get_base(current->ldt[2]); //取数据段基址
current->brk-=PAGE_SIZE;
//remove_page(data_base+current->brk); //将线性地址从页面中去除
return 0;
}
int sys_shmctl(int shmid,int shmcmd, struct shmid_ds * buf)
{
int ret=0;
switch(shmcmd)
{
case 0:
if(shm_list[shmid].attached>=0 && shm_list[shmid].page!=0)
{
free_page(shm_list[shmid].page);
}
else
{
ret=-EINVAL;
}
break;
}
return ret;
}
producer.c的实现如下:
#define __LIBRARY__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/kernel.h>
#include <fcntl.h>
#include <sys/types.h>
#define BUFFERSIZE 10
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value);
_syscall1(int,sem_post,sem_t *,sem);
_syscall1(int,sem_wait,sem_t *,sem);
_syscall1(int,sem_unlink,const char*,name);
static _syscall3(int,shmget,int,key,int,size,int,shmflag);
static _syscall3(void *,shmat,int,shmid,const void *,shmaddr,int,shmflag);
static _syscall1(int,shmdt,int,shmid);
static _syscall3(int,shmctl,int,shmid,int,shmcmd,struct shmid_ds *,buf);
struct int_buffer
{
int int_count;
int head;
int tail;
int data[BUFFERSIZE];
};
sem_t *mutex;
int main(void)
{
char err_desc[255];
int itemValue = -1;
char mutex_sem[]= "mutex";
struct int_buffer * logbuf;
int shmid_main;
int mutex_value;
mutex = sem_open(mutex_sem,1);
if(mutex == NULL)
{
printf("create semaphore mutex error!\n");
return 1;
}
shmid_main=shmget(1234,sizeof(struct int_buffer),0);
if(shmid_main == -1)
{
printf("shmget(1234,..) error!\n");
perror(err_desc);
return -1;
}
logbuf=(struct int_buffer *)shmat(shmid_main,NULL,0);
if((long)logbuf==-1)
{
printf("in producer shmat(shmid_main,NULL,0) error!\n");
perror(err_desc);
exit(-1);
}
while(itemValue<499)
{
itemValue++;
while(logbuf->int_count==10)
;
if(sem_wait(mutex)!=0)
{
printf("in customer %u,sem_wait(empty) error!",getpid());
perror(err_desc);
break;
}
logbuf->int_count++;
if(sem_post(mutex)!=0)
{
printf("in customer %u,sem_post(empty) error!\n",getpid());
perror(err_desc);
break;
}
logbuf->data[logbuf->head]=itemValue;
(logbuf->head)++;
if(logbuf->head>=BUFFERSIZE)
{
logbuf->head=0;
}
}
if(shmdt(shmid_main)!=0)
{
printf("in producer shmdt(logbuf) error!\n");
perror(err_desc);
}
return 0;
}
consumer.c的实现如下:
#define __LIBRARY__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/kernel.h>
#include <fcntl.h>
#include <sys/types.h>
#define BUFFERSIZE 10
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value);
_syscall1(int,sem_post,sem_t *,sem);
_syscall1(int,sem_wait,sem_t *,sem);
_syscall1(int,sem_unlink,const char*,name);
static _syscall3(int,shmget,int,key,int,size,int,shmflag);
static _syscall3(void *,shmat,int,shmid,const void *,shmaddr,int,shmflag);
static _syscall1(int,shmdt,int,shmid);
static _syscall3(int,shmctl,int,shmid,int,shmcmd,struct shmid_ds *,buf);
struct int_buffer
{
int int_count;
int head;
int tail;
int data[BUFFERSIZE];
};
sem_t *mutex;
int main(void)
{
int get_count=0;
char err_desc[255];
char mutex_sem[]= "mutex";
int itemValue = -1;
int shmid_main;
struct int_buffer * logbuf;
int log = open("pclog.log", O_CREAT|O_TRUNC|O_RDWR, 0666);
char buflog[255];
int mutex_value;
shmid_main=shmget(1234,sizeof(struct int_buffer),0);
if(shmid_main == -1)
{
printf("shmget(1234,..) error!\n");
perror(err_desc);
return -1;
}
mutex = sem_open(mutex_sem,1);
if(mutex == NULL)
{
printf("create semaphore mutex error!\n");
return 1;
}
logbuf=(struct int_buffer *)shmat(shmid_main,NULL,0);
if((long)logbuf==-1)
{
printf("in producer shmat(shmid_main,NULL,0) error!\n");
perror(err_desc);
exit(-1);
}
while(get_count<500)
{
while(logbuf->int_count<=0)
;
itemValue=logbuf->data[logbuf->tail];
if(sem_wait(mutex)!=0)
{
printf("in customer %u,sem_(empty) error!",getpid());
perror(err_desc);
break;
}
logbuf->int_count--;
if(sem_post(mutex)!=0)
{
printf("in customer %u,sem_post(empty) error!\n",getpid());
perror(err_desc);
break;
}
(logbuf->tail)++;
if(logbuf->tail>=BUFFERSIZE)
{
logbuf->tail=0;
}
lseek(log,0,SEEK_END);
sprintf(buflog,"%u:%d\n",getpid(),itemValue);
write(log,&buflog,sizeof(char)*strlen(buflog));
get_count++;
}
close(log);
if(shmdt(shmid_main)!=0)
{
printf("in customer shmdt(logbuf) error!\n");
perror(err_desc);
}
if(shmctl(shmid_main,0,0)==-1)
{
printf("in customer shmctl(shmid,IPC_RMID,0) error!\n");
perror(err_desc);
}
sem_unlink("mutex");
return 0;
}
编译后同时运行两程序:
./producer&
./consumer
运行结果如下,由图中可以看出,两程序逻辑地址不同但公用同一物理地址,实现了内存共享
问题:
对于地址映射实验部分,列出你认为最重要的那几步(不超过4步),并给出你获得的实验数据。
答:第一步为找到需要的各个选择符,DS:S=0X0017,LDT:s=0x0068,GDT:BASE=0X0000 5CB8。第二步为找到数据段的基址。实验中进程的NR为4,即第5个进程,所以按照LINUX 0.11的设计,其基址应为 4*64M,转换为16进制即为0x1000 0000。实验结果也验证了这一点。第三步根据找到的线性地址,通过查看页目录表就可以找到对应页表的物理地地址。实验中页表的物理地址为0x00fa 70000。 第四步根据找到的页表所对就应的物理地址就可以找到变量所在页的物理地址。实验中变量所在页的物理地址为0x00fa6000。这里有个很有趣的现象,页表的物理地址比具体一页的物理地址高。而页表一定是先分配的内存,这也说明linux 0.11中物理内存的分配是从高到低进行的。这一点通过查看get_free_page函数得到印证。
test.c退出后,如果马上再运行一次,并再进行地址跟踪,你发现有哪些异同?为什么?
答:若马上再运行一次,则可以看到得到的线性地址为0x1400 3004,即进程的数据段基地多了0x0400 0000。就是多了一个64M。这也正是linux 0.11中对进程线性地址分配的方法即:nr*64M。因为马上运行此时进程4并没有被删除,而只是状态个性为3,所以再运行的进程正好是5。所以其分配的线性地址基址为0x1400 0000。
关于shmdt及shmctl函数的补充
由于实验指导中没有要求实现shmdt以入shmctl函数,所以关于这两个函数问题没有打算写入报告中,但考虑到代码中有关于这两个函数的内容,而且本人以为这两个函数的实现在难度上要稍高于另外两个,所以还是补充在这里。
开始时shmdt函数的实现只是简单地从共享内存的结构中将计数器减1,以及从将进程的数据段长度修改回最初,但在运行中却发现,两个进程使用同一段共享内存时其中数据却是随机的。而且有时还会提示释放一段已释放的内存的信息(说明:在最初测试时使用两进程都没有对共享内存进行删除)。可以想象当时是如何意外,也很蒙的。随后仔细想了想,主要是考虑到系统提示说释放一段已释放的内存。那一定是对共享内存进行了删除操作的。而测试进程是一定没有释放的,那也就是说系统自动在释放这段内存。会在哪里呢?后来想到第二个进程运行时第一个进程已经结束,那一定是系统在删除结束进程时对进程所持有的物理内存进行了释放。那就得想办法让系统在结束一个僵尸进程时不释放指定内存,但看了看内存管理的全局结构,似乎不可行。后来就决定在shmat同时将mem_map对应数据增1,因为记得内存的低1M内存在mem_map中其值都是USED(=100),所以在其中加入了一个增加mem_map[]计数的函数,只是简单在将其增1。以为这样在进程结束被系统回收时其值只会减1,而不会真正被释放掉。但一运行才发现根本不可行,因为在用户态的put_page中要求mem_map只能为1,不能为其他值。也就是说LINUX 0.11不允许用户态内存共享。当然这里也可以简单地将其限制修改掉,但不能保证其他部分会不会有涉及此值的验证或相关代码,所以不能在这里修改。也就是说共享的这一页内存在mem_map中其值只能为1。那就只能在其他部分想办法。最的决定在shmdt时按照put_page的反操作将对应页表中的值去除。本以为有现在函数可用,找了好久也没找到,最后只能在memory.c中手动增加一个remove_page的函数来实现这个目的。具体代码见下面
void remove_page(long addr)
{
unsigned long * page_table;
page_table=(unsigned long*)((addr>>20)&0xffc); //取目录地址
if((*page_table) & 1) //如果对应页面在内存中存在,则取出页地址
page_table=(unsigned long *)(0xfffff000 & *page_table);
page_table[(addr>>12)&0x3ff]=0; //去掉页中存在的内存地址
}