实验要求

实验前须知

阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件:

  • 系统调用的用户空间代码在 user/user.huser/usys.pl 中。
  • 内核空间代码在 kernel/syscall.hkernel/syscall.c 中。
  • 与进程相关的代码在 kernel/proc.hkernel/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 添加到 MakefileUPROGS
  • 运行 make qemu , 你将看到编译器无法编译 user/trace.c ,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到 user/user.h ,将存根添加到 user/usys.pl ,以及将系统调用号添加到 kernel/syscall.h 中。 Makefile 调用 perl 脚本 user/usys.pl ,它生成 user/usys.S ,实际的系统调用存根,它使用 RISC-V ecall 指令转换到内核。修复编译问题后,运行 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() 函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。

实验思路

  1. 首先,根据实验前须知阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件。其中第 2 章讲的是 xv6 系统的组织结构,第 4 章的 4.3 节讲的是调用 system call 的过程,第 4 章的 4.4 节讲的是调用 system call 的参数。与本实验直接相关,所以必须依照源码进行阅读。
  2. 这里补充一点,做这个实验需要对 xv6 启动过程以及调用系统调用过程有一些了解,具体可以观看上课视频的结尾部分,视频地址:https://www.bilibili.com/video/BV19k4y1C7kA?p=2
  3. 具体过程解释见下面的实验步骤。

实验步骤

作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 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.csyscall 那个函数处,执行此函数。下面是 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.cfork 函数调用时,添加子进程复制父进程的 mask 的代码:

int
fork(void)
{...pid = np->pid;np->state = RUNNABLE;// 子进程复制父进程的 mask np->mask = p->mask;...
}

最后在 MakefileUPROGS 中添加:

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 添加到 MakefileUPROGS 中。
  • 运行 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);
}

最后在 MakefileUPROGS 中添加:

$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)相关推荐

  1. 操作系统实验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 模拟器启 ...

  2. 广州大学2020操作系统实验二:银行家算法

    相关资料 广州大学2020操作系统实验一:进程管理与进程通信 广州大学2020操作系统实验二:银行家算法 广州大学2020操作系统实验三:内存管理 广州大学2020操作系统实验四:文件系统 广州大学2 ...

  3. 广州大学2020操作系统实验四:文件系统

    相关资料 广州大学2020操作系统实验一:进程管理与进程通信 广州大学2020操作系统实验二:银行家算法 广州大学2020操作系统实验三:内存管理 广州大学2020操作系统实验四:文件系统 广州大学2 ...

  4. 广州大学2020操作系统实验一:进程管理与进程通信

    相关资料 广州大学2020操作系统实验一:进程管理与进程通信 广州大学2020操作系统实验二:银行家算法 广州大学2020操作系统实验三:内存管理 广州大学2020操作系统实验四:文件系统 广州大学2 ...

  5. mit 6.s081

    简介 xv6-book chapter1 Operating system interfaces chapter2 Operating system organization Code:startin ...

  6. 操作系统实验报告1:ucore Lab 1

    操作系统实验报告1 实验内容 阅读 uCore 实验项目开始文档 (uCore Lab 0),准备实验平台,熟悉实验工具. uCore Lab 1:系统软件启动过程 (1) 编译运行 uCore La ...

  7. [mit6.1810] Lab system calls

    文章目录 前言 Using gdb 题目 分析 system call tracing 题目 分析与代码 Sysinfo 题目 分析与代码 前言 在这个lab中我们会实现一些系统调用,这些系统调用类似 ...

  8. Race Condition Vulnerability Lab操作系统实验

    Race Condition Vulnerability Lab操作系统实验 实验准备 Task 2.A target_process.sh attack_process.c 运行&结果 异常 ...

  9. Lab system calls

    6s081 Lab2: system calls 一开始看的时候看见这两个实验都是moderate,还以为挺简单.结果因为对xv6 book不熟悉,没有弄明白整个系统调用的过程,花了很多的时间去理解x ...

  10. MIT操作系统实验lab1(pingpong案例:附代码、详解)

    1.题目描述:在xv6上实现pingpong程序,即两个进程在管道两侧来回通信.父进程将"ping"写入管道,子进程从管道将其读出并打印<pid>:received p ...

最新文章

  1. 专访周志华、宋继强:高端AI人才要具备哪些素质?深度学习的局限性和未来?...
  2. 有没有办法使用命令行cURL跟踪重定向?
  3. nginx location 在配置中的优先级
  4. js如何获取jwt信息_谈房地产公众号如何涨粉?一篇文章让你轻松获取信息
  5. 乌班图系统修改服务器时间的命令,ubuntu 修改系统时间无效
  6. Python+Appium+夜神模拟器安装与简单运行(2/2)
  7. 《信号与系统》(吴京)部分课后习题答案与解析——第七章(PART2)(系统及系统分析)
  8. 大数据综合实验的踩坑总结(林子雨)
  9. Matlab绘图线条颜色,线型,标记点选项参数
  10. 前端-获取treegrid的选中数据
  11. 综合柜台业务基本规范
  12. Java中使用Rational类实现分数精确的计算,
  13. 血压计模块|臂式血压计方案
  14. 使用appium桌面版在win平台连接逍遥模拟器(以梦幻西游手游为例)
  15. 一张图搞清楚中国茶叶分类
  16. 2019年度巨献:肠道微生物组研究领域重要成果解读!
  17. GBT22239-2019信息安全技术网络安全等级保护基本要求第三级安全要求管理部分表格版
  18. 2022年湖南成人高考答题卡简介及结构介绍
  19. xp 下启用 ahci 模式
  20. 解决IAR中Go to definition of不可用

热门文章

  1. ubuntu安装词典goldendict
  2. R语言-上海二手房数据分析
  3. 使用selenium爬验证码图片并识别
  4. SSIS 左边工具栏消失处理
  5. css设置div垂直居中
  6. vscode使用Setting Sync
  7. 荣耀9换从服务器获取安装包信息失败,华为荣耀9解锁BootLoader教程 荣耀9获取解锁码进行解锁...
  8. 10分钟学会数据地图制作,让你的可视化再高一级!
  9. python xlwt 表格样式
  10. JAVA网站后台管理系统