摘 要

如果一定要找出OS最重要的核心,那就是调度器,调度器本身即可以看作一个简单的操作系统,允许以周期性或单次方式来调用任务。从底层的角度看,调度器可以看作是一个由许多不同任务共享的定时器中断服务程序,因此,只需要初始化一个定时器,而且改变定时的时候通常只需要改变一个函数。此外,无论需要运行1个,10个还是多个不同的任务,通常都可以使用同一个调度器完成。随着计算机的发展,我们比较容易能够使用高级程序语言来实现调度器。那么使用一个低级程序语言像汇编语言能不能实现一个调度器。我们将要基于Bochs, 做一个小OS试一试。

特别说明

该项目是学习期间作为OS课程大作业而开发的,功能没有太多花样。本篇博客基于当时提交的报告更改而来。

技术文档PDF版:浏览链接

完整代码

在此下载

先行知识

1.1 虚拟计算机Bochs

即便没有听说过虚拟计算机,你至少应该听说过磁盘映像。如果经历过DOS时代,你可以就曾经用HD-COPY把一张软盘做成一个.IMG文件,或者把一个.IMG文件恢复成一张软盘。简单来讲,它相当于运行在计算机的小计算机。在介绍Bochs及其他工具之前,需要说明一点,这些工具并不是不可或缺的,介绍它们仅仅是为了提供一些可供选择的方法,用以搭建自己的工作环境。但是,这并不代表这一章就不重要,因为得心应手的工具不但可以愉悦身心,并且可以起到让工作事半功倍的功效。下面就从Bochs开始介绍。

我们先来看看Bochs是什么样子的,请看图1.1这一个屏幕截图。窗口的标题栏一行"Bochs x86 emulator"明白无误地告诉我们,这仅仅是个"emulator"——模拟器而已。在本文中我们把这种模拟器成为虚拟机,因为这个词使用得更广泛一些。不管是模拟还是虚拟,我们要的就是它,因为有了它我们不再需要频繁地重启计算机,即便程序有严重的问题,也丝毫伤害不到你的计算机。更加方便的是,可以用这个虚拟机来进行操作系统的调式。

图1.1,Linux中的Bochs

1.2 Bochs的安装

就像大部分软件一样,在不同的操作系统里面安装Bochs的过程是不同的,在Window中,最方便的方法就是从Bochs的官方网站获取安装程序来安装(安装时不妨将"DLX Linux Demo"选中,这样你可以参考它的配置文件)。在Linux中,不同的发行版(distribution)处理方法可能不同。比如,如果你用的是Debian GNU/Linux或其近亲(Ubuntu),可以使用这样的命令:

sudo apt-get install vgabios bochs bochs-x bximage

敲入这样一行命令,不一会儿就装好了。

很多Linux发行版都有自己的包管理机制,不如上面这行命令就使用了Debian的包管理命令,不过这样安装虽然省事,但有个缺点不得不说,就是默认安装的Bochs很可能是没有调式功能的,这显然不能满足我们的需要,所以最好的方法还是从源代码安装,源代码同样位于Bochs的官方网站,假设你下载的版本是2.3.5,那么安装过程差不多如下:

tar vxzf bochs-2.3.5.tar.gz

cd bochs-2.3.5

./configure –-enable-debuger –-enable-disasm

make

sudo make install

注意"./configure"之后的参数便是打开调式功能的开关。在安装过程中,如果遇到任何困难,不要惊慌,其官方网站有详细的安装说明。

1.3 Bochs的使用

上面有提到软盘,那么软盘究竟是什么?既然计算机都可以"虚拟",软盘当然也可以。在刚刚装好的Bochs组件中,就有一个工具叫做bximage,它不但可以生成虚拟软盘,还能生成虚拟硬盘,我们也称它们为磁盘映像。创建一个软盘映像的过程如图1.2所示:

图1.2,bximage,创建一个软盘映像

在上面只有一个地方没有使用默认值,就是被问到创建硬盘还是软盘映像的时候,就输入了"fd"。

完成这一步骤之后,当前目录下就多了一个a.img,这便是软盘映像了。所谓映像者可以理解为原始设备的逐字节复制,也就是说,软盘的第M个字节对应映像文件的第M个字节。

现在我们已经有了"计算机",也有了"软盘",是时候将引导扇区写进软盘了。可以使用dd命令:

dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc

注意这里多用了一个参数"conv=notrunc",如果不用它的话软盘映像文件a.img会被截断(truncated),因为boot.bin比a.img要小。

现在一切准备就绪,只差一个Bochs的配置文件。为什么要有配置文件呢?因为我们需要告诉Bochs,希望我们的虚拟机是什么样子的。比如,内存多大、硬盘映像和软盘映像都是哪些文件等内容。图1.3就是一个Linux下的典型配置文件例子。

图1.3,bochsrc示例

可以看到,这个配置文件本来就不长,除去注释之后内容更少了,而且很容易理解,字面上稍微不容易理解的只有romimage和vgaromimage,它们指定的文件对应的其实就是机器的BIOS和VGA BIOS,操作时需要确保它们的路径是正确的,不然过一会儿虚拟机启动时可能会被提示"couldn't open ROM image file"。除了之外还要注意floppya一项,它指定我们使用哪个文件作为软盘映像。现在一切准备就绪,是时候启动了,输入命令:

bochs –f bochsrc

一个回车,结果就如图1.1所示。

思路与代码

不借助任何外部代码,不借助《30天教你从头写XXX》类书籍,不借助现有OS的工程思路,不借助网络上博客的"讲解",进行如下工作:

  1. 阅读英特尔CPU产品说明书,掌握CPU工作方式、指令的详尽功能;

  2. 阅读BIOS产品说明书,全面掌握BIOS的功能,掌握使用要点与调用方法;

  3. 阅读GAS软件产品说明书,全面掌握GAS的汇编语法与工程使用;

  4. 阅读NASM软件产品说明书,全面掌握NASM的汇编语法与工程使用;

  5. 阅读各平台ABI说明书,选择性地大致掌握主流二进制文件的结构;

  6. 编写能够实现输出的HELLOWORLD程序。

  7. 编写能对简单可重入客户程序进行调度的调度器。

  8. 编写2级加载的内核加载器。

  9. 编写能实现位置无关运行的2级内核加载器。

  10. 编写2级加载的C语言内核,并实现调度、维护,提供简单的系统调用与服务。

  11. 编写2级加载的保护模式下的调度器。

如有雷同,绝非巧合。因为工程上的雷同大都因为框架的限制与规定。

2.1 研究思路

容易想出来的模式有两种:

1,FIFO模式(管道调度)

2,仲裁模式(事件调度)

我们采用明显更为适合的仲裁模式。

CPU上电基本环境为0x7c00开始运行,cs=0x0000 ip=0x7c00。内存结构如下图所示,共计1MB。A20地址线未打开,如果访存超1MB,则会被环回0地址。

2.2 研究过程与实现详解

实现打印与光标控制功能,直接覆盖到bios信息上面。首先展示我们研究过程中使用的编译方法,如下图所示。

因为该项目对编译的需求比较复杂,总结起来包括:

  1. 调用不同的汇编编译器处理S源代码

  2. 处理并抽象汇编器的一大堆参数,保存多种汇编方案

  3. 读写镜像文件

  4. 打包与保存,简单的版本管理

  5. 处理C语言的编译生成、编译器参数

  6. 处理链接器的参数

  7. 处理ABI格式并进行重构拼接

  8. 保存、显示功能提示笔记和帮助

第一个项目实现时,Makefile功能还很简单,只用了一小部分功能。在此实验之后的研究中,Makefile的功能逐渐被填充增加,以满足研发过程中出现过的乱七八糟的需求。

图2.1,Hello,World的运行结果(红色)

主要代码:

首先将地址翻译的预处理指针定位到7c00位置,设置代码段与数据段相同,便于索引数据。

VGABIOS将功能共享内存映射到显存上,主板BIOS再将显存映射到主存上,并根据VGA型号在内存中写入控制程序。这里利用的是10h号中断的2h偏移功能包装了一个DisplayString函数。

2.3 可重入程序调度器

这次编写的调度器尝试调度两个简单的打印程序,一个打印A,另一个打印B,两者交替运行。程序运行效果如图2.2所示。

图2.2,调度两个简单的打印程序

程序代码说明:

基于上一份代码主要的更改就是"中断劫持"。我们的思路是通过时钟中断唤醒调度程序。修改中断服务程序的返回地址,使得中断结束后,返回到一个我们指定的位置。

我们要劫持的中断号是1c,该中断是由08h中断触发的软中断,08h是由8259级联片触发的硬件中断,8259从晶体振荡钟获得时钟触发的电气脉冲。

鉴于每个中断向量占4字节,其中低2子节为IP,高2子节为CS,我们将1ch乘4获得要修改的内存首地址,将该处内容改写为我们的调度器地址即可完成劫持。

图2.3展示的是两个被调度的客户程序

图2.3,两个被调度的客户程序

之后,我们实现了调度器的逻辑,最初有两种考虑,分别为2状态调度与4状态调度,两种状态调度模式如下:

  1. 状态1:应该运行main1

  2. 状态2:应该运行main2

  3. 永远在1,2之间切换跳转。

四种状态调度模式如下:

  1. 状态1:应该启动main1

  2. 状态2:main1被中断,启动main2

  3. 状态3:main2被中断,还原main1

  4. 状态4:main1被中断,还原main2

  5. 稳定在3,4之间切换跳转。

我们最终选择了四种状态的调度逻辑,代码如下。

当中断触发,进入调度器后,首先保护父过程的栈底地址ebp,将上一次的状态从内存读取到ax中。之后将ax与0-3比较,实现C语言中switch的功能。由于INTEL芯片中断的行为如下:

1.查看int偏移值是否合法

2.检查栈够6byte

3.push(eflags[0:15]); 2bytes

4.关中断,清trap,清AC

5.push(CS) 2bytes

6.push(IP) 2bytes

故在状态的处理中,会将上次程序的flag、IP从栈底后16byte处读取出来,因为最初main被中断时,其地址保存在0x08中断的ebp+0 到 ebp+5的位置上,换算到第二次调用,就是6*2+size(ebp)=16。根据此地址,可以从此处取出上一次的状态,并写入想要的状态。

比较完成后,将新的状态号写入ax,跳转到结束保存位置"savestate",该位置的代码会将ax写回内存,退出中断,返回到指定位置。

同时,我们在这一阶段也实现了对CRT硬件的基本控制。首先是屏幕清除函数,代码如下。

它是用屏幕翻滚中断来实现的,当翻滚为0时,则清屏,光标位置不变。这里我们发现用双头方式定义默认参数构造函数非常方便,算是汇编编程领悟到的一个小trick。这个Scroll功能行为其实比较不完善,因为其会破坏eax与ebp、ebx寄存器,所以必须要预先保存。之后我们从内存位置取出行列值,发送命令让int10-06写入显存。

每次写入显存后,我们都要更新光标的位置,让它指向下一个字符块,如果到达行尾,则光标应该向下一行,然后回车;如果整个屏幕都已写满,则应该向上滚动屏幕,然后写在CRT最后一行,这样才能实现平时我们所熟悉的字符界面。函数cursor_deal如下。

我们的CRT是标准设备,大小为80x25。初始光标在(0,0)位置,每次写完成后,我们都要计算光标下一个可写的坐标,并保存该坐标到内存。必要时,会滚动屏幕。

下面的函数实现了打印单个字符到显示器的功能,通过修改参数,也可以实现打印多彩色字符。

这里我们也使用了双头函数来包装默认功能与详细功能的不同入口。之后我们编写了光标设置函数。该函数接收行列参数来放置光标。代码如下所示。

在实验的过程中,我们发现这个简单的调度器当陷入到我们自己编写的"系统调用"函数中时,如果被中断切出,则会因为已经使用的栈空间而造成内存泄漏。所以,我们在此确定了一个非抢断的实现方案,使得陷入API后进程不得被抢断。打印的服务函数如下。

该函数在进入时关中断,离开时开中断,避免被调度而泄漏内存。

2.4 2级位置无关的内核加载器

因为BIOS的规定与限制,首次读盘只能加载0柱面0磁头1扇区512byte的内容,这对于一个稍微复杂一点的启动程序来说已是远远不够的,所以实现从其他存储介质中读取代码复制到内存中运行是不可避免的。因此,我们计划将bootloader写入软驱的第一个扇区,再编写一个小程序放到硬盘中等待被bootloader加载。

我们曾想尝试直接从硬盘中读取第一扇区作为bootloader,读取之后的十个扇区作为mini-kernel,但是我们经历数次挫折后发现,由于bochs已知的BUG,从HD中读首扇区后再次读取时常会发生错误,而软盘则不会;另外考虑到我们应该实践练习一下对多种存储设备进行读写,所以最终选择了这种复杂的组合。

这次编写的代码中,bootloader的主干逻辑如下所示。

首先清空屏幕,读取mini-kernel到0x7e00位置,然后跳转到新加载的代码处运行。

一般来说,编译代码时必须制定一个代码起始的默认地址,这个数值是必须指定的,用来给所有的label、call、jmp或内存访问编址。对于bootsection,我们根据BIOS说明书已知其会被复制到0x7c00位置,所以我们可以显式用org指定。但是对于客户程序或者可能被升级的自写代码来说,编写者并不知道自己的代码可能被bootloader/kernel加载到什么位置上去,所以操作系统必须支持把任意编写的代码正确运行的功能,也就是PIC/PIE(Position Independent Code/ Position Independent Executable)。

我们根据阅读INTEL说明书地址翻译部分学习的知识,认为段地址+偏移值的机制很好的支持了这种特性,因为地址的翻译是基于eip计算的逻辑地址,也称偏移值。我们的设想是将代码加载到16B对齐的位置,改变CS值,使得地址翻译正确进行。这样,客户的程序只需要从0编址就可以了。

这里的机制比较复杂,有一些误区,比如PIE机制可能被人误解为程序中不出现绝对的内存寻址,比如gcc的编译选项fpie。实际上,任何程序都不能避免使用绝对值来表示地址,例如C语言的函数指针,必须是关于CS的偏移值。如果CS不改变的话,翻译一定会出错。所以即使在gcc中选了fpie,也无法保证这代码就是理论上地址无关的。地址无关代码的支持是编译器与内核加载器紧密联系才能实现的特性。我们没有阅读gcc或linux的工程代码,不过我们猜想他们用的PIE机制应该和我们探索的极为相似。

接下来我们将主体框架拆分讲解,首先是读取硬盘的代码。代码如下所示。

之后,我们把打印相关的代码统统移到mini-kernel里,这使得bootloader的大小减为原先的二分之一。Mini-kernel的主干代码如下所示。

该部分代码是依照段首0x00为准编译的,具有通用性,因为编译器生成的obj都是从0编址。代码在逻辑上首先关中断,然后不管之前的cs/ds如何。统一将代码段数据段扩展数据段都设置为代码段。然后调用mini-kernel里的清屏函数,如果位置无关工作正常的话,屏幕上的BIOS信息将被清除。紧接着调用光标设置与字符打印函数打印一个'C'字母,如果工作正常,就会在屏幕第一个字符块位置打印出该字符。该程序结果如图2.4。

图2.4,调用光标设置与字符打印函数打印一个'C'字母

可见我们设计的PIE机制工作正确。

2.5 2级加载的C语言内核

在这里我们的设计是让bootloader尽量小,装载完毕后直接跳转。而把中断处理、进程调度、系统服务等代码全都放进mini-kernel里面用C实现。这里的关键点有三个,分别是符号位置、代码格式、opcode格式 。

符号位置指的是我们如何定位终端服务函数,这样才能在bootloader中劫持中断。因为在之后的工作中我们会需要将内核改为保护模式下执行,所以必须在bootloader中设置中断。这时我们就需要详细知晓交叉编译常常遇到的ABI格式转换问题也就是代码格式,同时,原ABI中有很多没有用的结构,比如图2.5所示的ELF格式中,头部有巨大的标志结构,数据稀疏度达到了98%。而bin格式则无稀疏数据。

像上图这样的结构我们也应该按照ABI来剪裁,本次研究中使用的机器是darwin Mach-O 格式,该格式与linux系统采用的ELF(Executable and Linkable Format)格式类似,如图2.7所示。

通过阅读苹果的ABI说明书,我们编写了工具解析该格式,以达到定位首函数、定位中端服务函数并裁减二进制文件的目的。最后我们遇到了opcode问题,为了代码的清晰、结构的明确,我们需要链接多个文件,bin格式虽然紧凑但无法链接,所以不能使用bin格式,而在使用其他可链接格式的过程中,gcc与unix-cc编译C语言代码时虽然指定了相应的参数,生成了i386代码,但是却无法在机器上正确运行。我们通过反汇编发现,指令中立即数部分被延长了,如图2.8所示。

图2.8,反汇编的结果

因为intel指令集是变长指令,上述地址的翻译错误导致了后续代码空间顺序紊乱。我们阅读gcc与cc的说明书后,根据其声称的编写boot代码时所需设置再次进行试验,发现没有任何效果,代码依然不是i386-generic,而是i386-long模式。经过一系列的失败和探索,我们发现只能将编译过程手动分解为前端、汇编、链接、后处理,四步操作,并在其中采用大量的参数、配合自写脚本,才能使得cc\gas\ld输出正确的结果。我们最终探索得到的步骤可在Makefile中体现。简而言之抽取其中四条命令和部分参数,其步骤可大致理解为:

  1. nasm –f bin

  2. cc -m16 $(csrc) -S -O0 -fPIC –ffreestanding

  3. Asm(".code16 \n\t");

  4. as -arch i386 $(target).s -o $(target).o -O0

编译的过程输出如图2.9所示。

图2.9,编译过程

相比之前几次探究,这次bootloader的主要改变是放弃劫持1ch,转而劫持其父硬件中断08h,绑定到mini-kernel中的int08函数上。代码如下。

另一处不同,是对堆栈的处理,之前我们一直让堆栈处于BIOS执行完的默认状态,但是我们发现,如果在远跳转后依然保留堆栈为0000:ffff尺度的话,会因为字符串类操作与c语言传参操作占堆栈大而溢出到0x7e00以上的代码空间,所以我们在这里重新设置了对战的大小,代码如下。

下面将介绍说明C语言实现的mini-kernel。首先是文件头,如下所示。

这里定义了C语言一些默认的符号,定义了进程池的大小,TTY尺寸,与进程的状态标志。之后我们定义了进程描述符,结构如下所示。

进程描述符内主要存储了寄存器信息、id、进程状态、进程主函数与堆栈保留空间。Bar变量的作用是检测与防止堆栈读写问题导致溢出,使得系统能对该异常做出反应。

主要的示例过程与工具函数如下所示。

功能分别为:中断伺服、系统idle零号进程、客户进程、清屏调用、光标调用、字符串求长、非安全栈显示字符、安全显示字符、打印字符串、自旋睡眠、获得即时esp、开始调度API、加入任务、任务切换、非栈安全设置光标。除此外还有许多辅助函数,细节繁杂就不一一解释了,只挑出其中比较重要的任务切换相关代码说明。

主函数设置好段,清理堆栈,然后加入两个进程开始调度。如以上的代码。

以上代码是中断处理函数,首先将当前的通用寄存器都压栈,再将堆栈指针都压栈,然后压入cs、ip后调用保存上下文函数,上下文保存完毕后,清理堆栈保证无任何泄漏,再进入进程切换。下面首先展示上下文保护函数,如下。

该函数依据gcc说明书所描述的C语言标准传参过程,从右到左依次弹出,获得参数,保存到任务描述符中,并调用MOVSB指令保存当前任务从栈底到栈顶的所有信息到任务描述符中。

该函数执行完毕后,int08将会调用switch_proc函数。该函数上半部分形式如下面的代码。

首先switch_proc会选择一个程序来调度,在此处可以实现任意的调度算法或逻辑,本次实验中采取了便于解释2个进程关系的处理方式,即:只有idle进程时启动另外一个ready进程,idle进入paused状态,当两个进程都已启动时,进行公平时间片调度,被切换掉的进程设置为paused,新运行的进程设置为running。

紧接着是通用的处理过程,主要关注如何恢复现场。首先检测被还原的进程所占堆栈式否超过了128Btye上限,如果超出了就截断,否则继续。这是为了防止客户在编写程序时出现错误而无限制地毁坏堆栈,伤及代码;另一方面也是为了限制管理开销。同时,我们也可以看到,为了防止调度嵌套,我们只在控制转移之前向20h端口发送"看门狗"喂狗信号,允许再次产生中断,这个做法在后面一半代码中也有体现。

接下来展示的是switch_proc的后半部分代码。

这部分代码的作用是将保存在任务描述符中的寄存器、堆栈指针恢复到对应硬件,再将堆栈写回内存,最后恢复标志位寄存器、恢复cs、ip。

因为我们要恢复所有寄存器,而总是需要额外寄存器作为中转器,这就像玩华容道一样比较难搞,所以我们将要恢复的内容依次写到堆栈里,然后逐个弹出。注意,这个堆栈的位置不是随意指定的,否则客户程序栈内存恢复的过程中会破坏我们的寄存器临时栈,所以我们将这个位置显式地写在旧esp的后面。旧的esp存在esi中。

首先,我们利用嵌入式汇编从任务描述符中恢复ip、flags、edi、esi,然后恢复eax/ebx/ecx/edx,然后取出ebp。此时寄存器都已经在esp栈顶存放了,我们再依次将数据复制到旧的程序栈中。接着,将各个寄存器的值通过pop重新赋给寄存器,完成恢复。

在喂"看门狗"开中断后,我们将返回ip、cs、flag写到栈顶,利用正常的retl跳转到原程序运行的位置,至此完成调度。这种完全保存栈行为的切换方式能够调度任何程序,包括线性时无关或任意时相关程序(也可理解为可重入、不可重入程序)。此处有趣的一点是,int08函数进入switch_proc函数后是不会返回的,int08函数也不会返回,因为我们模拟了正常时间中断的行为后,伪造了一个正常函数调用的现场利用retl跳转到其他位置去了。

Idle程序打印字母"I",task程序打印字母"O",实现效果如图2.10所示。

图2.10,实现效果

至此成功进行了bootloader加载、minikernel加载、调度、保存、切换等一系列事件驱动调度器的功能。

总结

本次实验已经成功地,基于Bochs在两个不同模式下分别是8086的实模式和80386的保护模式下,实现了两个简单任务的调度器。在实现过程中有对比较底层的内核结构和使用进行详细的描述。因为本次实验对编译的需求比较复杂,所以我们除了实现汇编的编写还有编写了C语言,两个语言结合地使用就是为了读者更加容易理解。

参考

[X86 WIKIBOOK]https://en.wikibooks.org/wiki/X86_Assembly

[IntelAsm]http://www.logix.cz/michal/doc/i386

[NASM备忘lmu]http://cs.lmu.edu/~ray/notes/nasmtutorial/

[Nasm Manuall]http://www.nasm.us/doc/nasmdoc0.html

[NASM 数据定义]http://www.nasm.us/doc/nasmdoc3.html

[NASM]https://www.tutorialspoint.com/assembly_programming/assembly_system_calls.htm

[GAS]http://csiflabs.cs.ucdavis.edu/~ssdavis/50/att-syntax.htm

[Gas Doc]https://sourceware.org/binutils/docs-2.16/as/index.html

[macOS]http://orangejuiceliberationfront.com/intel-assembler-on-mac-os-x/

[INT table]https://en.wikibooks.org/wiki/X86_Assembly/Advanced_Interrupts

[Inline Assembly]http://www.ibm.com/developerworks/library/l-ia/index.html

[InlineAssemble][http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html]

[Code Optimization]http://www.agner.org/optimize/

[Apple ABI Function Call Guide] [link]

Aurora 极光城

讲技术,说人话

写一个简单的操作系统相关推荐

  1. 怎样写一个简单的操作系统?(原文标题:How to write a simple operating system) 分类: 翻译 2011-01-26 01:10 3175人阅读 评论(3) 收藏

    怎样写一个简单的操作系统?(原文标题:How to write a simple operating system) 分类: 翻译2011-01-26 01:10 3175人阅读 评论(3) 收藏 举 ...

  2. 怎样写一个简单的操作系统?

    翻译:magictong(童磊)2011年1月 版权:Mike Saunders和Mike OS的全体开发 2009年 原文地址:http://mikeos.berlios.de/write-your ...

  3. 用java做一个简单记事本_用记事本写一个简单的java程序

    用记事本写一个简单的java程序 第一步: 安装好jdk,并设置好环境变量. 桌面-计算机(右键)-属性-高级系统设置-环境变量-path-在变量值后加上:和jdk安装路径加上(路径即为C:\Prog ...

  4. 如何搭建python框架_从零开始:写一个简单的Python框架

    原标题:从零开始:写一个简单的Python框架 Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 你为什么想搭建一个Web框架?我想有下面几个原因: 有一个 ...

  5. ipad php mysql_如何用PHP/MySQL为 iOS App 写一个简单的web服务器(译) PART1

    原文:http://www.raywenderlich.com/2941/how-to-write-a-simple-phpmysql-web-service-for-an-ios-app 作为一个i ...

  6. python123程序设计题说句心里话_用c++写一个简单的计算器程序

    // 050305.cpp : 定义控制台应用程序的入口点. // // 050304.cpp : 定义控制台应用程序的入口点. // //四则运算 #include "stdafx.h&q ...

  7. 用java写一个简单的区块链(下)

    用java写一个简单的区块链(下) 2018年03月29日 21:44:35 java派大星 阅读数:725 标签: 区块链java 更多 个人分类: 区块链 版权声明:本文为博主原创文章,转载请标明 ...

  8. 怎样用java写一个简单的文件复制程序

    怎样用java写一个简单的文件复制程序 代码来源:https://jingyan.baidu.com/article/c35dbcb0d6f1398916fcbc07.html package Num ...

  9. 给 asp.net core 写一个简单的健康检查

    给 asp.net core 写一个简单的健康检查 Intro 健康检查可以帮助我们知道应用的当前状态是不是处于良好状态,现在无论是 docker 还是 k8s 还是现在大多数的服务注册发现大多都提供 ...

最新文章

  1. GARFIELD@04-09-2005
  2. 【论文解读】无需额外数据、Tricks、架构调整,CMU开源首个将ResNet50精度提升至80%+新方法...
  3. UDP客户端向服务器发送文件,基于UDP协议的客户端与服务器端的文件传送
  4. OpenCV学习笔记五-图像混合
  5. 退出页面删除cookie_Cookie 机制
  6. 在windows环境下ftp服务器的文件上传和下载
  7. 【CCF】201703-1分蛋糕
  8. redis数据类型list总结
  9. iPhone 13系列又有新配色:猛男必看!
  10. 【Flink】Flink Container exited with a non-zero exit code 143
  11. python制作查询工具发给别人使用_用Python制作天气查询软件
  12. 【JZOJ4743】【NOIP2016提高A组模拟9.2】积木
  13. 前端按字母搜索相关内容
  14. React 事件处理
  15. Chrome浏览器翻译无法使用和ide谷歌翻译插件【更新 TKK 失败,请检查网络连接】解决办法
  16. 怎么找主播卖货?最靠谱的5种直播带货方式
  17. PS新手教程:轻松掌握四种扁平化设计风格
  18. 同步和异步的区别和优缺点
  19. 闹市里的宁静一隅,乡村慢生活#观澜山水田园文化旅游园
  20. 地产2022价值启示录:房企必须闯过的“三重门”

热门文章

  1. 马云创造阿里巴巴的故事
  2. 机器学习算法(十三):word2vec
  3. word文档打不开,提示mso.dll错误
  4. 航空科普VR大型体验馆设备VR航天主题乐园星际飞碟vr游乐设备
  5. 在c++中使用easyx画一个实时走动的钟表(方法细节)
  6. java script 菜鸟教程_JS 基础知识之菜鸟教程(2016-09-30)
  7. 全国计算机a类高等学校,2018高考必看:高校A类学科数量及排名
  8. EXCEL 删除一列超过3小时的数据公式
  9. mysql数据库:Xtrabackup安装以及应用
  10. DRM架构介绍(一)