Linux(三) : 进程

一、计算机的基石:冯诺依曼体系结构

在我们日常使用的笔记本电脑或不常见的服务器中,大部分都遵循着冯诺依曼体系结构。这一结构奠定了现代计算机的基础。

  • 核心组件
    • 输入设备:如键盘、鼠标,用于向计算机输入信息。
    • 存储器:这里特指内存(RAM),用于存放数据和指令。
    • 中央处理器(CPU):包含运算器和控制器,负责执行指令和处理数据。
    • 输出设备:如显示器、打印机,用于展示处理结果。
  • 关键特性
    • CPU 能且仅能直接对内存进行读写操作,不直接访问外设。
    • 所有外设(输入输出设备)的数据交换也都必须通过内存进行。
    • 简而言之,所有设备都只能直接和内存打交道

想象一下你用QQ和朋友聊天,从你输入消息到对方接收到消息,这整个过程中的数据流动,都离不开这个体系结构中各个部件通过内存的协同工作。

二、操作系统的角色与定位

操作系统(OS)是一系列基本程序的集合,它在计算机系统中扮演着“管理者”的角色。

  • 广义概念:操作系统不仅包括核心的内核(Kernel),还包括外壳程序(如Shell)、库(如glibc)、预装系统级软件等。
    • 内核核心功能:进程/任务/线程管理、文件系统、内存管理、驱动管理等。
  • 设计目的
    • 对下:与硬件交互,管理所有的软硬件资源。
    • 对上:为用户程序(应用程序)提供一个良好的执行环境。
  • 核心功能定位:操作系统是一款纯正的“搞管理”的软件。

如何理解“管理”?
就像学校里校长管理辅导员,辅导员管理学生一样。操作系统要管理硬件设备,首先需要“描述”这些设备(通常用结构体 struct),然后将它们“组织”起来(例如用链表等数据结构)。

  • 系统调用 (System Call) 与库函数 (Library Function)
    • 操作系统会暴露一部分接口供上层开发使用,这些接口称为系统调用。它们功能基础,但对用户要求较高。
    • 开发者可以将部分系统调用进行封装,形成库函数,使得上层用户或开发者能更方便地进行二次开发。

三、进程:运行中程序的实体

当我们谈论操作系统如何进行管理时,进程管理是其核心之一。

  • 基本概念
    • 课本定义:程序的一个执行实例,正在执行的程序等。
    • 内核观点:担当分配系统资源(如CPU时间、内存)的实体。
  • 描述进程 - 进程控制块 (PCB)
    • 进程的信息被存放在一个叫做**进程控制块(Process Control Block, PCB)**的数据结构中。
    • 在Linux操作系统下,PCB的具体实现是 task_struct。它会被加载到内存中,包含进程的各种属性。
    • task_struct 内容分类
      • 标示符:唯一的进程ID (PID),用于区别其他进程。
      • 状态:任务状态、退出代码、退出信号等。
      • 优先级:相对于其他进程的优先级。
      • 程序计数器:指向下一条将被执行指令的地址。
      • 内存指针:指向程序代码、进程数据及共享内存块的指针。
      • 上下文数据:进程执行时CPU寄存器中的数据。
      • I/O状态信息:I/O请求、分配的I/O设备、使用的文件列表等。
      • 记账信息:CPU使用时间、时钟数总和等。
  • 组织进程:内核中所有运行的进程通常以 task_struct 链表的形式存在。
  • 查看进程
    • 可以通过 /proc 系统文件夹查看,例如 /proc/1 查看PID为1的进程信息。
    • 用户级工具如 topps aux 也可以获取进程信息。
  • 进程标示符
    • getpid():获取当前进程的ID (PID)。
    • getppid():获取当前进程的父进程ID (PPID)。
  • 创建进程 - fork() 系统调用
    • fork() 用于创建一个新的进程,即子进程。
    • fork() 调用一次,但返回两次:在父进程中返回子进程的PID,在子进程中返回0。
    • 父子进程共享代码段,但数据段各自独立(采用写时拷贝 Copy-on-Write技术,即只有在写入时才真正复制数据)。
    • 通常使用 if/else if 结构根据 fork() 的返回值来区分父进程和子进程的执行逻辑。

四、进程的生命周期:进程状态

一个进程在其生命周期中会经历多种状态。Linux内核中定义了以下主要状态:

  • R (running) - 运行状态:进程要么正在CPU上运行,要么在运行队列中等待被调度。
  • S (sleeping) - 睡眠状态:进程正在等待某个事件的完成(可中断睡眠)。
  • D (disk sleep) - 磁盘休眠状态:不可中断睡眠,通常在等待I/O操作结束(如磁盘I/O)。
  • T (stopped) - 停止状态:进程被信号(如SIGSTOP)暂停。可以通过SIGCONT信号让其继续运行。
  • t (tracing stop) - 追踪停止状态:进程在被调试器追踪时进入此状态。
  • X (dead) - 死亡状态:这是一个 transitory state,表示进程已经结束,但资源尚未完全释放。在任务列表中通常看不到。
  • Z (zombie) - 僵尸状态:进程已终止,但其父进程尚未使用 wait() 系统调用来读取其退出状态。
    查看进程状态:可以使用 ps auxps axj 等命令。
1. 特殊状态:僵尸进程 (Zombie Process)
  • 产生原因:子进程退出后,父进程没有及时调用 wait()waitpid() 来获取子进程的退出状态码。
  • 特性:僵尸进程会以终止状态保留在进程表中,等待父进程读取其退出信息。
  • 示例代码:可以编写一个子进程先退出,而父进程在一段时间后再处理的代码来观察僵尸进程。
  • 危害
    • 虽然僵尸进程本身不占用太多内存(主要是PCB的开销),但如果大量产生且不被回收,会持续占用进程表中的条目。
    • 这可能导致系统无法创建新的进程,因为它耗尽了进程ID或进程表资源,造成内存泄漏
2. 特殊状态:孤儿进程 (Orphan Process)
  • 产生原因:父进程在子进程结束之前就退出了。
  • 处理机制:孤儿进程会被1号进程(init进程或systemd)领养。init进程会负责回收这些孤儿进程的资源(即调用wait())。
  • 示例代码:可以编写一个父进程先于子进程退出的代码来观察孤儿进程。

五、进程间的竞争与协作:进程优先级

系统中通常有多个进程需要使用CPU,操作系统需要决定哪个进程先运行,这就是进程优先级的作用。

  • 基本概念:CPU资源分配的先后顺序。优先级高的进程有优先执行权。
  • 查看进程信息ps -l 命令可以显示进程的详细信息,包括:
    • UID:执行者身份。
    • PID:进程代号。
    • PPID:父进程代号。
    • PRI:进程可被执行的优先级,其值越小越早被执行。
    • NI:Nice值,用于修正PRI。
  • PRI (Priority) 和 NI (Nice value)
    • PRI是内核动态调整的,用户不能直接修改。
    • NI是用户可以调整的,其范围是 -20 到 19(共40个级别)。
    • 新的优先级计算公式近似为:PRI(new) = PRI(old) + nice
    • 负的nice值会使进程优先级变高(PRI值变小),从而更快被执行。
  • 调整优先级
    • top 命令中按 r键可以修改已存在进程的nice值。
    • nice 命令用于以指定nice值启动新进程。
    • renice 命令用于修改已运行进程的nice值。
  • 相关概念
    • 竞争性:多进程竞争少量CPU资源,因此需要优先级。
    • 独立性:多进程运行期间,各自拥有独立的资源,互不干扰。
    • 并行 (Parallelism):多个进程在多个CPU核心上同时运行。
    • 并发 (Concurrency):多个进程在一个CPU核心上通过快速切换(时间片轮转)的方式,在一段时间内都得以推进,给人同时运行的错觉。

六、进程的切换:CPU的舞蹈

为了实现并发,操作系统需要在不同进程之间切换CPU的执行权,这就是进程切换或上下文切换。

  • CPU上下文切换 (Context Switch)
    • 当内核决定运行另一个任务时,它会保存当前正在运行任务的状态(主要是CPU寄存器中的所有内容,如程序计数器、通用寄存器等)。
    • 这些状态被保存在任务自己的进程控制块(PCB)或内核栈中。
    • 然后,内核加载下一个将要运行任务的保存状态到CPU寄存器中,并开始执行该任务。
  • 时间片 (Time Slice)
    • 现代分时操作系统会给每个进程分配一个“时间片”(一个计数器)。当进程的时间片用完后,操作系统可能会将其从CPU上剥离,调度其他进程运行。
Linux 2.6 内核进程 O(1) 调度算法简介

Linux 2.6内核使用了一种被称为O(1)调度算法的机制,其核心思想是无论系统中进程数量多少,选择下一个要运行的进程的时间复杂度都是常数。

  • 运行队列 (Runqueue):每个CPU拥有一个自己的运行队列。
  • 优先级:分为普通优先级(100-139,对应nice值)和实时优先级(0-99)。
  • 活动队列 (Active Queue)
    • 包含所有时间片尚未用完的进程,按优先级组织。
    • 使用一个位图(bitmap)来快速定位优先级最高的非空进程队列,从而高效选择下一个运行的进程。
  • 过期队列 (Expired Queue)
    • 包含所有时间片已经耗尽的进程。
  • 指针交换:当活动队列中的所有进程都被处理完毕后,系统会重新计算过期队列中进程的时间片,并通过交换活动队列和过期队列的指针,使得过期队列成为新的活动队列。

这种设计确保了调度效率不会随着进程数量的增加而显著下降。

七、进程的“身份背景”:环境变量

环境变量是操作系统中用来指定运行环境的一些参数,它们通常具有全局特性,并可以被子进程继承。

  • 基本概念:例如,我们编译C/C++代码时,链接器能找到所需的库文件,就是因为有相关的环境变量(如LD_LIBRARY_PATH)指明了搜索路径。
  • 常见环境变量
    • PATH:指定命令的搜索路径。
    • HOME:指定用户的主工作目录(登录时的默认目录)。
    • SHELL:当前使用的Shell程序路径(通常是/bin/bash)。
  • 查看与操作命令
    • echo $NAME:显示名为NAME的环境变量的值。
    • export NAME=value:设置一个新的环境变量(使其能被子进程继承)。
    • env:显示所有环境变量。
    • unset NAME:清除环境变量。
    • set:显示本地定义的shell变量和环境变量。
  • 在代码中获取环境变量
    • 通过 main 函数的第三个参数 char *env[]
    • 通过libc定义的全局变量 extern char **environ;
    • 使用系统调用 getenv("NAME") 来获取特定环境变量的值。 putenv() 可以用来设置环境变量。
  • 继承性:父进程导出的环境变量会被子进程继承。

八、进程的“领地”:程序地址空间

我们学习C语言时,老师会画出内存布局图,包括栈区、堆区、全局/静态存储区、常量区、代码区。这实际上描述的是进程的虚拟地址空间

  • 虚拟地址 (Virtual Address)
    • 通过一个实验可以很好地理解:用 fork() 创建子进程,父子进程中访问同一个全局变量的地址,会发现打印出的地址值是相同的。但是,如果一个进程修改了这个变量的值,另一个进程读取到的值可能不同(因为发生了写时拷贝,数据在物理上分离了)。
    • 结论:我们在C/C++代码中看到的地址(如通过指针获取的地址)都是虚拟地址。物理地址对用户是透明的,由操作系统统一管理。操作系统负责将虚拟地址转换为物理地址。
  • 进程地址空间 (Process Address Space)
    • 每个进程都拥有自己独立的、私有的虚拟地址空间。
    • 这个虚拟地址空间从0到一个很大的值(例如32位系统是232,64位系统是264)。
    • 操作系统通过页表 (Page Table) 将进程的虚拟地址映射到实际的物理内存地址。同一个虚拟地址在不同进程中可以映射到不同的物理地址。
Linux内核中的内存管理结构:
  • mm_struct (内存描述符):每个用户进程都有一个 mm_struct 结构,用于描述其整个用户虚拟地址空间。它在 task_struct 中被引用。
    • mm_struct 内部记录了代码段、数据段、堆、栈等的起始和结束虚拟地址。
  • vm_area_struct (VMA - 虚拟内存区域)mm_struct 内部又通过 vm_area_struct 来描述不同类型的虚拟内存区域(如代码区、数据区、堆区、栈区、内存映射区等)。这些VMA可以组织成链表(mmap 指针)或红黑树(mm_rb 指针),以便高效查找和管理。
为什么需要虚拟地址空间?

如果程序直接操作物理内存,会带来很多问题:

  1. 安全风险:任何进程都可以访问任意物理内存,恶意程序可以轻易破坏系统或其他进程的数据。
  2. 地址不确定性:程序每次加载到内存的物理地址可能都不同,难以进行链接和寻址。
  3. 效率低下:当物理内存不足时,需要将整个进程完整地换出到磁盘(交换),效率很低。

虚拟地址空间带来的好处

  1. 安全性与隔离性:操作系统控制虚拟地址到物理地址的映射,可以确保进程只能访问分配给自己的物理内存,从而保护了物理内存和进程的独立性。
  2. 地址空间的统一与简化:每个进程都认为自己拥有从0开始的连续地址空间,简化了程序的链接和运行。物理内存的分配和进程管理可以解耦。
  3. 内存使用效率
    • 延迟分配/按需分配 (Demand Paging):当进程通过 malloc 等方式申请内存时,操作系统可能只在虚拟地址空间中分配,并不立即分配物理内存。只有当进程实际访问这些虚拟地址时,才会触发缺页中断,由操作系统分配物理内存并建立映射。
    • 内存共享:可以将不同进程的虚拟地址映射到同一块物理内存(例如共享库的代码段),节省物理内存。
  4. 程序加载灵活性:程序在物理内存中可以加载到任意位置,因为虚拟地址到物理地址的映射是由页表管理的。

九、总结

从冯诺依曼结构到复杂的进程管理和虚拟内存,我们今天一起探索了操作系统中关于“进程”的许多核心概念。理解这些机制,不仅能帮助我们更好地编写高效、稳定的程序,也能让我们对计算机系统的工作原理有更深刻的认识。希望这篇博客能为你打开一扇通往操作系统内部世界的大门!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值