操作系统实验——内存管理

说明:本实验综合学校实验指导书和个人上交的实验报告编写而成,感谢北京信息科技大学计算机学院操作系统实验指导老师的帮助。

一、实验目的

  本实验需要完成两个并发进程通过共享存储器(内存映射文件)机制对文件的修改。通过本次实验,能够使学生进一步理解并掌握内存映射文件的概念及实现原理,熟练使用Linux进程通信及内存管理相关函数和系统调用进行编程,进而掌握通过共享存储器方式实现进程间通信的基本原理及内存管理的基本功能。

二、实验环境

操作系统: Unix、Linux 或 MAC

编译器:  gcc

实验要求

1.学生应完成如下章节的学习:进程和线程、调度、进程通信、存储管理。

2.在Linux操作系统下,用C语言编程,使用相关函数和系统调用设计实现。

、实验内容及方案指导

1.打开当前目录下的文件f1(f1自己创建,内容为0123456789abcWXYZde),使用系统调用mmap()创建共享存储区,大小为一个页面,将文件f1映射进内存,地址返回到src。

2.使用malloc()函数申请一个内存块,地址返回到dst。申请成功后,使用memmove()函数将共享存储区的内容复制到dst。

3.使用fork()调用创建一个新进程,之后父进程阻塞自己。

4.子进程使用memchr()搜索dst中字符“W”出现的位置,返回到temp,利用temp将“WXYZ” 替换为你的学号后四位。修改后的dst的内容使用memmove()复制回src,关闭文件f1,释放dst。

5.父进程使用memset()将共享存储区src中前2个字符用QQ替换。最后显示文件f1的内容。

6.解除文件与内存的映射,关闭文件f1,释放dst。

、实验内容

1.实验程序框架(头文件根据定义补齐)

int main()

{  int fd,page_size;

   pid_t pid;

   ⑴显示当前系统的页面大小;

   fd=open("./f1",O_RDWR);

   char * src = (char *) mmap(NULL,page_size,PROT_WRITE,MAP_SHARED,fd,0);

   if(src==MAP_FAILED)

   {printf("error\n");

       exit(1);

    }

   ⑵显示src的内容;   

   ⑶使用malloc()函数申请一个页面大小的内存空间,地址返回到dst;

   ⑷使用memmove()函数将共享存储区的内容复制到dst;

   ⑸显示dst的内容;

   pid=fork();

   if(pid==0){ /*  子进程空间  */

   ⑹使用memchr()将dst中“W”的地址返回到temp;

   ⑺显示temp的内容及其内存起始地址;

   ⑻将temp中前4个字符用你学号的后四位替换,显示替换后的dst内容;

   ⑼使用memmove()将修改后的dst的内容复制回src;

   ⑽显示src的内容(包含PID及PPID);

   ⑾关闭之前打开的文件f1,释放dst;

   }

   else if(pid>0){ /*  父进程空间  */

   wait(NULL);  

   ⑿使用memset()将共享存储区src中前2个字符用”QQ”替换,并显示替换后的内容(包含PID及PPID);

   ⒀显示文件f1的最终内容;

(14)显示父进程中的dst内容

   }   

   printf("PID is: %d,  PPID is: %d,  The src value of end is: %s\n\n", getpid(),getppid(),src);    /*显示src指向的共享存储区内容*/

   if(munmap(src,page_size)==0)     /*释放共享存储区*/

{ printf("PID is: %d,  PPID is: %d,  munmap success\n\n",

getpid(),getppid());}

   else

{ printf("PID is: %d,  PPID is: %d,  munmap failed\n\n",

getpid(),getppid());}

}

2.实验代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <malloc.h>
#include <sys/types.h>
#include <memory.h>
#include <fcntl.h>
int main(){
	int fd, page_size;
	pid_t pid;
	//⑴显示当前系统的页面大小;	
	page_size = getpagesize();
	printf("(1)页面大小: %d\t当前进程号: %d\n",page_size,getpid());
	
	//打开文件,获得文件描述符
	fd=open("./f1",O_RDWR);
	
	//使用系统调用mmap()创建共享存储区,大小为一个页面,将文件f1映射进内存,地址返回到src。
        char * src = (char *) mmap(NULL,page_size,PROT_WRITE,MAP_SHARED,fd,0);//返回值类型是void *,强制转化为char *
	if(src==MAP_FAILED){
		printf("error\n");
		exit(1);
	}
	
	//⑵显示src的内容;   
	printf("(2)src是共享内存,指向内存中的内容为: %s当前进程号: %d\n",src,getpid());
	//printf("(2)src是共享内存,指向内存中的内容为: %p\n当前进程号: %d\n",src,getpid());//⑵显示src的地址 

	
	//⑶使用malloc()函数申请一个页面大小的内存空间,地址返回到dst;
	char * dst = (char *) malloc(page_size);//如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。
	if (dst == NULL) {
		printf("(3)内存分配失败!\n");
		exit(1);
	}
	else{
	        printf("(3)内存分配成功!\n");
	}
	
	printf("(TEST0)子进程dst逻辑地址:%p\n",dst);

	
	//⑷使用memmove()函数将共享存储区src的内容复制到dst;
	memmove(dst, src, page_size);//由src所指内存区域复制page_size个字节到dst所指内存区域。 
	printf("(4)已使用memmove()函数将共享存储区的内容复制到dst\n");
	
	//⑸显示dst的内容
	printf("(5)将共享内存复制到dst后,dst指向内存中的内容: %s当前进程号: %d\n",dst, getpid());
        
        pid = fork();
	if(pid==0){
		 //⑹  0123456789abcWXYZde   使用memchr()将dst指向内存中的内容中第一次出现“W”的地址返回到temp;
		
		char *temp = (char *) memchr(dst,'W',page_size);
		 printf("(6)已使用memchr()将dst中“W”的地址返回到temp\n");
		 
		 //⑺显示temp的内容及其内存起始地址;
		if(temp == NULL){
			printf("(7)没有在dst中找到“W”.\n");
		}
		else{
			printf("(7)Temp指向内存中的内容: %sdst中W的内存起始地址:%p\n", temp,temp);
		}

		//⑻将temp中前4个字符用你学号的后四位替换,显示替换后的dst指向内存中的内容
		// void *memset(void *s, char ch, int n)将s所指向的某一块内存中的前n个字节的内容全部设置为ch指定的ASCII值。
		
		temp = memset(temp, '2', 4);
		temp = memset(temp, '3', 3);
		temp = memset(temp, '4', 2);
		temp = memset(temp, '5', 1);
		
		/*错误的写法
		temp = memset(temp, '5', 0);
		temp = memset(temp, '4', 1);
		temp = memset(temp, '3', 2);
		temp = memset(temp, '2', 3);
		*/
		printf("(8)子进程中修改前四个字符后的dst指向内存中的内容: %s", dst);
		
		//⑼使用memmove()将修改后的dst的内容复制回src;(共享内存)
		memmove(src, dst, page_size);
		printf("(9)已使用memmove()将修改后的dst的内容复制回src\n");

		
		//⑽显示src的内容(包含PID及PPID);
		printf("(10)子进程分支中共享内存src指向内存中的内容: %sPid: %d, PPId:%d.\n", src,getpid(),getppid());
                               
                printf("(TEST1)子进程dst逻辑地址:%p\n",dst);

		//⑾关闭之前打开的文件f1,释放dst;
		close(fd);
		free(dst);
		printf("(11)已关闭文件f1,释放dst\n");
	}
	
	else if(pid>0){ //父进程空间
              wait(NULL); //父进程会被阻塞,直到任意一个子进程结束
               //⑿使用memset()将共享存储区src中前2个字符用”QQ”替换,并显示替换后的内容(包含PID及PPID);
               memset(src,'Q',2);
	       printf("(12)经过memset()之后的src中的内容: %sPID: %d, PPID : %d\n",src, getpid(),getppid());
               
               printf("(TEST2)父进程dst逻辑地址:%p\n",dst);
               
               //⒀普通显示文件f1的最终内容的方式
               /*
               lseek(fd, 0 , SEEK_SET);将文件位移量设置为距离文件开始处0个字节的位置,即开始处
		char buf[page_size + 1];
		size_t byteRead = read(fd,buf,page_size);//从文件fd中取出page_size字节的数据到内存buf中
		if(byteRead >0){
			buf[byteRead] = '\0';
			printf("(13)f1的最终内容: %s",buf);
		}
		*/
		
		// ⒀使用循环显示文件f1的最终内容的方式
                lseek(fd, 0, SEEK_SET);
                char buf[page_size + 1];
                ssize_t bytesRead;

                printf("(13) f1的最终内容:");
                while ((bytesRead = read(fd, buf, page_size)) > 0) {
                       buf[bytesRead] = '\0';
                       printf("%s", buf);
                }
		//(14)显示父进程中的dst指向内存中的内容
               printf("(14)父进程中的dst指向内存中的内容: %sPid: %d\n", dst,getpid());
               free(dst);
         } 
         
               //(15)显示src指向内存中的内容
               printf("(15)PID is: %d, PPID is: %d, src指向内存中的内容: %s", getpid(),getppid(),src);//ppid是bash进程的
               
               //一旦不再需要映射区域,就可以使用munmap来释放一个先前由mmap分配的内存映射区域
               //取消参数src所指的映射内存起始地址
	       if(munmap(src, page_size)==0){//释放共享内存
		     printf("(16)PID is: %d, PPID is: %d, munmap success.\n", getpid(),getppid());
	       }
	       else{
		     printf("(16)PID is: %d, PPID is: %d, munmap failed.\n", getpid(),getppid());
	       }  
         //0123456789abcWXYZde
         //gcc -o myprogram2 OS2.c
         //./myprogram2
}

3.代码分析

(1)首先输出页面大小和当前进程号。

(2)使用mmap将文件f1映射到内存中,并返回一个指向共享内存区域的指针src,并打印src指向共享内存中的内容。

(3)分配一个页面大小的内存空间,地址返回到dst。

(4)使用memmove将共享存储区src指向共享内存中的内容复制到dst中。

(5)输出dst指向内存中的内容和当前的进程号。

(6)在子进程中使用memchr查找dst中“W”的位置,并将其地址返回到temp。

(7)输出temp的内容和其内存起始地址。

(8)在子进程中,使用memset将temp中前四个字符替换为学号后四位,输出修改后的dst指向内存中的内容。

(9)将修改后的dst指向内存中的内容复制回共享内存src中。

(10)输出共享内存src指向共享内存中的内容,并打印PID和PPID。

(11)在子进程中关闭文件f1,释放dst。

(15)子进程执行完毕,跳出if 分支,打印出子进程的pid和子进程的ppid,并打印src指向内存中的内容。

(16)使用munmap释放共享内存。

(12)在父进程中,使用memset将共享内存src中前两个字符替换为“QQ”,输出修改后的内容,并打印PID和PPID。

(13)输出文件f1的最终内容。

(14)输出父进程中的dst指向内存中的内容

(15)父进程执行完毕,跳出else分支,打印出父进程的pid和父进程的ppid(即bash进程的pid),并打印src指向内存中的内容,

(16)使用munmap释放共享内存。

4.运行结果

5.问题及解决方法

问题:对 src和dst两个指针变量的理解不够到位,导致有时需要输出指针变量里面存放的地址所指向的存储单元里面的数据时出现错误。在打印时往往打印的是指针指向的地址,而不是地址中存放的内容。

printf("(2)src是共享内存,指向内存中的内容为: %p\n当前进程号: %d\n",src,getpid());

反思:

当printf语句使用了%p格式指示符时,它会打印指针地址。

当printf语句使用了%s格式指示符时,它用于打印字符串,它会将src解释为一个以null字符结尾的字符数组,并打印出这个字符串。

printf("(2)src是共享内存,指向内存中的内容为: %s当前进号: %d\n",src,getpid());

6.改进措施

改进一:

char *temp =memchr(dst,'W',page_size)

这行代码使用了memchr()函数来在指定的内存区域中查找字符'W'的第一次出现的位置。dst是要搜索的内存区域的起始地址,page_size是要搜索的内存区域的大小(以字节为单位)。函数返回一个指向找到的字符位置的指针,如果未找到字符,则返回NULL。

char *temp = (char *) memchr(dst,'W',page_size)

这行代码与上面的代码相同,只是对返回的指针进行了类型转换,将其强制转换为char*类型。这是因为memchr()函数返回的是一个void*类型的指针,而在这里将其转换为char*类型是为了方便后续对找到的字符进行操作。

改进二:

在打印f1文件的内容时仅考虑使用系统调用lseek移动开始读取的位置,看到f1中没有多少内容就想当然的以为所有文件的内容都很少,但实际上有些文件的内容时多过一页所能存放的内容的,而下段代码只能打印一页以内的内容。

//只能打印一页的代码 
lseek(fd, 0 , SEEK_SET);
char buf[page_size + 1];
	  size_t byteRead = read(fd,buf,page_size);
	  if(byteRead >0){
			buf[byteRead] = '\0';
			printf("(13)f1的最终内容: %s",buf);
}

使用一个循环来读取并打印文件的内容,直到读取到文件末尾。无论文件内容超过一个页面还是不足一个页面,都可以正确地打印出文件的完整内容。每次调用read()函数时,它会从文件当前的读取位置开始读取指定字节数(page_size),并将读取的内容存储到buf缓冲区中。然后将缓冲区的内容打印出来。在循环的每一次迭代中,read()函数会自动更新文件的当前读取位置,以便下一次读取从上一次结束的地方继续。因此即使文件内容超过一个页面,该循环也可以正确地读取和打印出剩余的内容。

  //能打印多页的代码 
lseek(fd, 0, SEEK_SET);
      char buf[page_size + 1];
      ssize_t bytesRead;

             printf("(13) f1的最终内容:");
       while ((bytesRead = read(fd, buf, page_size)) > 0) {
             buf[bytesRead] = '\0';
                     printf("%s", buf);
     }

内容提示及注意事项

  共享存储器方式是一种高级进程通信方式。它是在内存中分配一片空间作为共享存储区供多个进程共享,需要进行通信的各个进程把共享存储区附加到自己的进程空间,就像正常操作一样对其中的数据进行读写。因为进程可以直接读写内存,数据不需要来回复制,共享内存中的内容是在解除映射时才写回文件的,所以是最快的一种进程间通信机制。共享内存可以通过mmap()系统调用实现,应用接口和原理很简单,内部机制很复杂。在现代操作系统中,很多进程通信机制都是通过mmap实现的。实际应用中,普通文件被mmap()系统调用映射到内存空间后,调用fork()函数创建子进程,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()的返回地址。这样,父子进程就可以通过映射区进行通信了。相关进程间通过对共享存储区的访问可以进行大量的数据传输。某个共享存储区用完后,可以使用munmap()函数取消。

使用内存管理相关函数需要注意以下几点:

⑴mmap()系统调用与malloc()函数都涉及动态内存空间分配,它们的区别在于:

  ①mmap涉及文件和内存,malloc只涉及内存

②mmap以页(PAGE_SIZE)为单位分配,malloc以字节为单位分配;

③mmap是系统调用,malloc是库函数。

mmap()系统调用需要和munmap()系统调用配对使用

malloc()函数需要和free()函数配对使用,即申请的内存在使用后要释放,防止“内存泄漏”;

⑶内存空间释放后,将之前指向该区域的指针赋值为空(NULL),防止生成“野指针”。

七、附录

一、实验涉及函数及系统调用

1、getpagesize()

   头文件 <unistd.h>

   函数原型

     size_t  getpagesize(void)

   返回值:内存分页大小,单位为字节(byte)。

2、mmap()

   头文件 <sys/mman.h>

   函数原型

   void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset)

   功能:把文件fd(外存中)中从位移offset开始的length个字节映射到从start开始的内存空间。返回所映射到内存中的实际起始地址。(外存的东西拿到内存)

具体参数含义:
start :指向欲映射的内存起始地址,通常设为NULL,代表让系统自动选定地址,映

射成功后返回该地址。
length:代表将文件中多大的部分映射到内存。
prot  :映射区域的保护方式。可以为以下几种方式的组合:
        PROT_EXEC 映射区域可被执行
        PROT_READ 映射区域可被读取
         PROT_WRITE 映射区域可被写入
        PROT_NONE 映射区域不能存取
flags :影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或

MAP_PRIVATE。
    MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不

对地址做修正。通常不鼓励用此旗标。
    MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该

文件的进程共享。
    MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的

“写入时复制”(copy on write)对此区域作的任何修改都不

会写回原来的文件内容。
    MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映

射区域无法和其他进程共享。
    MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作

将会被拒绝。
    MAP_LOCKED 锁定映射区域,这表示该区域不会被置换(swap)。

fd:  要映射到内存中的文件描述符。

offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是

PAGE_SIZE的整数倍。

返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错

误原因存于errno中。

3、malloc()

头文件 <malloc.h>

函数原型

     void *malloc(size_t size)

返回值:如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。不能实现父子进程之间通信。

4、memmove()

头文件 <string.h>

函数原型

 extern void *memmove(void *dest, const void *src, unsigned int count)

功能:由src所指内存区域复制count个字节到dest所指内存区域。

说明:src和dest所指内存区域可以重叠,但复制后dest内容会被更改。函数返回指

      向dest的指针。

5、fork()

头文件 <unistd.h>,<sys/types.h>

函数原型   pid_t fork (void);

( pid_t 是一个宏定义,其实质是int。被定义在<sys/types.h>中 )

返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1。

6、memchr()

头文件 <string.h>

函数原型

     void * memchr(const void *src, char ch, size_t n)

功能:从头开始搜寻src 所指的内存内容前n 个字节,直到发现第一个值为ch的字节。

返回值:如果找到指定的字节,返回一个指针,指向ch 在字符串中首次出现的位置, 如果ch没有在字符串中找到,返回NULL。

7、getpid()

头文件 <unistd.h>

函数原型   pid_t   getpid(void)

功能:取得当前进程的PID。                  返回值:当前进程的PID。

8、getppid()

头文件 <unistd.h>

函数原型   pid_t   getppid(void)

功能:取得当前进程父进程的PID。            返回值:当前进程父进程的PID。

9、memset()

头文件 <memory.h>或<string.h>

函数原型   void *memset(void *s, char ch, int n)

功能:将s所指向的某一块内存中的前n个字节的内容全部设置为ch指定的ASCII值。就是在一段内存块中填充某个给定的值,它是对较大的结构体数组进行清零操作的一种最快方法。

返回值:指向S的指针。

10、munmap()

头文件 <sys/mman.h>

函数原型   int munmap(void * start,  size_t  length)

功能:取消参数start所指的映射内存起始地址,是 mmap()的反操作,其中 length 必须是 mmap() 时的 length。

返回值:如果解除映射成功则返回0,否则返回-1。

二、关于内存映射文件的一些资料

传统的文件访问技术是通过文件描述符读写文件,每次都需要经过系统调用(read/write),从外存读入的文件内容要先读入OS核心内存中的文件缓冲区中,然后再读入用户进程数据区。这样,核心内存文件缓冲区的内容与用户进程数据区的内容是重复存储的,相应的内存<-->内存的复制工作也会消耗部分时间,与直接访问内存相比速度要慢得多。随着内存容量的不断增大,进程虚拟空间也大幅度增加,于是人们想到能否利用内存加快文件的访问速度,这就是内存映射文件(memory mapping file,MMF)的由来。内存映射文件就是将文件映射到内存空间,然后以内存访问方式存取文件。内存映射文件所带来的另一个好处是利用共享文件实现进程间的高级通信

内存映射文件的原理是:把进程需要访问的文件映射到其虚地址空间中。可通过读写这段虚地址进行文件访问,将磁盘访问转变成内存访问。也就是说,把磁盘中的文件视为进程虚地址的一部分,故也称映射文件I/O

在具体实现上,系统提供一对系统调用,一个是mmap,功能是将指定文件映射到内存中;另一个是munmap,功能是撤销指定地址范围的映射,把映射文件从地址空间中移走,然后退出。

mmap与传统read/write相比最大的优点是,它绕过了OS核心内存的文件缓冲区,从外存读入的文件内容直接进入用户进程数据区,消除了核心内存文件缓冲区与用户进程数据区的重复存储(空间浪费)与相应的复制工作(时间浪费)。

在支持分段的系统中,文件映射工作得最好。在这样的系统中,每个文件可以映射到它自己的逻辑段中,在文件中的字节k也就是段中的字节k。

分页系统中,根据文件大小分配若干页表表项,这些页面与其他页面一样进行处理,即如果页面不在内存,会触发缺页中断,系统便从磁盘文件中加载被引用的页面。反之,当页面从内存回收时要把其中的信息写回相应磁盘文件。所以,每个页表表项指向一个页框,同时也指向一个磁盘块。

多进程共享文件时,可同时把同一文件映射到各自的虚地址空间中,且虚地址不必相同。随着进程的运行,被引用的映射文件由存储管理程序装入内存,共享映射文件的进程的虚页面指向相同的内存块,而内存块中只保存着磁盘文件的一个页面副本,进程读写虚存内容相当于执行文件读写操作,而不再需要使用文件系统调用来读写数据,既节省空间,又无需执行缓冲到内存的复制操作。多个进程读写虚存的映射文件区就可以共享文件信息。其优点是方便易用、节省空间、便于共享、利于通信

mmap()还非常利于进程通信的高效实现。现代操作系统中,包括共享存储器在内的很多进程通信机制都是通过基于mmap()的内存映射文件来实现的,即映射同一文件到通信进程的虚拟地址空间,使其充当通信进程之间的共享存储区。

  • 28
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值