cpp
void
exit(int status)
{
struct proc *p = myproc();
if(p == initproc)
panic("init exiting");
// Close all open files.
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;
acquire(&wait_lock);
// Give any children to init.
reparent(p);
// Parent might be sleeping in wait().
wakeup(p->parent);
acquire(&p->lock);
p->xstate = status;
p->state = ZOMBIE;
release(&wait_lock);
// Jump into the scheduler, never to return.
sched();
panic("zombie exit");
}
这段代码是 exit
函数的实现,用于让一个进程安全退出。在 xv6
中,exit
会进行一系列资源清理,确保不会留下未释放的文件或子进程。下面是逐步的解释:
exit
函数的参数status
表示退出状态码,可以供父进程在wait()
中获取。myproc()
用于获取当前调用exit
的进程(p
)。
cpp
if(p == initproc)
panic("init exiting");
- 检查当前进程是否是
initproc
。initproc
是系统中第一个用户进程(根进程),所有孤儿进程都会被重新分配给它。 - 如果
initproc
尝试退出,系统会触发panic
,因为initproc
不能被终止。
status
参数和 wait()
函数
-
status
参数 :当一个进程调用exit(status)
时,status
表示这个进程退出时的状态码。你可以把它理解为这个进程告诉外界自己是"正常退出"还是"因为某种错误退出"。这个状态码会被保存在进程的xstate
属性中。 -
wait()
函数 :父进程可以调用wait()
来等待子进程结束,并获取子进程的status
值。这样,父进程就能知道子进程是"正常退出"还是"出错退出"。
myproc()
函数
myproc()
:这个函数返回的是当前执行exit
函数的进程。在这段代码中,struct proc *p = myproc();
就是把当前退出的进程记录在变量p
中,以便后面释放资源和处理子进程的继承关系。
检查是否是 initproc
-
什么是
initproc
:initproc
是系统中第一个用户进程,相当于"根"进程。它是所有进程的"祖先",并且不能被终止,因为它有一个特别的职责:收养其他进程的孤儿。 -
为什么要检查
initproc
:如果initproc
退出,系统就无法正常工作,因为没有它去收养孤儿进程。孤儿进程会在系统中变成"僵尸进程"(资源没释放干净),最终可能导致系统资源耗尽。所以,如果initproc
尝试退出,panic("init exiting")
会触发系统崩溃,以防止更严重的问题。
这部分代码总结
status
参数:告诉父进程子进程是如何退出的(正常还是错误)。myproc()
:获取当前要退出的进程。initproc
不能退出:因为它是所有孤儿进程的"父亲",如果它退出了,系统会失去一个重要的功能,无法正常运行。
cpp
// Close all open files.
for(int fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd]){
struct file *f = p->ofile[fd];
fileclose(f);
p->ofile[fd] = 0;
}
}
- 关闭所有打开的文件描述符:
- 遍历
p->ofile
中的每个文件描述符(最大数量为NOFILE
)。 - 对于每个打开的文件描述符,调用
fileclose(f)
,释放文件资源。 - 将
p->ofile[fd]
设置为0
,表示该文件描述符已关闭。
- 遍历
cpp
begin_op();
iput(p->cwd);
end_op();
p->cwd = 0;
- 释放进程的当前工作目录(
p->cwd
)。iput(p->cwd)
将当前目录的引用计数减少 1,如果引用计数降为 0,则释放该目录的资源。p->cwd = 0
清空当前工作目录指针。
cpp
acquire(&wait_lock);
获取 wait_lock
锁,以保护父子进程之间的关系。wait_lock
确保在修改子进程状态时,父进程不会进入等待或退出的竞态条件。
cpp
// Give any children to init.
reparent(p);
- 将当前进程的所有子进程的父进程指针重新指向
initproc
,避免子进程成为孤儿。 reparent
函数会遍历所有进程,找到以p
为父进程的子进程,并将它们的parent
属性设置为initproc
。
cpp
// Parent might be sleeping in wait().
wakeup(p->parent);
唤醒父进程,以便它可以调用 wait()
获取当前进程的退出状态。
cpp
acquire(&p->lock);
p->xstate = status;
p->state = ZOMBIE;
- 获取当前进程的锁。
- 将进程的
xstate
(退出状态)设置为status
,记录退出码。 - 将进程状态设置为
ZOMBIE
,表示进程已退出但尚未被父进程回收。
cpp
release(&wait_lock);
释放 wait_lock
,允许其他进程修改父子关系。
cpp
// Jump into the scheduler, never to return.
sched();
panic("zombie exit");
}
- 调用
sched()
将当前进程调度出去,切换到其他进程。sched
会在当前进程被回收前保留其退出状态。 panic("zombie exit");
是一个安全机制,通常不会执行到。它提醒系统如果sched()
出现异常,则报错,防止僵尸进程未被正确回收。
总结
exit
函数负责让进程正常退出,释放文件和目录等资源,确保子进程不变成孤儿,并将退出状态通知父进程。通过将进程状态设置为 ZOMBIE
,系统可以在 wait()
中正确回收它,避免内存泄漏和资源浪费。
cpp
void
exit(int status)
{
struct proc *p = myproc();
if(p == initproc)
panic("init exiting");
// Close all open files.
for(int fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd]){
struct file *f = p->ofile[fd];
fileclose(f);
p->ofile[fd] = 0;
}
}
//TODO
for(int i=0; i<VMASIZE; i++)
{
uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len/PGSIZE, 1);
}
//TODO end
begin_op();
iput(p->cwd);
end_op();
p->cwd = 0;
acquire(&wait_lock);
// Give any children to init.
reparent(p);
// Parent might be sleeping in wait().
wakeup(p->parent);
acquire(&p->lock);
p->xstate = status;
p->state = ZOMBIE;
release(&wait_lock);
// Jump into the scheduler, never to return.
sched();
panic("zombie exit");
}
在这段修改后的代码中,增加了一部分处理 VMA
(虚拟内存区域)的代码,位于 TODO
部分。这段代码的作用是在进程退出时,释放该进程分配的所有虚拟内存映射区域。
cpp
for(int i = 0; i < VMASIZE; i++)
{
uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len / PGSIZE, 1);
}
- 作用 :遍历并解除进程的每个
VMA
的内存映射,确保这些内存区域在进程退出时被正确释放。 - 细节 :
for(int i = 0; i < VMASIZE; i++)
:遍历当前进程的所有 VMA 区域。uvmunmap(p->pagetable, p->vma[i].addr, p->vma[i].len / PGSIZE, 1)
:p->pagetable
:表示进程的页表。p->vma[i].addr
:表示当前VMA
的起始虚拟地址。p->vma[i].len / PGSIZE
:表示VMA
的页数(len
是字节数,除以PGSIZE
可以得到页的数量)。1
:最后一个参数1
表示解除映射时还要释放对应的物理内存。
为什么需要新增这段代码?
- 释放虚拟内存映射区域 :在进程退出时,如果不释放
VMA
,这些虚拟内存区域的映射关系和物理内存可能会被遗留在系统中,导致内存泄漏。 - 确保资源回收 :
exit
函数的目的是彻底清理退出进程的所有资源,包括文件、目录、内存等。新增的这段VMA
释放代码确保了退出进程不留有任何未回收的映射区域。
内存在什么情况下会泄露?
内存泄漏(Memory Leak)指的是程序在运行过程中分配了内存资源,但在不再需要使用它们时没有正确释放,从而导致这些内存资源无法被重用。内存泄漏会导致系统中可用内存逐渐减少,最终可能引发性能下降或系统崩溃。
1. 动态分配的内存未释放
使用 malloc
、kalloc
等函数动态分配了内存,但在使用完毕后没有调用 free
、kfree
等释放函数,导致这些内存资源无法回收。例如:
cpp
int *ptr = malloc(sizeof(int) * 100); // 分配内存
// ... 使用 ptr ...
// 忘记调用 free(ptr); 释放内存
如果在 exit
时没有释放进程分配的所有内存,就可能导致这些内存块无法再被使用,形成内存泄漏。
2. 长生命周期的对象持有无用内存
某些对象(如全局变量或静态变量)持有了本应释放的内存,使得该内存无法被回收。例如,一个全局列表不断存储新对象,而对象删除后列表并未清理,那么列表中指向的内存块会一直存在。
3. 循环引用
两个对象互相引用对方,导致垃圾回收机制(在支持垃圾回收的语言中)无法判断它们不再使用。例如:
cpp
struct Node {
struct Node *next;
struct Node *prev;
};
如果两个 Node
互相指向对方,即使程序不再需要它们,也无法自动回收这部分内存。
4. 未关闭的文件映射(例如 mmap)
使用 mmap
等内存映射技术将文件映射到进程地址空间,但在不再使用该映射时没有调用 munmap
来解除映射。这样做会导致文件映射所占的内存无法释放,进而导致内存泄漏。在操作系统内核中,例如 exit
函数中,如果没有释放虚拟内存区域 VMA
,这些内存映射将被遗留在内存中,形成泄漏。
5. 线程或进程未退出清理
一个线程或进程在退出时没有清理它所分配的内存资源。例如,如果一个子进程退出时没有正确释放分配的资源(例如打开的文件、分配的内存),这些资源就会泄漏,直到操作系统将它们强制回收。
6. 异常终止
程序在处理内存资源时发生异常(例如,未捕获的异常、信号中断)导致未执行清理代码(free
或 delete
),从而形成内存泄漏。例如,在使用动态内存分配时,程序突然崩溃,没有机会执行释放内存的代码。
总结
在 C 或 C++ 语言中,程序员必须手动管理内存。如果在动态分配后没有在合适的时机释放,或者程序未正确清理资源,就会导致内存泄漏。操作系统的内核代码(例如 exit
函数)中特别要小心,确保进程或线程退出时将所有分配的资源、内存和文件描述符都正确地释放,以防止内存泄漏。
在这里,内存泄漏 通俗地讲,就是进程退出时没有把自己占用的内存还给系统,导致系统认为这块内存还在使用中,但实际上已经没人再需要它了。
假设每个进程是一位借用图书馆资源的"租客":
- 当租客(进程)在图书馆工作时,他需要占用一些资源(比如座位、书本)。
- 租客离开时(进程退出),他应该把占用的资源归还,方便其他人使用。
内存泄漏的情况就像:
- 租客离开了,却没有归还座位或书籍,导致这些资源"被占用"而无法让其他人使用。
在 exit
函数中,如果进程退出时不释放它分配的内存块或映射的虚拟内存区域(VMA),系统的内存中会有一块区域持续被标记为"被占用",但实际上再也没人会用到它。这就是"泄漏"------内存还占在那里,却没法再利用。随着这样的"遗留物"越来越多,系统的可用内存会逐渐减少,影响性能,甚至导致系统崩溃。