前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >操作系统回收进程详解(基于rCore源码)

操作系统回收进程详解(基于rCore源码)

原创
作者头像
北京柴道
发布2025-01-01 20:04:36
发布2025-01-01 20:04:36
870
举报

进程回收

在操作系统中进程是资源隔离的一种抽象机制。进程出现的一个重要目的是为了保证操作系统的稳定性。在操作系统中,任务和任务之间的资源(例如内存)是隔离的,任务之间的行为不会相互影响,当单一任务崩溃时,并不会影响其他任务的继续进行。

每个进程都有创建,运行和结束。本文主要聚焦于进程结束阶段的操作系统行为,分析操作系统这样一个关键的软件系统是如何进行资源的回收和释放的,帮助我们更为深入地理解底层行为,让我们自己的编码更加鲁邦和合理。

rCore简介

rCore是由清华大学推出的一个简洁的操作系统实现。清华大学团队在rCore中开发了操作系统常用的:虚拟内存,进程管理,文件系统等组件,是低成本学习操作系统,进行底层开发实践的优秀平台。

本文就基于rCore中ch4分支的代码来解析操作系统是如何对进程进行回收的。

从内核角度看进程

在内核看来,进程是什么?

从数据结构的角度来看就是一个结构体。在rCore的ch4分支中,定义了这样的结构体来描述一个进程:

代码语言:rust
复制
pub struct TaskControlBlockInner {
    /// The physical page number of the frame where the trap context is placed
    pub trap_cx_ppn: PhysPageNum,

    /// Application data can only appear in areas
    /// where the application address space is lower than base_size
    pub base_size: usize,

    /// Save task context
    pub task_cx: TaskContext,

    /// Maintain the execution status of the current process
    pub task_status: TaskStatus,

    /// Application address space
    pub memory_set: MemorySet,

    /// Parent process of the current process.
    /// Weak will not affect the reference count of the parent
    pub parent: Option<Weak<TaskControlBlock>>,

    /// A vector containing TCBs of all child processes of the current process
    pub children: Vec<Arc<TaskControlBlock>>,

    /// It is set when active exit or execution error occurs
    pub exit_code: i32,

    /// Heap bottom
    pub heap_bottom: usize,

    /// Program break
    pub program_brk: usize,

    /// System calls
    pub system_calls: [u32; MAX_SYSCALL_NUM],

    /// execution time
    pub time: usize,
}

从注释中我们可以大致将一个进程持有的成员变量分为以下几类:

  • 运行时状态

运行时状态一方面包含了当前进程的执行状态,另一方面则记录了进程在执行中的CPU快照,用来进行进程在内核态和用户态之间的转换,以及内核中不同进程之间的切换。(需要注意的是此时还没有多线程的概念,每个进程只有一个线程)。

代码语言:rust
复制
pub task_cx: TaskContext,
pub task_status: TaskStatus,
pub trap_cx_ppn: PhysPageNum,
pub exit_code: i32,
pub system_calls: [u32; MAX_SYSCALL_NUM],
pub time: usize,
  • 内存资源 内存资源包含着一个进程所占据的内存,rCore基本复刻了Linux的可执行文件在内存中的模型,而MemorySet则是用来记录了所属进程的全部虚拟地址空间到物理内存的映射关系。
代码语言:rust
复制
pub memory_set: MemorySet,
  • 组织关系 组织关系记录了进程的父进程和子进程。
代码语言:rust
复制
pub parent: Option<Weak<TaskControlBlock>>,

pub children: Vec<Arc<TaskControlBlock>>,

内核回收进程的基本过程

进程的回收涉及到子进程和父进程之间的配合。大致过程如下:

代码语言:rust
复制
#[no_mangle]
#[link_section = ".text.entry"]
pub extern "C" fn _start(argc: usize, argv: usize) -> ! {
    clear_bss();
    unsafe {
        HEAP.lock()
            .init(HEAP_SPACE.as_ptr() as usize, USER_HEAP_SIZE);
    }
    let mut v: Vec<&'static str> = Vec::new();
    for i in 0..argc {
        let str_start =
            unsafe { ((argv + i * core::mem::size_of::<usize>()) as *const usize).read_volatile() };
        let len = (0usize..)
            .find(|i| unsafe { ((str_start + *i) as *const u8).read_volatile() == 0 })
            .unwrap();
        v.push(
            core::str::from_utf8(unsafe {
                core::slice::from_raw_parts(str_start as *const u8, len)
            })
            .unwrap(),
        );
    }
    exit(main(argc, v.as_slice()));
}

用户编写的main函数会被包装在用户库的_start函数中(定义如上),main函数结束后,会调用exit()系统调用。

用户进程进入内核态后,会执行exit_current_and_run_next内核函数,内核函数中会进行如下操作:

  1. 进程状态转换
    • 将进程设置为 Zombie 状态
  2. 子进程处理
    • 将子进程移交给 init 进程
  3. 资源回收
    • 回收内存页面
  4. 调度切换
    • 切换到其他就绪进程

这里有两个值得注意的问题:

  1. 为什么操作系统要将进程标记为 Zombie 状态,而不是直接结束?
  2. 为什么操作系统要将子进程交给 init 进程,而不是交付给父进程?

父进程对子进程的回收

Zombie 状态是进程生命周期的最后状态,之所以设置这个状态,主要目的在于:等待父进程回收子进程,并获取子进程的退出状态。

在Linux系统中,父进程会通过waitpid()的系统调用主动地回收子进程。rCore中也实现了这个系统调用,那么具体是如何实现的呢?

代码语言:rust
复制
pub fn sys_waitpid(pid: isize, exit_code_ptr: *mut i32) -> isize {
    let task = current_task().unwrap();
    
    // 1. 检查是否有符合条件的子进程
    let mut inner = task.inner_exclusive_access();
    if !inner.children.iter()
        .any(|p| pid == -1 || pid as usize == p.getpid()) {
        return -1;  // 没有这样的子进程
    }
    
    // 2. 查找已经变成僵尸进程的子进程
    let pair = inner.children.iter().enumerate()
        .find(|(_, p)| {
            // 检查是否是僵尸进程且匹配 pid
            p.inner_exclusive_access().is_zombie() && 
            (pid == -1 || pid as usize == p.getpid())
        });
    
    if let Some((idx, _)) = pair {
        // 3. 找到僵尸子进程,开始回收
        let child = inner.children.remove(idx);
        // 确保这是最后一个引用
        assert_eq!(Arc::strong_count(&child), 1);
        
        // 4. 获取退出码
        let found_pid = child.getpid();
        let exit_code = child.inner_exclusive_access().exit_code;
        
        // 5. 将退出码写入用户空间
        *translated_refmut(inner.memory_set.token(), exit_code_ptr) = exit_code;
        
        found_pid as isize
    } else {
        -2  // 子进程还在运行
    }
}

首先,sys_waitpid会根据我们输入的pid,判断对应的子进程是否处于 Zombie 状态。如果为-1,则检查所有的子进程。

当检查到子进程设为 Zombie 状态时,会从child进程容器中移除子进程。此时需要注意:

子进程的TaskControlBlockInner(文章开头介绍过)是通过基于引用计数的智能指针管理的,当引用计数清零即触发对象回收。当执行完if let Some((idx, _)) = pair分支判断之后, child局部变量结束了它的生命周期,其指向的内存区域就会被rust回收,进而完成对子进程的完整回收。

assert_eq!(Arc::strong_count(&child), 1); 则是用来保证,当前状态下,子进程的TaskControlBlockInner对象,只存在一个引用,保证当child生命周期结束时,子进程会被释放。

除此之外,sys_waitpid还会将子进程中的状态码写回到用户态内存区域,告知父进程:子进程的退出状态。

进程退出时对子进程的处理

在rCore的代码中,退出的进程将其下面的子进程加入到了initProc中(初始进程)。这是一个违背直觉的做法,因为每个进程的结构体中都存放了parent 进程的信息。那么为什么不能直接将子进程交付给父进程呢?

原因是可能存在的死锁问题,如下图所示:

代码语言:txt
复制
进程A (正在退出)           进程P (A的父进程)          进程C (A的子进程)
----------------------     ----------------------     ----------------------
获取自己的锁 (A_lock)      正在执行 wait 
                          获取自己的锁 (P_lock)
试图获取父进程P的锁        试图获取子进程A的锁
(等待 P_lock)             (等待 A_lock)
...                       ...
死锁!                     死锁!

为什么A进程会试图获取父进程的锁呢?原因在于子进程只保留Weak类型的父进程指针(防止父子进程之间存在循环引用的问题)。当当前进程企图修改父进程时(将自己的子进程加入到父进程中),必须将Weak指针升级为Arc指针。在这个过程中就需要获得父进程的锁,进而导致上图所示的死锁问题。

因此,当任何进程退出时,将子进程交付给initProc进程,避免了死锁问题,也让子进程能够被妥善回收。在rCore中,initProc进程如下定义:

代码语言:rust
复制
fn main() -> i32 {
    if fork() == 0 {
        // 子进程:执行 user_shell
        exec("user_shell");
    } else {
        // 父进程:循环等待回收子进程
        loop {
            let mut exit_code: i32 = 0;
            let pid = wait(&mut exit_code);
            if pid == -1 { // 没有子进程了
                // continue
            }
            println!("[initproc] Released a zombie process, pid={}, exit_code={}", pid, exit_code);
        }
    }
    0
}

简单来说,initProc中产生shell子进程之后,主要在无限循环中调用wait,适时回收挂载在initProc上的子进程。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进程回收
  • rCore简介
  • 从内核角度看进程
  • 内核回收进程的基本过程
    • 父进程对子进程的回收
    • 进程退出时对子进程的处理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档