进程概念
# 冯诺依曼体系
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系:

# 操作系统
操作系统的定位:
简单来说,操作系统就是一款进行软硬件资源管理的软件。
设计操作系统的目的:
- 与硬件进行交互,管理所有的软硬件资源。
- 为用户程序(应用程序)提供一个良好的执行环境。

# 进程概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU 时间,内存)的实体
可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU 才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程。 进程是操作系统分配资源的基本单位,它包含了程序执行所需的各种资源信息,如代码、数据、系统资源等。每个进程在系统中都是独立存在的,拥有自己的地址空间和运行状态。进程是程序执行的一个实例,通过进程,我们可以实现多任务的并发执行,提高系统的利用率和响应速度。如果有太多加载进来的程序,此时,操作系统就需要进行管理(先描述,后组织)。
# 描述进程-PCB
# task_struct
PCB 实际上是对进程控制块的统称,在 Linux 中描述进程的结构体叫做 task_struct。 task_struct 是 Linux 内核的一种数据结构,它会被装载到 RAM(内存)里并且包含进程的信息。
进程 = 内核数据结构(task_struct) + 进程对应的磁盘代码
# task_struct 内容分类
task_struct 就是 Linux 当中的进程控制块,task_struct 当中主要包含以下信息:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器(pc): 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O 状态信息: 包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
- 其他信息。

# 组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct 链表的形式存在内核里。
# 查看进程
# 系统目录查看
进程的信息可以通过bin/proc系统文件夹查看
比如:要获取 PID 为 2565167 的进程信息,你需要查看 /proc/2565167 这个文件夹

# ps 命令查看
使用ps aux可以打印出当前所有进程的信息。
比如:要获取 PID 为 2565167 的进程信息,你需要查看进行以下命令:
[cen@VM-8-2-opencloudos proc]$ ps aux | head -1 && ps aux | grep 2565167
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
cen 2565167 0.0 0.1 13856 1808 pts/2 S+ 15:54 0:00 ./test
cen 2567165 0.0 0.0 222012 1096 pts/3 S+ 15:59 0:00 grep --color=auto 2565167
2
3
4
# 获取进程的 PID 和 PPID
进程 id(PID) 父进程 id(PPID)
#include<iostream>
#include <unistd.h>
using namespace std;
#include<sys/types.h>
int main() {
while(1) {
sleep(20);
cout << "我是一个进程:" << "PID:" << getpid() << " PPID:" << getppid() << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
我是一个进程:PID:2578545 PPID:2577206
[cen@VM-8-2-opencloudos ~]$ ps aux | head -1 && ps aux | grep test
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
cen 2578545 0.0 0.1 13856 1892 pts/4 S+ 16:30 0:00 ./test
cen 2579086 0.0 0.0 222012 1180 pts/5 S+ 16:31 0:00 grep --color=auto test
[cen@VM-8-2-opencloudos ~]$ ps aux | head -1 && ps aux | grep 2577206
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
cen 2577206 0.0 0.2 226016 5144 pts/4 Ss 16:27 0:00 -bash
cen 2579349 0.0 0.0 222012 1120 pts/5 S+ 16:32 0:00 grep --color=auto 2577206
2
3
4
5
6
7
8
由此,命令行上启动的命令,它的父进程一般都是 bash
# 创建进程
# fork 创建子进程
fork 函数创建子进程:fork 是一个系统调用级别的函数,其功能就是创建一个子进程。(函数执行前,只有父进程;函数执行后,父进程 + 子进程) 返回值:
- 如果子进程创建成功,在父进程中返回子进程的 PID,而在子进程中返回 0。
- 如果子进程创建失败,则在父进程中返回 -1。
实际上,使用 fork 函数创建子进程,在 fork 函数被调用之前的代码被父进程执行,而 fork 函数之后的代码,则默认情况下父子进程都可以执行。需要注意的是,父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)并且通过返回值的不同,来让父子进程执行不同的代码。
#include<iostream>
#include <unistd.h>
using namespace std;
#include<sys/types.h>
int main() {
fork();
cout << "我是一个进程:" << "PID:" << getpid() << " PPID:" << getppid() << endl;
return 0;
}
2
3
4
5
6
7
8
9
[cen@VM-8-2-opencloudos ~]$ ./test
我是一个进程:PID:2607219 PPID:2577206
我是一个进程:PID:2607220 PPID:2607219
2
3
分析:运行 cpp 程序,父进程调用 fork(),产生一个子进程,fork()调用后,父子两个进程分别执行 cout 语句,输出各自的 PID 和 PPID。 例如:父进程开始,PID:2607219 PPID:2577206(bash 的 PID),子进程开始,PID:2607220 PPID:2607219(父进程的 PID)。
# if 分流
据返回值的不同来让父子进程执行不同的代码,从而做不同的事。
#include<iostream>
#include <unistd.h>
using namespace std;
#include<sys/types.h>
int main() {
pid_t id = fork();
if(id == 0) {
while(1) {
cout << "子进程..." << endl;
sleep(2);
}
}
else if(id > 0) {
while(1) {
cout << "父进程" << endl;
sleep(2);
}
}
else
// fork error
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
输出结果:父进程、子进程循环打印。
# 进程状态
Linux 操作系统的源代码当中对于进程状态的定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15


# 运行状态-R
一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个 R 状态的进程。
# 浅度睡眠状态-S
一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒(在等待资源或条件成立时,可以被信号唤醒并中断其等待状态),也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。
# 深度睡眠状态-D
一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),通常发生在进程等待某些关键资源或执行不能被中断的操作时,处于这个状态的进程会等待 IO 的结束。
例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)
# 僵尸进程-Z
一个进程在退出的时候,不能立即释放对用的资源,保存一段时间后,让父进程或 OS 来进行读取,如果没有回收,子进程的进程描述符仍然会保留在系统中,此时的状态就是僵尸状态。
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态。
僵尸进程的处理方法:
- 使用 wait()或 waitpid()系统调用:父进程可以通过这些系统调用来等待子进程的终止,并获取子进程的终止状态。当父进程调用这些系统调用时,子进程的状态信息会被完全清除,从而不再是僵尸进程。
- 使用信号处理:当子进程终止时,会发送 SIGCHLD 信号给父进程。父进程可以设置一个信号处理函数,在该函数中调用 wait()或 waitpid()来处理子进程的终止状态。
# 孤儿进程
如果父进程先退出,子进程就称之为“孤儿进程”。孤儿进程被1号init进程领养,退出的时候被 init 进程回收,因此,即使原始父进程结束,孤儿进程仍然会在系统中运行,直到它们自己正常结束或被终止。
如果一个进程变成孤儿,会自动变成后台进程,init 进程会周期性地调用 wait()或类似的系统调用来回收已结束的孤儿进程的资源,并释放它们在进程表中所占用的条目。
# 进程优先级
# 引入
cpu 资源分配的先后顺序,就是指进程的优先权(priority)。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux 很有用,可以改善系统性能,还可以把进程运行到指定的 CPU 上,这样一来,把不重要的进程安排到某个 CPU,可以大大改善系统整体性能。
# PRI 和 NI
执行ps -l来查看系统进程:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 1001 365554 365551 0 80 0 - 2063 do_wai pts/0 00:00:00 bash
0 R 1001 365795 365554 0 80 0 - 2736 - pts/0 00:00:00 ps
2
3
其中:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,即父进程的代号
- PRI :代表这个进程可被执行的优先级,值越小越早被执行
- NI :代表这个进程的 nice 值
理解: NI 就是我们所要说的 nice 值,其表示进程可被执行的优先级的修正数值,PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为:PRI(new) = PRI(old)(80) + nice,所以,调整进程优先级,在 Linux 下,就是调整进程 nice 值(PRI(old)默认等于 80,nice 其取值范围是-20 至 19,一共 40 个级别)。
# 查看进程优先级
ps -al
使用 ps -al 命令查看该进程优先级的信息
top通过 top 命令更改进程的 nice 值,top 命令就相当于 Windows 操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况。
具体:按 R,输入更改进程的 UID,进行更改 nice 值,最后按 Q 退出
# 环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
# 常见环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到 Linux 系统中时,默认的目录)
- SHELL : 当前 Shell,它的值通常是/bin/bash
- ...
- 查看环境变量方法:
echo $NAMENAME:你的环境变量名称 - 代码获取环境变量的方法:
cout << getenv("PATH") << endl;或者通过第三方变量 environ 获取
extern char **environ; // 一个环境变量指针数组,指向HOME、SHELL等等
int i = 0;
for(; environ[i]; i++){
cout << environ[i] << endl;
}
2
3
4
5
# 环境变量相关命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的 shell 变量和环境变量
# 进程地址空间
# 定义
进程地址空间是 Linux 操作系统为每个进程分配的一个独立的虚拟内存空间。这个空间是进程在内存中的逻辑表示,它包含了程序执行所需的各种资源和数据。页表则是 Linux 操作系统实现进程地址空间与物理内存映射的关键数据结构。通过页表,Linux 能够确保每个进程都拥有自己独立的地址空间,实现进程的隔离和保护。
进程地址空间就类似于一把尺子,尺子按照刻度被划分为各个区域(内核空间约 1GB,其余部分占 3GB)。 例如代码区、堆区、栈区等。而在结构体 mm_struct 当中,便记录了各个边界刻度,例如代码区的开始刻度与结束刻度,如下图所示:

在结构体 mm_struct 当中,各个边界刻度之间的每一个刻度都代表一个虚拟地址, 这些虚拟地址通过页表映射与物理内存建立联系。由于虚拟地址是线性增长的,所以虚拟地址又叫做线性地址。
进程地址空间里面不是物理地址,而是虚拟地址(线性地址)。物理地址用户一概是看不到的,是由操作系统统一进行管理的(通过页表映射:进程 -- 映射 --> 物理地址)
注意点:
- 虚拟内存:每个进程都有一个虚拟地址空间,这个空间的大小通常远大于物理内存的大小。操作系统使用虚拟内存技术,将进程的虚拟地址空间映射到物理内存和磁盘上的交换空间(swap space)。
- 页面映射:虚拟地址空间被划分为多个固定大小的页面(通常是 4KB),每个页面可以映射到物理内存的一个页面,或者被交换出去存放在磁盘上。当进程访问某个页面时,如果该页面不在物理内存中,操作系统会触发一个页面错误(page fault),然后将该页面从磁盘加载到物理内存中。
- 写时复制(Copy-on-Write):在 fork()调用后,子进程和父进程的地址空间是共享的,但使用了写时复制技术。这意味着,除非其中一个进程试图写入其地址空间中的某个页面,否则这两个进程仍然共享相同的物理内存页面。一旦有写入操作发生,操作系统会为该页面创建一个新的物理内存副本,并将写入操作应用到该副本上,以确保两个进程不会相互干扰。
- 地址空间保护:操作系统会确保每个进程只能访问其自己的地址空间,而不能直接访问其他进程的地址空间,这保证了进程之间的独立性。
- 动态内存分配:进程可以在运行时动态地请求和释放内存。操作系统会管理这些请求,并在物理内存中为进程分配和回收页面。
# 内存映射
页表是操作系统中用于实现虚拟内存管理的重要数据结构。它建立起了虚拟地址与物理地址之间的映射关系,使得进程可以使用连续且统一的虚拟地址空间,而实际上这些虚拟地址在物理内存中可能是分散且不连续的。 具体来说,页表将虚拟内存划分为固定大小的页面(通常为 4KB),并将每个虚拟页面的地址映射到物理内存中的某个页帧。每个页表项包含了虚拟页面的页号以及对应的物理页帧号,以及其他一些属性信息,如访问权限、是否驻留在内存中等。
以二级页表为例:
- 页表将虚拟内存划分为固定大小的页面(通常为 4KB),并拆分为 3 部分:页目录索引(10bit),页表索引(10bit)和页内偏移(12bit)
- 每个进程有一个页目录,其物理地址保存在 CPU 的 CR3 寄存器中,CPU 取出页目录索引 * 4(每个条目 4 字节),加上 CR3 中的页目录基址,找到对应的页目录项 (PDE),PDE 里存储的是下级页表的物理地址
- 从找到的 PDE 中取出页表的物理地址,CPU 取出页表索引 * 4,加上页表物理地址,找到对应的页表项 (PTE),PTE 里存储的就是进程需要的物理页框号 (PPN)
- 将 PTE 中的物理页框号 * 4KB,得到物理页的起始地址,再加上虚拟地址中的页内偏移,就得到了最终的物理地址