为了学这个lab1也是踩了很多坑…记录下此篇是希望让我自己的学习不只是走马观花一遍而过,也是给对Lab1一点头绪都没的小白提供一个理解的方式。希望后来者还是要有自己的思考,去完成这个lab1会对自己收获帮助比较大。对于完整代码文末提供了代码gitee地址。
go语言圣经(在线文档)
菜鸟教程go语言教程
b站韩顺平go语言教学视频
go语言精进之路
推荐先大概看一遍文档,然后韩顺平老师那部分主要看260多集管道并发那部分,讲的还是挺好的。
MapReduce中翻链接
MapReduce理解视频
以及我认为课程开篇Introduction也是很重要的,在我看来已经有点相当于课上写的笔记+框架介绍,以及能让你对分布式的理解再加深一些。
lab1 Introduction
因为go语言的插件编译需要,所以6.824的环境是需要在mac或者linux上完成。笔者是选择了ubuntu20.04在golang上进行。关于这个教程,可以看笔者写的另外一篇博客:2022-linux(ubuntu20.04)下go语言环境配置,以及goland安装。
最好使用sdk1.16,因为1.18差1.15是最多的,实验室用的是1.15,但是1.15不能进行调试,1.16才支持,选个相近的sdk以防导致插件导入等编译错误。
git clone git://g.csail.mit.edu/6.824-golabs-2022 6.824
cd 6.824
cd src/main
# 将wc.go编译成插件形式,生成wc.so
go build -race -buildmode=plugin ../mrapps/wc.go
rm mr-out*
# 进行并发检测,并将编译后生成的wc.so插件,以参数形式加入mrsequential.go,并运行
go run -race mrsequential.go wc.so pg*.txt
# 查看生成的文件
more mr-out-0
当然我们既然搭建了goland,那么就可以好好利用下集成环境。
# 进行并发检测,并将编译后生成的wc.so插件,以参数形式加入mrsequential.go,并运行
go build -race -buildmode=plugin ../mrapps/wc.go
# 删除生成的mr-out*以免每次第二次运行得先删除
#rm mr-out*
要注意一点的是程序实参的传入的txt,不能是*这种的匹配符,以golang运行这种实参不会进行自动匹配。 所以参数mrcoordinator应该为以下文件名:
pg-being_ernest.txt pg-dorian_gray.txt pg-frankenstein.txt pg-grimm.txt pg-huckleberry_finn.txt pg-metamorphosis.txt pg-sherlock_holmes.txt pg-tom_sawyer.txt
对于笔者来说一般是mrcoordinator通过golang运行或者调试,mrworker每次在命令行重现编译插件后运行,利用fmt打印输出体验较佳!!!(因为因为wc.go编译在golang中运行实在是给跪了orz…)
前置工作差不多就这些,然后给出一些官方文档以及自己觉得重要的提示:
从实现来看我们可以先完成worker与Coordinator之间的交互,首先可以来看看给的Rpc例子:首先运行main/mrworker.go 会进入到 mr/Worker的这个方法中。可以在这个方法中调用RPC的例子方法:CallExample()。
然后CallExample()这个方法中会有一行:
ok := call("Coordinator.Example", &args, &reply)
调用Coordinator包的Example方法。(这里有个刚学go语言的同学不会注意到的小细节。就是方法名开头为大写的代表可以为外包所调用。至于为什么传方法传的是指针可以看我另外一篇写的博客:Golang指针的应用场景理解。)
然后得到传修改后的reply,得到rpc返回值。至此coordinator与worker完成了简单的交互。
// Task worker向coordinator获取task的结构体
type Task struct {
TaskType TaskType // 任务类型判断到底是map还是reduce
TaskId int // 任务的id
ReducerNum int // 传入的reducer的数量,用于hash
Filename string // 输入文件
}
// TaskArgs rpc应该传入的参数,可实际上应该什么都不用传,因为只是worker获取一个任务
type TaskArgs struct{}
// TaskType 对于下方枚举任务的父类型
type TaskType int
// Phase 对于分配任务阶段的父类型
type Phase int
// State 任务的状态的父类型
type State int
// 枚举任务的类型
const (
MapTask TaskType = iota
ReduceTask
WaittingTask // Waittingen任务代表此时为任务都分发完了,但是任务还没完成,阶段未改变
ExitTask // exit
)
// 枚举阶段的类型
const (
MapPhase Phase = iota // 此阶段在分发MapTask
ReducePhase // 此阶段在分发ReduceTask
AllDone // 此阶段已完成
)
// 任务状态类型
const (
Working State = iota // 此阶段在工作
Waiting // 此阶段在等待执行
Done // 此阶段已经做完
)
接着我们就来worker里面构造发送请求rpc的方法,获取Map任务:(此处的代码都为当时笔者所写,与最终代码实现会有出入,忘后来者能有自己的斟酌考虑):
总的判断,获取的任务类型,后面reduce任务也直接加这里,笔者这里采用假任务(ExitTask)的方法退出,当然也可以通过RPC没有获取到task后再退出的方式,可以自己去试试。
func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) {
//CallExample()
keepFlag := true
for keepFlag {
task := GetTask()
switch task.TaskType {
case MapTask:
{
DoMapTask(mapf, &task)
callDone()
}
case WaittingTask:
{
fmt.Println("All tasks are in progress, please wait...")
time.Sleep(time.Second)
}
case ExitTask:
{
fmt.Println("Task about :[", task.TaskId, "] is terminated...")
keepFlag = false
}
}
}
// uncomment to send the Example RPC to the coordinator.
}
接下来实现上方中的方法:
// GetTask 获取任务(需要知道是Map任务,还是Reduce)
func GetTask() Task {
args := TaskArgs{}
reply := Task{}
ok := call("Coordinator.PollTask", &args, &reply)
if ok {
fmt.Println(reply)
} else {
fmt.Printf("call failed!\n")
}
return reply
}
func DoMapTask(mapf func(string, string) []KeyValue, response *Task) {
var intermediate []KeyValue
filename := response.Filename
file, err := os.Open(filename)
if err != nil {
log.Fatalf("cannot open %v", filename)
}
// 通过io工具包获取conten,作为mapf的参数
content, err := ioutil.ReadAll(file)
if err != nil {
log.Fatalf("cannot read %v", filename)
}
file.Close()
// map返回一组KV结构体数组
intermediate = mapf(filename, string(content))
//initialize and loop over []KeyValue
rn := response.ReducerNum
// 创建一个长度为nReduce的二维切片
HashedKV := make([][]KeyValue, rn)
for _, kv := range intermediate {
HashedKV[ihash(kv.Key)%rn] = append(HashedKV[ihash(kv.Key)%rn], kv)
}
for i := 0; i < rn; i++ {
oname := "mr-tmp-" + strconv.Itoa(response.TaskId) + "-" + strconv.Itoa(i)
ofile, _ := os.Create(oname)
enc := json.NewEncoder(ofile)
for _, kv := range HashedKV[i] {
enc.Encode(kv)
}
ofile.Close()
}
}
// callDone Call RPC to mark the task as completed
func callDone() Task {
args := Task{}
reply := Task{}
ok := call("Coordinator.MarkFinished", &args, &reply)
if ok {
fmt.Println(reply)
} else {
fmt.Printf("call failed!\n")
}
return reply
}
接下来去协调者完善方法:
type Coordinator struct {
// Your definitions here.
ReducerNum int // 传入的参数决定需要多少个reducer
TaskId int // 用于生成task的特殊id
DistPhase Phase // 目前整个框架应该处于什么任务阶段
TaskChannelReduce chan *Task // 使用chan保证并发安全
TaskChannelMap chan *Task // 使用chan保证并发安全
taskMetaHolder TaskMetaHolder // 存着task
files []string // 传入的文件数组
}
其中taskMetaHolder为存放全部元信息(TaskMetaInfo)的map,当然用slice也行。
// TaskMetaHolder 保存全部任务的元数据
type TaskMetaHolder struct {
MetaMap map[int]*TaskMetaInfo // 通过下标hash快速定位
}
// TaskMetaInfo 保存任务的元数据
type TaskMetaInfo struct {
state State // 任务的状态
TaskAdr *Task // 传入任务的指针,为的是这个任务从通道中取出来后,还能通过地址标记这个任务已经完成
}
// create a Coordinator.
// main/mrcoordinator.go calls this function.
// nReduce is the number of reduce tasks to use.
//
func MakeCoordinator(files []string, nReduce int) *Coordinator {
c := Coordinator{
files: files,
ReducerNum: nReduce,
DistPhase: MapPhase,
TaskChannelMap: make(chan *Task, len(files)),
TaskChannelReduce: make(chan *Task, nReduce),
taskMetaHolder: TaskMetaHolder{
MetaMap: make(map[int]*TaskMetaInfo, len(files)+nReduce), // 任务的总数应该是files + Reducer的数量
},
}
c.makeMapTasks(files)
c.server()
return &c
}
// 对map任务进行处理,初始化map任务
func (c *Coordinator) makeMapTasks(files []string) {
for _, v := range files {
id := c.generateTaskId()
task := Task{
TaskType: MapTask,
TaskId: id,
ReducerNum: c.ReducerNum,
Filename: v,
}
// 保存任务的初始状态
taskMetaInfo := TaskMetaInfo{
state: Waiting, // 任务等待被执行
TaskAdr: &task, // 保存任务的地址
}
c.taskMetaHolder.acceptMeta(&taskMetaInfo)
fmt.Println("make a map task :", &task)
c.TaskChannelMap <- &task
}
}
// 通过结构体的TaskId自增来获取唯一的任务id
func (c *Coordinator) generateTaskId() int {
res := c.TaskId
c.TaskId++
return res
}
// 将接受taskMetaInfo储存进MetaHolder里
func (t *TaskMetaHolder) acceptMeta(TaskInfo *TaskMetaInfo) bool {
taskId := TaskInfo.TaskAdr.TaskId
meta, _ := t.MetaMap[taskId]
if meta != nil {
fmt.Println("meta contains task which id = ", taskId)
return false
} else {
t.MetaMap[taskId] = TaskInfo
}
return true
}
// 分发任务
func (c *Coordinator) PollTask(args *TaskArgs, reply *Task) error {
// 分发任务应该上锁,防止多个worker竞争,并用defer回退解锁
mu.Lock()
defer mu.Unlock()
// 判断任务类型存任务
switch c.DistPhase {
case MapPhase:
{
if len(c.TaskChannelMap) > 0 {
*reply = *<-c.TaskChannelMap
if !c.taskMetaHolder.judgeState(reply.TaskId) {
fmt.Printf("taskid[ %d ] is running\n", reply.TaskId)
}
} else {
reply.TaskType = WaittingTask // 如果map任务被分发完了但是又没完成,此时就将任务设为Waitting
if c.taskMetaHolder.checkTaskDone() {
c.toNextPhase()
}
return nil
}
}
default:
{
reply.TaskType = ExitTask
}
}
return nil
}
func (c *Coordinator) toNextPhase() {
if c.DistPhase == MapPhase {
//c.makeReduceTasks()
// todo
c.DistPhase = AllDone
} else if c.DistPhase == ReducePhase {
c.DistPhase = AllDone
}
}
// 检查多少个任务做了包括(map、reduce),
func (t *TaskMetaHolder) checkTaskDone() bool {
var (
mapDoneNum = 0
mapUnDoneNum = 0
reduceDoneNum = 0
reduceUnDoneNum = 0
)
// 遍历储存task信息的map
for _, v := range t.MetaMap {
// 首先判断任务的类型
if v.TaskAdr.TaskType == MapTask {
// 判断任务是否完成,下同
if v.state == Done {
mapDoneNum++
} else {
mapUnDoneNum++
}
} else if v.TaskAdr.TaskType == ReduceTask {
if v.state == Done {
reduceDoneNum++
} else {
reduceUnDoneNum++
}
}
}
//fmt.Printf("map tasks are finished %d/%d, reduce task are finished %d/%d \n",
// mapDoneNum, mapDoneNum+mapUnDoneNum, reduceDoneNum, reduceDoneNum+reduceUnDoneNum)
// 如果某一个map或者reduce全部做完了,代表需要切换下一阶段,返回true
// R
if (mapDoneNum > 0 && mapUnDoneNum == 0) && (reduceDoneNum == 0 && reduceUnDoneNum == 0) {
return true
} else {
if reduceDoneNum > 0 && reduceUnDoneNum == 0 {
return true
}
}
return false
}
// 判断给定任务是否在工作,并修正其目前任务信息状态
func (t *TaskMetaHolder) judgeState(taskId int) bool {
taskInfo, ok := t.MetaMap[taskId]
if !ok || taskInfo.state != Waiting {
return false
}
taskInfo.state = Working
return true
}
func (c *Coordinator) MarkFinished(args *Task, reply *Task) error {
mu.Lock()
defer mu.Unlock()
switch args.TaskType {
case MapTask:
meta, ok := c.taskMetaHolder.MetaMap[args.TaskId]
//prevent a duplicated work which returned from another worker
if ok && meta.state == Working {
meta.state = Done
fmt.Printf("Map task Id[%d] is finished.\n", args.TaskId)
} else {
fmt.Printf("Map task Id[%d] is finished,already ! ! !\n", args.TaskId)
}
break
default:
panic("The task type undefined ! ! !")
}
return nil
}
//Done 主函数mr调用,如果所有task完成mr会通过此方法退出
func (c *Coordinator) Done() bool {
mu.Lock()
defer mu.Unlock()
if c.DistPhase == AllDone {
fmt.Printf("All tasks are finished,the coordinator will be exit! !")
return true
} else {
return false
}
}
至此map阶段已经能暂且构成一个循环,先运行mrcoordinator.go、再运行mrworker查看效果。
mrcoordinator.go运行效果(笔者为了测试效果只传入了两个文件):
mrworker.go运行效果:
再去查看生成的文件:
3.2 在map阶段上补充reduce阶段,并处理
有过大概一个流程写reduce阶段还是挺快,大部分逻辑其实和map阶段是相同的的,先继续初始写reduce方法:
func (c *Coordinator) makeReduceTasks() {
for i := 0; i < c.ReducerNum; i++ {
id := c.generateTaskId()
task := Task{
TaskId: id,
TaskType: ReduceTask,
FileSlice: selectReduceName(i),
}
// 保存任务的初始状态
taskMetaInfo := TaskMetaInfo{
state: Waiting, // 任务等待被执行
TaskAdr: &task, // 保存任务的地址
}
c.taskMetaHolder.acceptMeta(&taskMetaInfo)
//fmt.Println("make a reduce task :", &task)
c.ReduceTaskChannel <- &task
}
}
这里要注意的是我把原来Task结构字段做出了一个改变,由Filename变为了一个文件切片数组。
// Task worker向coordinator获取task的结构体
type Task struct {
TaskType TaskType // 任务类型判断到底是map还是reduce
TaskId int // 任务的id
ReducerNum int // 传入的reducer的数量,用于hash
FileSlice []string // 输入文件的切片,map一个文件对应一个文件,reduce是对应多个temp中间值文件
}
因为对于重新理了以下MapReduce框架就可知,输入阶段时,初始化一个map任务其实是对应一个输入文件,但是经过map过程来看,我们其实一个任务切分成了很多tmp文件,那么reduce任务输入则应该是一组哈希相同的中间文件。
func selectReduceName(reduceNum int) []string {
var s []string
path, _ := os.Getwd()
files, _ := ioutil.ReadDir(path)
for _, fi := range files {
// 匹配对应的reduce文件
if strings.HasPrefix(fi.Name(), "mr-tmp") && strings.HasSuffix(fi.Name(), strconv.Itoa(reduceNum)) {
s = append(s, fi.Name())
}
}
return s
}
func (c *Coordinator) PollTask(args *TaskArgs, reply *Task) error {
// 分发任务应该上锁,防止多个worker竞争,并用defer回退解锁
mu.Lock()
defer mu.Unlock()
// 判断任务类型存任务
switch c.DistPhase {
case MapPhase:
{
if len(c.MapTaskChannel) > 0 {
*reply = *<-c.MapTaskChannel
if !c.taskMetaHolder.judgeState(reply.TaskId) {
fmt.Printf("Map-taskid[ %d ] is running\n", reply.TaskId)
}
} else {
reply.TaskType = WaittingTask // 如果map任务被分发完了但是又没完成,此时就将任务设为Waitting
if c.taskMetaHolder.checkTaskDone() {
c.toNextPhase()
}
return nil
}
}
case ReducePhase:
{
if len(c.ReduceTaskChannel) > 0 {
*reply = *<-c.ReduceTaskChannel
if !c.taskMetaHolder.judgeState(reply.TaskId) {
fmt.Printf("Reduce-taskid[ %d ] is running\n", reply.TaskId)
}
} else {
reply.TaskType = WaittingTask // 如果map任务被分发完了但是又没完成,此时就将任务设为Waitting
if c.taskMetaHolder.checkTaskDone() {
c.toNextPhase()
}
return nil
}
}
case AllDone:
{
reply.TaskType = ExitTask
}
default:
panic("The phase undefined ! ! !")
}
return nil
}
func (c *Coordinator) toNextPhase() {
if c.DistPhase == MapPhase {
c.makeReduceTasks()
c.DistPhase = ReducePhase
} else if c.DistPhase == ReducePhase {
c.DistPhase = AllDone
}
}
func Worker(mapf func(string, string) []KeyValue, reducef func(string, []string) string) {
//CallExample()
keepFlag := true
for keepFlag {
task := GetTask()
switch task.TaskType {
case MapTask:
{
DoMapTask(mapf, &task)
callDone()
}
case WaittingTask:
{
//fmt.Println("All tasks are in progress, please wait...")
time.Sleep(time.Second)
}
case ReduceTask:
{
DoReduceTask(reducef, &task)
callDone()
}
case ExitTask:
{
//fmt.Println("Task about :[", task.TaskId, "] is terminated...")
keepFlag = false
}
}
}
// uncomment to send the Example RPC to the coordinator.
}
func DoReduceTask(reducef func(string, []string) string , response *Task) {
reduceFileNum := response.TaskId
intermediate := shuffle(response.FileSlice)
dir, _ := os.Getwd()
//tempFile, err := ioutil.TempFile(dir, "mr-tmp-*")
tempFile, err := ioutil.TempFile(dir, "mr-tmp-*")
if err != nil {
log.Fatal("Failed to create temp file", err)
}
i := 0
for i < len(intermediate) {
j := i + 1
for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
j++
}
var values []string
for k := i; k < j; k++ {
values = append(values, intermediate[k].Value)
}
output := reducef(intermediate[i].Key, values)
fmt.Fprintf(tempFile, "%v %v\n", intermediate[i].Key, output)
i = j
}
tempFile.Close()
fn := fmt.Sprintf("mr-out-%d", reduceFileNum)
os.Rename(tempFile.Name(), fn)
}
// 洗牌方法,得到一组排序好的kv数组
func shuffle(files []string) []KeyValue {
var kva []KeyValue
for _, filepath := range files {
file, _ := os.Open(filepath)
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
kva = append(kva, kv)
}
file.Close()
}
sort.Sort(SortedKey(kva))
return kva
}
If you choose to implement Backup Tasks (Section 3.6), note that we test that your code doesn’t schedule extraneous tasks when workers execute tasks without crashing. Backup tasks should only be scheduled after some relatively long period of time (e.g., 10s).
简单的翻译过来就是:如果你选择去实现一个备份任务来容错,请注意我们测试你的代码时候不会安排无关的任务(个人认为就是假死任务)让worker去执行,也因此不会崩溃。备份任务只有在一些任务很久没有得到响应后,才会被安排(例如10s)。
// TaskMetaInfo 保存任务的元数据
type TaskMetaInfo struct {
state State // 任务的状态
StartTime time.Time // 任务的开始时间,为crash做准备
TaskAdr *Task // 传入任务的指针,为的是这个任务从通道中取出来后,还能通过地址标记这个任务已经完成
}
// 判断给定任务是否在工作,并修正其目前任务信息状态
func (t *TaskMetaHolder) judgeState(taskId int) bool {
taskInfo, ok := t.MetaMap[taskId]
if !ok || taskInfo.state != Waiting {
return false
}
taskInfo.state = Working
taskInfo.StartTime = time.Now()
return true
}
在协调者中补充开启crash协程:
func MakeCoordinator(files []string, nReduce int) *Coordinator {
c := Coordinator{
JobChannelMap: make(chan *Job, len(files)),
JobChannelReduce: make(chan *Job, nReduce),
jobMetaHolder: JobMetaHolder{
MetaMap: make(map[int]*JobMetaInfo, len(files)+nReduce),
},
CoordinatorCondition: MapPhase,
ReducerNum: nReduce,
MapNum: len(files),
uniqueJobId: 0,
}
c.makeMapJobs(files)
c.server()
go c.CrashHandler()
return &c
}
crash探测协程的实现:
func (c *Coordinator) CrashDetector() {
for {
time.Sleep(time.Second * 2)
mu.Lock()
if c.DistPhase == AllDone {
mu.Unlock()
break
}
for _, v := range c.taskMetaHolder.MetaMap {
if v.state == Working {
//fmt.Println("task[", v.TaskAdr.TaskId, "] is working: ", time.Since(v.StartTime), "s")
}
if v.state == Working && time.Since(v.StartTime) > 9*time.Second {
fmt.Printf("the task[ %d ] is crash,take [%d] s\n", v.TaskAdr.TaskId, time.Since(v.StartTime))
switch v.TaskAdr.TaskType {
case MapTask:
c.MapTaskChannel <- v.TaskAdr
v.state = Waiting
case ReduceTask:
c.ReduceTaskChannel <- v.TaskAdr
v.state = Waiting
}
}
}
mu.Unlock()
}
}
在整个lab测试过程我也是不出意外的出了两个test-fail,分别是early_exit,和carsh测试,在这就简单的分享一下心路历程。
首先那个总体的test其实内容还是挺多,我建议先总体的跑一次,然后将没过的test单独,设为一个sh脚本,进行单独测试,例如early_exit一样:
#!/usr/bin/env bash
#
# map-reduce tests
#
# comment this out to run the tests without the Go race detector.
RACE=-race
if [[ "$OSTYPE" = "darwin"* ]]
then
if go version | grep 'go1.17.[012345]'
then
# -race with plug-ins on x86 MacOS 12 with
# go1.17 before 1.17.6 sometimes crash.
RACE=
echo '*** Turning off -race since it may not work on a Mac'
echo ' with ' `go version`
fi
fi
TIMEOUT=timeout
if timeout 2s sleep 1 > /dev/null 2>&1
then
:
else
if gtimeout 2s sleep 1 > /dev/null 2>&1
then
TIMEOUT=gtimeout
else
# no timeout command
TIMEOUT=
echo '*** Cannot find timeout command; proceeding without timeouts.'
fi
fi
if [ "$TIMEOUT" != "" ]
then
TIMEOUT+=" -k 2s 180s "
fi
# run the test in a fresh sub-directory.
rm -rf mr-tmp
mkdir mr-tmp || exit 1
cd mr-tmp || exit 1
rm -f mr-*
# make sure software is freshly built.
(cd ../../mrapps && go clean)
(cd .. && go clean)
(cd ../../mrapps && go build $RACE -buildmode=plugin wc.go) || exit 1
(cd ../../mrapps && go build $RACE -buildmode=plugin indexer.go) || exit 1
(cd ../../mrapps && go build $RACE -buildmode=plugin mtiming.go) || exit 1
(cd ../../mrapps && go build $RACE -buildmode=plugin rtiming.go) || exit 1
(cd ../../mrapps && go build $RACE -buildmode=plugin jobcount.go) || exit 1
(cd ../../mrapps && go build $RACE -buildmode=plugin early_exit.go) || exit 1
(cd ../../mrapps && go build $RACE -buildmode=plugin crash.go) || exit 1
(cd ../../mrapps && go build $RACE -buildmode=plugin nocrash.go) || exit 1
(cd .. && go build $RACE mrcoordinator.go) || exit 1
(cd .. && go build $RACE mrworker.go) || exit 1
(cd .. && go build $RACE mrsequential.go) || exit 1
failed_any=0
echo '***' Starting crash test.
# generate the correct output
../mrsequential ../../mrapps/nocrash.so ../pg*txt || exit 1
sort mr-out-0 > mr-correct-crash.txt
rm -f mr-out*
rm -f mr-done
($TIMEOUT ../mrcoordinator ../pg*txt ; touch mr-done ) &
sleep 1
# start multiple workers
$TIMEOUT ../mrworker ../../mrapps/crash.so &
# mimic rpc.go's coordinatorSock()
SOCKNAME=/var/tmp/824-mr-`id -u`
( while [ -e $SOCKNAME -a ! -f mr-done ]
do
$TIMEOUT ../mrworker ../../mrapps/crash.so
sleep 1
done ) &
( while [ -e $SOCKNAME -a ! -f mr-done ]
do
$TIMEOUT ../mrworker ../../mrapps/crash.so
sleep 1
done ) &
while [ -e $SOCKNAME -a ! -f mr-done ]
do
$TIMEOUT ../mrworker ../../mrapps/crash.so
sleep 1
done
wait
rm $SOCKNAME
sort mr-out* | grep . > mr-crash-all
if cmp mr-crash-all mr-correct-crash.txt
then
echo '---' crash test: PASS
else
echo '---' crash output is not the same as mr-correct-crash.txt
echo '---' crash test: FAIL
failed_any=1
fi
这次lab1其实是春招实习上岸过后开始肝的,加上go的学习、读paper、和看课程视频,写完这篇博客,总的还是花了10天和课程要求的一周还是有出入(实在是废物了orz…),但是扎扎实实自己写下来我觉得还是真的有非常大的收获了,后面有时间也想继续冲接下来的lab。回到这篇的初衷,希望的是当初学lab1不是走马观花,这篇虽然我尽可能的详细的介绍了我的思路,但是还是希望后来者能有自己的思考和实现。这篇也是纯手码的,希望有错误的地方欢迎指正。最后附上自己7/7的allpast截图和完整代码gitee地址~
- git仓库地址:6.824 lab地址
- 觉得有收获的可以帮忙点个star~