操作系统面试知识点汇总

计算机网络面试知识点汇总,请看这里
数据结构面试常见内容汇总,请看这里

1 进程与线程

进程:一个程序在一个数据集合上的一次运行过程,是对运行时程序的封装。进程是系统进行资源调度和分配的的基本单位, 实现了操作系统的并发;

线程:线程是进程的子任务, 是 CPU 调度和分派的基本单位, 用于保证程序的实时性, 实现进程内部的并发; 线程是操作系统可识别的最小执行和调度单位。

每个线程都独自占用一个虚拟处理器:独自的寄存器组, 指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间,打开的文件队列和其他内核资源。

并发(concurrency)和并行(parallelism)

并发(concurrency) : 指宏观上看起来两个程序在同时运行, 比如说在单核 cpu 上的多任务。 但是从微观上看两个程序的指令是交织着运行的, 你的指令之间穿插着我的指令, 我的指令之间穿插着你的, 在单个周期内只运行了一个指令。 这种并发并不能提高计算机的性能, 只能提高效率。

并行(parallelism) : 指严格物理意义上的同时运行, 比如多核 cpu, 两个程序分别运行在两个核上, 两者之间互不影响, 单个周期内每个程序都运行了自己的指令, 也就是运行了两条指令。 这样说来并行的确提高了计算机的效率。 所以现在的 cpu 都是往多核方面发展。

1.1 进程

1.1.1 进程的五种基本状态

1) 创建状态: 进程正在被创建

2) 就绪状态: 进程被加入到就绪队列中等待 CPU 调度运行

3) 执行状态: 进程正在被运行

4) 等待阻塞状态: 进程因为某种原因, 比如等待 I/O, 等待设备, 而暂时不能运行。

5) 终止状态: 进程运行完毕

进程状态转换图
在这里插入图片描述
阻塞与就绪

1) 活动阻塞: 进程在内存, 但是由于某种原因被阻塞了。

2) 静止阻塞: 进程在外存, 同时被某种原因阻塞了。

3) 活动就绪: 进程在内存, 处于就绪状态, 只要给 CPU 和调度就可以直接运行。

4) 静止就绪: 进程在外存, 处于就绪状态, 只要调度到内存, 给 CPU 和调度就可以运行。

1.1.2 进程间通信的方式

进程间通信主要包括管道、 系统 IPC(包括消息队列、 信号量、 信号、 共享内存等) 、 以及套接字 socket。

1.管道:
管道主要包括无名管道和命名管道:管道可用于具有亲缘关系的父子进程间的通信, 有名管道除了具有管道所具有的功能外, 它还允许无亲缘关系进程间的通信。

2.消息队列
消息队列, 是消息的链接表, 存放在内核中。 一个消息队列由一个标识符(即队列 ID) 来标记。具有写权限得进程可以按照一定得规则向消息队列中添加新信息; 对消息队列有读权限得进程则可以从消息队列中读取信息;

3.信号量
信号量(semaphore) 是一个计数器, 可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步, 若要在进程间传递数据需要结合共享内存。

4.信号 signal
信号是一种比较复杂的通信方式, 用于通知接收进程某个事件已经发生。

5.共享内存(Shared Memory)
它使得多个进程可以访问同一块内存空间, 不同进程可以及时看到对方进程中对共享内存中数据得更新。 这种方式需要依靠某种同步操作, 如互斥锁和信号量等。

6.套接字 SOCKET:
socket 也是一种进程间通信机制, 与其他通信机制不同的是, 它可用于不同主机之间的进程通信。

1.2 线程

1.2.1线程间通信的方式

1.临界区:
通过多线程的串行化来访问公共资源或一段代码, 速度快, 适合控制数据访问;

2.互斥量 Synchronized/Lock:
采用互斥对象机制, 只有拥有互斥对象的线程才有访问公共资源的权限。 因为互斥对象只有一个, 所以可以保证公共资源不会被多个线程同时访问。

3.信号量 Semphare:
为控制具有有限数量的用户资源而设计的, 它允许多个线程在同一时刻去访问同一个资源, 但一般需要限制同一时刻访问此资源的最大线程数目。

4.事件(信号), Wait/Notify:
通过通知操作的方式来保持多线程同步, 还可以方便的实现多线程优先级的比较操作

1.2.2 线程间的同步方式

1.信号量
信号量是一种特殊的变量, 可用于线程同步。 它只取自然数值, 并且只支持两种操作:
P(SV):如果信号量 SV 大于 0, 将它减一; 如果 SV 值为 0, 则挂起该线程。
V(SV): 如果有其他进程因为等待 SV 而挂起, 则唤醒, 然后将 SV+1; 否则直接将 SV+1。

2.互斥量
又称互斥锁, 主要用于线程互斥, 不能保证按序访问, 可以和条件锁一起实现同步。当进入临界区时, 需要获得互斥锁并且加锁; 当离开临界区时, 需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。

3.条件变量
又称条件锁, 用于在线程之间同步共享数据的值。 条件变量提供一种线程间通信机制:当某个共享数据达到某个值时, 唤醒等待这个共享数据的一个/多个线程。 此时操作共享变量时需要加锁。

1.2.3 线程切换

线程在切换的过程中需要保存当前线程 Id、 线程状态、 堆栈、 寄存器状态等信息。 其中寄存器主要包括 SP PC EAX 等寄存器, 其主要功能如下:
SP:堆栈指针, 指向当前栈的栈顶地址
PC:程序计数器, 存储下一条将要执行的指令
EAX:累加寄存器, 用于加法乘法的缺省寄存器

1.2.4 线程锁

什么是线程锁机制

多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误!所以,不使用线程锁, 可能导致错误

线程锁主要用来给方法、代码块加锁。当某个方法或者代码块使用锁时,那么在同一时刻至多仅有有一个线程在执行该段代码。当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。但是,其余线程是可以访问该对象中的非加锁代码块的。

进程锁也是为了控制同一操作系统中多个进程访问一个共享资源,只是因为程序的独立性,各个进程是无法控制其他进程对资源的访问的,但是可以使用本地系统的信号量控制(操作系统基本知识)。

分布式锁:当多个进程不在同一个系统之中时,使用分布式锁控制多个进程对资源的访问。

4种锁机制

互斥锁: mutex, 用于保证在任何时刻, 都只能有一个线程访问该对象。 当获取锁操作失败时, 线程会进入睡眠, 等待锁释放时被唤醒

读写锁: rwlock, 分为读锁和写锁。 处于读操作时, 可以允许多个线程同时获得读操作。 但是同一时刻只能有一个线程可以获得写锁。 其它获取写锁失败的线程都会进入睡眠状态, 直到写锁释放时被唤醒。 注意: 写锁会阻塞其它读写锁。 当有一个线程获得写锁在写时, 读锁也不能被其它线程获取; 写者优先于读者(一旦有写者, 则后续读者必须等待, 唤醒时优先考虑写者) 。适用于读取数据的频率远远大于写数据的频率的场合。

自旋锁: spinlock, 在任何时刻同样只能有一个线程访问对象。 但是当获取锁操作失败时,不会进入睡眠, 而是会在原地自旋, 直到锁被释放。 这样节省了线程从睡眠状态到被唤醒期间的消耗, 在加锁时间短暂的环境下会极大的提高效率。 但如果加锁时间过长, 则会非常浪费 CPU资源。

RCU: 即 read-copy-update, 在修改数据时, 首先需要读取数据, 然后生成一个副本, 对副本进行修改。 修改完成后, 再将老数据 update 成新的数据。 使用 RCU 时, 读者几乎不需要同步开销, 既不需要获得锁, 也不使用原子指令, 不会导致锁竞争, 因此就不用考虑死锁问题了。 而对于写者的同步开销较大, 它需要复制被修改的数据, 还必须使用锁机制同步并行其它写者的修改操作。 在有大量读操作, 少量写操作的情况下效率非常高。

多线程的同步与锁机制

同步的时候用一个互斥量, 在访问共享资源前对互斥量进行加锁, 在访问完成后释放互斥量上的锁。 对互斥量进行加锁以后, 任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。 如果释放互斥锁时有多个线程阻塞, 所有在该互斥锁上的阻塞线程都会变成可运行状态, 第一个变为运行状态的线程可以对互斥量加锁, 其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。 在这种方式下, 每次只有一个线程可以向前执行。

单核机器上写多线程程序, 是否需要考虑加锁?

在单核机器上写多线程程序, 仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序, 仍然存在线程同步的问题。 因为在抢占式操作系统中, 通常为每个线程分配一个时间片, 当某个线程时间片耗尽时, 操作系统会将其挂起, 然后运行另一个线程。如果这两个线程共享某些数据, 不使用线程锁的前提下, 可能会导致共享数据修改引起冲突。

1.3 进程和线程的区别

1.线程依赖于进程而存在。一个线程只能属于一个进程, 而一个进程可以有多个线程, 但至少有一个线程。

2.进程在执行过程中拥有独立的内存单元, 而多个线程共享进程的内存。
(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段,数据段,堆存储 。 但是每个线程拥有自己的栈段, 用来存放所有局部变量和临时变量)

3.进程是资源分配的最小单位, 线程是 CPU 调度的最小单位;

4.进程间不会相互影响;线程一个线程挂掉将导致整个进程挂掉

5.进程在创建、 切换和销毁时开销比较大, 而线程比较小。
(由于在创建或撤消进程时, 系统都要为之分配或回收资源, 如内存空间、 I/o 设备等。 因此, 操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。 类似地,在进行进程切换时,涉及到整个当前进程 CPU 环境的保存以及新被调度运行的进程的 CPU 环境的设置。 而线程切换只须保存和设置少量寄存器的内容, 并不涉及存储器管理方面的操作。 可见,进程切换的开销也远大于线程切换的开销。)

6.进程间通信比较复杂, 而同一进程的线程由于共享代码段和数据段, 所以通信比较容易。

有了进程,为什么还要线程?

进程可以使多个程序能并发执行, 以提高资源的利用率和系统的吞吐量; 但是其具有一些缺点:
进程在同一时间只能干一件事;进程在执行的过程中如果阻塞, 整个进程就会挂起, 即使进程中有些工作不依赖于等待的资源, 仍然不会执行。

因此, 操作系统引入了比进程粒度更小的线程, 作为并发执行的基本单位, 从而减少程序在并发执行时所付出的时空开销, 提高并发性。 和进程相比, 线程的优势如下:所需资源小,切换效率高,通信方便。

而且,一个既长又复杂的进程可以考虑分为多个线程, 成为几个独立或半独立的运行部分, 这样的程序才会利于理解和修改。

多线程和多进程的区别

进程是资源分配的最小单位, 而线程是 CPU 调度的最小单位。 多线程之间共享同一个进程的地址空间, 线程间通信简单, 同步复杂, 线程创建、 销毁和切换简单, 速度快, 占用内存少, 适用于多核分布式系统, 但是线程间会相互影响, 一个线程意外终止会导致同一个进程的其他线程也终止, 程序可靠性弱。 而多进程间拥有各自独立的运行地址空间, 进程间不会相互影响, 程序可靠性强, 但是进程创建、 销毁和切换复杂, 速度慢, 占用内存多, 进程间通信复杂, 但是同步简单, 适用于多核、 多机分布。

多进程模型, 适用于 CPU 密集型。 同时, 多进程模型也适用于多机分布式场景中, 易于多机扩展。

多线程模型也适用于单机多核分布式场景。

阻塞和非阻塞: 调用者在事件没有发生的时候, 一直在等待事件发生, 不能去处理别的任务这是阻塞。 调用者在事件没有发生的时候, 可以去处理别的任务这是非阻塞。

同步和异步: 调用者必须循环自去查看事件有没有发生, 这种情况是同步。 调用者不用自己去查看事件有没有发生, 而是等待着注册在事件上的回调函数通知自己, 这种情况是异步。

1.4 死锁

死锁发生的条件

死锁是指两个或两个以上进程在执行过程中, 因争夺资源而造成的下相互等待的现象。 死锁发生的四个必要条件如下:

1.互斥条件: 一个资源每次只能被一个进程使用。

2.请求与保持条件: 一个进程因请求资源而阻塞时, 对已获得的资源保持不放。

3.不剥夺条件:进程已获得的资源, 在末使用完之前, 不能强行剥夺。

4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

解决死锁的方法
即破坏上述四个条件之一, 主要方法如下:

资源一次性分配, 从而剥夺请求和保持条件

可剥夺资源: 即当进程新的资源未得到满足时, 释放已占有的资源, 从而破坏不可剥夺的条件

资源有序分配法: 系统给每类资源赋予一个序号, 每个进程按编号递增的请求资源, 释放则相反, 从而破坏环路等待的条件

2. 内存管理

2.1 虚拟内存

为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺, 采用了虚拟内存。虚拟内存技术使得不同进程在运行过程中, 它所看到的是自己独自占有了当前系统的内存。所有进程共享同一物理内存, 每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。

2.1.1 虚拟内存的优缺点

虚拟内存的好处

1.扩大地址空间;

2.内存保护: 每个进程运行在各自的虚拟内存地址空间, 互相不能干扰对方。 虚存还对特定的内存地址提供写保护, 可以防止代码或数据被恶意篡改。

3.公平内存分配。 采用了虚存之后, 每个进程都相当于有同样大小的虚存空间。

4.当进程通信时, 可采用虚存共享的方式实现。

5.当不同的进程使用同样的代码时, 比如库文件中的代码, 物理内存中可以只存储一份这样的代码, 不同的进程只需要把自己的虚拟内存映射过去就可以了, 节省内存

虚拟内存的代价

1.虚存的管理需要建立很多数据结构, 这些数据结构要占用额外的内存

2.虚拟地址到物理地址的转换, 增加了指令的执行时间。

3.页面的换入换出需要磁盘 I/O, 这是很耗时的

4.如果一页中只有一部分数据, 会浪费内存。

2.1.2 操作系统中程序的内存结构

在这里插入图片描述

一个可执行程序在存储(没有调入内存) 时分为代码段、 数据区和未初始化数据区三部分。

BSS 段(未初始化数据区) : 通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。 BSS 段属于静态分配, 程序结束后静态变量资源由系统自动释放。

数据段: 存放程序中已初始化的全局变量的一块内存区域。 数据段也属于静态内存分配

代码段: 存放程序执行代码的一块内存区域。 这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。

可执行程序在运行时又多出两个区域: 栈区和堆区。

栈区: 存放函数的参数值、 局部变量等,由编译器自动释放。 栈区是从高地址位向低地址位增长的, 是一块连续的内存区域, 最大容量是由系统预先定义好的, 申请的栈空间超过这个界限时会提示溢出, 用户能从栈中获取的空间较小。

堆区: 用于动态分配内存, 位于 BSS 和栈中间的地址区域。 由程序员申请分配和释放。 堆是从低地址位向高地址位增长, 采用链式存储结构。 频繁的 malloc/free 造成内存空间的不续,产生碎片。 当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。 因此堆的效率比栈要低的多。

** 堆和栈的区别**
(1)堆中的内存需要手动申请和手动释放,用于动态分配内存; 栈中内存是由 OS 自动申请和自动释放, 存放着参数、局部变量等内存
(2)堆是由低地址向高地址扩展; 栈是由高地址向低地址扩展
(3)堆中频繁调用 malloc 和 free,会产生内存碎片, 降低程序效率; 而栈由于其先进后出的特性, 不会产生内存碎片
(4)堆的分配效率较低, 而栈的分配效率较高

栈的效率高的原因
栈是操作系统提供的数据结构, 计算机底层对栈提供了一系列支持: 分配专门的寄存器存储栈的地址, 压栈和入栈有专门的指令执行; 而堆是由 C/C++函数库提供的, 机制复杂, 需要一系列分配内存、 合并内存和释放内存的算法, 因此效率较低。

栈溢出概念
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数, 因而导致栈中与其相邻的变量的值被改变。
栈溢出的原因:
(1) 局部数组过大。 局部变量是存储在栈中的,数组过大时有可能导致栈溢出。 解决这类问题的办法有两个, 一是增大栈空间,二是改用动态分配,使用堆(heap) 而不是栈(stack) 。
(2)递归调用层次太多。 递归函数在运行时会执行压栈操作, 当压栈次数太多时, 也会导致堆栈溢出。
(3)指针或数组越界。 这种情况最常见, 例如进行字符串拷贝, 或处理用户输入等等

2.1.3 页式内存管理

页式内存管理, 内存分成固定长度的一个个页片。 操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构, 叫页表, 页表中的每一项都记录了这个页的基地址。 通过页表, 由逻辑地址的高位部分先找到逻辑地址对应的页基地址, 再由页基地址偏移一定长度就得到最后的物理地址, 偏移的长度由逻辑地址的低位部分决定。

步骤
逻辑空间等分为页;并从0开始编号
内存空间等分为块,与页面大小相同;从0开始编号
分配内存时,以块为单位将进程中的若干个页分别装入到多个可以不相邻接的物理块中。

优点
比较灵活, 内存管理以较小的页为单位, 不要求进程的程序段和数据在内存中连续存放,从而有效地解决了碎片问题。而且,动态页式管理提供了内外存统一管理的虚存实现,方便内存换入换出,扩充了地址空间。

缺点
增加了系统开销,例如缺页中断处理;
要求有相应的硬件支持,例如地址变换机构;
如果请求调页的算法选择不当,有可能产生抖动现象。

置换算法
当访问一个内存中不存在的页, 并且内存已满, 则需要从内存中调出一个页或将数据送至磁盘对换区, 替换一个页, 这种现象叫做缺页置换。

常用的置换算法有:随机淘汰法、轮转法、先进先出法、最近最久未用页面淘汰算法、最近最少用页面淘汰法。

缺页中断
malloc()和 mmap()等内存分配函数, 在分配时只是建立了进程虚拟地址空间, 并没有分配虚拟内存对应的物理内存。 当进程访问这些没有建立映射关系的虚拟内存时, 处理器自动触发一个缺页异常。

缺页中断: 在请求分页系统中, 可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。 每当所要访问的页面不在内存是, 会产生一次缺页中断, 此时操作系统会根据页表中的外存地址在外存中找到所缺的一页, 将其调入内存。

缺页本身是一种中断, 与一般的中断一样, 需要经过 4 个处理步骤:
1、 保护 CPU 现场
2、 分析中断原因
3、 转入缺页中断处理程序进行处理
4、 恢复 CPU 现场, 继续执行

2.1.4 段式内存管理

段式内存管理,把把程序分成若干个段,每段对应一个程序模块,有完整的逻辑意义。段式管理中以段为单位分配内存,每段分配一个连续的内存区。由于各段长度不等,所以这些存储区的大小不一。

优点
提供了内外存同一管理的虚存实现;
段长可根据需要动态增加;
便于对具有完整逻辑功能的信息段进行共享。

缺点
需要更多的硬件支持;
处理碎片比较麻烦;
给系统管理带来一定的难度和开销。

页式与段式的区别

将程序分页时,页的大小是固定的,只根据页面大小大小死生生的将程序切割开;而分段时比较灵活,只有一段程序有了完整的意义才将这一段切割开。(例如将一个人每隔50厘米切割一段,即为分页;而将一个人分割为头部、身体、腿部(有完整逻辑意义)三段,即为分段)

段页式内存管理
结合段式管理和页式管理的优势,将每段程序分成若干页,虚拟空间的最小单位是页,内存空间也被划分为若干大小相等的页面,且每段所拥有的的程序和数据在内存中可以分开存放。

2.2 字节对齐

2.2.1 什么是字节对齐

现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。

2.2.2 对齐的原因和作用

1) 平台原因(移植原因) : 不是所有的硬件平台都能访问任意地址上的任意数据的; 某些硬件平台只能在某些地址处取某些特定类型的数据, 否则抛出硬件异常。

2) 性能原因: 数据结构(尤其是栈) 应该尽可能地在自然边界上对齐。 原因在于, 为了访问未对齐的内存, 处理器需要作两次内存访问; 而对齐的内存访问仅需要一次访问。

因此,通过合理的内存对齐可以提高访问效率。为使CPU能够对数据进行快速访问,数据的起始地址应具有“对齐”特性。比如4字节数据的起始地址应位于4字节边界上,即起始地址能够被4整除。

2.2.3 对齐准则

先来看四个重要的基本概念:

1.数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节,即数据成员自身的长度。

2.结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。

3.指定对齐值:#pragma pack (value)时的指定对齐值value。

4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值N=min{自身对齐值,当前指定的pack值}。

其中,有效对齐值N是最终用来决定数据存放地址方式的值。有效对齐N表示“对齐在N上”,即该数据的存放起始地址是N的整数倍。

而数据结构中的数据变量都是按定义的先后顺序存放。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐存放,结构体本身也要根据自身的有效对齐值圆整(即结构体成员变量占用总长度为结构体有效对齐值的整数倍)。

以下面的结构体为例进行分析

struct B{
    
    
   char  b;
   int   a;
   short c;
};

假设B从地址空间0x0000开始存放,且指定对齐值默认为4(4字节对齐)。成员变量b的自身对齐值是1,比默认指定对齐值4小,所以其有效对齐值为1,其存放地址0x0000符合0x0000%1=0。成员变量a自身对齐值为4,所以有效对齐值也为4,只能存放在起始地址为0x0004~0x0007四个连续的字节空间中,符合0x0004%4=0且紧靠第一个变量。变量c自身对齐值为 2,所以有效对齐值也是2,可存放在0x0008~0x0009两个字节空间中,符合0x0008%2=0。所以从0x0000~0x0009存放的都是B内容。

再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0000~\0x0009=10字节,(10+2)%4=0。所以0x0000A~0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12。之所以编译器在后面补充2个字节,是为了实现结构数组的存取效率。

结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:

1)结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

2)结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。

运行A a = new A; a->i = 10;在内核中的内存分配上发生了什么?*

1) A *a: a 是一个局部变量, 类型为指针, 故而操作系统在程序栈区开辟 4/8 字节的空间(0x000m) , 分配给指针 a。
2) new A: 通过 new 动态的在堆区申请类 A 大小的空间(0x000n) 。
3)a = new A: 将指针 a 的内存区域填入栈中类 A 申请到的空间地址。 即*(0x000m)=0x000n。
4) a->i: 先找到指针 a 的地址 0x000m, 通过 a 的值 0x000n 和 i 在类 a 中偏移 offset, 得到 a->i 的地址 0x000n + offset, 进行*(0x000n + offset) = 10 的赋值操作, 即内存 0x000n + offset 的值是 10。

2.3 内存溢出与内存泄露

2.3.1 内存溢出

指程序申请内存时, 没有足够的内存供申请者使用。 内存溢出就是你要的内存空间超过了系统实际分配给你的空间, 此时系统相当于没法满足你的需求, 就会报内存溢出的错误。

内存溢出原因

内存中加载的数据量过于庞大, 如一次从数据库取出过多数据

集合类中有对对象的引用, 使用完后未清空, 使得不能回收

代码中存在死循环或循环产生过多重复的对象实体

使用的第三方软件中的BUG

启动参数内存值设定的过小

2.3.2 内存泄露

内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。 内存泄漏并非指内存在物理上的消失, 而是应用程序分配某段内存后, 由于设计错误, 失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的分类

1、 堆内存泄漏 (Heap leak) 。 对内存指的是程序运行中根据需要分配通过 malloc, realloc, new等从堆中分配的一块内存, 再是完成后必须通过调用对应的 free 或者 delete 删掉。 如果程序的设计的错误导致这部分内存没有被释放, 那么此后这块内存将不会被使用, 就会产生 Heap Leak。

2、系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉, 导致系统资源的浪费, 严重可导致系统效能降低, 系统运行不稳定。

3、 没有将基类的析构函数定义为虚函数。 当基类指针指向子类对象时, 如果基类的析构函数不是 virtual, 那么子类的析构函数将不会被调用, 子类的资源没有正确是释放, 因此造成内存泄露。

3. 系统调用

系统调用(英语: system call) , 又称为系统呼叫, 指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。 系统调用提供了用户程序与操作系统之间的接口。

操作系统中的状态分为内核态和用户态 。 大多数系统交互式操作需求在内核态执行。 如设备 IO 操作或者进程间通信。 用户程序只在用户态下运行, 有时需要访问系统核心功能, 这时通过系统调用接口使用系统调用。

应用程序有时会需要一些危险的、 权限很高的指令, 如果把这些权限放心地交给用户程序是很危险的(比如一个进程可能修改另一个进程的内存区, 导致其不能运行), 但是又不能完全不给这些权限。 于是有了系统调用, 危险的指令被包装成系统调用, 用户程序只能调用而无权自己运行那些危险的指令。 另外, 计算机硬件的资源是有限的, 为了更好的管理这些资源, 所有的资源都由操作系统控制, 进程只能向操作系统请求这些资源。 操作系统是这些资源的唯一入口, 这个入口就是系统调用。

操作系统为什么要分内核态和用户态

为了安全性。 在 cpu 的一些指令中, 有的指令如果用错, 将会导致整个系统崩溃。 分了内核态和用户态后, 当用户需要操作这些指令时候, 内核为其提供了 API, 可以通过系统调用陷入内核,让内核去执行这些操作。

用户态切换到内核态的 3 种方式

1、 系统调用
这是用户进程主动要求切换到内核态的一种方式, 用户进程通过系统调用申请操作系统提
供的服务程序完成工作。 而系统调用的机制核心还是使用了操作系统为用户特别开放的一个中断来实现。

2、 异常
当 CPU 在执行用户态的程序时, 发现了某些事件不可知的异常, 这是会触发由当前进程切换到处理此异常的内核相关程序中, 也就到了内核态, 比如缺页异常。

3、 外围设备的中断
当外围设备完成用户请求的操作之后, 会向 CPU 发出相应的中断信号, 这时 CPU 会暂停执行下一条将要执行的指令, 转而去执行中断信号的处理程序, 如果先执行的指令是用户态下的程序, 那么这个转换的过程自然也就发生了有用户态到内核态的切换。 比如硬盘读写操作完成, 系统会切换到硬盘读写的中断处理程序中执行后续操作等。

源码到可执行文件的过程

1) 预编译
主要处理源代码文件中的以“#” 开头的预编译指令。 例如:将宏定义展开,处理“#include”预编译指令,将文件内容替换到它的位置;删除所有的注释等。

2) 编译
把预编译之后生成的文件, 进行一系列词法分析、 语法分析、 语义分析及优化后, 生成相应的汇编代码文件。

3) 汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。经汇编之后, 产生目标文件 (与可执行文件格式几乎一样)xxx.o(Windows 下)、 xxx.obj(Linux 下)。

4) 链接
将不同的源文件产生的目标文件进行链接, 从而形成一个可以执行的程序。

参考文献
牛客校招面试题
https://www.cnblogs.com/clover-toeic/p/3853132.html

猜你喜欢

转载自blog.csdn.net/qq_42820853/article/details/107301357