XV6 Code Reading report-TOPIC3
@ Xiaojiannan 20111013223
- XV6 Code Reading report-TOPIC3
- 1. Order
- 2. Context Switches
- 3. Process scheduling
- 4. Piping
- 5. Process scheduling Process
- 6. Pipe Implementation Overview
- 7. Reading experience
1. Order
Xv6 need to solve a series of problems in order to realize CPU multi-process. 1. How do I switch between processes? 2. How to make all this transparent? 3. Lock mechanism is required to avoid competition. 4. Automatic release of memory and resources.
Xv6 through the implementation of context switching (contextual switching), time interrupt processing, lock, sleep and wake mechanisms to solve the above problems. The main code includes Swtch. S, Defs.h, proc.h, PROC.C, mmu.h and other documents.
The above files are analyzed by module.
2. Context Switch 2.1 defs.h
Before everything else, let's look at the definition of DEFS.H's structure and the declaration of the function.
In this file, a series of functions and structures are declared, and for the parts that need to be discussed later in this chapter, you need to focus on the struct context
struct proc
structure.
struct context
Defined in Proc.h (40-56). Its structure is actually a value of five registers. That is, when context switches, the main thing to do is to save and update the register value. Also by convention, the caller will save %eax,%ecx,%edx
the value.
struct context { uint edi; uint esi; uint ebx; uint ebp; uint eip;};
By the way, the structure of proc and pipe is also analyzed.
struct proc
As defined in Proc.h (60-75), the state of each process is recorded through a struct.
struct proc { uint sz; // 进程的内存大小(以byte计) pde_t* pgdir; // 进程页路径的线性地址。 char *kstack; // 进程的内核栈底 enum procstate state; // 进程状态 volatile int pid; // 进程ID struct proc *parent; // 父进程 struct trapframe *tf; // 当前系统调用的中断帧 struct context *context; // 进程运行的入口 int killed; // 当非0时,表示已结束 struct file *ofile[NOFILE]; // 打开的文件列表 struct inode *cwd; // 进程当前路径 char name[16]; // 进程名称};
Pipe relies on the definition of structural body spinlock,cpu, see Spinlock.h and Proc.h (11-24).
The role of Spinlock is that when a process requests a lock that is being occupied, the process is in a loop check, waiting for the lock to be released.
struct spinlock { uint locked; // 锁是否处于锁住状态 // For debugging: char *name; // 锁名称 struct cpu *cpu; // 占有该锁的CPU信息 uint pcs[10]; // 占有该锁的指令栈};
pipe
structure is defined in pipe.h (12-19),
struct pipe { struct spinlock lock; char data[PIPESIZE]; // 保存pipe的内容,PIPESIZE为512 uint nread; // 读取的byte长度 uint nwrite; // 写入的byte长度 int readopen; // 是否正在读取 int writeopen; // 是否正在写入};
2.2 Swtch. S
The purpose of this file is to implement the Swtch function using assembly code,
.globl swtchswtch: # 将需要保存的context地址读取到%esp中,新context地址读取到%edx中 # 4(%esp)对应的是需要保存的context # 8(%esp)对应的是新的context movl 4(%esp), %eax movl 8(%esp), %edx # 将寄存器中过期的数值压栈 pushl %ebp pushl %ebx pushl %esi pushl %edi # 交换栈 movl %esp, (%eax) # 保存需要保存的context地址 movl %edx, %esp # 读取新的context信息 # 加载新的context信息 popl %edi popl %esi popl %ebx popl %ebp ret
3. Process scheduling
The main function of process scheduling is focused on PROC.C, let's start with this file.
For a single CPU, scheduler is the most important function. When the CPU is initialized, called Scheduler (), the loop selects a process execution from the process queue and, when the process finishes, transfers control through Swtch () to scheduler.
voidscheduler(void){ struct proc *p; for(;;){ // 在每次执行一个进程之前,需要调用sti()函数开启CPU的中断 sti(); // 遍历进程表找到一个进程执行 acquire(&ptable.lock); // 获取进程表的锁,避免其他CPU更改进程表 for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ // 如果进程的状态为不可运行,则略过 if(p->state != RUNNABLE) continue; // 切换到选择的进程,释放进程表锁,当进程结束时,再重新获取 proc = p; switchuvm(p); p->state = RUNNING; swtch(&cpu->scheduler, proc->context); switchkvm(); // Process is done running for now. // It should have changed its p->state before coming back. proc = 0; } release(&ptable.lock); }}
After each loop, the process table lock is released in a timely manner, which prevents the process table from being locked out of the CPU while the other CPUs are inaccessible when there are no programs in the process table that can be run temporarily. In one case, when the process waits for IO, it is not runnable, and the CPU is idle, the process table lock is occupied, and the IO signal is unreachable.
Sched () switches to CPU context and makes a series of judgements before switching to the context to avoid collisions.
voidsched(void){ int intena; // 是否获取到了进程表锁 if(!holding(&ptable.lock)) panic("sched ptable.lock"); // 是否执行过pushcli if(cpu->ncli != 1) panic("sched locks"); // 执行的程序应该处于结束或者睡眠状态 if(proc->state == RUNNING) panic("sched running"); // 判断中断是否可以关闭 if(readeflags()&FL_IF) panic("sched interruptible"); intena = cpu->intena; // 上下文切换至scheduler swtch(&proc->context, cpu->scheduler); cpu->intena = intena;}
The yield () function voluntarily yields a scheduling period (scheduling round), which is called in the current version of XV6, only in Trap (), see TRAP.C (100). The actual application is that when a process is using the CPU while the interrupt is open, you need to view the Nlock.
voidyield(void){ // 获取进程表锁 acquire(&ptable.lock); // 将进程状态设为可运行,以便下次遍历时可以被唤醒 proc->state = RUNNABLE; // 执行sched函数,准备将CPU切换到scheduler context sched(); // 释放进程表锁 release(&ptable.lock);}
Sleep and Wakeup are two complementary functions that work together to change the order in which processes are executed,
The sleep function has two parameters void *chan
and a struct spinlock *lk
.
voidsleep(void *chan, struct spinlock *lk){ if(proc == 0) panic("sleep"); if(lk == 0) panic("sleep without lk"); // 释放锁lk if(lk != &ptable.lock){ //DOC: sleeplock0 acquire(&ptable.lock); //DOC: sleeplock1 release(lk); } // 更改状态为SLEEPING,并切换至CPU context proc->chan = chan; proc->state = SLEEPING; sched(); // Tidy up. proc->chan = 0; // 重新获得刚刚释放的lk锁 if(lk != &ptable.lock){ //DOC: sleeplock2 release(&ptable.lock); acquire(lk); }}
It is important to note that to make the process go to sleep requires two locks, LK and ptable.lock, because it has been ptable.lock, so wakeup will not execute during this period, until the process completely into sleep, so lk this lock can be released.
The body part of the wakeup function is in the WAKEUP1 function.
voidwakeup(void *chan){ // 先获取ptable.lock,确保sleep不会执行,避免出现missed wakeup acquire(&ptable.lock); wakeup1(chan); // 唤醒结束,释放ptable.lock release(&ptable.lock);}
The WAKEUP1 function completes the main work of awakening. Wakeup1 and Wakeup as two independent functions, because in addition to being called by the wakeup, but also called in exit, will be described in detail later.
static voidwakeup1(void *chan){ struct proc *p; // 遍历进程表,当发现有符合运行条件的程序时,将其标记为RUNNABLE for(p = ptable.proc; p < &ptable.proc[NPROC]; p++) if(p->state == SLEEPING && p->chan == chan) p->state = RUNNABLE;}
The wait function is used for the parent process to wait for the child process to end, or 1 if there are no child processes, otherwise the PID of the child process that has ended is returned.
intwait (void) {struct proc *p; int Havekids, PID; Gets the process table lock acquire (&ptable.lock); for (;;) {//Traversal lookup whether there are child processes in the zombie state havekids = 0; for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {if (p->parent! = proc) continue; If a child process is found havekids = 1; If the process state is ZOMBIE, it is freed and returns the PID if (p->state = = ZOMBIE) {//Found one of the child process. PID = p->pid; Kfree (P->kstack); P->kstack = 0; FREEVM (P->pgdir); P->state = UNUSED; p->pid = 0; p->parent = 0; P->name[0] = 0; p->killed = 0; Release (&ptable.lock); return PID; }}//If no child process is returned directly if (!havekids | | proc->killed) {release (&ptable.lock); return-1; }//If a child process is asleep, the parent process is put to sleep (proc, &ptable.lock); Doc:wait-sleep}}
Where there is still a child process sleep, and does not release Ptable.lock, because the release operation is placed in the sleep function, and satisfies the sleep function call conditions, obtain ptable.lock beforehand.
Exit () completes the resource release at the end of the process and the processing of the child processes. There is only one acquire operation, which can make the process end operation Atomized, and there may be multiple wakeup1 operations, which reduces the time. After the end, there is no active call release, because sched to the context switching need to get Ptable.lock, release in scheduler.
voidexit(void){ struct proc *p; int fd; if(proc == initproc) panic("init exiting"); // 关闭之前打开的文件 for(fd = 0; fd < NOFILE; fd++){ if(proc->ofile[fd]){ fileclose(proc->ofile[fd]); proc->ofile[fd] = 0; } } iput(proc->cwd); proc->cwd = 0; acquire(&ptable.lock); // 唤醒父进程,一边父进程将处于zombie状态的该进程回收 wakeup1(proc->parent); // 将子进程移交给initproc for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ if(p->parent == proc){ p->parent = initproc; // 如果子进程处于zombie状态,则唤醒其新父亲initproc来料理后事 if(p->state == ZOMBIE) wakeup1(initproc); } } // 移交给scheduler,等待父进程处理 proc->state = ZOMBIE; sched(); panic("zombie exit");}
4. Piping
The pipe structure in XV6 is already mentioned in the previous analysis of Defs.h. The pipe.c is analyzed directly here.
The Pipealloc implements the pipe creation and associates the pipe to two files f0, f1
. Returns 0 if the creation is successful, otherwise 1.
intpipealloc(struct file **f0, struct file **f1){ struct pipe *p; p = 0; *f0 = *f1 = 0; // 如果f0,f1不存在则返回-1 if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0) goto bad; if((p = (struct pipe*)kalloc()) == 0) goto bad; // // 初始化pipe p->readopen = 1; p->writeopen = 1; p->nwrite = 0; p->nread = 0; initlock(&p->lock, "pipe"); (*f0)->type = FD_PIPE; (*f0)->readable = 1; (*f0)->writable = 0; (*f0)->pipe = p; (*f1)->type = FD_PIPE; (*f1)->readable = 0; (*f1)->writable = 1; (*f1)->pipe = p; return 0; // 如果创建失败,则将进度回滚,释放占用的内存、解除对文件的占有 bad: if(p) kfree((char*)p); if(*f0) fileclose(*f0); if(*f1) fileclose(*f1); return -1;}
The Pipeclose realizes the handling of the closed pipe.
voidpipeclose(struct pipe *p, int writable){ // 获取管道锁,避免在关闭的同时进行读写操作 acquire(&p->lock); // 判断是否有未被读取的数据 if(writable){ // 如果存在,则唤醒pipe的读进程;否则唤醒写进程 p->writeopen = 0; wakeup(&p->nread); } else { p->readopen = 0; wakeup(&p->nwrite); } // 当pipe的读写都已结束时,释放资源;否则释放pipe锁 if(p->readopen == 0 && p->writeopen == 0) { release(&p->lock); kfree((char*)p); } else release(&p->lock);}
The Pipewrite implements the write operation of the pipeline.
intpipewrite(struct pipe *p, char *addr, int n){ int i; acquire(&p->lock); // 逐字节写入 for(i = 0; i < n; i++){ // 如果pipe已经写满 while(p->nwrite == p->nread + PIPESIZE) { //DOC: pipewrite-full // 唤醒读进程,写进程进入睡眠,并返回-1 if(p->readopen == 0 || proc->killed){ release(&p->lock); return -1; } wakeup(&p->nread); sleep(&p->nwrite, &p->lock); //DOC: pipewrite-sleep } p->data[p->nwrite++ % PIPESIZE] = addr[i]; } // 写完之后唤醒读进程 wakeup(&p->nread); //DOC: pipewrite-wakeup1 release(&p->lock); return n;}
The Piperead implements the read operation of the pipe.
intpiperead(struct pipe *p, char *addr, int n){ int i; acquire(&p->lock); // 如果pipe已经读空,并且正在写入,则进入睡眠状态 while(p->nread == p->nwrite && p->writeopen){ //DOC: pipe-empty if(proc->killed){ release(&p->lock); return -1; } sleep(&p->nread, &p->lock); //DOC: piperead-sleep } for(i = 0; i < n; i++){ //DOC: piperead-copy if(p->nread == p->nwrite) break; addr[i] = p->data[p->nread++ % PIPESIZE]; } // 读取完毕,唤醒写进程 wakeup(&p->nwrite); //DOC: piperead-wakeup release(&p->lock); // 返回读取的字节长度 return i;}
5. Process scheduling Process
Process switching: After the CPU is booted, execute the scheduler function, infinite loop. In each cycle, a runnable process is found from the process table, which switches to the context of the process, at which time the function starts executing. When the function finishes, call the return function, which switches to the context of the CPU and starts the next loop.
Process wake-up and sleep: If a program needs to wait for IO, the CPU will set it to sleep and cannot be executed at this time. When the IO signal arrives, the executing process sets the process for the IO signal to runnable, which is the wake. During the next scheduler cycle, the process may be executed to process the IO signal.
Process table Lock: For multiprocessor architectures, it is necessary to obtain a table lock in advance of the process table, and then release it when it is finished, thus guaranteeing the atomicity of the process table operation, which avoids the problem of multi-processor competition.
6. Pipe Implementation Overview
The main part of the pipe is actually a small length of continuous data storage, read and write operations as an infinite loop length of memory blocks.
When initialized, associates the given file input and output stream with the struct, freeing the memory when it is closed, and releasing the file.
Read and write operations, the need to determine whether the scope of reading and writing, to avoid overwriting unread data or read the read data, if the write operation is not finished, you need to sleep wake up the way to complete the large segment of data read.
7. Reading experience
Because this part of the code is mainly implemented by C code, because it is relatively simpler than the first reading task. There are two difficulties, one need to understand the structure of the dependency of information, and through the actual code to see the role of each attribute, and two need to combine multiple functions to understand the process table lock management mechanism. The realization mechanism of XV6 is not complicated, and the subjective brain hole is very easy to understand when it is combined with sched.pdf.
XV6 code reading report process scheduling