4.25 日 操作系统课堂练习实验

4.25 日 操作系统课堂练习实验

“16281052 杨涵晨 计科1601 ”

TASK 1

1.1 题目

一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭,如图2-10所示。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿的时候,才试图拿起左、 右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

实现哲学家进餐问题,并测试何时发生死锁?

1.2 哲学家进餐问题分析
  1. 5名哲学家需要5个进程,且每一个都可以实现思考,拿筷子,进餐等功能。
  2. 需要编写对应函数来辅助实现这些函数
  3. 要考虑到死锁的产生和消除
  4. 如果所有哲学家都拿起左边的筷子,则一定会进行死锁

1.3 编程实现对应的功能函数
void take_forks(int id){
    //获取左右两边的筷子
    printf("Pil[%d], left[%d], right[%d]\n", id, left(id), right(id));
    pthread_mutex_lock(&forks[left(id)]);
    pthread_mutex_lock(&forks[right(id)]);
    //printf("philosopher[%d]  take_forks...\n", id);
}

void put_down_forks(int id){
    printf("philosopher[%d] is put_down_forks...\n", id);
    pthread_mutex_unlock(&forks[left(id)]);
    pthread_mutex_unlock(&forks[right(id)]);
}

void* philosopher_work(void *arg){
    int id = *(int*)arg;
    printf("philosopher init [%d] \n", id);
    while(1){
        thinking(id);
        take_forks(id);
        eating(id);
        put_down_forks(id);
    }
}
1.4 死锁避免的方法

避免死锁应该说,有很多种方法,这里分析三种

  • 方法1:利用AND 型信号量机制实现:根据课程讲述,在一个原语中,将一段代码同时需要的多个临界资源,要么全部分配给它,要么一个都不分配,因此不会出现死锁的情形。当某些资源不够时阻塞调用进程;由于等待队列的存在,使得对资源的请求满足FIFO 的要求, 因此不会出现饥饿的情形。
  • 方法2:利用信号量的保护机制实现。通过信号量mutex对eat()之前的取左侧和右侧筷子的操作进行保护,使之成为一个原子操作,这样可以防止死锁的出现
  • 方法3:至多四个人拿起左边筷子。。保证至少有一个人可以用餐,那么就能解决了,添加一个信号量room赋值等于4
1.5 实验步骤
  1. 根据方法三编写没有死锁的程序
    • every philosopher is in while loop: thinking -> take_forks -> eating -> put_down_forks -> thinking
    • 对于可能产生的死锁问题,我们这里采用一中解决的办法,那就是只有当哲学接的左右两只筷子均处于可用状态时,才允许他拿起筷子。这样就可以避免他们同时拿起筷子就餐,导致死锁。
    • 如果2号哲学家在吃饭那么1号和3号就必须是在思考.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#define N 5 // five philosopher
#define T_EAT 5
#define T_THINK 5
#define N_ROOM  4  //同一时间只允许4人用餐
#define left(phi_id) (phi_id+N-1)%N
#define right(phi_id) (phi_id+1)%N

enum { think , hungry , eat  }phi_state[N];
sem_t chopstick[N];
sem_t room;

void thinking(int id){
    sleep(T_THINK);
    printf("philosopher[%d] is thinking...\n", id);
}

void eating(int id){
    sleep(T_EAT);
    printf("philosopher[%d] is eating...\n", id);
}

void take_forks(int id){
    //获取左右两边的筷子
    //printf("Pil[%d], left[%d], right[%d]\n", id, left(id), right(id));
    sem_wait(&chopstick[left(id)]);
    sem_wait(&chopstick[right(id)]);
    //printf("philosopher[%d]  take_forks...\n", id);
}

void put_down_forks(int id){
    printf("philosopher[%d] is put_down_forks...\n", id);
    sem_post(&chopstick[left(id)]);
    sem_post(&chopstick[right(id)]);
}

void* philosopher_work(void *arg){
    int id = *(int*)arg;
    printf("philosopher init [%d] \n", id);
    while(1){
        thinking(id);
        sem_wait(&room);
        take_forks(id);
        sem_post(&room);
        eating(id);
        put_down_forks(id);
    }
}

int main(){
    pthread_t phiTid[N];
    int i;
    int err;
    int *id=(int *)malloc(sizeof(int)*N);

    //initilize semaphore
    for (i = 0; i < N; i++)
    {
        if(sem_init(&chopstick[i], 0, 1) != 0)
        {
            printf("init forks error\n");
        }
    }

    sem_init(&room, 0, N_ROOM);

    for(i=0; i < N; ++i){
        //printf("i ==%d\n", i);
        id[i] = i;
        err = pthread_create(&phiTid[i], NULL, philosopher_work, (void*)(&id[i])); //这种情况生成的thread id是0,1,2,3,4
        if (err != 0)
            printf("can't create process for reader\n");
    }

    while(1);

    // delete the source of semaphore
    for (i = 0; i < N; i++)
    {
        err = sem_destroy(&chopstick[i]);
        if (err != 0)
        {
            printf("can't destory semaphore\n");
        }
    }
    exit(0);
    return 0;
}
  1. 实验结果

  2. 修改代码查看何时发生死锁

  • 加入sleep()或者pthread_yield();

  • 取消room的信号量限制

void take_forks(int id){
    //获取左右两边的筷子
    //printf("Pil[%d], left[%d], right[%d]\n", id, left(id), right(id));
    sem_wait(&chopstick[left(id)]);
    sleep(2);
    sem_wait(&chopstick[right(id)]);
    //printf("philosopher[%d]  take_forks...\n", id);
}

void put_down_forks(int id){
    printf("philosopher[%d] is put_down_forks...\n", id);
    sem_post(&chopstick[left(id)]);
    sem_post(&chopstick[right(id)]);
}

void* philosopher_work(void *arg){
    int id = *(int*)arg;
    printf("philosopher init [%d] \n", id);
    while(1){
        thinking(id);
        //sem_wait(&room);
        take_forks(id);
        //sem_post(&room);
        eating(id);
        put_down_forks(id);
    }
}
  1. 产生死锁的实验结果

    ​ 可以明显看出,5个哲学家都拿起来左边的筷子导致了死锁

TASK 2

2.1 题目

malloc是在堆(heap)上进行内存分配的系统调用,设计一个实验逆向分析linux采用了何种分配策略,给出具体的解释

2.2 实验分析

heap堆分配在用户层面:malloc函数用于heap内存分配 。 heap内存分配必然属于linux内存分配的一种,因此我们先对liunx种的虚拟内存机制进行讲解

liunx会为每个进程创建一个虚拟内存,如下图所示 。

​ 虚拟内存的基本思想是:每个程序拥有自己的地址空间,每个空间被分割成多个块,每一块称作一页或页面,每一页有连续的地址范围,这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行,接下来就会涉及到很多操作系统书上讲到的置换与映射问题

刚好证明了上次我们对thread.h 种代码的理解。

对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:

  • Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
  • Data:这里存放的是初始化过的全局变量
  • BSS:这里存放的是未初始化的全局变量
  • Heap:堆,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
  • Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
  • Stack:这是栈区域,自高地址向低地址增长

malloc用于用户空间堆扩展的函数接口。该函数是C库,属于封装了相关系统调用(brk())的glibc库函数。而不是系统调用(系统可没有sys_malloc()。如果谈及malloc函数涉及的系统内核的那些操作,那么总体可以分为用户空间层面和内核空间层面。

而对于malloc分配的brk,mmap这两种方法,一般是根据门限来选择。

2.3 验证虚拟内存实验代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#define Testspace 10000 //Testspace为每次在堆内开辟的空间
void main()
{
    int *pointer;
    unsigned long int counter = 0, HeapCapacity = 0; //
    do
    {
        pointer = (int *)malloc(Testspace * sizeof(int)); //int 占4个字节
        counter = counter + 1;
        printf('------counter=%lu', counter); //为了验证共开辟了多少次堆
    } while (pointer != NULL);
    HeapCapacity = counter * Testspace * 4 / 1024 / 1024; //得出的单位是兆M,若要得到G,
    //还要再/1024
    printf('HeapCapacity is %luM', HeapCapacity);
    if (pointer == NULL)
        exit(1);
    free(pointer);
}

运行后,对Testspace采用不同的值,得到的结果几乎相同。

Testspace=10000,HeapCapacity=3064M;

Testspace=1000,HeapCapacity=3056M;

Testspace=5000,Heapcapacity=3063M;

Testspace=20000,HeapCapacity=3065M;

这里就可以发现,这是虚拟内存机制,来帮助我们的程序来映射和管理内存。

2.4 malloc分析实验代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
void *threadFunc(void *arg)
{
    printf("Before malloc in thread 1\n");
    getchar();
    char *addr = (char *)malloc(1000);
    printf("After malloc and before free in thread 1\n");
    getchar();
    free(addr);
    printf("After free in thread 1\n");
    getchar();
}

int main()
{
    pthread_t t1;
    void *s;
    int ret;
    char *addr;

    printf("Before malloc in main thread\n");
    getchar();
    addr = (char *)malloc(1000);
    printf("After malloc and before free in main thread\n");
    getchar();
    free(addr);
    printf("After free in main thread\n");
    getchar();
    ret = pthread_create(&t1, NULL, threadFunc, NULL);
    if (ret)
     {
        printf("Thread creation error\n");
        return -1;
     }

    ret = pthread_join(t1, &s);

    if (ret){
        printf("Thread join error\n");
        return -1;
     }
    return 0;
}

下面我们依次分析其各个阶段的堆内存分布状况。

  1. Before malloc in main thread :

在程序调用malloc之前程序进程中是没有heap segment的,并且在创建在创建线程前,也是没有线程堆栈的。

  1. After malloc in main thread :

在主线程中调用malloc之后,就会发现系统给程序分配了堆栈,且这个堆栈刚好在数据段之上

这就说明它是通过brk系统调用实现的。并且,还可以看出虽然我们只申请了1000字节的数据,但是系统却分配了132KB大小的堆,这是为什么呢?原来这132KB的堆空间叫做arena,此时因为是主线程分配的,所以叫做main arena(每个arena中含有多个chunk,这些chunk以链表的形式加以组织)。由于132KB比1000字节大很多,所以主线程后续再声请堆空间的话,就会先从这132KB的剩余部分中申请,直到用完或不够用的时候,再通过增加program break location的方式来增加main arena的大小。同理,当main arena中有过多空闲内存的时候,也会通过减小program break location的方式来缩小main arena的大小。

  1. After free in main thread :

在主线程调用free之后:从内存布局可以看出程序的堆空间并没有被释放掉,原来调用free函数释放已经分配了的空间并非直接“返还”给系统,而是由glibc 的malloc库函数加以管理。它会将释放的chunk添加到main arenas的bin(这是一种用于存储同类型free chunk的双链表数据结构,后问会加以详细介绍)中。在这里,记录空闲空间的freelist数据结构称之为bins。之后当用户再次调用malloc申请堆空间的时候,glibc malloc会先尝试从bins中找到一个满足要求的chunk,如果没有才会向操作系统申请新的堆空间。如下图所示:

  1. Before malloc in thread1 :

在thread1调用malloc之前:从输出结果可以看出thread1中并没有heap segment,但是此时thread1自己的栈空间已经分配完毕了:

  1. After malloc in thread1 :

在thread1调用malloc之后:从输出结果可以看出thread1的heap segment已经分配完毕了,同时从这个区域的起始地址可以看出,它并不是通过brk分配的,而是通过mmap分配,因为它的区域为b7500000-b7600000共1MB,并不是同程序的data segment相邻。同时,我们还能看出在这1MB中,根据内存属性分为了2部分:0xb7500000-0xb7520000共132KB大小的空间是可读可写属性;后面的是不可读写属性。原来,这里只有可读写的132KB空间才是thread1的堆空间,即thread1 arena。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: UE4.25是著名的游戏引擎Unreal Engine的最新版本,而Gitee则是一个国内知名的代码托管平台。在UE4.25中,通过使用Gitee平台,可以更加方便地进行代码的管理和团队协作。一方面,Gitee平台提供了完整的代码托管和版本管理功能,使得开发者可以随时备份和恢复项目代码,并通过分支和合并等操作,轻松地协同工作。另一方面,Gitee平台还提供了项目协作、代码审核等功能,有利于多人协同开发代码和提高开发效率。不仅如此,Gitee平台还优化了代码仓库的访问速度,提供丰富的开发者社区资源,以及支持多种Webhooks和CI/CD服务,使得开发者能够拥有更加顺畅的开发体验。因此,对于使用UE4.25进行游戏开发的团队来说,Gitee平台是一个非常值得考虑和使用的代码托管和协作平台。 ### 回答2: UE4.25是开发游戏的一款强大的引擎,而Gitee是一个国内知名的代码托管平台。将UE4.25与Gitee结合使用,可以更高效地管理团队的代码。通过在Gitee上创建仓库,团队成员可以很方便地进行协作开发,同步更新代码,并保证代码的版本控制。同时,Gitee支持一个可视化的操作界面,方便开发者进行代码的管理和修改。与其他代码托管平台相比,Gitee的优势在于服务器位于国内,能够提供更好的访问速度和稳定性。此外,Gitee还通过开放API支持与第三方工具的集成,方便开发者进行项目管理和自动化构建等工作。因此,将UE4.25与Gitee结合使用,可以有效地提高团队协作和开发效率,使开发者更加专注于游戏的制作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值