文章目录
- 1. 进程概念
- 1.1 什么叫做进程?
- 1.2 进程和程序的区别
- 2. linux下的进程
- 2.1 task_struct 包含哪些内容
- 2.2 访问(查看)进程信息
- 2.3 通过系统调用获取进程标示符
- 2.4 通过系统调用创建进程
- 2.5 进程状态
- 2.6 如何查看进程状态(指令)
- 2.7 僵尸进程 && 孤儿进程
- 2.8 其他进程概念
- 3. 进程优先级
- 3.1 基本概念
- 3.2 进程的重要参数
- 3.3 PRI 与 NI
- 4. 环境变量
- 4.1 echo 查看环境变量
- 4.2 配置环境变量
- 4.3 常见 环境变量
- 4.4 程序获取环境变量
- 4.5 环境变量表
- 4.6 本地变量
- 4.7 Linux命令:常规 / 内建
- 5. 进程地址空间
- 5.1 什么是进程地址空间
- 5.2 地址空间 与 区域划分
- 5.3 进程地址空间的作用
1. 进程概念
1.1 什么叫做进程?
对于这点,用白话文来说就是:
程序是一个静态的代码集合,通常存储在磁盘上。进程是程序的动态执行实例,包括程序代码、当前活动的状态、寄存器、堆栈等。在操作系统中,进程代表正在运行的程序,它具有独立的地址空间和资源。简言之,程序是文件,而 进程是运行中的程序(程序+PCB),下文会进行解释。
下面我们来详细探讨两者的区别:
1.2 进程和程序的区别
程序:即我们用代码编写的各种可执行程序;存在磁盘上,通过加载到内存中,经由操作系统管理后,转变为进程。
操作系统的管理过程我们通过下图,来分析:
我们知道:
-
内存是由操作系统管理的,而进程在内存中,所以操作系统内部会存在多个进程。
-
操作系统自然需要管理所有进程,如何管理?这里在于操作系统管理进程的基本原则:先描述,再组织。
- 我们引入PCB的概念:在操作系统中,每个进程都有一个进程控制块(Process Control Block,简称 PCB),它是操作系统管理进程的数据结构。PCB 包含了描述进程状态和属性的各种信息
- 对于C语言实现的操作系统,PCB通常是由
struct
实现的(格式例如下面代码)
struct PCB
{
/*
id
代码地址 && 数据地址
状态
优先级
链接字段
等等... ...
*/
struct PCB *next;
};
将其以图像理解为:
-
根据上面代码和图片,我们可以得到操作系统管理进程的全貌:
- 当用户启动一个程序时,操作系统将该程序加载到内存中,并创建一个新的进程。
- 操作系统会为每个运行着的程序创建数据进程块(process control block),即PCB,用于管理和跟踪进程的状态、资源分配、调度信息等。
- 而操作系统通过管理pcb进而管理进程,在程序执行过程中,操作系统可以通过修改PCB中的状态信息来管理进程的执行。
- 例如,当一个进程需要等待某个资源时,操作系统会将该进程的状态设置为阻塞状态,并将其PCB中的等待资源信息更新。当该资源可用时,操作系统会将该进程的状态设置为就绪状态,并重新调度该进程的执行。
- 另外,通常情况下,操作系统会使用链表(或其他数据结构) 来组织存储(PCB),比如要删去进程3,只需在链表上删除其PCB即可。
以上我们得出结论:进程 = 程序+PCB
2. linux下的进程
Linux操作系统中,进程的PCB(进程控制块)被称为task_struct构体。
- 当需要创建新的进程时:内核会为其分配一个新的
task_struct
结构体,并将其插入到进程链表中。 - 当需要调度进程时:内核可以按照特定的调度算法遍历进程链表,选择合适的进程进行执行。
- 当进程终止时:内核会从进程链表中删除相应的task_struct结构体,并释放相关资源。
2.1 task_struct 包含哪些内容
task_struct
是 Linux 内核中用于表示进程的结构体,包含以下主要内容:
- 进程状态:包括进程的运行状态(如就绪、运行、阻塞)。
- 进程标识符:如 PID(进程标识符)和父进程 PID。
- 调度信息:调度策略、优先级等。
- 内存管理:进程的虚拟内存映射、页表等信息。
- 文件描述符:指向进程打开的文件描述符表。
- 进程上下文:寄存器状态、程序计数器等。
- 进程通信:如信号处理器、进程间通信的相关信息。
2.2 访问(查看)进程信息
- 通过命令查看:
-
ps
:显示当前系统中的进程。常用选项包括ps aux
(显示所有进程和详细信息)和ps -ef
(显示全格式列表)。 -
top
:实时显示系统中进程的动态信息,包括 CPU 和内存使用情况。 -
htop
:增强版的top
,提供更友好的用户界面和更多交互功能。 -
pidof
:获取指定进程名称的 PID。 -
pstree
:以树形结构显示进程及其父子关系。
- 通过
/proc
系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1
这个文件夹
2.3 通过系统调用获取进程标示符
- 进程id(PID)
- 父进程id(PPID)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
2.4 通过系统调用创建进程
在linux中,可以通过fork
命令创建新的进程,新进程是调用进程的副本。
这里不对fork作更详细的解释,先知道这个概念。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
2.5 进程状态
每个运行着的进程都有相应的运行状态,下面是
/*
* 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 */
};
分类解析:
- “
R (running)
”:进程正在运行。 - “
S (sleeping)
”:进程正在等待某个事件发生,例如输入/输出完成或定时器到期。 - “
D (disk sleep)
”:进程正在等待磁盘操作完成。 - “
T (stopped)
”:进程已停止,例如由于收到了 SIGSTOP 信号。 - “
t (tracing stop)
”:进程已停止,以便调试器可以附加并跟踪其执行。 - “
X (dead)
”:进程已经退出,但尚未被回收。 - “
Z (zombie)
”:进程已经退出,并且其父进程还没有回收它的资源。
2.6 如何查看进程状态(指令)
Linux下,可以使用以下命令来查看进程状态:
- ps 命令:
ps 命令用于显示当前正在运行的进程。可以使用不同的选项来指定要显示的进程信息。其中,最常用的选项包括:
ps aux
:显示所有进程的详细信息。ps -ef
:显示所有进程的详细信息,与 ps aux 类似。ps -e
:显示当前所有正在运行的进程。ps -f
:显示进程的详细信息,包括进程的 UID、PPID、CPU 占用率等。ps -l
:以长格式显示进程信息。
- top 命令:
top 命令用于实时显示系统中正在运行的进程信息。它会动态更新进程信息,并按照 CPU 占用率或内存占用率进行排序。可以使用不同的选项来指定要显示的信息,例如:
top
:默认情况下,显示系统中 CPU 占用率最高的进程。top -u username
:显示指定用户的进程信息。top -p pid
:显示指定 PID 的进程信息。
- htop 命令:
htop
命令是 top 命令的改进版,它提供了更加友好的用户界面和更多的选项。可以使用以下命令安装 htop:
sudo apt-get install htop
然后,可以使用 htop 命令来查看进程状态,例如:
htop
:显示系统中所有进程的详细信息。htop -u username
:显示指定用户的进程信息。htop -p pid
:显示指定 PID 的进程信息。
2.7 僵尸进程 && 孤儿进程
上文介绍了不同的进程状态,下面主要探讨僵尸进程与孤儿进程::
僵尸进程 和 孤儿进程是两种不同类型的进程状态,它们各自有不同的特点和处理方式:
僵尸进程(Zombie Process)
-
定义:僵尸进程是指一个已经完成执行(终止)但仍然存在于进程表中的进程。 它已经释放了所有的资源,但其进程控制块(PCB)仍保留在系统中,因为其父进程尚未读取它的退出状态。
-
特征:
- 僵尸进程占用进程表中的一个条目。
- 它的状态是“Z”或“Zombie”。
- 僵尸进程通常会被父进程通过
wait()
或waitpid()
系统调用收集其退出状态来彻底清除。 - 如果父进程没有适时处理子进程的退出状态,僵尸进程可能会积累,导致进程表中的条目枯竭。
-
处理:
- 父进程应定期调用
wait()
或waitpid()
来收集子进程的退出状态,从而避免僵尸进程的积累。 - 如果父进程结束而没有收集子进程的状态,子进程将被
init
进程(PID 1)接管,init
会处理它们并清理僵尸进程。
- 父进程应定期调用
孤儿进程(Orphan Process)
-
定义:孤儿进程是指其父进程已经终止,而孤儿进程仍在运行。 这些进程没有父进程,但它们仍然在系统中运行。
-
特征:
- 孤儿进程的状态通常是“R”(运行)或“S”(睡眠),但它们在进程表中并不表示为“孤儿”状态。
- 当一个进程的父进程终止时,孤儿进程会被
init
进程(PID 1)接管,init
进程会成为它们的新父进程。 - 孤儿进程并不会直接导致系统问题,因为
init
进程会处理它们。
-
处理:
- 孤儿进程由
init
进程接管,init
进程负责对这些孤儿进程进行管理,并在它们终止时清理它们的资源。
- 孤儿进程由
两者的不同点
- 僵尸进程 是已经终止但还在进程表中,因为父进程未收集退出状态;孤儿进程 是其父进程已终止但自己仍在运行,系统会将其转交给
init
进程管理。 - 僵尸进程 占用进程表条目,而 孤儿进程 不会占用额外的进程表条目。
- 僵尸进程 需要父进程处理其退出状态来清除,而 孤儿进程 会自动由
init
进程接管,不需要特殊处理。
了解这些概念可以帮助你更好地管理和调试进程相关的问题。
代码演示:
1. 僵尸进程示例
在这个示例中,父进程启动一个子进程,子进程结束后,父进程没有调用 wait()
来回收子进程的状态,导致子进程变成僵尸进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// Fork failed
perror("fork");
exit(1);
}
if (pid == 0) {
// Child process
printf("Child process (PID: %d) is running...\n", getpid());
// Simulate work with sleep
sleep(2);
printf("Child process (PID: %d) exiting...\n", getpid());
exit(0); // Child process exits
} else {
// Parent process
printf("Parent process (PID: %d) is running...\n", getpid());
printf("Parent process will not call wait(), causing child to become a zombie...\n");
// Simulate parent doing some work without calling wait
sleep(10);
printf("Parent process (PID: %d) exiting...\n", getpid());
// Parent process exits without calling wait(), causing the child to become a zombie
exit(0);
}
return 0;
}
在这个代码中:
fork()
创建了一个子进程。- 子进程打印一些信息,然后睡眠 2 秒后退出。
- 父进程打印一些信息,睡眠 10 秒,然后退出,没有调用
wait()
来处理子进程的退出状态。
当父进程退出时,子进程已经退出,但由于父进程没有调用 wait()
,子进程会成为僵尸进程。
2. 孤儿进程示例
在这个示例中,父进程启动一个子进程,然后父进程在子进程仍在运行时退出。子进程变成孤儿进程,并由 init
进程接管。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
// Fork failed
perror("fork");
exit(1);
}
if (pid == 0) {
// Child process
printf("Child process (PID: %d) is running...\n", getpid());
// Simulate work with sleep
sleep(10);
printf("Child process (PID: %d) exiting...\n", getpid());
exit(0); // Child process exits
} else {
// Parent process
printf("Parent process (PID: %d) is running...\n", getpid());
// Simulate parent terminating early
sleep(2);
printf("Parent process (PID: %d) exiting...\n", getpid());
// Parent process exits, child becomes an orphan
exit(0);
}
return 0;
}
在这个代码中:
fork()
创建了一个子进程。- 子进程打印一些信息,然后睡眠 10 秒后退出。
- 父进程在子进程运行时睡眠 2 秒后退出。
由于父进程在子进程完成其任务之前退出,子进程成为孤儿进程。init
进程(PID 1)会接管这个孤儿进程,管理它的生命周期。
2.8 其他进程概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
3. 进程优先级
3.1 基本概念
进程优先级(priority),即cpu资源分配的先后顺序。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很必要,可以改善系统性能。
- 同时可以把进程运行到指定的CPU上,把不重要的进程安排到某个CPU,改善系统整体性能。
3.2 进程的重要参数
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 1234 5678 0 80 0 0 500 0 tty1 00:00:00 bash
0 R 1000 2345 1234 0 80 0 0 1000 0 tty1 00:00:01 top
1 Z 1000 3456 2345 0 80 0 0 0 0 tty1 00:00:00 defunct
下面对这些字段进行解释:
- F: 进程标志位。例如,
0
表示没有特殊标志,1
表示进程处于某种特殊状态。 - S: 进程状态。例如,
S
表示休眠状态,R
表示正在运行,Z
表示僵尸进程。 - UID: 进程的用户 ID。
- PID: 进程 ID。
- PPID: 父进程 ID。
- C: CPU 使用率(百分比)。
- PRI: 进程的优先级。
- NI: 进程的
nice
值,影响进程的调度优先级。 - ADDR: 进程的内存地址(通常为
0
表示未分配)。 - SZ: 进程的虚拟内存大小(以页面为单位)。
- WCHAN: 当前等待的内核函数。
- TTY: 关联的终端。
- TIME: 进程使用的 CPU 时间。
- CMD: 执行的命令。
为什么有两种优先级?PRI 和 NI?
下面对这两个字段进行解释:
3.3 PRI 与 NI
PRI (Priority)
- 含义:
PRI
字段表示进程的优先级。优先级用于确定进程在调度中的重要性。数值越小,优先级越高,进程会越频繁地获得 CPU 时间。 - 解释: 在大多数 Unix-like 系统中,进程的优先级会影响其调度。优先级的范围通常从 0(最高优先级)到 139(最低优先级),但具体实现可能有所不同。一般来说,操作系统会根据优先级来决定哪个进程应该获得 CPU 时间。
NI (Nice Value)
- 含义:
NI
字段表示进程的nice
值。nice
值是一个调整进程调度优先级的机制。其范围通常从 -20(最高优先级)到 19(最低优先级)。nice
值可以通过nice
命令或setpriority
系统调用来设置。 - 解释:
nice
值会影响进程的实际优先级。正值会降低进程的优先级,使它在 CPU 时间竞争中处于劣势,而负值会提升进程的优先级,使它在调度时获得更多的 CPU 时间。nice
值本身不会直接显示在PRI
字段中,但它会影响进程的实际优先级。
总体来说:
PRI
: 实际的进程优先级,决定了进程在系统调度中的优先级。NI
:nice
值,用于调整进程的调度优先级,影响PRI
的计算。
4. 环境变量
我们知道,linux中,ls等多数命令本身是可执行程序,而指令后的参数为命令行参数,比如执行 ls -a -l
命令时,ls 为可执行程序,而-a -l 属于命令行参数
看下面的程序执行:
当由我们自实现的程序时,直接执行,会弹出bash: myProc: command not found
的错误,只有使用./myProc
,即加上路径后才能使用。
因为:要执行一个命令,必须找到对应的可执行程序。
当我们执行ls 命令时,其具有默认搜索路径,即PATH
,所以可以直接执行命令
4.1 echo 查看环境变量
我们可以利用echo
命令查看环境变量,下面用 echo $PATH
查看PATH
路径
4.2 配置环境变量
- 如何将我们的路径配置到
PATH
中?- 通过
PATH=$PATH:/home/test
命令将我们自己的路径配置到PATH
中
- 通过
- 此时我们执行 该路径下的程序 就不必指明路径了:
- 如何删除手动加上的路径?
- 只需要
PATH=/路径
,即将之前的路径重新赋给PATH即可。
- 只需要
需要注意的是,通过
PATH=$PATH:/home/test
这样的命令来修改 PATH 环境变量,仅仅在当前 shell 进程中将PATH 变量增加了一个路径(即内存上 的修改),而没有将修改持久化到系统中。
当关闭当前 shell 进程时,修改的 PATH 环境变量也会随之被销毁。即关闭运行软件后,修改就恢复了,这是为什么?
我们知道:命令行启动的进程都是 shell/bash
的子进程,子进程的命令行参数和环境变量,是父进程bash所传递的。
我们使用PATH=$PATH
命令直接修改的是bash内部的环境变量信息,而重启shell进程后,父进程bash依然可以找到对应路径,父进程的环境变量信息从哪里来?
答:
-
每一次重新登录,系统都会形成新的bash解释器 且 新的bash解释器 会从()中读取形成自己的环境变量表信息。
-
而 环境变量信息是以脚本配置文件的形式存在的 ,则上面括号内即为相应的脚本配置文件。
而在linux下,在目录的主目录下会有一个隐藏文件 .bash_profile
4.3 常见 环境变量
常见的环境变量包括:
PATH
: 指定系统查找可执行文件的路径。HOME
: 当前用户的主目录。USER
或USERNAME
: 当前用户的用户名。SHELL
: 当前用户的默认 Shell。PWD
: 当前工作目录。LANG
: 系统的语言和区域设置。EDITOR
: 默认文本编辑器。LOGNAME
: 当前登录用户名。TEMP
或TMP
: 存放临时文件的目录。
这些变量在系统和脚本中经常用于配置和控制行为。
4.4 程序获取环境变量
下面介绍部分编程语言获得环境变量的方法:
-
Python:
import os value = os.getenv('VARIABLE_NAME')
-
C:
#include <stdlib.h> char *value = getenv("VARIABLE_NAME");
-
Java:
String value = System.getenv("VARIABLE_NAME");
-
Shell脚本:
value=$VARIABLE_NAME
4.5 环境变量表
环境变量表是操作系统中存储环境变量的数据结构,它是一个键值对(key-value)的集合,用于存储各种系统级和用户级的配置信息。
- Linux下,环境变量表是通过名为环境变量列表(Environment Variables List)的数据结构来实现的。
- 该列表是一个字符串数组,每个字符串都包含一个环境变量的定义。
- 环境变量表中的每个元素都是以 key=value 的形式表,可以使用
env
或 printenv 命令来查看当前环境变量表中的所有环境变量。你也可以使用echo $VAR_NAME
命令来检索特定环境变量的值。
下面的例子展示了,本地变量的相关操作
4.6 本地变量
linux下,在命令行中用下面的方法声明本地变量:
variable_name=value
如果要使用本地变量,需要在变量名前加上 $
符号。例如,在 Bash Shell
中输入以下命令:
my_var="Hello, World!"
echo $my_var
- 但命令行下创建的本地变量的作用域是有限的,通常仅在当前 Shell 会话中有效。
- 这意味着在当前 Shell 中定义的本地变量不能被其他 Shell 或子进程访问。
如果要在子 Shell(如子进程或命令替换)中使用本地变量,应该使用 export 命令将其导出为环境变量 ,例如:
my_var="Hello, World!"
export my_var
这样,在子 Shell 中就可以使用 $my_var
访问该变量的值了。
如果要删除本地变量,则可使用 unset
命令,例如:
unset my_var
4.7 Linux命令:常规 / 内建
我们将Linux命令分为两类:常规命令 与 内建命令:
1. 常规命令(外部命令)
常规命令是指那些在系统的文件系统中作为独立的可执行文件存在的命令。它们通常位于 /bin
, /usr/bin
, /sbin
, /usr/sbin
等目录下。执行这些命令时,Shell 会在指定的路径中查找并执行这些文件。
示例:
ls
:列出目录内容,通常位于/bin/ls
cp
:复制文件或目录,通常位于/bin/cp
grep
:文本搜索工具,通常位于/bin/grep
find
:查找文件,通常位于/usr/bin/find
tar
:打包和解包文件,通常位于/bin/tar
查看常规命令:
你可以使用 which
命令来查找常规命令的路径,例如:
which ls
2. 内建命令
内建命令是指由 Shell 本身提供的命令,它们不依赖于外部可执行文件,而是由 Shell 内部实现。这些命令通常用于控制 Shell 行为或对 Shell 环境进行管理。
示例:
cd
:改变当前目录echo
:输出文本export
:设置环境变量history
:显示命令历史pwd
:显示当前工作目录alias
:创建命令别名unset
:删除环境变量
查看内建命令:
要查看 Shell 内建命令,使用 help
命令或查阅 Shell 的文档。例如,在 bash
中,你可以使用:
help cd
或者查看所有内建命令:
help
3. 总结
- 常规命令: 外部可执行文件,存放在系统目录中,使用
which
可以找到其路径。 - 内建命令: Shell 提供的内建功能,直接由 Shell 解释和执行,使用
help
可以查看其帮助信息。
5. 进程地址空间
5.1 什么是进程地址空间
在C / C++ 中,我们将内存分布大概理解为下图:
但看图比较抽象,我们可以利用下面的代码打印出相应的地址:
#include <iostream>
#include <stdlib.h>
int uninit_gval; // 未初始化全局变量
int init_gval = 100; // 初始化全局变量
int main(int argc, char *argv[], char *env[])
{
printf("main函数 地址: %p\n", main);
const char* str = "test message";
printf("只读字符串常量 地址: %p\n", str);
printf("已初始化全局变量 地址: %p\n", &init_gval);
printf("未初始化全局变量 地址: %p\n", &uninit_gval);
static int num = 0;
printf("静态局部变量 地址: %p\n", &num);
char* heap1 = (char*)malloc(100); // 在堆上申请空间
char* heap2 = (char*)malloc(100);
char* heap3 = (char*)malloc(100);
char* heap4 = (char*)malloc(100);
printf("heap1 地址: %p\n", heap1);
printf("heap2 地址: %p\n", heap2);
printf("heap3 地址: %p\n", heap3);
printf("heap4 地址: %p\n", heap4);
printf("stack1(&heap1) 地址: %p\n", &heap1);
printf("stack2(&heap2) 地址: %p\n", &heap2);
printf("stack3(&heap3) 地址: %p\n", &heap3);
printf("stack4(&heap4) 地址: %p\n", &heap4);
for(int i = 0; argv[i]; ++i)
{
std::cout << "命令行参数地址:" << std::endl;
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; ++i)
{
std::cout << "环境变量地址:" << std::endl;
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}
代码执行结果如下:
从上面的代码及其执行结果可以证明,之前所说的内存分布基本正确。
且内存分布可以理解为是 整体向下,局部向上 的。
这里我们提出一个疑问:
- 对于一个int型变量,其占有四个字节,当我们获取其地址时,获取到的是地址最大的字节还是最小的?
对于 C 语言中,一个 int型变量的地址表示的是其在内存中的起始位置。地址本身是内存中的最小单位,通常是字节。因此,获取到的地址是该变量占用内存的最小字节地址。即,获取的地址是该变量的第一个字节的地址,而不是最后一个字节。
那么,上图中我们所讲述的有关进程地址空间的内容是内存吗?
请看下面的代码:
#include <iostream>
#include <unistd.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int cnt = 5;
while(1)
{
// 重复打印5次进程pid 和g_val信息,最后一次更改g_val的值
printf("子进程pid: %d, g_val: %d, &g_val= %p\n", getpid(), g_val, &g_val);
sleep(1);
if(cnt == 0)
{
g_val = 200;
std::cout << "子进程改变g_val值: 100->200" << std::endl;
}
}
}
else
{
// 父进程
while(1)
{
// 循环打印信息
printf("父进程pid: %d, g_val: %d, &g_val: %p", getpid(), g_val, &g_val);
sleep(1);
}
}
sleep(50);
return 0;
}
代码分别对父进程,子进程进行了不同操作,代码执行结果如下,
- 我们可以看到,在循环数次后,子进程将全局变量
g_val
的值改为了200,而父进程的g_val
的值没有变化,这是由于进程间的独立性(拷贝) - 根据输出:尽管
g_val
值不同,但&g_val
的值却相同,即地址相同,为什么会出现这种情况? - 答:证明了这里的地址绝对不是物理地址,而是虚拟地址/线性地址。则上图结构也不是内存,而是 进程地址空间。
为什么有上面的结果?页表
页表是一种用于管理虚拟内存和物理内存映射关系的数据结构。
每一个进程运行时,都会有对应的进程地址空间。而进程地址空间里的是虚拟地址,虚拟地址通过页表映射到物理地址!
-
而对于上面代码中的父子进程,父子进程各有一个进程地址空间,指向相同的虚拟地址,通过页表映射到同一物理内存中
-
当子进程将全局变量g_val改变后,会发生写时拷贝,即子进程的虚拟地址会映射到一块新的物理空间,此时父子进程的实际物理地址已经不同,而虚拟地址还是同一个。
5.2 地址空间 与 区域划分
根据上述对进程地址空间的介绍,这里再次进行总结:
-
地址空间 **指的是一个进程可以使用的虚拟内存范围。**它是进程独立的内存空间,通常包括代码段、数据段、堆和栈。地址空间确保进程间的内存隔离,防止相互干扰。
-
区域划分 **涉及将地址空间分成不同的区域,以便于管理和保护。**例如,一个地址空间可以划分为代码区、数据区、堆区和栈区。这种划分帮助操作系统和应用程序有效地组织内存,提高性能和安全性。
地址空间提供了一个整体的视角,而区域划分则细化了这个空间以便于实际管理和使用。
5.3 进程地址空间的作用
根据上面的内容,显然进程地址空间为什么存在已经显而易见了,这里作最后的总结:
主要涉及系统的安全性、稳定性、效率和管理。以下是几个主要的原因:
1. 内存隔离
每个进程拥有独立的地址空间,这样可以确保不同进程之间的内存不会互相干扰。内存隔离:
- 防止一个进程意外或恶意地访问或修改另一个进程的数据。
- 提高系统的稳定性,因为一个进程的崩溃不会直接影响到其他进程。
2. 虚拟内存管理
虚拟内存是现代操作系统的核心功能之一。通过虚拟内存:
- 扩展内存容量:允许程序使用比实际物理内存更大的地址空间。操作系统可以将不活跃的部分存储在硬盘上(交换空间),从而支持大内存应用。
- 内存共享和重用:虚拟内存允许多个进程共享相同的代码段或库,减少内存使用的冗余。
3. 地址空间布局随机化(ASLR)
ASLR 是一种安全机制,用于随机化进程地址空间的布局。这:
- 增加了攻击者预测内存地址的难度,防止了许多针对内存的攻击,如缓冲区溢出攻击。
- 提高了系统的安全性,通过使恶意软件更难以利用已知的内存地址。
4. 动态内存分配和管理
虚拟地址空间使得动态内存分配变得更加灵活:
- 程序可以在运行时分配和释放内存(如使用
malloc
或new
)。 - 操作系统和内存管理单元(MMU)可以高效地管理和优化内存使用。
5. 简化编程模型
进程地址空间提供了一种简化的编程模型:
- 程序员可以假设程序有一个线性的、连续的内存空间,而不需要考虑物理内存的具体位置。
- 可以方便地使用相对地址而不是物理地址来进行内存访问,提高编程效率和可移植性。
6. 支持多任务处理
操作系统通过为每个进程分配独立的地址空间,实现了多任务处理:
- 进程可以并行运行而不会相互干扰。
- 操作系统可以有效地调度和管理多个进程的内存需求,提高系统的整体性能。
7. 错误检测和保护
进程地址空间的隔离有助于系统检测和处理错误:
- 操作系统可以检测非法内存访问并执行适当的错误处理(如进程终止或生成错误报告)。
- 提高了系统的可靠性,减少了由于程序错误导致的系统崩溃风险。
综上所述,进程地址空间的存在是为了提供安全、稳定和高效的内存管理,支持多任务处理,并简化编程模型。这些特性使得操作系统能够有效地管理系统资源,并为应用程序提供可靠的执行环境。