哈工大操作系统课程实验记录

0-课程准备

课程视频地址:
https://www.bilibili.com/video/BV1d4411v7u7

实验楼地址:
https://www.shiyanlou.com/courses/115

缓存下来的离线课程(更新中):
链接: https://pan.baidu.com/s/1OoVD3MLO2hBCRX3sMMXunA
密码: 6rlq

摘录下来的实验指导(更新中):
链接: https://pan.baidu.com/s/139wBQV4L73lzHlrZmUmdhQ
密码: kbaq

我的实验环境:Ubuntu18.04

关于如何在本机上搭建实验环境:https://github.com/DeathKing/hit-oslab

关于实验参考代码和实验报告:https://github.com/haohuaijin/hit-linux-0.11-lab
如果访问不到的话,在分享的百度网盘链接里有下载好的zip包,对着README用就好了

下面是我的实验记录,主要是一些避坑指南,如果哪里有写的不对的地方请指正,感激不尽(正文开始):

1-熟悉实验环境

整个实验环境搭建过程在hit-oslab-master.zip文件中;

在linux0.11进行make后生成的Image文件就是内核镜像文件;

run脚本运行Bochs启动linux0.11,记得在你的终端中输入c才能启动linux0.11

运行后 bochs 会自动在它的虚拟软驱 A 和虚拟硬盘上各挂载一个镜像文件,软驱上挂载的是linux0.11内核linux-0.11/Image(Bochs配置的从软驱启动),硬盘上挂载的是linux0.11的文件系统hdc-0.11.img

hdc-0.11.img 文件的格式是 Minix 文件系统的镜像,是linux0.11的虚拟文件系统,你在Ubuntu上可以挂载这个文件系统与linux0.11进行文件互传;

#挂载
sudo ./mount-hdc
#卸载
sudo umount hdc

注意 1:不要在 0.11 内核运行的时候 mount 镜像文件,否则可能会损坏文件系统。同理,也不要在已经 mount 的时候运行 0.11 内核。

注意 2:在关闭 Bochs 之前,需要先在 0.11 的命令行运行 “sync”,确保所有缓存数据都存盘后,再关闭 Bochs。

2-操作系统的引导

这里还没关os内核什么事,主要是在引导程序上做文章

首先在bootsect中显示个性化字符串,这个比较简单,照着给的示例敲了一遍:

SETUPLEN=2
SETUPSEG=0x07e0entry _start
_start:!寄存器置参数,bios系统调用获取光标位置mov ah,#0x03xor bh,bhint 0x10!字符串信息参数mov cx,#18mov bx,#0x0007mov bp,#msg1!显示mov ax,#0x07c0mov es,axmov ax,#0x1301int 0x10load_setup:mov dx,#0x0000mov cx,#0x0002mov bx,#0x0200!读磁盘的0磁道2扇区,是setup程序mov ax,#0x0200+SETUPLENint 0x13jnc ok_load_setupmov dx,#0x0000mov ax,#0x0000int 0x13jmp load_setup!暂时阻塞在这
ok_load_setup:jmpi    0,SETUPSEG!数据
msg1:.byte   13,10.ascii  "Sunix by syc".byte   13,10,13,10!引导扇区标志
.org 510
boot_flag:.word   0xAA55

编译和运行:

$ as86 -0 -a -o bootsect.o bootsect.s
$ ld86 -0 -s -o bootsect bootsect.o

如果出现读入错误,用vim重新打开文件,再保存,关闭,这是vscode编码导致的问题。

用dd工具生成镜像文件(跳过了minix文件格式的32B的文件头):

$ dd bs=1 if=bootsect of=Image skip=32

让bootsect从磁盘的0磁道2扇区读入setup(setup.s暂时照抄bootsect.s,把显示的字符串改一改就行):

load_setup:
! 设置驱动器和磁头(drive 0, head 0): 软盘 0 磁头mov dx,#0x0000
! 设置扇区号和磁道(sector 2, track 0): 0 磁头、0 磁道、2 扇区mov cx,#0x0002
! 设置读入的内存地址:BOOTSEG+address = 512,偏移512字节mov bx,#0x0200
! 设置读入的扇区个数(service 2, nr of sectors),
! SETUPLEN是读入的扇区个数,Linux 0.11 设置的是 4,
! 我们不需要那么多,我们设置为 2(因此还需要添加变量 SETUPLEN=2)mov ax,#0x0200+SETUPLEN
! 应用 0x13 号 BIOS 中断读入 2 个 setup.s扇区int 0x13
! 读入成功,跳转到 ok_load_setup: ok - continuejnc ok_load_setup
! 软驱、软盘有问题才会执行到这里。我们的镜像文件比它们可靠多了mov dx,#0x0000
! 否则复位软驱 reset the diskettemov ax,#0x0000int 0x13
! 重新循环,再次尝试读取jmp load_setup
ok_load_setup:
! 接下来要干什么?当然是跳到 setup 执行。
! 要注意:我们没有将 bootsect 移到 0x9000,因此跳转后的段地址应该是 0x7ce0
! 即我们要设置 SETUPSEG=0x07e0

现在需要将bootsect.s和setup.s联编,用linus给的tools/build.c工具进行make,用的时候把后面一部分内容注掉,参见实验指导;

获取硬件参数:

把光标位置,内存大小,磁盘容量等硬件参数写到0x9000处,后面配合显示程序进行显示。此处主要用一些int中断来获取硬件参数。

mov    ax,#INITSEG
! 设置 ds = 0x9000
mov    ds,ax
mov    ah,#0x03
! 读入光标位置
xor    bh,bh
! 调用 0x10 中断
int    0x10
! 将光标位置写入 0x90000.
mov    [0],dx! 读入内存大小位置
mov    ah,#0x88
int    0x15
mov    [2],ax! 从 0x41 处拷贝 16 个字节(磁盘参数表)
mov    ax,#0x0000
mov    ds,ax
lds    si,[4*0x41]
mov    ax,#INITSEG
mov    es,ax
mov    di,#0x0004
mov    cx,#0x10
! 重复16次
rep
movsb

最后将得到的硬件参数信息进行打印显示,自己要写一些显示程序,挺恶心的,直接复制粘贴源码了,不赘述。

所以,总结来说,这一节的工作就是:

  1. bootsect进行信息输出;
  2. 将setup从0磁道2扇区读入;
  3. setup获取硬件参数;
  4. 将得到的硬件参数显示出来;

说起来也不复杂,但是汇编嘛,你懂得,粒度太细,一点点写不对就完蛋,还不能调试,也挺难的,希望后面C代码能舒服一些。

3-系统调用

这一节主要是通过实现自定义系统调用深刻理解systemcall是怎么实现的,至少能理解个大概。
此次实验的基本内容是:在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。

(1)iam()

第一个系统调用是 iam(),其原型为:

int iam(const char * name);

完成的功能是将字符串参数 name 的内容拷贝到内核中保存下来。要求 name 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL。

(2)whoami()

第二个系统调用是 whoami(),其原型为:

int whoami(char* name, unsigned int size);

它将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name 越界访存(name 的大小由 size 说明)。返回值是拷贝的字符数。如果 size 小于需要的空间,则返回“-1”,并置 errno 为 EINVAL。

(4)测试:

运行添加过新系统调用的 Linux 0.11,在其环境下编写两个测试程序 iam.c 和 whoami.c。最终的运行结果是:

$ ./iam lizhijun$ ./whoamilizhijun

可见,我们的主要工作就是在内核区和用户区借助指针和数组空间相互传递字符串。另外,上面的函数prototype只是一个示意,并没有真正实现它。
先看系统调用过程:
操作系统实现系统调用的基本过程(在 MOOC 课程中已经给出了详细的讲解)是:

  • 应用程序调用库函数(API);
  • API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
  • 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
  • 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
  • 中断处理函数返回到 API 中;
  • API 将 EAX 返回给应用程序。

关键点:系统调用号,中断,内核函数(地址:函数指针),中断返回
在unistd.h中定义了几个宏函数:_syscall0, _syscall1, _syscall2…他们实现是所有系统调用的关键,以close为例:
close的原型定义在lib/close.c中:

#define __LIBRARY__
#include <unistd.h>
_syscall1(int, close, int, fd)

其中 _syscall1 是一个宏,在 include/unistd.h 中定义(里面还定义了0个,2个,3个参数的宏函数)

#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \: "=a" (__res) \: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \return (type) __res; \
errno = -__res; \
return -1; \
}

_syscall1(int,close,int,fd) 进行宏展开,可以得到:

int close(int fd)
{long __res;__asm__ volatile ("int $0x80": "=a" (__res): "0" (__NR_close),"b" ((long)(fd)));if (__res >= 0)return (int) __res;errno = -__res;return -1;
}

可见,close就是只有一个参数的系统调用_syscall1(type,name,atype,name)的宏展开,对照上面的宏展开结果的内容,close的过程就是将__NR_close参数压入eax,这是close的系统调用号的宏,unistd.h中定义了所有的系统调用号的宏;fd是文件描述符,是sys_close()的要用参数,压入ebx寄存器;然后int0x80中断进入内核;最终的返回结果存入eax寄存器,然后再从eax取到_res,视其情况返回并置全局变量errno。

ok,现在我们要着手改动内核了哟,兴不兴奋?
参考close的实现,对于我们要添加的系统调用,也需要在unistd.h中添加对应的宏,宏值顺延,别瞎搞:

注意上面的改动实在linux0.11源码中改的,这是编译内核用的代码。但是启动linux0.11后,你需要将bochs虚拟机里面的unistd.h也做相同改动,因为在bochs虚拟机里编程用的是他自己的unistd.h,就像你再你本机上编程要#include <unistd.h>一样。

调用中断后,我们要着手编写中断处理程序(内核代码),那么首先我们要清楚怎么跳到中断处理函数:
系统初始化后,对于80号中断:

set_system_gate(0x80,&system_call);`

set_system_gate 是个宏,在 include/asm/system.h 中定义为:

#define set_system_gate(n,addr) \_set_gate(&idt[n],15,3,addr)

_set_gate 的定义是:

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \"movw %0,%%dx\n\t" \"movl %%eax,%1\n\t" \"movl %%edx,%2" \: \: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \"o" (*((char *) (gate_addr))), \"o" (*(4+(char *) (gate_addr))), \"d" ((char *) (addr)),"a" (0x00080000))

虽然看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call
system_call的源码是汇编程序,我们需要把nr_system_calls的值修改为74,因为我们增加了两个系统调用:

其余的代码忽略,剩下里面最核心的一句就是:

    call sys_call_table(,%eax,4)

根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号,即 __NR_xxxxxx,因为函数指针大小为32bit,所以将他乘以4

显然,sys_call_table 一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中:

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...

所以我们在这个函数指针数组后面加上两个函数指针,同时在前面添加上函数声明:

凡是以sys打头的都是内核函数,然后我们去kernel/who.c中实现这两个函数就好了,如果你会写的话现在就可以着手编写这两个函数了。

为了编译,我们还需要修改kernel/makefile

在目标中添加who.o,在依赖中添加who的依赖规则。
调试程序的话可以用printk函数,就是运行在kernel空间的printf函数。

下面贴出sys_iam和sys_whoami的实现源码,内含剧透,需要请回避:

#include <asm/segment.h>
#include <errno.h>
#include <string.h>
char myname[24];
int sys_iam(const char *name){char str[25];int i = 0;do{ // get char from user inputstr[i] = get_fs_byte(name + i);} while (i <= 25 && str[i++] != '\0');if (i > 24){return -(EINVAL);}strcpy(myname, str);return i-1;
}int sys_whoami(char *name, unsigned int size){int length = strlen(myname);if (size < length){return -(EINVAL);}int i = 0;for (i = 0; i < length; i++){// copy from kernel mode to user modeput_fs_byte(myname[i], name + i);}return length;
}

上面的代码是写在kernel/who.c中的内核代码,里面的get_fs_byte和put_fs_byte是linus给的用于内核区和用户区数据拷贝的函数。
编译,启动bochs虚拟机,在虚拟机中编写如下测试程序:

#define __LIBRARY__
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
//iam()在用户空间的接口函数
// 这是定义在unistd.h里面的一个宏,展开后是一个包含int 0x80中断的代码。
_syscall1(int, iam, const char*, name);
int main(int argc, char* argv[]) {iam(argv[1]);return 0;
}

在虚拟机中编译,运行就行了,注意宏定义写在第一行
另外还有一个whoami的测试程序,和其他测试程序和脚本,不赘述。
最后就能实现实验要求的效果了。

回顾一下我们对内核做了什么改动:
1-我们在kernel/who.c中实现了两个内核函数,他们能将用户区和内核区的指定字符串相互传递。
2-而这两个内核函数暴露给用户态的接口是一(两)个宏函数_syscall1(int, iam, const char*, name);,借助它,我们得以传递参数,引发中断,进入内核。
3-进入内核后,中断服务子程序的入口地址借助我们定义的系统调用号(由eax寄存器传递)在函数指针的数组fn_ptr sys_call_table[]中找出,然后就去执行内核函数代码了。
4-从内核返回后,由eax传递返回值给_res,然后中断返回。

这大概就是实验3的全部内容了,这其中还涉及一些理论支撑:比如内存的保护方法(CPL和DPL的关系)。

4-进程运行轨迹的跟踪与统计

怎么说呢,这一节没有上一节来的刺激,本节的核心任务是在linux0.11涉及进程调度的内核程序中找到所有发生进程状态切换的代码点,并在这些点插入函数fprintk()(内核的fprintf()函数),来输出进程状态变化的情况到 log 文件中,最后用给定的py程序分析进程的运行轨迹,进而能得到进程的周转时间,等待时间,CPU时间,IO时间等信息。

《注释》那本书我没读多少,所以对内核代码中进程切换点的把握还不是很好,不过跟着答案做一遍还是有一些收获的。

首先需要编写一个多进程并发的程序,会用fork()就没啥问题:

基于模板 process.c 编写多进程的样本程序,实现如下功能:所有子进程都并行运行,每个子进程的实际运行时间一般不超过 30 秒;父进程向标准输出打印所有子进程的 id,并在所有子进程都退出后才退出;

主要的源码如下:

void cpuio_bound(int last, int cpu_time, int io_time);
#define CHILD_PROCESS_NUM 4int main(void) {pid_t pid;int i = 0;while (i < CHILD_PROCESS_NUM)    {if ((pid = fork()) < 0)        {fprintf(stderr, "Error in fork()\n");return -1;}else if (pid == 0)  {/* 子进程执行指定时间后退出 */cpuio_bound(CHILD_RUN_TIME, 1, 1);exit(0);} else    {fprintf(stdout, "Process %lu created.\n", (long)(pid));++i;}}/* 父进程中一直要等待所有的子进程退出 */while ((pid = wait(NULL)) != -1) {fprintf(stdout, "Process %lu terminated.\n", (long)(pid));}  return 0;
}

其中cpuio_bound函数是已经实现的一个可以调节CPU时间和IO时间比例的函数。
在main中循环创建4个子进程,执行指定时间后退出,父进程打印提示信息,并阻塞等待所有子进程都被wait回收后退出。代码参考:https://github.com/iLoveTangY/hit-oslab

把上面的那个程序在linux0.11下编译,执行,应该能正常执行,当然在本机上应该是能跑通的。

接下来我们需要记录linux0.11的进程切换情况:

  • Linux0.11 上实现进程运行轨迹的跟踪。基本任务是在内核中维护一个日志文件 /var/process.log,把从操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一 log 文件中。
  • 在修改过的 0.11 上运行样本程序,通过分析 log 文件,统计该程序建立的所有进程的等待时间、完成时间(周转时间)和运行时间,然后计算平均等待时间,平均完成时间和吞吐量。可以自己编写统计程序,也可以使用python 脚本程序—— stat_log.py(在 /home/teacher/ 目录下) ——进行统计。

/var/process.log` 文件的格式必须为:

pid    X    time

其中:

  • pid 是进程的 ID;
  • X 可以是 N、J、R、W 和 E 中的任意一个,分别表示进程新建(N)、进入就绪态(J)、进入运行态®、进入阻塞态(W) 和退出(E);
  • time 表示 X 发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick)

linux0.11机器上的8253定时器每隔10ms产生一次中断,产生一次系统滴答,在kernel/schhed.c中定义:long volatile jiffies=0;,所以上面的第三列time就是全局变量jiffies的值。

首先我们让系统刚一开始就打开var/process.log文件,并将其dup到文件描述符3,这样我们每次向文件描述符3中fprintk格式化字符串就可以了:

操作系统启动后先要打开 /var/process.log,然后在每个进程发生状态切换的时候向 log 文件内写入一条记录,其过程和用户态的应用程序没什么两样。然而,因为内核状态的存在,使过程中的很多细节变得完全不一样。

为了能尽早开始记录,应当在内核启动时就打开 log 文件。内核的入口是 init/main.c 中的 main()(Windows 环境下是 start()),其中一段代码是:

//……
move_to_user_mode();
if (!fork()) {        /* we count on this going ok */init();
}
//……

这段代码在进程 0 中运行,先切换到用户模式,然后全系统第一次调用 fork() 建立进程 1。进程 1 调用 init()

在 init()中:

// ……
//加载文件系统
setup((void *) &drive_info);// 打开/dev/tty0,建立文件描述符0和/dev/tty0的关联
(void) open("/dev/tty0",O_RDWR,0);// 让文件描述符1也和/dev/tty0关联
(void) dup(0);// 让文件描述符2也和/dev/tty0关联
(void) dup(0);// ……

这段代码建立了文件描述符 0、1 和 2,它们分别就是 stdin、stdout 和 stderr。这三者的值是系统标准,不可改变。

可以把 log 文件的描述符关联到 3。文件系统初始化,描述符 0、1 和 2 关联之后,才能打开 log 文件,开始记录进程的运行轨迹。

为了能尽早访问 log 文件,我们要让上述工作在进程 0 中就完成。所以把这一段代码从 init() 移动到 main() 中,放在 move_to_user_mode() 之后(不能再靠前了),同时加上打开 log 文件的代码。

修改后的 main() 如下:

这样,文件描述符 0、1、2 和 3 就在进程 0 中建立了。根据 fork() 的原理,进程 1 会继承这些文件描述符,所以 init() 中就不必再 open() 它们。此后所有新建的进程都是进程 1 的子孙,也会继承它们。但实际上,init() 的后续代码和 /bin/sh 都会重新初始化它们。所以只有进程 0 和进程 1 的文件描述符肯定关联着 log 文件,这一点在接下来的写 log 中很重要。

为了能在内核态使用fprintf,实验指导提供了实现好了fprintk函数,具体参见实验指导。

然后就是实验的核心内容了(好多地方理解的还很模糊,有时间研读源码吧):
必须找到所有发生进程状态切换的代码点,并在这些点添加fprintk函数,来输出进程状态变化的情况到 log 文件中。
此处要面对的情况比较复杂,需要对 kernel 下的 fork.csched.c 有通盘的了解,而 exit.c 也会涉及到。

首先是在fork.ccopy_process函数中:

这里进程被创建好后,发生了进程状态切换:新建态和就绪态。

接着是在sched.c中,见名知义,这是内核中有关任务调度函数的程序,所以这里面进程状态切换一定少不了:
(1)切换到就绪态:

(2)需要判别是否真的发生了任务的切换:

否则可能还在运行原来的进程;

(3)当进程主动sleep_on阻塞自己时,当前进程状态为W,然后选择一个就绪进程:

(4)当进程被唤醒时,回到就绪态:

最后exit.c中就是当进程退出时状态切换:
(1)当进程退出时,当然是E:

(2)以及最后,这个我熟,父进程调用waitpid后自己阻塞,进入W态:

上面就是需要在内核中插入的代码,涉及到文件有 init/main.ckernel/fork.ckernel/sched.c。然后重新make内核,在linux0.11中编译执行process,然后把生成的process.log拷贝到本机,用py程序分析即可,后面的修改时间片没有做,具体可以参考实验指导。

哈工大操作系统课程实验记录相关推荐

  1. 课程linux实验报告,Linux操作系统课程实验报告.doc

    Linux操作系统课程实验报告.doc Linux操作系统课程实验报告班级姓名学号指导老师田丽华完成时间2014年7月目录一.实验目的1二.实验要求1三.实验内容1[第一题]1[第二题]2[第三题]4 ...

  2. linux课程实验报告,Linux操作系统课程实验报告

    Linux操作系统课程实验报告 Linux操作系统 课程实验报告 班级: 姓名: 学号: 指导老师:田丽华 完成时间:2014年7月 目录 一.实验目的1 二.实验要求1 三.实验内容1 [第一题]1 ...

  3. 操作系统课程实验代码汇总

    操作系统课程实验代码汇总 本次内容供需要有相关实验需要的提供参考,代码下载方式在文末 文章目录 操作系统课程实验代码汇总 前言 一.进程管理 实验目的 代码 说明 二.进程调度 实验目的 说明 三.银 ...

  4. 电子科技大学---操作系统课程实验(一)

     电子科技大学-操作系统课程实验(一) 系统化思维模式下计算机操作系统进程与资源管理设计 1.实验目的: 设计和实现进程与资源管理,并完成Test shell的编写,以建立系统的进程管理.调度.资源 ...

  5. 哈工大编译系统课程实验一词法分析报告

    编译系统课程实验报告 实验1:词法分析 一.需求分析 得分 要求:阐述词法分析系统所要完成的功能 1.巩固对词法分析的基本功能和原理的认识. 2.能够应用自动机的知识进行词法分析. 3. 理解并处理词 ...

  6. 山东大学linux应用实验五,【Linux】山东大学Linux应用课程实验记录

    找到这篇博文的人,一定被Linux实验弄得很爆炸吧哈哈哈. 这里是我Linux实验的记录,供大家学习和参考.如有错误,还请指正. 实验一 一. 基本命令 显示系统当前时间. date 显示2003年的 ...

  7. 清华大学操作系统课程实验

    课程 链接: https://www.bilibili.com/video/av94122925/?spm_id_from=333.788.videocard.0

  8. eNSP动态NAT实验记录

    将内部网络10.1.1.0/24转换为公网地址200.1.1.1-200.1.1.10/28上网(访问Server3),并抓包分析 验证动态NAT是单向转换 搭建实验环境 实现此案例需要按照如下步骤进 ...

  9. 操作系统期末实验:多用户二级文件系统

    多用户二级文件系统 写在最前面 问题描述 要 求: 1 功能设计 1.1 系统层次结构 1.2初始化 1.2 子功能设计 2 源程序 2.1 系统实现主要的软件技术 2.2 数据结构 2.3 后端 2 ...

最新文章

  1. 数据库事务的隔离级别
  2. 每天一道LeetCode-----逆序链表
  3. Angular library 学习笔记
  4. Class的getName、getSimpleName与getCanonicalName的区别
  5. STM32F0使用LL库实现SHT70通讯
  6. URL Routing
  7. 掘金企服:ICP经营许可证和ICP备案的区别
  8. 阿里云服务器端口请求失败(在控制台把端口添加到服务器的安全组)
  9. ipa包瘦身之图片无损压缩瘦身
  10. (保姆级)国内1块钱注册火爆全网的OpenAI-ChatGPT机器人
  11. python+websocket匿名聊天室实现
  12. GPT4论文翻译 by GPT4 and Human
  13. Aqara网关、yeelight智能灯、智能窗帘电机如何实现场景化互联?
  14. python处理wps表格数据匹配_两个excel表格数据匹配wps-WPS怎样用VLOOKUP引用另一个表格的数据...
  15. 微信公众号用秀米网插入视频
  16. 渲染多层材料的综合框架
  17. Barbara Liskov:CLU与Argus语言发明人
  18. 想要秒变“优牙人”,只需要uya.ren
  19. 关于舵机的漂移与不听指挥乱动的问题
  20. Java 微信开发(四)生成带参数二维码及分享到朋友圈、好友、QQ

热门文章

  1. 计科1111-1114班第三周讲义、课外作业(截止日期:2014年3月27日23点-周四晚,学委飞信通知同学)
  2. 【BZOJ3165】Segment(李超线段树)
  3. Excel合并单元格如何填充序号
  4. 【解决方案】Oracle插入/更新CLOB字段报ORA-01704:字符串文字太长
  5. ncr管理系统_项目管理信息平台
  6. 渠道、裂变、留存,App获客增长转化方案
  7. JavaC#实现账号登录、账号注册、修改密码、账号注销等功能
  8. nodeJS的环境搭建以及nodeJS和npm简介
  9. 狼人杀超详入门攻略2之狼人战术
  10. dv路由算法c语言实现,路由协议之DV算法