Linux系统进程概念详解(上)

简介: 我们使用top命令查看PID为1的进程为操作系统,所有孤儿进程被1号systemd进程领养,当然也由systemd进程回收

75cc2299478b402a8776135abf918574.png


冯·诺依曼体系结构


冯·诺依曼体系结构(Von Neumann Architecture),又称为普林斯顿体系结构,是一种计算机系统的基本设计范式,由数学家冯·诺依曼于1945年提出。这一体系结构成为现代计算机体系结构的基础,并在计算机发展史上具有重要意义。它的主要特点是将计算机的程序和数据存储在同一存储器中,并使用存储程序的概念,使得计算机可以根据指令序列自动执行程序。


冯·诺依曼体系结构包含以下关键组成部分:


1.中央处理单元(Central Processing Unit,CPU):CPU是计算机的核心,负责执行指令、进行算术和逻辑运算。它包括算术逻辑单元(Arithmetic Logic Unit,ALU)和控制单元(Control Unit)。ALU执行各种算术和逻辑运算,而控制单元负责从内存中取指令、解析指令并控制计算机的其他部件执行指令。


2.存储器(Memory):存储器用于存放程序和数据。冯·诺依曼体系结构使用统一的存储器空间,即指令和数据都存储在同一个存储器中。这使得程序可以像数据一样被读取和处理。


3.输入/输出设备(Input/Output,I/O):I/O设备用于将数据和信息输入计算机或将计算机处理的结果输出到外部世界。这些设备可以是键盘、鼠标、显示器、打印机等。


4.控制流(Control Flow):计算机按照存储在存储器中的指令序列顺序执行。控制单元从存储器中读取指令,并根据指令类型执行适当的操作。指令中的条件分支和循环等控制结构使得程序可以根据条件和需要来改变执行流程。


5.存储程序(StoredProgram):冯·诺依曼体系结构中的重要概念之一是存储程序。程序以二进制指令的形式存储在存储器中,可以被顺序执行,也可以根据条件或跳转指令进行非顺序执行。


冯·诺依曼体系结构的优点在于其简单和通用性。由于程序和数据存储在同一存储器中,程序可以自我修改和操作,这使得计算机具有高度的灵活性和可编程性。这一体系结构为现代计算机的发展奠定了基础,并成为绝大多数通用计算机系统的基本结构。然而,随着技术的发展,也出现了一些其他类型的体系结构,如并行计算、向量处理和现代超大规模集成电路(Very Large Scale Integration, VLSI)技术所采用的结构。


d12804ac2fcd46acb02bd22eb4fb27b7.png


我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

截至目前,我们所认识的计算机,都是有一个个的硬件组件组成:


  1. 输入单元:包括键盘, 鼠标,摄像头,话筒,扫描仪, 写板,磁盘,网卡等
  2. 中央处理器(CPU):含有运算器和控制器等
  3. 输出单元:显示器,打印机,音响,磁盘,网卡等
  4. 存储器:内存


关于冯诺依曼,必须强调几点:


  1. 这里的存储器指的是内存
  2. 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
  3. 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
  4. 所有设备都只能直接和内存打交道。


对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,从上面的硬件组成我们可以知道有些设备既可以做输入设备,又可以做输出设备。比如,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程是怎么样的呢?


首先当然是从键盘输入数据,经过内存,再由内存向cpu发出请求,cpu向内存允许此步操作同时发出指令调用网卡,网卡再通过网线发送数据到聊天软件的云服务器,云服务器发送消息到你朋友电脑上,由网卡进行接收,网卡向内存响应,内存向cpu发出请求,cpu向允许此步操作同时内存发出指令调用显示器,输出消息。


虽然这里表述没有特别详细,但是大概的系统调用的结构,也就是冯·诺依曼体系结构就是这么回事,下面我们再来了解操作系统。


操作系统(Operator System)


91e7fa6440964cf392b345c1d1a9e7ef.png


1.概念


操作系统是一种软件,是计算机系统中最基本和最重要的系统软件之一。它是连接计算机硬件和应用软件之间的桥梁,负责管理和协调计算机的各种资源,为用户和应用程序提供服务,使得计算机能够高效地运行和执行任务。


操作系统的主要功能包括以下几个方面:


1.资源管理:操作系统管理计算机的硬件资源,包括中央处理器(CPU)、内存(RAM)、硬盘、输入/输出设备等。它决定哪个程序可以占用CPU的时间,将内存分配给不同的应用程序,并控制外部设备的访问。


2.进程管理:操作系统管理计算机运行的进程。进程是指在计算机上运行的程序的实例。操作系统负责创建、终止、暂停、恢复进程,并在它们之间进行切换,以实现多任务处理。


3.内存管理:操作系统负责将内存空间分配给不同的进程,并管理内存中的数据和指令。它实现虚拟内存技术,将部分程序和数据存储在辅助存储设备(如硬盘)上,以扩展可用的内存空间。


4.文件系统管理:操作系统管理计算机的文件系统,包括文件的创建、删除、读取和写入。它通过文件系统提供对数据的持久性存储和访问,使得用户可以保存和获取数据。


5.设备驱动程序:操作系统提供设备驱动程序,用于控制和管理硬件设备。这些驱动程序使得操作系统能够与不同类型的硬件设备进行通信。


6.用户界面:**操作系统提供用户界面,使得用户可以与计算机进行交互。**常见的用户界面包括命令行界面和图形用户界面(GUI)。


操作系统还有一些其他重要功能,例如网络管理、安全管理、调度算法等,这些功能使得计算机能够高效地运行多个任务,提高资源利用率,确保数据的安全性和完整性。


不同类型的计算机和设备都需要适配不同的操作系统。目前,市场上最常见的操作系统包括Windows、macOS、Linux等。每种操作系统都有其独特的特点和优势,可以满足不同用户和应用程序的需求。


2.目的


设计操作系统的主要目的是为了管理计算机的硬件资源并提供一个方便、高效、安全和稳定的运行环境,使得计算机能够有效地执行用户程序和应用软件。以下是设计操作系统的主要目的:


1.资源管理:操作系统负责管理计算机的硬件资源,包括中央处理器(CPU)、内存(RAM)、硬盘、输入/输出设备等。它决定如何分配这些资源给不同的应用程序和进程,以实现多任务处理和资源利用的最大化。


2.多任务处理:现代计算机需要能够同时运行多个任务,例如同时运行多个应用程序或处理多个用户请求。操作系统通过进程管理和调度算法,使得多个任务可以在CPU上交替执行,给用户带来了更好的响应和效率。


3.内存管理:操作系统负责将内存空间分配给不同的程序和进程,并在需要时进行内存交换(虚拟内存),以扩展可用的内存空间。这样可以更好地利用内存资源,使得计算机可以处理更大规模的任务。


4.文件系统管理:操作系统提供文件系统来管理数据的持久性存储和访问。它允许用户创建、读取、写入和删除文件,并确保数据的安全性和完整性。


5.用户界面:操作系统提供用户界面,使得用户可以与计算机进行交互,输入指令并获得计算机的输出。这可以是命令行界面或图形用户界面(GUI),提供了友好的方式来操作计算机。


6.安全性:操作系统需要保护计算机系统和用户数据的安全。它通过访问控制、身份验证和权限管理等方式来确保只有授权用户可以访问特定的资源和数据。


7.错误处理:操作系统需要具备一定的错误处理能力,能够检测和处理硬件和软件出现的异常情况,防止系统崩溃和数据损坏。


8.稳定性和可靠性:设计操作系统时需要追求稳定性和可靠性,使得系统能够长时间稳定运行,不容易出现崩溃和故障。


总的来说,设计操作系统的目的是为了让计算机系统能够高效、安全地运行各种应用程序,提供用户友好的交互界面,并最大限度地利用计算机硬件资源,以满足用户的需求并提高计算机系统的整体性能。


3.管理


在实际的操作系统中,为了更高效地管理硬件资源,通常会使用链表或其他高效的数据结构组织struct结构体灵活地添加、删除和修改数据项,非常适合用于管理动态变化的硬件资源。


以下是一个示例,展示操作系统如何使用链表组织struct结构体来管理CPU、内存和硬盘资源:


#include <stdio.h>
#include <stdlib.h>
// 定义CPU的结构体
struct CPU {
    int id;           // CPU编号
    int cores;        // CPU核心数
    double clock;     // CPU主频
    // 其他CPU相关的属性和方法
};
// 定义内存的结构体
struct Memory {
    int id;           // 内存编号
    int size;         // 内存大小(单位:字节)
    // 其他内存相关的属性和方法
};
// 定义硬盘的结构体
struct Disk {
    int id;           // 硬盘编号
    int size;         // 硬盘大小(单位:字节)
    // 其他硬盘相关的属性和方法
};
// 定义操作系统管理的硬件资源链表节点
struct HardwareNode {
    struct CPU cpu;
    struct Memory memory;
    struct Disk disk;
    struct HardwareNode* next; // 指向下一个节点的指针
};
// 定义操作系统的结构体
struct OperatingSystem {
    struct HardwareNode* hardwareList; // 硬件资源链表的头节点
    // 其他操作系统相关的属性和方法
};
// 初始化操作系统硬件资源链表
void initHardwareList(struct OperatingSystem *os) {
    os->hardwareList = NULL; // 初始化为空链表
}
// 添加硬件资源节点到链表
void addHardwareNode(struct OperatingSystem *os, struct HardwareNode* node) {
    if (os->hardwareList == NULL) {
        os->hardwareList = node;
        node->next = NULL;
    } else {
        node->next = os->hardwareList;
        os->hardwareList = node;
    }
}
// 使用链表中的硬件资源进行任务处理
void processTasks(struct OperatingSystem *os) {
    // 遍历链表,处理每个硬件资源节点中的任务
    struct HardwareNode* currentNode = os->hardwareList;
    while (currentNode != NULL) {
        // 处理当前节点中的任务
        // ...
        currentNode = currentNode->next; // 移动到下一个节点
    }
}
// 释放链表资源,避免内存泄漏
void freeHardwareList(struct OperatingSystem *os) {
    struct HardwareNode* currentNode = os->hardwareList;
    while (currentNode != NULL) {
        struct HardwareNode* temp = currentNode;
        currentNode = currentNode->next;
        free(temp); // 释放当前节点
    }
    os->hardwareList = NULL; // 确保链表头指针为空
}
int main() {
    struct OperatingSystem os;
    initHardwareList(&os);
    // 创建CPU、内存、硬盘节点并添加到链表中
    struct HardwareNode* cpuNode = (struct HardwareNode*)malloc(sizeof(struct HardwareNode));
    cpuNode->cpu.id = 1;
    cpuNode->cpu.cores = 4;
    cpuNode->cpu.clock = 2.6;
    addHardwareNode(&os, cpuNode);
    struct HardwareNode* memoryNode = (struct HardwareNode*)malloc(sizeof(struct HardwareNode));
    memoryNode->memory.id = 1;
    memoryNode->memory.size = 8192; // 8 GB
    addHardwareNode(&os, memoryNode);
    struct HardwareNode* diskNode = (struct HardwareNode*)malloc(sizeof(struct HardwareNode));
    diskNode->disk.id = 1;
    diskNode->disk.size = 102400; // 100 GB
    addHardwareNode(&os, diskNode);
    // 使用链表中的硬件资源进行任务处理
    processTasks(&os);
    // 释放链表资源,避免内存泄漏
    freeHardwareList(&os);
    return 0;
}


在上述示例中,我们使用了一个硬件资源链表,其中每个链表节点都包含一个CPU结构体、一个Memory结构体和一个Disk结构体,分别表示不同的硬件资源。通过链表,操作系统可以动态地添加和管理硬件资源,更加灵活高效地处理任务。同时,在程序结束时,通过释放链表资源,避免了内存泄漏问题。实际的操作系统会更复杂和完善,但这个示例演示了如何使用链表组织struct结构体来管理硬件资源。


4.系统调用和库函数概念


1.在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。


2.系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。


acfcba665abd46f78b053e153c695c3c.jpg


进程


进程是计算机中运行的程序的实例。它是操作系统进行资源分配和调度的基本单位,是计算机系统中最基本的执行单元之一。


1.概念


1.程序:程序是一组指令的有序集合,用于完成特定的任务或解决特定的问题。它通常存储在磁盘等存储介质上,是静态的。


2.进程:进程是程序的一次执行过程。当程序被加载到内存中并开始执行时,就会形成一个进程。进程是动态的,具有运行状态、内存空间、寄存器集合、程序计数器等信息。


3.多进程:操作系统支持同时运行多个进程。多进程使得计算机可以同时执行多个任务,每个任务运行在独立的进程中,相互之间互不干扰。


4.进程状态:进程可以处于多种状态,如运行态、就绪态、阻塞态等。运行态表示进程正在执行,就绪态表示进程已准备好运行但还未被调度,阻塞态表示进程由于某种原因(如等待输入/输出完成)而暂时无法执行。


5.进程控制块(PCB):每个进程都有对应的数据结构,称为进程控制块。PCB包含了进程的所有信息,如进程状态、程序计数器、寄存器内容、内存分配、优先级等。操作系统通过PCB来管理和控制进程的执行。


6.进程间通信(IPC):不同的进程可能需要相互通信和共享数据。为了实现进程间的交互,操作系统提供了各种进程间通信机制,如管道、消息队列、共享内存等。


7.进程调度:多个进程争夺CPU时间执行,操作系统需要进行进程调度,决定当前应该执行哪个进程。进程调度算法的设计影响着系统的响应性能和资源利用率。


总结:

进程是程序的一次执行过程,是操作系统资源管理的基本单位。多进程使得计算机可以同时运行多个任务。每个进程有其特有的状态、PCB和执行信息,操作系统通过进程调度来决定如何分配CPU时间和内存。进程间通信允许不同进程之间进行数据交互。进程的概念是操作系统中理解多任务处理和资源管理的重要基础。


2.描述进程-PCB


PCB(Process Control Block)是一种数据结构,用于表示操作系统中的进程。每个进程都有对应的PCB,PCB包含了与该进程相关的所有信息,以便操作系统能够管理和控制进程的执行。PCB通常存储在内核的地址空间中,而不是进程的用户空间。


PCB中包含的信息可以因操作系统的设计而异,但通常包括以下一些关键信息:


1.进程ID(PID):唯一标识操作系统中每个进程的整数值,用于在系统中识别和管理进程。

2.进程状态:表示进程当前的执行状态,常见的状态有运行态(正在执行)、就绪态(准备执行但还未被调度)、阻塞态(等待某事件完成,如输入/输出)等。

3.寄存器集合:保存了进程在被切换出运行时的寄存器内容,包括程序计数器(PC)等。

4.进程优先级:用于调度算法中,表示进程被分配CPU时间的优先级。

5.内存管理信息:包括进程的内存分配情况、页表信息等。

6.文件描述符表:记录了进程打开的文件和管道等的状态和信息。

7.进程统计信息:如运行时间、累计CPU时间、IO操作次数等。


PCB 中包含了进程的状态、标识信息、资源管理信息、上下文信息、调度信息、进程间通信信息等。它是操作系统进行资源分配和调度的基本单位,是操作系统了解和管理进程的关键数据结构。


每当操作系统进行进程切换时,会保存当前进程的上下文信息到其 PCB 中,然后加载下一个进程的上下文信息到 CPU 寄存器中,使得下一个进程能够继续执行。

在Linux操作系统中,进程控制块(PCB)使用的数据结构是task_struct。task_struct是Linux内核中表示进程的数据结构,定义在头文件<linux/sched.h>中。task_struct包含了大量的信息,用于管理进程的所有方面。


在task_struct中,包含了上述提到的一些关键信息,同时还包含了与Linux进程管理相关的其他信息,如调度信息、信号处理信息、进程所属的用户和组等。由于Linux是开源操作系统,task_struct的结构非常复杂,并且在不同版本的内核中可能会有所不同。


总的来说,PCB(或task_struct)是操作系统用来管理进程的重要数据结构,它会被装载到RAM(内存)里且包含了与进程相关的所有信息,帮助操作系统管理进程的执行和资源分配。


task_ struct内容分类


  1. 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  2. 状态: 任务状态,退出代码,退出信号等。
  3. 优先级: 相对于其他进程的优先级。
  4. 程序计数器: 程序中即将被执行的下一条指令的地址。
  5. 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  6. 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  7. I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  8. 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  9. 其他信息


注:


1.在计算机体系结构中,CPU(中央处理器)中的PC(程序计数器)指向当前正在执行的指令的地址。


2.PC是一个特殊的寄存器,它用于存储即将被执行的指令的地址。PC在CPU的运行过程中起着至关重要的作用。当CPU执行一个指令时,它会从PC指向的地址处读取该指令,并根据指令的操作码和操作数执行相应的操作。执行完当前指令后,PC会自动递增,指向下一条即将执行的指令的地址,以便继续执行下一条指令。这样,CPU可以顺序地执行一系列指令,从而完成程序的执行过程。


3.PC的值在指令执行期间不断变化,使得CPU可以按照程序的顺序依次执行指令,实现程序的流程控制。在条件分支和循环等控制结构中,PC的值会根据指令执行的结果来修改,从而跳转到不同的指令地址,实现程序的跳转和转移。


4.PC的保存和切换是实现多任务处理的关键。在多任务操作系统中,每个进程都有自己的PC值。当操作系统进行进程切换时,会将当前进程的PC保存到其进程控制块(PCB)中,然后将下一个进程的PC加载到CPU的PC寄存器中,以便继续执行该进程的指令。


5.总之,CPU中的PC指针是用于存储当前正在执行的指令的地址,并且它在指令执行过程中不断变化,控制着程序的流程和执行顺序。


3.查看进程


进程的信息可以通过ls /proc系统文件夹查看


d46f4c04c84e43fa8816a107470e690c.png


如:要获取PID为1的进程信息,你需要查看/proc/1这个文件夹。


c66843f7cf5a476e9a46df396d03f13b.png


大多数进程信息同样可以使用top和ps这些用户级工具来获取

top


d01f06aee1c94afe894ca8df01315cce.png


top经常用来监控Linux的系统状况,是常用的性能分析工具,能够实时显示系统中各个进程的资源占用情况。

ps


4f78b47da1a144e8ab58ef2beb6e0177.png


但是ps命令通常我们不这么使用,首先建立下面的一段死循环代码,方便我们查看该进程


#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
  while(1){
  sleep(1);
  }
  return 0;
}


生成可执行程序后我们执行,同时输入命令ps axj | head -1 && ps ajx | grep 'test'


f1c2b3dba62f4c58a2dc6c79ebbe4961.png


4.通过系统调用获取进程标示符


首先我们要了解两个系统调用函数getpid()和getppid(),通过man工具我们可以看到以下定义:


函数所需头文件为<sys/types.h>和<unistd.h>


getpid()返回进程ID,即PID

getppid()返回父进程ID,即PPID


6d75955640da49d4a5019cdb0b4d5940.png


接下来我们调用这两个函数


  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <unistd.h>
  4 int main()
  5 {
  6   while(1){
  7    printf("pid: %d\n", getpid());
  8    printf("ppid: %d\n", getppid());
  9    sleep(1);
 10   }                                                                                                         
 11  return 0;                                                  
 12 }

659fabe2ba2646a8857b8fcd8a8c7ea7.png


我们在程序运行时输入指令ls /proc/PID(PPID) -al查看进程信息

子进程


4eceb4588bfd4deea5fbf66743a26a84.png


父进程


3e2ac705048741019e96d275e9de0bac.png


我们可以看到,父进程是bash,也就是Linux系统的shell程序,如果我们输入命令kill -9 PID杀死进程,父进程并不会受影响,但是输入命令kill -9 PPID,杀死父进程也就是shell进程,不但子进程直接被杀死,且将会导致整个所有系统指令瘫痪,使用不了。


3654fac763ab4b19bc2fd499736faf18.png


碰到意外将shell进程杀死的情况不用慌张,我们只需重新对服务器进行连接就ok了,而且我们可以看到,右侧窗口杀死的shell进程并不会影响左侧窗口,这是因为Linux系统在建立不同的连接时,会创建不同的shell进程,所以两个连接互不干扰。我们多建立几个连接后查询bash进程,就会看到多个bash进程在运行,且为不同PID。

命令ps axj | head -1 && ps ajx | grep 'bash'


4c481472462a40d89a90e930532c4de8.png


5.通过系统调用创建进程-fork


首先我们来了解一下fork函数


974739ed51c844aba5c95d97a043e026.png


下面我们来看这段神奇的代码:


  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <unistd.h>
  4 int main()
  5 {
  6    int ret = fork();
  7    if(ret < 0){
  8        perror("fork");
  9        return 1;
 10    }
 11    else if(ret == 0){ //child
 12        printf("I am child : %d!, ret: %d\n", getpid(), ret);
 13    }
 14    else{ //father
 15        printf("I am father : %d!, ret: %d\n", getpid(), ret);
 16    }
 17    sleep(1);
 18    return 0;
 19 }  


我们在学习C/C++语言的时候,都知道函数只能返回一个值,但是fork函数却可以有两个,我们看下面的运行结果


命令while :; do ps axj | head -1 && ps ajx | grep test | grep -v grep;sleep 1;echo "--------------------------";done


86a777bb2da0465fa9c3137c88a2852a.png


那么这是为什么呢,这是因为在执行fork函数时,建立了子进程,也就是说这里同时执行了两个相同的进程,且为父子关系,子进程和父进程共享代码,数据各自开辟空间,具体要在后面我们讲到虚拟地址才能更好的解释。


进程状态


1.Linux内核源代码


为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

下面的状态在kernel源代码里定义:


static const char * const task_state_array[] = {
"R (running)",    /* 0 */
"S (sleeping)",   /* 1 */
"D (disk sleep)",   /* 2 */
"T (stopped)",    /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)",     /* 16 */
"Z (zombie)",     /* 32 */
};


1.R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。


2.S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。


3.D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。


4.T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。


5.X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。


6.Z僵尸状态(zombie):僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)

没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态


2.进程状态查看


R运行状态


  1 #include <stdio.h>  
  2 #include <sys/types.h>  
  3 #include <unistd.h>  
  4 int main()  
  5 {
  6    while(1)
  7    {
  8                                                                                                                                                                                                                                  
  9    }
 10    return 0;
 11 }


9a291963e48e4df7b757292f0f3b6f70.png


S睡眠状态


  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <unistd.h>
  4 int main()
  5 {
  6    while(1)
  7    {
  8       sleep(1);                                                                                             
  9    }                                                             
 10    return 0;                                                     
 11 }  

9cc6768812d342a1b2a917bf2db55808.png


T停止状态(stopped)

代码同上,运行时我们输入暂停进程命令kill -19 PID,再恢复进程kill -18 PID


f585e85d41f848f697c8b15c17752941.png


Z僵尸状态

首先我们来一个创建维持30秒的僵死进程例子:


    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 int main()
    4 {
    5    pid_t id = fork();
    6    if(id < 0){
    7        perror("fork");
    8        return 1;
    9    }
   10    else if(id > 0){ //parent
   11        printf("parent[%d] is sleeping...\n", getpid());
   12        sleep(30);
   13   }else{
   14       printf("child[%d] is begin Z...\n", getpid());
   15       sleep(5);
   16       exit(EXIT_SUCCESS);
   17         
   18   }
   19    return 0;
   20  } 


输入监控指令while :; do ps aux | grep test | grep -v grep; sleep 1;echo "----------------------------"; done


ff50cc08483d4dc6852ed2ffda598de5.png


僵尸进程是已经结束执行的子进程,但其进程控制块(PCB)仍保留在系统中,等待父进程调用系统调用 wait() 或 waitpid() 来获取其退出状态。僵尸进程的存在虽然不会再消耗 CPU 时间或其他资源,但它们仍然具有一定的危害和潜在问题,主要表现在以下几个方面:


1.资源浪费:僵尸进程的 PCB 仍然占用系统内核资源,尤其在大量产生僵尸进程的情况下,会浪费系统内存和进程表的空间。


2.进程数限制:系统对同时存在的进程数量有限制,如果有太多僵尸进程积压,可能会导致系统达到进程数限制,进而影响系统的正常运行。


3.父进程资源泄漏:如果父进程没有正确处理僵尸进程,不调用 wait() 或 waitpid() 来回收子进程资源,可能导致父进程的资源泄漏,尤其是文件描述符等资源没有得到释放。


4.可能导致资源耗尽:如果僵尸进程过多且父进程不进行处理,可能会耗尽进程表资源,导致其他合法进程无法创建。


解决僵尸进程问题的方法是,父进程在子进程退出后调用 wait() 或 waitpid() 等系统调用,回收子进程的资源并获取其退出状态。通过这样的处理,操作系统会将僵尸进程的 PCB 从进程表中移除,避免资源浪费和潜在问题。如果父进程不关心子进程的退出状态,也可以使用 waitpid() 函数的 WNOHANG 参数,让 waitpid() 变为非阻塞模式,即使子进程还未退出,也不会阻塞父进程的执行。


总的来说,虽然僵尸进程不会对系统造成巨大的直接危害,但它们可能会导致资源浪费、进程数限制和父进程资源泄漏等问题,因此在编写程序时,应该正确处理子进程的退出状态,避免产生过多的僵尸进程。


注:


1.X死亡状态(dead)是在程序结束的一瞬间的状态,不好演示。

2.D磁盘休眠状态同样不方便展示,在正常情况下,D 状态的进程数应该是较少的。不过我们可以通过命令ps aux | grep "^D"来显示系统所有的D状态进程。

3.进程是R状态,不一定是在CPU上运行,进程在运行队列中,就是R状态(进程已准备好,等待调度)

4.S状态为浅度休眠(对外部事件可以做出反应),大部分情况下都是这种状态

5.D状态为深度休眠(不可以被杀掉,即便是操作系统,只能等待D状态进程自动醒来,或者是关机重启(可能被卡死))

6.S状态和D状态称为等待状态

7.进程退出,一般不是立刻让OS回收信息,释放进程的所有资源


进程退出后,操作系统通常不会立刻回收进程的所有资源。相反,它会留下一段时间,让父进程或其他相关机制有机会处理退出的进程的信息,可能进行资源回收和善后工作。

在进程退出时,操作系统会做以下一些操作:

--------终止进程的执行:操作系统会立即停止进程的执行,不再分配 CPU 时间给该进程。

--------将退出状态保存在 PCB 中:操作系统会将进程的退出状态(退出码)保存在进程控制块(PCB)中,以便父进程或其他进程可以查询和处理。

--------进程资源回收:操作系统会在一段时间内保留进程的资源,包括内存、文件描述符等,以便其他进程或父进程可以获取退出的进程信息或进行善后处理。这段时间也称为"僵尸状态"(Zombie State)。

--------向父进程发送信号:操作系统会向父进程发送一个 SIGCHLD 信号,通知父进程子进程已经退出,并且可以查询退出状态。

父进程通常会调用系统调用 wait() 或 waitpid() 来等待子进程的退出,并获取其退出状态。一旦父进程调用这些函数,操作系统将回收僵尸进程的资源,并从进程表中移除该进程的 PCB。

在父进程没有处理子进程退出状态时,如果子进程退出,但父进程没有调用 wait() 或 waitpid(),则子进程会进入僵尸状态,PCB 仍然保留在系统中,但资源没有被回收,这会导致资源浪费。

因此,父进程应该在子进程退出后及时处理其退出状态,确保子进程的资源被正确回收,避免僵尸进程的产生。


普通状态与+状态的区别


75bff39814c14ceea11d0f68551e9fb1.png


后台运行时,我们可以输入命令jobs查看后台进程状态fg+对应序号将后台转为前台


690bfeeb8b5a45aea6459af0d58af315.png


kill命令

用于删除执行中的程序或工作

指令:kill [-s <信息名称或编号>][PID] 或 kill [-l <信息编号>]


ee3d9bab1840471bbba7a592e107ef3c.png


接上数字即为对应功能,比如kill -9 PID即为直接终止该进程,要注意的是D状态和Z状态用kill命令是杀不掉的。


进程状态图


1cb6442aaecf4813be25c6c0ed6ff310.png


孤儿进程

**孤儿进程(Orphan Process)**是指在父进程终止或意外退出后,子进程还在继续运行,但此时其父进程已经不存在了。孤儿进程会被操作系统接管,并由systemd进程(centos6.5中的1号进程为initd)(通常是 PID 为 1 的进程)成为其新的父进程。


通常情况下,当一个进程创建子进程后,父进程会等待子进程完成,并调用 wait() 或 waitpid() 等系统调用来获取子进程的退出状态。但是,如果父进程意外退出或提前终止,子进程就会成为孤儿进程。


孤儿进程的创建机制主要涉及以下几个步骤:


1.父进程创建子进程。


2.父进程意外退出或被终止,无法处理子进程的退出状态。


3.子进程继续在系统中运行,但其父进程已经不存在,成为孤儿进程。


4.操作系统将孤儿进程的父进程 ID(PPID)设置为systemd进程的进程 ID,systemd进程成为孤儿进程的新父进程。


由于 init 进程会定期调用 wait() 或类似系统调用来处理已终止的子进程,所以孤儿进程的退出状态最终会被处理,其资源会被回收。因此,孤儿进程并不会像僵尸进程一样导致资源泄漏。


总而言之,孤儿进程是指在其父进程终止后,仍然在系统中运行的进程,其父进程已经不存在。操作系统会将孤儿进程的父进程 ID 设置为systemd进程的进程 ID,使systemd进程成为孤儿进程的新父进程,并最终处理孤儿进程的退出状态。


示例:


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
  pid_t id = fork();
  if(id < 0){
    perror("fork");
    return 1;
  }
  else if(id == 0){//child
    printf("I am child, pid : %d\n", getpid());
    sleep(10);
  }
  else{//parent
    printf("I am parent, pid: %d\n", getpid());
    sleep(3);
    exit(0);
  }
 return 0;
}

40dca2284f1a46809555eaec2c444f3a.png


我们使用top命令查看PID为1的进程为操作系统,所有孤儿进程被1号systemd进程领养,当然也由systemd进程回收


4ae9cae484e04beb8a2550c125b97701.png

相关文章
|
1天前
|
Linux Shell C语言
Linux进程控制——Linux进程程序替换
Linux进程控制——Linux进程程序替换
|
1天前
|
Linux 调度
Linux进程控制——Linux进程等待
Linux进程控制——Linux进程等待
|
1天前
|
存储 缓存 Linux
Linux进程控制——Linux进程终止
Linux进程控制——Linux进程终止
|
1天前
|
安全 Linux 编译器
Linux进程——进程地址空间
Linux进程——进程地址空间
|
1天前
|
Linux Shell 编译器
Linux进程——Linux环境变量
Linux进程——Linux环境变量
|
1天前
|
Linux 调度
Linux进程——Linux进程间切换与命令行参数
Linux进程——Linux进程间切换与命令行参数
|
1天前
|
Linux 调度
Linux进程——Linux进程与进程优先级
Linux进程——Linux进程与进程优先级
|
1天前
|
Linux Shell 调度
Linux进程——Linux下常见的进程状态
Linux进程——Linux下常见的进程状态
|
1天前
|
算法 Linux 调度
Linux进程——进程的创建(fork的原理)
Linux进程——进程的创建(fork的原理)
|
1天前
|
Linux Shell
Linux进程——Linux进程的概念(PCB的理解)
Linux进程——Linux进程的概念(PCB的理解)