[xv6]:wait-exit-kill

1. wait

xv6中并没有实现Linux中的信号(signal)机制,当一个子进程终止时,如果其父进程还未终止,那么其会将自己的state设置为ZOMBIE,

然后wakeup()正处于wait()状态的父进程,父进程遍历进程表,找到stateZOMBIE的子进程,然后释放其资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/** @param addr 子进程状态写入的地址
*/
int
wait(uint64 addr)
{
struct proc *np;
int havekids, pid;
struct proc *p = myproc();

// hold p->lock for the whole time to avoid lost
// wakeups from a child's exit().
// 获取p->lock避免丢失唤醒
acquire(&p->lock);

for(;;){
// Scan through table looking for exited children.
havekids = 0;
for(np = proc; np < &proc[NPROC]; np++){
// this code uses np->parent without holding np->lock.
// acquiring the lock first would cause a deadlock,
// since np might be an ancestor, and we already hold p->lock.
// 这里没有使用np->lock因为在扫描进程表过程中
// 可能会扫描到p的祖先(ap),如果ap也正在使用wait, 那么它就会持有ap->lock(417行)
// 此时就会发生deadlock
if(np->parent == p){
// np->parent can't change between the check and the acquire()
// because only the parent changes it, and we're the parent.
acquire(&np->lock);
havekids = 1;
// 检查处于ZOMBIE状态的子进程,将其回收
if(np->state == ZOMBIE){
// Found one.
pid = np->pid;
if(addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate,
sizeof(np->xstate)) < 0) {
release(&np->lock);
release(&p->lock);
return -1;
}
// 释放子进程的最后资源
freeproc(np);
release(&np->lock);
release(&p->lock);
return pid;
}
release(&np->lock);
}
}

// No point waiting if we don't have any children.
if(!havekids || p->killed){
release(&p->lock);
return -1;
}

// Wait for a child to exit.
// 子进程还未退出, sleep等待
// sleep在自己身上, 即p->chan = p
sleep(p, &p->lock); //DOC: wait-sleep
}
}

2. exit

当一个进程退出时,需要让init进程收养自己的子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 一个子进程退出之后会变为僵尸进程, 直到父进程调用wait()将其回收
// exit()并没有释放子进程的所有资源,因为其正在运行当中,如果贸然释放
// 会产生很多问题, 因为等子进程退出之后,再由父进程的wait()来释放子进程的资源
void
exit(int status)
{
struct proc *p = myproc();

if(p == initproc)
panic("init exiting");

// Close all open files.
// 1.关闭所有的打开文件
for(int fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd]){
struct file *f = p->ofile[fd];
fileclose(f);
p->ofile[fd] = 0;
}
}

// 进程对与当前目录的一个引用,需要将其释放给文件系统
// 因为文件系统中使用了引用计数
begin_op();
iput(p->cwd);
end_op();
p->cwd = 0;

// we might re-parent a child to init. we can't be precise about
// waking up init, since we can't acquire its lock once we've
// acquired any other proc lock. so wake up init whether that's
// necessary or not. init may miss this wakeup, but that seems
// harmless.
acquire(&initproc->lock);
wakeup1(initproc);
release(&initproc->lock);

// grab a copy of p->parent, to ensure that we unlock the same
// parent we locked. in case our parent gives us away to init while
// we're waiting for the parent lock. we may then race with an
// exiting parent, but the result will be a harmless spurious wakeup
// to a dead or wrong process; proc structs are never re-allocated
// as anything else.
// 这里是为了防止子进程与父进程同时退出
acquire(&p->lock);
struct proc *original_parent = p->parent;
release(&p->lock);

// we need the parent's lock in order to wake it up from wait().
// the parent-then-child rule says we have to lock it first.
//
acquire(&original_parent->lock);

acquire(&p->lock);

// Give any children to init.
// 让init进程收养当前进程的子进程
reparent(p);

// Parent might be sleeping in wait().
// 唤醒父进程
wakeup1(original_parent);

p->xstate = status;
p->state = ZOMBIE;

// 释放父进程的锁
release(&original_parent->lock);

// Jump into the scheduler, never to return.
// 此时进程的资源还没有完全释放,进入到调度器线程
sched();
// 由于进程的state为ZOMBIE, 因此其不会被调度
panic("zombie exit");
}

3. kill

在xv6中, kill所做的事其实十分少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int
kill(int pid)
{
struct proc *p;

for(p = proc; p < &proc[NPROC]; p++){
acquire(&p->lock);
if(p->pid == pid){
// 这里基本上没有干什么事情
p->killed = 1;
if(p->state == SLEEPING){
// Wake process from sleep().
p->state = RUNNABLE;
}
release(&p->lock);
return 0;
}
release(&p->lock);
}
return -1;
}

只有短短20行,可以看出,kill并没有直接杀死进程,因为当对一个进程执行kill操作的时候,进程可能正在更新某些数据,也可能正在创建一个文件,它们还可能持有锁,因此,直接杀死进程会导致一系列问题

xv6中的做法十分温和,仅仅是将进程的killed标志位置为了1,但是对于处于SLEEPING状态的进程有着特殊处理,接下来会说
通过将进程的killed标志位置为1, 然后在一些安全的地方killed标志位进行检查,这样可以确保进程安全的退出

来看一个例子:

  • trap.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    void
    usertrap(void)
    {
    ...

    if(r_scause() == 8){
    // system call
    // 第一处
    if(p->killed)
    exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
    } else if((which_dev = devintr()) != 0){
    // ok
    } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
    }

    // 如果进程发现killed标志位为1,会自愿调用exit()退出
    // 在这里,进程并没有持有任何的锁
    // 第二处
    if(p->killed)
    exit(-1);
    ...
    }

    usertrap当中,有两处这样的killed标志位检查, 当程序运行到这时,是没有锁的,并且也没有什么更新动作,即安全

    所以,通常,当kill()“杀死”进程后,该进程通常不会立即死亡,而是会在下一次的某个系统调用/计时器中断/设备中断时自愿的调用exit()退出

特殊的SLEEPING

设想这样一种情况,当一个进程正在读取控制台输入,那么它会进入SLEEPING状态,而用户kill()该进程之后就没有继续理这个进程了,因此该进程就一直不会退出,因为其没有陷入trap, 所以,xv6的代码对SLEEPING状态的进程做了特殊处理,使其变为RUNNABLE,在接下来的某个时刻,中断调度线程就会调度该进程,该进程便会从sleep()中返回,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int
piperead(struct pipe *pi, uint64 addr, int n)
{
int i;
struct proc *pr = myproc();
char ch;

acquire(&pi->lock);
// 没有数据可读
while(pi->nread == pi->nwrite && pi->writeopen){ //DOC: pipe-empty
// *************************************************//
if(pr->killed){
release(&pi->lock);
return -1;
}
//*************************************************//
sleep(&pi->nread, &pi->lock); //DOC: piperead-sleep
}
// 每字节读取数据到addr
for(i = 0; i < n; i++){ //DOC: piperead-copy
if(pi->nread == pi->nwrite)
break;
ch = pi->data[pi->nread++ % PIPESIZE];
if(copyout(pr->pagetable, addr + i, &ch, 1) == -1)
break;
}
// 读取完毕之后唤醒writer进程
wakeup(&pi->nwrite); //DOC: piperead-wakeup
release(&pi->lock);
return i;
}

在上面的例子中,当进程从sleep()中返回之后,便会重新进入循环,通常情况下仍然没有数据可读,进而return -1

然后回到usertrap()syscall()处,最后检查到killed标志位为1, 自愿exit()


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!