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
:进程IDUSER
:进程所有者PR
:进程优先级(Priority)NI
:进程的nice值,用于调整进程优先级VIRT
:进程占用的虚拟内存大小(包括已交换到磁盘的部分)RES
:进程使用的物理内存(常驻内存)SHR
:共享内存S
:进程状态(如S=Sleeping
,R=Running
,Z=Zombie
,T=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的地址是相同的?
这种现象的原因在于虚拟内存系统。虚拟内存允许操作系统为每个进程创建一个独立的虚拟地址空间,使得每个进程都认为它在使用整个计算机的内存。但实际上,物理内存被分割和共享。
所以这里我们看到的地址其实是虚拟地址,这意味着在父子进程中,变量的地址看起来是相同的,但实际上它们可能指向不同的物理内存位置。