阮毋是喜爱虚华,阮只是环境来拖磨;
人客若叫阮,风雨嘛着行,为伊唱出留恋的情歌。
-- 流浪到淡水
终於进到结尾的部份了。过去三十天在铁人赛官方规定的范畴内,笔者也算是半刻意地压抑行文笔调,结果就是整体风格生硬死板;现在的话笔者当然就没有什麽枷锁,可以畅所欲言了。如果说这个系列是一出公路电影,那接下来的结论与展望,就当作是额外收录的幕後花絮吧。
捾着风琴、提着吉他,双人牵做伙 ...
关於 Hoddarla 专案的双轴线,RISC-V 与 Golang,笔者已经不太能够考察其成形之原因了。手边的资料翻来翻去,可以追溯到的是 2017 年初、年假时在日记里面写下的很粗略的想像:希望今年可以用 Golang 写个作业系统跑在 RISC-V 上面。与其说这是个积极的目标,或许更可以解读成是一种逃避。所逃避的,当然就是指两者的主流对应:C 与 x86,作业系统的话当然就是 Linux 了。
当时想将之命名为 Ivix,一方面是因为 2017 的 XVII 可以重新组合,另一方面是因为感觉
-ix
是一种真正有在玩作业系统的感觉。现在想起来果然天真。
C 语言的存在感实在是太强了。深厚的历史(不是与人类文明史相比,而是与电脑科技史相比)、经过时间考验的软件专案们根本数不完、一流的效能、繁荣的生态圈 ... ,再加上任何赞美都是锦上添花。差不多是那个时候吧,也是 Jserv 老师的影响力遍及系统圈的时候,总是会一直在社群网站上看到各种边边角角的 C 语言问题;那些问题,就像赛扬强投每一球都投在好球带框框上一样,对於我这个玻璃心平庸打者来说,只是加深对於未来的焦虑。
焦虑之余,心里还会冒出一堆问号:学 C 语言,真的要学到那的地步才可以吗?真的会有科技公司的面试官故意考未定义行为,然後还不肯承认吗?嘿,说到底,真的有人会为了任何现实的理由,写出那些程序码吗?总之,尽管 C 语言是我的母语,也几乎可以说是我唯一的武器,但心里实在是越来越不喜欢这个语言。推力自然形成。
至於 Golang 的拉力,当然就是简洁、典范化,还有与之同时从一个 C 语言的平庸使用者的视角难以想像的强大功能与标准函式库。典范化这一点其实在笔者心里的洁癖占了很大的因素,比方说,至今我们都还能够在 Linux 文件当中看到这一段话:
First off, I'd suggest printing out a copy of the GNU coding standards,
and NOT read it. Burn them, it's a great symbolic gesture.
多麽愚蠢的事情,空白和 tab 也可以战?在 C 语言专案待太久,会让人误以为语言本身不该明订这种规矩。
很巧,我几天前才看到这篇论文,描述 C 语言规格是如何将未定义行为委托给编译器自行处理,而历史悠久的 C 语言专案(尤其是作业系统)是如何因此建造在不稳顾的地基之上,往往只能仰赖编译器的特殊实作。
後来的语言设计当然会试图记取这教训,其中也包含 Golang。
笔者最早认真考虑也成为一个 OS 自干者的时候,其实并没有排除 x86 这个选项。事实上,为了表达我的认真程度,我自掏腰包印了四册版的 Intel x86_64 手册(花了超过四千元),然後招揽学弟妹们,「我们这暑假来开始 K 这个,然後我们自己试着能不能刻个 OS 出来吧!」
然後,那四本厚重的手册,还躺在笔者实验室的铁柜里面,至少我离开时是如此;PDF 的话还可以搜寻,纸本规格书就只剩下前个世代的人会翻阅吧。至於那个计画,一行程序码都还没有产出。因为 x86 启动时所需的各种权限模式转换的繁文缛节,直接劝退了当时的我们那个臭皮匠团。
所以当我第一次看到 RISC-V,不多关注一点,当然是说不过去的。印象中我应该也是在 RISC-V 已经有名到会出现在脸书墙上时,我才第一次接触到。当然,也没有说非常狂热的投入,但就是隐约觉得,如果我真的要花时间让系统软件成为我的生命的一部分,我希望我能够花在 RISC-V 上。至於 ARM?当然那时候 ARM 已经很有市占率,但也不知道为什麽,从来没有起过那个念头想要靠近它。
命名的经纬实在不足为外人道。总之是觉得,虽然没有硬性规定,但软件专案名称几乎一定是英文的原生字或是谐音字,我为什麽要继续依循这种规则?实在有不甘心之感。如果往日文找,当然有机会多一些中二感,但台湾人有更好的选择。
予焦啦,於是成为一个最後定案的选择。因为一些台语词,对於外国人来讲应该不可能太好发音,所以应该找个简单有力的。至於罗马字的呈现,没有选择 Hodala
或是 Hodarla
,是因为前者已经被饮料店使用,後者也已经被国内服饰品牌使用。我也忘记是其中哪一个,在东南亚某国的语汇里面,但因为我没有解析那些文本的能力,所以也不感贸然共用那些名字。最後就重复 d
表示短促的感觉?家人们都表示 8 个字元太长,但也没办法再砍了。
以结果来说,这个本来毫不相干的命名,慢慢咀嚼倒是有点意思。笔者素不饮酒,也并不欣赏各种酒品带来的感受,会知道这个词,也纯粹是对於流浪到淡水这首歌很有亲近感而已。如此矛盾,用来表示这个专案本身犹如醉汉劝酒的呓语,有一种无法回避的滑稽感。
人生浮沈起起落落毋免来烦恼,有时月圆有时也抹平 ...
从 2017 年初至今,也快要五年了。这一节纪录一下与 Hoddarla 有关的大事记。
cmd/dist
工具对 opensbi/riscv64
系统组合的认识。Korean Fish, go to hell!!!
和这个专案完没关系,但我有纪录这一行在我的开发笔记里面。SysMap
里面完全放任,只决定回传的虚拟记忆体位址。这麽做会使得後续 Golang 执行期真的要存取的时候,触发页表错误的例外;然後,实作非常宽容的页表错误例外处理,只要 Golang 执行期有需求,就生物理页面出来配置映射给它。笔者後来参赛之後,决定重做,也就变成了现在的第二章的样子。要是使用这个方法的话,上下文之类的处理就得拉到更前面来做了。newosproc
的问题,如断章:问题分析当中的分析。It’s hard to believe its 2021 now. Rebasing makes me tired.
nanotime
、walltime
的问题。当时也如现在一样是暂解。Hello World!
,对应到Hello World 与 Uart 机制观察的前半部。这个章节用来回顾这系列以来欠下的技术债。要展望未来之前,得先看过一次这些问题才行。
这个部份,笔者从 2020 年初起算,总共跑过两次。两次的方法其实都一样,只是细节部份不同而已。第一次是,随意搜寻未定义符号,随意解消,也不太遵守原本的架构。比方说,几乎所有的东西都塞在 signal_opensbi_riscv64.go
里面之类的。这一次本战系列文当中,就有特别以 js/wasm
系统组合作为模板来建构大部份符号的解消。
所以,以支援系统组合 opensbi/riscv64
的角度来说,这个部份还算乾净,毕竟空壳就是空壳,能管理就好了。但是技术债仍然有,那就是有些 Golang 档案是由 script 生成的,但笔者并没有完全遵守那一套机制来做事情。如果真的要做到堂堂正正地成为一个 Golang 支援的系统组合,那麽就应该要全部依循那一套作法才行。
GDB 其实还是可以用得很优雅才对,但是笔者在一开始成文之时没有留意到,src/runtime/runtime-gdb.py
竟然可以提供那麽强大的除错功能。所以应该是在本战的系列文的某篇补上了吧。原本的参考来自官方部落格,使用 GDB 除错 Golang 程序的读者应该可以一试。
BSS 初始化也很有趣。笔者第一次清除 BSS 是起源於不同的错误,但现在也没有什麽好考证的了。
这个部份主要在於操作和早期的理解,所以没有什麽技术债。不欠债的好日子也到这里而已了。
笔者藏拙的迹象很明显,那就是关於 Golang 实际上如何做记忆体管理都是轻轻带过,因为其实笔者也不清楚其中内容,甚至比对 Linux 的记忆体管理的理解还要少。但这个部份绝对是至关重要的。Hoddarla 现在只跑一个很简单的输入输出,而且应该还没有任何动态的垃圾回收机制触发,看起来是没有资源的问题;但显然在日後扩充之前,应该先设法触发垃圾回收,看看有没有其他非预期的问题。
守在 Sys*
系列呼叫对於移植系统组合到 Golang 上来说是足够的没有错,但我们今天是需要自己实作出 mmap
系统呼叫的角色,这个部份该怎麽置放程序码呢?笔者当然不想重蹈 Biscuit 覆辙,它们不但得加执行期之下的一层 Shim
层,还得修改执行期本身。以现在的 Hoddarla/ethanol 状态来看,我们其实也在执行期组件当中置放了大量的程序码,这其实不太理想。原先笔者打算多使用 go:linkname
的技巧,让执行期组件里面完全没有 ethanol 核心程序码,而是置放在 main
组件中,就像一般的 Golang 程序一样,但执行起来有其难度,也许之後再尝试吧。
欠债:
runtime/ethanol
组件内容以及 runtime
规划杂乱,是否有望仅留入口,将实作放在 main
之外专案本身的部份?显然现在的虚拟位址配置很阳春,实体位址配置也是如此,而且根本完全不能够回收再利用。
欠债:
欠债:
现在每一个页表项都是读、写、执行全开的状态,当然不是很理想。
欠债:
参数与环境变数还蛮理想的,虽然笔者没有测试过参数的部份(也就是使用 os.Args
去存取),但环境变数的部份我们至少也试过除错用的一个 GODEBUG
。所以这个部份也还好。
理想上我们应该实作真正的计时器系统,否则日後若是多个执行绪都呼叫 sleep
,没有一个机制来记忆所有的到期时间的话,就没有办法精准地按照大家的计时需求来运作。
又,这个部份我们确立了使用 gsignal
来服务中断的构想。只是,这样不会有其他问题吗?比方说我们在第五章已经看过的 malloc during signal
,或许之後其他的事件也会触发也不一定。由於我们与其他系统组合共用的 Golang 框架还是很多,很难说不会有非预期的状况发生。
毕竟笔者自己就是 RISC-V 支援的非同步抢占贡献者。理想上,笔者其实想要接上非同步抢占当作计时器中断,因为两者根本很像;在 linux/riscv64
系统组合中,非同步抢占的原理是让共常式自己发 SIGPREEMPT
给自己,再从讯号处理函数接出去,概念上几乎可以类比。只是非同步抢占的部份程序码,在笔者最後一次确认的印相当中是在某个 Unix-like 系统才有的通用部份之中,所以 ethanol 说真的不是很容易可以挪用。
欠债:
gsignal
机制与其他机制相容性的问题?Biscuit 大方承认他们用的是 Golang 共常式的排程,所以应该是没有另外再多开 m
物件出来了。关於这件事情,目前我们还保留很高的弹性,但是现在的作法很明显的如果在多核心系统上面运行会是个灾难。除了上锁之外,是否还有什麽要小心的呢?所有的 m
与 g
的互动?与 p
的互动?这些都是目前还没有妥善准备的。说穿了,本战当中的准备,只是确保背景的 sysmon
与频道处理的执行绪能够有机会执行而已。
执行期的锁的部份也欠了些东西,就是时间的准确度。这里必须一路从装置树拿到对的内容,然後再看看睡眠模式该怎麽样触发排程,好让其他执行绪能够取得运算资源;如果多核心系统加入进来之後,又该怎麽管理呢?这些笔者都还没有具体的图像。
欠债:
这个部份最大的残念反而不是 Hoddarla 专案本身,而是笔者没有机会探索 QEMU 为何禁止 GDB 直接对 PLIC MMIO 暂存器写入,并加在附录里。但也许这个对於专案本身也不是那麽重要。
结构上欠最多债的还是 PLIC 与 UART 如何能够从 DTB 当中拆解出来的部份。笔者在撰写这个部份时,有意识地让外部中断相关的实作位在 main
组件当中,因为这样比较理想,但如何从执行期一路继承到装置树所在的位置,那就比较困难了。
功能上,笔者其实没有完整追踪完一个完全体的 UART 驱动程序;读者也能发现,在最後实作基本的命令列的时候,笔者并没有确认线路状态,而是一味从读取暂存器中取值,这是过於简单的驱动程序行为。又,驱动程序应该是可以分上下部的,在那之上还有 console 抽象层,而我们现在都还缺乏。
欠债:
你来跳舞,我来念歌诗 ...
说到未来发展路线,笔者认为很复杂。回归初衷吗?但初衷就是 2017 年日记里一句断片,用 Golang 写作业系统跑在 RISC-V 上,但什麽是作业系统?每个主题演讲都会在这个问题上至少逗留一下子,轻松一点的打个哈哈说这个没人说得算,认真一点的会回顾一下古往今来曾经有过的比较权威的定义。那我呢?修补完技术债,然後把系统呼叫刻一刻,然後可以跑既有的应用程序,看起来勉强构得上人家 MIT 博士的 Biscuit 这样吗?像是 Hoddarla: Yet Another POSIX Kernel Done in Modified Golang -- Which Is Still Useless Compared to Linux 这样吗?
王道 POSIX 路线对於笔者来说,实在没有什麽吸引力。既然 Hoddarla 原本是劝酒词,自己也得先醉得厉害才行,那麽接下来的规划有多麽荒谬,应该也都是没有关系的吧。以下是一些好高骛远的幻想,按优先权由高至低:
先打造使用者空间,一样用 Golang 作 ethanol/riscv64
系统组合,然後在上面发展生态系。
之所以将这个优先权排最高,是因为这是我最向往的事情:一个不需要看 gcc 脸色的作业系统发行版,一个使用者空间不会有一堆基本软件包需要 configure; make; make intall
,尤其是你知道那些所谓可携性都已经没有人在乎了的时候。相反的,任何人在任何电脑上,管他是目前最强大的 linux/amd64
系统组合或是 ios/arm64
,只要有一个 Golang 工具链,就可以 bootstrap 整个 Hoddarla。
要做到这点的话,笔者有些小型目标可以先完成,比方说减少 GNU 工具链的依赖程度、减少 shell 的依赖程度;但很现实的是,GDB 和 QEMU 可能短期内都难以割舍。所以相关的延伸工具部份也得再重新实作就是了。
这个时代,档案有何意义?人们真的还需要档案这种东西吗?人们要的是以网路为骨干的人际关系与数位物流、串流影音。人们要的是随拿即有,但又不黏牙不占空间。当网路已经成为基础设施,家用游戏主机、Windows PC、智慧型手机、扫地机器人、空气清静机都只是物理上外出执行任务的登陆艇,只要有需要,它们随时会概念上回到母舰进行更新。
What is a file? 字典有云:A folder or box for holding loose papers together and in order for easy reference. 呃,可是我有 Google Doc,可以多人编辑,我为什麽需要数位实作的一种纸、或是数位实作的一种物件匣?我有付钱给 whatever 网路服务供应商,而且我也付钱给串流平台,我何必存整个影片或音乐在我的手机里?
但关於这点,笔者现在只有破坏的动机,却还没有建设的计画。真的要彻底背弃这个概念的话,我想我还需要多读一些资料库与档案系统的书与实作。所以之後得研究一下吧。
以铁人赛来抽样,浏览器就是最热门的抽象层,超过两百人在上面写程序吧。浏览器确实厉害,已经有太多 javascript 函式库,可以魔术般地在不同浏览器上做出各种兼具视觉冲击与实用性的成果,往日的可携性软件都比不上浏览器这种虚拟机器来得可携。所以如果接下来要设计桌面环境,除了先前宣言的应该参考一下简单版的 Oberon 如何做之外,应该也要参考浏览器提供的方便性与可开发性。
另一个则是云原生。大家都在微服务、容器、分散式架构。如果能够有个系统,在上面写程序之後,编译出来的东西就原生地可以弹性调度在多个物理节点之间、取用各式资源、且可组态可监控、它的输出可以导向归纳整理为日志,那我们是否就可以放下 OCI 层的抽象了?毕竟它也大量仰赖了各种系统呼叫与 namespace 与 cgroup。更具体一点的想像是,如果我写程序最後可以编出一个东西,然後我可以把它像容器一样调度呢?
虽说还在非常简单的状态,Hoddarla 事实上已经展示了所有作业系统模式(S-mode)的状态与控制暂存器的基本使用方法。如果以这个精神延伸,继续把才刚冻结的虚拟机器(HS-/VS-/VU-mode)规格也都用最基本的功能兜起来,似乎也蛮有趣的。
这个优先权最低,因为笔者心中的想像是,当我对这个专案的责任感与成就感消弭的比我想像中还要飞快,但又想要捞出来做个资源回收的时候,才会想要走的路线。
很现实的是,如果先前的技术债没有偿还,这些也都是空谈。总的来说,笔者估计如果接下来的精力持续消退,那比较可行的时程应该是,
项目\时间 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 |
---|---|---|---|---|---|---|
还技术债 | >>>>++ | ++++++ | ++++++ | |||
思考系统架构图 | >>>>++ | ++++++ | ++++++ | ++++++ | ++++++ | |
实作 virtio-net driver | >>>+++ | +++<<< | ||||
实作 HTTP2/TCP/IP | >>>+++ | +++<<< | ||||
研究使用者空间 Golang | ++++++ | ++++++ | ||||
实作所需之 system call | ++++++ | ++++++ | ++++++ |
所以,就先抓 2025 Q4 释出 Hoddarla 1.0 版吧!
上述的时间轴分类法适用於不同阶段的项目,但 Hoddarla 专案本身的维护方法,却是各个阶段皆然。本节两段更新的经验,首先是描述 1.3 节到 2.0 节之间的更新。也就是,以上游的 master
分支(branch)为基准来讲,从 ab4085ce84f8378b4ec2dfdbbc44c98cb92debe5 到 9e26569293c13974d210fd588ebfd29b857d8525 的改动。两者相距约莫两周。
在开始撰写任一小节之前,笔者会确保 GOBASE
档案内容存放一个 Golang 专案上游 master
分支的 commit ID。当开发者执行 make apply
时,首先会从上游拉取 Golang 专案,并重设(reset)到指定的提交(commit)序号。之後,patch
资料夹底下的所有修补(patch)就会在该提交之上建立起来。
打上所有修补之後,应当要能够没有任何错误地执行完建置工具链的流程,并且能够编译 opensbi/riscv64
的 ethanol 核心映像档。
从这里出发,就能够开始撰写新的小节,进行恰当的修改之後,在 Golang 专案内创造新的提交。再之後,在 hoddarla 专案内执行 make patch
时,新的提交就会成为新的修补,进入到 patch
资料夹。
一段时间之後,总是得进行升版,否则会和 Golang 上游的发展逐渐脱节,导致难以维护的後果。Hoddarla 进行至今已经超过一年半,目前为止是以两周到一个月不等的频率升版。目前还没有决定基底更新的策略,总之是都先取一个当下最新的提交作为新的基底。
当然,最常使用的工具就是 git rebase
。每次升版的流程是:
$ git fetch origin master
remote: Enumerating objects: 109, done.
remote: Counting objects: 100% (109/109), done.
remote: Compressing objects: 100% (48/48), done.
remote: Total 109 (delta 63), reused 101 (delta 61), pack-reused 0
接收物件中: 100% (109/109), 205.96 KiB | 1.37 MiB/s, 完成.
处理 delta 中: 100% (63/63), 完成 11 个本机物件.
来自 https://github.com/golang/go
c8f4e6152d..9e26569293 master -> origin/master
aa4da4f189..ab361499ef dev.cmdgo -> origin/dev.cmdgo
c6d3d0b0ad..897970688b dev.typeparams -> origin/dev.typeparams
$ git rebase origin/master
如果顺利的话,基本上可以沿着前一小节的描述继续进行,因为所有既有的修补都能够顺利的在新的基底上使用。但若像是 1.3 到 2.0 节时的更新,那问题就比较复杂一点了。续前指令:
$ git rebase origin/master
自动合并 src/go/build/syslist.go
冲突(内容):合并冲突於 src/go/build/syslist.go
error: 不能套用 e57488ec62... 0.1 Toolchain
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
不能套用 e57488ec62... 0.1 Toolchain
go (master|REBASE 1/7)
$
如果读者还没有在常用的工作环境内启用
git-prompt
工具,我会强力建议设置!像此时的状况就能够展示 git 本身的状态为何,相当具有提示性。
基本上,遇到冲突当然是不太情愿,但毕竟 Golang 是个跨系统的程序语言,所以冲突的状况有很多种:
opensbi/riscv64
组合删改。git apply
指令可以执行,但是无法建置工具链。幸好,升版时遭遇的冲突大多停留在前两者的状况,但每一次也是需要见招拆招。以这次为例,以下再分小节描述。
这里的冲突算是蛮轻微的,只要观察事发的档案即可了解:
$ git diff
diff --cc src/go/build/syslist.go
index 60ac5511bd,dcb9f2bfb7..0000000000
--- a/src/go/build/syslist.go
+++ b/src/go/build/syslist.go
@@@ -7,5 -7,5 +7,10 @@@ package buil
// List of past, present, and future known GOOS and GOARCH values.
// Do not remove from this list, as these are used for go/build filename matching.
++<<<<<<< HEAD
+const goosList = "aix android darwin dragonfly freebsd hurd illumos ios js linux nacl netbsd o
penbsd plan9 solaris windows zos "
+const goarchList = "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le loong64 mips mips
le mips64 mips64le mips64p32 mips64p32le ppc riscv riscv64 s390 s390x sparc sparc64 wasm "
++=======
+ const goosList = "aix android darwin dragonfly freebsd hurd illumos ios js linux nacl netbsd o
penbsd opensbi plan9 solaris windows zos "
+ const goarchList = "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips6
4 mips64le mips64p32 mips64p32le ppc riscv riscv64 s390 s390x sparc sparc64 wasm "
++>>>>>>> e57488ec62 (0.1 Toolchain)
差异点在於 goosList
我们需要加上 opensbi
,而 goarchList
中新增了一个 loong64
架构。这个不难处理,兼容即可。
修补的预设行为是不会更新提交当中的时间。如果这个行为无法接受的话,一开始的重定基底指令可以增加一个参数变为
git rebase --ignore-date origin/master
。
修改完 src/go/build/syslist.go
之後,
$ git rebase --continue
[分离 HEAD b2c95baada] 0.1 Toolchain
20 files changed, 56 insertions(+), 20 deletions(-)
create mode 100644 src/runtime/internal/sys/zgoos_opensbi.go
成功重定基底并更新 refs/heads/master。
看起来第一步是成功了。
以这次的例子来讲,也是成功建置,没有问题。
...
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000a109
HI0000000000000005
ffffff8000075d00
0000000080245ae8
仍然是当时还没启用虚拟记忆体的状态。顺利升级!
最後就是接续该小节的主题,继续开始进行罗!
在这两篇的写作之间,Golang 专案正在经历 1.17 版的发行,也进了相当大型的变动,关於系统组合的管理。
goarch
与 goos
这两组搬到 src/internal
之下。笔者仍然得重新引入 opensbi,但是甚至无法执行 go run gengoos.go
,因为变动太过剧烈的关系,这个指令会抱怨 runtime.convT2E
是不存在的符号。但这里可以先编译一支新的工具链,之後再继续重定基底。
变成 2.4 时加入的 SFENCEVMA
指令不被认得!这也是没办法的,因为针对 master 分支重新编译了工具链,所以这些我们在第 0 章已经实作完的部分就不在工具链的支援范围了。
所以,这里笔者先回溯到 0.4 去,重新编成工具链後,再重设到 2.4 的状态,重新编成工具链。
笔者也不知道这样的滚动式维护模式可以维持多久。但相较於前述的 Biscuit,在初期就缺乏一个良好的维护规划的情况下,专案只能随着时间尘封在旧版本当中。虽然说学术论文导向的开发就是这样子,但也难免唏嘘。
铁人赛期间,可以很简单的以每个章节的内容来区分修补的范围如何。但是铁人赛之後,乃至於 Hoddarla 专案本身的未来目标,笔者期望还是能够更能按照功能或是概念重新对应这些修补。并且,如果能够研拟一套自动流程,就能够以更高频率进行这个升版的作业。
最不敢奢求的梦,当然就是 Hoddarla 计划吸引足够多的玩家并累积足够的人气,让这些化外之修补也能够进入上游。
这个专案对我来说,是第一次执行时间切分地如此零碎(每天下班一、两个小时,周末顶多三、四个小时),但时间尺度又横跨一年半的专案。过程中累积的很多感觉,随着铁人赛完赛的兴奋快速流失,但是那些过程中的痛苦,才是 Hoddarla 更真实的样貌吧。
回顾完 Hoddarla 专案本身,明天的最後一篇结语章节,会分享我在其他铁人系列当中学到的东西。总之,祝福各位铁人都能完赛!但我还要再发一篇。各位读者,我们明天再会!
<<: 语义检索 Semantic Search NLP ( BM25 +wordcloud+LSA summary )
客制化PWM 这里所说的客制化PWM指的就是我们可以输出任何想要的方波波形,例如输出10个完整的波後...
昨天我们已经成功建立资料库了,今天要做的是将资料存进资料库并且让TableView能马上更新资料库里...
昨天我们介绍fetch用get方式来请求资料,并将取得的资料转为JSON格式做运用,今天要来介绍fe...
[Day2] CSS + JS Clock 运用 CSS 和 Javascript 做一个虚拟时钟 ...
现在使用智慧型手机比率最高,手机画面很小,所以在制作网页时应注意以下细节 只显示重要的资讯及减少栏位...