通过本次实习,了解同步的原理及其实现方法。信号量及PV操作是一种经典的同步方法,通过P和V操作在进程间传递一个整数值。为了解决信号量机制带来的程序编写困难、效率低等不足,出现了管程机制,这是一种高级的同步机制。管程类似于面向对象中的类,进程只能通过调用管程中的过程来间接的访问管程中的数据结构。同时,管程的进入是互斥的,管程中设置了条件变量以解决同步问题,一个进程或线程可以等待在条件变量上,但在此之前要先释放管程的锁。进程可以通过发送信号将等待在条件变量上的进程或线程唤醒。对条件变量的操作有wait和signal操作。
【用简洁的语言描述本次lab的主要内容;阐述本次lab中涉及到的重要的概念,技术,原理等,以及其他你认为的最重要的知识点。这一部分主要是看大家对lab的总体的理解。
要求:简洁,不需要面面俱到,把重要的知识点阐述清楚即可。】
|
Exercise1 |
Exercise2 |
Exercise3 |
Exercise4 |
完成情况 |
Y |
Y |
Y |
Y |
Exercise1
调研Linux中实现的同步机制
在Linux中进行多线程开发,同步是不可避免的一个问题。在POSIX标准中定义了三种线程同步机制:Mutexes(互斥量),ConditionVariable(条件变量)和POSIX Semaphores(信号量)。
同步互斥的实现要依赖于院子操作,linux内核中原子操作定义在include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。原子操作类型定义如下:
typedef struct { volatile int counter; } atomic_t;
volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。原子操作API包括:atomic_read、atomic_set、atomic_add等。
Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝对不可能在内核之外使用,因此它与System V的IPC机制信号量毫不相干。信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的临界资源。对信号量操作的API有DECLARE_MUTEX(name)、DECLARE_MUTEX_LOCKED(name)、sema_init(sem,val)、init_MUTEX()等。
自旋锁。自旋锁与互斥锁类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。信号量适用于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。操作自旋锁的API有spin_lock_init()、DEFINE_SPINLOCK()、SPIN_LOCK_UNLOCKED、spin_trylock()、spin_lock()等。
大内核锁。大内核锁本质上也是自旋锁,但又不同于自旋锁,自旋锁不可以递归获得,因为那样会导致死锁,而大内核锁可以递归获得。大内核锁用于保护整个内核,因而整个内核只有一个大内核锁。
除了上面介绍的以外,Linux对同步机制的实现还有很多方法,在此不一一介绍了。
Exercise2
阅读下列源代码,理解Nachos现有的线程调度算法。
code/threads/synch.h和code/threads/synch.cc
code/threads/synchlist.h和code/threads/synchlist.cc
Nachos提供了三种同步和互斥的手段:信号量、锁机制以及条件变量,方便了用户的使用,具体实现在一下的文件和函数中。
synch.h和synch.cc
Semaphore,这个类中定义了私有变量信号量值(value)和线程等待队列(queue),以及提供操作的公有方法。P()和V()。
P操作:
1. 当value等于0时,将当前运行线程放入线程等待队列,当前进程进入睡眠状态,并切换到其他线程运行。
2. 当value大于0时,value--。
V操作:
如果线程等待队列中有等待该信号量的线程,取出其中一个将其设置成就绪态,准备运行。value++。
Lock。现有的Nachos中,没有给出锁机制的实现,锁的基本结构也只给出了部分内容,其他内容可以视具体实现决定。总体来说,锁的操作有两个,获得锁(Acquire)和释放锁(Release),他们都是原子操作。
Acquire:
当锁处于BUSY态,进入睡眠状态。当锁处于FREE态,当前进程获得该锁,继续运行。
Release:
释放锁(只能由拥有锁的线程才能释放锁),将锁的状态设置为FREE态,如果有其他线程等待该锁,将其中的一个唤醒,进入就绪态。
Condition,条件变量。条件变量和信号量和锁机制不一样,它是没有值的。(实际上锁机制是一个二元信号量,可以通过信号量来实现,见我的Exercise3实现方式)当一个线程需要的某种条件没有得到满足时,可以将自己作为一个等待条件变量的线程插入所有等待该条件变量的队列,只要条件一旦得到满足,该线程就会被唤醒继续运行。条件变量总是和锁机制一起使。它的基本方法有Wait、Signal以及BroadCast。所有这些操作必须在当前线程获得一个锁的前提下,而且所有对一个条件变量进行的操作必须建立在同一个锁的前提下。
Wait(Lock *conditionLock) 线程等待条件变量
1. 释放该锁
2. 进入睡眠状态
3. 重新申请该锁
Signal(Lock *conditionLock)
从条件变量的等待队列中唤醒一个等待该条件变量的线程。
BroadCast(Lock *conditionLock)
唤醒所有等待该条件变量的线程。
SynchList.h和SynchList.cc
提供了对一个List的互斥访问,确保在多线程同时访问该List时,不会发生同步错误。
类中初始化时生成一个Lock对象和List对象。提供了Append(),Remove()和Mapcar()操作。每个操作中都要先获得该锁,然后在对List进行相应的操作。
Exercise3
实现锁和条件变量
设计思路、及代码修改
Nachos中的Synch.cc已经对这两个类的基本变量和方法进行了定义,我们只需要在其 中添加具体的实现方法即可。
Lock的实现:在初始化方法中,实例化了一个二元的信号量,当该信号量初始值为1。
为保证Acquire()和Release()的原子操作,进入操作前都先将中断关闭,这里利用信号量实现锁操作。Acquire()中,利用信号量P操作以获取锁。Release()中,通过V操作,解除锁(必须保证当前线程是锁的拥有者)
条件变量的实现Wait(),同样先关闭中断,将锁释放,然后将当前线程加入等待条件变量的队列,当前线程睡眠。如果被唤醒,需要重新获得锁。
Signal()操作,如果等待队列非空,则将队列中的一个线程唤醒,加入线程就绪队列。
Exercise3
基于Nachos中的信号量、锁和条件变量,采用两种方式实现同步和互斥机制应用
用锁和条件变量实现生产者消费者问题:
设计思路、及代码修改
这部分代码直接写在了ThreadTest中,核心方法包括生产一个产品(produce_item()),我用产生一个int型的数字来模拟。将数放入缓冲区方法(put_item())、从缓冲区获取数方法(get_item())。生产者线程(producer())、消费者线程(consumer())。
重点介绍一下producer()和consumer()方法。
producer()的实现过程:
1. 生产一个产品
2. 获得锁
3. 判断产品数量是否到达临界资源(存放产品的数组)的最大值,如果是,则调用条件变量的Wait操作,等待有消费者消费产品。并将锁释放(在wait中实现)
4. 将产品放入数组。
5. 如果是放入的是当前数组的第一个产品,则调用条件变量的Signal方法,将等待在条件变量下的消费者线程唤醒
6. 释放锁
consumer()实现过程:
1. 获得锁
2. 如果当前数组里没有产品,则调用条件变量的Wait等待,并将锁释放(在wait中实现)
3. 获得一个产品
4. 如果当前获得是第N个产品,则调用条件变量的Signal方法,唤醒等待的生产者线程。
5. 释放锁
实验结果截图
测试用例如下,产生一个生产者线程,让其生成八个产品,产生一个消费者线程,让其消费十个产品。观察运行情况:
可以看到,生产者先生成了5个产品,并放入数组中。由于已经没有位置可以再放产品,此时生产者被阻塞。消费者开始执行。当消费者将产品消费完时,由于没有产品可以再消费,被阻塞,并唤醒生产者重新执行。
生产者将剩余的三个产品生产完,没有产品可以再生成,唤醒了消费者进程,消费这三个产品,可是消费者还想再消费两个产品,但是已经没有了课消费的产品,因此又一次被阻塞。
用信号量实现生产者消费者问题:
设计思路、及代码修改
用信号量实现,只是在生产者线程和消费者线程上有些修改,其他的都一样。首先需要定义三个信号量;
mutex用于对临界资源的互斥访问,初值设为1.
full用于标识临界资源是否已满,初值设为0.
empty用于标识临界资源是否为空,初值设为N。
semaProducer()实现
1. 生成一个产品
2. 调用empty->P()断定是否有空余的位置可以放置产品
3. 获得对临界资源的互斥访问权
4. 将产品放入临界资源(数组)中,并将产品计数器加1
5. 释放对临界资源的访问权
6. 调用full->V()。唤醒在等待的消费者线程
semaConsumer()实现
1. 调用empty->P()判断是否有可以消费的产品
2. 获取对临界资源的互斥访问权。
3. 从临界资源中获取产品。
4. 释放临界资源的访问权
5. 调用empty->V(),唤醒在等待的生产线程。
实验结果截图
测试用例如下,产生一个生产者线程,让其生成八个产品,产生一个消费者线程,让其消费十个产品。观察运行情况:
可以看大生产者生产了5个产品,由于没有可放着产品的位置,被阻塞。唤醒了消费者消费,消费者消费完这5个产品后,唤醒生产者继续生产。
这次生产者将剩下的3个产品生产完,并唤醒消费者将这3个产品消费。
【对于阅读代码类的exercise,请对其中你认为重要的部分(比如某文件,或某个类、或某个变量、或某个函数……)做出说明。
对于要编程实现的exercise,请对你增加或修改的内容作出说明。如果增加或修改了某个类,请写出这个类写在那个文件中,它的功能是什么,你自己添加或修改的成员变量以及成员函数有哪些,它们的功能是什么;特别的,对于复杂的函数,请说明你的实现方法。不需要贴具体的实现代码。
要求:表述清楚即可, 可采用图表来辅助说明,不需要贴代码】
感觉到线程的同步互斥是操作系统中的一个比较难的概念,需要仔细分析,稍微不留神就会搞迷糊,需要多看,多练才能更好的掌握这部分知识。
【描述你在实习过程中遇到的困难,是与实习直接相关的技术方面的难题。突出说明你是如何攻克这些难关的。
要求:只需写一下有较深体会的困难。如果觉得整个过程都比较简单的话此部分可不用写。】
相信通过操作系统课的实习,我写C++代码的能力会有显著的提升,以前都是写Java的web,对C++忘的差不多了,通过这次的实验,可以捡回来不少。
另一个就是对线程同步互斥了解更加的透彻,相信可以通过以后的实验,对操作系统的各种机制有更深入的了解。
【自己的收获,任何关于实习的感想,可以是技术方面的或非技术方面的,可随意发挥。
要求:内容不限,体裁不限,字数不限,多多益善,有感而发。】
目前来讲,我觉得一切还都挺好的,老师讲的很不错,进度也可以,没有什么建议。
【请写下你认为课程需要改进的地方,任何方面,比如进度安排、难易程度、课堂讲解、考核方式、题目设置……甚至如果你认为源代码哪里写得不好也欢迎提出。
各位同学反馈的信息对课程建设会有极大帮助。】
[1] Linux内核同步机制
http://www.51cto.com/html/2006/0322/24177.htm
[2] 佚名 Nachos中文教程
http://wenku.baidu.com/link?url=1rGnypg8Hq6-43gAvuIYPWyVlPLZ0S_XNEXQJ-2ShqPPg3n2bqWvQgRYC8PdVXLmr66e9GpC2nCSbE1ofkgcT6aASWqVklMWUaBuZNSmXDy
【我们希望大家不要使用复制粘贴来拼凑你的报告。详细地列出你在完成lab的过程中引用的书籍,网站,讲义,包括你咨询过的大牛们。
要求:诚实,格式尽量按照论文的要求,请参考“论文参考文献格式.doc”】