操作系统第三十一章作业(1-2,4-6题)

操作系统第三十一章作业(1-2,4-6题)

所有题目均为自己所作,答案仅供参考,禁止抄袭
觉得有用可赏个赞,有不对之处欢迎评论指正

第一题

The first problem is just to implement and test a solution to the fork/join problem, as described in the text. Even though this solution is described in the text, the act of typing it in on your own is worthwhile; even Bach would rewrite Vivaldi, allowing one soon-to-be master to learn from an existing one. See fork-join.c for details. Add the call sleep(1) to the child to ensure it is working.

题目要求:实现并测试fork/join问题的解决方案,查看fork-join.c获取更多细节。在child中调用sleep(1)确保其工作正常。

首先查看fork-join.c

sem_t s; //定义信号量s

void *child(void *arg) {
    printf("child\n");
    // use semaphore here
    return NULL;
}

int main(int argc, char *argv[]) {
    pthread_t p;
    printf("parent: begin\n");
    // init semaphore here
    Pthread_create(&p, NULL, child, NULL);
    // use semaphore here
    printf("parent: end\n");
    return 0;
}

期望实现:main函数打印一条父线程开始信息,接着初始化信号量,创建子线程,子线程结束后父线程再打印一条结束信息。

main函数我们需要实现两个操作,一是初始化信号量,二是使用信号量。

初始化信号量:

sem_init(&s,0,0);

使用信号量:

sem_wait(&s);

child函数需要实现一个操作,即使用信号量:

sem_post(&s);

父线程先初始化信号量,之后创建子线程。若子线程先运行,则其打印child后,调用sem_post函数,信号量加1变为1,此时没有线程在等待,直接返回,之后父线程调用sem_wait函数,信号量减1变为0,不是负数所以不等待,父线程打印结束信息;若父线程先运行,其会先调用sem_wait函数,信号量减1,变为-1,为符数所父线程等待,子线程打印child后,调用sem_post函数,信号量加1变为0,并唤醒等待的父线程,之后父线程打印结束信息。不论父线程/子线程哪个先运行,程序行为均正确。

执行编译命令:

gcc -o fork-join fork-join.c -Wall -pthread

出现报错,提示没有找到头文件common_threads.h,这个头文件要我们自己实现,其中主要定义了信号量结构体sem_t,实现了sem_init、sem_wait、sem_post函数(主要参考了课本上的Zem_t的实现)。

再次编译,仍然报错,发现是因为fork-join.c中创建子线程使用的是Pthread_create函数,这个函数不是头文件pthread.h中包含的,是一个包装函数,也需要我们在common_threads.h中实现,但比较简单。

最后实现的common_threads.h如下:

#ifndef __COMMON_THREADS_H__
#define __COMMON_THREADS_H__
#include <pthread.h>
#include <assert.h>
typedef struct sem_t{
	int value;
	pthread_cond_t cond;
	pthread_mutex_t lock;
} sem_t;

void sem_init(sem_t *s, int value){
	s->value = value;
	pthread_cond_init(&s->cond,NULL);
	pthread_mutex_init(&s->lock,NULL);
}
void sem_wait(sem_t *s){
	pthread_mutex_lock(&s->lock);
	while(s->value<=0)
		pthread_cond_wait(&s->cond, &s->lock);
	s->value--;
	pthread_mutex_unlock(&s->lock);
}
void sem_post(sem_t *s){
	pthread_mutex_lock(&s->lock);
	s->value++;
	pthread_cond_signal(&s->cond);
	pthread_mutex_unlock(&s->lock);
}
#define Pthread_create(thread, attr, start_routine, arg) assert(pthread_create(thread, attr, start_routine, arg) == 0);

#endif

再次执行编译命令,编译成功,生成了可执行文件fork-join,之后运行此可执行文件,结果如下:

现在在child中加入sleep(1)(加入到第一句),执行结果仍与上面一样,只不过程序没有立即打印完全部内容,而是在打印父线程开始信息后”暂停“了一秒左右打印出child和父线程结束信息。

在main中加入sleep(1)(加入到创建子线程后的第一句),执行结果还是与上面一样,只不过程序没有立即打印完全部内容,而是在打印完父线程开始信息和child后”暂停“了一秒左右才打印出父线程结束信息。

不论父线程还是子线程先运行,执行结果均正确,说明实现正确。

后来我意识到,信号量的相关函数并不用自己实现,只要包含头文件semaphore.h即可,所以common_threads.h中只需要有Pthread_create一个宏定义就可以了。

第二题

Let’s now generalize this a bit by investigating the rendezvous problem. The problem is as follows: you have two threads, each of which are about to enter the rendezvous point in the code. Neither should exit this part of the code before the other enters it. Consider using two semaphores for this task, and see rendezvous.c for details.

题目要求:考虑两个线程,每一个都将进入代码的”集合点“,在另一个线程进入之前,另外一个线程不应该退出这段代码。在这个任务中使用两个信号量,可以查看rendezvous.c获取更多细节。

查看rendezvous.c

#include <stdio.h>
#include <unistd.h>
#include "common_threads.h"

// If done correctly, each child should print their "before" message
// before either prints their "after" message. Test by adding sleep(1)
// calls in various locations.

sem_t s1, s2;

void *child_1(void *arg) {
    printf("child 1: before\n");
    // what goes here?
    printf("child 1: after\n");
    return NULL;
}

void *child_2(void *arg) {
    printf("child 2: before\n");
    // what goes here?
    printf("child 2: after\n");
    return NULL;
}

int main(int argc, char *argv[]) {
    pthread_t p1, p2;
    printf("parent: begin\n");
    // init semaphores here
    Pthread_create(&p1, NULL, child_1, NULL);
    Pthread_create(&p2, NULL, child_2, NULL);
    Pthread_join(p1, NULL);
    Pthread_join(p2, NULL);
    printf("parent: end\n");
    return 0;
}

结合注释和题干,执行正确的标志是:两个before信息必须在两个after信息之前打印。

main函数首先打印父线程开始信息,之后初始化信号量,然后创建了两个子线程child_1和child_2,之后调用Pthread函数等待两个子线程完成,最后打印父线程结束信息。

Pthread_join函数需要我们在common_threads.h中实现,在common_threads.h中添加:

#define Pthread_join(thread, value_ptr)                  assert(pthread_join(thread, value_ptr) == 0);

child_1和child_2类似,且都比较简单,不再说明。实现如下:

main函数初始化信号量:

sem_init(&s1,0,0);
sem_init(&s2,0,0);

要想两个before信息在两个after信息之前打印,不管哪个子线程先运行,都必须在打印完before之后,确保另一个子线程也打印完before才可以接着往下运行。所以,考虑设计让child_1以&1为参数调用sem_wait,以&2为参数调用sem_post,让child_2以&2为参数调用sem_wait,以&1为参数调用sem_post,这样一个子线程打印完before后会等待另外一个线程打印完before再运行,就可以保证两个before先打印了。

child_1注释处实现:

sem_wait(&s1);
sem_post(&s2);

child_2注释处实现:

sem_wait(&s2);
sem_post(&s1);

执行编译命令:

gcc -o rendezvous rendezvous.c -Wall -pthread

编译成功,生成可执行文件rendezvous,运行它:

在打印完两个before信息后便没有下文了,推测两个子线程都进入了睡眠状态,分析后发现原因如下:

初始时两个信号量s1和s2均为0。子线程2先运行,打印child 2: before后调用sem_wait(&s2),s2变为-1,子线程2开始睡眠等待;之后子线程1运行,打印child 1: before后调用sem_wait(&s1),s1变为-1,子线程1也开始睡眠等待。二者都在睡眠,但都在等对方唤醒。

考虑将其中一个子线程的sem_post提到sem_wait之前执行,就可以解决这个问题,运行结果如下:

当然,将两个子线程的sem_post都提到sem_wait之前执行,结果依然正确。

在子线程1打印before信息之前调用sleep的运行结果如下:

(程序在打印完child 2: before后”暂停“1秒接着打印下面的内容。)

在子线程2打印before信息之前调用sleep的运行结果如下:

(程序在打印完child 1: before后”暂停“1秒接着打印下面的内容。)

不论子线程1还是子线程2先运行,执行结果均正确,说明实现正确。

第三题

Now let’s solve the reader-writer problem, also as described in the text. In this first take, don’t worry about starvation. See the code in reader-writer.cfor details. Add sleep() calls to your code to demonstrate it works as you expect. Can you show the existence of the starvation problem?

题目要求:解决读者/写者问题,不考虑饥饿,查看reader-writer.c获取更多细节。在代码中调用sleep来演示它确如期望那样工作,并说明饥饿问题的存在。

查看reader-writer.c已给部分:

int loops;
int value = 0;

rwlock_t lock;

void *reader(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
	rwlock_acquire_readlock(&lock);
	printf("read %d\n", value);
	rwlock_release_readlock(&lock);
    }
    return NULL;
}

void *writer(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
	rwlock_acquire_writelock(&lock);
	value++;
	printf("write %d\n", value);
	rwlock_release_writelock(&lock);
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    assert(argc == 4);
    int num_readers = atoi(argv[1]);
    int num_writers = atoi(argv[2]);
    loops = atoi(argv[3]);

    pthread_t pr[num_readers], pw[num_writers];

    rwlock_init(&lock);

    printf("begin\n");

    int i;
    for (i = 0; i < num_readers; i++)
	Pthread_create(&pr[i], NULL, reader, NULL);
    for (i = 0; i < num_writers; i++)
	Pthread_create(&pw[i], NULL, writer, NULL);

    for (i = 0; i < num_readers; i++)
	Pthread_join(pr[i], NULL);
    for (i = 0; i < num_writers; i++)
	Pthread_join(pw[i], NULL);

    printf("end: value %d\n", value);

    return 0;
}

main函数:

  • 确认参数个数为4,否则抛出异常;
  • 从参数数组中读取读者/写者个数以及循环次数;
  • 初始化读写锁lock并打印开始信息;
  • 创建读者和写者线程;
  • 等待读者和写者线程结束;
  • 打印结束信息和value的值。

读者:

  • 先获取读锁lock;
  • 打印read与value值;
  • 释放读锁;
  • 循环loops次后返回NULL。

写者:

  • 先获取写锁lock;
  • value值加1;
  • 打印write和value值;
  • 释放写锁;
  • 循环loops次后返回NULL。

下面是我们需要补充的结构以及函数:

typedef struct __rwlock_t {
} rwlock_t;


void rwlock_init(rwlock_t *rw) {
}

void rwlock_acquire_readlock(rwlock_t *rw) {
}

void rwlock_release_readlock(rwlock_t *rw) {
}

void rwlock_acquire_writelock(rwlock_t *rw) {
}

void rwlock_release_writelock(rwlock_t *rw) {
}

我主要是参考课本上的实现(因为不需要考虑饥饿问题),实现如下:

typedef struct __rwlock_t {
	sem_t lock;
	sem_t writelock;
	int readers;
} rwlock_t;


void rwlock_init(rwlock_t *rw) {
	rw->readers = 0;
	sem_init(&rw->lock, 0, 1);
	sem_init(&rw->writelock, 0, 1);
}

void rwlock_acquire_readlock(rwlock_t *rw) {
	sem_wait(&rw->lock);
	rw->readers++;
	if(rw->readers == 1)
		sem_wait(&rw->writelock);//first reader acquires writelock
	sem_post(&rw->lock);
}

void rwlock_release_readlock(rwlock_t *rw) {
	sem_wait(&rw->lock);
	rw->readers--;
	if(rw->readers == 0)
		sem_post(&rw->writelock);//last reader releases writelock
	sem_post(&rw->lock);
}

void rwlock_acquire_writelock(rwlock_t *rw) {
	sem_wait(&rw->writelock);
}

void rwlock_release_writelock(rwlock_t *rw) {
	sem_post(&rw->writelock);
}

执行编译命令:

gcc -o reader-writer reader-writer.c -Wall -pthread

生成可执行文件reader-writer,运行这个可执行文件,要带上三个参数:

1读者1写者loops=1:

1读者1写者loops=5:

2读者2写者loops=3:

在代码的不同位置加如sleep(1),再运行几次:

读/写无异常。

读/写无异常。

读/写无异常。

以上测试说明实现正确。但这个实现确实存在饥饿问题。

【注意】函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。

考虑以下情况:

  • 刚开始有1个读者和1个写者。
  • 假设读者1先运行。在rwlock_acquire_readlock函数中,它调用sem_wait,lock变为0,非负,读者数目rw->readers变为1。
  • 之后调用sem_wait获取writelock,使writelock变为0。
  • 然后它调用sem_post,lock又变为1,此时没有阻塞在lock上的线程,所以没有线程会被唤醒。
  • 如果在读者调用rwlock_release_readlock函数释放writeock之前,队列中又加入了若干个读者且发生了线程切换,假设写换到写者1。写者1在rwlock_acquire_writelock函数中试图获取写锁,它调用sem_wait,writelock变为-1,写者开始睡眠。
  • 之后切换到剩余的读者依次获取读锁,进行读的操作,由于读者数目rw->readers再不为1,新来的读者不会睡眠等待,可以成功的获取锁。
  • 由于写者线程没有阻塞在lock上,而是阻塞在writelock上,所以在调用sem_post(&lock)时,写者线程并不会被唤醒。
  • 如果有无限多个新加入的读者,或者读者一直源源不断地加入,那么sem_post(&writelock)就永远不会被执行,这个可怜的写者就一直无法进行写的操作——被饿死了。

第四题

Let’s look at the reader-writer problem again, but this time, worry about starvation. How can you ensure that all readers and writers eventually make progress? See reader-writer-nostarve.c for details.

题目要求:对于读者/写者问题,这次要考虑饥饿。你如何确保所有的读者和写者最终都有进展?查看reader-writer-nostarve.c获取更多细节。

这一题需要我们重新实现,以避免出现饥饿问题。main函数,读者,写者行为与第三题一样。

第三题中之所以会出现写者饥饿的情况,就是因为读者调用sem_post(&lock)并不会唤醒写者,因为写者线程是被writelock所阻塞,那么考虑在读者调用sem_post(&lock)之前先调用sem_post(&writelock),就可以避免写者饥饿:

void rwlock_release_readlock(rwlock_t *rw) {
	sem_wait(&rw->lock);
	rw->readers--;
	if(rw->readers == 0)
		sem_post(&rw->writelock);//last reader releases writelock
	sem_post(&rw->writelock);  //新增
	sem_post(&rw->lock);
}

多测试几组数据:

10读者1写者loops=2:

5读者5写者loops=2:

100读者2写者loops=2:

考虑第四题中的情况:

  • 刚开始有1个读者和1个写者。
  • 假设读者1先运行。在rwlock_acquire_readlock函数中,它调用sem_wait,lock变为0,非负,读者数目rw->readers变为1。
  • 之后调用sem_wait获取writelock,使writelock变为0。
  • 然后它调用sem_post,lock又变为1,此时没有阻塞在lock上的线程,所以没有线程会被唤醒。
  • 如果在读者1调用rwlock_release_readlock函数释放writeock之前,队列中又加入了若干个读者且发生了线程切换,假设切换到写者1。写者1在rwlock_acquire_writelock函数中试图获取写锁,它调用sem_wait,writelock变为-1,写者开始睡眠。
  • 之后切换到剩余的读者依次获取读锁,进行读的操作,由于读者数目rw->readers再不为1,新来的读者不会睡眠等待,可以成功的获取锁。
  • 当有读者调用rwlock_release_readlock时,在调用sem_post(&lock)之前会先调用sem_post(&writelock),就可以唤醒其中一个阻塞在writelock上的线程,包括写者1。
  • 所以即使有很多读者,写者1仍旧有机会被唤醒。虽然具体唤醒哪个线程取决于调度程序,但——写者1总会被唤醒的。

考虑另外一种情况:

  • 刚开始有1个读者和1个写者。
  • 假设写者1先运行。在rwlock_acquire_writelock中,它调用sem_wait,writelock变为0,非负,获取锁成功,写者开始写。
  • 如果在写者1调用rwlock_release_writelock函数释放writelock之前,队列中又加入了若干个写者且发生了线程切换,假设切换到读者1。读者1在rwlock_acquire_readlock函数中试图获取锁。
  • 读者1先调用sem_wait,lock变为0,之后读者数目rw->readers自加1。
  • 然后它试图获取writelock锁,writelock变为-1,读者1开始睡眠。
  • 切换到剩余的写者依次获取写锁,它们均在调用sem_wait(&readlock)后进入睡眠状态,直到切换到写者1,它调用sem_post(&writelock),writelock加1,同时唤醒阻塞在writelock上的其中一个线程,包括读者1。
  • 所以即使有很多写者,读者1仍旧有机会被唤醒。

综上,这样的实现可以使得读者、写者线程最终都取得进展。

第五题

Use semaphores to build a no-starve mutex, in which any thread that tries to acquire the mutex will eventually obtain it. See the code in mutex-nostarve.c for more information.

题目要求:使用信号量建立一个没有饥饿的锁,也就是说,任何一个想要获取锁的线程最终都能得到锁。查看mutex-nostarve.c获取更多信息。

查看mutex-nostarve.c

// Here, you have to write (almost) ALL the code. Oh no!
// How can you show that a thread does not starve
// when attempting to acquire this mutex you build?
//

typedef __ns_mutex_t {
} ns_mutex_t;

void ns_mutex_init(ns_mutex_t *m) {
}

void ns_mutex_acquire(ns_mutex_t *m) {
}

void ns_mutex_release(ns_mutex_t *m) {
}


void *worker(void *arg) {
    return NULL;
}

int main(int argc, char *argv[]) {
    printf("parent: begin\n");
    printf("parent: end\n");
    return 0;
}

我们需要补充所有的函数。如何判定“想要获取锁的线程最终都能得到锁”呢?我们可以设置一个全局计数量count,每个线程都对其执行自加1的操作。这样,如果“想要获取锁的线程最终都能得到锁”,那么count最终的值就是我们创建的线程个数。补充完代码如下:

typedef struct __ns_mutex_t {
	sem_t mutex;
} ns_mutex_t;

void ns_mutex_init(ns_mutex_t *m) {
	sem_init(&m->mutex, 0, 1);
}	

void ns_mutex_acquire(ns_mutex_t *m) {
	sem_wait(&m->mutex);
}

void ns_mutex_release(ns_mutex_t *m) {
	sem_post(&m->mutex);
}

ns_mutex_t n;
int counter = 0;
void *worker(void *arg) {
	ns_mutex_acquire(&n);
	counter++;
	ns_mutex_release(&n);
    return NULL;
}

int main(int argc, char *argv[]) {
    assert(argc == 2);
    int num_threads = atoi(argv[1]);
    pthread_t p[num_threads];
	ns_mutex_init(&n);

	printf("parent: begin\n");	
	int i;
	for(i = 0; i < num_threads; i++)
		Pthread_create(&p[i],NULL, worker, NULL);
	for(i = 0; i < num_threads; i++)
		Pthread_join(p[i],NULL);
	printf("counter: %d\n",counter);
    printf("parent: end\n");
    return 0;
}

运行结果:

最后参数为50000时失败了,因为创建不了这么多线程。最多创建的线程数为32754。

但是比较迷惑的一点是,即使不加锁,测试了许多情况也是对的。这一点还没想明白为什么。

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值