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

news2024/11/16 7:28:07

目录

一、实验目的

二、实验内容

(一)跟踪地址翻译过程

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

(三)共享内存的实现

三、实验准备

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:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/692947.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TS学习笔记——模块

模块 module 模块&#xff1a;在自身的作用域里执行&#xff0c;不是在全局作用域中&#xff0c;若是外部想要使用需要导入导出。 好处&#xff1a;1、提高了代码的复用性 2、解决命名冲突 3、提高代码的可维护性 模块导入导出&#xff1a;export &#xff08;interface…

同步和异步、同步复位、异步复位、同步释放

文章目录 同步和异步同步复位、异步复位、同步释放同步复位异步复位同步释放&#xff08;异步信号和CLK信号存在时序检查、Recover time&Removel time&#xff09;典型的异步复位同步释放的verilog电路设计 同步和异步 数字电路根据逻辑功能的不同特点&#xff0c;可以分成…

如何判断端口有没有被占用

第一步&#xff1a;打开cmd 输入netstat -ano 可以查看自己的所有端口&#xff0c;第一行的本地地址是端口22&#xff0c;最右面对应的pid13260 第二步&#xff1a;打开任务管理器对应的PID就知道那个服务项占用了22端口了 第二种查看指定方法&#xff1a; netstat -ano|finds…

将数据库与LLMs结合,增强模型的长期记忆能力--ChatDB

ChatDB: AUGMENTING LLMS WITH DATABASES AS THEIR SYMBOLIC MEMORY 返回论文和资料目录 论文地址 项目地址 1.导读 清华团队针对大模型LLMs的长期记忆能力进行的改进。改进思路是将LLMs与数据库结合&#xff0c;将信息以符号化的形式存储在数据库中。同时&#xff0c;使用大…

极致呈现系列之:Echarts日历坐标系的时光流转

目录 什么是日历坐标系Echarts日历坐标系的特性Echarts日历坐标系的应用场景Echarts日历坐标系中常用的配置项Vue3中使用Echats日历坐标系实现健康可视化图表日历饼图 什么是日历坐标系 日历坐标系是一种用于展示时间数据的坐标系&#xff0c;将数据按照日期呈现在一个日历形式…

「Java核心技术大会 2023」6月重磅启动,邀你共同探讨Java生态 ~文末福利

Java核心技术大会 2023 大会简介直播预约&#xff1a;视频号“IT阅读排行榜”PART 1 特邀启动专场PART 2 Java语言、平台和趋势专场PART 3 Java应用开发专场PART 4 Java应用与系统架构专场PART 5 Java应用性能优化专场PART 6 大数据与数据库专场PART 7 云原生与Serverless专场P…

STM32学习 6月27日

51单片机中有时钟和时钟树的概念&#xff0c;外设只有GPIO、定时器、和一个串口&#xff0c;使用的都是11.0592MHZ的频率&#xff0c;除了定时器外&#xff0c;其他外设只要上电就可以使用。 stm32每个外设都有对应控制的开关&#xff0c;其所使用的频率各不相同&#xff0c;需…

[Windows] ImageGlass Kobe v8.9便携版

这款ImageGlass Kobe 神仙看图软件&#xff0c;UI漂亮&#xff0c;而且官方收费95元限时免费&#xff0c;打开大图速度极快&#xff0c;界面简洁纯净无广告&#xff0c;简直就是我的梦中情软&#xff0c;看图体验真的嘎嘎好&#xff01;是优秀的 Windows 照片查看器替代品。 具…

数据库监控与调优【十一】—— 索引调优技巧

索引调优技巧 长字段的索引调优使用组合索引的技巧覆盖索引排序优化冗余、重复索引的优化 长字段的索引调优 举例 实际项目中&#xff0c;我们可能需要给很长的字段添加索引。 比如以下first_name字段里面存储的数据普遍在200以上。 SELECT* FROMemployees WHEREfirst_…

Qt 动态手势识别“握拳”

系列文章目录 通过Qt实现手势识别控制软件操作相关系列技术方案 &#xff08;一&#xff09;Qt 将某控件、图案绘制在最前面的方法&#xff0c;通过QGraphicsScene模块实现 &#xff08;二&#xff09;Qt QGraphicsScene模块实现圆点绘制在所有窗体的最前方&#xff0c;实现圆…

永磁同步电机无位置传感器控制,采用的是龙贝格,基于模型的 定点开发,仿真效果和实际95%高度吻合

永磁同步电机无位置传感器控制&#xff0c;采用的是龙贝格&#xff0c;基于模型的 定点开发&#xff0c;仿真效果和实际95%高度吻合&#xff0c;可以仿真学习&#xff0c;也可以直接移植到项目中 YID:32768642079012761

深度学习框架背景

深度学习框架背景 本文目录&#xff1a; 一、深度学习框架概念 二、为何要用深度学习框架 三、掌握深度学习框架要做哪些准备 四、深度学习主要应用场景 五、常见深度学习框架的对比 六、深度学习框架在市场上的占比 七、中国深度学习开源框架状况 八、备注 一、深度学…

redis------Hash操作(字典)

Hash操作&#xff0c;redis中Hash在内存中的存储格式如下图&#xff1a; # hash类型就是咱们python中的字典&#xff0c;key-value,字典又叫hash类型 字典的key必须可hash -字典类型在底层存储&#xff0c;基于数组存的 key---{key:value,key:value} hset(name, key, val…

【Vue】CI持续集成Vue前端项目--20230627

我的实践&#xff08;CI&#xff09; 1.打包 npm run build&#xff0c;产生dist文件 2.将dist文件的内容拷贝到static下面。不用在nginx文件夹中 3.编写nginx配置 default.conf server {listen 80;listen [::]:80;server_name localhost;location / {root /usr/…

【MySQL数据库】MHA高可用配置及故障切换

目录 一、MHA简介1.1什么是MHA1.2MHA的组成1.3MHA的特点 二、搭建MHA2.1故障模拟2.2故障修复 一、MHA简介 1.1什么是MHA MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。MHA 的出现就是解决MySQL 单点的问题。M…

掌握构造函数:打破面向对象编程难题

文章目录 I. 介绍解释构造函数的概念及其作用为什么构造函数是面向对象编程中至关重要的一部分 II. 创建构造函数介绍如何定义并创建一个构造函数着重介绍构造函数的语法和使用方法定义一个构造函数创建一个对象实例使用对象实例继承和原型 III. 构造函数的参数介绍构造函数的参…

模拟测试 青少年软件编程(Python)等级考试试卷(一级) 试题解析

【单选题】&#xff08;每题2分&#xff09; 1、与a>b and b>c等价的是&#xff1f;&#xff08; &#xff09; A、a>b or b>c B、a>b>c C、a>b or not b D、not a 正确答案&#xff1a;B 试题解析&#xff1a;a>b并且b>c&#xff0c;那么a…

施工作业票管理

施工作业票管理是建筑行业中的一项重要管理&#xff0c;它是指在进行安全施工前&#xff0c;必须依据工程特点及安全要求&#xff0c;制定符合施工现场实际情况的施工作业方案&#xff0c;并按照程序进行作业申请及作业批准的程序书。比如说吊装作业票&#xff0c;高空作业票&a…

控制请求来源的HTML Meta标签 - Referrer详解

83. 控制请求来源的HTML Meta标签 - Referrer详解 在Web开发中&#xff0c;为了保护用户的隐私和安全&#xff0c;控制请求的来源信息是至关重要的。HTML中的<meta>标签提供了一种简单而有效的方式来控制请求的来源&#xff0c;其中包括Referrer&#xff08;引荐者&…

NVIDIA Jetson TX1,TX2,TX2 NX,AGX Xavier 和 Nano开发板GPIO口配置及应用

NVIDIA Jetson TX1&#xff0c;TX2&#xff0c;TX2 NX&#xff0c;AGX Xavier 和 Nano开发板GPIO口配置及应用 简介 ​ Jetson TX1、TX2、TX2 NX、AGX Xavier和Nano开发板都配备了一个包含40个引脚的GPIO接头&#xff0c;这些引脚可以用于数字输入输出。如下图 ​ 类似于Rasp…