xv6学习笔记(4) : 进程

xv6所有程序都是单进程、单线程程序。要明白这个概念才好继续往下看

1. XV6中进程相关的数据结构

在XV6中,与进程有关的数据结构如下

  1. // Per-process state
  2. struct proc {
  3. uint sz; // Size of process memory (bytes)
  4. pde_t* pgdir; // Page table
  5. char *kstack; // Bottom of kernel stack for this process
  6. enum procstate state; // Process state
  7. int pid; // Process ID
  8. struct proc *parent; // Parent process
  9. struct trapframe *tf; // Trap frame for current syscall
  10. struct context *context; // swtch() here to run process
  11. void *chan; // If non-zero, sleeping on chan
  12. int killed; // If non-zero, have been killed
  13. struct file *ofile[NOFILE]; // Open files
  14. struct inode *cwd; // Current directory
  15. char name[16]; // Process name (debugging)
  16. };

与前述的两类信息的对应关系如下

  1. 操作系统管理进程有关的信息:内核栈kstack,进程的状态state,进程的pid,进程的父进程parent,进程的中断帧tf,进程的上下文context,与sleepkill有关的chankilled变量。
  2. 进程本身运行所需要的全部环境:虚拟内存信息szpgdir,打开的文件ofile和当前目录cwd

额外地,proc中还有一条用于调试的进程名字name

在操作系统中,所有的进程信息struct proc都存储在ptable中,ptable的定义如下

下面是proc结构体保存的一些重要数据结构

  • 首先是保存了用户空间线程寄存器的trapframe字段

  • 其次是保存了内核线程寄存器的context字段

  • 还有保存了当前进程的内核栈的kstack字段,这是进程在内核中执行时保存函数调用的位置

  • state字段保存了当前进程状态,要么是RUNNING,要么是RUNABLE,要么是SLEEPING等等

  • lock字段保护了很多数据,目前来说至少保护了对于state字段的更新。举个例子,因为有锁的保护,两个CPU的调度器线程不会同时拉取同一个RUNABLE进程并运行它

  1. struct {
  2. struct spinlock lock;
  3. struct proc proc[NPROC];
  4. } ptable;

除了互斥锁lock之外,一个值得注意的一点是XV6系统中允许同时存在的进程数量是有上限的。在这里NPROC为64,所以XV6最多只允许同时存在64个进程。

要注意操作系统的资源分配的单位是进程,处理机调度的单位是线程;

2. 第一个用户进程

1. userinit函数

main 初始化了一些设备和子系统后,它通过调用 userinit建立了第一个进程。

userinit 首先调用 allocprocallocproc的工作是在页表中分配一个槽(即结构体 struct proc),并初始化进程的状态,为其内核线程的运行做准备。注意一点:userinit 仅仅在创建第一个进程时被调用,而 allocproc 创建每个进程时都会被调用。allocproc 会在 proc 的表中找到一个标记为 UNUSED的槽位。当它找到这样一个未被使用的槽位后,allocproc 将其状态设置为 EMBRYO,使其被标记为被使用的并给这个进程一个独有的 pid(2201-2219)。接下来,它尝试为进程的内核线程分配内核栈。如果分配失败了,allocproc 会把这个槽位的状态恢复为 UNUSED 并返回0以标记失败。

  1. // Set up first user process.
  2. void
  3. userinit(void)
  4. {
  5. struct proc *p;
  6. extern char _binary_initcode_start[], _binary_initcode_size[];
  7. p = allocproc();
  8. initproc = p;
  9. if((p->pgdir = setupkvm()) == 0)
  10. panic("userinit: out of memory?");
  11. inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size);
  12. p->sz = PGSIZE;
  13. memset(p->tf, 0, sizeof(*p->tf));
  14. p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
  15. p->tf->ds = (SEG_UDATA << 3) | DPL_USER;
  16. p->tf->es = p->tf->ds;
  17. p->tf->ss = p->tf->ds;
  18. p->tf->eflags = FL_IF;
  19. p->tf->esp = PGSIZE;
  20. p->tf->eip = 0; // beginning of initcode.S
  21. safestrcpy(p->name, "initcode", sizeof(p->name));
  22. p->cwd = namei("/");
  23. // this assignment to p->state lets other cores
  24. // run this process. the acquire forces the above
  25. // writes to be visible, and the lock is also needed
  26. // because the assignment might not be atomic.
  27. acquire(&ptable.lock);
  28. p->state = RUNNABLE;
  29. release(&ptable.lock);
  30. }

2. allocproc函数

  1. 在ptable中找到一个没有被占用的槽位
  2. 找到之后分配pid然后把他的状态设置为EMBRYO
  1. static struct proc*
  2. allocproc(void)
  3. {
  4. struct proc *p;
  5. char *sp;
  6. acquire(&ptable.lock);
  7. for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
  8. if(p->state == UNUSED)
  9. goto found;
  10. release(&ptable.lock);
  11. return 0;
  12. found:
  13. p->state = EMBRYO;
  14. p->pid = nextpid++;
  15. release(&ptable.lock);
  16. // Allocate kernel stack.
  17. if((p->kstack = kalloc()) == 0){
  18. p->state = UNUSED;
  19. return 0;
  20. }
  21. sp = p->kstack + KSTACKSIZE;
  22. // Leave room for trap frame.
  23. sp -= sizeof *p->tf;
  24. p->tf = (struct trapframe*)sp;
  25. // Set up new context to start executing at forkret,
  26. // which returns to trapret.
  27. sp -= 4;
  28. *(uint*)sp = (uint)trapret;
  29. sp -= sizeof *p->context;
  30. p->context = (struct context*)sp;
  31. memset(p->context, 0, sizeof *p->context);
  32. p->context->eip = (uint)forkret;
  33. return p;
  34. }

这里进行调用完之后得到的状态如下图所示

3. mpmain函数

  1. // Common CPU setup code.
  2. static void
  3. mpmain(void)
  4. {
  5. cprintf("cpu%d: starting %d\n", cpuid(), cpuid());
  6. idtinit(); // load idt register
  7. xchg(&(mycpu()->started), 1); // tell startothers() we're up
  8. scheduler(); // start running processes
  9. }

1. scheduler()函数

这个函数是非常重要的,进行进程之间的调度,在上面我们创建了第一个用户进程但是还没有进行执行。

  1. void
  2. scheduler(void)
  3. {
  4. struct proc *p;
  5. struct cpu *c = mycpu();
  6. c->proc = 0;
  7. for(;;){
  8. // Enable interrupts on this processor.
  9. sti();
  10. // Loop over process table looking for process to run.
  11. acquire(&ptable.lock);
  12. for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
  13. if(p->state != RUNNABLE)
  14. continue;
  15. // Switch to chosen process. It is the process's job
  16. // to release ptable.lock and then reacquire it
  17. // before jumping back to us.
  18. c->proc = p;
  19. switchuvm(p);
  20. p->state = RUNNING;
  21. swtch(&(c->scheduler), p->context);
  22. switchkvm();
  23. // Process is done running for now.
  24. // It should have changed its p->state before coming back.
  25. c->proc = 0;
  26. }
  27. release(&ptable.lock);
  28. }
  29. }

2. switchuvm函数

  1. 这里要设置当前cpu的taskstate。关于taskstate的知识补充

taskstate的知识补充

  1. 对于cpu而言是没有进程或者线程的概念,对于cpu只有任务的概念
  2. 对于ss0是存储的0环的栈段选择子
  3. 对于esp是存储的0环的栈指针
  4. 而对于ring的概念也就是环的概念这里可以简单理解成特权集参考博客

  1. // Switch TSS and h/w page table to correspond to process p.
  2. void
  3. switchuvm(struct proc *p)
  4. {
  5. if(p == 0)
  6. panic("switchuvm: no process");
  7. if(p->kstack == 0)
  8. panic("switchuvm: no kstack");
  9. if(p->pgdir == 0)
  10. panic("switchuvm: no pgdir");
  11. pushcli();
  12. mycpu()->gdt[SEG_TSS] = SEG16(STS_T32A, &mycpu()->ts,
  13. sizeof(mycpu()->ts)-1, 0);
  14. mycpu()->gdt[SEG_TSS].s = 0;
  15. mycpu()->ts.ss0 = SEG_KDATA << 3;
  16. mycpu()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;
  17. // setting IOPL=0 in eflags *and* iomb beyond the tss segment limit
  18. // forbids I/O instructions (e.g., inb and outb) from user space
  19. mycpu()->ts.iomb = (ushort) 0xFFFF;
  20. ltr(SEG_TSS << 3);
  21. lcr3(V2P(p->pgdir)); // switch to process's address space
  22. popcli();
  23. }

3. 第一个程序Initcode.S

第一个程序会在虚拟地址[0-pagesize]这一段

  1. # exec(init, argv)
  2. .globl start
  3. start:
  4. pushl $argv
  5. pushl $init
  6. pushl $0 // where caller pc would be
  7. movl $SYS_exec, %eax
  8. int $T_SYSCALL
  9. # for(;;) exit();
  10. exit:
  11. movl $SYS_exit, %eax
  12. int $T_SYSCALL
  13. jmp exit
  14. # char init[] = "/init\0";
  15. init:
  16. .string "/init\0"
  17. # char *argv[] = { init, 0 };
  18. .p2align 2
  19. argv:
  20. .long init
  21. .long 0

这里是调用了exec执行init函数

这个其实更像什么,更像shell终端的启动

  1. int
  2. main(void)
  3. {
  4. int pid, wpid;
  5. if(open("console", O_RDWR) < 0){
  6. mknod("console", 1, 1);
  7. open("console", O_RDWR);
  8. }
  9. dup(0); // stdout
  10. dup(0); // stderr
  11. for(;;){
  12. printf(1, "init: starting sh\n");
  13. pid = fork();
  14. if(pid < 0){
  15. printf(1, "init: fork failed\n");
  16. exit();
  17. }
  18. if(pid == 0){
  19. exec("sh", argv);
  20. printf(1, "init: exec sh failed\n");
  21. exit();
  22. }
  23. while((wpid=wait()) >= 0 && wpid != pid)
  24. printf(1, "zombie!\n");
  25. }
  26. }

4. 进程切换

进程切换解决之后,对于xv6的进程调度就会有一个比较清晰的分析了

这里有几个重要的概念就是

  • 每一个进程都有一个对应的内核线程(也就是scheduler thread)线程。

  • 在xv6中想要从一个进程(当然这里叫线程也是无所谓的)切换到另一个线程中,必须要先从当前进程-->当前进程的内核线程-->目的线程的内核线程-->目的线程的用户进程。这样一个过程才能完成调度

1. 先从yied和sched开始

其实yield函数并没有干很多事情,关于的操作后面会单独来讲一下,这里就先跳过去

这个函数就是当前进程要让出cpu。所以把当前proc()的状态设置成RUNNABLE

最后调用sched()

  1. // Give up the CPU for one scheduling round.
  2. void
  3. yield(void)
  4. {
  5. acquire(&ptable.lock); //DOC: yieldlock
  6. myproc()->state = RUNNABLE;
  7. sched();
  8. release(&ptable.lock);
  9. }

这里先进行一些状态判断,如果出问题就会panic。

2. 随后调用swtch函数

其实这个函数就是switch这里为了不与c语言中的库函数同名

  1. void
  2. sched(void)
  3. {
  4. int intena;
  5. struct proc *p = myproc();
  6. if(!holding(&ptable.lock))
  7. panic("sched ptable.lock");
  8. if(mycpu()->ncli != 1)
  9. panic("sched locks");
  10. if(p->state == RUNNING)
  11. panic("sched running");
  12. if(readeflags()&FL_IF)
  13. panic("sched interruptible");
  14. intena = mycpu()->intena;
  15. swtch(&p->context, mycpu()->scheduler);
  16. mycpu()->intena = intena;
  17. }

swtch函数就是传说中的上下文切换。只不过和之前说的用户状态的上下文切换不一样

这里是把当前cpu的内核线程的寄存器保存到p->context

这里的(esp + 4)存储的就是edi寄存器的值。而(esp + 8)存储的就是esi寄存器的值,也就是第一个参数和第二个参数

  1. .globl swtch
  2. swtch:
  3. movl 4(%esp), %eax
  4. movl 8(%esp), %edx
  5. # Save old callee-saved registers
  6. pushl %ebp
  7. pushl %ebx
  8. pushl %esi
  9. pushl %edi
  10. # Switch stacks
  11. movl %esp, (%eax)
  12. movl %edx, %esp
  13. # Load new callee-saved registers
  14. popl %edi
  15. popl %esi
  16. popl %ebx
  17. popl %ebp
  18. ret

所以这里最后就会把mycpu()->scheduler中保存的context信息弹出到寄存器中。同时把esp寄存器更换成mycpu()->scheduler那里。所以这里的ret的返回地址就是mycpu()->scheduler保存的eip的值。也就会返回到

红色箭头所指向的一行。

3. 回到scheduler函数

现在我们在scheduler函数的循环中,代码会检查所有的进程并找到一个来运行。随后再来调用swtch函数

又调用了swtch函数来保存调度器线程的寄存器,并恢复目标进程的寄存器(注,实际上恢复的是目标进程的内核线程)

这里有件事情需要注意,调度器线程调用了swtch函数,但是我们从swtch函数返回时,实际上是返回到了对于switch的另一个调用,而不是调度器线程中的调用。我们返回到的是pid为目的进程的进程在很久之前对于switch的调用。这里可能会有点让人困惑,但是这就是线程切换的核心。

4. 回到用户空间

最后的返回是利用了trapret

  1. # Return falls through to trapret...
  2. .globl trapret
  3. trapret:
  4. popal
  5. popl %gs
  6. popl %fs
  7. popl %es
  8. popl %ds
  9. addl $0x8, %esp # trapno and errcode
  10. iret

这个函数把保存的trapframe恢复。最后通过iret恢复到用户空间

5. 看一下fork、wait、exit函数

1. fork函数

  1. 创建一个进程
  2. 把父进程的页表copy过来(这里还不是cow方式的)
  3. 这里比较重要的点是先加锁。然后把子进程的状态设置成runnable。如果在解锁之前子进程就被调度的话。那返回值就是利用tf->eax来获取
  4. 否则的话解锁return父进程的pid,表示从父进程返回
  1. // Create a new process copying p as the parent.
  2. // Sets up stack to return as if from system call.
  3. // Caller must set state of returned proc to RUNNABLE.
  4. int
  5. fork(void)
  6. {
  7. int i, pid;
  8. struct proc *np;
  9. struct proc *curproc = myproc();
  10. // Allocate process.
  11. if((np = allocproc()) == 0){
  12. return -1;
  13. }
  14. // Copy process state from proc.
  15. if((np->pgdir = copyuvm(curproc->pgdir, curproc->sz)) == 0){
  16. kfree(np->kstack);
  17. np->kstack = 0;
  18. np->state = UNUSED;
  19. return -1;
  20. }
  21. np->sz = curproc->sz;
  22. np->parent = curproc;
  23. *np->tf = *curproc->tf;
  24. // Clear %eax so that fork returns 0 in the child.
  25. np->tf->eax = 0;
  26. for(i = 0; i < NOFILE; i++)
  27. if(curproc->ofile[i])
  28. np->ofile[i] = filedup(curproc->ofile[i]);
  29. np->cwd = idup(curproc->cwd);
  30. safestrcpy(np->name, curproc->name, sizeof(curproc->name));
  31. pid = np->pid;
  32. acquire(&ptable.lock);
  33. np->state = RUNNABLE;
  34. release(&ptable.lock);
  35. return pid;
  36. }

2. wait函数

  1. 如果找到了处于ZOMBIE状态子进程会把他释放掉。(分别释放对于的pid、内核栈、页表)
  2. 否则如果没有子进程则return -1
  3. 否则调用slepp函数等待
  1. // Wait for a child process to exit and return its pid.
  2. // Return -1 if this process has no children.
  3. int
  4. wait(void)
  5. {
  6. struct proc *p;
  7. int havekids, pid;
  8. struct proc *curproc = myproc();
  9. acquire(&ptable.lock);
  10. for(;;){
  11. // Scan through table looking for exited children.
  12. havekids = 0;
  13. for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
  14. if(p->parent != curproc)
  15. continue;
  16. havekids = 1;
  17. if(p->state == ZOMBIE){
  18. // Found one.
  19. pid = p->pid;
  20. kfree(p->kstack);
  21. p->kstack = 0;
  22. freevm(p->pgdir);
  23. p->pid = 0;
  24. p->parent = 0;
  25. p->name[0] = 0;
  26. p->killed = 0;
  27. p->state = UNUSED;
  28. release(&ptable.lock);
  29. return pid;
  30. }
  31. }
  32. // No point waiting if we don't have any children.
  33. if(!havekids || curproc->killed){
  34. release(&ptable.lock);
  35. return -1;
  36. }
  37. // Wait for children to exit. (See wakeup1 call in proc_exit.)
  38. sleep(curproc, &ptable.lock); //DOC: wait-sleep
  39. }
  40. }

sleep函数会在后面讲锁的时候去看

3. exit函数

  1. 首先exit函数关闭了所有已打开的文件。这里可能会很复杂,因为关闭文件系统中的文件涉及到引用计数,虽然我们还没学到但是这里需要大量的工作。不管怎样,一个进程调用exit系统调用时,会关闭所有自己拥有的文件。
  2. 进程有一个对于当前目录的记录,这个记录会随着你执行cd指令而改变。在exit过程中也需要将对这个目录的引用释放给文件系统。
  3. 如果这个想要退出的进程,它又有自己的子进程,接下来需要设置这些子进程的父进程为init进程。我们接下来会看到,每一个正在exit的进程,都有一个父进程中的对应的wait系统调用。父进程中的wait系统调用会完成进程退出最后的几个步骤。所以如果父进程退出了,那么子进程就不再有父进程,当它们要退出时就没有对应的父进程的wait。所以在exit函数中,会为即将exit进程的子进程重新指定父进程为init进程,也就是PID为1的进程。
  4. 最后把要exit的进程状态设置成ZOMBIE
  5. 执行sched函数重新回到内核线程。。。找新的线程去执行
  1. void
  2. exit(void)
  3. {
  4. struct proc *curproc = myproc();
  5. struct proc *p;
  6. int fd;
  7. if(curproc == initproc)
  8. panic("init exiting");
  9. // Close all open files.
  10. for(fd = 0; fd < NOFILE; fd++){
  11. if(curproc->ofile[fd]){
  12. fileclose(curproc->ofile[fd]);
  13. curproc->ofile[fd] = 0;
  14. }
  15. }
  16. begin_op();
  17. iput(curproc->cwd);
  18. end_op();
  19. curproc->cwd = 0;
  20. acquire(&ptable.lock);
  21. // Parent might be sleeping in wait().
  22. wakeup1(curproc->parent);
  23. // Pass abandoned children to init.
  24. for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
  25. if(p->parent == curproc){
  26. p->parent = initproc;
  27. if(p->state == ZOMBIE)
  28. wakeup1(initproc);
  29. }
  30. }
  31. // Jump into the scheduler, never to return.
  32. curproc->state = ZOMBIE;
  33. sched();
  34. panic("zombie exit");
  35. }

4. kill函数

最后我想看的是kill系统调用。Unix中的一个进程可以将另一个进程的ID传递给kill系统调用,并让另一个进程停止运行。如果我们不够小心的话,kill一个还在内核执行代码的进程,会有风险,比如我们想要杀掉的进程的内核线程还在更新一些数据,比如说更新文件系统,创建一个文件。如果这样的话,我们不能就这样杀掉进程,因为这样会使得一些需要多步完成的操作只执行了一部分。所以kill系统调用不能就直接停止目标进程的运行。实际上,在XV6和其他的Unix系统中,kill系统调用基本上不做任何事情。

  1. // Kill the process with the given pid.
  2. // Process won't exit until it returns
  3. // to user space (see trap in trap.c).
  4. int
  5. kill(int pid)
  6. {
  7. struct proc *p;
  8. acquire(&ptable.lock);
  9. for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
  10. if(p->pid == pid){
  11. p->killed = 1;
  12. // Wake process from sleep if necessary.
  13. if(p->state == SLEEPING)
  14. p->state = RUNNABLE;
  15. release(&ptable.lock);
  16. return 0;
  17. }
  18. }
  19. release(&ptable.lock);
  20. return -1;
  21. }

xv6学习笔记(4) : 进程调度的更多相关文章

  1. XV6学习笔记(1) : 启动与加载

    XV6学习笔记(1) 1. 启动与加载 首先我们先来分析pc的启动.其实这个都是老生常谈了,但是还是很重要的(也不知道面试官考不考这玩意), 1. 启动的第一件事-bios 首先启动的第一件事就是运行 ...

  2. XV6学习笔记(2) :内存管理

    XV6学习笔记(2) :内存管理 在学习笔记1中,完成了对于pc启动和加载的过程.目前已经可以开始在c语言代码中运行了,而当前已经开启了分页模式,不过是两个4mb的大的内存页,而没有开启小的内存页.接 ...

  3. xv6学习笔记(3):中断处理和系统调用

    xv6学习笔记(3):中断处理和系统调用 1. tvinit函数 这个函数位于main函数内 表明了就是设置idt表 void tvinit(void) { int i; for(i = 0; i & ...

  4. xv6学习笔记(5) : 锁与管道与多cpu

    xv6学习笔记(5) : 锁与管道与多cpu 1. xv6锁结构 1. xv6操作系统要求在内核临界区操作时中断必须关闭. 如果此时中断开启,那么可能会出现以下死锁情况: 进程A在内核态运行并拿下了p ...

  5. Linux System Programming 学习笔记(六) 进程调度

    1. 进程调度 the process scheduler is the component of a kernel that selects which process to run next. 进 ...

  6. 《Linux内核设计与实现》第四章学习笔记——进程调度

                                                                        <Linux内核设计与实现>第四章学习笔记——进程调 ...

  7. 操作系统学习笔记(五)--CPU调度

    由于第四章线程的介绍没有上传视频,故之后看书来补. 最近开始学习操作系统原理这门课程,特将学习笔记整理成技术博客的形式发表,希望能给大家的操作系统学习带来帮助.同时盼望大家能对文章评论,大家一起多多交 ...

  8. Android动画学习笔记-Android Animation

    Android动画学习笔记-Android Animation   3.0以前,android支持两种动画模式,tween animation,frame animation,在android3.0中 ...

  9. MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: PC bootstrap

    Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的:        ...

随机推荐

  1. 『心善渊』Selenium3.0基础 — 28、unittest中测试套件的使用

    目录 1.测试套件的作用 2.使用测试套件 (1)入门示例 (2)根据不同的条件加载测试用例(了解) (3)常用方式(推荐) 1.测试套件的作用 在我们实际工作,使用unittest框架会有两个问题: ...

  2. YAOI Round #1 题解

    前言 比赛网址:http://47.110.12.131:9016/contest/3 总体来说,这次比赛是有一定区分度的, \(\text{ACM}\) 赛制也挺有意思的. 题解 A. 云之彼端,约 ...

  3. python 爬取网络小说 清洗 并下载至txt文件

    什么是爬虫 网络爬虫,也叫网络蜘蛛(spider),是一种用来自动浏览万维网的网络机器人.其目的一般为编纂网络索引. 网络搜索引擎等站点通过爬虫软件更新自身的网站内容或其对其他网站的索引.网络爬虫可以 ...

  4. [刘阳Java]_处理并发有哪些方法

    1.HTML静态化 ,将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素2.禁止重复提交:用户提交之后按钮置灰,禁止重复提交3.用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取 ...

  5. [iconfont_dart]帮你快速生成Icon,再也不用手动写Icon方法

    iconfont_dart iconfont to dart.Icon can be implemented by calling iconfont classname. iconfont转dart. ...

  6. linux查看电脑温度

    sudo apt-get install lm-sensors # 安装yes | sudo sensors-detect # 侦测所有感测器 sensors # 查看温度

  7. 网络损伤仪WANsim中的乱序功能

    乱序 乱序功能需要指定每个帧 发生乱序的概率,以及新的帧的位置相较于原来位置的时间范围. 乱序的概率范围是0%~20%,颗粒度是0.001%.Delay的设置范围为 0s~10s,颗粒度为0.1 ms ...

  8. 使用Maven打包可运行jar和javaagent.jar的区别

    简介 javaagent 是 Java1.5 之后引入的新特性,其主要作用是在class被加载之前对其拦截,以插入我们的字节码. java1.5 之前使用的是JVMTI(jvm tool interf ...

  9. django中路由配置的正则

    在django中配置路由遇到正则的坑: django2.x版本中使用re_path来进行正则表达式的匹配 用法如下: from Django.urls import re.path(导入re_path ...

  10. pointnet.pytorch代码解析

    pointnet.pytorch代码解析 代码运行 Training cd utils python train_classification.py --dataset <dataset pat ...