实验6-地址映射与共享 进程间通信

基于共享内存的生产者-消费者程序:

  • 进行本次实验需要先完成实验5:信号量的实现和应用
  • 不用文件做缓冲区,而是使用共享内存
  • 不将生产者和消费者放置同一个文件pc.c,而是生产者producer.c,消费者consumer.c,两个程序都是单进程的,通过信号量共享缓冲区进行进程间的通信。

Linux下,可以通过shmget()shmat()两个系统调用使用共享内存,当然Linux 0.11本身是没有这两个系统调用的,要自己来实现。

shmget()系统调用的函数原型:

int shmget(key_t key, size_t size, int shmflg);

shmat()系统调用的函数原型为:

void * shmat(int shmid, const void * shmaddr, int shmflg);

两个进程都调用shmat可以关联到同一页物理内存上,此时两个进程读写p指针就是在读写同一页内存,从而实现了基于共享内存的进程间通信。

共享内存结构体shm_ds

函数 int shmget(key_t key, size_t size, int shmflg)会新建或打开一页内存,然后返回该页共享内存的 shmid
忽略 shmflg 参数后,可知一页共享内存需要保存的信息有

  • 唯一标识符 key
  • 共享内存的大小 size
  • 然后还需要一个参数保存共享内存页面的地址 page

于是共享内存信息的结构体如下:

建议挂载虚拟机,在(~/oslab/hdc/usr/include/linux)中创建shm.h,定义数据类型shm_ds:

typedef struct shm_ds
{
    unsigned int key;
    unsigned int size;
    unsigned long page;
}shm_ds;

在这里插入图片描述

shmget()函数

根据要求,该系统调用会新建或者打开一页物理内存作为共享内存,返回该共享内存的shmid
如果多个进程使用相同的key调用shmget(),则这些进程就会获得相同的shmid,即得到了同一块共享内存的标识

函数 get_free_page()能够(1获取一块空闲的物理页面,并且返回该页面的起始物理地址,用于 shmget()的实现。

kernel/shm.c的源码及注释

#define __LIBRARY__
#include <unistd.h>
#include <linux/shm.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>
#include <errno.h>

#define SHM_SIZE 64

static shm_ds shm_list[SHM_SIZE] = {{0,0,0}};

int sys_shmget(unsigned int key, size_t size)
{
	int i;
	unsigned long page;
	/* 如果`size`超过一页内存大小,返回-1,并置`errno`为`EINVAL`。 */
  	if(size>PAGE_SIZE){
      	printk("shmget: size %u cannot be greater than the page size %ud. \n", size, PAGE_SIZE);
      	return -EINVAL;
 	 }
 	 
 	 /* 如果`key`所对应的内存已经建立,直接返回`shmid` */
  	for(i=0; i<SHM_SIZE; i++){
      	if(shm_list[i].key == key)
          	return i;
  	}
	/* 获取空闲物理内存页面 */
	page = get_free_page();
	/* 如果系统没有空闲内存了,返回-1,并置`errno`为`ENOMEM`。*/
	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; /* 共享内存数量已满 */
}


shmat()函数

作用:
1、找到获得一段空闲的线性地址空间
2、把物理页面映射到指定线性地址空间处

1、进程数据段空间的分布

要从数据段中划出一段空间,首先需要了解进程数据段空间的分布,而这个分布显然是由 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;
    
    //数据段限长64MB
    data_limit = 0x4000000; 
    
    // 数据段基址 = 代码段基址
    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":: );

    // 从数据段的末尾开始 put_page 建立映射
    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;
}

可见,内核为每个进程虚拟了一块地址空间,然后分配数据段、代码段和栈段,由函数 do_execve()实现,虚拟空间的分配如下图:
在这里插入图片描述
其中 start_code为代码段起始地址,brk为代码段和数据段的总长度,bss是进程未初始化的数据段, start_stack为栈的起始地址,这些值保存在进程的 task_struct中。brkstart_stack之间的空间为栈准备,栈底是闲置的,可将共享内存映射到这块空间。

2、物理页面——>线性地址空间

该系统调用会将shmid对应的共享内存页面映射到当前进程的虚拟地址空间中,并返回一个逻辑地址p,调用进程可以读写逻辑地址p来读写这一页共享内存

两个进程都调用shmat可以关联到同一页物理内存上,此时两个进程读写p指针就是在读写同一页内存,从而实现了基于共享内存的进程间通信。

如果shmid不正确,返回-1,并置errnoEINVAL

函数 put_page()能够把物理页面映射到指定线性地址空间处,用于 shmat()的实现。

3、实现

有用的长度就是brk,即堆的开头,后面的就是你要申请的地方。

这里给出提示,

current->ldt[1]指向当前任务的代码段CS局部表描述符
current->ldt[2]指向数据堆栈段DS&SS局部表描述符。

/* 建立物理地址和线性地址的映射*/
put_page(shm_list[shmid].page, brk);

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基址,当前进程的虚拟地址*/
  	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在虚拟内存中的偏移 = brk的虚拟地址*/
  	brk = data_base + current->brk;
  	/* 紧接着空闲的一页,作为共享内存空间 */
  	current->brk += PAGE_SIZE;
  	/* 建立物理地址和线性地址的映射*/
  	if(put_page(shm_list[shmid].page, brk) == 0)
      	return (void *)-ENOMEM;

	/* 返回时返回首地址,所以要减去一个页面大小 */
  	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
  • 修改/kernel/system_call.s,需要修改总的系统调用的和值:
nr_system_calls = 80
  • 修改/include/linux/sys.h,声明新增函数
...
/* 添加的系统调用定义 */
#include<linux/sem.h>
extern sem_t * sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();

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运行:

生产者消费者程序

/*consumer*/ 
#define __LIBRARY__
#include <stdio.h>
#include <unistd.h>
#include <linux/kernel.h>
#include <fcntl.h>
#include <sys/types.h>

_syscall2(sem_t *,sem_open,const char *,name,int,value);
_syscall1(int,sem_post,sem_t *,sem);
_syscall1(int,sem_wait,sem_t *,sem);
_syscall1(int,sem_unlink,const char*,name);

_syscall1(int, shmat, int, shmid);
_syscall2(int, shmget, unsigned int, key, size_t, size);

#define PRODUCE_NUM 200
#define BUFFER_SIZE 10
#define SHM_KEY 2020

int main(int argc, char* argv[])
{
    sem_t *Empty,*Full,*Mutex;    
    int used = 0, shm_id,location = 0;
    int *p;
    
    Empty = sem_open("Empty", BUFFER_SIZE);
    Full = sem_open("Full", 0);
    Mutex = sem_open("Mutex", 1);
    
    if((shm_id = shmget(SHM_KEY, BUFFER_SIZE*sizeof(int))) < 0)
        printf("shmget failed!\n");    

    if((p = (int * )shmat(shm_id)) < 0)
        printf("link error!\n");
    while(1)
    {
        sem_wait(Full);
        sem_wait(Mutex);
        
        printf("pid %d:\tconsumer consumes item %d\n", getpid(), p[location]);
        fflush(stdout);
        
        sem_post(Mutex);
        sem_post(Empty);
        location  = (location+1) % BUFFER_SIZE;
        
        if(++used == PRODUCE_NUM)
            break;
    }

    sem_unlink("Mutex");
    sem_unlink("Full");
    sem_unlink("Empty");
    return 0;    
}

/*producer*/ 
#define __LIBRARY__
#include <stdio.h>
#include <unistd.h>
#include <linux/kernel.h>
#include <fcntl.h>
#include <sys/types.h>

_syscall2(sem_t *,sem_open,const char *,name,int,value);
_syscall1(int,sem_post,sem_t *,sem);
_syscall1(int,sem_wait,sem_t *,sem);

_syscall1(int, shmat, int, shmid);
_syscall2(int, shmget, unsigned int, key, size_t, size);

#define PRODUCE_NUM 200
#define BUFFER_SIZE 10
#define SHM_KEY 2018

int main(int argc, char* argv[])
{
    sem_t *Empty,*Full,*Mutex;    
    int i, shm_id, location=0;
    int *p;

    Empty = sem_open("Empty", BUFFER_SIZE);
    Full = sem_open("Full", 0);
    Mutex = sem_open("Mutex", 1);

    if((shm_id = shmget(SHM_KEY, BUFFER_SIZE*sizeof(int))) < 0)
        printf("shmget failed!");    
      
    if((p = (int * )shmat(shm_id)) < 0)
        printf("shmat error!");
    for(i=0; i<PRODUCE_NUM; i++)
    {
        sem_wait(Empty);
        sem_wait(Mutex);
        
        p[location] = i;
        
        sem_post(Mutex);
        sem_post(Full);
        location  = (location+1) % BUFFER_SIZE;
    }
    return 0;    
}

在这里插入图片描述

HIT-OS-LAB参考资料:
1.《操作系统原理、实现与实践》-李治军、刘宏伟 编著
2.《Linux内核完全注释》
3.哈工大同学的实验报告
4.Linux-0.11源代码

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值