当前位置: 首页 > article >正文

[操作系统] 进程的概念与基础操作详解

在现代操作系统中,**进程(Process)**是一个重要的核心概念。它是操作系统管理资源的基本单位,理解进程的概念以及如何操作它是学习操作系统的基础。本篇文章将深入讲解进程的基本概念、结构和一些典型操作。


什么是进程?

前文所提:应用程序从磁盘加载进内存,而操作系统的管理方法是描述 + 组织,所以通过该种管理方法形成的管理对象就是进程。

从用户的视角来看,进程是一个程序的运行实例;从操作系统的视角来看,进程是一个拥有资源分配能力的实体。

进程 = 内核数据结构对象 + 自己的代码和数据

在Linux中进程可以看做是PCB(task struct)和自己的代码和数据组成的。PCB中包含该进程的所有属性,与代码以及数据共同组成进程,PCB中存在指向其他进程的指针,通过指针的指向,进程通过双向链表的数据结构来进行链接,而进程的管理就是对链表的增删查改。并且每个进程都有独立的地址空间,以避免相互干扰。

我们所使用的指令、工具以及自己的程序,运行起来,都是进程!

进程控制块(PCB)

操作系统使用**进程控制块(Process Control Block, PCB)**来描述和管理进程的所有信息。PCB 是一个重要的数据结构,操作系统通过它来追踪每个进程的状态。

在 Linux 操作系统中,PCB 被实现为一个名为 task_struct 的结构体,其主要内容包括:

PCB 的主要内容分类

  • 标识符:如进程 ID (PID),用于唯一标识进程。
  • 状态:包括进程当前的运行状态(运行、就绪、阻塞等)。
  • 优先级:用于调度时比较不同进程的重要性。(CPU计算的优先级)
  • 程序计数器:存储下一条将要执行的指令地址。
  • 内存指针:指向进程的代码段、数据段以及共享内存块。
  • 上下文数据:包括处理器寄存器中的数据。
  • I/O 状态信息:描述进程使用的文件和 I/O 设备。
  • 记账信息:记录进程使用的资源总量和时间。

PCB 的组织结构

在 Linux 内核中,所有进程的 PCB 以链表形式组织。通过 task_struct 中的 nextprev 指针,形成一个双向链表,对进程进行遍历和管理。

如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如何查看进程信息

在 Linux 系统中,可以通过 /proc 文件系统以及用户级工具来查看进程信息:

通过 /proc 文件夹

  • 每个进程在 /proc 中都有一个对应的文件夹,文件夹名称是该进程的 PID。
  • 数字进程目录是针对单个进程的详细信息存储,字母进程目录(或文件)是关于系统整体信息的汇总。
  • 例如,要查看 PID 为 1 的进程信息,可以访问 /proc/1

通过命令行工具

  • ps** 命令**:显示进程的详细信息。

bash就是命令行解释器,每启动一个XShell就会有一个bash进程启动,所以输入的指令等信息都是通过父进程bash处理的,所以当使用命令行启动多个进程后可以发现它们的父进程(PPID)都是bash

  • top** 命令**:实时显示系统运行的进程和资源使用情况。

通过系统调用获取进程标识符

进程id(PID) :**<font style="color:rgb(100,106,115);">getpid();</font>**

⽗进程id(PPID): **<font style="color:rgb(100,106,115);">getppid();</font>**

sys/types.h包含获取当前进程ID的函数,比如使用getpid();获取当前进程的PID:

进程的cwdexe

  1. 现在将进程启动。

  1. 通过指令查看进程是否存在。

grep作为指令也是进程,所以显示的时候也会显示grep的进程信息。

  1. 查看进程具体信息。


/proc/[PID]目录下的cwdexe是与进程相关的重要符号链接,它们分别代表了进程的当前工作目录和可执行文件路径。理解这两个概念对于深入掌握进程的行为和状态非常有帮助。

cwd(Current Working Directory)

  • 定义
    • cwd是一个符号链接,指向进程的当前工作目录。当前工作目录是指进程在执行过程中,其相对路径的基准目录。就好比你在终端中切换到某个目录,然后运行一个程序,这个被切换到的目录就是程序的当前工作目录。
    • 例如,假设你在/home/user/projects目录下启动了一个名为my_app的程序,那么/proc/[PID]/cwd就会指向/home/user/projects目录。
    • 使用chdir可以改变cwd的指向路径。
  • 作用和用途
    • 文件访问基准:当进程尝试打开一个相对路径的文件时,这个相对路径是相对于cwd来解析的。比如,如果my_app程序尝试创建data.txt文件,直接使用(./data.txt)而没有指定绝对路径,那么系统会直接在/home/user/projects下建立/home/user/projects/data.txt(假设cwd/home/user/projects)。
    • 监控和调试:对于系统管理员和开发者来说,通过查看cwd可以了解进程是在哪个目录下运行的,这对于调试程序(特别是当程序试图访问文件时出现路径错误等问题)和监控进程行为非常有用。例如,如果一个进程试图访问一个不存在的文件并报错,查看cwd可以帮助确定它试图访问文件的完整路径,从而更容易地找到问题所在。

exe(Executable)

  • 定义
    • exe是一个符号链接,指向启动该进程的可执行文件的路径。这个可执行文件是进程运行的主体,包含了程序的机器代码和资源。
    • 例如,如果你使用命令/usr/bin/my_app启动了一个程序,那么/proc/[PID]/exe就会指向/usr/bin/my_app
  • 作用和用途
    • 程序识别:通过exe链接,你可以清楚地知道是哪个可执行文件启动了这个进程。这对于系统监控工具来说非常重要,因为它们可以根据可执行文件的路径来识别和分类进程。例如,在一个包含多个不同版本应用程序的系统中,通过exe可以区分是哪个版本的应用程序正在运行。
    • 安全和审计:在安全审计方面,exe可以帮助确定是否有未经授权的程序在运行。如果发现exe指向一个不熟悉或可疑的路径,这可能是一个安全风险的信号。此外,它也可以用于追踪软件的使用情况,比如统计某个特定可执行文件被启动的次数等。
    • 重新启动和分析:对于开发者来说,如果需要重新启动进程或对进程进行分析(如性能分析),知道exe的路径是非常有用的。可以直接通过这个路径来启动新的进程实例,或者使用调试工具(如gdb)附加到这个可执行文件上进行分析。

实际应用示例

假设你正在运行一个名为example_app的程序,你可以在终端中使用以下命令来查看其cwdexe

pid=$(pgrep example_app)  # 获取example_app进程的PID
ls -l /proc/$pid/cwd      # 查看cwd链接
ls -l /proc/$pid/exe      # 查看exe链接

这将输出类似以下内容:

lrwxrwxrwx 1 user user 0 Jan  1 12:34 /proc/1234/cwd -> /home/user/projects
lrwxrwxrwx 1 user user 0 Jan  1 12:34 /proc/1234/exe -> /usr/local/bin/example_app

从这个输出中,你可以看到example_app进程的当前工作目录是/home/user/projects,而其可执行文件位于/usr/local/bin/example_app。这些信息对于理解进程的行为和进行系统管理非常关键。

认识fork以及进程的独立

进程的创建和管理是操作系统的重要功能。在 Linux 中,创建进程主要通过 fork() 系统调用。

通过man查看fork():

  • 返回值为pid_t类型
  • 包含在头文件<unistd.h>

获取进程和父进程的标识符

可以通过以下代码获取进程的 PID 和其父进程的 PPID:

PID:getpid();

PPID:getppid();

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

int main() {
    printf("pid: %d\n", getpid());
    printf("ppid: %d\n", getppid());
    return 0;
}

如何创建子进程以及父子进程关系的理解

fork() 是 Linux 中用于创建新进程的函数。

得到的运行结果如下:

可以看出,在fork();执行后出现了两个进程,其中一个进程的pid是fork前的进程的pid,一个是新进程的pid。此时就成功的创建了子进程。但是要如何使用fork()呢?


首先从fork()函数本身开始理解:

以下是一个代码示例。

printf("父进程开始运行,pid:%d \n", getpid());

pid_t id = fork(); // 父子进程的独立过程是在调用 fork() 函数时完成,之后父子进程独立

if(id < 0)
{
	perror("fork");
	return 1;
}	
else if(id == 0)
{
	// child 
	while(1)
	{
		sleep(1);
		printf("我是一个子进程!我的pid:%d,我的父进程id:%d\n",getpid(), getppid());
	}
}
else
{
	// father
	while(1)
	{
		sleep(1);
		printf("我是父进程!我的pid:%d,我的父进程id:%d\n",getpid(), getppid());
	}
}

运行结果如下:

fork()执行后创建了子进程,并且同上文所讲相同,父进程的父进程是bash进程。

什么是 fork()

fork() 是用于创建进程的系统调用。

  • 它会从当前运行的进程(称为父进程)中复制出一个几乎完全相同的新进程(称为子进程)。
  • 父子进程几乎完全独立,但共享相同的代码段
  • 父子进程拥有不同的内存空间,彼此之间不影响。

fork() 的返回值

fork() 返回两个值,因为它在两个进程中执行,分别是:

  1. 在父进程中fork() 返回子进程的 PID(进程 ID),这是一个正整数(> 0)。
  2. 在子进程中fork() 返回 0
  3. 创建子进程失败返回-1。
为什么 **fork()** 有两个返回值?

操作系统在执行 fork() 时,会基于当前父进程的状态,创建一个几乎完全相同的子进程。

fork() 会把当前的程序和运行环境复制一份,创建一个新的进程。在fork()函数内,return也是代码语句,所以也会作为拷贝的代码,申请新的PCB,拷贝父进程的PCB给子进程。在fork中通过区分父子进程后,通过return返回两个返回值,两个返回值都对id进行修改,对变量进行修改,触发了写时拷贝,因此系统会进行空间及数据的分配。这就是为什么返回两个返回值的原因,下文会对该过程进行详细讲解。

  • **父进程调用 ****fork()**,操作系统知道它是父进程,所以返回子进程的 PID,方便父进程管理。
  • **子进程调用 ****fork()**,它的视角是:我是子进程,我没有子进程,所以返回 0

注意:

  • fork() 的执行结果是两套完全独立的运行环境
  • fork() 的返回值是区分父进程和子进程的关键。

进程独立的过程详解

父子进程的独立过程是在调用 **fork()** 函数时完成的。具体地说,当 fork() 被调用时,操作系统会执行以下步骤,从而使父进程和子进程完全独立:

进程复制的时机

  • **fork()**** 的调用时刻**:操作系统在执行 fork() 时,会基于当前父进程的状态,创建一个几乎完全相同的子进程。

进程复制的内容

  • 进程控制块(PCB)
    • 操作系统为子进程分配新的 PCB,记录子进程的状态信息(如进程号 PID、父进程号 PPID 等)。
    • 子进程的 PCB 是从父进程的 PCB 复制的,因此子进程最初看起来与父进程完全相同。
  • 地址空间
    • 操作系统复制父进程的内存结构给子进程,形成一份几乎完全相同的内存空间。这包括:
      • 代码段:子进程共享父进程的代码段(只读)。
      • 数据段:父进程中的全局变量和静态变量会被复制到子进程。
      • 堆和栈:子进程的堆和栈也被复制,但它们的内存分配是独立的。
  • 文件描述符
    • 父进程打开的所有文件描述符会被子进程继承,两者对同一文件的操作是共享的(文件偏移量同步)。

父子进程何时独立?

一旦 fork() 返回,父子进程开始独立运行:

  • 子进程的内存空间是父进程的副本,但它与父进程完全分离,修改变量不会相互影响。
  • 子进程和父进程的执行流从 fork() 的返回值处分叉:
    • 父进程继续运行时,fork() 返回子进程的 PID。
    • 子进程继续运行时,fork() 返回 0

父子进程的独立性体现在以下几点:

  1. 内存空间独立
    • 虽然子进程初始时与父进程的内存内容相同,但它的地址空间是独立的,修改子进程的内存不会影响父进程。
  2. PID 和资源独立
    • 子进程有自己的 PID,调度策略也可能不同。
    • 子进程的状态和运行不会直接影响父进程。
  3. 文件描述符共享但独立操作
    • 父子进程共享文件描述符,但可以独立关闭或操作文件。

独立的实现机制:写时复制(Copy-on-Write, COW)

现代操作系统使用了一种优化机制,叫做 写时复制(COW),以减少不必要的资源浪费:

  • **fork()** 刚返回时,父子进程共享相同的物理内存页(只读),因此复制过程很快。
  • 当父进程或子进程试图修改内存时
    • 操作系统会为需要修改的部分分配新的物理内存。
    • 修改后的内存空间对父子进程来说是独立的。

因此,只有在需要时,内存的独立性才真正实现,也就是需要对对内存中数据进行修改的时候,但逻辑上,父子进程从 fork() 返回后就已经被视为完全独立了。

流程图

调用 fork() 后,父子进程的分离流程可以表示如下:

父进程:
  ret = fork();               // 返回子进程 PID (> 0)
  ------------------------------
 |   父进程逻辑                |
 |   printf("父进程部分");      |
 |   独立运行,继续父进程代码   |
  ------------------------------

子进程:
  ret = fork();               // 返回 0
  ------------------------------
 |   子进程逻辑                |
 |   printf("子进程部分");      |
 |   独立运行,继续子进程代码   |
  ------------------------------

写时拷贝修改ret内容,进程独立。

总结:操作系统完成进程独立的过程

  • **fork()**** 是操作系统分离父子进程的起点**。
  • 通过资源复制、地址空间分离和调度机制,父子进程实现了完全独立
  • 父子进程虽然共享代码和部分资源,但内存、PID 和运行状态是互相独立的,确保了它们可以并发执行,互不干扰。
  • 写时拷贝:当父子进程尝试修改共享数据时,操作系统会将数据复制到独立空间。

基本的独立靠的是**struct task_struct(PCB)**独立。

当父子进程任何一方进行数据修改的时候触发写时拷贝,操作系统就把修改的数据在底层拷贝一份,让整个目标进程修改这个拷贝,脱离代码共享,实现完全独立


http://www.kler.cn/a/508858.html

相关文章:

  • 【逆境中绽放:万字回顾2024我在挑战中突破自我】
  • 【数据库】MySQL数据库SQL语句汇总
  • python mysql库的三个库mysqlclient mysql-connector-python pymysql如何选择,他们之间的区别
  • idea 如何安装 github copilot
  • css盒子水平垂直居中
  • SQLite 3.48.0 发布,有哪些更新?
  • 5 分钟复刻你的声音,一键实现 GPT-Sovits 模型部署
  • SSH config
  • 麒麟v10 安装php5.6
  • 第83期 | GPTSecurity周报
  • Linux的常用命令(一)
  • 在Mac mini上实现本地话部署AI和知识库
  • C++实现设计模式--- 观察者模式 (Observer)
  • 从 JIRA 数据到可视化洞察:使用 Python 创建自定义图表
  • yolo训练数据集样本的标签形状一致是什么意思
  • ReactiveSwift 简单使用
  • ThreeJS能力演示——界面点选交互能力
  • 探索基于机器学习的信用评分:从数据到洞察
  • Android BottomNavigationView不加icon使text垂直居中,完美解决。
  • PyTorch使用教程(4)-torch.nn
  • PCL 计算多边形的面积【2025最新版】
  • Redisson分布式锁的原理和实践?
  • 0基础跟德姆(dom)一起学AI 自然语言处理16-输入部分实现
  • Kotlin Bytedeco OpenCV 图像图像55 图像透视变换
  • macOS docker hub / docker desktop替代方案
  • 逻辑结构与存储结构