基于共享内存的生产者-消费者程序:
- 进行本次实验需要先完成实验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
中。brk
和 start_stack
之间的空间为栈准备,栈底是闲置的,可将共享内存映射到这块空间。
2、物理页面——>线性地址空间
该系统调用会将shmid
对应的共享内存页面映射到当前进程的虚拟地址空间中,并返回一个逻辑地址p,调用进程可以读写逻辑地址p来读写这一页共享内存。
两个进程都调用shmat
可以关联到同一页物理内存上,此时两个进程读写p
指针就是在读写同一页内存,从而实现了基于共享内存的进程间通信。
如果shmid
不正确,返回-1,并置errno
为EINVAL
。
函数 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源代码