Bootstrap

《操作系统》by李治军 | 实验7 - 地址映射与共享

目录

一、实验目的

二、实验内容

(一)跟踪地址翻译过程

(二)基于共享内存的生产者—消费者程序

(三)共享内存的实现

三、实验准备

1. Linux 中的共享内存

2. 获得空闲物理页面

3. 地址映射

4. 寻找空闲的虚拟地址空间

四、实验过程

(一)跟踪地址翻译过程

0. 编写 test.c

1. 准备

2. 暂停

3. 段表

4. 段描述符

5. 段基址和线性地址

6. 页表

7. 物理地址

8. 结束程序

(二)实现共享内存

1. 添加系统调用

2. 实现系统调用

(三)基于共享内存的生产者—消费者程序

1. producer.c

2. cosumer.c

3. 挂载文件

4. 编译并运行

5. 运行结果


一、实验目的

1、深入理解操作系统的段、页式内存管理,深入理解段表、页表、逻辑地址、线性地址、物理地址等概念。

2、实践段、页式内存管理的地址映射过程。

3、编程实现段、页式内存管理上的内存共享,从而深入理解操作系统的内存管理。

二、实验内容

(一)跟踪地址翻译过程

       用 Bochs 调试工具跟踪 Linux 0.11 的地址翻译(地址映射)过程,了解 IA-32 和 Linux 0.11 的内存管理机制。

       首先以汇编级调试的方式启动 Bochs,引导 Linux 0.11,在 0.11 下编译和运行 test.c(它是一个无限循环的程序,永远不会主动退出)。然后在调试器中通过查看各项系统参数,从逻辑地址、LDT 表、GDT 表、线性地址到页表,计算出变量 i 的物理地址。最后通过直接修改物理内存的方式让 test.c 退出运行。

【test.c】

#include <stdio.h>

int i = 0x12345678;
int main(void)
{
    printf("The logical/virtual address of i is 0x%08x", &i);
    fflush(stdout);
    while (i)
        ;
    return 0;
}

(二)基于共享内存的生产者—消费者程序

在 Ubuntu 上编写多进程的生产者—消费者程序,用共享内存做缓冲区

本项实验在 Ubuntu 下完成,与信号量实验中的 pc.c 的功能要求基本一致,仅有两点不同:

  • 不用文件做缓冲区,而是使用共享内存
  • 生产者和消费者分别是不同的程序。生产者是 producer.c,消费者是 consumer.c。两个程序都是单进程的,通过信号量和缓冲区进行通信

Linux 下,可以通过 shmget() 和 shmat() 两个系统调用使用共享内存。

(三)共享内存的实现

在信号量实验的基础上,为 Linux 0.11 增加共享内存功能,并将生产者—消费者程序移植到 Linux 0.11。

进程之间可以通过页共享进行通信,被共享的页叫做共享内存,结构如下图所示:

本部分的实验内容是在 Linux 0.11 上实现上述的页面共享,并将之前实现的 producer.c 和 consumer.c 移植过来,验证页面共享的有效性。

【具体要求】

在 mm/shm.c 中实现 shmget()  shmat() 两个系统调用,能支持 producer.c  consumer.c 的运行即可,不需要完整地实现 POSIX 所规定的功能。

  • shmget()
int shmget(key_t key, size_t size, int shmflg);
功能新建/打开一页内存,并返回该页共享内存的 shmid(该块共享内存在操作系统内部的 id )
参数

所有使用同一块共享内存的进程都要使用相同的 key 参数

shmflg 参数可忽略

返回值

如果 key 所对应的共享内存已经建立,则直接返回 shmid

如果 size 超过一页内存的大小,返回 -1,并置 errno 为 EINVAL

如果系统无空闲内存,返回 -1,并置 errno 为 ENOMEM

  • shmat()
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能shmid 指定的共享页面映射到当前进程的虚拟地址空间中,并返回其首地址
参数

shmid:该块共享内存在操作系统内部的 id

shmaddr 和 shmflg 参数可忽略

返回值如果 shmid 非法,返回 -1,并置 errno 为 EINVAL

三、实验准备

1. Linux 中的共享内存

       Linux 支持两种方式的共享内存。一种方式是 shm_open()mmap()  shm_unlink() 的组合;另一种方式是 shmget()shmat() 和 shmdt() 的组合。这些系统调用的详情,请查阅 man 及相关资料。本实验建议使用后一种方式。

【特别提醒】没有父子关系的进程之间进行共享内存,shmget() 的第一个参数 key 不要用 IPC_PRIVATE ,否则无法共享。用什么数字可视心情而定。

2. 获得空闲物理页面

       实验者需要考虑如何实现页面共享。首先看一下 Linux 0.11 如何操作页面,如何管理进程地址空间。

在 kernel/fork.c 中有:

int copy_process(…)
{
    struct task_struct *p;
    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
//    ……
}

其中 get_free_page() 用来获得一个空闲物理页面,在 mm/memory.c 中:

unsigned long get_free_page(void)
{
    register unsigned long __res asm("ax");
    __asm__("std ; repne ; scasb\n\t"
            "jne 1f\n\t"
            "movb $1,1(%%edi)\n\t"
            // 页面数*4KB=相对页面起始地址
            "sall $12,%%ecx\n\t"
            // 在加上低端的内存地址,得到的是物理起始地址
            "addl %2,%%ecx\n\t"
            "movl %%ecx,%%edx\n\t"
            "movl $1024,%%ecx\n\t"
            "leal 4092(%%edx),%%edi\n\t"
            "rep ; stosl\n\t"
            //edx赋给eax,eax返回了物理起始地址
            "movl %%edx,%%eax\n"
            "1:" :"=a" (__res) :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
            "D" (mem_map+PAGING_PAGES-1):"di","cx","dx");
    return __res;
}

static unsigned char mem_map [ PAGING_PAGES ] = {0,};

显然 get_free_page 函数就是在 mem_map 位图中寻找值为 0 的项(空闲页面),然后返回该页面的起始物理地址。

3. 地址映射

       有了空闲的物理页面,接下来需要完成线性地址和物理页面的映射,Linux 0.11 中也有这样的代码,看看 mm/memory.c 中的 do_no_page(unsigned long address) ,该函数用来处理线性地址 address 对应的物理页面无效的情况(即缺页中断),do_no_page 函数中调用一个重要的函数 get_empty_page(address),其中有:

// 函数 get_empty_page(address)
    ……

    unsigned long tmp=get_free_page();
    // 建立线性地址和物理地址的映射
    put_page(tmp, address);

    ……

这两条语句就用来获得空闲物理页面,然后填写线性地址 address 对应的页目录和页表。

4. 寻找空闲的虚拟地址空间

       有了空闲物理页面,也有了建立线性地址和物理页面的映射,但要完成本实验还需要能获得一段空闲的虚拟地址空间。

       要从数据段中划出一段空间,首先需要了解进程数据段空间的分布,而这个分布显然是由 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+PAGE_SIZE -1;
    code_limit &= 0xFFFFF000;
    //code_limit为代码段限长=text_size对应的页数(向上取整)
    data_limit = 0x4000000; //数据段限长64MB
    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":: );

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

仔细分析 change_ldt 函数,分析如何从数据段中找到一页空闲的线性地址。

四、实验过程

(一)跟踪地址翻译过程

0. 编写 test.c

在 Ubuntu 下编写 test.c ,然后拷贝到 Linux 0.11 系统。

// oslab 目录下
sudo ./mount-hdc
cp ./exp_07/test.c ./hdc/usr/root/
sudo umount hdc/

1. 准备

编译好 Linux 0.11 后,首先通过运行以下命令启动调试器。

./dbg-asm

此时 Bochs 的窗口处于黑屏状态:

此时终端显示如下:

其中 Next at t=0 表示下面的指令是 Bochs 启动后要执行的第一条软件指令,此时单步跟踪进去就能看到 BIOS 的代码,不过这不是本实验需要的。

这里我们直接输入命令 c,即 continue ,继续运行程序,Bochs 就会一如既往地启动 Linux 0.11。

Linux 0.11 成功启动!

现在我们在 Linux 0.11 下编译并运行 test.c 

只要 test.c 不变,0x00003004(逻辑/虚拟地址)这个值在任何人的机器上都是一样的,即使在同一个机器上多次运test 程序,也是一样的。

另外 test 程序是一个死循环,只会不停占用 CPU,并不会退出。

2. 暂停

当 test 程序运行的时候,在终端命令行窗口按下 Ctrl + c ,Bochs 就会暂停运行,并进入调试状态。

绝大多数情况下都会暂停在 test 程序内,显示类似如下的信息:

(0) [0x00fc8031] 000f:00000031 (unk. ctxt): cmp dword ptr ds:0x3004, 0x00000000 ; 833d0430000000
  • 如果其中的 000f 是 0008,则说明中断在了内核里。此时就要 c 继续运行,然后再 Ctrl+c 暂停,直到变为 000f 为止。
  • 如果显示的下一条指令不是 cmp ...(指语句以 cmp 开头),就用 n 命令单步运行几步,直到停在 cmp ...

接下来使用命令 u /8 ,显示从当前位置开始的 8 条指令的反汇编代码,结构如下:

上面 8 条指令就是 test.c 中从 while 开始一直到 return 的汇编代码。其中变量 i 保存在 ds:0x3004 这个地址,并不停地和 0 进行比较,直到它为 0,才会跳出循环。

接下来,我们开始寻找逻辑地址 ds:0x3004 对应的物理地址。

3. 段表

       ds:0x3004 是一个虚拟地址,其中 ds 表明这个地址属于 ds 段,0x3004 是段内偏移。我们首先要找到段表,然后通过 ds 的值在段表中找到 ds 段的具体信息,才能继续进行地址翻译。

       每一个在 IA-32(Intel Architecture 32-bit)上运行的应用程序都有一个段表,叫 LDT(局部描述符表),段的信息叫段描述符。

       那么 LDT 在哪里呢 —— ldtr 寄存器是线索的起点,通过它可以在 GDT(全局描述符表)中找到 LDT 的物理地址。

通过 sreg 命令(调试窗口下输入)查看各个寄存器的值:

可以看到 ldtr 的值是 0x0068,转换为二进制就是 0000000001101000,表示 LDT 表存放在 GDT 表的 1101(二进制)= 13(十进制)号位置(每位数据的意义参考后文叙述的段选择子)。而 GDT 表的位置由 gdtr 给出,即物理地址的 0x00005cb8

利用 xp /32w 0x00005cb8 查看从物理地址 0x00005cb8始的 32 个字(128 字节)的内容,即 GDT 表的前 16 项,如下:

其中 GDT 表中的每一项占 64 位(8 个字节,即 2 个字),我们已经知道 LDT 表存放在 GDT 表的 13 号位置,所以要查找的项的地址就是 0x00005cb8+13*8

输入 xp /2w 0x00005cb8+13*8,得到:

上面两个数值可能和这里的截图数值不一致,这是很正常的。如果想确认是否正确,就看之前 sreg 的输出中,ldtr 所在行里 dl 和 dh 的值,它们是 Bochs 的调试器自动计算出的,你寻找到的值必须和它们一致,否则一定是找错位置了。

现在将 0x92d00068 和 0x000082fd 中的加粗数字组合为 0x00fd92d0 ,这就是 LDT 表的物理地址(为什么这么组合,参考后文介绍的段描述符)。

xp /8w 0x00fd92d0,得到 LDT 表的前 8 个字内容:

以上就是 LDT 表的前 4 项内容(一项占 2 个字)。

4. 段描述符

保护模式下,段寄存器有另一个名字 —— 段选择子,因为它保存的信息主要是该段在段表里的索引值,用这个索引值可以从段表中 “选择” 出相应的段描述符。

先看看 ds 选择子的内容,还是用 sreg 命令:

可以看到,ds 的值是 0x0017 ,而段选择子 ds 是一个 16 位寄存器,它各位的含义如下图: 

  • RPL 是请求特权级,当访问一个段时,处理器要检查 RPL 和 CPL(放在 cs 的位 0 和位 1 中,用来表示当前代码的特权级),即使程序有足够的特权级(CPL)来访问一个段,但如果 RPL(放在 ds 中,表示请求数据段)的特权级不足,则仍不能访问,即如果 RPL 的数值大于 CPL(数值越大,权限越小),则用 RPL 的值覆盖 CPL 的值。
  • TI 是表指示标记,如果 TI=0,则表示段描述符(段的详细信息)在 GDT(全局描述符表)中,即去 GDT 中去查;而 TI=1,则去 LDT(局部描述符表)中去查。

       我们再看上面的 ds,0x0017 = 0000000000010111(二进制),所以 RPL=11,可见是在最低的特权级(因为在应用程序中执行),TI=1,表示查找 LDT 表,索引值为 10(二进制)= 2(十进制),表示找 LDT 表中编号为 2 的段描述符(从 0 开始编号,所以是第 3 项)。

       LDT 和 GDT 的结构一样,每个表项占 8 个字节,所以 LDT 表中的第 3 项就是 0x00003fff 0x10c0f300 ,也就是搜寻好久的 ds 的段描述符了。

我们可以通过 sreg 输出中 ds 所在行的 dldh 值验证找到的描述符是否正确:

接下来看看段描述符里面放置的是什么内容:

可以看出,段描述符是一个 64 位二进制的数,存放了段基址和段限长等重要的数据,其中:

  • 位 P(Present)是段是否存在的标记
  • 位 S 用来表示是系统段描述符(S=0)还是代码或数据段描述符(S=1)
  • 四位 TYPE 用来表示段的类型,如数据段、代码段、可读、可写等
  • DPL 是段的权限,和 CPL、RPL 对应使用
  • 位 G 是粒度,G=0 表示段限长以位为单位,G=1 表示段限长以 4KB 为单位

其他内容就不详细解释了。

  

5. 段基址和线性地址

       费了很大的劲,实际上我们需要的只有段基址这一项数据,即段描述符中的 3 个基地址。将段描述符 0x00003fff 和 0x10c0f300 中加粗部分(基地址)组合成 0x10000000 ,就是 ds 段在线性地址空间中的起始地址。用同样的方法也可以得到其它段的基址,都是这个数。

段基址 + 段内偏移 = 线性地址

所以 ds:0x3004 的线性地址就是:

0x10000000 + 0x3004 = 0x10003004

用 calc ds:0x3004 命令可以验证这个结果:

6. 页表

从线性地址到物理地址,需要查找页表

线性地址变成物理地址的过程如下:

首先需要算出线性地址中的页目录号、页表号和页内偏移,它们分别对应了 32 位线性地址的 10 位 + 10 位 + 12 位。

所以线性地址 0x10003004 = 10000000000000011000000000100(二进制)的页目录号就是 64(1000000),页表号为 3(0000000011),页内偏移为 4(000000000100)。

在 IA-32 下,页目录表的位置由 CR3 寄存器指引,通过 creg 命令查看: 

说明页目录表的基址为 0,通过 xp /68w 0 查看页目录表前 68 个字内容:​​

页目录表和页表中的内容很简单,是 1024 个 32 位数(正好是 4K)。这 32 位中前 20 位是物理页框号,后面是一些属性信息(其中最重要的是最后一位 P)。其中页目录号 64 就是第 65 个页目录项(从 0 开始编号),也就是我们要找的内容,通过 xp /w 0+64*4 查看:

以上就是我们要找的页目录表项,表项的具体结构如下图:

可以看出页表所在的物理页框号为 0x00fa7 ,即页表在物理内存为 0x00fa7000 处,所以从该位置开始查找 3 号页表项(每个页表项 4 个字节,即 1 个字),通过命令 xp /w 0x00fa7000+3*4 查看:

页目录表和页表的表项格式是一样的,所以 067属性,fa6 就是对应的物理页框号。

7. 物理地址

线性地址 0x10003004 对应的物理页框号为 0x00fa6,接着和页内偏移 0x004 接到一起,得到 0x00fa6004 ,就是变量 i 的物理地址,可以通过两种方法验证。

方法一:通过命令 page 0x10003004,可以得到信息:

可以看到线性地址 0x10003000 对应的物理页地址就是 0x00fa6000

方法二:通过命令 xp /w 0x00fa6004,可以得到信息:

这个数值就是 test.c 中 i 的初值。

8. 结束程序

只要 i 不为 0 ,test 程序就不会停止,所以我们通过直接修改内存来改变 i 的值为 0 以结束 test 程序。

setpmem 0x00fa6004 4 0

上面指令表示从地址 0x00fa6004 开始的 4 个字节都设为 0 。然后再用 c 命令继续 Bochs 的运行,如果 test 退出了,说明 i 的修改成功了,此项实验结束。

test 程序成功退出!

(二)实现共享内存

这里实现共享内存就是实现 shmget()  shmat() 两个系统调用,能支持 producer.c  consumer.c 的运行即可,不需要完整地实现 POSIX 所规定的功能。

1. 添加系统调用

接下来我们开始添加 shmget 和 shmat 的系统调用,参照系统调用实验。

(1)添加系统调用编号 - linux-0.11/include/unistd.h

(2)修改系统调用总数 - linux-0.11/kernel/system_call.s

  

(3)添加系统调用函数名并维护系统调用表 - linux-0.11/include/linux/sys.h

 

2. 实现系统调用

  • sys_shmget() 函数的主要作用是获得一个空闲的物理页面,可通过调用已有的 get_free_page 实现。
  • sys_shmat() 函数的主要作用是将这个页面和进程的虚拟地址以及逻辑地址关联起来,让进程对某个逻辑地址的读写就是在读写该内存页。该函数首先要完成虚拟地址和物理页面的映射,核心就是填写页表,可通过调用已有的 put_page 实现。

 (1)编写 shm.h
include 目录下新建一个 shm.h 文件,用于相关数据类型声明和函数声明。

#include <stddef.h>     

typedef unsigned int key_t;

struct struct_shmem
{
    unsigned int size;
    unsigned int key;
    unsigned long page;
};

int shmget(key_t key, size_t size);
void* shmat(int shmid);

#define SHM_NUM  16 

(2)编写 shm.c

kernel 目录下新建一个 shm.c 文件,用于实现这个两个函数。

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

struct struct_shmem shm_list[SHM_NUM] = {{0,0,0}};

int sys_shmget(key_t key, size_t size)
{
    int i;
    unsigned long page;
    
    if(size > PAGE_SIZE)
    {
        errno = EINVAL;
        printk("shmget:The size connot be greater than the PAGE_SIZE!\r\n");
        return -1;
    }

    if(key == 0)
    {
        printk("shmget:key connot be 0!\r\n");
        return -1;
    }

    //判斷是否已经创建
    for(i = 0; i < SHM_NUM; i++)
    {
        if(shm_list[i].key == key)
            return i;
    }

    page = get_free_page();  //申请内存页
    if(!page)
    {
        errno = ENOMEM;
        printk("shmget:connot get free page!\r\n");
        return -1;
    }

    for(i = 0; i < SHM_NUM; i++)
    {
        if(shm_list[i].key == 0)
        {
            shm_list[i].size = size;
            shm_list[i].key = key;
            shm_list[i].page = page;
            break;
        }
    }
    return i;
}


void* sys_shmat(int shmid)
{
    unsigned long tmp;  //虚拟地址
    unsigned long logicalAddr;
    if(shmid < 0 || shmid >= SHM_NUM || shm_list[shmid].page == 0 || shm_list[shmid].key <= 0)
    {
        errno = EINVAL;
        printk("shmat:The shmid id invalid!\r\n");
        return NULL;
    }
    tmp = get_base(current->ldt[1]) + current->brk;  //计算虚拟地址
    put_page(shm_list[shmid].page,tmp);
    logicalAddr = current->brk;  //记录逻辑地址
    current->brk += PAGE_SIZE;  //更新brk指针
    return (void *)logicalAddr;
}

(3)挂载文件

将编写的 shm.h 和修改后的 unistd.h 都拷贝到 Linux 0.11 系统中。

// oslab 目录下
sudo ./mount-hdc
cp ./linux-0.11/include/unistd.h ./hdc/usr/include/
cp ./linux-0.11/include/shm.h ./hdc/usr/include/
sudo umount hdc/

(4)修改文件编译规则

对 linux-0.11/kernel 目录下的 Makefile 进行如下修改。

shm.s shm.o: shm.c ../include/unistd.h ../include/linux/kernel.h \
../include/linux/sched.h ../include/linux/mm.h ../include/errno.h

(5)重新编译 Linux 0.11

// linux-0.11 目录下
make all

(三)基于共享内存的生产者—消费者程序

该部分程序与信号量实验中的 pc.c 的功能要求基本一致,仅有两点不同:

  • 不用文件做缓冲区,而是使用共享内存
  • 生产者和消费者分别是不同的程序。生产者是 producer.c,消费者是 consumer.c。两个程序都是单进程的,通过信号量和缓冲区进行通信

1. producer.c

/* producer.c */

#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>
#include <linux/kernel.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(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

sem_t *Empty,*Full,*Mutex;

int main(int argc, char* argv[])
{
    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!");

	printf("producer start.\n");
	fflush(stdout);

    for(i=0; i<PRODUCE_NUM; i++)
    {
        sem_wait(Empty);
        sem_wait(Mutex);

        p[location] = i;

        printf("pid %d:\tproducer produces item %d\n", getpid(), p[location]);
        fflush(stdout);

        sem_post(Mutex);
        sem_post(Full);
        location  = (location+1) % BUFFER_SIZE;
    }

	printf("producer end.\n");
	fflush(stdout);

    /* 释放信号量 */
    sem_unlink("Full");
    sem_unlink("Empty");
    sem_unlink("Mutex");

    return 0;    
}

2. cosumer.c

/* consumer.c */

#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>
#include <linux/kernel.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(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

sem_t *Empty,*Full,*Mutex;

int main(int argc, char* argv[])
{
    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");

	printf("consumer start.\n");
	fflush(stdout);

    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;
    }

	printf("consumer end.\n");
	fflush(stdout);

    /* 释放信号量 */
    sem_unlink("Mutex");
    sem_unlink("Full");
    sem_unlink("Empty");

    return 0;    
}

3. 挂载文件

将 producer.c 和 consumer.c 拷贝到 Linux 0.11 系统中。

sudo ./mount-hdc
cp ./exp_07/producer.c ./hdc/usr/root/
cp ./exp_07/consumer.c ./hdc/usr/root/
sudo umount hdc/

4. 编译并运行

gcc -o pro producer.c
gcc -o con consumer.c

./pro > pro.txt &
./con > con.txt

编译时出现的 warning 问题不大,我们将程序运行的结果重定向到 txt 文件中进行查看。

如何在同一终端中同时运行两个程序?

Linux 的 shell 有后台运行程序的功能。只要在命令的最后输入一个 & ,命令就会进入后台运行,前台马上回到提示符,进而能运行下一个命令,例如:

$ sudo ./producer &
$ sudo ./consumer

当运行 ./consumer 的时候,producer 正在后台运行。

5. 运行结果

直接退出 bochs,挂载后将 pro.txt 和 con.txt 拷贝到 Ubuntu 下查看。

sudo ./mount-hdc
sudo cp ./hdc/usr/root/pro.txt ./exp_07
sudo cp ./hdc/usr/root/con.txt ./exp_07
sudo chmod 777 exp_07/pro.txt
sudo chmod 777 exp_07/con.txt

pro.txt:

con.txt:

;