实验环境:
地址映射与共享
实验理论:
Linux-0.11操作系统实验6理论-地址映射与共享
实验任务:
- 用 Bochs 调试工具跟踪 Linux 0.11 的地址翻译(地址映射)过程,了解 IA-32 和 Linux 0.11 的内存管理机制;
- 在 Ubuntu 上编写多进程的生产者—消费者程序,用共享内存做缓冲区;
- 在信号量实验的基础上,为 Linux 0.11 增加共享内存功能,并将生产者—消费者程序移植到 Linux 0.11。
跟踪地址翻译过程
这节实验的目的是用 Bochs 的调试功能获取变量的虚拟地址映射的物理地址。实验过程跟着实验楼的描述进行即可。此处简单记录需要注意的几点:
-
根据实验中变量 i 保存在
ds:0x3004
这个地址,通过查询6.3段表,注意要使用自己本机实验环境的LDT表的物理地址:实验楼中数据:
“0x
a2d0
0068 0x000082
fa
” 将其中的加粗数字组合为“0x00faa2d0”,这就是 LDT 表的物理地址本机实验环境数据:
-
得到线性地址为
0x10003004
后,接下来通过页表将线性地址映射到物理地址,注意要使用自己本机实验环境的页框号,得到变量i的物理地址:实验楼中数据:
线性地址 0x10003004 对应的物理页框号为 0x00fa7,和页内偏移 0x004 接到一起,得到 0x00fa7004,这就是变量 i 的物理地址。
本机实验环境数据:
-
最终使用命令
setpmem 0x00fa3004 4 0
直接修改内存,将变量 i 的值设为 0,再用 c 命令继续运行
Bochs
,死循环程序退出。结果如上图所示。
基于共享内存的生产者—消费者程序
-
共享内存相关函数
-
struct shmid_ds
数据结构表示每个新建的共享内存。当
shmget()
创建了一块新的共享内存后,返回一个可以用于引用该共享内存的
shmid_ds
数据结构的标识符。 -
shmget()函数
用来创建共享内存,
shmget()
函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.int shmget(key_t key, size_t size, int shmflg);
第一个参数,
key
标识共享内存的键值:
0/IPC_PRIVATE
。 当key的取值为
IPC_PRIVATE
,则函数
shmget()
将创建一块新的共享内存;如果key的取值为0,而参数
shmflg
中设置了
IPC_PRIVATE
这个标志,则同样将创建一块新的共享内存。第二个参数,
size
以字节为单位指定需要共享的内存容量第三个参数,
shmflg
是权限标志,它的作用与
open
函数的
mode
参数一样,如果要想在
key
标识的共享内存不存在时,创建它的话,可以与
IPC_CREAT
做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。 -
shmat()
函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.void *shmat(int shm_id, const void *shm_addr, int shmflg);
第一个参数,
shm_id
是由
shmget()
函数返回的共享内存标识。第二个参数,
shm_addr
指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。第三个参数,
shm_flg
是一组标志位,通常为0。 -
shmdt()函数
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。int shmdt(const void *shmaddr);
参数
shmaddr
是
shmat()
函数返回的地址指针,调用成功时返回0,失败时返回-1. -
shmctl()函数
用来控制共享内存nt shmctl(int shm_id, int command, struct shmid_ds *buf);
第一个参数,
shm_id
是
shmget()
函数返回的共享内存标识符。第二个参数,
command
是要采取的操作,它可以取下面的三个值 :-
IPC_STAT
:把
shmid_ds
结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖
shmid_ds
的值。 -
IPC_SET
:如果进程有足够的权限,就把共享内存的当前关联值设置为
shmid_ds
结构中给出的值 -
IPC_RMID
:删除共享内存段
第三个参数,
buf
是一个结构指针,它指向共享内存模式和访问权限的结构。请注意,共享内存不会随着程序结束而自动消除,要么调用
shmctl
删除,要么自己用手敲命令去删除,否则永远留在系统中。 -
-
实验描述:使用共享内存替换文件缓冲区;将生产者和消费者分成两个不同的程序,两个都是单进程的,两个进程都可以访问共享内存,但是为了确保对共享内存操作的互斥,仍需要使用一个信号量在每次读写的时候进行限制,然后由另外两个信号量保来保证共享内存中每次至多有 10 个数字。
-
实验代码:
-
producer.c
#include <stdio.h> #include <stdlib.h> #include <sys/shm.h> #include <semaphore.h> #include <unistd.h> #include <fcntl.h> #define NUMBER 520 /*打出数字总数*/ #define BUFSIZE 10 /*缓冲区大小*/ sem_t *empty, *full, *mutex; int main() { int i,shmid; int *p; int buf_in = 0; /*写入缓冲区位置*/ /*打开信号量*/ mutex = sem_open("mutex", O_CREAT|O_EXCL, 0644, 1); empty = sem_open("empty", O_CREAT|O_EXCL, 0644, BUFSIZE); full = sem_open("full", O_CREAT|O_EXCL, 0644, 0); /*创建共享内存*/ shmid = shmget((key_t)1234, BUFSIZE, IPC_CREAT | IPC_EXCL | 0664); printf("shmid:%d\n", shmid); if (shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); } p = (int *)shmat(shmid, NULL, 0); if (p == (void *)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); } /*生产者进程*/ printf("producer start.\n"); fflush(stdout); for( i = 0 ; i < NUMBER; i++) { sem_wait(empty); sem_wait(mutex); p[buf_in] = i; buf_in = ( buf_in + 1)% BUFSIZE; printf("Producer: %d\n", buf_in); fflush(stdout); sem_post(mutex); sem_post(full); } printf("producer end.\n"); fflush(stdout); /*释放信号量*/ sem_unlink("full"); sem_unlink("empty"); sem_unlink("mutex"); return 0; }
-
consumer.c
#include <stdio.h> #include <stdlib.h> #include <sys/shm.h> #include <semaphore.h> #include <unistd.h> #include <fcntl.h> #define NUMBER 520 /*打出数字总数*/ #define BUFSIZE 10 /*缓冲区大小*/ sem_t *empty, *full, *mutex; int main() { struct shmid_ds buf; int i,shmid,data; int *p; int buf_out = 0; /*从缓冲区读取位置*/ /*打开信号量*/ mutex = sem_open("mutex", 1); empty = sem_open("empty", BUFSIZE); full = sem_open("full", 0); /* 创建共享内存,标志符要与生产者相同*/ shmid = shmget((key_t)1234, 0, 0); printf("shmid:%d\n", shmid); if (shmid == -1) { fprintf(stderr, "shmget failed\n"); exit(EXIT_FAILURE); } p = (int *)shmat(shmid, NULL, 0); if (p == (void *)-1) { fprintf(stderr, "shmat failed\n"); exit(EXIT_FAILURE); } for( i = 0; i < NUMBER; i++ ) { sem_wait(full); sem_wait(mutex); data = p[buf_out]; buf_out = (buf_out + 1) % BUFSIZE; sem_post(mutex); sem_post(empty); /*消费资源*/ printf("%d: %d\n",getpid(),data); fflush(stdout); } printf("consumer end.\n"); fflush(stdout); /*释放信号量*/ sem_unlink("full"); sem_unlink("empty"); sem_unlink("mutex"); shmctl(shmid, IPC_RMID, &buf); return 0; }
编译上述两个程序,并在同一个终端后台运行,将输出信息重定位到文件便于查看:
gcc consumer.c -o consumer -lpthread gcc producer.c -o producer -lpthread ./producer > p.txt & ./consumer > c.txt cat p.txt cat c.txt
如果程序运行过程出现阻塞,可以重启ubuntu再运行
在
Ubuntu
下的运行结果(左边是p.txt,右边是从c.txt)如图:
。。。。。。
在linux-0.11中共享内存的实现
-
进行本次实验需要先完成实验5:
信号量的实现和应用
-
函数 int shmget(key_t key, size_t size, int shmflg)会新建或打开一页内存,然后返回该页共享内存的 shmid。忽略 shmflg 参数后,可知一页共享内存需要保存的信息有 唯一标识符 key、共享内存的大小 size,然后还需要一个参数保存共享内存页面的地址。于是共享内存信息的结构体如下:
typedef struct shm_ds { unsigned int key; unsigned int size; unsigned long page; }shm_ds;
根据要求,
shmget()
函数需要获取一块空闲的内存的物理页面来创建共享内存,
shmat()
函数需要将该物理页面映射到进程的虚拟内存空间,然后返回其首地址。函数
get_free_page()
能够获取一块空闲的物理页面,并且返回该页面的起始物理地址,用于
shmget()
的实现。函数
put_page()
能够把物理页面映射到指定线性地址空间处。为了能让两个进程操作这块共享内存,需要把物理页面分别映射到该进程自己的虚拟空间。内核为每个进程虚拟了一块地址空间,然后分配数据段、代码段和栈段,由函数
do_execve()
实现,虚拟空间的分配如下图:
其中
start_code
为代码段起始地址,
brk
为代码段和数据段的
总长度
,
start_stack
为栈的起始地址,这些值保存在进程的
task_struct
中。
brk
和
start_stack
之间的空间为栈准备,栈底是闲置的,可将共享内存映射到这块空间。 -
在linux-0.11/kernel/shm.c目录下新建shm.c,实现共享内存
#define __LIBRARY__ #include <unistd.h> #include <linux/kernel.h> #include <linux/sched.h> #include <linux/mm.h> #include <errno.h> static shm_ds shm_list[SHM_SIZE] = {{0,0,0}}; int sys_shmget(unsigned int key, size_t size) { int i; unsigned long page; if(size>PAGE_SIZE)/* 内存大小超过一页 */ { printk("shmget: size %u cannot be greater than the page size %ud. \n", size, PAGE_SIZE); return -ENOMEM; } if(key==0) { printk("shmget: key cannot be 0.\n"); return -EINVAL; } /* 若共享内存描述符已存在,直接返回索引 */ for(i=0; i<SHM_SIZE; i++) { if(shm_list[i].key == key) return i; } /* 获取空闲物理内存页面 */ page = get_free_page(); if(!page) return -ENOMEM; printk("shmget get memory's address is 0x%08x\n",page); /* 找到一个未用的共享内存描述符初始化,并返回索引 */ for(i=0; i<SHM_SIZE; i++) { if(shm_list[i].key==0) { shm_list[i].key = key; shm_list[i].size = size; shm_list[i].page = page; return i; } } return -1; /* 共享内存数量已满 */ } void * sys_shmat(int shmid) { unsigned long data_base, brk; /*判断 shmid 是否合法*/ if(shmid < 0 || SHM_SIZE <= shmid || shm_list[shmid].page==0 || shm_list[shmid].key <= 0) return (void *)-EINVAL; 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; /* 建立线性地址和物理地址的映射*/ if(put_page(shm_list[shmid].page, brk) == 0) return (void *)-ENOMEM; return (void *)(current->brk - PAGE_SIZE); } /* sys_shmat另一种实现 */ void * sys_shmat(int shmid) { /* 判断 shmid 是否合法 */ if (shmid < 0 || shmid >= SHM_SIZE || shm_list[shmid].key == 0) return -EINVAL; /* 把物理页面映射到进程的虚拟空间 */ put_page(shm_list[shmid].page, current->brk + current->start_code); /* brk为代码段和数据段的总长度,修改总长度 */ current->brk += PAGE_SIZE; return (void*)(current->brk - PAGE_SIZE); }
-
修改/include/unistd.h,添加新增的系统调用的编号:
/* 添加系统调用号 */ #define __NR_whoami 72 /* 实验2 */ #define __NR_iam 73 #define __NR_sem_open 74 /* 实验5 */ #define __NR_sem_wait 75 #define __NR_sem_post 76 #define __NR_sem_unlink 77 #define __NR_shmget 78 /* 实验6 */ #define __NR_shmat 79 #define SHM_SIZE 64 typedef struct shm_ds { unsigned int key; unsigned int size; unsigned long page; }shm_ds; int sys_shmget(unsigned int key,size_t size); void * sys_shmat(int shmid);
-
修改/kernel/system_call.s,需要修改总的系统调用的和值:
nr_system_calls = 80
-
修改/include/linux/sys.h,声明新增函数
extern int sys_shmget(); extern int sys_shmat(); fn_ptr sys_call_table[] = { //...sys_setreuid,sys_setregid,sys_whoami,sys_iam, sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink, sys_shmget, sys_shmat};
-
修改linux-0.11/kernel目录下的Makefile
OBJS = sched.o system_call.o traps.o asm.o fork.o \ panic.o printk.o vsprintf.o sys.o exit.o \ signal.o mktime.o who.o sem.o shm.o // ... ### Dependencies: shm.s shm.o shm.c: ../include/asm/segment.h ../include/linux/kernel.h \ ../include/linux/sched.h ../include/linux/mm.h ../include/unistd.h \ ../include/string.h
-
重新编译内核:make all
-
修改producer.c和consumer.c,适用于linux-0.11运行:
注意:
(1). 在linux0.11系统的应用程序中,注释不能写
//
,必须要写
/* */
(2). 不能在程序中间对变量定义,比如使用循环时的
i
要在开始定义,所有变量都必须要在一开始统一定义。/*producer.c*/ #define __LIBRARY__ #include <unistd.h> #include <sys/types.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <linux/sem.h> _syscall2(sem_t*,sem_open,const char *,name,unsigned int,value); _syscall1(int,sem_wait,sem_t*,sem); _syscall1(int,sem_post,sem_t*,sem); _syscall1(int,sem_unlink,const char *,name); _syscall1(void*,shmat,int,shmid); _syscall2(int,shmget,int,key,int,size); /*_syscall1(int,shmget,char*,name);*/ #define NUMBER 520 /*打出数字总数*/ #define BUFSIZE 10 /*缓冲区大小*/ sem_t *empty, *full, *mutex; int main() { int i,shmid; int *p; int buf_in = 0; /*写入缓冲区位置*/ /*打开信号量*/ if((mutex = sem_open("mutex",1)) == NULL) { perror("sem_open() error!\n"); return -1; } if((empty = sem_open("empty",10)) == NULL) { perror("sem_open() error!\n"); return -1; } if((full = sem_open("full",0)) == NULL) { perror("sem_open() error!\n"); return -1; } /*shmid = shmget("buffer");*/ shmid = shmget(1234, BUFSIZE); printf("shmid:%d\n",shmid); if(shmid == -1) { return -1; } p = (int*) shmat(shmid); /*生产者进程*/ printf("producer start.\n"); fflush(stdout); for( i = 0 ; i < NUMBER; i++) { sem_wait(empty); sem_wait(mutex); p[buf_in] = i; buf_in = ( buf_in + 1)% BUFSIZE; sem_post(mutex); sem_post(full); } printf("producer end.\n"); fflush(stdout); /*释放信号量*/ sem_unlink("full"); sem_unlink("empty"); sem_unlink("mutex"); return 0; }
/*consumer.c*/ #define __LIBRARY__ #include <unistd.h> #include <sys/types.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <linux/sem.h> _syscall2(sem_t*,sem_open,const char *,name,unsigned int,value); _syscall1(int,sem_wait,sem_t*,sem); _syscall1(int,sem_post,sem_t*,sem); _syscall1(int,sem_unlink,const char *,name); _syscall1(void*,shmat,int,shmid); _syscall2(int,shmget,int,key,int,size); /*_syscall1(int,shmget,char*,name);*/ #define NUMBER 520 /*打出数字总数*/ #define BUFSIZE 10 /*缓冲区大小*/ sem_t *empty, *full, *mutex; int main() { int i,shmid,data; int *p; int buf_out = 0; /*从缓冲区读取位置*/ /*打开信号量*/ if((mutex = sem_open("mutex",1)) == NULL) { perror("sem_open() error!\n"); return -1; } if((empty = sem_open("empty",10)) == NULL) { perror("sem_open() error!\n"); return -1; } if((full = sem_open("full",0)) == NULL) { perror("sem_open() error!\n"); return -1; } printf("consumer start.\n"); fflush(stdout); /*shmid = shmget("buffer");*/ shmid = shmget(1234, BUFSIZE); printf("shmid:%d\n",shmid); if(shmid == -1) { return -1; } p = (int *)shmat(shmid); for( i = 0; i < NUMBER; i++ ) { sem_wait(full); sem_wait(mutex); data = p[buf_out]; buf_out = (buf_out + 1) % BUFSIZE; sem_post(mutex); sem_post(empty); /*消费资源*/ printf("%d: %d\n",getpid(),data); fflush(stdout); } printf("consumer end.\n"); fflush(stdout); /*释放信号量*/ sem_unlink("full"); sem_unlink("empty"); sem_unlink("mutex"); return 0; }
-
将已经修改的/usr/include/unistd.h以及新修改的producer.c consumer.c拷贝到linux-0.11系统中,用于测试实现的信号量以及共享内存。
sudo ./mount-hdc cp ./unistd.h ./hdc/usr/include/ cp ./producer.c consumer.c ./hdc/usr/root/ sudo umount hdc/
-
启动新编译的linux-0.11内核,用producer.c consumer.c测试实现的信号量以及共享内存。
./run gcc consumer.c -o consumer -lpthread gcc producer.c -o producer -lpthread ./producer > p.txt & ./consumer > c.txt sync
-
关闭linux-0.11,挂载虚拟磁盘,查看信息输出文件p.txt和c.txt,和ubuntu下运行的结果相同。
实验问题
- 对于地址映射实验部分,列出你认为最重要的那几步(不超过 4 步),并给出你获得的实验数据。
最重要的四步:
- 获取 i 的虚拟地址
- 获取 LDT 表的地址
- 获取线性地址
- 获取物理地址
前提:知道段选择子(16位),段描述符(64位),页目录表及页表项(32位)的意义
Step1:首先需要获得逻辑地址,LDT的地址:
逻辑地址 0x00003004
全局描述符表物理地址: gdtr:base=0x00005cb8, limit=0x7ff
而局部描述符选择子:ldtr:s=0x0068 二进制0000,0000,0110,1000b 1101即GDT表第14项
bochs:4> xp /2w 0x5cb8 + 13*8
[bochs]:
0x00005d20 <bogus+ 0>: 0x52d40068 0x000082fd
即LDT的物理地址: 0x00fd52d4Step2:然后需要获得线性地址:
DS段选择子 ds:s=0x0017 即LDT表中第3项
bochs:5 xp /2w 0xfd52d4 + 2*8
[bochs]:
0x00fd52e4 <bogus+ 0>: 0x00003fff 0x10c0f300
即DS段基地址为 0x10000000
所以线性地址为:0x10003004Step3:其次获得页目录及页表地址计算得物理地址
CR3内容: CR3=0x00000000
虚拟地址对应页目录第65项,页表第4项,页表内偏移0x004
查询页目录第65项:
bochs:6 xp /w 64
4
[bochs]:
0x00000100 <bogus+ 0>: 0x00fa5027
页表物理地址为: 0x00fa5000, 查询第4项
bochs:7 xp /w 0xfa5000 + 3
4
[bochs]:
0x00fa500c <bogus+ 0>: 0x00fa3067
页框物理地址:0x00fa3000 加上偏移0x004
计算得到物理地址: 0x00fa3004
bochs:8 xp /w 0x00fa3004
[bochs]:
0x00fa3004 <bogus+ 0>: 0x12345678
- test.c 退出后,如果马上再运行一次,并再进行地址跟踪,你发现有哪些异同?为什么?
逻辑地址和虚拟地址不变,页目录地址是操作系统放置的, 物理分页变了,所以物理地址也变了。原因是每次进程加载后都有64M的虚拟地址空间,
而且,逻辑地址没有变化。操作系统加载程序时,由于虚拟地址是按nr分配64M,两次运行nr一致,所以虚拟地址没变。段基址可能会变化,因为操作系统为每个进程分配的 64M 空间位置不同,导致段基址不同。
而数据段偏移量不变,这是编译时就设置完毕的。
参考链接:
https://blog.csdn.net/laoshuyudaohou/article/details/103843023
https://blog.csdn.net/realfancy/article/details/90645987
https://www.cnblogs.com/tradoff/p/5838266.html