[OS] 项目三-2-proc.c: exit(int status)

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");
  • 检查当前进程是否是 initprocinitproc 是系统中第一个用户进程(根进程),所有孤儿进程都会被重新分配给它。
  • 如果 initproc 尝试退出,系统会触发 panic,因为 initproc 不能被终止。

status 参数和 wait() 函数

  • status 参数 :当一个进程调用 exit(status) 时,status 表示这个进程退出时的状态码。你可以把它理解为这个进程告诉外界自己是"正常退出"还是"因为某种错误退出"。这个状态码会被保存在进程的 xstate 属性中。

  • wait() 函数 :父进程可以调用 wait() 来等待子进程结束,并获取子进程的 status 值。这样,父进程就能知道子进程是"正常退出"还是"出错退出"。

myproc() 函数

  • myproc() :这个函数返回的是当前执行 exit 函数的进程。在这段代码中,struct proc *p = myproc(); 就是把当前退出的进程记录在变量 p 中,以便后面释放资源和处理子进程的继承关系。

检查是否是 initproc

  • 什么是 initprocinitproc 是系统中第一个用户进程,相当于"根"进程。它是所有进程的"祖先",并且不能被终止,因为它有一个特别的职责:收养其他进程的孤儿

  • 为什么要检查 initproc :如果 initproc 退出,系统就无法正常工作,因为没有它去收养孤儿进程。孤儿进程会在系统中变成"僵尸进程"(资源没释放干净),最终可能导致系统资源耗尽。所以,如果 initproc 尝试退出,panic("init exiting") 会触发系统崩溃,以防止更严重的问题。

这部分代码总结

  1. status 参数:告诉父进程子进程是如何退出的(正常还是错误)。
  2. myproc():获取当前要退出的进程。
  3. 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. 动态分配的内存未释放

使用 mallockalloc 等函数动态分配了内存,但在使用完毕后没有调用 freekfree 等释放函数,导致这些内存资源无法回收。例如:

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. 异常终止

程序在处理内存资源时发生异常(例如,未捕获的异常、信号中断)导致未执行清理代码(freedelete),从而形成内存泄漏。例如,在使用动态内存分配时,程序突然崩溃,没有机会执行释放内存的代码。

总结

在 C 或 C++ 语言中,程序员必须手动管理内存。如果在动态分配后没有在合适的时机释放,或者程序未正确清理资源,就会导致内存泄漏。操作系统的内核代码(例如 exit 函数)中特别要小心,确保进程或线程退出时将所有分配的资源、内存和文件描述符都正确地释放,以防止内存泄漏。

在这里,内存泄漏 通俗地讲,就是进程退出时没有把自己占用的内存还给系统,导致系统认为这块内存还在使用中,但实际上已经没人再需要它了。

假设每个进程是一位借用图书馆资源的"租客":

  • 当租客(进程)在图书馆工作时,他需要占用一些资源(比如座位、书本)。
  • 租客离开时(进程退出),他应该把占用的资源归还,方便其他人使用。

内存泄漏的情况就像:

  • 租客离开了,却没有归还座位或书籍,导致这些资源"被占用"而无法让其他人使用。

exit 函数中,如果进程退出时不释放它分配的内存块或映射的虚拟内存区域(VMA),系统的内存中会有一块区域持续被标记为"被占用",但实际上再也没人会用到它。这就是"泄漏"------内存还占在那里,却没法再利用。随着这样的"遗留物"越来越多,系统的可用内存会逐渐减少,影响性能,甚至导致系统崩溃。

相关推荐
醉の虾21 分钟前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧30 分钟前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm39 分钟前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
asleep7011 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm1 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
旦沐已成舟1 小时前
DevOps-Jenkins-新手入门级
服务器
珹洺1 小时前
C语言数据结构——详细讲解 双链表
c语言·开发语言·网络·数据结构·c++·算法·leetcode
幼儿园的小霸王2 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue
疯狂的沙粒2 小时前
对 TypeScript 中高级类型的理解?应该在哪些方面可以更好的使用!
前端·javascript·typescript
.Cnn2 小时前
用邻接矩阵实现图的深度优先遍历
c语言·数据结构·算法·深度优先·图论