从入门到精通:详解Linux进程管理

前言

在这篇文章中,我将带领大家深入学习和理解Linux系统中的进程管理。无论你是初学者还是有一定经验的开发者,相信这篇文章都会对你有所帮助。我们将详细讲解冯诺依曼体系结构、操作系统概念、进程管理、进程调度、进程状态、环境变量、内存管理以及其他相关内容。

冯诺依曼体系结构

概述

冯诺依曼体系结构是现代计算机系统的基础。它由数学家兼物理学家冯·诺依曼于1945年提出,至今仍被广泛应用于各种计算机系统中。冯诺依曼体系结构的核心思想是将程序和数据存储在同一存储器中,并由中央处理器(CPU)按顺序读取和执行指令。通过这种方式,计算机系统能够以更高效、更灵活的方式运行各种应用程序。

组成部分

冯诺依曼体系结构由以下几个主要部分组成:

  1. 输入单元:包括键盘、鼠标、扫描仪等设备,用于向计算机输入数据和指令。
  2. 中央处理器(CPU):包含运算器和控制器,用于执行指令和处理数据。运算器负责执行各种算术和逻辑运算,控制器负责指挥和协调各个部分的工作。
  3. 内存:用于存储程序和数据。内存分为随机存取存储器(RAM)和只读存储器(ROM),RAM用于存储正在运行的程序和数据,ROM用于存储固化的程序和数据。
  4. 输出单元:包括显示器、打印机等设备,用于输出计算结果和信息。

数据流动过程

在冯诺依曼体系结构中,所有数据的输入和输出都必须经过内存。具体来说,数据流动过程如下:

  1. 用户通过输入单元(如键盘)输入数据。
  2. 数据被存储在内存中。
  3. CPU从内存中读取指令和数据,并进行处理。
  4. 处理结果被写入内存。
  5. 输出单元(如显示器)从内存中读取结果并显示给用户。

这种数据流动方式确保了计算机系统的统一和高效。以QQ聊天为例,当你登录QQ并与好友聊天时,输入的信息首先被存储在内存中,CPU从内存中读取并处理这些信息,处理后的信息再次存储在内存中,最后通过显示器输出。若你发送文件,文件数据也会经过相同的路径流动,确保信息传递的可靠性。

操作系统(Operating System)

概念

操作系统(OS)是管理计算机硬件和软件资源的系统软件,负责为用户提供一个良好的操作环境。操作系统的核心部分是内核,它负责进程管理、内存管理、文件管理和驱动管理等。此外,操作系统还包括一些其他程序,如函数库和Shell程序。操作系统的功能可以概括为两个方面:资源管理和用户接口。

设计目的

操作系统的设计目的是:

  1. 与硬件交互:管理计算机的所有硬件资源,如CPU、内存、磁盘和输入输出设备。操作系统通过设备驱动程序与硬件进行交互,确保硬件设备能够被正确使用。
  2. 提供执行环境:为用户程序(应用程序)提供一个良好的执行环境,使用户能够方便地开发和运行应用程序。操作系统提供了丰富的系统调用和库函数,简化了应用程序的开发过程。

定位

在计算机软硬件架构中,操作系统的定位是一款“管理”软件。它通过描述和组织被管理对象,实现对系统资源的有效管理。例如,操作系统通过使用结构体(struct)描述硬件资源,通过链表或其他高效数据结构组织这些资源,从而实现对资源的管理。

系统调用和库函数

操作系统通过系统调用向上层开发者暴露部分接口,供其使用。系统调用提供了基本的功能,而库函数对系统调用进行了封装,提供了更高层次的接口,方便用户进行二次开发。例如,文件操作的系统调用包括openreadwrite等,而C标准库中的fopenfreadfwrite等函数则对这些系统调用进行了封装,使得文件操作更加方便和易于理解。

进程(Process)

基本概念

进程是程序的一个执行实例,代表正在运行的程序。进程是操作系统资源分配的基本单位,负责管理CPU时间、内存和其他资源。在内核中,进程被描述为一个分配系统资源的实体。每个进程都有自己独立的地址空间、堆栈以及文件描述符表。

描述进程—PCB

进程信息存储在一个叫做进程控制块(PCB)的数据结构中。PCB包含了进程的所有属性,是操作系统管理进程的核心数据结构。在Linux操作系统中,PCB被实现为task_struct结构体。

task_struct内容分类

task_struct包含以下内容:

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

组织进程

在Linux内核中,所有运行的进程都以task_struct链表的形式存在内核中。通过这种方式,操作系统可以高效地管理和调度进程。每个task_struct结构体都包含指向下一个进程的指针,这样所有进程就形成了一个双向链表,操作系统可以方便地遍历和管理这些进程。

查看进程

用户可以通过/proc文件系统查看进程的信息。例如,要获取PID为1的进程信息,可以查看/proc/1文件夹。此外,用户还可以使用topps等命令行工具获取进程信息。ps命令可以显示系统中所有正在运行的进程及其详细信息,而top命令则可以动态地显示系统资源的使用情况和进程状态。

示例代码

以下示例代码展示了如何使用ps命令查看系统中所有进程的信息:

ps -aux

该命令输出的信息包括进程ID、用户ID、CPU使用率、内存使用率、进程状态、命令名称等。

进程状态

进程的不同状态

在Linux内核中,进程可以处于以下几种状态:

  1. R(运行状态):表明进程正在运行或在运行队列中等待运行。
  2. S(睡眠状态):表明进程在等待事件完成,有时也称为可中断睡眠(interruptible sleep)。
  3. D(磁盘休眠状态):有时也称为不可中断睡眠状态(uninterruptible sleep),通常等待I/O操作完成。
  4. T(停止状态):进程被停止,可以通过发送SIGSTOP信号暂停进程,通过SIGCONT信号恢复运行。
  5. X(死亡状态):进程已经终止,不会出现在任务列表中。
  6. Z(僵尸状态):进程已经终止,但其退出状态还没有被父进程读取,保持在进程表中。

查看进程状态

用户可以通过pstop等命令查看进程状态。例如,使用ps aux命令可以查看系统中所有进程及其状态。以下是ps aux命令的示例输出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  22568  1196 ?        Ss   10:00   0:01 /sbin/init
root       672  0.0

  0.3  37644  3312 ?        Ss   10:00   0:02 /usr/sbin/sshd

在输出信息中,STAT字段表示进程的状态。例如,Ss表示进程处于睡眠状态且是会话领导进程,R表示进程正在运行。

示例代码

以下示例代码展示了如何使用top命令动态查看系统资源使用情况和进程状态:

top

top命令界面中,用户可以看到系统的总体资源使用情况,包括CPU、内存和交换分区的使用率,以及所有正在运行的进程的信息。用户可以通过按k键终止进程,通过按r键调整进程的优先级。

僵尸进程(Zombie Process)

概念与形成原因

僵尸进程是已经终止但其退出状态尚未被父进程读取的进程。当子进程退出后,父进程需要通过waitwaitpid系统调用读取子进程的退出状态,否则子进程会保持在僵尸状态。僵尸进程的出现是由于父进程没有及时回收子进程的资源,导致子进程的信息无法从系统中清除。

危害

僵尸进程会占用系统资源,特别是进程控制块(PCB)中的内存资源。如果大量僵尸进程存在,会导致系统资源枯竭,影响系统性能和稳定性。此外,僵尸进程的存在还可能影响系统的正常运行和维护,因为系统管理员可能会误以为这些进程仍在运行。

解决方法

通过在父进程中使用waitwaitpid函数可以避免僵尸进程。例如,父进程可以在子进程终止时调用wait函数读取子进程的退出状态,从而释放其占用的资源。以下是一个示例代码,展示了如何在父进程中使用wait函数回收子进程的资源:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        printf("Child process\n");
        sleep(2);
        exit(0);
    } else {
        printf("Parent process\n");
        wait(NULL);  // 回收子进程资源
        printf("Child process terminated\n");
    }
    return 0;
}

在这个示例中,父进程通过调用wait函数等待子进程终止,并回收其资源,避免了僵尸进程的产生。

孤儿进程(Orphan Process)

概念与形成原因

孤儿进程是其父进程已经终止,但子进程仍在运行的进程。孤儿进程会被系统的1号进程(init进程)收养,并由init进程负责回收资源。孤儿进程的产生通常是由于父进程异常终止或故意终止,而子进程仍需要继续执行其任务。

危害与处理

孤儿进程不会对系统造成危害,因为它们会被init进程收养并管理。操作系统通过这种机制确保所有进程都能被正确管理和回收。以下是一个示例代码,展示了孤儿进程的形成过程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        sleep(5);  // 保证子进程在父进程退出后继续运行
        printf("Child process: parent PID = %d\n", getppid());
        exit(0);
    } else {
        // 父进程
        printf("Parent process\n");
        exit(0);  // 父进程立即退出
    }
    return 0;
}

在这个示例中,父进程立即退出,子进程在父进程退出后继续运行,此时子进程成为孤儿进程,并被init进程收养。通过在子进程中打印父进程的PID,可以验证子进程在成为孤儿进程后,其父进程PID会变为1(init进程的PID)。

进程优先级

基本概念

进程优先级决定了进程获得CPU时间的先后顺序。优先级高的进程优先获得CPU资源,从而更快地执行。Linux系统中,用户可以通过调整进程的nice值来改变进程的优先级。nice值的范围为-20到19,值越小优先级越高。

查看进程优先级

用户可以使用ps -l命令查看进程的优先级和nice值。例如:

ps -l

输出信息中包含以下重要字段:

  • UID:执行者的身份。
  • PID:进程ID。
  • PPID:父进程ID。
  • PRI:进程的优先级,值越小优先级越高。
  • NI:进程的nice值。

调整进程优先级

用户可以使用nice命令启动一个具有特定优先级的进程,也可以使用renice命令调整已有进程的优先级。例如:

nice -n 10 ./myprogram
renice -n -5 -p 12345

以下是一个示例代码,展示了如何使用nice命令启动一个具有特定优先级的进程:

nice -n -10 ./myprogram

在这个示例中,myprogram程序将以较高的优先级运行,因为其nice值被设置为-10。

示例代码

以下是一个完整的示例代码,展示了如何调整进程的优先级并查看其效果:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        int ret = nice(-10);  // 设置较高优先级
        if (ret == -1) {
            perror("nice");
        }
        printf("Child process: nice value = %d\n", ret);
        while (1) {
            // 子进程持续运行,观察优先级的影响
        }
    } else {
        // 父进程
        printf("Parent process\n");
        while (1) {
            // 父进程持续运行,观察优先级的影响
        }
    }
    return 0;
}

运行该程序后,可以使用ps -l命令查看子进程和父进程的优先级和nice值,并观察其在系统中的表现。

环境变量

基本概念

环境变量是操作系统中用来指定操作系统运行环境的一些参数。在编写C/C++代码时,编译器可以通过环境变量查找所需的动态或静态库。环境变量通常具有全局特性,可以影响系统中的所有进程。

常见环境变量

  • PATH:指定命令的搜索路径。当用户在终端中输入命令时,系统会在PATH指定的目录中搜索可执行文件。
  • HOME:指定用户的主工作目录,即用户登录到系统后的默认目录。
  • SHELL:指定当前Shell的路径,通常是/bin/bash

查看和设置环境变量

用户可以使用以下命令查看和设置环境变量:

echo $PATH  # 查看PATH环境变量
export MYVAR="Hello, World!"  # 设置环境变量
unset MYVAR  # 清除环境变量
env  # 显示所有环境变量

示例代码

以下是一个示例代码,展示了如何在程序中获取和设置环境变量:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *path = getenv("PATH");
    if (path) {
        printf("PATH: %s\n", path);
    }
    setenv("MYVAR", "Hello, World!", 1);
    printf("MYVAR: %s\n", getenv("MYVAR"));
    return 0;
}

在这个示例中,程序首先获取并打印PATH环境变量的值,然后设置一个新的环境变量MYVAR并打印其值。

环境变量的全局属性

环境变量通常具有全局属性,可以被子进程继承。例如,通过export命令设置的环境变量可以在子进程

中访问:

export MYVAR="Hello, World!"
./myprogram

子进程运行时可以访问并打印MYVAR的值。

示例代码

以下是一个示例代码,展示了环境变量的全局属性:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process: MYVAR = %s\n", getenv("MYVAR"));
    } else {
        // 父进程
        printf("Parent process\n");
        setenv("MYVAR", "Hello from parent", 1);
        wait(NULL);  // 等待子进程终止
    }
    return 0;
}

在这个示例中,父进程设置了一个环境变量MYVAR,子进程继承并打印了该环境变量的值。

进程地址空间

基本概念

进程地址空间是操作系统为每个进程分配的虚拟内存空间。在32位系统中,进程地址空间通常为4GB。地址空间分为用户空间和内核空间,用户空间用于存放用户程序和数据,内核空间用于存放操作系统内核和内核数据。

虚拟地址与物理地址

虚拟地址是用户程序看到的地址,而物理地址是内存中的实际地址。操作系统通过页表将虚拟地址映射到物理地址,确保程序在运行时能够正确访问内存。

进程地址空间布局

进程地址空间通常包含以下几部分:

  1. 代码段:存放程序代码。
  2. 数据段:存放全局变量和静态变量。
  3. :用于动态内存分配。
  4. :用于函数调用时存放局部变量和返回地址。

示例代码

以下是一个简单的示例,展示了进程地址空间的使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int global_var = 0;

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {  // 子进程
        global_var = 100;
        printf("Child: %d, %p\n", global_var, &global_var);
    } else {  // 父进程
        sleep(1);
        printf("Parent: %d, %p\n", global_var, &global_var);
    }
    return 0;
}

运行该程序会显示父子进程中变量地址相同但值不同的现象,说明虚拟地址相同但物理地址不同。通过这个示例,可以理解虚拟地址和物理地址的区别,以及进程地址空间的布局。

进程调度

调度算法

Linux内核使用多种调度算法来管理进程的执行顺序。常见的调度算法包括先来先服务(FCFS)、最短作业优先(SJF)、优先级调度(Priority Scheduling)和时间片轮转(Round Robin)。这些算法各有优缺点,适用于不同的场景和需求。

O(1)调度算法

Linux 2.6内核采用了O(1)调度算法,该算法确保调度操作的时间复杂度为常数,不随进程数量增加而增加。O(1)调度算法使用两个队列来管理进程:活动队列和过期队列。活动队列存放正在运行或准备运行的进程,过期队列存放时间片已耗尽的进程。

活动队列与过期队列

  • 活动队列:存放正在运行或准备运行的进程。调度器从活动队列中选择优先级最高的进程进行调度。
  • 过期队列:存放时间片已耗尽的进程。当活动队列中的进程全部运行完毕后,调度器会将活动队列和过期队列交换,重新开始调度。

示例代码

以下是一个示例代码,展示了如何在Linux内核中实现进程调度:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid1, pid2;
    pid1 = fork();
    if (pid1 == 0) {
        // 子进程1
        while (1) {
            printf("Child 1 running\n");
            sleep(1);
        }
    } else {
        pid2 = fork();
        if (pid2 == 0) {
            // 子进程2
            while (1) {
                printf("Child 2 running\n");
                sleep(1);
            }
        } else {
            // 父进程
            while (1) {
                printf("Parent running\n");
                sleep(1);
            }
        }
    }
    return 0;
}

运行该程序后,可以观察到父进程和两个子进程轮流执行,展示了时间片轮转调度的效果。

环境变量的组织方式

环境表

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以\0结尾的环境字符串。

获取和设置环境变量

用户可以通过系统调用或库函数获取和设置环境变量。例如,使用getenvsetenv函数可以访问特定的环境变量:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *path = getenv("PATH");
    if (path) {
        printf("PATH: %s\n", path);
    }
    setenv("MYVAR", "Hello, World!", 1);
    printf("MYVAR: %s\n", getenv("MYVAR"));
    return 0;
}

在这个示例中,程序首先获取并打印PATH环境变量的值,然后设置一个新的环境变量MYVAR并打印其值。

环境变量的全局属性

环境变量通常具有全局属性,可以被子进程继承。例如,通过export命令设置的环境变量可以在子进程中访问:

export MYVAR="Hello, World!"
./myprogram

子进程运行时可以访问并打印MYVAR的值。

示例代码

以下是一个示例代码,展示了环境变量的全局属性:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process: MYVAR = %s\n", getenv("MYVAR"));
    } else {
        // 父进程
        printf("Parent process\n");
        setenv("MYVAR", "Hello from parent", 1);
        wait(NULL);  // 等待子进程终止
    }
    return 0;
}

在这个示例中,父进程设置了一个环境变量MYVAR,子进程继承并打印了该环境变量的值。

进程内存映像

程序地址空间回顾

程序地址空间通常包含代码段、数据段、堆和栈。在32位系统中,地址空间分为用户空间和内核空间。用户空间用于存放用户程序和数据,内核空间用于存放操作系统内核和内核数据。

虚拟地址与物理地址

虚拟地址是用户程序看到的地址,而物理地址是内存中的实际地址。操作系统通过页表将虚拟地址映射到物理地址,确保程序在运行时能够正确访问内存。

进程地址空间布局

进程地址空间通常包含以下几部分:

  1. 代码段:存放程序代码。
  2. 数据段:存放全局变量和静态变量。
  3. :用于动态内存分配。
  4. :用于函数调用时存放局部变量和返回地址。

示例代码

以下是一个简单的示例,展示了进程地址空间的使用:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int global_var = 0;

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {  // 子进程
        global_var = 100;
        printf("Child: %d, %p\n", global_var, &global_var);
    } else {  // 父进程
        sleep(1);
        printf("Parent: %d, %p\n", global_var, &global_var);
    }
    return 0;
}

运行该程序会显示父子进程中变量地址

相同但值不同的现象,说明虚拟地址相同但物理地址不同。通过这个示例,可以理解虚拟地址和物理地址的区别,以及进程地址空间的布局。

总结

通过本文的学习,我们详细介绍了Linux系统中的进程管理。从冯诺依曼体系结构、操作系统概念、进程管理、进程调度、进程状态、环境变量、内存管理等多个方面进行了深入讲解。掌握这些知识,可以帮助我们更高效地管理和使用Linux系统。希望这篇文章对大家有所帮助。如果有任何问题或建议,欢迎在评论区留言与我交流。感谢大家的阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天进步亿丢丢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值