目录
存储管理
linux内存管理基本框架
系统空间管理和用户空间管理
进程与进程调度
进程四要素
用户堆栈的扩展
进程三部曲:创建,执行,消亡
系统调用exit(),wait()
内核中的互斥操作
存储管理
linux内存管理基本框架
系统空间管理和用户空间管理
所以,对于系统空间而言,虚拟地址到物理地址的映射关系。给定一个虚拟地址x,其物理地址是x-PAGE_OFFSET,给定一个物理地址,起虚拟地址为x+ PAGE_OFFSET
我们刚刚讲完段式映射和页式映射,怎么会有上面的虚拟地址到物理地址的映射关系呢?
其实这是一个单独设立的映射,只是对系统空间而言是这么管理的
而对于用户空间则还是进行段映射和页映射。
因为每个进程都是独自拥有自己的用户空间3G,系统空间却是所有进程共享的。
后面介绍页面管理中的页面换入,换出,分配,撤销,都是对用户虚拟空间进行,不考虑系统空间,因为系统空间已经创建,基本不存在换入换出问题(除进程控制块的创建与撤销)。
所以,这里就理解为系统空间的映射关系为虚拟地址到物理地址的映射关系,而用户空间的映射就是接下来我们要讲的段映射和页映射的全过程。
我们要讲的段映射和页映射的全过程。
- 虚拟地址(存在于进程创建时系统分配的进程控制块中)通过段映射得到线性地址。
- 线性地址通过页映射得到物理地址
进程与进程调度
相信学过操作系统的同学都知道进程这个概念,但肯定有一部分同学无法说清楚进程是什么,只知道进程是程序运行的一个实例。的确,在我学完操作系统课程之后,我依然无法说清楚进程是什么,直到我看了计算机系统这本书,我才有了一个概念,那就是进程不是什么,它是一个抽象的概念(文件其实也是一个抽闲的概念),一张图让我明白进程这个概念。
进程四要素
然后此书上给出进程的四要素,让我对进程有了一个更深的理解
1有一段程序供其执行,这段程序不一定是进程所专有,可以与其他进程公用
2有前面的“私有财产”,就是进程专用的系统堆栈空间。(系统空间不是独立的,任何进程不能直接改变系统空间的内容(除自身的系统堆栈空间))
3有“户口”,每一个进程都必须有一个task_struct数据结构,有了这个结构,才能成为内核调度的一个基本单位接收内核调度,这个结构记录着进程所占用的各项资源。
4有独立的用户空间,这部分是供进程独自占有的
然后,书中有这么一段话(帮助大家度进程和线程有一个了解):缺少上面的任何一条都不能称为进程,如果只具备前面3条,那就称为线程,如果完全没有用户空间,就称为“内核线程”,而如果是共享用户空间则称为“用户线程”。 但是,注意,不要把这里的“线程”与有些系统中在用户空间的同一进程内实现的“线程”相混淆。那种线程显然不拥有独立、专用的系统堆栈,也不作为一个调度单位接收内核调度。
其中系统空间堆栈用于分配下面2页内容
其中非常有必要介绍一下进程控制块task_struct数据结构,里面包含了一个进程各种信息,进程调度得以实现,绝大功劳归功于进程控制块
注意,系统空间堆栈的空间不像用户堆栈那样可以在运行时动态扩展,而是静态的确定了的。所以在中断服务程序,内核软中断服务程序以及设备驱动程序的设计中,应该注意不然这些函数嵌套太深。同时,这些函数也避免使用太多太大局部变量。
用户堆栈的扩展
越界访问其中一种情况——相应的页面目录项和页表项未空,也就是线性地址与物理地址的映射关系尚未建立,或者已撤销。
然后需要注意的是,在建立页面目录和页表项的同时,我们不光在内存上分配页面目录项和页表项,而且需要把虚存上的内容拷贝到内存页面上,即一页4K,但不一定是建立完全,可以慢慢来嘛,这也就是为什么那么多的进程可以同时运行在计算机上,当他们所占的内存总和大于内存实际大小时,操作系统同样能使其有条不紊的运行。这里举个例子,想象一下,比如要铺设从南昌到北京的火车轨道,需要最短的轨道长度是多少,答案不是两点线段最短,而是只需要火车的长度就行,因为我们可以拆东墙补西墙,理论上只要我们铺轨道的速度足够快的话,那么火车就好像一直行驶在轨道上了。
当用户进程申请扩展堆栈区间时,由于申请的部分还没建立页面映射,所以会产生一次缺页异常,然后建立页面映射,在内存分配页表,而且分配一部分必须的页面空间。
进程三部曲:创建,执行,消亡
linux操作系统,每一个进程的创建都需要父进程,复制出来的子进程有自己的task_struct(进程控制块)和系统空间堆栈,但与父进程共享其他所有的资源。
实际上,子进程通过fork(),clone()创建一个基于父进程一样的进程,他们之间的区别在于子进程继承父进程资源的多少问题,fork是完全继承,而clone是有选择性的继承。
其中继承的资源有,系统空间和用户空间,但是子进程有自己的进程控制块。好了,进程创建好了,可以开始执行么,显然,和父进程一样的子进程有何用,子进程必须得独立起来,
这就到第二步,子进程通过execve()加载自己的目标程序,然后子进程和父进程分道扬镳,走自己的路。
那么父进程呢,父进程有三种选择,
- 继续走自己的路,与子进程分道扬镳,如果子进程先于父进程“去世”,则有内核给父进程发一个报丧信号(若父进程没有收到,那么子进程就成立僵尸进程,需要init进程来处理)。
- 停下来进入睡眠,等待子进程结束后唤醒父进程。
- 父进程结束自己的生命,此时子进程就成了孤儿进程,全部都指向init作为自己的父进程,子进程结束后统一给init进程发信号。
系统调用exit(),wait()
exit()系统调用是用来结束一个进程,一个进程的结束,那么就必须得释放它生前所占有的资源,但是有部分资源是他自己不能主动释放的,必须交给父进程来处理,
还有两个关键问题
- 一个进程结束自己后,他的子进程就成孤儿了,它需要发送一个信号给它的所有子进程,然后修改子进程的父进程指针,统一指向init
- 一个进程结束自己后,它必须的通知它的父进程,最后给它收尸(获得一个信号,然后释放子进程结束后还占有的资源)
注意,
所谓的父进程有“生父”和“养父”之分,一个进程在创建之初其生父和养父是一致的,所以两个指针指向同一个父进程。
养父在运行时可以被短暂的改变,比如。debug能让我们跟踪进程的执行情况,被跟踪的进程同样成为子进程,主动跟踪的进程则为其养父进程,
如果一个进程在其子进程之前“去世”的话,就要把它的子进程托付给某个进程。托付给谁呢?如果当前进程是一个线程,就托付给同一线程组中的下一个线程,否则,就只好托付给init进程,
wait()调用可以说是进程同步的一个系统调用,因为之前讲过父进程的情况又三种,其中一种就是等待子进程结束,那么wait()系统调用就是这么一个作用,使自己进入睡眠,并进行一次进程调用操作,直到子进程结束后发送一个信号量唤醒父进程,重新让父进程挂入活动进程队列,参与进程调度。
内核中的互斥操作
如果一个进程进入了临界区A,而又企图进入另外一个临界区B的话,那就可能 因为进入不了那个临界区,也就是得不到所需的资源,而只好在B的队列中等待。那么,所等待的资源又在谁的手里呢?如果已经占有了那项资源的进程恰好也在A的队列中等待,那就发生了“死锁”
互斥操作的两种机制就是信号量和锁。
信号量一般用于解决进程间互斥问题,
锁则有很多种类,其中自旋锁常用于多处理器之间互斥操作
======
信号量: 简单的理解用信号量来表示某种资源(一般资源数大多为1),使用一次down一次,直到为0后就不允许使用,需要使用的进程都暂时进入睡眠状态,等其他进程使用完up一次后又资源可以使用后再唤醒因此资源而睡眠的进程,一次只能唤醒一个。
然后需要注意的一个问题就是当两个进程A,B分别申请资源a,b而进程A申请到a,进程B申请到了b,而彼此必须两者都拥有才能执行下次,那么AB之间就陷入了死锁,为了避免死锁,也提出了很说算法,其中最简单就是按序申请,或者一次性能拿到所有资源的进程才允许分配(如银行家算法)。
自旋锁:当不同的处理器访问临界资源时,其中一个处理器允许访问,那么另外一个处理器就必须等待,那么这是处理器干嘛呢,能不能做其他的事呢,对于自旋锁而言,另外一个处理器是什么都不做,一直在循环做无用功,看上去有那么点浪费,但也没办法,只能把处理器锁住。