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的地址是相同的?

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

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

相关推荐
运维自动化&云计算1 分钟前
Centos虚拟机硬盘报错,根分区满,已用显示为负40G
linux·运维·centos
Web极客码37 分钟前
在Ubuntu 22.04上安装远程桌面服务
linux·运维·ubuntu
sqmeeting1 小时前
QT6 如何在Linux Wayland 桌面系统抓屏和分享屏幕
linux·qt
teeeeeeemo1 小时前
跨域及解决方案
开发语言·前端·javascript·笔记
大白同学4211 小时前
【Linux】编辑器vim的使用
linux·编辑器·vim
SKYDROID云卓小助手2 小时前
三轴云台之控制信号解析与执行
运维·服务器·网络·人工智能·信号处理
板鸭〈小号〉2 小时前
Linux网络基础(一)
linux·网络·智能路由器
清和已久2 小时前
nginx高性能web服务器
服务器·前端·nginx
丁满与彭彭2 小时前
嵌入式学习笔记--MCU阶段--DAY12实时操作系统rt_thread1
笔记·学习
范纹杉想快点毕业3 小时前
嵌入式 C 语言编程规范个人学习笔记,参考华为《C 语言编程规范》
linux·服务器·数据库·笔记·单片机·嵌入式硬件·fpga开发