目录
一、冯诺依曼体系结构

目前为止,我们所认识到的所有计算机都是由硬件组件组成;输入设备比如鼠标、键盘;输出设备比如显示器;存储器可以理解为内存;中央处理器(CPU):运算器和控制器构成。
CPU在数据层面只和内存打交道,外设(输入设备和输出设备)也是只和内存打交道;
存储器不能忽略,这是因为CPU的处理速度远远快于外设,没有存储器的处理会大大降低效率,并且计算机的价格会更高;
理解数据流动:假设两个人从qq给对方发送消息,从冯诺依曼体系结构看,他们是如何接收到对方的消息的?首先qq是软件,而软件在运行之前必须先加载到内存,之后从输入设备键盘读取消息到内存再经过CPU的处理之后给内存给输出设备网卡,网卡通过网络将数据给另一个用户的输入设备网卡,在经过内存和CPU的处理之后给输出设备显示器对方就能看到消息了;那么如果发送的是文件,输入设备就是磁盘了,对方的输出设备也就成了磁盘。
二、操作系统(Operator System)
概念:
操作系统包含一个基本的程序集合,是对硬软件进行管理的一款软件

操作系统包括:
内核:进程管理、文件管理、内存管理、驱动管理;
其他程序:例如库函数、shell程序等。
理解操作系统:
核心功能:在整个计算机软硬件架构中,操作系统是一款纯正的搞管理的软件;
如何理解“管理”:就像校长、辅导员、学生之间的管理关系一样:管理者是校长,被管理者是学生,管理者管理被管理者不需要和被管理者见面,只需要对被管理者的相关数据进行管理即可,而这些相关数据就是由辅导员帮助获得(即中间层面帮助获得);在这个过程中,会先对学生的数据进行描述,之后再用表格进行组织,这样就能很方便的进行管理了;
总结:学生相当于硬件,计算机对硬件进行管理,先用struct等对硬件数据进行描述起来,之后再用链表或者更加高效的容器进行组织。(即先描述再组织)
设计OS的目的:
设计OS的目的是通过与硬件交互对硬软件进行管理从而向用户程序提供良好的执行环境;前者是手段和方法,后者是目的。

- 软硬件体系结构是层状结构
- 想要访问操作系统,必须要进行系统调用接口-实际上就是系统提供的函数
- 只要是访问了硬件,那么一定贯穿了整个软硬件体系结构
- 库可能在底层封装了系统调用
系统调用和库函数
操作系统要向上提供对应的服务,但是它不相信任何用户或者人,此时就需要系统调用接口来帮助用户使用操作系统,系统调用接口是操作系统的一部分,操作系统通过系统调用接口向上系统服务;用户输入的数据会经过系统调用接口进入操作系统,在底层完成一系列操作之后再由系统调用接口返回给上层用户;
库函数是库函数是为了方便程序员进行软件开发而提供的一组函数集合,许多库函数最终都需要调用操作系统的功能来实现其功能。是对系统调用接口的封装;
三、进程
1、基本概念与基本操作
进程的定义:
当我们要运行某个程序或者代码时,这个程序或者代码会从磁盘载入到内存,而操作系统会在开机时就载入内存进行运行,操作系统对载入的程序或者代码进行管理,通过先描述再组织的方式进行管理形成的PCB(process control block即进程控制块)(PCB也可以叫做内核数据结构对象)和载入的程序或者代码就是进程。
这些一个一个PCB数据结构是操作系统管理中描述过程的结果,PCB中包含一个进程的相关属性(如标识符、状态、优先级、程序计数器、内存指针等等);
在Linux中的PCB是task_struct,也就是用这个结构体描述了进程的属性,是进程属性的集合,可以通过task_struct直接或者间接找到对应进程的所有属性;组织时通过双向链表数据结构将这些结构体进行连接达到管理的目的(对进程的管理就变成了对链表的增删查改)。
我们所执行的所用指令、使用的工具、自己的程序,运行起来全部都是进程.
查看进程:

在一个终端运行这个代码,那么这个代码就会进入内存新建一个进程,再打开另一个终端,对正在运行的这个进程进行查看;
查看的方式有两种:ps axj | head -1 && ps axj | grep code.exe /进程ID(&&表示and的意思)
ls /proc/进程ID -l
getpid查看进程的ID(get process identification);

右图显示grep是因为grep在进行查找时自身也有code.exe这个关键字,若要屏蔽加上| grep -v grep

这个查找引入两个知识点:
cwd -> /home/user1/Linuxcode/linux1/fork
exe -> /home/user1/Linuxcode/linux1/fork/code.exe
exe对应的意思就是这个进程对应的可执行文件,若此时就算我们在另一个终端删除这个文件,在进程运行的终端这个进程不会停止,因为你此时删除的只是磁盘上的文件,而真正运行的是在内存中,不过删除之后就找不到了,要重新运行一次进程对应的可执行文件;
cwd意思就是当前工作目录(current work dir)为什么新建文件什么的默认会在当前目录下新建呢?这是因为进程会记录自己的当前路径
实验:在code.c中输入fopen("hello.txt","a");那么运行这个文件,新建的文件一定是在当前工作目录的:
果然如此。
实际上可以改变cwd,使用chdir("指定路径"),那么之后查看进程cwd和exe就会发生变化:
实验:


至此默认在当前路径生成文件的原理就是进程会记录当前路径,那么生成时就把当前路径和可执行文件进行拼接。
父进程ID是不变的,父进程对应的实际上就是-bash(命令行解释器,也就是说命令行解释器本质上就是一个进程),我们每新开一个进程窗口就会有一个新的-bash,即父进程ID不一样:


杀死进程:
在进程所在的终端直接CTRL+c,或者在另一个终端kill -9 进程id;杀死进程之后就看不到这个进程了;![]()
创建进程:



出现三行打印的原因是创建了子进程之后往下会出现两个执行流,一个是新创建的进程即子进程另外一个是原本的执行流;
子进程默认没有自己的程序和代码,默认会拷贝父进程的程序和代码,当然看到有小部分是和父进程不一样的比如ID等等;但是当程序有新加载之后子进程才会不一样;
创建成功一个进程,会返回两个值,给父进程返回子进程的pid,自己返回0;创建失败返回-1;
实验:


问题一?为什么给父子返回不同的值?对于子进程返回0表示的是创建成功,对于父进程返回一个大于0的数,并且若是有多个子进程每个子进程创建之后返回给父进程的值是不一样的,目的就是为了标识每个子进程的不同;
问题二?为什么一个函数会返回两个值?首先返回的本质就是写入变量,那么就涉及到程序的新加载,此时会给子进程申请新的pcb,拷贝父进程大部分pcb给子进程,子pcb放入进程list甚至调度队列中去了;也就是说子进程被创建甚至被调度了;
问题三?为什么一个变量既大于又等于0,导致else if和else 同时成立?这里涉及到写实拷贝,父子任何一方出现修改,OS会对要被修改的程序进行拷贝,之后让出现修改的进程修改这份拷贝的数据;(这也反映了进程具有独立性,一旦存在修改就申请新的pcb不对之前的pcb造成影响)
实验:
用一个变化的gval值

可以看出父进程对应的gval一直不变,说明申请的pcb给了修改变量的子进程,那么子进程用就会显示变化的gval,而父进程一直使用原本的pcb,也就是说gval不会变化。
2、进程状态
进程状态就是task_struct里面的一个整数,这个整数起的作用就是标识的作用,标识此PCB进程对应的状态。
运行、阻塞、挂起
###运行:
一个CPU中有且只有一个调度队列,这个队列中的数据类型也是task_struct类型的,同样的这些PCB也可以通过内存指针找到管理的数据、代码;放在调度队列中的每个PCB就是要即将被CPU运行的,所以放在调度队列中的进程状态都是running运行;
###阻塞:
所谓阻塞就是等待某个设备或者资源就绪的状态,比如scanf在读取数据时操作系统等待就是我们从键盘设备上的输入,没有输入前输入阻塞状态;
在Linux中,各种硬件设备也是被OS所管理的,管理的方式也是先描述再组织,即通过task_struct进行描述再通过队列进行组织;
具体地,假设运行队列中的某个PCB块对应的是键盘,此时要进行输入之后才能运行,那么OS会检查PCB中对应设备的状态,当发现键盘没有输入时,这个PCB将会从运行队列中拿出来放入wait_queue进行管理)等待用户输入,OS发现输入之后会将这个设备状态置为输入,那么OS再把这个PCB放入运行队列的尾部,等到执行这个进程时再读取数据;
进程状态变化的本质就是task_struct在不同的队列中进行流动,本质就是数据结构的增删查改。
###挂起:
挂起就是当内存不够时,将对列中的PCB对应的代码、数据放到磁盘的交换区(唤出)来节省内存空间方便其他进程正常运行,挂起状态有阻塞挂起,即从wait_queue中拿取数据和代码,有运行挂起,即从调度队列的尾部拿去PCB的数据代码进入磁盘交换区,当内存重新足够时,这些数据代码又会被OS从磁盘交换区中取出来还给对应的PCB(唤入)。
理解内核链表:
内核双链表不像普通的双向链表;在源代码中,会单独封装一个struct list head{struct list* next,*prev},这个head会被放入task_struct里面,叫做struct list links,也就是可以理解为双向链表在PCB内部;next、prev指向的不是下一个task_struct节点,而是指向这个节点里面的links。
那么将如何访问task_struct里面的其他成员呢?因为links在struct中有偏移量可能在一个我们不知道的地址位置;在struct里面成员地址是依次增大的,可以通过 &((struct task_struct*)0->links)找到某一个PCB的links地址,再(struct task_struct*)(next-&((struct task_struct*)0->links))得到这个PCB的起始地址,此时这个地址就是这个PCB指针,再通过这个指针取访问其他成员;

并且,在一个task_struct里面放入了多个这样的links,它们分别在不同的组织结构中起作用,这也是为什么一个类型的数据可以被放入多个不同的数据结构中进行组织,实际上是通过PCB中对应的指针进行的管理。
Linux中的进程状态:
R代表running运行

Linux阻塞包含S(sleeping)和D(disk sleep)。
S也叫可中断睡眠,浅睡眠;在进程状态处于S状态时,可以杀死进程来停止;

此时代码运行需要从键盘上读取数字,处于等待设备就绪的状态,即S阻塞

D也叫不可中断休眠,深睡眠;即只能等进程自己醒来或者直接断电终止,这个状态是不可被OS杀死的;举例:当内存空间不够时,进程的代码数据需要被暂存入磁盘中,但是若是磁盘空间不够,可能这份代码数据也放不下,在存放载入的过程中,OS认为内存严重不够了甚至要开始杀死进程了,那么这个进程对应的数据十分重要,但是OS将这个进程杀死了;但是此时磁盘终于发现自己存放不了这个已经被杀死进程的代码数据了,那么它准备将代码数据还给这个进程时,发现进程不在了,那么会丢弃这份代码数据;这样一来就造成了宝贵数据的丢失;所以某些进程需要D状态来保护自己从而保护用户的数据。
T(stopped)停止状态、t(tracing stop)追踪停止状态
这两种状态存在的意义就是当程序出现异常的时候,OS会将状态置为停止状态,来让用户检查出现的问题并且确认是否继续;
T状态的实验

用kill -19 接进程pid来SIGSTOP进程即停止

想要继续就使用kill -18,标识continue
t状态是在gdb进行调试时到断点之后会停止,那么此时进程的状态就是t

X(dead)死亡进程,这个进程只是一个返回状态,我们不会在任务列表中看到这个状态;
Z(zombie)僵尸状态:在子进程退出时,父进程没有收到子进程的返回信息,那么这个子进程的状态就是Z;当父进程一直收不到子进程的结束信息时,这个子进程的部分信息会一直存在,造成资源浪费,以及内存泄漏;
实验:


僵尸进程产生原因:
子进程在完成其任务后会正常退出,退出时会向父进程发送 SIGCHLD 信号,告知父进程自己已经结束。然而,父进程在其代码逻辑中并没有对这个信号进行处理,也没有用 wait() 或 waitpid() 函数来获取子进程的终止状态信息。因此,子进程在退出后,其进程描述符仍然保留在系统中,处于僵尸状态,成为僵尸进程。
引入一个知识点:
进程退出了还会有内存泄漏吗?一般进程是没有,因为进程退出了系统会自动回收占用的资源;但是有一种进程还会有内存泄漏,这种进程只要开启了就会一直在内存中,不会因为退出而结束,比如操作系统,那么对于这种进程我们需要谨慎对待,尽量避免出现僵尸进程;所以这也是为什么我们写的代码,使用了malloc函数不使用free直接退出也不会出问题,因为进程结束系统会自动释放。
关于内核结构申请:
task_struct会被复用,一个进程结束后其task_struct可能不会被释放而是被标记为空闲状态,当下一个进程被创建时,这个task_struct先会被初始化,然后这个PCB中的信息就是对应新进程的信息;
这种复用技术使用的是slab分配器,好处是这种复用机制带来了显著的性能提升。一方面,避免了频繁的内存分配和释放操作带来的开销,因为内存分配和释放是相对耗时的操作;另一方面,由于 slab 中的内存是预先分配好的,所以分配速度非常快,能够提高系统的整体性能。

孤儿进程
所谓孤儿进程就是一个子进程还没有结束但是其父进程先结束了,此时的子进程就叫做孤儿进程;孤儿进程会被一号进程所领养,若是没有进程领养这个子进程,那么等这个子进程释放的时候没有父进程收到其结束信息,那么子进程就会变成僵尸进程;
那什么是1号进程呢,可以理解为操作系统的一部分,一号进程也叫systemd;所有的孤儿进程都会被1号进程所领养;
被系统领养的子进程变为后台进程,也就是说,这个进程会自动执行;并且不能ctrl+c杀死这个后台进程了,必须使用kill -9 进程pid;
实验:
这个代码在执行一段时间之后,父进程结束;此时子进程还在打印,那么就会被1号进程领养;


ctrl+c杀不死

查看一号进程

3、进程优先级
进程优先级即进程得到CPU资源的先后顺序;
因为资源是有限的,要通过优先级来确定谁先谁后;(权限是能不能,而优先级是谁先谁后);
优先级就是task_struct里面的一个整数,值越低对应的优先级越高。
查看UID(执行者的身份)、PRI(优先级的值)、NI(nice值,控制PRI大小)

###ls -n首先看UID:
UID(user id)核心含义:
- 唯一性:每个用户(包括系统用户和普通用户)在系统中都有一个唯一的UID。
- 权限基础:UID是Linux权限系统的核心,系统通过UID判断用户对文件、进程等资源的访问权限。
- 底层识别:内核和文件系统直接通过UID进行权限验证,而非用户名(用户名仅用于人类可读)。
- 也就是说其实是进程代表用户;
第三列就是拥有者的UID、第四行就是所属组的GID

###PRI和NI:
PRI:就是实际上这个进程的代表优先级的值,这个值越低,对应进程优先级越高;默认值是80;不能直接修改;
NI:nice值,PRI修改的值,取值范围是 [-20, 19 ];通过修改NI来修改PRI;
也就是说,一个进程的真实的优先级值 = PRI(默认值)+NI;
这样设计的原因包含:每次更改优先级值,不需要记录上一次的PRI,只需要在80的基础上+NI
###修改PRI的方法:
1、top 之后按 r ,之后输入进程pid回车,再输入NI值回车


因为NI最大19,即PRI最大99,所以NI为20时,PRI只能到99:

2、renice:
renice -n NI值 -p 进程pid;

###为什么PRI有范围限制:
为了保证公平性,各个进程之间优先级差别不能太大;
当优先级设立不合理时,比如其他进程通过减少优先级导致有的优先级一直得不到CPU资源,那么这些得不到资源的优先级就会进程饥饿。
###补充概念:
竞争:系统中进程很多,但是CPU是少量的,可能只有1个,为了高效地完成任务,竞争相关资源,所以进程之间具有竞争属性;
独立:多进程运行,需要独享各种资源,多进程运行期间互不干扰(一个进程的task_struct被释放了,不会影响其他进程的);
并行:多个进程在CPU分别同时进行运行;
并发:多个进程在CPU下采用切换运行的方式,同一个时间段让多个进程都得以推进。
4、进程切换与调度
切换:
###铺垫:
1、首先进程是基于时间片的东西,也就是说,一个进程占有CPU运行时只会运行时间片的时间,当这个进程较大时,超过了时间片时间,那么这个进程不会一次性运行完;从此可以知道,死循环的程序并不会让系统挂掉,因为它每次只能运行时间片,但这个时间片过去了,其他的进程还是可以正常运行;
2、寄存器是CPU内部的临时空间,寄存器上面存放了一些临时数据,这些临时数据就是进程的硬件上下文数据;值得注意的是,寄存器并不等于寄存器上面的数据,寄存器是一个物理空间,可以一直存在,但是上面的数据可以切换多分。
###正式进入切换:

已知寄存器上面存放了一个正在占用CPU资源的进程的硬件上下文数据,那么当这个进程的运行完时间片之后,这个进程的上下文数据会被从寄存器上面剥离,保存在这个进程自己的task_struct里面;当下次轮到这个进程运行时,这个进程自己的上下文数据又被恢复到寄存器里面,方便往下运行;
切换的核心:保存和恢复进程的硬件上下文数据(即CPU的临时数据);
几个问题:
这些上下文数据被保存于何处?
这些上下文数据会被保存在进程自己的task_struct的TSS任务状态段里面:
内核代码:
如何区分是新进程还是已经运行过的进程?
在进程的task_struct里面会设置一个标记值,当这个值为0就是新,反之为老;
Linux的O(1)调度:

首先一个CPU具有一个运行队列;
##先介绍queue[140]:这是一个结构体指针数组(struct task_struct* queue[140] ),140对应着优先级,前100个为实时优先级(这个不考虑,是在实时操作系统中,应用领域),后40个就是我们学习的进程即PRI为[60 , 99];这个数组存放的是链表,就像哈希桶一样,每个位置是一个链表,每个链表中的进程优先级一样,不同链表的优先级不同,同一个链表上面的进程会FIFO来被调度运行;
##这个调度算法是O(1)的;那么到底是怎么样找到队列的?这里有一个bitmap[5]也就是位图,int类型的,也就是160个比特位(4个不够,6个多余,5个最优),其中140个比特位对应着queue[140]个链表,通过位图可以快速索引到对应链表来进行调度;nr_active是记录有多少个进程的,当一个进程都没有时,nr_active就是0,当然只有nr_active不为0时会开始找;
##那么具体的进程是如何调度的?首先通过上述操作找到这个优先级值的链表,之后按照FIFO队列来将进程调度到CPU中,当这个进程的时间片完毕时,再通过切换来运行链表中下一个进程或者下一个优先级链表的对头进程;但是这样还是不行,假设现在有一个进程对应的程序是死循环,那么它的时间片完毕之后,会被插入到对应优先级的队尾中,就算当这个队列中其他进程运行完毕之后,由于优先级,还是会继续运行这个死循环进程的,那么就永远走不出这个优先级值对应的队列,那么后面的队列的进程就会进程饥饿;
为了解决这个问题,让其他的进程正常分配到CPU资源,这里设置了两个队列的结构,一个活跃队列(active queue),一个过期队列(expired queue),这两个队列的结构一模一样:
具体的过程,每次占用CPU资源的都是active queue。还是上述操作,不同的是当一个进程的时间片完毕时,这个进程会被从active queue里面拿出来,放到expired queue相同优先级值的链表中(当然这个进程程序没有走完,只是时间片到了,若是走完了就直接结束了),这样下来active queue里面的进程会越来越少,expired queue里面的会越来越多,只到active queue里面的nr_active为0时,交换两个队列的指针,此时上次的过期队列就变成了活跃队列,并开始运行:
这样反复交换,一定会让所有的进程都能占有CPU资源;
之前那个死循环的进程会被放到expired queue里面,active queue里面的其他进程就会正常运行了。
插入的新进程会插入到expired里面处于等待状态;
另外现在也可以解释为什么,会有nice值了:举例:active queue里面有一个PRI为80的进程,那么运行到这个进程进行修改PRI为60时,这个进程有几种走向:第一就是放到activequeue的60优先级对应了链表,但是明显不合理,因为已经走过60了,第二就是不动,也不合理,60的怎么能放到80的位置呢。于是设置了expired queue,当这个被修改PRI的进程时间片完毕时,就会用到nice值计算出新的PRI并放到expired queue里面,等待指针交换;
###进程切换、调度整体的流程:
插入新进程被放到active queue对应优先级值的链表里面;进程运行时,先判断nr,不为0,通过bitmap找到优先级最高的链表开始进程的运行(通过全局的指针current链接这个进程放到寄存器里面);一个进程时间片完毕后会切换下一个进程,并且将这个进程放到expired queue里面等到active queue里面进程个数为0时(时间片都走完或者结束)交换指针再开始运行。
四、环境变量
(1)命令行参数
main函数虽说是程序的入口,但是main函数也会被调用,也有参数;
argv是一个指针数组,数组里面的指针都是字符串,argc是这些字符串的个数:

执行结果:
bash会将输入的字符串以空格分开,形成一个一个字符串,对应的就是argv,个数对应的就是argc

实际上./code就是一个指令,而后面的-a -b 什么的就是对应的选项,用来实现指令的子功能
指令选项的实现原理:


最后以null结尾

那么像ls -l这样的指令也是这样实现的;
命令行参数用来实现指令的子功能 。
(2)引入环境变量
执行文件时要先找到文件;
在我们执行code的时候要在前面加上./表示在当前路径找到这个二进制文件来执行;但是例如ls、cd、pwd这样的指令执行不需要加./,也就是说它们对应的二进制文件的执行不需要我们指定在哪里;
这是为什么?
这是bash通过环境变量PATH帮助找到这些指令的路径的;指令(以及可执行程序)搜索的默认路径是PATH,在这个环境变量下有一个路径是/usr/bin,就是在这个路径下找到指令的;
env以名字加上内容的方式打印系统支持所有的环境变量:

echo $PATH,打印这个环境变量的内容:

那么将code这个文件添加至usr/bin中就能像系统指令那样执行了:

或者将code所在路径放在环境变量PATH中,也能达到相同效果:
直接重新给环境变量赋值:
给环境变量拼接上值:

要注意:上面这些操作产生得效果都是临时的,也就是说退出再次登录之后,就恢复成了原来的环境变量值;
从存储的角度理解环境变量
登录之后就会有bash,bash会生成两张表,一张是环境变量表,表中的内容也就是env打印出来的环境变量,表的存储方式是指针数组;另一张表就是命令行参数表,这些表记录的是输入的指令,存储方式相同;

bash帮助我们找到指令;具体地,bash先拿到输入命令地名字比如ls、cd等等,之后再去环境变量表中查找,找到即可。
环境变量是从哪里来的
最开始环境变量来自于配置文件,也就是没有登录也会有的文件;
在家目录中查看隐藏文件,就有两个文件,这两个文件就是 环境变量的出生点

若是将code的路径放进配置文件中,那么这个code就会像系统指令一样了,永久有效。
最后若是有很多个用户,那么也会对应生成这么多个bash,同时每个bash都有两张表。
(3)其他的环境变量
环境变量基本概念
环境变量的组织方式
每个程序都会收到⼀张环境表,环境表是⼀个字符指针数组,每个指针指向⼀个以’\0’结尾的环境 字符串

HOSTNAME主机名;
SHELL命令行解释器;
SSH_CLIENT客户端i环境变量;

SSH_TTY 标识当前SSH连接所关联的终端设备;
USER 当前用户;
MAIL 邮件;
PATH 可执行文件搜索路径 ;
PWD 当前路径;

HISTSIZE 保存最近指令的最大条数,也是使用上下键查找指令的最大条数 ;

HOME 家目录,这也是为什么cd ~能够直接回到家目录的原因,因为有这个环境变量;

LOGNAME 登录用户
su之后USER和LOGNAME都不改变,除非su-(重新登录)USER和LOGNAME才会变成root;

OLDPWD 记录上次路径,这也是为什么cd - 能够直接回到上次路径

(4)获取环境变量的方法
增加、删除环境变量的方法
export 环境变量名=内容//增加环境变量
unset 环境变量名//删除环境变量
这两个方式是内存级的,下一次登录不会存在

方法1 env获取
main函数可以有三个参数,前两个是之前提到的,第三个就是char* env[],这是父进程bash给子进程的环境变量表,并且子进程的子进程也能继承这张表


环境变量通常具有特殊用途,并且在系统中通常具有全局性
在这里修改父进程的环境变量表,子进程也会改变通过看自己设置的MYNAME就可以发现;
方法2 getenv
头文件stdlib.h 在自己写的程序中使用


引深:
若是想写一个程序,其他人不准执行
登录其他用户后,name不再是user1,那么这个程序就不能运行

方法3 extern char** environ
相当于env,声明写 extern char** environ


(5)环境变量的特性
环境变量具有全局性
本地变量
本地变量在bash中,bash会记录两套变量:本地变量和环境变量;
本地变量只在bash内部被使用,并不会被子进程继承;
本地变量定义:直接写,例如i=1 ;set查看本地变量和环境变量;定义为本地变量的可以直接用export接本地变量名变为环境变量;


为什么用export可以给父进程bash增加环境变量?
父进程能给子进程增加环境变量可以理解,继承;
但是子进程是这么弄得?这里不是子进程的原因,是因为export是一个内建命令(built-in commend),是由bash自己执行的(bash自己调用函数或者系统调用)不需要创建子进程。
五、程序地址空间
一段代码:


可以看到父子进程中的gal地址都一样,但是打印出来子进程中的变化,父进程中不变化;
这说明
变量内容不⼀样,所以父子进程输出的变量绝对不是同⼀个变量但地址值是⼀样的,说明,该地址绝对不是物理地址!在Linux地址下,这种地址叫做 虚拟地址我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,用户⼀概看不到,由OS统⼀管理
OS必须负责将 虚拟地址 转化成 物理地址
进程地址空间
程序地址空间说法不准确,应该是进程地址空间(或者虚拟地址空间)

描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀ 个mm_struct结构,在每个进程的task_struct结构中,有⼀个指向该进程的结构。并且每一个进程都有一个页表;
解释上述代结果:
mm_struct里面存放地址,这个地址和代码加载到内存的实际物理地址都会对应的存放到页表中;就比如gal这个变量,进程通过页表映射就能找到在实际地址中的gal地址;子进程拷贝父进程,页表也是拷贝,并且是浅拷贝,那么gal虚拟地址相同,物理地址相同,当子进程中gal发生改变时,在内存中发生写实拷贝,拷贝gal,生成另一份gal,并且有不同的地址再将子进程中页表的gal的映射关系改变,使得子进程的虚拟地址通过页表的映射能找到物理地址中的写实拷贝出来的新的gal;那么子进程改变的是写实拷贝出来的gal,但是父进程和子进程中的gal在mm_struct中的地址相同,所以打印出来的gal地址相同但是值不同;
并且我们只能看到虚拟地址而不能看到物理地址。
进程的独立性:
1、内核数据结构的独立性(包括task_struct mm_struct);
2、代码和数据的独立性
在mm_struct里面会有区域划分
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
}
程序运行时:首先给mm_struct申请指定大小的空间并且调整区域划分;之后加载程序时,内存再申请物理空间;这两部都弄完了页表的映射关系就建立起来了,之后再通过映射找到进程的代码数据
问题:堆区只有一个吗?不是,那么怎么找到想要找到的堆区呢?
linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。
也就是说mm_struct里面的vm明确指向的区域属于什么类型的区域

mm_struct:代表一个进程的整个虚拟地址空间,其中包含了很多成员变量,用于管理和维护进程的内存信息。其中有一个重要的成员是mmap,它指向一个由vm_area_struct组成的链表头,这些vm_area_struct描述了进程虚拟地址空间中的不同区域。vm_area_struct:描述了进程虚拟地址空间中的一个连续区域,每个堆区就是一个连续的虚拟地址空间范围,会由一个或多个vm_area_struct来表示。这些vm_area_struct按照虚拟地址从小到大的顺序排列,形成一个链表或红黑树(根据具体实现)。
为什么要有虚拟地址空间?
1、让地址从“无序”变为“有序”
因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进⾏映射,在 进程视⻆所有的内存分布都可以是有序 的。
2、保护物理内存中的数据
地址空间和⻚表是OS创建并维护的!是不是也就意味着,凡是想使⽤地址空间和⻚表进⾏映射,也⼀定要在OS的监管之下来进⾏访问!!也顺便 保护了物理内存中所有的合法数据,包括各个进程以及内核的相关有效数据!
3、进程管理模块和的解耦合
因为有地址空间的存在和⻚表的映射的存在,我们的物理内存中可以对未来的数据进⾏任意位置的加载!物理内存的分配 和 进程的管理就可以做到没有关系, 进程管理模块和内存管理模块就完成了解耦合。
一些问题
1、可以不需要加载代码和数据,可以只有task_struct、mm_struct、页表
2、创建进程是先有内核数据结构(task_struct、mm_struct)再加载代码和数据
3、如何理解进程挂起?
首先页表中存在一种机制:页表中断(即可以只有虚拟地址的地址而没有物理地址,在运行到这段虚拟地址时再加载程序代码并填上页表中的物理地址)
那么挂起的时候因为内存资源不足,将代码数据唤出放到磁盘交换区,但是此时页表中的虚拟地址可以不删除,同时这份代码数据的进程内核数据结构也可以不改变。
1783

被折叠的 条评论
为什么被折叠?



