从0到1学Linux:Linux进程
原作者:Linux教程,原文「链接」:https://mp.weixin.qq.com/s/39rQMl3V2Egot9cZ14NCLg
【获得原作者转载授权】
每个计算机系统都包含一个核心软件集合,即操作系统。
- 内核层:提供进程控制、内存管理、文件系统和设备驱动等核心功能
- 系统软件层:包含shell界面、数据库管理系统等实用程序
本文将讲解 Linux 中进程的相关知识,帮助读者更好地理解系统内部的运行机制。
一、什么是进程?
在 Linux 系统中,进程指的是正在执行的一个程序实例。每个进程都有其独立的运行环境,包括内存空间、打开的文件、寄存器状态等信息。从操作系统的角度来看,所有在系统上运行的东西,都可以称为一个进程。
每一个进程都会被分配一个唯一的标识符,称为进程 ID(PID),操作系统正是通过这个 PID 来识别和管理各个进程。此外,大多数进程还有一个父进程 ID(PPID),用来记录创建它的父进程。
进程从创建到结束会经历多个阶段,包括启动、运行、等待、终止等,整个过程由内核调度和管理。
需要特别注意的是:程序(Program) 和 进程(Process) 是两个不同的概念。
程序是指存储在磁盘上的可执行文件,是一组静态的指令集合。它本身不会占用系统的运行资源(如内存),只有在被加载到内存并开始执行时,才会创建一个或多个对应的进程。
而进程是程序在计算机内存中的一次动态执行过程。它是操作系统进行资源分配和调度的基本单位。每个进程都拥有独立的内存空间,并且具有生命周期:当进程启动时,系统为其分配资源;当进程结束时,这些资源会被释放。
换句话说:
- 程序是静态的,只占用磁盘空间;
- 进程是动态的,存在于内存中,是程序的实际运行实例。
例如,在 Linux 系统中,当用户打开一个文件时,系统会为该操作创建一个对应的进程;关闭文件后,该进程也随之终止。再比如,当我们启动 Tomcat 服务时,系统会生成一个由 Java 虚拟机驱动的进程来处理请求;如果启动 Apache HTTP Server 并有多个用户同时发起请求,Apache 会为这些请求创建多个 httpd 子进程来并发地提供服务。
二、Linux进程的结构
在 Linux 系统中,每个进程在内存中主要由三个核心部分组成:代码段(Text Segment)、数据段(Data Segment) 和 堆栈段(Stack Segment)。这种划分方式有助于操作系统高效地管理进程资源,并充分利用硬件特性(如基于 I386 架构的段寄存器)来提升性能。
代码段:存放的是程序代码的数据,也就是CPU执行的指令集合。如果机器中有多个进程运行同一个程序,那么它们可以共用同一个代码段。
数据段:用于存放程序中的全局变量、常量以及动态分配的数据空间,也就是被执行指令所访问的数据内容。
堆栈段:存放的是子程序的返回地址、函数参数以及程序的局部变量等内容。堆栈段包含在进程控制块PCB(Process Control Block)中。PCB位于进程核心堆栈的底部,不需要额外分配空间。堆栈段主要包括堆和栈两个部分:
堆(heap):堆是进程中用于动态分配的内存区域,其大小不固定,可以根据需要动态增长或缩小。当进程调用如malloc等函数申请内存时,新的内存会被添加到堆上(堆被扩展);而当使用free等函数释放内存时,这部分内存又会从堆中移除(堆被缩减)。
栈(stack):栈用于存放函数中临时定义的局部变量,即在函数的“{}”内部定义的变量(但不包括用static声明的变量,因为这类变量存放在数据段中)。此外,在函数调用时,其参数也会被压入调用者的栈中,并在调用结束后将函数的返回值保存回栈中。由于栈具有先进后出的特点,因此非常适合用来保存和恢复调用现场。从这个意义上讲,我们可以把堆栈看作是一个临时数据存储和交换的内存区域。
三、Linux 中的进程状态
在 Linux 系统中,每个进程在其生命周期中会处于不同的状态,这些状态反映了进程当前的运行情况。常见的进程状态如下:
- 运行状态(R):表示进程当前正在运行,或者已经准备好运行,正在等待 CPU 调度器为其分配时间片。
- 可中断睡眠状态(S):表示进程正在等待某些条件满足,比如等待输入输出完成或信号通知。这种状态下的进程可以被外部信号唤醒或终止。
- 不可中断睡眠状态(D):与 S 状态类似,但该状态下的进程不能被信号打断,通常出现在等待硬件资源的过程中,例如磁盘 I/O 操作。
- 停止状态(T):表示进程已经被暂停执行,通常是由于收到了特定的停止信号,如调试过程中触发的暂停操作。
- 僵尸状态(Z):表示该进程已经执行完毕并退出,但其父进程尚未读取它的退出状态信息,因此该进程仍然保留在进程表中。
- 死亡状态(X):表示进程已经彻底结束并被系统回收,这一状态不会在用户层面看到。
重要知识补充:阻塞和挂起
一个进程可以处于多种状态,在这些状态中,阻塞和挂起是最为重要的两种状态。
以下是对这两个概念的简要说明:
阻塞
当一个进程因为等待某种条件就绪(如I/O设备、网络数据、锁等)而导致无法继续执行时,该进程就进入了阻塞状态。这是一种“暂停推进”的状态,给人的感觉就是程序“卡住了”。
换句话说,一个进程之所以被阻塞,是因为它正在等待某些必要的资源准备好。例如,等待磁盘读写完成、等待网络数据到达、等待用户输入等。
挂起
当系统内存不足时,操作系统会将阻塞进程的代码和数据交换到磁盘,以释放内存空间。此时进程处于挂起状态(阻塞挂起)。就比如:边走路边玩手机 → 将手机放入口袋(挂起)→ 专心走路 → 需要时再取出手机。
阻塞与挂起的区别
特性 | 阻塞 | 挂起 |
触发原因 | 等待资源(被动) | 内存不足(主动) |
CPU状态 | 立即释放 | 已处于释放状态 |
内存状态 | 保持占用 | 数据换出到磁盘 |
恢复条件 | 资源就绪 | 内存充足且资源就绪 |
管理方式 | 加入设备等待队列 | 加入挂起队列 |
四、Linux进程树与进程组
进程树的概念
在 Linux 操作系统中,进程树(Process Tree) 是由当前运行的所有进程及其父子关系构成的一个层次结构。每个进程都可能有一个父进程和零个或多个子进程。通过这种父子关系,我们可以清楚地了解各个进程的启动顺序以及它们之间的依赖关系。
进程树的关键概念
- 根进程:系统的第一个进程是由内核直接启动的进程,在大多数现代 Linux 系统中,这个进程是 systemd(PID 为 1),它作为整个进程树的根节点,负责启动其他所有系统进程。
- 父进程和子进程:每个进程都有一个父进程,父进程可以创建子进程。子进程通常会继承父进程的一些属性,并在其基础上独立运行。
- 进程ID (PID):每一个进程在系统中都有一个唯一的标识符,称为进程 ID,简称 PID。
- 父进程ID (PPID):每个进程还有一个父进程 ID(PPID),用于标识它的创建者。
- 孤儿进程:当一个父进程退出而其子进程仍在运行时,这些子进程就被称为孤儿进程。此时,init 或 systemd 会接管这些孤儿进程,成为它们的新父进程。
- 僵尸进程:当一个进程终止后,如果它的父进程尚未调用 wait() 函数来获取该进程的终止状态信息,那么该进程就处于“僵尸”状态。僵尸进程虽然不再执行,但仍然保留在进程表中,等待父进程回收其状态信息。
进程组与会话
在 Linux 系统中,除了父子关系之外,进程还可以属于某个进程组或某个会话。这些机制有助于操作系统对进程进行更高级别的组织和控制,尤其是在处理终端交互和信号传递时尤为重要。
进程组(Process Group)
- 进程组是一组相关联的进程集合,它们可以作为一个整体接收相同的信号,并且通常共享同一个终端。
- 每个进程组都有一个唯一的进程组 ID(PGID),这个 ID 通常由该组中的某个成员进程设置生成。
- 在同一个进程组中,一个进程可以通过指定进程组 ID,将信号发送给组内的所有进程,实现统一控制。
会话(Session)
- 会话是一个由一个或多个进程组组成的集合,它们共享同一个控制终端。
- 每个会话都有一个唯一的会话 ID(SID),这个 ID 通常由该会话的第一个进程(即会话首领)创建。
- 控制终端由会话的首领进程打开。当控制终端关闭时,会话中的所有进程都会收到 SIGHUP 信号,表示终端已断开连接。
- 进程可以通过调用 setsid() 系统调用来创建一个新的会话,并在此会话中成为首领进程。
进程组与会话的关系
- 一个进程可以属于一个进程组,同时也可以作为某个会话的首领进程。
- 会话的首领进程通常是用户登录时启动的 shell 进程。
- 同一会话中的进程组通常共享同一个控制终端。但如果需要脱离当前会话并独立运行,一个进程可以通过调用 setsid() 来创建新的会话,并在其中成为新会话的首领进程。
五、进程间通信
我们知道进程的程序地址空间决定了进程之间的独立性,然而,在实际应用中,进程之间往往需要进行通信,以便交换数据、共享资源或协同完成任务。
进程间通信的目的
进程间通信(IPC, Inter-Process Communication)主要有以下几个目的:
- 数据传输:一个进程需要将自己的数据发送给另一个进程,实现信息的传递。
- 资源共享:多个进程可能需要访问和操作相同的资源,如文件、内存区域等。
- 通知事件:当某个进程发生特定事件时,需要向其他进程发送通知,告知其事件的发生。
- 进程控制:某些进程希望对另一个进程进行完全控制,例如在调试过程中,调试器进程需要监控被调试进程的所有异常和状态变化,及时获取其运行状态的转变。
进程间通信的方法
Linux 系统为用户提供了多种进程间通信的方式,主要包括以下三种类型:
1. 管道通信(Pipe)
管道是一种最基本的进程间通信机制,常用于本地进程之间的数据传输。
例如我们在“进程篇”中使用过的命令 ' | ' 就是管道的一种典型应用。它通常与其他命令组合使用,如搭配 grep 文本过滤工具使用:
ps -ajx | grep lrk
该命令表示将 ps -ajx 命令执行的结果通过管道传递给 grep 命令,从而筛选出我们关心的内容。
管道通信分为两种形式:
- 匿名管道(Anonymous Pipe):只能用于具有亲缘关系的进程之间(如父子进程),生命周期较短。
- 命名管道(FIFO):可以在无亲缘关系的进程之间使用,具有文件名,可以像普通文件一样打开和关闭。
2. POSIX 进程通信
POSIX 是一套标准接口规范,定义了统一的进程通信机制,适用于各种符合 POSIX 标准的操作系统。
这类通信方式包括:
- POSIX 共享内存
- POSIX 消息队列
- POSIX 信号量
为开发者提供了更灵活、高效的 IPC 接口,便于跨平台开发。
3. System V 进程通信
System V 是早期 Unix 系统的一个重要版本,它也定义了一套完整的进程通信标准,至今仍在 Linux 中广泛支持。
常见的 System V IPC 包括:
- System V 共享内存
- System V 消息队列
- System V 信号量
这些机制功能强大,适用于复杂的多进程协作场景,但使用相对复杂,需注意同步与权限管理等问题。
五、进程优先级
5.1、为什么要有优先级
进程在运行过程中需要访问系统资源(如 CPU 时间、内存、I/O 等),而这些资源是有限的。为了合理地分配资源,操作系统会采用排队机制,让进程按照某种顺序依次享受资源。
资源不足导致必须排队,而排队就必然存在先后顺序。这个“先后顺序”就是所谓的优先级。
5.2 优先级的具体表示
在 Linux 中,进程的优先级是 PCB(task_struct)中的一个整型变量(int priority 或 PRI)。Linux 系统中进程的默认优先级为 80,并且允许用户对优先级进行调整。
Linux 的优先级范围为:[60, 99],其中数字越小,表示优先级越高。
需要注意的是:
Linux 并不直接允许用户修改 PRI 值,而是通过修改一个叫做 nice 值 的参数来间接影响优先级。
- nice 值不是优先级本身,而是用于计算优先级的一个修正值。
- 每次重新设置优先级时,系统都会以默认值 80 为基准进行调整。
设置优先级的范围是为了防止某些高优先级进程长期占用资源,从而造成低优先级进程“饿死”的情况。任何分时操作系统,在调度进程中都必须兼顾公平性与效率,避免出现进程饥饿问题。
六、进程的调度与切换
进程被加载到 CPU 上执行时,并不需要一次性执行完毕。现代操作系统普遍采用基于时间片轮转的方式来进行任务调度。
进程调度的基本特性:
- 竞争性:系统中进程数量远多于 CPU 数量,因此进程之间存在对 CPU 资源的竞争。为了更高效地完成任务并合理分配资源,引入了优先级机制。
- 独立性:多个进程各自拥有独立的地址空间和资源,互不干扰。
- 并行性:多个进程可以在多个 CPU 上同时运行,称为并行执行。
- 并发性:在一个 CPU 上,通过快速切换不同进程的执行状态,在一段时间内使多个进程都能得到推进,这种现象称为并发执行。
6.1 进程的切换
当一个进程在 CPU 上运行时,会产生大量的临时数据,这些数据保存在 CPU 的寄存器中。
当该进程的时间片用完后,CPU 会将其寄存器中的所有数据保存到该进程的 PCB 中,这个过程称为上下文保护。
这些保存在 PCB 中的数据被称为硬件上下文。所有的保存操作都是为了后续的恢复,所有的恢复操作则是为了让进程能从上次中断的位置继续执行。
当下一次该进程被调度时,操作系统会将之前保存的硬件上下文恢复到 CPU 寄存器中,然后继续运行。
虽然 CPU 是共享设备,但其内部某一时刻只属于一个进程。每个进程都“独占”CPU 一段时间,通过频繁切换实现多任务并发。
6.2 进程的调度
CPU 在实现进程调度时,需要综合考虑多个因素,包括:
- 进程优先级
- 是否存在饥饿问题
- 调度效率
Linux 的运行队列中维护着一个名为 queue 的 task_struct 指针数组,该数组的下标从 100 到 139,正好对应进程优先级 60 到 99 的四十个等级。
举个例子:
如果有一个优先级为 60 的进程准备被调度,那么它会被链入 queue 数组的第 100 号下标对应的队列中(类似于哈希表的结构)。
每个队列对应一个特定的优先级。这样设计的好处是,CPU 在调度时可以非常高效地从高优先级到低优先级依次查找并调度进程。
七、进程调度和管理
7.1、为什么要调度
调度是为了实现多任务并发,提高系统资源利用率。早期计算机只能依次运行程序,无法同时处理多个任务。后来引入了协作式和抢占式多任务机制,提高了系统的效率和用户体验。
7.2、为什么能调度
调度分为主动调度(进程主动放弃 CPU)和被动调度(中断触发)。中断机制使得被动调度成为可能,通过定时器中断检测并触发调度。
7.3、何时调度
调度时机包括主动调度(如 I/O 等待、加锁失败等)和被动调度(如定时器中断、进程唤醒等)。被动调度在合适的时间点执行调度,确保公平性和响应性。
7.4、如何调度
调度分两步:选择下一个要执行的进程和切换进程。选择进程基于调度算法,切换进程涉及用户空间和执行栈的切换。
7.5、调度均衡
为了平衡多个 CPU 的负载,调度器会在 CPU 之间迁移进程,以保证所有 CPU 的工作量尽量平均。
7.6、常见调度策略
(1)实时调度策略
实时调度策略保障对时间敏感的任务。主要类型有 DEADLINE、SCHED_FIFO 和 SCHED_RR。DEADLINE 根据截止时间调度,SCHED_FIFO 按优先级调度,SCHED_RR 带时间片的 FIFO。
(2)完全公平调度策略(CFS)
CFS 旨在为每个进程提供公平的 CPU 时间分配,基于虚拟运行时间(vruntime)概念,使用红黑树管理进程。高优先级进程 vruntime 增长较慢,低优先级较快,从而实现公平竞争。
7.7、调度器的工作机制
7.7.1、进程优先级的确定
进程优先级由 Nice 值和 Priority 权重值决定。Nice 值取值 -20 到 19,数值越小优先级越高。Priority 权重值结合 Nice 值和其他因素计算得出。
7.7.2、调度的时机与触发
调度时机包括进程状态变化(如等待、就绪)、时间片用完和中断事件。这些情况触发调度器进行进程切换,确保系统高效运行。
7.7.3、进程调度器特点
Linux 调度器采用时间分片方式,确保每个进程近乎相等的 CPU 使用权。进程多数处于睡眠状态,只有特定条件触发时才获得调度。调度器关注吞吐量和延迟,优化系统性能。
八、环境变量
环境变量是进程运行环境的抽象载体,进程则是环境变量的消费者与传播者。二者通过操作系统的内存管理、进程创建机制深度耦合,共同实现灵活的系统配置与资源共享
以下是操作系统中常见的环境变量及其作用:
环境变量 | 描述 | 示例 |
PATH | 定义可执行程序的搜索路径,多个路径用冒号分隔。 | /usr/bin:/bin:/usr/local/bin |
HOME | 当前用户的主目录。 | /home/username |
USER | 当前用户名。 | username |
SHELL | 用户默认的Shell路径。 | /bin/bash |
LANG | 语言和区域设置,影响程序的语言显示。 | en_US.UTF-8 |
LD_LIBRARY_PATH | 动态链接库搜索路径。 | /usr/local/lib:/usr/lib |
TEMP 或 TMP | 临时文件的存储路径。 | /tmp |
EDITOR | 默认文本编辑器。 | vim |
管理环境变量的常用指令
在Linux中,可以通过命令行轻松管理环境变量。
指令 | 描述 | 示例 |
echo $VAR | 查看环境变量的值。 | echo $HOME |
export VAR=value | 设置环境变量,并导出到子进程。 | export MYVAR=123 |
unset VAR | 删除环境变量。 | unset MYVAR |
env 或 printenv | 查看当前进程的所有环境变量。 | env |
欢迎大家关注我【文章底部↓】