内存管理机制

什么是分页机制

逻辑地址和物理地址分离的内存分配管理方案

程序的逻辑地址划分固定大小的页 物理地址划分为同样大小的帧 通过页表对应逻辑地址和物理地址

什么是分段机制

分段是为了满足代码的一些逻辑需求

数据共享、数据保护、动态链接 通过短标来对应逻辑地址和物理地址 每个段内部是连续的内存分配,段和段之间是离散分配的

分段和分页的区别

分页是出于内存利用率的角度提出的离散分配机制 分段是出于用户的角度,用于数据保护,数据隔离等用途的管理机制 页的大小是固定的,操作系统决定(与物理帧保持同样大小);段大小不确定,用户程序确定

页面的大小根据不同的CPU不而有所不同。x86和x64系统使用的页面大小都是4KB,而IA-64系统使用的页面大小是8KB。

什么是虚拟内存

通过把一部分暂时不用的内存信息放到硬盘上

局部性原理,程序运行时只有部分必要的信息装入内存 内存中暂时不需要的内容放到硬盘上

虚拟地址和物理地址转换,分级页表查找

32位的虚拟地址被分成3个域,页号1(10位),页号2(10位),偏移(12位) 根据页号1的10位,找到1级页表目录 根据页号2的10位,找到目的页表,即页的起始的地址 加上偏移的12位,找到目的地址

什么是内存抖动

本质上是频繁的页调度行为

频繁的页调度,进程不断产生缺页中断 置换了一个页,又不断再次需要这个页 运行的程序太多;页面替换策略不好,终止进程或增加物理内存

操作系统虚拟内存换页的过程是什么?

正常都是访问内存时,发现页面不存在,产生了缺页中断,要进行虚拟内存换页。

处理过程如下:

  • 如果内存中有空闲的物理页面,则分配一物理页帧r,然后转第4步,否则转第2步;
  • 选择某种页面置换算法,选择一个将被替换的物理页帧r,它所对应的逻辑页为q,如果该页在内存期间被修改过,则需把它写回到外存;
  • 将q所对应的页表项进行修改,把驻留位置0;
  • 将需要访问的页p装入到物理页面r中;
  • 修改p所对应的页表项的内容,把驻留位置1,把物理页帧号置为x;
  • 重新运行被中断的指令。

至此,整个换页过程结束。

虚拟内存换页的算法有哪些(将磁盘页存入内存)?

最近最少使用算法(LRU):指维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾,优先淘汰表尾的页面。

最少频率使用算法(LFU):它为每个页面设计了一个访问频次计数器,页面每次被访问时,频次加一,优点淘汰频次最小的页面。

先进先出算法(FIFO):维护一个所有页面的链表,最新进入的页面放在表尾,最早进入的页面放在表头。当发生缺页中断时,淘汰表头的页面,并把新页面加到表尾。缺点是有可能会将经常访问的页面淘汰。

最近未使用算法(NRU):优先淘汰没有被访问的页面。

第二次机会算法(FIFO的改进):它对先进先出算法做了改进,当页面被访问时设置该页面的R(Read)位为1。需要替换时,检查最老页面的R位,如果为0,就表示这个页面又老又没有被使用,可以置换掉;如果为1,就将R位清0,并放到链表尾部。

时钟算法:第二次机会算法需要在链表中移动页面,降低了效率,时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。

最优算法:将最长时间内不再被访问的页面置换标记出来,然后把因调用这个页面而发生的缺页中断推迟到将来。是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。

一个线程和进程的最大空间

linux系统下,一个进程的虚拟地址空间是4GB,虚拟地址空间的大小跟操作系统的位数相关,比如32为的操作系统,其虚拟地址不可能超过4GB的大小。但是物理内存可以配置大于4GB,大于4GB的内存在配置PAE(PAE:Physical Address Extension)的情况下是有用的,因为多个程序要使用的物理内存总和是很容易超过4GB的。

7. 两种IO多路复用的事件处理模式

Proactor模式和Reactor模式的区别?

事件处理模式:Reactor模式(同步IO)/Proactor模式(异步IO)。服务器程序通常要处理3类事件,I/O事件、信号和定时事件。在处理事件上有两种高效的事件处理模式:Reactor和Proactor。这两种其实也都是I/O复用模型。

Reactor:它要求主线程只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程。除此之外,主线程不做任何其他实质性的工作,读写数据、接收新的连接,以及处理客户请求均在工作线程中完成。使用同步I/O模型(以epoll_wait()为例)实现的Reactor模式的工作流程如下:

1)主线程往epoll内核事件表中注册socket上的读就绪事件。

2)主线程调用epoll_wait()等待socket上有数据可读。

3)当socket上有数据可读时,epoll_wait()通知主线程。主线程将socket可读事件放入请求队列。

4)睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写事件。

5)主线程调用epoll_wait()等待socket可写。

6)当socket可写时,epoll_wait()通知主线程。主线程将socket可写事件放入请求队列。

7)睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。

Proactor模式:与Reactor模式不同,Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。使用异步I/O模型(aio_read和aio_write为例)实现的Proactor模式工作流程:

1)主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序。(信号)

2)主线程继续处理其他逻辑。

3)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户缓冲区的位置,以及写操作完成时如何通知应用程序(信号)。

4)主线程继续处理其他逻辑。

5)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。

6)应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket。

同步模拟Proactor模型:《Linux高性能服务器编程》一书中提到了一种用同步I/O来模拟Proactor模式的方法。其原理是:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。那么工作线程直接获得了读写结果,接下来只要做的只是对读写结果的处理。

1)主线程调用epoll_wait()内核事件表中注册socket上的读就绪事件。

2)主线程调用epoll_wait()等待socket上有数据可读。

3)当socket上有数据可读时,epoll_wait()通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将所有数据封装成一个请求对象并插入请求队列。

4)睡眠在请求队列中的某个工作流程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件。

5)主线程调用epoll_wait()等待socket可写。

6)当socket可写时,epoll_wait()通知主线程。主线程往socket上写入服务器处理客户请求的结果。 这里同步模拟和异步模拟Proactor的区别在于,同步模拟是主线程自己进行I/O操作,需要等待读完数据,而异步I/O是通过aio_read()异步读取,主线程不需要等待I/O完成。

总结:Reactor和Proactor的本质区别在于谁负责I/O操作,其实它们都可以分别利用同步I/O或者异步I/O来实现。

简单的理解:首先它们都是IO复用下的事件驱动模型,然后就从同步异步这两个点来切入概念。注意关键区别在于何时IO,reactor是关心就绪事件,比如可读了,就通知你,就像epoll_wait 。proactor关心的是完成比如读完了,就通知你。

8. epoll, poll,selector以及三者之间的区别

(1)select==>时间复杂度O(n) 无差轮询

它仅仅知道了,有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll==>时间复杂度O(n) 忙轮询

poll本质上和select没有区别,它将用户传入的数据拷贝到内核空间,然后查询每个fd(file description)对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)epoll==>时间复杂度O(1) 事件驱动

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

select:

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll:

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的水平触发模式,,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了。

所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll的优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1 G的内存上能监听约10万个端口);

2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数; 即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

总结:

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

epoll为什么要有EPOLLET触发模式?

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。

而采用EPOLLET这种边缘触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

epoll事件

  • EPOLLIN,读事件
  • EPOLLOUT,写事件
  • EPOLLPRI,带外数据,与select的异常事件集合对应
  • EPOLLRDHUP,TCP连接对端至少写半关闭
  • EPOLLERR,错误事件
  • EPOLLET,设置事件为边沿触发
  • EPOLLONESHOT,只触发一次,事件自动被删除

9. 内存多线程安全问题

对一块内存1,值为a。线程1:将该内存的值修改为b:a->b。线程2:a->c,c->a。如何避免当内存值a被修改为c之后,线程1修改为b。

可以使用CAS(比较与交换,Compare and swap)算法来解决,它是一种有名的无锁算法,使用方法很简单,每次去检测一个值是不是目标值是则修改为某一个值,否则不做处理,这整个操作是原子的。这个场景中我们只需要在线程1中使用这个算法即可保证内存被修改为c的时候,线程1不修改内存的值。

10. fork的时候子进程还拥有父进程的文件描述符吗?

fork的时候子进程会拥有父进程的文件描述符,对打开的文件子进程可以进行共享操作。

11. 三种特殊的进程

孤儿进程

如果父进程先退出,子进程还没退出那么子进程将被 托孤给init进程,这里子进程的父进程就是init进程(1号进程)。将父进程终止,子进程并没有退出,而是在后台继续运行。

僵尸进程

指的是进程终止后进入僵死状态(zombie), 等待告知父进程自己终止, 然后才能完全消失。但是如果一个进程已经终止了, 但是其父进程还没有获取其状态, 那么这个进程就称之为僵尸进程。僵尸进程还会消耗一定的系统资源,并且还保留一些概要信息供父进程查询子进程的状态。

守护进程

同样我们需要了解一下什么是守护进程,守护进程就是在后台运行,不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务**.习惯上守护进程的名字通常以d结尾(如sshd**), 但这不是必须的.

12. 内存与指针等

指针变量的长度

任何类型的指针变量所占用的内存空间都是4字节,指针变量本身的长度是固定的!而指针长度是不确定的,32位操作系统指针的长度是4字节,64位是8字节,这样指针才能寻址完程序的虚拟地址空间。

内存地址空间是否一定大于所有物理存储器的容量?

当然不是。举个例子,intel的奔3处理器支持32位虚拟地址,从而每个程序最大使用4G空间,但是其物理地址线有36根,可以使用64G的物理内存。所以内存地址空间的大小跟所有物理存储器的容量大小没有一定的关系。

加载的数据大于内存容量时怎么操作 只能分批读入,没有其他的办法。但是,分批归分批,具体操作还是有不少讲究。

32位Linux系统内核虚拟地址空间在进程虚拟地址空间中的哪一部分

进程虚拟地址空间中3-4 GB划分为内核所使用

Linux中的Page Cache

CPU如果要访问外部磁盘上的文件,需要首先将这些文件的内容拷贝到内存中,由于硬件的限制,从磁盘到内存的数据传输速度是很慢的,如果现在物理内存有空余,干嘛不用这些空闲内存来缓存一些磁盘的文件内容呢,这部分用作缓存磁盘文件的内存就叫做page cache。

和CPU里的硬件cache是不是很像?两者其实都是利用的局部性原理,只不过硬件cache是CPU缓存内存的数据,而page cache是内存缓存磁盘的数据,这也体现了memory hierarchy分级的思想。

相对于磁盘,内存的容量还是很有限的,所以没必要缓存整个文件,只需要当文件的某部分内容真正被访问到时,再将这部分内容调入内存缓存起来就可以了,这种方式叫做demand paging(按需调页),把对需求的满足延迟到最后一刻,很懒很实用。

malloc

多次调用malloc()后空闲内存被切成很多的小内存片段,这就使得用户在申请内存使用时,由于找不到足够大的内存空间,malloc()需要进行内存整理,使得函数的性能越来越低。聪明的程序员通过总是分配大小为2的幂的内存块,而最大限度地降低潜在的malloc性能丧失。也就是说,所分配的内存块大小为4字节等等。这样做最大限度地减少了进入空闲链的怪异片段(各种尺寸的小片段都有)的数量。

13. 硬中断软中断

硬中断:

  1. 硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)。
  2. 处理中断的驱动是需要运行在CPU上的,因此,当中断产生的时候,CPU会中断当前正在运行的任务,来处理中断。在有多核心的系统上,一个中断通常只能中断一颗CPU(也有一种特殊的情况,就是在大型主机上是有硬件通道的,它可以在没有主CPU的支持下,可以同时处理多个中断。)。
  3. 硬中断可以直接中断CPU。它会引起内核中相关的代码被触发。对于那些需要花费一些时间去处理的进程,中断代码本身也可以被其他的硬中断中断。
  4. 对于时钟中断,内核调度代码会将当前正在运行的进程挂起,从而让其他的进程来运行。它的存在是为了让调度代码(或称为调度器)可以调度多任务。

软中断:

  1. 软中断的处理非常像硬中断。然而,它们仅仅是由当前正在运行的进程所产生的。
  2. 通常,软中断是一些对I/O的请求。这些请求会调用内核中可以调度I/O发生的程序。对于某些设备,I/O请求需要被立即处理,而磁盘I/O请求通常可以排队并且可以稍后处理。根据I/O模型的不同,进程或许会被挂起直到I/O完成,此时内核调度器就会选择另一个进程去运行。I/O可以在进程之间产生并且调度过程通常和磁盘I/O的方式是相同。(用户态进程可以使用软中断,从用户态进入内核态)
  3. 软中断仅与内核相联系。而内核主要负责对需要运行的任何其他的进程进行调度。一些内核允许设备驱动的一些部分存在于用户空间,并且当需要的时候内核也会调度这个进程去运行。
  4. 软中断并不会直接中断CPU。也只有当前正在运行的代码(或进程)才会产生软中断。这种中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。有一个特殊的软中断是Yield调用,它的作用是请求内核调度器去查看是否有一些其他的进程可以运行。

14. 无状态和有状态

对服务器程序来说,究竟是有状态服务,还是无状态服务,其判断是指两个来自相同发起者的请求在服务器端是否具备上下文关系。如果是状态化请求,那么服务器端一般都要保存请求的相关信息,每个请求可以默认地使用以前的请求信息。

无状态请求,服务器端所能够处理的过程必须全部来自于请求所携带的信息,以及其他服务器端自身所保存的、并且可以被所有请求所使用的公共信息。

无状态的服务器程序,最著名的就是WEB服务器。COOKIE的存在,是无状态化向状态化的一个过渡手段,他通过外部扩展手段,COOKIE来维护上下文关系。

状态化的服务器有更广阔的应用范围,比如MSN、网络游戏等服务器。他在服务端维护每个连接的状态信息,服务端在接收到每个连接的发送的请求时,可以从本地存储的信息来重现上下文关系。这样,客户端可以很容易使用缺省的信息,服务端也可以很容易地进行状态管理。比如说,当一个用户登录后,服务端可以根据用户名获取他的生日等先前的注册信息;而且在后续的处理中,服务端也很容易找到这个用户的历史信息。

15. 用户态和内核态

通过系统调用将Linux整个体系分为用户态和内核态(或者说内核空间和用户空间)。那内核态到底是什么呢?其实从本质上说就是我们所说的内核,它是一种特殊的软件程序,特殊在哪儿呢?控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行

用户态就是提供应用程序运行的空间。为了使应用程序访问到内核管理的资源例,内核必须提供一组通用的访问接口,这些接口就叫系统调用

用户态的进程能够访问的资源受到了极大的控制,而运行在内核态的进程可以“为所欲为”。一个进程可以运行在用户态也可以运行在内核态,那它们之间肯定存在用户态和内核态切换的过程。打一个比方:C库接口malloc申请动态内存,malloc的实现内部最终还是会调用brk()或者mmap()系统调用来分配内存

那为问题又来了,从用户态到内核态到底怎么进入?只能通过系统调用吗?还有其他方式吗?

从用户态到内核态切换可以通过三种方式:

  • 系统调用,其实系统调用本身就是软中断。
  • 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
  • 外设中断:当外设完成用户的请求时,会向CPU发送中断信号。