DP读书:《openEuler操作系统》(五)进程与线程

进程与线程

这章我啃了很久,作为计算机领域最为重要的两个概念之一,今天我想讲一讲进程的概念、描述、控制。这三小结的内容,差不多我每天啃一小节终于啃完了,那就敲吧~~~

现代计算机采用多道程序技术,允许多个程序并发执行,不同于早期的单道批处理系统计算机只能一次性处理一道的。而在这个过程中,程序只是对计算任务与数据的静态描述,并不能反应多道程序之间相互影响的这一状态。因此计算机系统中使用进程这一概念来反映程序在执行过程以及能来共享资源的基本单位。另外由于进程的创建于切换之间切换资源开销较大,操作系统中引入了线程这一概念。

进程的概念

程序:从源码到执行

下图展示了一份C语言源代码(符合C99标准),判断日期是否为闰年。

好的,以下是一份C语言源代码,用于判断给定日期是否为闰年:

c 复制代码
#include <stdio.h>

int isLeapYear(int year) {
    if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
        return 1;
    } else {
        return 0;
    }
}

int main() {
    int year = 2024;
    if (isLeapYear(year)) {
        printf("%d is a leap year.\n", year);
    } else {
        printf("%d is not a leap year.\n", year);
    }
    return 0;
}

1. 编译阶段:

在编译阶段,编译器将源代码转换为机器代码。编译器首先检查代码的语法错误,例如拼写错误、缺失的括号等。如果源代码没有语法错误,编译器将创建一个可执行文件,其中包含计算机可以直接执行的机器代码。对于上述代码,编译器将检查变量和函数是否正确定义,然后将其转换为相应的机器代码。

以上为 C语言程序示例

bash 复制代码
gcc -o leap_year leap_year.c
./leap_year

以上为编译命令示意

为了使操作系统能够以标准方法对编译后二进制文件进行处理,类UNIX操作系统通常采用ELF格式(Executable and Linkable Format,可执行可链接文件格式)作为二进制文件的标准格式,ELF文件的执行视图如下:

ELF头部包含了描述整个ELF文件的基本信息,段头(Program Header)表包含了描述各个段(Segment)的信息,其中Segment是存储程序中数据或代码的逻辑结构。

在ELF(Executable and Linkable Format)中,有以下一些段(segments)是比较重要的:

  1. .text:这个段包含了程序的代码。这是执行程序时首先执行的段。它通常是可执行的,即它包含了那些被操作系统允许执行的指令。.text段通常被分为多个区段(section),包括.text.plt.got等。
  2. .data:这个段包含了初始化的全局变量和静态变量。这些数据在程序开始执行之前被加载到内存中,并被存储在只读和可写的方式。
  3. .rodata:这个段包含的是只读数据,比如常量,字符串字面量等。这些数据在程序开始执行之前被加载到内存,且只能被读取,不能被修改。
  4. .bss:这个段包含了未初始化的全局变量和静态变量。这些数据在程序开始执行之前被加载到内存,并被假定为零或空值。

这些不同的段在ELF文件和程序执行中扮演着重要的角色,它们描述了如何将程序的代码和数据加载到内存中,以及这些数据应该如何被操作系统和程序使用。

示例程序编译后的二进制可执行文件反汇编:

2. 加载阶段:

在加载阶段,操作系统将可执行文件加载到内存中,并准备开始执行。操作系统将分配必要的内存来存储程序的不同部分,例如代码、全局变量和堆栈。此外,操作系统还将设置程序所需的任何其他资源,例如打开文件句柄或分配网络连接。在这个阶段,程序的各种组成部分被放置在内存中适当的位置,并为执行做好准备。

操作系统把ELF文件装入内存,这个过程称为程序的加载。分为两步:1.解析ELF头部,进行程序加载,检查ELF文件格式是否被当前CPU架构支持;2.读取段头表获取每段的信息,加载至内存段分配内存空间。

3. 执行阶段:

程序加载之后,利用ELF找到程序入口。

在执行阶段,计算机开始执行程序。计算机从程序的main函数开始执行,即上述代码中的main()函数。在上述代码中,程序首先定义一个名为isLeapYear的函数,该函数接受一个整数参数year,并返回一个整数值。函数检查年份是否是闰年,如果是,则返回1,否则返回0。接下来,程序在main函数中调用isLeapYear函数,并打印结果。在这个阶段,CPU开始执行程序中的指令,并根据指令执行各种操作,例如读取和写入内存、执行算术运算等。根据上述代码,如果年份是闰年,程序将打印"2024是闰年",否则将打印"2024不是闰年"。

对于程序执行过程中,动态申请的内存保存在一个称为堆的内存空间。让操作人员自主申请与及时释放。栈是先入后出的结构,在内存中,通常用低地址向高地址生长。上图,图四,为程序在内存中的布局。

程序的并发执行与进程抽象

进程的描述

进程控制块

进程控制块(Process Control Block,PCB)是操作系统用于管理进程的重要数据结构。它包含了关于进程的诸多信息,包括描述信息、控制信息、CPU上下文和资源管理信息。

1. 描述信息

描述信息主要包括以下部分:

  • 进程标识符(PID):用于唯一标识系统中的进程。
  • 进程状态:表示进程在执行过程中的状态,例如就绪、运行、等待或结束。
  • 父进程标识符:对于一个非初始进程,记录其父进程的PID。
  • 进程优先级:表示进程执行的优先级。

2. 控制信息

控制信息主要用于控制和协调进程的执行,包括以下部分:

  • 程序计数器(PC):指向下一条要执行的指令。
  • 栈指针:指向栈的顶部。
  • 中断向量:当发生中断时,指向中断处理程序的地址。
  • 信号掩码:用于屏蔽某些信号,防止它们中断程序的执行。
  • 调度参数:用于进程调度,例如估计运行时间、等待时间和剩余运行时间。

3. CPU上下文

CPU上下文记录了当前进程在CPU上执行所需的所有信息,包括以下部分:

  • 全局寄存器:例如程序计数器、栈指针等。
  • 局部寄存器:例如通用寄存器、状态寄存器等。
  • CPU的标志位:例如中断标志位、陷阱标志位等。
  • 堆栈:用于保存程序运行时的上下文信息。

4. 资源管理信息

资源管理信息用于记录和管理进程所需的资源,包括以下部分:

  • 打开文件表:记录进程打开的所有文件信息。
  • 内存管理信息:例如分页表、段表等,用于进程的内存管理。
  • 权限和安全信息:例如用户ID、组ID、文件权限等。
  • 进程间通信(IPC)信息:例如信号量、消息队列等,用于进程间的通信。
  • 其他资源:例如定时器、线程等。

进程状态

1.就绪状态

当进程已分配到必要的资源,并准备运行时,它处于就绪状态。在这种情况下,进程在等待CPU时间片的分配,一旦分配到时间片,它就可以被调度到CPU上执行。

2.运行状态

当进程正在占用CPU并执行其任务时,它处于运行状态。这是进程进行实际工作的状态。

3.阻塞状态

如果进程需要等待某些条件或资源才能继续执行,例如等待I/O操作完成,它就会进入阻塞状态。在这状态下,进程不会获得CPU时间片,直到它所需的资源或条件得到满足。

4.终止状态

当进程已完成任务或出现错误而不能继续执行时,它就处于终止状态。在这种情况下,进程不再占用CPU时间片,并且它的资源将被释放回系统,供其他进程使用。

进程的控制

进程控制源语

1.创建

创建一个新的进程。这通常由操作系统内核或用户程序发起。新的进程从父进程继承了许多属性,如代码、数据、内存配置、文件描述符等。

2.销毁

结束一个进程。这可以通过操作系统内核或用户程序发起。当一个进程结束时,它的资源(如内存、文件句柄等)会被回收,以便分配给其他进程。

3.阻塞与唤醒

进程在执行过程中可能遇到某种情况,使其无法继续执行,此时进程会进入阻塞状态。例如,当进程等待某个I/O操作完成时,它通常会进入阻塞状态。当阻塞的条件被满足时(例如,I/O操作完成),进程会被唤醒并回到就绪状态。

上述这些操作都是由操作系统提供的系统调用实现的。例如,在Unix和类Unix系统中,"fork()"系统调用用于创建新进程,"kill()"用于销毁进程,"wait()"用于阻塞进程直到其子进程结束。

进程创建

1.PCB的复制

PCB是操作系统用于管理进程的重要数据结构,其中包含了进程的各种信息,如进程标识符、状态、优先级、寄存器上下文等。在进程创建时,操作系统会为新进程创建一个新的PCB,并将父进程的PCB中的一些信息复制到子进程的PCB中,这些信息包括进程状态、优先级、寄存器上下文等。

2.CPU上下文的复制

CPU上下文是指当前进程在CPU上执行所需的各种信息,如寄存器状态、内存映射等。在进程创建时,操作系统会将父进程的CPU上下文复制到子进程的PCB中,以便子进程在CPU上执行时使用。

3.地址空间的复制

在进程创建时,操作系统还会为新进程分配一个独立的地址空间,这个地址空间是只读的,可以保证不同进程之间的数据隔离。父进程的地址空间会被复制到子进程的地址空间中,包括代码、数据、堆、栈等。当然,对于共享内存等特殊情况,操作系统会采取其他措施来管理不同进程之间的数据共享。

程序装载

程序装载是操作系统中一个重要的过程,它涉及到将可执行文件加载到内存中,并在CPU上执行。以下是程序装载的常见步骤:

1.exec函数簇的系列函数接口

在许多编程语言中,如C和C++,可以使用exec函数簇来装载和执行新的程序。这些函数包括exec()、execl()、execv()、execlp()等,它们接受一个或多个参数,指定要执行的新程序的路径和参数列表。调用这些函数后,当前进程的代码和数据将被替换为新程序的代码和数据。

需要注意的是,上述步骤仅适用于传统的静态链接的执行文件。对于动态链接的执行文件(如DLL或.so文件),装载的过程会有所不同,通常需要涉及到动态链接器的帮助。此外,不同的操作系统在具体实现上可能有所不同,但大致的流程是相似的。

2.可执行文件的寻找与打开

当调用exec函数簇时,需要提供要执行的可执行文件的路径。操作系统会解析这个路径,并在文件系统中寻找指定的可执行文件。一旦找到文件,操作系统会打开文件并准备将其加载到内存中。

3.可执行文件的装载

在可执行文件被打开后,操作系统将其从磁盘读入到内存中。这个过程称为"装载",操作系统会根据可执行文件的二进制格式解析其内容,并将其映射到内存中的适当位置。装载过程涉及到一些底层的操作,如分配内存空间、设置内存保护等。

4.新程序的执行

一旦可执行文件被装载到内存中,操作系统会将CPU的控制权转移到新的程序中。新的程序从其入口点开始执行,入口点通常是程序的main()函数。此时,原来的进程将不复存在,取而代之的是新的程序在内存中的副本。

进程终止

进程终止是指一个正在运行的进程由于某种原因停止执行,结束其运行。在操作系统中,进程终止通常由系统或程序员通过发送终止信号来触发。当一个进程终止时,操作系统会执行一系列操作来清理和回收该进程所使用的资源,并确保该进程对系统的影响最小化。

1.用户资源的回收

在进程终止时,操作系统会回收该进程所使用的用户资源,例如打开的文件、分配的内存、创建的线程等。这些资源的回收可以防止资源泄漏和不必要的资源占用。回收这些资源可以确保其他进程可以使用这些资源,或者系统可以将其重新分配给其他需要的进程。

2.状态信息的发送与僵尸状态的设置

当一个进程终止时,操作系统会将其状态信息发送给父进程或操作系统本身。这些状态信息通常包括进程的PID、退出状态码(表示进程的结束状态)、时间戳等。在发送状态信息后,操作系统会将该进程的状态设置为"僵尸状态",以表示该进程已经结束,但仍然占用着部分系统资源(例如内存)。

3.内核资源的回收

在进程终止后,操作系统会回收该进程所使用的内核资源,例如系统调用表、进程控制块(PCB)、虚拟内存空间等。这些资源的回收可以确保系统正常运行并防止资源泄漏。回收这些资源后,操作系统可以将这些资源重新分配给其他需要的进程。

4.为所有子进程寻找新父进程

在进程终止时,如果该进程有子进程,操作系统会为这些子进程寻找新的父进程。这是为了确保子进程不会成为孤儿进程,从而导致系统资源的浪费。寻找新父进程的过程通常涉及到操作系统的进程调度器,它会将一个父进程指定给子进程,以确保子进程可以被正确地管理和调度。

在进程终止后,操作系统会将其从进程调度中移除,并将其放入到一个适当的等待队列中,直到其状态被设置为僵尸状态并被彻底清理掉。整个过程是为了确保系统的稳定性和资源的有效利用。

openEuler中的进程树

bash 复制代码
1. 根进程(Root Process):根进程是整个进程树的起点,也是系统的第一个进程。在openEuler中,根进程的PID(进程ID)为0。
2. 系统进程(System Processes):系统进程是伴随着操作系统启动而生成的进程,它们负责维护操作系统的运行。在openEuler中,系统进程的PID通常以1开头。
3. 用户进程(User Processes):用户进程是由用户执行的程序产生的进程。在openEuler中,用户进程的PID以大于1的整数开头。
4. 子进程(Child Processes):子进程是根进程或系统进程派生出来的进程,它们通常执行一些特定的任务。子进程的PID与父进程的PID相关联。
5.僵尸进程(Zombie Processes):当子进程结束时,其父进程会为其回收资源并将其状态信息发送给内核。这个过程被称为"僵尸化"。僵尸进程已经结束,但仍然占用着部分资源(如内存),直到其父进程将其彻底清理掉。

在openEuler中,可以通过图形界面查看进程树。一种常用的方法是通过systemd工具。

以下是通过systemd工具在图形界面下查看进程树的步骤:

  1. 首先,确保你的系统中安装了systemd工具。如果你不确定是否已安装,可以通过以下命令进行安装:
sql 复制代码
sudo apt-get update
sudo apt-get install systemd-gui
  1. 安装完毕后,可以通过以下命令打开进程树:
bash 复制代码
systemctl --no-pager --state=running

这将在图形界面下显示当前正在运行的进程及其之间的关系。你可以通过这个图形界面浏览和查看各个进程的状态、PID等信息。

除了systemd工具外,还可以使用其他一些图形界面工具来查看进程树,如htop、glances等。这些工具通常需要在安装后才能使用,你可以通过包管理器进行安装。

相关推荐
最新资讯动态3 分钟前
DialogHub上线OpenHarmony开源社区,高效开发鸿蒙应用弹窗
前端
淬渊阁4 分钟前
汇编学习之《扩展指令指针寄存器》
汇编·学习
lalapanda5 分钟前
UE5学习记录part12
学习·ue5
lvbb6612 分钟前
框架修改思路
前端·javascript·vue.js
树上有只程序猿14 分钟前
Java程序员需要掌握的技术
前端
从零开始学安卓17 分钟前
Kotlin(三) 协程
前端
阿镇吃橙子21 分钟前
一些手写及业务场景处理问题汇总
前端·算法·面试
庸俗今天不摸鱼22 分钟前
【万字总结】前端全方位性能优化指南(九)——FSP(First Screen Paint)像素级分析、RUM+合成监控、Lighthouse CI
前端·性能优化
逆袭的小黄鸭22 分钟前
JavaScript:作用域与作用域链的底层逻辑
前端·javascript·面试