[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),系统的内存中会有一块区域持续被标记为"被占用",但实际上再也没人会用到它。这就是"泄漏"------内存还占在那里,却没法再利用。随着这样的"遗留物"越来越多,系统的可用内存会逐渐减少,影响性能,甚至导致系统崩溃。

相关推荐
崔庆才丨静觅13 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
2601_9491465314 小时前
C语言语音通知接口接入教程:如何使用C语言直接调用语音预警API
c语言·开发语言
passerby606114 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了14 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅14 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅15 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
七夜zippoe15 小时前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann
盟接之桥15 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
崔庆才丨静觅15 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment15 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端