问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

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

创作时间:
作者:
@小白创作中心

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

引用
1
来源
1.
https://www.cnblogs.com/kevinbee/p/18678186

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

什么是进程?

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

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

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

在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
中的
next

prev
指针,形成一个双向链表,对进程进行遍历和管理。

如下图所示:

如何查看进程信息

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

通过

/proc
文件夹

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

通过命令行工具

  • ps 命令:显示进程的详细信息。
    bash
    就是命令行解释器
    ,每启动一个XShell就会有一个bash进程启动,所以输入的指令等信息都是通过父进程
    bash
    处理的,所以当使用命令行启动多个进程后可以发现它们的父进程(PPID)都是
    bash
  • top 命令:实时显示系统运行的进程和资源使用情况。

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

进程id(PID) :
getpid();
⽗进程id(PPID):
getppid();


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

进程的

cwd

exe

  1. 现在将进程启动。
  2. 通过指令查看进程是否存在。
    grep
    作为指令也是进程,所以显示的时候也会显示
    grep
    的进程信息。
  3. 查看进程具体信息。
    /proc/[PID]
    目录下的
    cwd

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

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
的程序,你可以在终端中使用以下命令来查看其
cwd

exe


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

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

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


  • fork()
    刚返回时,父子进程共享相同的物理内存页(只读),因此复制过程很快。
  • 当父进程或子进程试图修改内存时
  • 操作系统会为需要修改的部分分配新的物理内存。
  • 修改后的内存空间对父子进程来说是独立的。
    因此,只有在需要时,内存的独立性才真正实现,也就是需要对对内存中数据进行修改的时候,但逻辑上,父子进程从
    fork()
    返回后就已经被视为完全独立了。

流程图

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


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

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

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

-
fork()
是操作系统分离父子进程的起点

  • 通过资源复制、地址空间分离和调度机制,父子进程实现了完全独立
  • 父子进程虽然共享代码和部分资源,但内存、PID 和运行状态是互相独立的,确保了它们可以并发执行,互不干扰。
  • 写时拷贝:当父子进程尝试修改共享数据时,操作系统会将数据复制到独立空间。
    基本的独立靠的是
    struct task_struct(PCB)
    独立。
    当父子进程任何一方进行数据修改的时候触发写时拷贝,操作系统就把修改的数据在底层拷贝一份,让整个目标进程修改这个拷贝,脱离代码共享,实现完全独立
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号