冯诺依曼体系结构、操作系统、Linux进程概念、环境变量、进程地址空间

今天,我带来冯诺依曼体系结构、操作系统、Linux进程概念、环境变量、进程地址空间。





冯诺依曼体系结构

在这里插入图片描述

  1. 输入单元:包括键盘, 鼠标,扫描仪, 写板等
  2. 中央处理器(CPU):含有运算器和控制器等
  3. 输出单元:显示器,打印机等

关于冯诺依曼体系结构,需要注意以下几点

  1. 这里的存储器指的是内存
  2. 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
  3. 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
  4. 一句话,所有设备都只能直接和内存打交道。

输入、输出设备称之为外围设备了,外设一般会比较慢,以磁盘为例,相对于内存,磁盘是比较慢的。

因为有了内存的存在,我们可以对数据进行预加载,CPU以后在进行数据计算的时候,根本不需要访问外设了,两只手直接向内存要就可以了。

理解:可执行程序是不是一个文件?(磁盘里)
是,那么为什么我们的程序,必须先加载到内存里?冯诺依曼体系结构决定的。

结论1:在数据层面,一般CPU不和外设直接沟通,而是直接和内存打交道。
结论2:外设只会和内存打交道——数据层面。



操作系统(Operator System)

概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

  1. 内核(进程管理,内存管理,文件管理,驱动管理)
  2. 其他程序(例如函数库,shell程序等等)

设计OS的目的

  1. 与硬件交互,管理所有的软硬件资源
  2. 为用户程序(应用程序)提供一个良好的执行环境

定位

在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。

总结

计算机管理硬件

  1. 描述起来,用struct结构体
  2. 组织起来,用链表或其他高效的数据结构

系统调用和库函数概念

  1. 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
  2. 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

操作系统:一款进行软硬件资源管理的软件。

  1. 管理:管理者和被管理者,其实是不需要直接沟通的。
  2. 管理和被管理者没有自己沟通,是怎么做到管理的?
    管理的本质:对被管理对象的数据做管理。
  3. 管理者是如何拿到被管理者的数据呢?
    由决策被执行人去收集。如:校长是管理者,收集被管理者(学生)的信息由辅导员来做。

管理的本质:先描述,再组织。
操作系统为什么要对软硬件资源进行管理呢?操作系统对下通过管理好硬件资源(手段),对上给用户提供良好(安全、稳定、高效、功能丰富等)执行环境。

操作系统给我们提供非常良好的服务,并不代表着操作系统相信我们,反之,操作系统不相信任何人,所以,我们没有办法随心所欲的去访问操作系统内部,只能通过操作系统提供的系统调用接口去访问。



进程

基本概念

课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。

描述进程-PCB

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

task_struct-PCB的一种

在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

task_ struct内容分类

  1. 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  2. 状态: 任务状态,退出代码,退出信号等。
  3. 优先级: 相对于其他进程的优先级。
  4. 程序计数器: 程序中即将被执行的下一条指令的地址。
  5. 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  6. 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  7. I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  8. 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  9. 其他信息

组织进程

可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。

查看进程

1.进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
在这里插入图片描述

proc属于内存级别的文件系统,只有操作系统启动后才存在,在磁盘上并不存在proc目录。

假设某个进程的PID是13045

ls /proc/13045

该指令可以查看该进程。

2.大多数进程信息同样可以使用top和ps这些用户级工具来获取
首先,写一个简单程序,运行起来,让它变成进程。
在这里插入图片描述

ps axj //查看系统中所有的进程
ps axj | head -1 //查看第一行,即属性名
ps axj | grep //再加上可执行程序的名字,表示只查看该进程

while :; do ps axj | head -1 && ps axj | grep myfile | grep -v grep; sleep 1;done
所以,我们可以用该条指令,循环的间隔一秒去查看可执行程序myfile的进程。

在这里插入图片描述
接下来,我在另一个窗口运行该程序。
在这里插入图片描述
我们就可以完成,循环的查看进程了。

while :; do ps axj | head -1 && ps axj | grep myfile | grep -v grep; sleep 1;echo “############################################################”;done

我们也可以加上分隔符,这样就容易辨别了。
在这里插入图片描述
启动并运行程序的行为——由操作系统帮助我们将程序转换为进程——完成特定的任务。

进程:加载到内存的代码和数据 和 该进程在内核中创建的pcb/task_struct数据结构合并称为进程。
进程 = 内核关于进程的相关数据结构 + 当前进程的代码和数据

在这里插入图片描述

通过系统调用获取进程标示符

进程id(PID)
父进程id(PPID)

使用该指令,查看getpid、getppid函数

man getpid

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

当我们不断ctrl+c结束该进程,再重新运行可以发现。PID一直在变化,PPID一直没变。
在这里插入图片描述
为什么呢?

bash命令行解释器,本质也是一个进程。
命令行启动的所有程序,最终都会变成进程,而该进程对应的父进程都是bash。
所以PPID没有变化,而进程由于被终止,那么原来PID就被其他进程拿去使用,重新运行时,会重新分配PID。

./ 运行程序,使之变成进程(修改说法)。

通过系统调用创建进程-fork初识

man fork

查看fork系统调用。

在这里插入图片描述

fork函数如果成功创建子进程,子进程的PID返回给父进程,0返回给子进程;如果创建子进程失败,-1返回给父进程,没有子进程被创建。

一个函数调用居然有两个返回值?其实,从调用fork函数开始的那一行,代码已经被拷贝两份了,具体更深刻的理解,还得等到进程地址空间那里。以下是证明:
在这里插入图片描述
在这里插入图片描述
如上,第二个打印函数,执行了两次,一个是由进程29364执行,一个是由进程29365执行。由此证明,从fork的调用开始,代码被拷贝了两份,分别由父子进程去执行。

fork之后,执行流会变成2个执行流。
fork之后,谁先运行由调度器决定。
fork之后,通常代码共享,一般使用if和else if进行执行分流。
在这里插入图片描述
在这里插入图片描述
由父进程的PID是子进程的PPID可知,创建子进程成功。

原理:
fork做了什么?
在这里插入图片描述
fork如何看待代码和数据?
进程在运行的时候,是具有独立性的。
父子进程,运行的时候,一样具有独立性。

父子进程共享代码和数据,是如何做到独立性的呢?

  1. 代码:代码是只读的。
  2. 数据:当有一个执行流尝试修改数据的时候,OS会自动给我们的进程促发写实拷贝。

写实拷贝验证如下:当父进程修改x时,将会为x重新开辟一个空间,存入修改后的值,但是,为什么x的地址没有发生改变呢?其实,我们打印出来的地址是虚拟的地址,真实的地址是有改变的,一样在进程地址空间会提到。
在这里插入图片描述

在这里插入图片描述

fork如何理解两个返回值的问题?
当函数内部准备执行的return的时候,主体功能已经完成。
在这里插入图片描述

进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

从调度的角度理解进程状态

在这里插入图片描述

阻塞

阻塞:进程因为等待某种条件就绪,而导致的一种不推进的状态——进程卡住了——阻塞一定是在等待某种资源——为什么阻塞?进程要通过等待的方式,等具体资源被别人使用之后,再被自己使用。

阻塞:进程等待某种资源就绪的过程。

大量的进程,操作系统会对这些进程先描述为task_struct,再组织起来进行管理。对于键盘、网卡、显卡等各自外设也是先描述、再组织,只不过这里是描述为struct dev的设备结构体,然后再将这些结构体用一些数据结构进行管理起来。

阻塞的例子:
假如CPU正在调用程序下载一个文件。
突然,网断了,此时下载任务便不能再继续了,CPU就不会让该任务浪费CPU资源,等待网络资源恢复正常后,再进行下载,便发生了阻塞。

在这里插入图片描述

task_struct链接到所缺资源的struct dev里的队列,当网络恢复以后,该下载任务会被CPU重新调度。

struct dev
{
	struct tast_struct* queue;//链接在这里
	//dev的其他属性
};

写一个程序,其中包含cin,那么在运行的时候,程序就等待键盘输入,此时也是发生了阻塞。该进程的task_struct将被链接到磁盘struct dev的队列里面。

PCB是可以被维护在不同的队列里的。

阻塞:阻塞就是不被调度—— 一定是当前进程需要等待某种资源就绪 —— 一定是进程task_struct结构体需要在某种被OS管理的资源下排队。

挂起

当内存资源比较紧张的时候,操作系统就可能将正在阻塞的进程的代码和数据重新放到磁盘中,此时就可以称为挂起状态(严格称为阻塞挂起状态)。

当资源准备就绪的时候,可以被CPU调度时,操作系统会将代码和数据重新加载到内存中。

挂起在上面示意图中,没有画出。

PCB当中细分的描述进程的状态

task_struct是一个结构体,内部会包含各种属性,其中就有状态。

struct task_struct
{
	int status;//如:0表示R状态
	//其他属性
};

下面的状态在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 */
}; 
  1. R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  2. S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
  3. D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  4. T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  5. t追踪停止状态(tracing stop):“跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作,比如在gdb中对被跟踪的进程添加一个断点。
  6. X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
  7. Z僵尸状态(zombie):在一个进程执行完毕时,需要有另一个进程来回收该进程执行完毕后的属性数据然后,如果这个回收数据的进程没有来得及回收进程的数据,这个执行完毕的进程就处于僵尸状态。
R状态和S状态

进程是R状态,就一定是在CPU上运行?
答案是否定的。
在这里插入图片描述
CPU调度进程的时候,即在队列当中挑选去运行。

进程,是什么状态,一般也看这个进程在哪里排队。进程在CPU运行和在运行队列排队,都是R状态,剩下的在其他设备排队都是阻塞状态。

R状态并不直接代表进程在运行,而代表该进程在运行队列中排队。运行队列由操作系统维护,操作系统在内存里,运行队列也在内存里。操作系统管理task_struct,就是将它放在不同的队列里。

我运行下面的代码,并且通过指令查看进程。
在这里插入图片描述

在这里插入图片描述
我们可以查看到,虽然程序一直在运行,但是查看进程的时候,却发现是S+状态,阻塞的一种。

为什么程序在死循环打印,不是R状态,而是S状态呢?
原因:该程序是在死循环打印,那么就要频繁的访问显示器的外设,那么出现等待显示器回应时,便出现了阻塞的状态,但是程序打印出了信息,证明该程序在运行呀?因为代码只有几行,一瞬间就运行完了,所以该程序大部分时间都在等待显示器的回应,由此查询的过程中,有可能出现的都是阻塞状态,多查询,可能出现R状态。

如果我们将打印函数注释掉呢?
在这里插入图片描述
这里我们把休眠函数和打印函数也注释掉,让程序一味的死循环。
在这里插入图片描述
此时,我们可以发现,一直是R状态。

因为代码中没有访问如何资源,只有while判断,就是纯计算,所以在进程调度的生命周期,只会用CPU资源,所以一定是R状态。

S休眠状态,可中断休眠。休眠的本质就是一种阻塞状态。

证明如下:
在这里插入图片描述
写一个程序,让程序等待键盘输入,运行起来,此时,查询进程状态。
在这里插入图片描述
我们可以发现是S状态,所以S状态本质就是一种阻塞状态。

D状态

D状态是一种休眠状态,不可中断休眠。

比如:CPU在调度一个进程,往磁盘输入100MB的数据,由于存储数据,需要一段时间,那么该进程进入阻塞状态,假如为S状态。
在这里插入图片描述
在这里插入图片描述
如果此时内存资源处于紧张的状态,操作系统便会想方设法的清理内存,观察到处于S状态的该进程,便将进程杀死得以释放资源。

此时,如果磁盘存储发生错误,返回信息给该进程,反而查询不到该进程,此时就乱套了,还丢失了数据。

为了防止该情况的出现,便出现了D休眠状态,该状态不可被中断,连操作系统也不行,有时连关机也不行,只能拔电源,但是电源一拔,责任便是自己的了。

T状态

T状态,为暂停状态。
在这里插入图片描述
在这里插入图片描述

给该进程发生19号信号,可以让该进程从S+状态,转换为T状态,即暂停。

在这里插入图片描述
继续向该进程发生18号信号,可以发现当前进程状态为S状态。此时,无法使用ctrl+c终止。
在这里插入图片描述
只能通过发生9号信号杀手该进程。
在这里插入图片描述
S+状态,证明在显卡执行,ctrl+c可以终止,发生9号信号可以终止。
S状态,后台运行,此时ctrl+c不可以终止,发生9号信号可以终止。

t状态

t状态表示,暂停状态。
在这里插入图片描述
在这里插入图片描述

调试,打断点,此时发现进程处于S+状态。我们给gdb发生r指令。
在这里插入图片描述
可以发现,处于t状态。

X状态

X状态,死亡状态,瞬时状态,查询不到。

Z状态

Z状态,僵尸状态。

我们为什么要创建进程?因为我们要让进程帮我们办事——1.我们关心结果 2.我们不关心结果。

在运行程序后,输入指令echo $? 可查询退出码。

如果一个程序退出了,立马X状态,立马退出,作为父进程,没有机会拿到退出结果。Linux设计当进程退出的时候,一般进程不会立马彻底退出,而是维持一个状态叫做Z状态,也叫做僵尸状态——方便后续父进程/OS读取该子进程退出时的退出结果。

写一个程序,创建子进程,让子进程先退出,观察它们的状态。在这里插入图片描述
在这里插入图片描述

观察可以得知,子进程退出的时候,为Z+状态,即僵尸状态,等待子进程的回收。

Z(zombie)-僵尸进程

僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

僵尸进程危害

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。

那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存。

孤儿进程
  1. 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  2. 父进程先退出,子进程就称之为“孤儿进程”
  3. 孤儿进程被1号init进程领养,当然要由init进程回收。

在这里插入图片描述
在这里插入图片描述
父进程退出,子进程会被OS自动领养(通过让1号进程成为新的父进程。由子进程的PPID变成1可以证明)——被领养的进程被称之为孤儿进程。

如果不领养会发生什么?
子进程在后续退出的时候,无人回收了。

观察上面的图片,可以发现父进程退出的时候,不是Z状态,而是X状态(死亡状态)呢?
原因:bash进程是已经回收了运行结果,所以回收了父进程。(bash进程是该父进程的父进程)。

进程优先级
基本概念
  1. cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  2. 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
  3. 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
优先级 vs 权限

权限代表的是能与不能的问题。

优先级代表的是已经能,但是谁先,谁后的问题。

为什么会有优先级?CPU资源有限。

查看系统进程

在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
在这里插入图片描述

我们很容易注意到其中的几个重要信息,有下:

  1. UID : 代表执行者的身份
  2. PID : 代表这个进程的代号
  3. PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  4. PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  5. NI :代表这个进程的nice值
PRI and NI
  1. PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  2. 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  3. PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
  4. 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  5. 所以,调整进程优先级,在Linux下,就是调整进程nice值
  6. nice其取值范围是-20至19,一共40个级别。
PRI vs NI
  1. 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
  2. 可以理解nice值是进程优先级的修正数据
查看进程优先级的命令

用top命令更改已存在进程的nice:

  1. top
  2. 进入top后按“r”–>输入进程PID–>输入nice值
其他概念
  1. 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
  2. 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
  3. 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。
  4. 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。


环境变量

基本概念

  1. 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
  2. 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  3. 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

常见环境变量

  1. PATH : 指定命令的搜索路径
  2. HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  3. SHELL : 当前Shell,它的值通常是/bin/bash。

查看环境变量方法

echo $NAME //NAME:你的环境变量名称

如:echo $USER //查看当前用户
在这里插入图片描述
文件的拥有者、所属组的概念,对相应的用户进行权限限制,而如何得知现在是哪个用户呢?那么就是基于USER的环境变量来判定的。

如:我们可以利用USER的环境变量来确认当前用户是否有权限来执行我的代码。

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<string.h>
#include<stdlib.h>
using namespace std;

#define User "zrb"

int main()
{
	char* user = getenv("USER");
	if (strcmp(user, User) == 0)
	{
		cout << "有权限执行当前代码" << endl;
		//......
	}
	else
	{
		cout << "无权限执行代码,直接退出" << endl;
	}
	return 0;
}

在这里插入图片描述

echo $PATH //查看PATH
在这里插入图片描述
我们的命令本质就是一个可执行程序,但是为什么命令不需要带路径,自己执行,如ps -l,而我们写的可执行程序却要./myfile,./声明是当前路径呢?原因在于,PATH的环境变量,我们输入一个命令时,操作系统会自动根据PATH环境变量去找该命令的路径,如何,运行起来。

那么,我们也可以将我们当前目录加到PATH的环境变量里,下次运行该目录下的程序,也不用声明路径了。
在这里插入图片描述
在这里插入图片描述

在Linux中,把可执行程序,拷贝到系统默认的路径下,让我们可以直接访问的方式——相当于Linux下软件的安装。

和环境变量相关的命令

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量,设置的环境变量会在关闭终端后即刻消失
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的shell变量和环境变量

环境变量的组织方式

在这里插入图片描述
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串

main函数有三个参数,main(int argc , char* argv[ ] , char* envp[ ]),这三个参数平时由编译器传参,其中envp是一个表结构——环境变量表。

环境变量本质就是内存级别的一张表,这张由用户在登录系统的时候,进行给特定用户形成属于形成属于自己的环境变量表。环境变量中的每一个,都有自己的用途:有的是进行路径查找、有的是进行身份验证、有的是进行动态库查找、有的用来确认当前路径。每一个环境变量都有自己的特定的应用场景,每一个元素都是kv。那么环境变量对应的数据,都是从哪里来的呢?系统的相关配置文件中读取进来的。

通过代码如何获取环境变量

  1. 命令行的第三个参数
    在这里插入图片描述
    在这里插入图片描述

  2. 通过第三方environ获取
    在这里插入图片描述
    在这里插入图片描述
    libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。

通过系统调用获取或设置环境变量

在这里插入图片描述
在这里插入图片描述
常用getenv和putenv函数来访问特定的环境变量。

环境变量通常是具有全局属性的

环境变量通常具有全局属性,可以被子进程继承下去。

在这里插入图片描述

验证如下:
首先,我们给shell进程加上一个环境变量,再写一个程序,让它运行起来,便是shell进程的子进程,在该程序中获取我们写的环境变量,观察能不能获取成功。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
证明子进程拿到了父进程的环境变量。

本地变量

本地变量,只在shell内部有效。

在这里插入图片描述
如:上面我所设置的count就是一个本地变量。

如果我们要让该本地变量加到环境变量表,让子进程可以接收的话。就需要用到export指令了。

main函数的三个参数

main(int argc , char* argv[ ] , char* envp[ ]),envp在前面已经提到过,是一个表结构——环境变量表。

那么其他两个参数呢?

argv是一个指针数组,argc是argv数组的元素个数。

那么第二个参数是指向什么内容的指针数组呢。

我们一般运行自己的可执行程序,都是用./myfile的
如果加参数呢,./myfile -a ,此时argv指针数组存的就是指向-a内容的指针,argc存的就是argv的元素个数,即1。
如果加两个参数呢,如./myfile -a -l 那么argv指针数组存的就是分别指向-a和-l的指针,argc存的就是argv的元素个数,即2。

那么Linux程序指令,如何设置的就知道了吧。
如ls指令,ls -a -l

那么,如何验证上面的说法呢?我们可以通过一个程序。

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<string.h>
#include<stdlib.h>
using namespace std;

int main(int argc, char* argv[])
{
	for (int i = 0; i < argc; ++i)
	{
		printf("argv[%d]->%s\n",i,argv[i]);
	}
	return 0;
}

在这里插入图片描述
bash认为指令是一个字符串,制作一个表(argv),存储着指令(以空格为分隔符),给子进程使用。

接下来,我写一个使用命令行参数的例子。

#include<stdio.h>
#include<string.h>
#include<iostream>
using namespace std;

void Usage(const char* name)
{
	cout << "使用手册" << endl;
	printf("\tUsage:%s -[a][b][c]\n",name);
	exit(0);
}

int main(int argc,char* argv[])
{
	if (argc != 2)
	{
		Usage(argv[0]);
	}
		
	if (strcmp(argv[1], "-a") == 0)
		cout << "打印文件信息" << endl;
	else if (strcmp(argv[1], "-b") == 0)
		cout << "打印文件的详细信息" << endl;
	else if (strcmp(argv[1], "-c") == 0)
		cout << "打印隐藏文件" << endl;
	else
		cout << "其他功能,待开发" << endl;
	
	return 0;
}

在这里插入图片描述

Linux底部设计都是C语言开发的,包括命令。C语言实现的命令,其中的选项就以字符串的形式通过传参给程序,对应的程序对选项做判断,让同样的一个软件带不同的选项就能表现出不同的现象和执行结果。



进程地址空间

进程地址空间的概念

在C语言的学习过程中,我们肯定画过这样的图。
在这里插入图片描述
通过代码感受一下:
在这里插入图片描述

在这里插入图片描述

我们发现,输出的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量没有进行任何修改。可是将代码稍加改动:

在这里插入图片描述

在这里插入图片描述

我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:

  1. 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  2. 但地址值是一样的,说明,该地址绝对不是物理地址!
  3. 在Linux地址下,这种地址叫做 虚拟地址或者线性地址
  4. 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
  5. OS必须负责将 虚拟地址 转化成 物理地址 。

进程地址空间,本质就是一个内核数据结构struct mm_struct[ ]。
地址空间是线性结构。

struct mm_strcut//4GB
{
	long code_start;
	long code_end;
	long init_start;
	long init_end;
	//...
	long brk_start;
	long bra_end;
	long stack_start;
	long stack_end;
};

该结构体分别去指向物理空间,限定区域,如【1000,2000】,那么该区域的数据是虚拟地址或者线性地址。

如何从虚拟地址到物理地址转换。
在这里插入图片描述
页表的两边分别存放着虚拟地址和物理地址,借助MMU进行转换,而这项工作是由操作系统来执行的。页表就先这样浅浅的理解吧,真正的页表还有4kb这个数字,会让你惊叹设计者如此聪明。

解释上面的现象:

父子进程的数据不同,但地址为何相同?
在这里插入图片描述
在最开始,g_val没有被修改,指向同一个物理地址。
在这里插入图片描述
当子进程进行修改g_val时,发生写实拷贝,程序开辟一个地址,存着子进程的g_val修改之后的值,此时,子进程页表指向物理地址将被修改。

父子进程返回的都是虚拟地址,所以在父子进程中,g_val的地址相同,但在两个页表中,相同的虚拟地址指向不同的物理地址,它们的数据不相同,着就导致了地址相同的变量,却有不同数值的现象。

另外,在文章的上面,我已经提到过程序会被从磁盘先预加载到内存里,虚拟内存,是在物理内存上存储的,是一种内核数据结构,先有虚拟内存,才进行预加载。

扩展1 为什么要有进程地址空间

如果没有地址空间,我们的OS是如何工作的?

如果没有地址空间,我们所写的进程都是直接往物理内存进程存储的,如果代码执行出现问题,发生了越界访问和越界写入,那么将对其他进程造成较大的影响。

设计进程地址空间,每一个进程看到的都是相同的地址空间,有堆区、有栈区、常量区、代码区,以统一的视角去看待自己的代码和数据。

拥有地址空间的好处:

  1. 防止物理内存被随意的访问,保护物理地址和其他进程。
  2. 将进程管理和内存管理进程解耦合。
  3. 可以让进程以统一的视角,看待自己的代码和数据。

在这里插入图片描述

扩展2 malloc的本质

向OS申请内存时,OS只要给你在虚拟地址空间上申请,再填充页表的虚拟地址的那一边,当你要真正访问该地址的时候,MMU硬件发现,页表填充的虚拟地址存在,即已经申请过,而页表没有填充物理地址,此时,MMU硬件就会触发缺页中断,即向CPU针脚发送信息,操作系统执行中断处理方法,即陷入操作系统内存(执行操作系统的代码),申请物理内存和填充页表,再返回来执行你的代码。

原因:

  1. OS一般不允许任何的浪费和不高效。
  2. 申请内存不一定马上使用。
  3. 在你申请成功之后和你使用之前,就有一段小小的时间窗口——这个空间没有被正常使用,但别人也使用不了(闲置状态)。

扩展3 重新理解地址空间

我们的程序在被编译的时候,没有加载到内存,我们的程序内部有没有地址?有。

不要以为虚拟地址只要的策略只会影响OS,还要让我们的编辑器遵守这样的规则。

源代码被编译的时候,就是按照虚拟地址空间的方式对代码和数据编好对应的编制?(ELF文件格式)

在这里插入图片描述
编译好以后,放在磁盘里,每一份代码和数据都有自己的地址,如变量就是在栈区范围的地址,代码就是在代码区范围内的地址。

在这里插入图片描述
填充在页表虚拟地址处,就是在磁盘存的时候的地址。

在这里插入图片描述
此时,映射到物理内存里,便有自己的真实物理地址。
在这里插入图片描述
填充页表的物理地址。

这就是可执行程序加载的全过程。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值