本文参考https://blog.csdn.net/laoshuyudaohou/article/details/103843023
一、实验目标
深入理解操作系统的段、页式内存管理,深入理解段表、页表、逻辑地址、线性地址、物理地址等概念;
实践段、页式内存管理的地址映射过程;
编程实现段、页式内存管理上的内存共享,从而深入理解操作系统的内存管理。
二、实验1内容和结果
(一). 跟踪地址翻译过程
这节实验的目的是用 Bochs 的调试功能获取变量的虚拟地址映射的物理地址。
在 Linux-0.11 中运行下面的死循环程序:
#include <stdio.h>
int i = 0x12345678;
int main(void)
{
printf("The logical/virtual address of i is 0x%08x", &i);
fflush(stdout);//stdout是系统定义的标准输出文件指针.默认情况下指屏幕,那就是把缓冲区的内容写到屏幕上。
//int fflush(FILE * stream);fflush()会强迫将缓冲区内的数据写回参数stream指定的文件中.
//清空标准输出缓冲区,即刷新输出缓冲区,即将缓冲区的东西输出到屏幕上
while (i)
;
return 0;
}
(二)在上述程序text.c的运行过程中,在命令行窗口按下 Ctrl+C,进入 Bochs 的调试状态,在Ubuntu 中的 Bochs下
直接输入命令 c,continue 程序的运行
如果显示的下一条指令不是 cmp …(这里指语句以 cmp 开头),就用 n 命令单步运行几步,直到停在 cmp …。
使用 u /8命令获取反汇编代码,如图:
首先,cmp指令是比较指令,,比较第一个参数和第二个参数的大小
现在第一个参数是 dword ptr[esp]
dword ptr表示这是一个双字指针,即所要寻址的数据是一个双字(4字节)
由于text.c中只有while (i){}语句有一个判断语句,即i为0吗,为0结束循环。可知 i 的虚拟地址为 ds:0x3004。
(三)使用 sreg命令查看段寄存器的值如图:
地址翻译相关的几张简图资料:
关于此地址翻译过程的更多细节,请参考《Linux0.11内核完全注释》一书中的 5.3.1-5.3.4 节。
1)16位的段选择符LDTR 的值为 0x0068 = 0000000001101 0 00。
根据段选择符的结构,最后两位表示请求特权级 RPL = 00,倒数第三位为表指示标志 TI - 0,表示该选择符存放在 GDT 中,前面的 13 位为索引值 Index = 1101 (二进制) = 13 (十进制),表示该进程的 LDT 表存放在 GDT 表中的 13 号位置。
2)GDTR 的base值为 0x00005cb8,GDT基址为 0x00005cb8。 LDT 表在GDT表的第13项描述符。于是目的 LDT 表的地址存在内存地址0x00005cb8+13*8:
3)用 xp /32w 0x00005cb8 查看从地址0x00005cb8 开始,32 个字的内容
所得结果为 0x52d00068 (4~7)
0x000082fd (0~3),根据段描述符的格式
这里将上述结果组合为 0x00fd52d0,即为 LDT 表的物理地址。查看 LDT 表的内容如图所示:
Linux-0.11 中,LDT 表的第一项为空,第二项为代码段,第三项为数据和堆栈段 ds&ss。
可以看看地址翻译相关的几张简图资料的图5-8
于是所需的 ds 的段描述符为 0x00003fff 0x10c0f300,组合为 0x10000000,即为 ds 段的起始地址。
于是 i 即 ds:0x3004的线性地址为 0x10003004。
接下来通过页表将线性地址映射到物理地址,规则如下:
线性地址为 0x10003004的二进制为0001 0000 00 00 0000 0011 0000 0000 0100
首先计算线性地址的 页目录号为 64, 页表号为 3, 页内偏移为 4。
页目录表的物理地址保存在 CR3 寄存器中,使用 creg命令获取其值:
资料:一页内存为4KB,4x2^10
通过缺页加载机制形成4G线性地址空间。
4G线性地址空间=独享的内存空间+共享的内存空间x共享次数+占用的磁盘空间
占用的磁盘空间涉及缓冲区,缓冲区是内存。主要涉及函数do_no_page()执行缺页处理,会调用get_empty_page();get_free_page();share_page();bread_page();put_page()。
每个进程64M,就支持了126,当然挤了会卡,所有人工定义最多64个
可知页目录表的基址为 0,这是 Linux-0.11 启动时 boot/head.s设置的。目的页目录号为 64,查看该目录项:
于是线性地址对应的页帧为 0x00fa3,加上业内偏移 0x004,得到 0x00fa3004 即为变量 i 的物理地址。验证如下:
使用命令 setpmem 0x00fa3004 4 0直接修改内存,将变量 i 的值设为 0,再用 c 命令继续运行 Bochs,死循环程序退出。
三、实验2
(一)基于共享内存的生产者—消费者程序
信号量实现生产者消费者模型代码 pc.c:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/wait.h>
#define M 530 /*打出数字总数*/
#define N 5 /*消费者进程数*/
#define BUFSIZE 10 /*缓冲区大小*/
int main()
{
sem_t *empty, *full, *mutex;/*3个信号量*/
int fd; /*共享缓冲区文件描述符*/
int i,j,k,child;
int data;/*写入的数据*/
pid_t pid;
int buf_out = 0; /*从缓冲区读取位置*/
int buf_in = 0; /*写入缓冲区位置*/
/*打开信号量,O_CREAT|O_EXCL:如果没有指定的信号量就创建,第三个和第四个参数此时是需要的,如果有指定的信号量就打开,第三个和第四个参数此时是不需要的*/
empty = sem_open("empty", O_CREAT|O_EXCL, 0644, BUFSIZE); /*剩余资源,初始化为size。生产者消耗资源--empty,消费者消耗资源中的数据++empty*/
full = sem_open("full", O_CREAT|O_EXCL, 0644, 0); /*已使用资源,初始化为0.生产者消耗资源++full,消费者消耗资源中的数据--full*/
mutex = sem_open("mutex", O_CREAT|O_EXCL, 0644, 1); /*互斥量,初始化为1*/
fd = open("buffer.txt", O_CREAT|O_TRUNC|O_RDWR,0666); //只读模式打开或创建文件buffer.txt。 (O_CREAT如果指定文件不存在,则创建这个文件,O_TRUNC如果文件存在,并且以只写/读写方式打开,则清空文件全部内容,O_RDWR读写模式)
lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);/*lseek v.前后移动。刷新了10*4个字节的缓冲区,可存放10个数字*/
write(fd,(char *)&buf_out,sizeof(int));//把buf_out中的4字节数据写入文件fd,即将待读取位置存入buffer.txt后,以便子进程之间通信*/
/*生产者进程*/
if((pid=fork())==0)
{
printf("I'm producer. pid = %d\n", getpid());
/*生产多少个产品就循环几次*/
for( i = 0 ; i < M; i++)
{
/*empty大于0,才能生产*/
sem_wait(empty);//此时信号量empty为初值10,减一变为9,9大于0不阻塞
sem_wait(mutex);//此时信号量mutex初值1,减一变为0
/*写入一个字符*/
lseek(fd, buf_in*sizeof(int), SEEK_SET); //新的读写位置变为buf_in*4
write(fd,(char *)&i,sizeof(int));//将i处4字节的数据写入文件buffer.txt
/*更新写入缓冲区位置,保证在0-9之间*/
/*生产完一轮产品(文件缓冲区只能容纳BUFSIZE个产品编号)后*/
/*将缓冲文件的位置指针重新定位到文件首部。*/
buf_in = (buf_in + 1) % BUFSIZE;
sem_post(mutex);
sem_post(full); /*共享区中已使用资源++,唤醒消费者线程*/
}
printf("producer end.\n");
fflush(stdout); /*确保将输出立刻输出到标准输出。*/
return 0;
}
else if(pid < 0)//fork()返回负值:创建子进程失败
{
perror("Fail to fork!\n");
return -1;
}
/*消费者进程*/
for( j = 0; j < N ; j++ )//循环fork()N次
{
if((pid=fork())==0)//fork()子进程返回0,父进程返回子进程ID
{
for( k = 0; k < M/N; k++ )
{
sem_wait(full);/*共享区中已使用资源--,一开始为0会阻塞此处*/
sem_wait(mutex);
/*获得读取位置*/
lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);
read(fd,(char *)&buf_out,sizeof(int));
/*读取数据*/
lseek(fd,buf_out*sizeof(int),SEEK_SET);
read(fd,(char *)&data,sizeof(int));
/*写入读取位置*/
buf_out = (buf_out + 1) % BUFSIZE;
lseek(fd,BUFSIZE*sizeof(int),SEEK_SET);
write(fd,(char *)&buf_out,sizeof(int));
sem_post(mutex);
sem_post(empty);/*共享区中剩余资源++,唤醒生产者进程*/
/*消费资源*/
printf("%d: %d\n",getpid(),data);
fflush(stdout);
}
printf("child-%d: pid = %d end.\n", j, getpid());
return 0;
}
else if(pid<0)//fork()返回负值:创建子进程失败
{
perror("Fail to fork!\n");
return -1;
}
}
/*回收线程资源*/
child = N + 1;
while(child--)
wait(NULL);//父进程等待回收所有子进程
/*释放信号量*/
sem_unlink("full");
sem_unlink("empty");
sem_unlink("mutex");
/*释放资源*/
close(fd);
return 0;
}
这个程序跟上次实验的不同之处在于:使用共享内存替换文件缓冲区;将生产者和消费者分成两个不同的程序,两个都是单进程的。
Linux 中,将不同进程的虚拟地址空间通过页表映射到物理内存的同一区域即为共享内存。如图所示:
两个进程都可以访问共享内存,但是为了确保对共享内存操作的互斥,仍需要使用一个信号量在每次读写的时候进行限制,然后由另外两个信号量保来保证共享内存中每次至多有 10 个数字。
用共享内存和信号量实现的 producer.c代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#define SIZE 10
#define M 510
int main()
{
int shm_id;
int count = 0;
int *p;
int curr;
sem_t *sem_empty, *sem_full, *sem_shm;
sem_empty = sem_open("empty", O_CREAT|O_EXCL, 0644, SIZE);
sem_full = sem_open("full", O_CREAT|O_EXCL, 0644, 0);
sem_shm = sem_open("shm", O_CREAT|O_EXCL, 0644, 1);
shm_id = shmget(2521, SIZE, IPC_CREAT | IPC_EXCL | 0664); // 创建共享内存
p = (int *)shmat(shm_id, NULL, 0);
while (count <= M) {
sem_wait(sem_empty);
sem_wait(sem_shm);
curr = count % SIZE;
*(p + curr) = count;
printf("Producer: %d\n", *(p + curr));
fflush(stdout);
sem_post(sem_shm);
sem_post(sem_full);
count++;
}
printf("producer end.\n");
fflush(stdout);
return 0;
}
consumer.c的代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <semaphore.h>
#include <unistd.h>
#include <fcntl.h>
#define SIZE 10
#define M 510
int main()
{
int shm_id;
int count = 0;
struct shmid_ds buf;
int *p;
int curr;
sem_t *sem_empty, *sem_full, *sem_shm;
sem_empty = sem_open("empty", SIZE);
sem_full = sem_open("full", 0);
sem_shm = sem_open("shm", 1);
shm_id = shmget(2521, 0, 0);//得到一个共享内存标识符或创建一个共享内存对象,并返回共享内存标识符
p = (int *)shmat(shm_id, NULL, 0);//连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问
while(count <= M) {
sem_wait(sem_full);
sem_wait(sem_shm);
curr = count % SIZE;
printf("%d:%d\n", getpid(), *(p + curr));
fflush(stdout);
sem_post(sem_shm);
sem_post(sem_empty);
count++;
}
printf("consumer end.\n");
fflush(stdout);
sem_unlink("empty");
sem_unlink("full");
sem_unlink("shm");
shmctl(shm_id, IPC_RMID, &buf);//shmctl共享内存管理.IPC_RMID删除这片共享内存
return 0;
}
为了让两个进程在同一个终端运行,这里使用终端的后台运行功能:
./producer &
./consumer
在 Ubuntu 下的运行结果如图所示:
(二). 共享内存的实现
- 实现共享内存
函数 int shmget(key_t key, size_t size, int shmflg)会新建或打开一页内存,然后返回该页共享内存的 shmid。忽略 shmflg 参数后,可知一页共享内存需要保存的信息有 唯一标识符 key、共享内存的大小 size,然后还需要一个参数保存共享内存页面的地址。于是共享内存信息的结构体如下:
struct shm_tables //共享内存信息结构体
{
int key; //标识符
int size; //共享内存的大小
unsigned long page; //主内存页面号
};
根据要求,shmget()函数需要获取一块空闲的内存的物理页面来创建共享内存,
shmat()函数需要将该物理页面映射到进程的虚拟内存空间,然后返回其首地址。
函数 get_free_page()能够获取一块空闲的物理页面,并且返回该页面的起始物理地址,用于 shmget()的实现。
函数 put_page()能够把物理页面映射到指定线性地址空间处。为了能让两个进程操作这块共享内存,需要把物理页面分别映射到该进程自己的虚拟空间。内核为每个进程虚拟了一块地址空间,然后分配数据段、代码段和栈段,由函数 do_execve()实现,虚拟空间的分配如下图:
其中 start_code为代码段起始地址,brk为代码段和数据段的总长度,start_stack为栈的起始地址,这些值保存在进程的 task_struct中。brk和 start_stack之间的空间为栈准备,栈底是闲置的,可将共享内存映射到这块空间。
shm.c的代码如下:
#include <asm/segment.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <errno.h>
#define _SHM_NUM 20//共享内存数量最大值
struct shm_tables
{
int key;
int size;
unsigned long page;
} shm_tables[_SHM_NUM];
int sys_shmget(int key, int size)
{
int i;
unsigned long page;
for (i = 0; i < _SHM_NUM; i++) /* 查看 key 对应的共享内存是否已存在 */
if(shm_tables[i].key == key)
return i;
if (size > PAGE_SIZE) /* 内存大小超过一页 */
return -EINVAL;
page = get_free_page(); /* 获取物理内存页面 */
if(!page)
return -ENOMEM;
for (i = 0; i < _SHM_NUM; i++) {
if(shm_tables[i].key == 0) {
shm_tables[i].key = key;
shm_tables[i].size = size;
shm_tables[i].page = page;
return i;
}
}
return -1; /* 共享内存数量已满 */
}
//get_free_page(void)在主内存区中取空闲物理页面,搜索标志每个页面引用次数的数组mem_map[物理内存页面数]
//put_page(page,address)把一给定物理内存页面映射到虚拟地址空间指定处,即在虚拟地址空间指定处映射物理内存。
//据地址page判断指定页面是否是在用户区,是否空闲。据虚拟地址空间指定处address计算address对应的目录项指针和二级页表指针。
//物理页面空闲是指没有被独占。把空闲页指针page置给二级页表指针。如果之前判断指定页面是不在用户区或不空闲,就要使用get_free_page()
void * sys_shmat(int shmid)
{
int i;
unsigned long data_base;
if (shmid < 0 || shmid >= _SHM_NUM || shm_tables[shmid].key == 0) // 判断 shmid 是否合法,shid在0~20
return -EINVAL;
put_page(shm_tables[shmid].page, current->brk + current->start_code); // 把物理页面映射到进程的虚拟空间,物理页面要空闲且在用户区,物理页面空闲是指没有被独占。
current->brk += PAGE_SIZE; // 修改总长度brk,current->brk为代码段长度+数据段长度+bss段长度
return (void*)(current->brk - PAGE_SIZE);
}
修改 mm/Makefile,将 shm.c一块编译进 Image:
OBJS = memory.o page.o shm.o
# add
shm.o shm.c: ../include/asm/segment.h ../include/linux/kernel.h \
../include/linux/sched.h ../include/linux/mm.h ../include/errno.h
然后对系统调用的实现做后续补充。在 include/linux/sys.h中添加:
extern int sys_shmget();
extern int sys_shmat();
fn_ptr sys_call_table[] = { ..., sys_shmget, sys_shmat };
在 include/unistd.h及 Linux-0.11 中的 usr/include/unistd.h添加:
#define __NR_shmget 76
#define __NR_shmat 77
最后修改 kernel/system_call.s中系统调用的数量:
nr_system_calls = 78
共享内存实现完毕。
2. 在 Linux-0.11 运行生产者——消费者程序
;修改 consumer.c和 producer.c,从而能够在 Linux-0.11 运行这两个程序:
#define __LIBRARY__ /* 在第一行添加 */
/* add */
_syscall2(int,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_value,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
_syscall2(int,shmget,int,key,int,size)
_syscall1(int, shmat, int, shmid)
int main()
{
...
/* change */
sem_empty = sem_open("empty", SIZE);
sem_full = sem_open("full", 0);
sem_shm = sem_open("shm", 1);
shm_id = shmget(2521, SIZE);
p = (int *)shmat(shm_id);
...
return 0;
}
然后在 Linux-0.11 中编译运行,结果如下(为了便于显示,这里将 M 改为 60、将 SIZE 改为 4):