Linux笔记---进程:初识进程

1. 进程的概念

**进程是操作系统中的一个核心概念,它代表了正在运行的程序的实例。**每个进程都有自己的内存空间、代码、数据和系统资源。进程是操作系统进行资源分配和调度的基本单位。

进程具有以下几个主要特征:

  • 独立性:每个进程都有自己独立的内存空间,进程之间互不干扰。一个进程的崩溃通常不会影响其他进程。
  • 动态性:进程是动态的,它从创建到终止经历一系列状态变化。
  • 并发性:多个进程可以同时运行,操作系统通过调度算法在多个进程之间切换,实现并发执行。
  • 资源分配:操作系统为每个进程分配必要的资源,如CPU时间、内存、文件和I/O设备。

1.1 进程的本质

我们知道,程序要运行,首先其代码和数据需要被加载到内存当中。

既然进程代表了正在运行的程序的实例,那么进程到底是以什么形式存在于内存中的呢?他是如何代表一个正在运行的程序的实例的?操作系统是如何对其进行管理与资源分配的?

操作系统是一个软件,软件能做的无非就是对数据进行管理,无论什么概念或事物,操作系统都需要先将其数据化,才能进行统一的管理。

由于操作系统是C语言写的,所以其数据化概念或事物并进行管理的方式必然也是按照C语言的方式来实现的,即利用数据结构来描述(数据化),再利用数据结构来管理。

我们称之为 "先描述,再组织" ,操作系统对计算机中的一切的管理本质上都是通过这样的方式。

所以,进程 = 程序的数据和代码 + 描述并组织进程信息的数据结构!

1.2 PCB与task_struct

在操作系统中,进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。

课本上称之为PCB(process control block,通常以结构体的方式实现),Linux操作系统下的PCB是:task_struct(进程有时也被称作任务,例如windows下的任务管理器)。

task_struct通常会包括如下的信息:

  • 标识符(PID):描述本进程的唯一标识符,用来区别其他进程。
  • 状态:任务状态,退出代码,退出信号等。
  • 优先级:相对于其他进程的优先级。
  • 程序计数器:程序中即将被执行的下一条指令的地址。
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据:进程执行时处理器的寄存器中的数据。
  • I/O状态信息:包括显示的I/O请求,分配给进程的I∕O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息。

task_struct的组织方式

task_struct对一个进程的信息进行管理,而在操作系统层面上,一个个的task_struct又通过链表的形式(双向的链表,链式栈,链式队列等)被管理起来,因为进程会经历频繁的增删查改。

task_struct中包含的信息十分多,想要将其管理好,一种数据结构肯定是不够的,task_struct常常会被放到多种数据结构中。

为了方便管理,以及信息的同步,肯定不能拷贝多个task_struct以放到多个数据结构中。

那么,要将同一个task_struct结点放到不同数据结构中,task_struct内部就必然需要多组prev和next指针,在Linux源代码中我们会看到一种巧妙的组织方式:

cpp 复制代码
struct list_head{
    struct list_head *next, *prev;  
};

注意,进程在各个数据结构中的位置顺序是不一样的,只是在二维平面上不好画出来,就画成了上面这样,大家能理解就好。

通过list_head结构体来存储next/prev对,这样就解除了多种数据结构之间的耦合,还确保了同一个task_struct被多个数据结构管理。

假设队列结点在task_struct中被定义为links,当需要访问task_struct中存储的信息时,只需要按照下面的公式计算出task_struct的首地址即可:

上面的公式可以被设计成宏,个人认为这个设计十分有参考学习价值。


2. 进程的基本操作

2.1 查看进程

查看系统中目前正在运行的进程有两种方式:通过文件查看、通过指令查看。

通过文件查看

我们之前说过,在Linux中一切皆文件,进程也不例外。当进程被创建之后,"/proc/"目录下会同步生成对应的文件,我们就可以通过查看文件的方式来查看进程:

这些数字目录就代表了进程,数字值就是其对应进程的PID。

每个目录中记载了进程的相关信息。

通过指令查看

ps命令

ps命令用于报告当前系统的进程状态。它可以显示瞬间行程(process)的动态,包括进程ID、用户ID、CPU和内存使用率等信息。ps命令的参数非常多,常用的参数包括:

  • -A:列出所有的进程
  • -a:显示一个终端的所有进程,除了会话引线
  • -u:显示进程的归属用户及内存的使用情况
  • -x:显示没有控制终端的进程
  • -e:显示所有进程,等同于-A
  • -f:使用完整格式显示信息,包括PPID、UID等

例如,ps -aux命令可以显示所有用户的所有进程,包括其他使用者的行程,并且显示各个命令的详细路径。ps命令的输出通常包括以下列信息:

  • USER:进程的所有者用户名
  • PID:进程的唯一标识符,即进程ID
  • %CPU:进程的CPU占用率
  • %MEM:进程的内存占用率
  • VSZ:进程所使用的虚存的大小(Virtual Size)
  • RSS:进程使用的驻留集大小或者是实际内存的大小(Kbytes字节)
  • TTY:与进程关联的终端(tty)
  • STAT:进程的状态,如R(运行)、S(睡眠)、Z(僵尸)等
  • START:进程启动时间和日期
  • TIME:进程使用的总CPU时间
  • COMMAND:正在执行的命令行命令

top命令

top命令是一个实时的系统监视工具,用于查看系统的整体性能和进程的运行情况。它可以动态显示进程的资源使用情况,即可以通过用户按键来不断刷新当前状态。top命令的输出信息分为两部分:顶部的信息和进程的详细列表。

顶部信息(系统统计)

  • top - 15:47:39 up 10 days, 2:45, 3 users, load average: 0.05, 0.12, 0.09:当前时间、系统运行时间、登录用户数以及过去1分钟、5分钟和15分钟的系统负载平均值。
  • Tasks: 187 total, 1 running, 186 sleeping, 0 stopped, 0 zombie:总进程数、正在运行的进程数、睡眠的进程数、停止的进程数和僵尸进程数。
  • %Cpu(s): 2.5 us, 0.3 sy, 0.0 ni, 97.1 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st:CPU使用情况,包括用户态、系统态、空闲、等待输入输出等的百分比。
  • MiB Mem : 7863.5 total, 1242.2 free, 3185.4 used, 3435.9 buff/cache:内存使用情况,包括总内存、空闲内存、已使用内存和缓存内存。
  • MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 3897.5 avail Mem:交换空间使用情况,包括总交换空间、空闲交换空间和可用内存。

进程列表(详细进程信息)

  • PID:进程ID
  • USER:进程所有者
  • PR:进程优先级(Priority)
  • NI:进程的nice值,用于调整进程优先级
  • VIRT:进程占用的虚拟内存大小(包括已交换到磁盘的部分)
  • RES:进程使用的物理内存(常驻内存)
  • SHR:共享内存
  • S:进程状态(如S=SleepingR=RunningZ=ZombieT=Stopped
  • %CPU:进程占用的CPU百分比
  • %MEM:进程占用的内存百分比
  • TIME+:进程使用的总CPU时间
  • COMMAND:进程的执行命令

top命令还允许用户通过键盘命令进行交互操作,例如:

  • M:按内存占用排序
  • P:按CPU占用排序
  • T:按运行时间排序
  • k:杀死一个进程
  • q:退出top命令
  • r:修改进程的优先级(即nice值)
  • 1:显示每个CPU核心的使用情况

↑来自天工大模型。

2.2 C语言中的进程相关函数

这些函数都是Linux中的系统调用,依赖于操作系统而存在,C语言标准并没有定义。

2.2.1 getpid和getppid

顾名思义,就是获取当前进程的pid和ppid(父进程pid,parent pid)。

2.2.1 fork函数

该函数可以在一个进程中创建子进程。

cpp 复制代码
pid_t fork(void);

该函数被调用之后立即启动一个子进程,子进程与父进程共享代码和数据,所以子进程会从其被创建的位置处开始执行父进程的代码。

如果进程创建失败,fork返回"-1";

如果进程创建成功,fork给父进程返回子进程的pid,给子进程返回0。

举个例子:

cpp 复制代码
#include <stdio.h>
// fork函数包含在下面两个头文件中
#include <sys/types.h>
#include <unistd.h>

int main()
{
    printf("我是一个进程,我的pid=%d,我即将创建子进程...\n", getpid());
    int id = fork();
    if(id == 0)
    {
        printf("我是一个子进程,我的pid=%d,我父进程的pid=%d,我得到的id=%d,&id=%p\n", getpid(), getppid(), id, &id);
    }
    else
    {
        printf("我是一个父进程,我的pid=%d,我子进程的pid=%d,我得到的id=%d,&id=%p\n", getpid(), id, id, &id);
    }
    return 0;
}

1. 为什么fork可以给父子进程返回不同的值呢?

这是因为在fork函数执行结束之前,子进程就已经被创建好了,所以在fork函数内部,子进程就已经开始与父进程分别运行了,只要在返回之前根据pid等信息就可以区分二者,并控制返回的值。

2. 父子进程不是共享数据吗,为什么id既可以是0又可以是2500559?

父子进程确实共享数据,但是当某个数据被写入时,对该数据的共享就会结束,子进程会为自己拷贝一份,这种技术被称作写时拷贝。

fork函数在返回时,id就发生了写入,所以子进程拷贝了一份自己的id变量。

3. 既然子进程拷贝了id,那为什么两个id的地址是相同的?

这种现象的原因在于虚拟内存系统。虚拟内存允许操作系统为每个进程创建一个独立的虚拟地址空间,使得每个进程都认为它在使用整个计算机的内存。但实际上,物理内存被分割和共享。

所以这里我们看到的地址其实是虚拟地址,这意味着在父子进程中,变量的地址看起来是相同的,但实际上它们可能指向不同的物理内存位置。

相关推荐
m0_6949380119 分钟前
Leetcode打卡:字符串及其反转中是否存在同一子字符串
linux·服务器·leetcode
飞的肖1 小时前
从测试服务器手动热部署到生产环境的实现
java·服务器·系统架构
看星星的派大星1 小时前
rk3588 android12 root
linux
飘飘燃雪1 小时前
Linux Modbus协议详解,代码示例
linux·运维·服务器·modbus
lulinhao1 小时前
IP组播基础
笔记·计算机网络·华为
蜗牛hb1 小时前
Kali基础知识
linux·运维·服务器
代码欢乐豆2 小时前
计算机网络——期末复习(3)4-6章考试重点
笔记·计算机网络
红色的山茶花2 小时前
YOLOv9-0.1部分代码阅读笔记-loss_tal_dual.py
笔记·深度学习·yolo
一棵开花的树,枝芽无限靠近你2 小时前
【PPTist】表格功能
前端·笔记·学习·编辑器·ppt·pptist
乐维_lwops2 小时前
安全筑堤,效率破浪 | 统一运维管理平台下的免密登录应用解析
运维·服务器·安全