操作系统实验Lab 2:system calls(MIT 6.S081 FALL 2020)
实验要求
实验前须知
阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件:
- 系统调用的用户空间代码在
user/user.h
和user/usys.pl
中。 - 内核空间代码在
kernel/syscall.h
和kernel/syscall.c
中。 - 与进程相关的代码在
kernel/proc.h
和kernel/proc.c
中。
使用下面的命令切换到 syscall
分支。
$ git fetch
$ git checkout syscall
$ make clean
推荐新建文件夹,重新使用下面的命令下载代码作为实验 2 工作区。
$ git clone git://g.csail.mit.edu/xv6-labs-2020
$ git checkout syscall
System call tracing (moderate)
实验目的
- 添加一个系统调用跟踪功能,该功能可以在以后的实验中为你提供帮助。
- 你将创建一个新的
trace
系统调用来控制跟踪。 - 它应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。例如,为了跟踪
fork
系统调用,程序调用trace (1 << SYS_fork)
,其中SYS_fork
是来自kernel/syscall.h
的系统调用号。 - 如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。
- 该行应包含 进程 ID 、 系统调用名称 和 返回值 ;您不需要打印系统调用参数。
trace
系统调用应该为调用它的进程和它随后派生的任何子进程启用跟踪,但不应影响其他进程。
实验要求及提示
- 将
$U/_trace
添加到Makefile
的UPROGS
中 - 运行
make qemu
, 你将看到编译器无法编译user/trace.c
,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到user/user.h
,将存根添加到user/usys.pl
,以及将系统调用号添加到kernel/syscall.h
中。Makefile
调用 perl 脚本user/usys.pl
,它生成user/usys.S
,实际的系统调用存根,它使用 RISC-Vecall
指令转换到内核。修复编译问题后,运行trace 32 grep hello README
;它会失败,因为你还没有在内核中实现系统调用。 - 在
kernel/sysproc.c
中添加一个sys_trace()
函数,该函数通过在proc
结构中的新变量中记住其参数来实现新系统调用(请参阅kernel/proc.h
)。从用户空间检索系统调用参数的函数位于kernel/syscall.c
中,你可以在kernel/sysproc.c
中查看它们的使用示例。 - 修改
fork()
(参见kernel/proc.c
)以将跟踪的掩码从父进程复制到子进程。 - 修改
kernel/syscall.c
中的syscall()
函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。
实验思路
- 首先,根据实验前须知阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件。其中第 2 章讲的是 xv6 系统的组织结构,第 4 章的 4.3 节讲的是调用 system call 的过程,第 4 章的 4.4 节讲的是调用 system call 的参数。与本实验直接相关,所以必须依照源码进行阅读。
- 这里补充一点,做这个实验需要对 xv6 启动过程以及调用系统调用过程有一些了解,具体可以观看上课视频的结尾部分,视频地址:https://www.bilibili.com/video/BV19k4y1C7kA?p=2
- 具体过程解释见下面的实验步骤。
实验步骤
作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义,模仿已经存在的系统调用序号的宏定义,我们定义 SYS_trace
如下:
#define SYS_trace 22
查看了一下 user 目录下的文件,发现官方已经给出了用户态的 trace
函数( user/trace.c ),所以我们直接在 user/user.h 文件中声明用户态可以调用 trace
系统调用就好了,但有一个问题,该系统调用的参数和返回值分别是什么类型呢?接下来我们还是得看一看 trace.c 文件,可以看到 trace(atoi(argv[1])) < 0
,即 trace
函数传入的是一个数字,并和 0
进行比较,结合实验提示,我们知道传入的参数类型是 int
,并且由此可以猜测到返回值类型应该是 int
。这样就可以把 trace
这个系统调用加入到内核中声明了:
// system calls
int trace(int);
接下来我们查看 user/usys.pl 文件,这里 perl 语言会自动生成汇编语言 usys.S ,是用户态系统调用接口。所以在 user/usys.pl 文件加入下面的语句:
entry("trace");
如果你编译后查看 usys.S 文件,就能可以看到存在把系统调用号放入 a7
寄存器的指令,然后就直接使用命令 ecall
进入系统内核。不信我们先查看上一次实验编译后的 usys.S 文件,可以看到如下的代码块:
.global fork
fork:li a7, SYS_forkecallret
li a7, SYS_fork
指令就是把 SYS_fork
的系统调用号放入 a7
寄存器,使用 ecall
指令进入系统内核。
那么,执行 ecall
指令会跳转到哪里呢?答案是跳转到 kernel/syscall.c 中 syscall
那个函数处,执行此函数。下面是 syscall
函数的源码:
void
syscall(void)
{int num;struct proc *p = myproc();num = p->trapframe->a7;if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {p->trapframe->a0 = syscalls[num]();} else {printf("%d %s: unknown sys call %d\n",p->pid, p->name, num);p->trapframe->a0 = -1;}
}
其中,我们可以看到, num = p->trapframe->a7;
从寄存器 a7
中读取系统调用号,所以上面的 usys.S 文件就是系统调用用户态和内核态的切换接口。接下来是 p->trapframe->a0 = syscalls[num]();
语句,通过调用 syscalls[num]();
函数,把返回值保存在了 a0
寄存器中。我们看看 syscalls[num]();
函数,这个函数在当前文件中。该函数调用了系统调用命令。
static uint64 (*syscalls[])(void) = {[SYS_fork] sys_fork,[SYS_exit] sys_exit,...
}
所以我们把新增的 trace
系统调用添加到函数指针数组 *syscalls[]
上:
static uint64 (*syscalls[])(void) = {...[SYS_trace] sys_trace,
};
接下来在文件开头给内核态的系统调用 trace
加上声明,在 kernel/syscall.c 加上:
extern uint64 sys_trace(void);
在实现这个函数之前,我们可以看到实验最后要输出每个系统调用函数的调用情况,依照实验说明给的示例,可以知道最后输出的格式如下:
<pid>: syscall <syscall_name> -> <return_value>
其中, <pid>
是进程序号, <syscall_name>
是函数名称, <return_value>
是该系统调用的返回值。注意:冒号和 syscall
中间有个空格,刚开始的时候自己就踩了一个坑。
根据提示,我们的 trace
系统调用应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。所以,我们在 kernel/proc.h 文件的 proc
结构体中,新添加一个变量 mask
,使得每一个进程都有自己的 mask
,即要跟踪的系统调用。
struct proc {...int mask; // Mask
};
然后我们就可以在 kernel/sysproc.c 给出 sys_trace
函数的具体实现了,只要把传进来的参数给到现有进程的 mask
就好了:
uint64
sys_trace(void)
{int mask;// 取 a0 寄存器中的值返回给 maskif(argint(0, &mask) < 0)return -1;// 把 mask 传给现有进程的 maskmyproc()->mask = mask;return 0;
}
接下来我们就要把输出功能实现,因为 RISCV 的 C 规范是把返回值放在 a0
中,所以我们只要在调用系统调用时判断是不是 mask
规定的输出函数,如果是就输出。
因为 proc
结构体(见 kernel/proc.h )里的 name
是整个线程的名字,不是函数调用的函数名称,所以我们不能用 p->name
,而要自己定义一个数组,我这里直接在 kernel/syscall.c 中定义了,这里注意系统调用名字一定要按顺序,第一个为空,当然你也可以去掉第一个空字符串,但要记得取值的时候索引要减一,因为这里的系统调用号是从 1
开始的。
static char *syscall_names[] = {"", "fork", "exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir", "dup", "getpid", "sbrk", "sleep", "uptime", "open", "write", "mknod", "unlink", "link", "mkdir", "close", "trace"};
然后我们就可以在 kernel/syscall.c 中的 syscall
函数中添加打印调用情况语句。 mask
是按位判断的,所以判断使用的是按位运算。进程序号直接通过 p->pid
就可以取到,函数名称需要从我们刚刚定义的数组中获取,即 syscall_names[num]
,其中 num
是从寄存器 a7
中读取的系统调用号,系统调用的返回值就是寄存器 a0
的值了,直接通过 p->trapframe->a0
语句获取即可。注意上面说的那个空格。
void
syscall(void)
{int num;struct proc *p = myproc();num = p->trapframe->a7;if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {p->trapframe->a0 = syscalls[num]();// 下面是添加的部分if((1 << num) & p->mask) {printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);}} else {printf("%d %s: unknown sys call %d\n",p->pid, p->name, num);p->trapframe->a0 = -1;}
}
然后在 kernel/proc.c 中 fork
函数调用时,添加子进程复制父进程的 mask
的代码:
int
fork(void)
{...pid = np->pid;np->state = RUNNABLE;// 子进程复制父进程的 mask np->mask = p->mask;...
}
最后在 Makefile
的 UPROGS
中添加:
UPROGS=\...$U/_trace\
实验结果
编译并运行 xv6 进行测试。
$ make qemu
...
init: starting sh
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
4: syscall trace -> 0
4: syscall exec -> 3
4: syscall open -> 3
4: syscall read -> 1023
4: syscall read -> 966
4: syscall read -> 70
4: syscall read -> 0
4: syscall close -> 0
$
$ grep hello README
$
$ trace 2 usertests forkforkfork
usertests starting
test forkforkfork: 407: syscall fork -> 408
408: syscall fork -> 409
409: syscall fork -> 410
410: syscall fork -> 411
409: syscall fork -> 412
410: syscall fork -> 413
409: syscall fork -> 414
411: syscall fork -> 415
...
$
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-syscall trace
通过测试样例。
== Test trace 32 grep == trace 32 grep: OK (2.6s)
== Test trace all grep == trace all grep: OK (1.0s)
== Test trace nothing == trace nothing: OK (0.5s)
== Test trace children == trace children: OK (10.8s)
Sysinfo (moderate)
实验要求
在本实验中,您将添加一个系统调用 sysinfo
,它收集有关正在运行的系统信息。系统调用接受一个参数:一个指向 struct sysinfo
的指针(参见 kernel/sysinfo.h )。内核应该填写这个结构体的字段: freemem
字段应该设置为空闲内存的字节数, nproc
字段应该设置为状态不是 UNUSED 的进程数。我们提供了一个测试程序 sysinfotest
;如果它打印 “sysinfotest:OK”
,则实验结果通过测试。
实验提示
- 将
$U/_sysinfotest
添加到Makefile
的UPROGS
中。 - 运行
make qemu
, 你将看到编译器无法编译user/sysinfotest.c
。添加系统调用sysinfo
,按照与之前实验相同的步骤。要在 user/user.h 中声明sysinfo()
的原型,您需要预先声明struct sysinfo
:
struct sysinfo;
int sysinfo(struct sysinfo *);
- 修复编译问题后,运行
sysinfotest
会失败,因为你还没有在内核中实现系统调用。 sysinfo
需要复制一个struct sysinfo
返回用户空间;有关如何使用copyout()
执行此操作的示例,请参阅sys_fstat()
( kernel/sysfile.c ) 和filestat()
( kernel/file.c )。- 要收集空闲内存量,请在 kernel/kalloc.c 中添加一个函数。
- 要收集进程数,请在 kernel/proc.c 中添加一个函数。
实验步骤
跟上个实验一样,首先定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义 SYS_sysinfo
如下:
#define SYS_sysinfo 23
在 user/usys.pl 文件加入下面的语句:
entry("sysinfo");
然后在 user/user.h 中添加 sysinfo
结构体以及 sysinfo
函数的声明:
struct stat;
struct rtcdate;
// 添加 sysinfo 结构体
struct sysinfo;// system calls
...
int sysinfo(struct sysinfo *);
在 kernel/syscall.c 中新增 sys_sysinfo
函数的定义:
extern uint64 sys_sysinfo(void);
在 kernel/syscall.c 中函数指针数组新增 sys_trace
:
[SYS_sysinfo] sys_sysinfo,
记得在 kernel/syscall.c 中的 syscall_names
新增一个 sys_trace
:
static char *syscall_names[] = {"", "fork", "exit", "wait", "pipe", "read", "kill", "exec", "fstat", "chdir", "dup", "getpid", "sbrk", "sleep", "uptime", "open", "write", "mknod", "unlink", "link", "mkdir", "close", "trace", "sysinfo"};
接下来我们就要开始写相应的函数实现了。
首先我们写获取可用进程数目的函数实现。通过阅读 kernel/proc.c 文件可以看到下面的语句:
struct proc proc[NPROC];
这是一个进程数组的定义,这里保存了所有的进程。我们再阅读 kernel/proc.h 查看进程结构体的定义:
enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };// Per-process state
struct proc {struct spinlock lock;// p->lock must be held when using these:enum procstate state; // Process statestruct proc *parent; // Parent processvoid *chan; // If non-zero, sleeping on chanint killed; // If non-zero, have been killedint xstate; // Exit status to be returned to parent's waitint pid; // Process ID// these are private to the process, so p->lock need not be held.uint64 kstack; // Virtual address of kernel stackuint64 sz; // Size of process memory (bytes)pagetable_t pagetable; // User page tablestruct trapframe *trapframe; // data page for trampoline.Sstruct context context; // swtch() here to run processstruct file *ofile[NOFILE]; // Open filesstruct inode *cwd; // Current directorychar name[16]; // Process name (debugging)int mask; // Mask
};
可以看到,进程里面已经保存了当前进程的状态,所以我们可以直接遍历所有进程,获取其状态判断当前进程的状态是不是为 UNUSED
并统计数目就行了。当然,通过 proc
结构体的定义,我们知道使用进程状态时必须加锁,我们在 kernel/proc.c 中新增函数 nproc
如下,通过该函数以获取可用进程数目:
// Return the number of processes whose state is not UNUSED
uint64
nproc(void)
{struct proc *p;// counting the number of processesuint64 num = 0;// traverse all processesfor (p = proc; p < &proc[NPROC]; p++){// add lockacquire(&p->lock);// if the processes's state is not UNUSEDif (p->state != UNUSED){// the num add onenum++;}// release lockrelease(&p->lock);}return num;
}
接下来我们来实现获取空闲内存数量的函数。可用空间的判断在 kernel/kalloc.c 文件中。
这里定义了一个链表,每个链表都指向上一个可用空间,这里的 kmem
就是一个保存最后链表的变量。
struct run {struct run *next;
};struct {struct spinlock lock;struct run *freelist;
} kmem;
要想更深入了解的话就详细看看当前这个文件(下面摘了部分内容):
extern char end[]; // first address after kernel.// defined by kernel.ld.void
kinit()
{initlock(&kmem.lock, "kmem");freerange(end, (void*)PHYSTOP);
}void
freerange(void *pa_start, void *pa_end)
{char *p;p = (char*)PGROUNDUP((uint64)pa_start);for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)kfree(p);
}// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{struct run *r;if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)panic("kfree");// Fill with junk to catch dangling refs.memset(pa, 1, PGSIZE);r = (struct run*)pa;acquire(&kmem.lock);r->next = kmem.freelist;kmem.freelist = r;release(&kmem.lock);
}
这里把从 end (内核后的第一个地址)
到 PHYSTOP (KERNBASE + 128*1024*1024)
之间的物理空间以 PGSIZE
为单位全部初始化为 1
,然后每次初始化一个 PGSIZE
就把这个页挂在了 kmem.freelist
上,所以 kmem.freelist
永远指向最后一个可用页,那我们只要顺着这个链表往前走,直到 NULL
为止。所以我们就可以在 kernel/kalloc.c 中新增函数 free_mem
,以获取空闲内存数量:
// Return the number of bytes of free memory
uint64
free_mem(void)
{struct run *r;// counting the number of free pageuint64 num = 0;// add lockacquire(&kmem.lock);// r points to freelistr = kmem.freelist;// while r not nullwhile (r){// the num add onenum++;// r points to the nextr = r->next;}// release lockrelease(&kmem.lock);// page multiplicated 4096-byte pagereturn num * PGSIZE;
}
然后在 kernel/defs.h 中添加上述两个新增函数的声明:
// kalloc.c
...
uint64 free_mem(void);// proc.c
...
uint64 nproc(void);
接下来我们按照实验提示,添加 sys_sysinfo
函数的具体实现,这里提到 sysinfo
需要复制一个 struct sysinfo
返回用户空间,根据实验提示使用 copyout()
执行此操作,我们查看 kernel/sysfile.c 文件中的 sys_fstat()
函数,如下:
uint64
sys_fstat(void)
{struct file *f;uint64 st; // user pointer to struct statif(argfd(0, 0, &f) < 0 || argaddr(1, &st) < 0)return -1;return filestat(f, st);
}
这里可以看到调用了 filestat()
函数,该函数在 kernel/file.c 中,如下:
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{struct proc *p = myproc();struct stat st;if(f->type == FD_INODE || f->type == FD_DEVICE){ilock(f->ip);stati(f->ip, &st);iunlock(f->ip);if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)return -1;return 0;}return -1;
}
我们可以知道,复制一个 struct sysinfo
返回用户空间需要调用 copyout()
函数,上面是一个例子,我们来查看一下 copyout()
函数的定义( kernel/vm.c ):
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{uint64 n, va0, pa0;while(len > 0){va0 = PGROUNDDOWN(dstva);pa0 = walkaddr(pagetable, va0);if(pa0 == 0)return -1;n = PGSIZE - (dstva - va0);if(n > len)n = len;memmove((void *)(pa0 + (dstva - va0)), src, n);len -= n;src += n;dstva = va0 + PGSIZE;}return 0;
}
我们知道该函数其实就是把在内核地址 src
开始的 len
大小的数据拷贝到用户进程 pagetable
的虚地址 dstva
处,所以 sys_sysinfo
函数实现里先用 argaddr
函数读进来我们要保存的在用户态的数据 sysinfo
的指针地址,然后再把从内核里得到的 sysinfo
开始的内容以 sizeof(info)
大小的的数据复制到这个指针上。模仿上面的例子,我们在 kernel/sysproc.c 文件中添加 sys_sysinfo
函数的具体实现如下:
// add header
#include "sysinfo.h"uint64
sys_sysinfo(void)
{// addr is a user virtual address, pointing to a struct sysinfouint64 addr;struct sysinfo info;struct proc *p = myproc();if (argaddr(0, &addr) < 0)return -1;// get the number of bytes of free memoryinfo.freemem = free_mem();// get the number of processes whose state is not UNUSEDinfo.nproc = nproc();if (copyout(p->pagetable, addr, (char *)&info, sizeof(info)) < 0)return -1;return 0;
}
最后在 user 目录下添加一个 sysinfo.c 用户程序:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/sysinfo.h"
#include "user/user.h"int
main(int argc, char *argv[])
{// param errorif (argc != 1){fprintf(2, "Usage: %s need not param\n", argv[0]);exit(1);}struct sysinfo info;sysinfo(&info);// print the sysinfoprintf("free space: %d\nused process: %d\n", info.freemem, info.nproc);exit(0);
}
最后在 Makefile 的 UPROGS
中添加:
$U/_sysinfotest\
$U/_sysinfo\
实验结果
编译并运行 xv6 进行测试。
$ make qemu
...
init: starting sh
$ sysinfo
free space: 133386240
used process: 3
$ sysinfotest
sysinfotest: start
sysinfotest: OK
退出 xv6 ,运行单元测试检查结果是否正确。
./grade-lab-syscall sysinfo
通过测试样例。
make: 'kernel/kernel' is up to date.
== Test sysinfotest == sysinfotest: OK (2.6s)
Lab 2 所有实验测试
退出 xv6 ,使用命令 vim time.txt
新建文件写入你做该实验所花的时间(小时),运行整个 Lab 2 测试,检查结果是否正确。
$ make grade
...
== Test trace 32 grep ==
$ make qemu-gdb
trace 32 grep: OK (2.3s)
== Test trace all grep ==
$ make qemu-gdb
trace all grep: OK (1.0s)
== Test trace nothing ==
$ make qemu-gdb
trace nothing: OK (0.8s)
== Test trace children ==
$ make qemu-gdb
trace children: OK (10.1s)
== Test sysinfotest ==
$ make qemu-gdb
sysinfotest: OK (2.3s)
== Test time ==
time: OK
Score: 35/35
操作系统实验Lab 2:system calls(MIT 6.S081 FALL 2020)相关推荐
- 操作系统实验Lab 1:Xv6 and Unix utilities(MIT 6.S081 FALL 2020)
Lab 1 Xv6 and Unix utilities 实验要求链接 Boot xv6 (easy) 实验目的 切换到 xv6-labs-2020 代码的 util 分支,并利用 QEMU 模拟器启 ...
- 广州大学2020操作系统实验二:银行家算法
相关资料 广州大学2020操作系统实验一:进程管理与进程通信 广州大学2020操作系统实验二:银行家算法 广州大学2020操作系统实验三:内存管理 广州大学2020操作系统实验四:文件系统 广州大学2 ...
- 广州大学2020操作系统实验四:文件系统
相关资料 广州大学2020操作系统实验一:进程管理与进程通信 广州大学2020操作系统实验二:银行家算法 广州大学2020操作系统实验三:内存管理 广州大学2020操作系统实验四:文件系统 广州大学2 ...
- 广州大学2020操作系统实验一:进程管理与进程通信
相关资料 广州大学2020操作系统实验一:进程管理与进程通信 广州大学2020操作系统实验二:银行家算法 广州大学2020操作系统实验三:内存管理 广州大学2020操作系统实验四:文件系统 广州大学2 ...
- mit 6.s081
简介 xv6-book chapter1 Operating system interfaces chapter2 Operating system organization Code:startin ...
- 操作系统实验报告1:ucore Lab 1
操作系统实验报告1 实验内容 阅读 uCore 实验项目开始文档 (uCore Lab 0),准备实验平台,熟悉实验工具. uCore Lab 1:系统软件启动过程 (1) 编译运行 uCore La ...
- [mit6.1810] Lab system calls
文章目录 前言 Using gdb 题目 分析 system call tracing 题目 分析与代码 Sysinfo 题目 分析与代码 前言 在这个lab中我们会实现一些系统调用,这些系统调用类似 ...
- Race Condition Vulnerability Lab操作系统实验
Race Condition Vulnerability Lab操作系统实验 实验准备 Task 2.A target_process.sh attack_process.c 运行&结果 异常 ...
- Lab system calls
6s081 Lab2: system calls 一开始看的时候看见这两个实验都是moderate,还以为挺简单.结果因为对xv6 book不熟悉,没有弄明白整个系统调用的过程,花了很多的时间去理解x ...
- MIT操作系统实验lab1(pingpong案例:附代码、详解)
1.题目描述:在xv6上实现pingpong程序,即两个进程在管道两侧来回通信.父进程将"ping"写入管道,子进程从管道将其读出并打印<pid>:received p ...
最新文章
- 专访周志华、宋继强:高端AI人才要具备哪些素质?深度学习的局限性和未来?...
- 有没有办法使用命令行cURL跟踪重定向?
- nginx location 在配置中的优先级
- js如何获取jwt信息_谈房地产公众号如何涨粉?一篇文章让你轻松获取信息
- 乌班图系统修改服务器时间的命令,ubuntu 修改系统时间无效
- Python+Appium+夜神模拟器安装与简单运行(2/2)
- 《信号与系统》(吴京)部分课后习题答案与解析——第七章(PART2)(系统及系统分析)
- 大数据综合实验的踩坑总结(林子雨)
- Matlab绘图线条颜色,线型,标记点选项参数
- 前端-获取treegrid的选中数据
- 综合柜台业务基本规范
- Java中使用Rational类实现分数精确的计算,
- 血压计模块|臂式血压计方案
- 使用appium桌面版在win平台连接逍遥模拟器(以梦幻西游手游为例)
- 一张图搞清楚中国茶叶分类
- 2019年度巨献:肠道微生物组研究领域重要成果解读!
- GBT22239-2019信息安全技术网络安全等级保护基本要求第三级安全要求管理部分表格版
- 2022年湖南成人高考答题卡简介及结构介绍
- xp 下启用 ahci 模式
- 解决IAR中Go to definition of不可用