1.什么是进程?
有些书上对进程的描述是这样一句话:进程是在内存中的程序。一个运行起来(加载到内存)的程序称作进程。
这样描述确实是没有问题,但我们需要进一步的理解这一句话所代表的知识。
首先,我们要知道,程序和进程相比,进程具有动态属性,那么这就代表着,当程序加载到内存中后,这个程序不能直接叫做进程。
我们写的程序,本质是文件,在磁盘中放着,从输入设备中输入,先存放在磁盘中,根据冯诺依曼体系,当我们要运行程序的时候,会先将程序从磁盘中搬到内存中。
那么问题来了,当有太多的加载进来的程序时,操作系统要不要管理这些加载进来的多个程序?怎么管理?如果那么多个程序中,我需要关闭一个,那要怎么关闭?怎么找到?......
同样的,操作系统管理的本质:是先描述再组织!
所以,当程序进入内存后,操作系统将会为程序创建进程控制块,将程序的属性写入。描述后,再使用数据结构(链表)组织起来。
组织完成后,当CPU需要调动某个程序的时候,操作系统只需要遍历一下链表(注意是遍历PCB,不是加载进来的程序),找到那个需要的进程,然后交给CPU就可以了。同理,当需要释放某个进程的时候,将对应的进程内存空间释放,然后在链表上将释放的进程的下一个进程链接起来就OK了。
所以,所谓的对进程管理,就遍历对进程对应的PCB进行相关的管理,对进程的管理转化成了对链表的增删查改!
补充说明:这里的struct task_struct是操作系统的内核结构体,用来描述进程的结构体,当有进程加载到内存的时候,就会创建内核对象(task_struct),并且将该结构和代码和数据关联起来,完成管理工作!所以,所谓进程管理,管的是进程在内存中的PCB,也就是上图黑色的那一块!
得出结论:进程 = 内核数据结构+进程对应的代码。
通过这里我们就能明白,为什么操作系统里面要有PCB(struct task_struct)结构体了,因为管理的核心是对数据的管理,最初拿到的数据是杂乱无章的,量大,因此需要将数据的抽象属性分离出来,将其组织起来。
2.查看进程
接下来,我们去见见进程。
在Windows下,就是在任务管理器中,相信这里绝大多数人都知道任务管理器中就可以看见我们打开的进程。
那么在Linux下呢?
先在Linux下,编一个测试代码:
当我们写好代码后,然后make一下,编译出来,此时,myproc还不是一个进程,是在磁盘里面的一个代码文件。
等我们将其运行,跑起来了,才是一个进程。
那我们要在哪里看这个进程呢?
2.1查看进程的指令
指令: ps ajx 这个指令是查看当前系统下的进程
使用指令:ps ajx | grep 'myproc' 找出myproc这个程序的进程情况
使用指令:ps ajx | head -l && ps ajx | grep 'myoroc' 可以显示出对应信息的说明
其中:PPID叫做父进程的IP,PID是子进程的PID,也是myproc的进程ID
使用指令:杀掉一个进程:kill -9 进程的PID
3.常见进程调用
①getpid()和getppid();
getpid()是获取子进程ID的函数,getppid是获取父进程ID的函数。
我们发现,父进程,只要文件代码没有被修改,父进程的ID是不会变的,但是子进程,每一次重新运行,都会变。
②fork();函数
现在需要了解的是,fork函数会有两个返回值,使得父进程和子进程将fork后续的代码共享起来,也就是说,通过不同的返回值,让父子进程同时执行后续代码。
发现,在同一个时间,没有循环,竟然同时执行了if 和 else if的代码,同时,在while是死循环的时候,两个死循环的while同时执行!这就是fork函数带来的效果。。
4.进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)
进程的状态有:运行、新建、挂起、阻塞、等待、停止、死亡等等。
4.1 普遍的操作系统层面是如何理解上面所述说的进程的状态的概念呢?
进程有那么多状态,本质上是为了满足不同的运行场景!
4.1.1 运行和阻塞
运行状态:
对于运行来说,其实操作系统内核,会为CPU准备一个运行队列,一个CPU一个运行队列。因为CPU的速度很快,所以对所有需要运行起来的进程来说,这些进程的PCB(sturtc task_struct 对象)都需要先进入CPU的运行队列中,等待运行。而这些PCB进入运行队列的进程,也就是准备运行的进程所处的状态,就叫做运行状态!不是说在运行的时候,才叫运行状态!
阻塞状态:
进程或多或少地也会去访硬件。比如,我们在写scanf,cin等等的代码的时候,访问的就是键盘。但是硬件的速度,相比于CPU来说,是非常慢的。同样的,一般来说我们计算机里面的硬件数量很少,一块键盘、一个显示器、一个磁盘、一个网卡等等,但是会有很多个进程,同时去访问硬件。那么问题来了!硬件速度比不上CPU的速度,但是CPU运行的进程却有好几个要去访问硬件,那怎么办?
比如:A、B、C三个进程(PCB),要访问磁盘,A先访问,那么B和C只能选择等待!所以,这意味着进程,也可能是在占用外设资源,不单单地只占用CPU资源!
那么外设硬件是如何管理这些进程的呢?
也是通过PCB(task_struct)来进行管理!
那么,当B和C进程(PCB),本来是在CPU的运行队列中的,因为需要访问硬件,但是硬件正在忙碌,就将B和C进程(PCB)从CPU上剥离下来,转到硬件的q中。这意味着,此时的B和C,已经不再是处于运行状态了,那么是什么状态呢?阻塞状态!阻塞状态:等待外设资源
当A的进程结束,磁盘说:我好了,可以下一个了!那么,B就会再次地,从阻塞状态,回到运行状态,也就是说,此时的B,会从磁盘的队列中剥离出来,回到CPU的运行队列中,然后CPU就会运行它,在运行的时候,就会自动地调用磁盘,完成进程的运行!当然,从磁盘到CPU,或者从CPU到磁盘,都是操作系统干的活!
这说明了,所谓的不同进程状态,本质就是进程在不同的队列当中,等待某种资源!
4.1.2 挂起和阻塞
挂起状态:
当有许多个进程(PCB)处于阻塞状态的时候,这意味着有些是在短期内不能被使用的,如果此时内存空间不够了怎么办?当内存空间不够了,内存空间都被阻塞了,那么那些处于运行状态的进程,怎么办?
此时就需要操作系统出手了!操作系统会将那些短期内不被使用的进程的代码和数据,暂时地搬到磁盘中去保存起来,因为磁盘空间很大,磁盘会有专门的空间用来存放它们。这样,内存不就节省了空间了吗,这些空间就能够被继续使用了!
被暂时地保存在磁盘中的进程所处的状态,就叫做挂起状态!
需要注意的是,此时被挂起的进程的PCB还是在内存中,是代码和数据放在了磁盘中!
将进程的相关数据加载或保存到磁盘中,叫内存数据的做唤入唤出。当挂起的进程可以去运行了,那么,操作系统也会给这个进程腾出内存空间出来,操作系统是公平的!
挂起和阻塞的区别在哪?
阻塞不一定挂起,挂起一定是阻塞。这得看内存空间的大小,内存空间够大,那么就不用去挂起,就只是阻塞了。不够大,那么就得去挂起!
其实对于挂起,只要不是处于运行状态,都有可能被挂起!因此,挂起状态可以与跟很多状态联系在一起。
4.2 Linux是怎么做的?在Linux下,具体的Linux操作系统的状态。
下面的状态在kernel源代码里定义:
/* * 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 */ };
Linux下的阻塞状态
我们在Linux下进行查看进程状况:
我们发现,为什么在状态那一栏,不是R,而是S,S是休眠?不对啊,我的进程在运行这呀!怎么回事?
原因是printf,输出的地方是在显示器,是外设,而外设的速度很慢,相比于CPU,也就意味着在每次打印的时候,有百分之九十九的时间,都是在等IO就绪,百分之一的时间执行打印输出。因此,在进程中显示的状态,为休眠状态。
这里就可以推出,只要是需要访问外设的进程,基本上都查不到处于运行状态的!
因此,在Linux中,Sleep是阻塞状态的一种!
这货是R:
停止进程
可以使用kill -l来停止当前进程:
这里是19号就是T,stop暂停!
此时我们再去查这个进程的状态,就变成了T状态。
其实,它也属于阻塞的一种!但也没有被挂起,就不知道了,由操作系统决定。
暂停后,想要继续的话,使用18号,代表着继续:
但此时,我们发现,R前没有了+号。
9号,代表着干掉这个进程。我们发现,这个进程的信息没了。
+号是什么意思?
有加号的话,就表示,这个进程是前台进程,当我们在进程在运行的时候,在打印信息的时候,我们往shell的命令行输入指令,都没有用,没有任何反应,打印还在继续。然后使用CTRL+c可以使其终止下来。
没有加号,表示这个进程是后台进程,当我们在进程运行的时候,在打印信息的时候,我们往shell的命令行输入指令,这个指令对于的操作就会执行。打印还在继续。但是不能使用CTRL+c使其终止,只能使用kill -9 {PID}来使其终止。
这就是加号的意思,以及前后台进程的意思。
R叫做运行状态,T是暂停,S是休眠,T和S都是阻塞的一种,那么D呢?
深度睡眠
D是深度睡眠,S是浅度睡眠。
先说明一下:如果当内存空间不足,然后通过挂起,也不能腾出空间的时候,OS就会自动将短期内不使用,且挂起的进程杀掉。
浅度睡眠是可以被终止的,也就是说我们使用CTRL+c,就可以将其终止,但是深度睡眠就不行了。在深度睡眠下的进程,无法被操作系统杀掉,只能通过断电,或者进程自己醒来解决。深度睡眠是在高IO的情况下出现的,在Linux下,dd这个指令可以试试,但我不敢,嘿嘿。
T是暂停,t也叫暂停,表示该进程正在被追踪。
僵尸进程
在Linux中,X代表着进程的死亡状态,Z代表着僵尸状态。
Z是Linux中比较重要的状态。
一个进程被创建出来,是为了完成任务。当这个进程完成任务后,便需要退出。但是,进程退出的时候,并不能立即释放该进程对于的资源!而是需要保存一段时间,让父进程或OS来读取该进程是什么原因而退出之后,该进程的状态才会被记为X状态,也就是死亡状态。
在保存的这一段时间里,就叫做僵尸状态。
孤儿进程
子进程退出,留下父进程,那么这个进程叫做僵尸进程。如果反过来呢?父进程先退出,留下子进程,那么这个子进程就叫做孤儿进程。
1.孤儿进程的这种现象一定存在的
2.当孤儿进程存在的时候,这意味着,留下的这个子进程被操作系统领养,也就是1号进程。
3.如果不领养的话,那么在子进程退出的时候,对应的僵尸进程就没有人能回收(子进程的父进程先退出后,由于父进程也有自己的父进程,也就是子进程的爷爷进程)
4.被领养的子进程,就叫做孤儿进程
5.孤儿进程是后台进程
5.进程优先级(了解范畴)
什么是优先级?
优先级就是先做和后做的问题。
为什么存在优先级?
资源有限。我们的CPU就一个,磁盘就一个等等,但是进程太多,需要访问硬件的进程很多,所以需要确认进程的优先级进行排队处理。
Linux优先级特点
优先级的本质和就是PCB里面的一个整数数字(也可能是几个)。也就是使用这个整数,来确认优先级。
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
其中:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
对于最终优先级,最终优先级 = 老的优先级 + nice。Linux支持进程运行时对优先级进行调整,调整的策略就是更改nice。
如何改nice值?
用top命令更改已存在进程的nice:
top进入top后按“r”–>输入进程PID–>输入nice值
懂得去看优先级
主要是看PRI and NI:
PRI即进程的优先级,通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级别越高,就跟排名一样。
NI则表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
需要注意的是,当我们每次去调整nice值的时候,PRI的值就会重新变为80,也就是说老的优先级,都是80。再去与nice值相加。所以,这样就会让最终优先级是处于[80-20,80+19]的区间内,不会使得意外发生。
当然,我们需要区分nice值和优先级的关系,他们不是一个概念,但是进程nice值会影响到进程的优先级变化,可以理解成nice值是进程优先级的修正修正数据
6.其他概念
进程独立性:什么是进程独立性?举个简单的例子,当我们打开了好几个进程,比如QQ、抖音、微信、微博等等,当其中某个进程卡死了,失去响应,退出去的时候,其它进程是否也会受到干扰?答案是不会。这意味着,进程之间是独立的,每个进程都有自己的PCB,互不干扰。这就是进程的独立性。
看到这里,或许有人立刻反驳,哎哎哎,不是还有父子进程吗?难道它们也是各自独立的吗?
下面举个例子:
我们可以看到,子进程崩溃了,但是父进程还是在运行着,没有受到干扰。所以,是独立的。
并行和并发:
并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
对于并发:
我们在进程在CPU开始运行到结束上都有个理解上的误区,进程在CPU上一定是要执行完才行。
其实不是的,而是采用时间片来操作。每个进程只有一定的时间段内可以占用CPU资源,超过这个时间,这个进程就不能使用CPU资源了,需要重新排队。
这里也许会有人质疑:CPU这样来回切换,是不是会浪费时间?其实不是,我们不能用我们的思维去衡量CPU,CPU的速度是非常非常快的,CPU只需一丁点时间,就能跑完一大堆进程。
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
进程切换
我们先来了解一些概念:
①当我们的进程在运行的时候,一定会产生非常多的临时数据,这份数据是属于当前进程的。
②CPU内部只有一套寄存器硬件,寄存器里面保存的数据,是属于当前进程的!
③寄存器是被所有进程共享的,但是寄存器内部的数据是每个进程私有的,这种数据就叫做上下文数据。
④进程在运行的时候,是需要占用CPU,但不是一直占用到进程结束,因为进程有自己的时间片,因此需要进行进程切换。
所以,对于进程切换:下面的两句话总结了进程切换是怎么做的。
进程在切换的时候,要进行进程的上下文保护,即对寄存器内的数据进行保存保护。保存是交由操作系统来完成,操作系统有这个专门的空间。
进程在恢复运行的时候,要进行进程的上下文恢复,即对寄存器内的数据进行恢复,重新写入寄存器当中。
7.环境变量
概念:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
系统指令和自己创建的指令的小区别:
通过file {指令},可以知道,我们平常使用的指令,如touch、mkdir、pwd等等,都是可执行程序,是系统自带的,而我们自己创建的mytest可执行程序,不是系统自动的,这就有个区别了:我们使用这些程序(指令)的时候,系统自带的,我们不需要加上 ./,而我们自己创建的,就需要加上 ./。
要执行一个程序(指令),就需要找到这个程序在哪。
./mytest -> ./ -> 代表当前路径 -> 即找到我们需要这些的程序
所以,如果我们不想在执行自己创建的程序的时候,要带上 ./,我们可以拷贝到/usr/bin当中去。
但是这样是有风险的,因为我们写的指令或程序,是没有经过测试的,会污染系统的指令池。
使用指令:sudo rm /usr/bin/mytest 来删掉
因此,这就需要用到环境变量了。
环境变量相关的命令:
1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量
1.查看环境变量(PATH),echo:
同时常用的环境变量:
HOME:
LOGNAME:
2.设置新的环境变量,export:
把我们写的可执行程序的路径弄到环境变量当中。
①先使用指令:export PATH=$PATH:/home/......(程序所在的路径)将原本的环境变量也要放进去
②再使用:echo $PATH
此时,就成功添加到了环境变量当中了。
例子二:在bash命令行上创建的局部变量,在环境变量中找不到的时候,可以用export设置到环境变量中。
我们发现,在env中是查不了的,这种变量叫做本地变量。可以类比我们在C/C++的局部变量。
使用代码测试一下myval是否存在环境变量当中:
1#include<stdio.h>2#include<stdlib.h>3#include<string.h>4#defineUSER"USER"5#defineMY_ENV"myval"6intmain() 7 { 23char*myenv=getenv(MY_ENV); 24if(NULL==myenv) 25 { 26printf("%s, not found\n",MY_ENV); 27return1; 28 } 29printf("%s=%s\n",MY_ENV,myenv); 303132return0; 33 }
结果:
这个结果很正常,因为myval是本地变量。
接下来,就是使用export,将myval从本地变量变成环境变量。
使用指令:export myval就可以将其导入环境变量,然后使用env | grep myval来查看这个环境变量,同时我们也能发现,当再次使用./mycmd的时候,也能找到myval了
这里就说明了环境变量可以类似于我们的全局变量,本地变量可以类似于局部变量,只会在当前进程(bash)内有效。那为什么环境变量有全局性呢?在哪都能调用?
环境变量本来就是定义给bash的,因为bash是一个系统进程,mycmd也是一个进程,同时,mycmd是bash的子进程(两个进程同时运行就是因为fork),然而环境变量能够被子进程继承,从而导致了环境变量具有全局性。
继承的原因是为了应对不同的应用场景;
举个例子:为什么我们在使用ls mycmd的时候,会直接找到mycmd的路径?为什么当我cd .. 退出当前路径后,再次使用ls mycmd的时候又找不到呢?
原因很简单:
其实ls也是一个进程,是bash的子进程,它继承了bash的环境变量,而环境变量中有一个变量叫做PWD,PWD就是用来记录当前路径!
3.set显示本地定义的shell变量和环境变量
set相对于把环境变量和本地变量全显示出来了。
4.unset清除环境变量
OS预先准备的环境变量
有一些问题:
操作系统为了让我们找到可执行程序,它定义了一堆的环境变量帮我们找。那么其中:
环境变量PATH只是操作系统需要解决的问题之一,还有其他的问题:
①Linux怎么知道当前登录的用户是谁?
②Linux怎么知道当前的主机名是谁?
③Linux怎么知道当前使用的shell的种类?
④Linux怎么知道当前对应的配色方案,编码方式,记录历史、怎么知道shell在运行期间,动态库的搜索路径......
这些都是不同的领域,有的是为了查找指令,有的确定用户等等,因此不同种类的场景,就要求操作系统在启动Linux命令行解释的时候,就必须得预先设置好可能用到的变量,这批变量,就叫做环境变量。
这里是个概念,我们使用env指令,就可以查看到操作系统预先准备好的环境变量。
所谓的环境变量,就是操作系统为了满足不用的应用场景,而预先在系统内设置好的一大堆的全局变量,这些变量,实际上,在整个系统中,在bash往后,一直都会被我们的进程访问。
通过环境变量USER再次理解权限访问
在环境变量中,有个叫做USER。它可以查看到当前用户是谁。
使用指令或程序获取环境变量
现在,我们来看看,如何通过指令获取环境变量,除了echo $(环境变量名)这个,还有:
使用getenv函数,可以获得指定环境变量。
getenv函数:
头文件:#include<stdlib.h>
char *getenv(const char *name)
name为环境变量名,成功了返回这个环境变量的内容,失败了返回null。
使用下面代码,进行获取:
结果:
当我们是普通用户:wjmhlh的时候,环境变量名USER的内容就是wjmhlh
当我们切换到根目录的时候,环境变量名USER的内容就是root
USER环境变量最大意义就是:可以标识当前的使用Linux的用户,然后可以通过用户来确认权限的大小!
解释意义:
看看下面代码,来解释这个意义:
结果:
当我们使用普通用户的时候:
当我们使用root的时候:
这就能进一步的解释了,我们有时候会对一些目录文件的访问权限不足,打不开或者写不了,会显示"Permission denied"。因为操作系统会根据环境变量USER中的内容,来确认此时此刻的用户是谁。当我们去访问某个文件或目录的时候,需要与这个文件或目录的拥有者、所属组匹配。匹配成功则可以访问,不成功则不能访问!也就是身份认证!所以,身份认证是需要根据USER认证。
命令行参数与获取环境变量
命令行参数跟环境变量有些关联。先来讲讲命令行参数。
我们在写main函数的时候,一般都是这样写的:int main(){};
但我们或多或少地也会遇到这样写的main函数:int main(int argc,char *argv[]){};
这个是什么意思?
①char *argv[]是一个指针数组,存放的是char*。
②int argc代表着这个数组存放元素的个数
当我们写出这样的代码:
intmain(intargc,char*argv[]) { inti=0; for(i=0;i<argc;i++) { printf("iargc[%d]->%s\n",i,argv[i]); } }
并在bash命令行中输入:./mycmd -a -b -c,结果如下:
解释:
char*指针数组,只存两种类型的值,第一种是字符,第二种是字符串。
我们输入的./mycmd -a -b -c会以长字符串的形式输入,然后再以空格为间隔,变成字符,存到了数组中。比如:ls -a -b -c。这是由shell和操作系统完成的。
那这个东西的参数的最大意义是在哪,有什么作用?
其实就是我们在Linux 命令行输入的各种指令,比如ls -a,ls等等的指令,通过main函数来实现对应功能,换句话说,Linux是使用C语言写的,其中的main函数就是带参数的main函数。
当然Windows也有它的命令行模式,也就是cmd,当我们在cmd输入各种指令,就是在调用main函数,然后通过main函数来调用其他功能函数。
总的来说,main函数是操作系统来调用的,我们在平时写代码的时候,main函数不需要我们来写参数就是因为我们用不到,我们在使用main函数来作为入口,调用我们写的其他函数的同时,操作系统也在调用着main函数。
main函数的参数不止两个,还有3个参数的形式
第三种形式,就是用来存放环境变量的指针数组!
int main(int argc,char *argv[],char *env[])
说明:
char *env[]没有个数设置,也就是说argc不是表示env的个数的
我们来写代码来看看这个形式:
intmain(intargc,char*argv[],char*env[]) { inti=0; for(i=0;env[i];i++) { printf("env[%d]:%s\n",i,env[i]); } }
因为env没有个数,同时这个数字也是以NULL结尾的,当运行这个代码的时候,它就会一直打印,直到把环境变量的内容全部打印,遇到NULL就会停下来。
当我们在export一个变量到环境变量的时候,就是将这个变量放到env数组里面去。
获取环境变量的方法:
①在代码中使用getenv函数
②使用char *env[]
③environ
environ:
extern char **environ是一个二级指针,它指向char *env[],当我们在main函数没有使用env参数的时候,操作系统也会生成env,是使用environ指向的。所有,我们在没有使用env参数的们main函数的时候,可以使用environ来获取环境变量。
再次提醒的是,environ是一个二级指针!头文件为:#include<unistd.h>
int main() { extern char **environ; int i = 0; for(i = 0;environ[i];i++) { printf("%d:%s\n",i,environ[i]); } }
这就是获取环境变量的三种方式!
环境变量的组织方式
通过上面命令行参数的讲解,我们就能知道了环境变量的组织方式:
每个程序都会收到一张环境表,环境表是一个字符指针数组,每一个指针指向一个以'\0'结尾的环境字符串
8.程序地址空间
回顾C/C++地址空间
在学习C语言的时候,我们就初步接触了C的地址空间,知道C的地址空间是从低地址到高地址,然后里面分有代码区、堆区、栈区等等。可是我们并不完全了解它!那么接下来,我们是时候去了解这位一直在编程之路上陪伴我们的好伙伴了!
其实地址空间并不是内存,地址空间的的本质是内核的数据结构。
对于地址空间,我们必须知道:
1. 地址空间描述的是基本空间大小的单位是字节
2. 在32位下,一共有2^32个地址
3. 2^32位*1字节 = 4GB空间范围
4. 每一个字节都有自己的唯一地址
所以,在地址空间上,每一个字节的地址的编号,从000...000(32个)到FFFF FFFF划分。如下图:
区域划分和区域调整的理解:
相信大部分人在读小学的都有跟同桌划38线的经历吧,其实区域划分跟调整可以拿划38线来理解。比如桌子长100cm(某一块地址空间的大小),男生要占一半,即(1,50),女生也占一半(51,100)。这就是区域划分,同理,地址空间划分区域也是这样划分的,通过数据结构的结构体来划分:
structDestop{ //给男生划区域unsignedintmale_start; unsignedintmale_end; //给女生划区域unsignedintfemale_start; unsignedintfemale_end; };
struct Destop d = {1,50,51,100};
当然,如果某个人小动作特别多,总是不小心越界了,于是两个人就商量了一下,各自的空间缩小一点,空出一个缓冲区出来,如果越界了,但是是越在了缓冲区,就不算真正越界。这就是区域的调整。struct Destop d = {1,45,55,100};
当然,区域的调整也有这样的,就是男生动作特别多,女生生气了,直接将男生的空间缩小到1到30,自己的空间则是31到100.struct Destop d = {1,30,31,100};这也是区域的调整。
通过这个例子,我们就理解了区域的划分和调整。所以,在32位下,地址空间大小为2^32个地址,每个地址都被划分出来了。
structmm_struct{ uint32_tcode_start, code_end; uint32_tdata_start, data_end; uint32_theap_start, heap_end; uint32_tstack_start, stack_end; ...... };
当一个进程被创建后,它就会有自己的地址空间,它这个地址空间如下图:
每个区域里面有许许多多个地址编号,这些就称为虚拟地址。所以,在地址空间中,它的2^32个地址,都属于虚拟地址,这些区域的空间,就是虚拟空间,给代码、数据用的。
堆区和栈区是会变化的,比如我们在创建局部变量,在malloc或new,或free一块空间的时候,就是对栈区或堆区进行调整。调整的的本质就是对各个区域的start或end进行修改。
所以,我们修正一下我们的错误,那就是地址空间不是C/C++地址空间,而是叫做进程地址空间。
进程地址空间与物理地址空间之间的联系
我们写了一个代码程序后,代码会在磁盘中保存,当代码需要被拿出来执行的时候,是如何从磁盘到内存,再到进程空间的呢?其中的新角色就出现了,它就是页表。
这里只是浅谈一下页表。看下图,下图说明了进程地址空间 ——页表——物理空间的关系
页表会将虚拟地址保存在左边,将物理地址保存在右边。当我们通过虚拟地址去修改代码程序的数据时,就会通过页表找到对应的物理地址,然后在物理地址的空间中将数据修改。
当然,这些操作都是由操作系统帮我们做好的。
总结一下:
操作系统给每一个进程都“画了个大饼”,那就是“承诺”分配给它们的空间大小有4GB那么多(32位),但实际上从内存中映射出来的内存大小,并没有那么多。所以进程地址空间所谓的4GB大小,就是虚拟地址。
说了那么多,为什么要存在进程地址空间?
1.如果让进程直接访问物理内存,不安全。因为当这个进程是一个恶意进程,那么我们存在物理内存中的信息全暴露或者没了。所以,通过进程地址空间,再通过页表,可以阻止那些恶意的、非法的进程去访问物理内存。
2.可以方便地进行进程和进程的代码数据的解耦,保证进程的独立性。当父进程执行后,创建了一个子进程,此时父子进程在物理内存中地址是指向同一块的。但当子进程需要改变这一块空间的值,操作系统就会额外开辟一块空间给子进程,意味着父子进程所指向的空间不再一样,这就确保了进程的独立性。看下图:
操作系统为了保证进程的独立性,就通过地址空间,页表,让不同的进程映射到不同的物理内存。当操作系统为子进程额外开辟一快空间的同时,是先将原来指向的空间的值拷贝到新的空间,再修改值,这种拷贝叫做写时拷贝:任何一方尝试写入,OS先进行数据拷贝,更改页表映射,再让进程进行修改。
3.让进程以统一的视角,来看待进程的代码数据各个区域,方便编译器编译代码。意思是说,进程地址空间的存在,让编译器在编译代码的时候,以同样的规则去划分区域,等代码编译完后,进入内存空间即可直接使用。这意味着,编译器在编译代码的时候,就已经将代码数据的各个区域划分出来了(栈堆区除外,因为栈堆区是在内存中动态开辟的),此时的地址称为逻辑地址,在Linux中,逻辑地址跟虚拟地址可以看成是一样的。看下图:
最后总结一下:
本文较详细地讲解了进程的概念。
先从什么是进程,到如何去查看我们创建出来的进程,然后是学习了进程的调用和进程的状态,接着是进程的优先级、环境变量,最后是进程地址空间。
讲解就到这里了!