111qqz的小窝

老年咸鱼冲锋!

2019 to do list

  • Operating Systems: Three Easy Pieces
  • fluent python
  • 《计算机网络:自顶向下方法》
  • 《mysql必知必会》
  • PC Assembly Language ( for mit 6.828 )

【试工中】 halide学习笔记

Halide is a programming language designed to make it easier to write high-performance image and array processing code on modern machines. 

halide有两个特性比较吸引人。一个是对于各种平台架构的支持。

  • CPU architectures: X86, ARM, MIPS, Hexagon, PowerPC
  • Operating systems: Linux, Windows, macOS, Android, iOS, Qualcomm QuRT
  • GPU Compute APIs: CUDA, OpenCL, OpenGL, OpenGL Compute Shaders, Apple Metal, Microsoft Direct X 12

另一个是把计算什么和怎么计算(何时计算)分离开来。

可以直接参考tutorials 来学习

 

下面是一段将Halide Buffer转化成opencv Mat的代码,用于调试。

 

 

 

【施工中】MIT 6.828 lab 2: Memory Management

Part 1: Physical Page Management

操作系统必须时刻追踪哪些物理内存在使用,哪些物理内存没有在使用。

一个问题是,

Exercise 1. In the file kern/pmap.c, you must implement code for the following functions (probably in the order given).

boot_alloc()
mem_init() (only up to the call to check_page_free_list(1))
page_init()
page_alloc()
page_free()

check_page_free_list() and check_page_alloc() test your physical page allocator. You should boot JOS and see whether check_page_alloc() reports success. Fix your code so that it passes. You may find it helpful to add your own assert()s to verify that your assumptions are correct.

练习1要求写一个physical page allocator。我们先看第一个函数boot_alloc()

 

这个函数只有在JOS初始化虚拟内存之前会被调用一次。

通过查看 mem_init 函数可以知道,boot_alloc 是用来初始化页目录(page directory)

为什么我们需要一个单独的page allocator呢?原因是:

kernel启动时需要将物理地址映射到虚拟地址,而我们需要一个page table来记录这种映射关系。但是创建一个page table涉及到为page table所在的page分配空间…而为一个page分配空间需要在将物理地址映射到虚拟地址以后。。

解决办法是,使用一个单独的page allocator,在一个固定的位置allocate memory. 然后在这部分去做初始化的工作。

参考xv6-book:

There is a bootstrap problem: all of physical memory must be mapped in order
for the allocator to initialize the free list, but creating a page table with those mappings
involves allocating page-table pages. xv6 solves this problem by using a separate page
allocator during entry, which allocates memory just after the end of the kernel’s data
segment. This allocator does not support freeing and is limited by the 4 MB mapping
in the entrypgdir, but that is sufficient to allocate the first kernel page table.

这个函数有两个难点,第一个是,如何才能”allocate memory”? 说到”allocate memory”总是想到malloc…但是现在我们什么都没有…

然而实际上很简单(虽然我卡了好一会。。。),我们只要计算出第一个虚拟地址就好了。根据注释, magic symbol ‘end’位于没有被任何kernel code或全局变量占用的虚拟地址的起始位置。

第二个是,如何确定何时空间不够? 我们观察函数i386_detect_memory

发现这个函数的作用是得到剩余的物理内存。其中basemem就是0-640k之间的memory,extmem是1M以后的memory.

npages是剩余物理内存的页数,每页的大小是PGSIZE。因此一共能分配的空间大小为(npages*PGSIZE)

而虚拟地址的base为KERNBASE(定义在inc/memlayout.h中),因此最大能访问的虚拟地址为KERNBASE+(npages*PGSIZE)

最后的实现为:

接下来的部分就相对简单了。首先是mem_init,初始化PageInfo,由于是在page_init之前,不能使用page_alloc,因此这部分allocate也是由boot_alloc完成的。这也是唯二的由boot_alloc来分配内存的部分。代码如下:

接下来是page_init.这部分主要是判断哪些page是free的,哪些不是,参考注释,主要是[EXTPHYSMEM,…)这部分。 我们知道,对于EXTPHYSMEM之上的内存空间,首先kernel占用的空间,kernel之后是分配给kern_pgdir的空间,再然后是分配给PageInfo的空间。这之后的空间,应该都是可用的。因此代码如下:

再然后是page_alloc函数。其实就是取一个链表头的操作。

再之后的page_free. 相对应的,就是在链表头插入一个节点的操作。

到现在,练习1就算完成了。怎么知道我们的实现是对的呢,启动JOS,断言应该挂在page_insert处,并且make grade显示Physical page allocator: OK  就应该是没问题了。

C语言变长参数

说起C语言的变长参数,可能听起来比较陌生,因为很少会需要自己实现。不过想一下scanf和printf,参数个数的确是不固定的。

stdarg.h 中提供以一套机制来实现变长参数。以及,要说明的是,变长参数不是什么黑魔法,原理依赖于stack frame的结构,具体可以参考x86-calling-conventions   简单来说,由于函数参数入栈的顺序是固定的,因此一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变长参数的位置 

在实现上,需要了解的是:

  • va_list,一个类型,可以看做是变长参数列表;
  • va_start,用来初始化变长参数列表的宏,声明为void va_start( va_list ap, parm_n );  ap为va_list变量,parm_n为变长参数前一个变量(C语言要求至少有一个named variable作为函数的parameter)
  • va_arg,用来得到下一个参数的宏,声明为T va_arg( va_list ap, T ); 返回的类型取决于传入的类型T。特别注意:”If  va_arg is called when there are no more arguments in  ap, the behavior is undefined.”
  • va_end ,用来将va_list释放的宏。

下面看一个例子就明白怎么用了orz

如果想研究c语言中变长参数的具体实现,可以参考 也谈C语言变长参数

参考资料:

Variable numbers of arguments

 

x86 calling conventions

x86的调用约定主要说的是这几件事:

  • The order in which atomic (scalar) parameters, or individual parts of a complex parameter, are allocated
  • How parameters are passed (pushed on the stack, placed in registers, or a mix of both)
  • Which registers the called function must preserve for the caller (also known as: callee-saved registers or non-volatile registers)
  • How the task of preparing the stack for, and restoring after, a function call is divided between the caller and the callee

调用约定实际上并不唯一

我们比较关注gcc编译器下的cdecl(C declaration)

对于如下这段代码:

调用过程如下:

在c语言中,函数的参数被以从右向左的顺序压入栈,也就是最后一个参数最先入栈。

这里是栈指的是调用栈(Call_stack)

调用栈的结构如下(注意此图的栈是从下往上增长的,这与通常情况并不相符,不过不影响此处的说明),这是在调用的DrawSquare函数中调用DrawLine函数时的情景

stack frame通常按照入栈顺序(写在前面的先入栈)由三部分组成(可能某部分为空):

  • 函数的参数值(以从右向左的顺序入栈)
  • caller的地址值,为的是调用函数之后能继续执行caller其余的代码。
  • 函数的局部变量

 

接下来我们看一下调用过程对寄存器的影响。这里暂且不提eax寄存器通常用来保存结果之类,主要想谈谈调用过程对sp和bp两个寄存器的影响。

sp是stack pointer,保存的是当前栈顶地址

bp是base pointer(就是stack frame中的frame pointer), 值为函数刚刚被调用时的栈顶位置。

bp这个寄存器的作用主要是比较方便,因为如果只有stack pointer,那么在函数里面,stack pointer也是可能变的,显然不如使用base pointer方便。

具体来说,在使用base pointer的情况下,函数的返回地址永远为ebp + 4,第一个参数的地址为ebp+8,第一个局部变量的地址为ebp-4

而且使用bp的情况下,回溯调用栈会变得非常方便。

At ebp is a pointer to ebp for the previous frame (this is why push ebp; mov ebp, esp is such a common way to start a function).  This effectively creates a linked list of base pointers.  This linked list makes it very easy to trace backwards up the stack.  For example if foo() calls bar() and bar() calls baz() and you’re debugging baz() you can easily find the parameters and local variables for foo() and bar().

为什么ebp指向的内容是上一个 stack frame中的ebp?我们看push ebp; mov ebp esp这两条指令。push ebp相当于先esp-=4,然后将ebp放到esp所指向的位置。接着mov ebp esp,相当于把当前的esp,也就是上一个ebp所在的位置,赋值给新的ebp.  所以。。这其实是个链表啊

 

 

参考资料:

x86 calling conventions

Stack_register

Call_stack#STACK-FRAME

What is exactly the base pointer and stack pointer? To what do they point?

All About EBP

 

 

【施工完成】MIT 6.828 lab 1: C, Assembly, Tools and Bootstrapping

花费了30+小时,终于搞定了orz

 

Part 1: PC Bootstrap

The PC’s Physical Address Space

8086/8088时代

由于8086/8088只有20跟地址线,因此物理内存空间就是2^20=1MB.地址空间从0x00000到0xFFFFF.其中从0x00000开始的640k空间被称为”low memory”,是PC真正能使用的RAM。从 0xA0000 到 0xFFFFF 的384k的non-volatile memory被硬件保留,用作video display buffers和BIOS等。

READ MORE →

优化学习笔记(1):Loop unrolling

迫于生计,最近要学习halide

先去学习/复习一下常见的编译优化技巧。

loop unrolling,也就是循环展开,顾名思义,就是把循环展开来写。

循环展开是一种优化,可以手动实现也可以编译器自动实现。

为什么要将循环展开?

  • 循环每次都需要判断终止条件,展开后可以消除这部分开销。
  • 减少分支预测开销。循环里的分支是指“跳出循环”还是“进行下一次迭代”
  • vectorization

    可以看到最里面一层循环被展开以实现向量化.向量化是一种优化计算的手段。该优化的实现基于SIMD 和Advanced_Vector_Extensions(AVX)指令集架构的支持。
  • 消除和loop counter(i)有关的除法计算。
  • 消除循环内部的分支。比如loop counter奇数和偶数时会进入不同的分支,那么将循环展开后,就消除了该分支。

有什么缺点?

  • 代码体积增加了。这对于嵌入式设备等往往是不可接受的。
  • 代码可读性变差了。
  • 单一指令可能会使用更多的寄存器,导致性能下降。
  • 如果循环内部包含函数调用,那么和函数的内联(inline)优化会有冲突。原因是,循环展开+函数展开…代码的体积会爆炸。

参考资料

 

【施工中】MIT 6.828 Operating System Engineering 学习笔记

课程主页

这课稍微有点硬核…感觉基础稍微有些不扎实就做不下去orz.

网上似乎是有博客写了6.828的学习笔记,不过我更希望自己能够独立完成,二手的知识,谁知道是对的错的呢…况且课程本身给的参考资料应该还是足够多的。

环境的话,手头没有ubuntu系统,恰好半年前剁了阿里云的轻应用服务器,就在上面做吧。

为了这门课,我读了/计划读以下书籍(随时更新)。大概也是为了检查一遍自己的知识体系。

每个lab用到的网页形式的参考资料,会在每个lab的博客中分别给出。

最后,放一段《游褒禅山记》中的文字,与君共勉!

夫夷以近,则游者众;险以远,则至者少。而世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也。有志矣,不随以止也,然力不足者,亦不能至也。有志与力,而又不随以怠,至于幽暗昏惑而无物以相之,亦不能至也。然力足以至焉,于人为可讥,而在己为有悔;尽吾志也而不能至者,可以无悔矣,其孰能讥之乎?

 

codeforces round 530 div2

A,B,C:都很简单,不说了。

D:一棵树,给出树的结构,以及从树根到某个深度为偶数的节点的路径和,问能否构造一种所有节点点权和最小的树,输出最小点权和。

思路:

容易知道,如果想要点权和最小,那么尽可能让靠近树根的点承担更多的点权。

具体做法是,bfs,对于每个节点u,取其儿子中最小的S值求节点u的信息。

比赛的时候wa16…最后发现是答案要用long long存…因为单个路径和是<=1E9的。。多个加起来会超过int…  长时间不打连这种常见的坑都不敏感了啊。。。

 

codeforces hello 2019

好久没玩cf了,竟然还能涨分(虽然我用的小号Orz)

三题,D应该是数学+DP…数学实在是忘干净了。。。

前面三题大体还好,都是1A,不过因为没有提前配置环境。。耽误了一些时间。。。

A:给出一个扑克牌x,和另一个包含5个扑克牌的集合。问扑克牌x是否和扑克牌集合中至少一张扑克牌的花色或者数字相同。

不多说了。

B:一块钟表(只有一个指针),初始指向12点,需要拨动指针恰好n次(n<=15),每次可能顺时针,也可能逆时针,拨动的角度范围在[1,180],问是否有一种方案,使得拨动n次后,指针回到12点。

思路:观察下数据范围,n最大才15,最多也不过2^15的情况…既然如此,不如暴力。

枚举的话我记得有三种方法来着。。。但是已经不记得怎么写了。。所以用了最朴素的办法。。。

C: 给出n(n<=1E5)个括号序列,括号序列可能不合法,现在要从这n个括号序列中,组成尽可能多的有序二元组,使得有序二元组的括号序列合法,并且每个括号序列只能出现在一个有序二元组中,现在问最多能组成多少这样的有序二元组。

思路:我们先考虑一下怎样的两个括号序列组成的有序二元组才是合法的有序序列。容易想到的是,如果两个括号序列本身都是合法的,那么组合在一起也一定是合法的。进一步,对于本身不合法的括号序列,容易知道,其必须只有一种没有完成匹配的括号方向,且该括号方向的数量与相反括号方向的数量相同,才能完成匹配。

因此做法是,对于括号序列预处理,得到该括号序列的状态(本身匹配为0,正数代表'(‘的个数,负数代表’)’的个数,如果有两个方向同时存在,则直接舍弃掉,因为这种括号序列不可能组成合法的括号序列。预处理之后,用multiset搞一下。

代码写得比较乱…flag存的时候其实没必要存index…

D:初始一个数n(n<=1E15),k次操作,每次操作等概率将当前的数变为其因子中的一个。问k次操作之后,结果的期望是多少。

在@适牛的指导下,以及参考了官方题解。。写了出来。。

dp还是太弱了。。。。

比较重要的一点是,不需要考虑所有因子,只需要考虑质因子。

质因子的个数不超过50个(因为 2^50 > 1E15)

另外一个重要的是,对于每一个质因子的概率是积性函数,可以乘在一起。

因此问题变成了,对于一个质因子唯一的n,如何算所求的期望。

我们考虑dp[i][j]表示第i次操作后,质因子还剩j个的概率。

显然dp[0][tot]=1,其中 p^tot = n,p为某个质因子。

转移方程为:

dp[i][j] = sum(dp[i-1][jj]) (j=<jj<=tot)

然后最后结果的期望就是:sum(dp[k][j]*p^j) (0=<j<=tot)

还有一点,由于题目的输出要求,需要用到费马小定理求个逆元。。。

逆元相关的参考 acdreamer的博客。。。

 

 

 

 

我在公司的服务器上执行了sudo rm -rf /*

TL;DR

  • 依靠人的小心谨慎是不靠谱的,人总有失误的时候
  • 看了下docker volume的权限机制,貌似是从docker image中继承。
  • 写了两个脚本,用来把rm alias到mv,避免手滑

 

又是一个可以摸鱼的周五晚上,sensespider系统测试了一天,fix了几个Bug,似乎可以发布了。系统一直是部署在了docker中..这几天测试产生了不少结果文件在host的volume中… 看着不舒服,干脆删一下好了

嗯?怎么所有者是root。。。那我sudo一下,也没什么大不了的嘛

然而手滑了… 打了个 sudo rm -rf /*   …

 

提示无法删除/boot  device is busy…

吓了一跳,下意识Ctrl-C…

从新在本地ssh到服务器,发现已经登不上去了…报错在找不到sh

看了一下,果然服务器的/bin 目录已经被删干净了…

google了一些从rm中恢复文件的帖子…

试图用 sudo apt-get install  装一些工具包…

这个时候已经提示我找不到apt-get 了。。。

非常慌。花了3分钟思考了我到目前为止的一生

看了下scp命令还在,赶紧趁着这个终端回话还没关,把本地的/bin目录拷贝了上来。

试了下,ssh命令可以用了。 这样至少后续的修复(在不麻烦IT同事的情况下)不用跑机房了。有些镇定。

然后发现apt-get 命令还是用不了。。。思考了1分钟。。。

然后发现服务器用的是centos…….

再试了各种常用命令,试了docker相关的各种命令,都可以正常工作。

然而整个人都被吓傻了….睡了一觉才回过神。

又查了下docker volume权限的事情,发现挂载目录继承docker image中用户的权限是feature  Volumes files have root owner when running docker with non-root user.   那似乎就没办法了。

以及写了两个脚本,来避免手滑,分别是zsh环境和bash环境下的。

kkrm

 

 

docker network 与 本地 network 网段冲突

起因:

公司部署在hk的爬虫服务器突然挂掉了。后来发现只是在深圳办公区无法访问。排查后发现原因是docker的网络(包括docker network的subnet或者是某个容器的ip)与该host在内网的ip段相同,导致冲突。

排查过程:

有两个方面需要排查。一个是docker服务启动时的默认网络。

默认网络使用bridge桥接模式,是容器与宿主机进行通讯的默认办法。

修改默认网段可以参考 http://blog.51cto.com/wsxxsl/2060761

除此之外,还需要注意docker创建的network的网段。

使用docker network ls 命令查看当前的网络

然后可以使用docker inspect 查看每个network的详细信息。

也可以直接使用ip addr 来查看各种奇怪的虚拟网卡的ip,是否有前两位的地址和host的ip地址相同的。

解决办法:

本想在docker-compose up 时指定默认网络的subnet

结果发现好像并不支持?version 1.10.0 error on gateway spec

Was there any discussion on that? I do need to customize the network, because my company uses the 172.16.0.0/16 address range at some segments and Docker will simply clash with that by default, so every single Docker server in the whole company needs a forced network setting.

Now while upgrading my dev environment to Docker 1.13 it took me hours to stumble into this Github issue, because the removal of those options was completely undocumented.

So please, if I am working on a network which requires a custom docker subnet, how am I supposed to use Docker Compose and Docker Swarm?

最后用了个比较间接的办法。

先手动创建一个docker network,然后再在docker-compose的配置文件中指定。

 

 

 

 

 

 

记一次在 docker compose 中使用volume的踩坑记录

现象:

使用docker compose 挂载 named volume 无效(且没有错误提示)

排查过程:

一开始是没有使用docker-compose命令,直接使用docker run  -v 命令,挂载两个绝对路径,没有问题。

然后使用named volume,在这里使用了local-persist 插件,来指定数据卷(volume)在host上的位置。直接用docker run  -v 命令,依然没有问题。

接下里打算放到docker compose里面,发现并没有挂载成功。

但是在docker compose里面,挂载两个绝对路径是ok的。

于是怀疑是volume的问题

此时使用docker inspect 查看 用docker compose 启动起来的,挂载named volume的容器

发现mount里面,挂载的named volume并不是我在docker-compose.yml填写的名称,而是多了一个前缀,这个前缀恰好是docker-compose.yml 文件所在的目录名称。

查了一下,发现果然不止我一个人被坑到orz Docker-compose prepends directory name to named volumes

其实应该直接使用docker inspect来排查的…应该会更快找到问题

解决办法:

有几种解决办法:

  • 不手动创建volume,而是在docker-compose.yml中,设置volume的mountpoint
  • 在docker-compose.yml中,添加external: true的选项到 volume中,参考external

顺便附上我的docker-compose.yml文件

 

 

 

 

How to use Scrapy with Django Application(转自medium)

在meidum上看到一篇很赞的文章…无奈关键部分一律无法加载出来…挂了梯子也不行,很心塞…刚刚突然发现加载出来了…以防之后再次无法访问,所以搬运过来.

There are couple of articles on how to integrate Scrapy into a Django Application (or vice versa?). But most of them don’t cover a full complete example that includes triggering spiders from Django views. Since this is a web application, that must be our main goal.

What do we need ?

Before we start, it is better to specify what we want and how we want it. Check this diagram:

It shows how our app should work

  • Client sends a request with a URL to crawl it. (1)
  • Django triggets scrapy to run  a spider to crawl that URL. (2)
  • Django returns a response to tell Client that crawling just started. (3)
  • scrapy  completes crawling and saves extracted data into database. (4)
  • django fetches that data from database and return it to Client. (5)

Looks great and simple so far.

A note on that 5th statement

Django fetches that data from database and return it to  Client. (5)

Neither Django nor client don’t know when  Scrapy completes crawling. There is a callback method named  pipeline_closed, but it belongs to Scrapy project. We can’t return a response from Scrapy  pipelines. We use that method only to save extracted data into database.

 

Well eventually, in somewhere, we have to tell the client :

Hey! Crawling completed and i am sending you crawled data here.

There are two possible ways of this (Please comment if you discover more):

We can either use  web sockets to inform client when crawling completed.

Or,

We can start sending requests on every 2 seconds (more? or less ?) from client to check crawling status after we get the  "crawling started" response.

Web Socket solution sounds more stable and robust. But it requires a second service running separately and means more configuration. I will skip this option for now. But i would choose web sockets for my production-level applications.

Let’s write some code

It’s time to do some real job. Let’s start by preparing our environment.

Installing Dependencies

Create a virtual environment and activate it:

Scrapyd is a daemon service for running Scrapy spiders. You can discover its details from here.

python-scrapyd-api is a wrapper allows us to talk  scrapyd from our Python progam.

Note: I am going to use Python 3.5 for this project

Creating Django Project

Create a django project with an app named  main :

We also need a model to save our scraped data. Let’s keep it simple:

Add  main app into  INSTALLED_APPS in  settings.py And as a final step, migrations:

Let’s add a view and url to our  main app:

I tried to document the code as much as i can.

But the main trick is,  unique_id. Normally, we save an object to database, then we get its  ID. In our case, we are specifying its  unique_id before creating it. Once crawling completed and client asks for the crawled data; we can create a query with that  unique_id and fetch results.

And an url for this view:

Creating Scrapy Project\

It is better if we create Scrapy project under (or next to) our Django project. This makes easier to connect them together. So let’s create it under Django project folder:

Now we need to create our first spider from inside  scrapy_app folder:

i name spider as  icrawler. You can name it as anything. Look  -t crawl part. We specify a base template for our spider. You can see all available templates with:

Now we should have a folder structure like this:

Connecting Scrapy to Django

In order to have access Django models from Scrapy, we need to connect them together. Go to  settings.py file under  scrapy_app/scrapy_app/ and put:

That’s it. Now let’s start  scrapyd to make sure everything installed and configured properly. Inside  scrapy_app/ folder run:

$ scrapyd

This will start scrapyd and generate some outputs. Scrapyd also has a very minimal and simple web console. We don’t need it on production but we can use it to watch active jobs while developing. Once you start the scrapyd go to http://127.0.0.1:6800 and see if it is working.

Configuring Our Scrapy Project

Since this post is not about fundamentals of scrapy, i will skip the part about modifying spiders. You can create your spider with official documentation. I will put my example spider here, though:

Above is  icrawler.py file from  scrapy_app/scrapy_app/spiders. Attention to  __init__ method. It is important. If we want to make a method or property dynamic, we need to define it under  __init__ method, so we can pass arguments from Django and use them here.

We also need to create a  Item Pipeline for our scrapy project. Pipeline is a class for making actions over scraped items. From documentation:

Typical uses of item pipelines are:

  • cleansing HTML data
  • validating scraped data (checking that the items contain certain fields)
  • checking for duplicates (and dropping them)
  • storing the scraped item in a database

Yay!  Storing the scraped item in a database. Now let’s create one. Actually there is already a file named  pipelines.py inside  scrapy_project folder. And also that file contains an empty-but-ready pipeline. We just need to modify it a little bit:

And as a final step, we need to enable (uncomment) this pipeline in scrapy  settings.py file:

Don’t forget to restart  scraypd if it is working.

This scrapy project basically,

  • Crawls a website (comes from Django view)
  • Extract all URLs from website
  • Put them into a list
  • Save the list to database over Django models.

And that’s all for the back-end part. Django and Scrapy are both integrated and should be working fine.

Notes on Front-End Part

Well, this part is so subjective. We have tons of options. Personally I have build my front-end with  React . The only part that is not subjective is  usage of setInterval . Yes, let’s remember our options:  web sockets and  to send requests to server every X seconds.

To clarify base logic, this is simplified version of my React Component:


 

 

You can discover the details by comments i added. It is quite simple actually.

Oh, that’s it. It took longer than i expected. Please leave a comment for any kind of feedback.

 

 

 

 

lua学习笔记

lua是一门轻量级的脚本语言…好像比较适合写游戏?在 太阳神三国杀 中见过很多lua脚本。 由于splash 的渲染脚本需要用lua来写,因此来学习一波。

直接上语法…看到了python和pascal的影子orz