问题1:为什么需要有系统调用?
系统分为用户空间和内核空间,内核空间存有重要的数据(比如用户密码)和重要的代码(调度程序,文件管理系统什么的)。用户空间不能随意访问内核空间,否则会有极大的安全隐患(比如得到了内核空间的用户密码,随意下载软件,可能会下载恶意软件,比如更改调度程序,让程序一直运行)

问题2:如何分隔用户态和内核态?
1)用户态无法访问内核态数据,而内核态可以访问所有数据。
2)指令的最后两位标识特权,3为用户态,0为内核态。
3)检查当前指令和目标指令的特权

系统调用的具体过程
1)用户调用系统接口API
(我的理解API是连接用户程序和内核的桥梁,用户程序可使用内核中的函数接口,但是得借助API将数据传入内核中运行)

【系统调用和普通的函数调用没有区别,都是直接调用然后实现一定的功能,
自定义函数是直接通过call指令跳转到该函数的地址上去运行,
但是系统调用则是调用为该系统编写的一个接口函数,也就是API(application programming interface)
而API不能直接的完成系统调用的功能,它只是提供了一个接口去调用真正的系统调用。
————————————————
版权声明:本文为CSDN博主「东瓜lqd」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41708792/article/details/89604209】

2)API将系统调用号存入EAX(寄存器),其他传入的参数依次存入寄存器EBX、ECX、、、以此类推。
(系统调用号是用来表明调用的具体是内核中的哪一个函数,但是我不明白的是:如果有多个进程同时调用同一个函数,那如何区分是哪个进程在调用该函数,像c++的类一样做区分吗?
【函数本身只是代码,代码是只读的,无论多少个线程同时调用都无所谓,因为是只读嘛.但是函数里面总要用到暑假 ,如果数据属性线程级别(比如函数形参–>局部变量–>存在栈上–>每个线程都有自己的栈),那么同时调用是没关系的,因为用的都是本线程的数据;但是如果函数用到一些全局数据,比如全局变量,根据堆内存首地址去访问的堆内存(形参传入的),同时操作一个数据结构(如对一个链表有什么操作),静态局部变量,那就不行了,必须要加锁!!】上网找的资料不知道是否靠谱。)

4)中断处理程序根据对应的系统调用号,调用对应的系统函数(处理中断)
5)对应的系统调用函数调用完后,将返回值存入到EAX中,返回到中断处理函数
6)中断函数返回到API中
7)API将EAX返回给应用程序
(6、7两点没有看懂)

一、应用程序如何调用系统调用?
自定义函数是通过调用call直接跳转到自定义函数的位置开始执行;
系统调用不能直接跳转到内核函数的位置开始执行,而是调用真正的内核函数,它是联通用户程序和系统函数的一道桥梁。
具体的过程为
1)将系统调用号存入EAX。系统调用号是指具体调用了什么系统函数。
2)将其他传递的参数传入其他通用寄存器。
3)触发int 0x80中断
(我的理解是根据中断的类型查找中断向量表,找到对应的解决的方法)


_syscallN(type, name,atype,a,btype,b,…)
N表示要调用的函数中有N个参数。
【因为在unsitd.h已经声明了四种参数传递的方式,在32位处理器上有四个寄存器(eax,ebx,ecx,edx),
eax用于传递中断调用号和返回值,使用ebx,ecx,edx三个寄存器传递参数,所以最多只可以三个参数,
如果要增加参数需要采用栈来传递参数,寄存器只需要获取栈的地址和参数的长度。
————————————————
版权声明:本文为CSDN博主「东瓜lqd」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41708792/article/details/89604209】

以__syscall1(int,close,int ,fd)为例,这是用户程序通过API调用内核中的close()函数

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

其中_NR_##name为系统调用号,指明调用系统中的哪个函数,并将其存放到寄存器eax中,其他的参数依次存放到通用寄存器当中。

“int 0x80”:”=a”(__res):””(__NR_##name),
”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))
//((long)(a))为函数传入的参数,“b”为存储参数的寄存器

例如将close()函数用_syscall1(int,close,int,fb)进行宏展开

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;
}

这就是 API 的定义。它先将宏 __NR_close 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值。

因此在本实验中,需要在unistd.h中为系统编写的函数加上系统调用号。

/*
而在应用程序中,要有:
*//* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);

Linux的内核中实现了一些API,因为在内核加载完成后,会跳转到用户态中做一些初始化的工作,用户态需要一些系统调用,因此在内核中实现了一部分的API。

二、通过int 0x80进入内核态
以下内容由计算机在开机启动的时候,初始化设置IDT表的表项


sched_initi()在init/main.c函数中调用,用于在内核加载完成后的初始化。
【其中15表示此中断号对应的是陷阱门,注意,这个中断向量不是中断门描述符。比如硬盘中断(hd_interrupt)或定时器中断(timer_interrupt)等硬件类的中断才设置为中断门描述符。陷阱门是可被中断的。】
上图中的n为0x80,是在中断向量表基址中查找相应的中断处理程序?
其他的参数设置:
type = 15,dpl = 3,addr = &system_call;
用段选择符和便宜地址设置成新的CS:IP

其他的补充:

void main(void)
{//    ……time_init();sched_init();buffer_init(buffer_memory_end);
//    ……
}

sched_init() 在 kernel/sched.c 中定义为:

void sched_init(void)
{//    ……set_system_gate(0x80,&system_call);
}

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

  设置系统陷阱门函数。// 上面 set_trap_gate()设置的描述符的特权级为 0,而这里是 3。因此 set_system_gate()设置的// 中断处理过程能够被所有程序执行。例如单步调试、溢出出错和边界超出出错处理。// 参数:n - 中断号;addr - 中断程序偏移地址。(发生该中断的时候需要调用的函数)// &idt[n]是中断描述符表中中断号 n 对应项的偏移值;中断描述符的类型是 15,特权级是 3。
#define set_system_gate(n,addr) \_set_gate(&idt[n],15,3,addr)

_set_gate 的定义是:

 设置门描述符宏。// 根据参数中的中断或异常处理过程地址 addr、门描述符类型 type 和特权级信息 dpl,设置位于// 地址 gate_addr 处的门描述符。(注意:下面“偏移”值是相对于内核代码或数据段来说的)。// 参数:gate_addr -描述符地址;type -描述符类型域值;dpl -描述符特权级;addr -偏移地址。// %0 - (由 dpl,type 组合成的类型标志字);%1 - (描述符低 4 字节地址);// %2 - (描述符高 4 字节地址);%3 - edx(程序偏移地址 addr);%4 - eax(高字中含有段选择符 0x8)。
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
// 将偏移地址低字与选择符组合成描述符低 4 字节(eax)。"movw %0,%%dx\n\t" \// 将类型标志字与偏移高字组合成描述符高 4 字节(edx)。"movl %%eax,%1\n\t" \// 分别设置门描述符的低 4 字节和高 4 字节。"movl %%edx,%2" \: \: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \"o" (*((char *) (gate_addr))), \"o" (*(4+(char *) (gate_addr))), \"d" ((char *) (addr)),"a" (0x00080000))//"a"内放置段选择符

其作用为填写IDT表格(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call。
【汇编注释:
1、mov :为寄存器移动指令,例如movw dx,ax 即为dx-〉ax,mov为移动指令。“w”为长度的指定w=word=16位=2个字节;相应的“l”=long=32位=4字节。
2、% :AT&T汇编在引用寄存器时要在前面加1个%,%%是因为GCC在编译时会将%视为特殊字符,拥有特殊意义,%%仅仅是为了汇编的%不被GCC全部转译掉
3、ax 与 eax :ax与eax之间是有联系的,他们并不是孤立的,eax为32位寄存器,ax为16位寄存器,而ax就是eax的低16位。dx与edx具有相同的关系。
4、%0、%1、%2、%3:0、1、2、3可以看作变量,这些变量在程序的":“之后,程序的两个”:",是定义输入、输出项的。针对这段程序这些变量的前面都加了明确的限定,例如"i"(输入项)、“o”(输出项),剩下的"d"(edx的初始值),“a”(eax的初始值)。而0、1、2、3的概念就是指第几个变量,这里输入项、输出向、寄存器初始混合编号;相应的0(“i”((short)(0x8000+(dpl<<13)+(type<<8)))));1((*((char )(gate_addr))));2(((4+(char *)(gate_addr))));3(“d”((char )(addr)));4(“a”(0x00080000))
5、<<:这是个运算符,如果大家觉得<<不好理解可以用乘2的次方来实现相同的效果,例如14<<13=14
2的13次方
6、\n\t:这是嵌入式汇编一种书写格式】

三、实现系统调用
每个系统调用都有一个 sys_xxxxxx() 与之对应,他们都是实实在在的做他们应该做的事情

四、具体实现

1、在kernel文件夹中新建一个who.c文件,里面写入iam和whoami函数的系统调用sys_iam和sys_whoami。
who.c :

#define __LIBRARY__
#include <unistd.h>
//提供对 POSIX 操作系统 API 的访问功能,在Linux网络编程中往往会用到
#include <errno.h>
//定义了错误码来返回错误信息的宏
//#define EINVAL 22 /* Invalid argument */
//Invalid argument of a function call as defined by POSIX.
#include <asm/segment.h>
//<asm/segment.h>:段操作头文件,定义了有关段寄存器操作的嵌入式汇编函数。
char kname[24];
//存入到内核中的字符串的长度为23,最后一个放置'\0'int sys_iam(const char* name)
//将用户名称从用户空间拷贝到内存空间
{int len = 0;while (get_fs_byte(name + len) != '\0')len++;
//功能:从用户空间addr地址处取出一个字节char
//参数:addr用户空间中的逻辑地址
//返回:fs:[addr]处的一个字节内容
//len为用户地址字符串的偏移量,而name为用户字符串的起始地址,只要没有遇到空字符串就自行向后偏移if (len > 23){errno = EINVAL;//errno 用来保存最后的错误代码,它是一个宏,被展开后是一个 int 类型的数据(在单线程程序中可以认为是一个全局变量),并且当前程序拥有读取和写入的权限。//<errno.h> 头文件中有一个 errno 宏,它就用来存储错误代码,当系统调用或者函数调用发生错误时,就会将错误代码写入到 errno 中,再次读取 errno 就可以知道发生了什么错误。//errno 被设置为 0;程序在运行过程中,任何一个函数发生错误都有可能修改 errno 的值,让其变为一个非零值,用以告知用户发生了特定类型的错误。return -1;}//printk("%d\n", i);int j;for (j = 0; j < len; j++)kname[j] = get_fs_byte(name + j);//从用户的地址逐个读取数据,然后存放在内存当中kname[j] = '\0';return len;//返回读取到的字符串的长度
}
//感受到了:平时写的C语言程序是在用户空间之内运行的,int sys_whoami(char* name, unsigned int size)
//将sys_iam()所数据从内核空间拷贝到用户空间
//并保证拷贝的字符串的大小不超过size
{int len = 0;while (kname[len] != '\0')len++;//统计字符串的长度if (len > size){errno = EINVAL;return -1;//如果字符串的长度超出了指定的范围,就将其改为-1}int i;for (i = 0; i < len; i++)put_fs_byte(kname[i], name + i);//name+1为起始地址+偏移地址put_fs_byte('\0', name + i); //注意这里一定要使用put_fs_byte函数来存入数据,尽管内核可以直接访问用户数据,但也要使用API,否则会报错//记得在字符串的最后加上空字符return len;//返回数据的长度
}

为什么要加LIBRARY呢?

这张图片解释了加上__LIBRARY__的原因

2、修改一系列头文件
1)修改/oslab/linux-0.11/kernel的Mikefile
第一处:画线的部分为增加的部分

第二处:画线的部分为增加的部分

注意修改完Makefile后要返回到/linux-0.11/中进行编译

cd ../../../
make all

2)修改/oslab/linux-0.11/include/中的unistd.h
画线的部分为添加的部分,加上自己写的函数的系统调用号

3)修改/oslab/linux-0.11/include/linux中的sys.h
第一处:模仿其他的系统调用,加上函数的声明。
第二处:sys_call_table为函数指针数组的起始地址,需要在这个数组中加上sys_iam和sys_whoami的引用,在数组的中的位置必须和在系统调用号列表中的位置一致。

【call sys_call_table(,%eax,4)这一句话,根据汇编寻址方式来解释这句话:
=> call sys_call_table + 4 * %\eax (eax中存放的是系统调用号,系统调用号也就是那些_nr_xxx)
这里就是通过 一个函数指针数组的起始地址 + 4 * %eax
表示从函数起始地址开始跳过%eax个项,而且每个项是4个字节,然后对应到一个函数入口,
所以也就是跳转到从sys_call_table中的第%eax个函数开始执行,
也就是开始执行那个系统调用。
————————————————
版权声明:本文为CSDN博主「东瓜lqd」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41708792/article/details/89604209】

4)修改/oslab/linux-0.11/kernel中的system_call.s
将nr_system_calls = 72改成74,因为新增了两个系统调用号

3、然后编写iam.c和whoami.c放在hdc/usr/root下的任何位置
iam.c
1)首先挂载硬盘,并进入到hdc/usr/root/当中

sudo mount-hdc
cd /hdc/usr/root/

2)编写程序
iam.c

#define __LIBRARY__
//这句话是什么含义?
//猜测是一个ifndef,然后就不做其他的事情的东西,所以要定义
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>_syscall1(int, iam, const char*, name)
//是一个宏定义
int main(int argc, char* argv[])
{if (iam(argv[1]) != -1)printf("Input successfully.\n");//命令行的第二个参数为要存储在内核的字符串,如果超出了范围,就会得到-1,意味着输入失败return 0;
}

whoami.c:

#define __LIBRARY__
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>_syscall2(int, whoami, const char*, name, unsigned int, size)int main(int argc, char* argv[])
{char uname[24];if (whoami(uname, 24) != -1)printf("%s\n", uname);return 0;
}

3)修改头文件/hdc/usr/include/中的unistd.h

注意这里是加上写的测试函数的调用号(这是我困惑的地方,其他的就没有什么要修改的了)
否则会报错

4、测试
1)先取消挂载硬盘

$ sudo umount hdc

2)进入到/oslab/下执行./run
并编译两个程序

gcc -o iam iam.c
gcc -o whoami whoami.c

问题:这几个程序分属于不同的程序,究竟是怎么将其联系在一起的?

补充1:在用户态和内核态之间传递地址
问题:为什么要使用get_fs_byte() 和put_fs_byte()?
Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据。应用程序采用的是虚拟内存机制,段页式管理空间内存。直接通过虚拟地址访问可能访问到的数据已经被换出,或者访问的是内核空间的地址。

内核空间数据段的选择符为0x10,用户空间数据段选择符为0x17。内核空间、用户空间之间的数据传输,是段间数据传输。
在segment.h中定义了一系列用于内核空间和用户空间传输数据的函数。从用户空间取得数据的函数中, mov指令的源操作数段寄存器都明确指出是fs,向用户空间写数据的函数中, mov指令的目的操作数段寄存器都是fs。当系统调用发生时,int 0x80处理函数会把fs设成用户数据段选择符(0x17),
(从这个角度来说,将内存空间分为用户空间和内核空间,也是分段管理内存的体现)

// 功能:从用户空间中addr地址处取出一个字节
// 参数:addr   用户空间中的逻辑地址
//*%0-(_v),%1-内存地址
// 返回:fs:[addr]处的一个字节内容
extern inline unsigned char get_fs_byte(const char * addr)
{unsigned register char _v;__asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));//该指令会完成将*addr中的数据拷贝到_v中的操作return _v;
}
// 功能:向用户空间中addr地址处写一个字节的内容
// 参数:val   要写入的数据
//      addr     用户空间中的逻辑地址
// 返回:(无)
extern inline void put_fs_byte(char val,char *addr)
{   // addr是相对于用户数据段的偏移,而当前数据段为内核数据段  // 所以要写成fs:[addr]的形式
__asm__ ("movb %0,%%fs:%1"::"r" (val),"m" (*addr));
}

————————————————
仅以第一个接口get_fs_byte为例,其中"=r"代表函数返回值_v以任意一个寄存器返回,“m"代表输入参数*addr在存入内存当中。%0和%1分别按照寄存器或内存出现的顺序代表它们,所以%1就是那个"m”,%0就是%r。现在程序的意思就比较明了了,其使用%fs作为段寄存器加上 *addr的偏移,取出内存中对应的内容后放入寄存器%r中返回,达到了取一个字节数据的功能。
————————————————
版权声明:横线中的为CSDN博主「刘维汉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44167788/article/details/107942067
————————————————

补充2:
AT&T基础知识
内嵌汇编使用的是AT&T汇编,所以首先稍微讲解下AT&T的汇编指令的基础知识。

操作数前缀

movl   $8,%eax
movl   $0xffff,%ebx
int     $0x80
//看到在AT%T汇编中诸如"%eax"、"%ebx"之类的寄存器名字前都要加上"%";"$8"、"$0xffff"这样的立即数之前都要加上"$"。

源/目的操作数顺序
在Intel语法中,第一个操作数是目的操作数,第二个操作数源操作数。而在AT&T中,第一个数是源操作数,第二个数是目的操作数。

// INTEL语法
MOV EAX,8 //EAX是目的操作数, 8是源操作数
// AT&T语法
movl $8,%eax //8是源操作数 EAX是目的操作数
下面是一个内嵌汇编的例子

int main(){int input = 8;int result = 0;__asm__ __violate__  ("movl %1,%0" : "=r" (result) : "r" (input));printf("%d\n",result);return 0;
}

violate表明不希望编译器对代码进行优化,保持后面的代码原样
“movl %1,%0”是指令模板;“%0”和“%1”代表指令的操作数( 操作数(operand),是计算机指令中的一个组成部分,它规定了指令中进行数字运算的量 。 操作数指出指令执行的操作所需要数据的来源。),称为占位符,“=r”代表它之后是输入变量且需用到寄存器,指令模板后面用小括号括起来的是C语言表达式 ,其中input是输入变量,该指令会完成把input的值复制到result中的操作 。

补充3:CPL、RPL、DPL

补充4:print法调试程序
让运行中的程序向外输出一些信息,告知程序的运行状态也是一种很好的调试方式。
printf()是只有在用户空间中才能运行的函数,在内核空间中得使用printk()。
这两个函数的接口差不多,只是printk()需要特别处理一下 fs 寄存器,它是专用于用户模式的段寄存器。

int printk(const char *fmt, ...)
{//    ……__asm__("push %%fs\n\t""push %%ds\n\t""pop %%fs\n\t""pushl %0\n\t""pushl $buf\n\t""pushl $0\n\t""call tty_write\n\t""addl $8,%%esp\n\t""popl %0\n\t""pop %%fs"::"r" (i):"ax","cx","dx");
//    ……
}

printk() 首先 push %fs 保存这个指向用户段的寄存器,在最后 pop %fs 将其恢复,printk() 的核心仍然是调用 tty_write()。查看 printf() 可以看到,它最终也要落实到这个函数上。

参考文章:
1.内嵌汇编学习
2、哈工大操作系统实验OSLab2-系统调用by刘维汉
3、蓝桥oslab——系统调用
4、

2021-08-11hit-oslab2系统调用相关推荐

  1. 2021.08.09【普及组】模拟赛C组比赛总结

    文章目录 2021.08.09[普及组]模拟赛C组比赛总结 写在前面: T1 :[普及模拟]生产武器 题目大意: 正解: T2 :[普及模拟]城市连接 题目大意: 正解: T3 :[普及模拟]抢救文件 ...

  2. 《安富莱嵌入式周报》第227期:2021.08.23--2021.08.29

    往期周报汇总地址:http://www.armbbs.cn/forum.php?mod=forumdisplay&fid=12&filter=typeid&typeid=104 ...

  3. 纯Go实现的Firebase的替代品 | Gopher Daily (2021.08.11) ʕ◔ϖ◔ʔ

    每日一谚:Global variables should have longer names. Go技术生态 如何才能成功将Python切换到Go - https://itnext.io/opinio ...

  4. GNSS数据下载网站整理,包括gamit、bernese更新文件地址[2021.08更新]

    本人博客园同名原创文章,展示到CSDN供大家参考,转载请声明地址:https://www.cnblogs.com/ydh2017/p/6474654.html 从事GNSS研究的小伙伴大都离不开GNS ...

  5. 【Yolov5】1.认真总结6000字Yolov5保姆级教程(旧版本2021.08.03作为备份)

    旧版本2021.08.03 新版本https://blog.csdn.net/m0_53392188/article/details/119334634​​​​​​​ 以作备份 目录 一.前言 二.学 ...

  6. 本博客导读(2021/08/09更新)

    文章目录 1. 简介 1.1 博客精神 1.2 写作目的 1.3 技术方向 1.4 博主 1.5 版权说明 2 推荐内容 2.1 主要代表作 2.2 其他推荐内容 3. 程序类 3.1 C#程序设计 ...

  7. Doris Weekly FAQ】2021.07.19~2021.08.01

    观众朋友们: 晚上好! 欢迎收看[ Doris 近日要闻]~本次为您带来的是 2021年07月19日 - 2021年08月01日 的双周总结. Doris 社区周报每期会包含 FAQ 环节.我们会在社 ...

  8. 2021.08.28-MMsegmentation0.16.0+Cuda10.1+Ubuntu16.04+Pytorch1.8环境安装

    个人在目标检测方向的学习比较深入,但在深度学习的图像处理中,语义分割也是一个很重要的方向,所以也想一探究竟,熟悉一下基本流程和工作原理. 现打算在LINUX系统Ubuntu16.04上安装mmsegm ...

  9. 2021.08.26学习内容 Win10+GeForce GTX1650安装NVIDIA显卡驱动及CUDA11.4+cuDNN8.2

    之前主要使用Ubuntu系统,但是个人笔记本更多使用windows,为了方便跑一些pytorch的小代码,所以想在windows配置一下相关环境,达到调用GPU运算的目的. 记录也是为了自己以后有安装 ...

  10. 《惢客创业日记》2021.08.06-09(周五)惢客与征信的区别(下)

    这两天,没啥关于惢客的事儿,正好把我写的<惢客>书中"惢客与征信的区别"下半部分分享一下. 3.在"诚信辨识度"上的差异分析 [差异点]:征信(目前 ...

最新文章

  1. Java多线程复习:3(在操作系统中查看和杀死进程线程)
  2. Flask-Email的相关知识点实现(发送电子邮件)
  3. 南京林业大学计算机专升本,2018江苏专转本学校之:南京林业大学
  4. Redis缓存穿透、击穿、雪崩及主从复制
  5. 这4部有生之年必看的“教材级”纪录片,免费领取!
  6. 还在随缘炼丹?一文带你详尽了解机器学习模型可解释性的奥秘
  7. Software Construction Series(1)
  8. 交通运输部:预计五一假期全国客运量2.65亿人次
  9. UVa 11636 - Hello World!
  10. htpasswd小工具生成密码
  11. 【存储知识】RAID(磁盘冗余阵列)与 LVM(逻辑卷管理器)
  12. Pytorch 运行加速
  13. 机器学习篇——MNIST手写数字识别
  14. java实现牛牛算法
  15. 《Python编程:从入门到实践》 练习 9-4 9-5
  16. [渝粤教育] 天水师范学院 地球科学概论 参考 资料
  17. USBTO232的几个问题,乱码,回车无效,驱动安装
  18. Nginx配置并使用SSI功能
  19. MOS管工作动画原理图详解
  20. homeassistant mysql_给Homeassistant更换PostgreSQL数据库

热门文章

  1. 求最短路径的多种算法实现
  2. pythonjam可以画图吗_Matplotlib画图如此简单
  3. 2017 ACM-ICPC 亚洲区(西安赛区)网络赛 F. Trig Function(切比雪夫定理)
  4. 30个Python奇淫技巧集
  5. 边玩边学?这些游戏帮你更好的学习编程,终于不用担心家里不让玩游戏了
  6. GDC2016 【全境封锁】的全局照明技术
  7. 生成Openni使用的oni数据集文件
  8. python和pycharm哪个好_初学python,pycharm和Spyder哪个好?
  9. 肿瘤分类与预测(朴素贝叶斯)
  10. 开展全媒体营销的具体步骤和策略