冯诺伊曼体系结构
对于我们常见的计算机,包括笔记本,服务器等等都遵循冯诺依曼体系结构
简单而言计算机就是由一堆硬件构成的,输入设备经过数据的处理在到输出设备。
输入设备: 键盘,鼠标,扫描仪等
中央处理器(cpu):运算器和控制器和寄存器等
输出设备:显示屏,打印机等
关于冯诺伊曼体系结构必须强调一下几点
1.对于这里的存储器,是指内存,也就是8g,16g等,不是硬盘
2.不考虑缓存情况,这里的cpu只能和存储器打交道,也就是内存,不能访问其他外设(输入或输 出设备)
3.输入或者输出设备也只能写入内存或者从内存中读取
4.外存是相对于内存和cpu所说的,像磁盘、网卡等等都是外存,具有永久性存储能力
总而言之,cpu只能和内存打交道(提高整机效率)
对于cpu,只能被动接受别人的指令和数据,那就必须让cpu认识指令,所以cpu有自己的指令集,我们平时写的代码本质经过编译后产生二进制可执行程序就是写cpu认识的指令,让cpu去执行我们想要的过程。
操作系统概念与定位(operator system)
概念:是一个进行软硬件资源管理的软件,笼统的讲,操作系统包括内核(进程管理、内存管理、文件管理、驱动管理)和其他程序(例如数据库、shell程序等等)
设计os的目的:为用户提供稳定的良好的执行环境(因为你也不想你在使用计算机设备时出现蓝屏啊等情况吧)和与硬件交互,管理所有的软硬件资源
管理:管理的本质就是对数据的管理,管理的方法就是先描述后组织,在后续的学习中我们会深有体会
感性理解操作系统:
用户:也就是使用计算机的我们
系统调用接口(System Call Interface):是操作系统提供给应用程序的一组接口,它允许应用程序请求操作系统内核执行特定的操作,是应用程序与操作系统内核之间进行交互的桥梁。 例如进程管理:包括创建新进程、终止进程、等待进程结束、进程调度等。例如,在 Linux 系统中,fork()
函数用于创建一个新的进程,exec()
函数用于执行一个新的程序。
用户操作接口(User Interface,简称 UI):,也常被称为用户界面,是指用户与计算机系统、软件应用程序或其他设备进行交互的接口。它涵盖了用户与系统之间的信息输入和输出方式,旨在为用户提供便捷、高效且直观的操作体验
简单通俗来讲,c/c++库封装好给你的就是用户操作接口,我们可以操作操作系统进而操作硬件,而封装好的里面肯定调用的是系统接口,像malloc是封装好的,你只管用,malloc主要依赖与两个系统接口,mmap()和brk();
感性理解:操作系统更像一个决策者,根据数据做决策,驱动更像一个执行者,而硬件就是一个被管理者,执行者通过被管理者拿数据,交给决策者(os),os做出命令,驱动执行命令
也就是os做决策,驱动通过硬件拿数据和执行os的命令,硬件去执行任务,然后返回数据给驱动然后os就能拿到数据
总结:
管理:先描述后组织
如何描述:简单来说就是用结构体struct
如何组织:简单来说就是用所学到的高效的数据结构,链表等等
什么是进程?
概念:进程是指在系统中正在运行的一个程序实例,是操作系统进行资源分配和调度的基本单位。当一个程序被加载到内存中并开始执行时,它就成为了一个进程。(Windows下打开任务管理器就可以看到各个进程的调度情况) 内核观点:进程是担当分配系统资源(cpu时间,内存)的实体
描述进程-PCB
之前所认识到的冯诺依曼体系结构中提到:cpu只能和内存打交道,所以磁盘上有很多的程序要执行,就必然有很多程序加载到内存中,就会变成进程,那么多的进程我们如何进行管理(先描述,后组织),先描述:PCB(process control block)也就是进程控制块,在Linux下是task_struct,用于描述进程的各种属性和状态,是 Linux 内核管理进程的核心数据结构。它包含了进程运行所需的各种信息,内核通过对task_struct的操作来实现对进程的创建、调度、执行和销毁等管理。
struct task_struct
{
//该进程的所有属性;
属性:标识符:(用来区别其他进程)
状态:任务状态,退出码等
优先级:等等属性
//该进程对应的代码和数据地址;
struct tast_struct* next;//指针链接起来,链表
}
//这里是操作系统帮你自动做的,你的可执行程序只有你的代码逻辑,并没有这些属性
先描述后组织:如何组织:通过链表
优先级:遍历链表找到优先级高的先执行,退出:判断pcb属性状态是否为死亡,死亡就删掉结点
所谓的管理:先描述(pcb)后组织(链表),就变成了对进程对应的PCB进行相关的管理,最后是对链表这种数据结构增删查改。
进程:现在我们可以对进程有一定的认识,进程就是内核数据结构(task_struct)+进程对应的磁盘代码
查看进程(Linux下如何查看)
1.ps ajx | grep "test" //test是我们的可执行程序 grep "test"可以过滤其他的进程,方便我们查看test这个进程
2.ls /proc/ PID //这个目录是内存级的目录,这个目录下也有进程的相关信息,一个进程如果源程序被删了,但还会执行,因为已经被加载到内存了,此时是进程,源程序被删跟它此时执不执没关系,只是ls /proc/ PID exe会变红
head -1 可以拿到标题,也就是PPID等信息,方便我们查看
PID:这个进程的id (可以kill -9 PID,就可以杀掉这个进程)
PPID:父进程的id
进程有关的系统调用(通过系统调用获取进程提示符)
getpid():获得进程的id
getppid():获得父进程的id
这些函数定义于 #include <sys/types.h>#include <unistd.h>
头文中 重启进程,PID会变,但是PPID不会(bash)后面会学习
fork():创建进程,这个新进程被称为子进程,而调用 fork
的进程则是父进程 父子进程共享一份代码,数据各自开辟空间,私有一份(采用写时拷贝)
fork的返回值:pid_t fork (void); //fork之后,给父进程返回子进程的id,给子进程返回0
通常fork用if语句进行分流(通过返回值不同),让父子进程执行后续共享代码的一部分 (并发式编程)
进程状态:
概念:进程状态描述了进程在其生命周期内所处的不同阶段,它反映了进程当前的活动情况和系统对进程的管理状态。(简单来说,就是进程在不同的队列中等待某种资源)(学习的越多后面对概念的理解会更清晰)
在大多数教材当中:进程状态有运行、新建、就绪、死亡、等待、阻塞等等 (接下来的讲解以Linux内核为准)
struct runqueque
{
task_struct*head;//头结点
//其他属性
}
通常情况,一个cpu对应一个运行队列 让进程入队列, 本质就是让进程的PCB(task_struct)结构体对象放入运行队列中
R(running):进程PCB要么在运行队列中,要么在运行中就叫运行状态
进程不只会等待和占用cpu的资源,也有可能占有外设的资源。 例如:一个进程在cpu中跑,碰到fwrite是要去磁盘写入,但此时磁盘正在被其他进程占用,cpu快,磁盘慢,那cpu会等磁盘中的进程结束后,在让此时在cpu中跑的进程去磁盘写入吗?显然不会,这样效率太慢了,实际:cpu这个进程去对应的硬件的等待序列,cpu去跑其他进程,等磁盘好了就把状态改为R(先简单理解后续会详细讲解)
struct dev_disk
{
task_struct* wait_queue//这是磁盘的等待序列
//其他属性
}
这就叫由R状态变为阻塞状态,这样搞来搞去的不是代码和数据,是task_struct结构体对象
问题:如果阻塞状态过多,进程一开始会被加载到内存当中,是会占有一定的大小的,阻塞状态过多,加载到内存的代码和数据就会长时间没有被执行而留在内存上,占有的内存大小过多,就会导致其他程序不能加载到内存上。解决方案:操作系统把不会立即被cpu调度的进程的数据和代码暂时放到磁盘上的特定区域(换出)(这样的进程的状态叫挂起),当资源被释放了,轮到这个进程跑了,操作系统会自动帮你从磁盘当中的挂起状态改为R(换入),进程从阻塞到挂起,在由挂起到运行的状态转换过程中包含了内存数据的换入和换出。
总结:阻塞不一定挂起,挂起一定阻塞
我们简单了解了一些状态,那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 */
};
简单介绍各个状态的意思
R(running):运行状态
S(sleeping):休眠状态,阻塞的一种
T(stopped):暂停状态,阻塞的一种
D(disk sleep):深度睡眠状态,S是浅度睡眠,可以被终止,D状态无法被os杀死,只能断电/自己醒来,只有在高IO的状态下才可能出现D状态(例如很多的数据正要写入磁盘,如果你被os杀死,就可能会导致数据丢失)
t(tracing stop):是一种暂停状态,比如你在调试的时候就会出现t状态,表示该进程正在被追踪
X(dead):死亡状态,太快了,你ps axj时看不到
Z(zombie):僵尸状态
int a=0;
while(1)
{
a=1+1;
printf("%d",a)
}
[Day2@VM-8-11-centos ~]$ ps axj | head -1 && ps axj | grep a.out
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
27297 29772 29772 27297 pts/1 29772 S+ 1001 0:01 ./a.out
29847 30071 30070 29847 pts/0 30070 S+ 1001 0:00 grep --color=auto a.out
[Day2@VM-8-11-centos ~]$ kill -9 29772
我先写了一个死循环,死循环中由printf语句,像显示器中打印,显示器是外设,对于cpu来说很慢,cpu太快了,即使有时间在R,你每次ps axj大概率都看到的状态是S+,可以看到STAT那一栏变为S+(这里有个+号,后面会做解释)(阻塞状态)
在 Linux 系统中,kill
命令用于向进程发送信号,从而对进程进行控制 可以通过kill -l查看所有的编号
kill -9 PID :会杀死某个进程
kill -19 PID:会把某个进程由别的状态变为T状态(暂停状态):属于阻塞的一种,至于有没有被挂起,取决于操作系统
kill -18 PID:信号编号 18 对应的信号名称是 CONT
(Continue),即继续执行信号。 由T变为R(没有+号)由T变为S(没有+号)
需要注意的是,使用 STOP
信号停止进程后,通常需要使用 CONT
信号或其他方式来恢复进程执行,否则进程将一直处于暂停状态。如果不希望进程继续执行,可以使用 kill -9 PID
命令来强制终止进程,但这种方式可能会导致进程数据丢失或系统不稳定,应谨慎使用。
S与T的区别
S
状态(可中断睡眠状态):进程处于睡眠状态,正在等待某个事件的发生,例如等待 I/O 操作完成、等待信号等。处于该状态的进程可以被信号中断,一旦等待的事件发生或者接收到信号,进程就会被唤醒,进入运行队列等待 CPU 调度。例如,当进程读取文件时,如果数据尚未准备好,进程就会进入 S
状态等待数据准备完成。
T
状态(暂停状态或跟踪状态):进程被暂停执行,通常是因为收到了特定的信号,如 STOP
信号(通过 kill -19
发送)或 TSTP
信号(通常由终端输入 Ctrl + Z
产生)。处于 T
状态的进程不会占用 CPU 资源,也不会被调度执行,直到收到 CONT
信号(通过 kill -18
发送)才会继续执行。另外,当进程被调试器跟踪时,也会进入 T
状态,以便调试器对其进行控制和检查。
状态有无+号的区别
状态有+号的叫前台进程,可以由ctrl+c终止,并且运行期间ls等没用,就像刚刚打印了一堆a的值,你在屏幕当中ls等命令是没有用的
状态没有+号的叫后台进程,不可以由ctrl+c终止,ls有用,只能通过kill -9 PID杀死
如何看到Z(僵尸状态)
你写代码让cpu去完成任务,有两种情况,第一你想要知道完成如何,第二你不关心,无论你关不关心,os都必须保留结果,这就当进程退出时,不能立即释放该进程对应的资源,而是会保存一段时间,让父进程或os来读取(这里后面会讲解),在读取期间的状态就叫Z状态,然后读取完了之后就会变成X状态,由os来回收或者父进程回收,不回收可能会导致内存泄漏,所以如何看到?当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵尸状态,僵尸进程会以终止状态保持在进程表中,等待父进程读取退出状态码,所以只要子进程退出,父进程在运行并且没有读取子进程的状态,子进程就会进入Z状态。
僵尸进程的危害:
1.进程的退出状态必须被维持下去,因为它要告诉父进程你交给我办的任务完成得怎么样,如果父 进程一直不读取,子进程就会一直处于Z状态
2.维护退出状态本身也要用数据维护,属于进程基本信息,所以保存在task_struct(PCB)它包含了进程得各种信息,包括进程的状态,标识符,内存映射,打开的文件描述符等等中,换句话说,Z状态一直不退出,PCB就要一直维护,就要一直占有空间
3.如果一个父进程创建了很多的子进程就是不回收,会造成内存资源的浪费。
4.内存泄漏
特殊的进程:孤儿进程
如果父进程先退出,子进程后退出,此时的子进程就叫孤儿进程
父进程exit时是由bash回收了,看不到Z状态,此时子进程在运行称为孤儿进程,孤儿进程会被操作系统中的特殊进程收养,通常是 init 进程(进程号为 1)成为孤儿进程的新父进程,然后由init进程回收
进程优先级
cpu资源分配的先后顺序,优先级高的有优先执行权利,配置进程优先级对多任务环境的Linux很有用,可以改善系统性能。
查看系统进程
在Linux中,用ps -l命令:用于查看当前进程详细信息的命令 -a
选项用于显示所有与终端关联的用户的进程
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 2534 2533 0 80 0 - 3915 wait pts/0 00:00:00 bash
0 R 1000 2578 2534 0 80 0 - 3916 - pts/0 00:00:00 ps
F
(进程标志):显示进程的标志位,它是一个数字,不同的数字组合代表不同的含义。例如:1
表示进程拥有超级用户权限。4
表示进程是通过fork()
产生的子进程。S
(进程状态):显示进程当前的状态UID
(用户 ID):运行该进程的用户的 ID。可以通过/etc/passwd
文件查看用户 ID 对应的用户名。PID
(进程 ID):进程的唯一标识符,系统中每个进程都有一个独一无二的 PID。PPID
(父进程 ID):该进程的父进程的 ID。父进程是创建当前进程的进程。C
(CPU 使用率):进程最近使用 CPU 的百分比,是一个相对值。PRI
(优先级):进程的优先级,数值越小表示优先级越高,越容易被 CPU 调度执行。NI
(nice 值):进程的 “nice” 值,也称为谦让值。可以通过nice
或renice
命令调整该值,范围是 -20 到 19,值越小优先级越高。ADDR
(内存地址):进程在内存中的地址,通常显示为-
,表示不显示具体地址。SZ
(内存大小):进程使用的虚拟内存大小,单位是页(通常一页为 4KB)WCHAN
(等待通道):进程正在等待的内核函数或事件。如果进程没有在等待,显示为-
。TTY
(终端设备):与进程关联的终端设备。如果进程不与终端关联,显示为?
。TIME
(CPU 时间):进程总共使用的 CPU 时间。CMD
(命令):启动该进程的命令。
我们先关注第7和第8点 即PRI和NI,PRI中的值越小代表优先级越高 ,nice值就表示可被执行的优先级的修正数值,PRI(new)=PRI(old)+nice,所以,调整优先级在Linux下就是调整nice值 范围是 -20 到 19,
用top命令更改已存在进程的nice值
top(sudo提权成root才可以改nice值)
进入top后按"r"->输入进程PID->输入nice值,clear退出
环境变量
环境变量:一般是指在操作系统中用来知道操作系统运行环境的一些参数,为了满足不同的应用场景而预先在系统内设置的一大批的全局变量
例如:我们在编写c/c++代码的时候,在链接的时候,我们从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关的环境变量帮助编译器进行查找
环境变量通常具有某些特殊用途,在系统中通常具有全局特性,意味着它们在整个操作系统环境中都可以被访问和使用。一旦环境变量被设置,无论是在当前用户的会话中还是系统级别的设置中,它们都可以在系统的各个部分被引用。
查看环境变量的方法:
echo $NAME //NAME为你环境变量的名称,可以显示环境变量的值
[Day2@VM-8-11-centos ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Day2/.local/bin:/home/Day2/bin
常见的环境变量:
PATH:指定命令的搜索路径
HOME:指定用户的主工作目录(即用户登录到Linux系统时默认的目录)
SHELL:当前shell,它的值通常是/bin/bash
USER:当前登录的用户,知道了用户就可以知道它的权限,清楚它能干什么不能干什么
PATH:解释
为什么我们的可执行文件需要加绝对路径才能执行,也就是./a.out,而系统级别的命令如ls等他们也是一个可执行程序,直接就ls就会执行,因为ls在环境变量PATH的搜索路径中
[Day2@VM-8-11-centos ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Day2/.local/bin:/home/Day2/bin
可以看到当你echo $PATH时出现一堆路径,每次执行命令时会在这些路径下搜索,有就执行命令,没有就报找不到命令
如何让自己的可执行程序像ls一样,可以export PATH=$PATH:test(可执行文件) 所在路径 这样新的PATH=旧的+你的程序所在路径
当然你也可以sudo cp test /usr/bin,把可执行程序拷贝到其中一个路径,但这样会污染指令集
[Day2@VM-8-11-centos dir1]$ gcc test.c -o test1
[Day2@VM-8-11-centos dir1]$ ll
total 16
-rwxrwxr-x 1 Day2 Day2 8360 Apr 24 14:14 test1
-rw-rw-r-- 1 Day2 Day2 83 Apr 24 14:13 test.c
[Day2@VM-8-11-centos dir1]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Day2/.local/bin:/home/Day2/bin:/home/Day2/dir1
[Day2@VM-8-11-centos dir1]$ test1
0[Day2@VM-8-11-centos dir1]$
可以看到我编译了一个文件形成test1可执行程序,并且已经把路径添加到了PATH中,当我直接test1时,打印出来一个0;
HOME:解释
[root@VM-8-11-centos ~]# echo $HOME
/root
[Day2@VM-8-11-centos dir1]$ echo $HOME
/home/Day2
可以查看当前的家目录,用root用户和普通用户分别执行echo $HOME对比差别
根据命令cd ~就可以知道这个命令在执行时,是找HOME这个环境变量
和环境变量相关的命令:
1.echo:用来查看当前环境变量值
2.export:设置一个新的环境变量,当你使用export
命令在当前终端会话中改变了某些环境变量,通常情况下只在这次会话中有效。当你重新登录或者打开一个新的终端时,系统会重新加载原来的环境变量,这是因为你没有修改相应的配置文件
3.env(environment):显示所有的环境变量
4.unset:清楚环境变量和本地变量(这个本地变量稍后讲解)
5.set:显示本地定义的shell变量和环境变量
如何通过函数调用获取环境变量:
getenv()函数调用
NAME
getenv, secure_getenv - get an environment variable
SYNOPSIS
#include <stdlib.h>
char *getenv(const char *name);
char *secure_getenv(const char *name);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
secure_getenv(): _GNU_SOURCE
通过查询手册,我们可以知道getenv是用来获取环境变量的
char* who=getenv("USER");
那对于这条语句,我们是不是就可以知道当前运行这个程序的主机的用户,就可以标识,然后确定这个用户的权限能做什么
本地变量与环境变量的区别:
可以看到最上面第一行,直接myval=1234,echo $myval时可以看到打印1234 但是当我env时并没有查到myval ,因为此时的myval是本地变量,这种本地变量是局部变量, 环境变量具有全局性,getenv():获取环境变量,这种本地变量用getenv获取不到
如何从本地(局部属性)到环境变量(全局属性):export myval
可以看到env后有环境变量myval,这样env和getenv都可以得的到环境变量
深入理解环境变量和本地变量:
你自己写了一个可执行程序mycmd,里面获取myval(只在bash中muval=1234,并没有加到环境变量,此时是本地变量),当你./mycmd时,系统进程bash,会创建一个子进程,你的mycmd就是bash的子进程,环境变量具有全局性,是会被子进程继承下去的(因为要适应不同的应用场景),那就可以getenv("USER")来进行身份标识,而myval没有被继承,只会在当前进程(bash)中有效,所以你getenv("myval”);时获取不了,而你在echo $myval可以看到它的值是因为你此时在bash的进程中
环境变量的应用场景体会:
为什么ls时就会帮你显示当前路径下的文件和目录;
因为编写ls这个命令程序的人,getenv("PWD");pwd是记录当前路径的环境变量,那是不是获得环境变量后可以找到你当前所属的路径,那就可以显示出来;总之ls是一个程序,当你运行时,bash创建子程序去ls,ls这个程序继承环境变量,所以ls知道PWD
环境变量的组织方式:
每一个程序都会获得一张环境表。环境表以"\0"结尾的环境字符串
如何继承
1.理解命令行参数,在devc++编译器中, 你会看到你编写的int main(int argc,char* argv[],char* env[]);vs2022可以自己添加;
argc
和argv
:用于传递命令行参数。argc
表示命令行参数的数量,argv
是一个字符串数组,存储着每个命令行参数。argv的第一个参数是“ls”,第二个是你传的“-a”等env
:是一个字符串数组,用于存储环境变量。不过,即使你没有在main
函数中声明env
参数,环境变量仍然会被继承,只是你无法通过env
数组来直接访问它们。
当你在 bash
进程下执行 myls -a
命令时,-a
会作为一个命令行参数被传递给 main
函数的 argv
数组
通过现在的学习就可以编写一个属于自己的myls命令程序:大致思路,可以通过比较命令行参数,比如传进来-a就去做-a的事情
int main(int argc, char *argv[]) {
DIR *dir;
struct dirent *entry;
const char *path;
// 如果没有提供命令行参数,默认使用当前目录
if (argc == 1) {
path = ".";
} else {
path = argv[1];
}
// 打开指定目录
dir = opendir(path);
if (dir == NULL) {
perror("无法打开目录");
return 1;
}
// 读取并打印目录中的每个条目
while ((entry = readdir(dir)) != NULL) {
printf("%s\n", entry->d_name);
}
// 关闭目录
closedir(dir);
return 0;
}
总之:会传两只表,一个是运行时传进来的命令行参数形成argv[]命令行参数表(通过比较表中的值就可以知道去执行什么样的操作)另一个是继承父进程的环境变量表
2.如果我没有显示的继承char* env[],操作系统也会帮我们隐式继承,这是操作系统已经帮我们做好的事情,你照样可以getenv()获得环境变量,但你无法打印出来所以的环境变量,也就是通过for循环去操作env[i]来打印所有的环境变量
可以通过第三方变量environ获取
可以看到environ是一个二级指针,libc中定义的environ是一个全局变量,并没有包含在任何的头文件中,声明一下就可以使用了
#include <stdio.h>
#include <stdlib.h>
// 声明 environ 变量
extern char **environ;
int main() {
// 遍历 environ 数组
for (char **env = environ; *env != NULL; env++) {
// 打印每个环境变量
printf("%s\n", *env);
}
return 0;
}
进程地址空间:
对于一个进程来说,根本就不会给4G的空间给这个进程用 ,当进程进来的时候,创建PCB,PCB当中又会创建一个struct mm_struct结构体对象,mm_struct*mm=malloc之后
struct mm_struct{
code_start=null;
code_end=null;
等等区域的划分;}
mm->code_start=0x11111111
mm->code_end=0x12111111
代码区多大?当程序被加载到内存中时就可以读取然后区初始化start和end了
stack向下增长,heap向上增长,本质就是修改start和end的数据
free缩小,malloc扩大这样就可以为区域划分
每个地址都是虚拟地址(不是真实的物理地址),因为可能有的进程的数据地址大小可能一样,但实际数据不一样
这张图能够清晰的表示虚拟地址和物理地址的区别:
假设对于全局变量g_val,创建子进程去修改g_val,即使你打印g_val的地址一样,但两个也属于不同的空间。
解释:当你创建进程时,就会创PCB,然后根据其在加载到内存的代码和数据,把PCB中的mm初始化,什么虚拟地址到什么虚拟地址是代码区,划分区域,这也就是进程地址空间,然后建立页表的映射关系,把虚拟地址通过页表和物理地址映射起来,所以我们平时&g_val是一个虚拟地址。
所以进程地址空间与物理内存之间是通过页表进行映射的
1.为什么要有进程地址空间?直接让它访问物理空间不就行了? 因为不安全,万一进程非法越界操作呢?因为页表存在就只会映射到安全区域
2. 那为什么上面的子进程修改g_val没有影响到父进程?因为这是进程的独立性的体现? 一个进程对被共享的数据做修改,而如果影响了其他进程,这就不能称作进程的独立性,任何一方尝试对共享数据修改时,os会先进行数据拷贝,更改页表映射(写时拷贝),所以子进程在改全局变量g_val时发生写时拷贝,映射关系改变了,所以子进程修改其值不影响父进程的值
3.为什么你的可执行程序中在没有被加载到内存时是有地址的? 因为虚拟地址空间,不仅仅是os要遵守对应的规则,编译器也要遵守,所以在编译代码的时候,就是按照虚拟地址空间的方式对我们的代码和数据进行编址的 ,在实际运行时,在通过页表映射到物理内存中,这样方便进程以统一的视角看待进程对应的代码和数据等各个区域,也方便编译器以统一的视角进行编译代码(编完即可使用,因为规则一样)
4.cpu至始至终看到的都是虚拟地址,通过虚拟地址,然后通过页表映射到实际的物理内存,然后读取数据
总结:
至此,我们已经初步了解了进程的概念,以及初步了解了一个程序到进程在结束的大致的流程。