准备紫光展锐时收集的面经
没做完全,也分享出来,会有点用吧
堆和栈
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
-
栈内存是由高地址向低地址分配的连续内存块,栈的大小一般是2M或者10M;堆内存是由低地址向高地址分配的非连续内存块,这是由于系统使用链表来存储空闲的内存地址;堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。
-
栈内存的分配释放速度快,效率高,原因是其分配的内存是连续的;
-
堆内存的分配释放相对于栈内存小效率低一些,原因是其内存不一定是连续的,容易产生碎片,但是其灵活性好。
vector删除机制
vector 还维持着一个last指针,开始的时候=end,随着删除,last前移,最终vector的size是last-begin,或者我们可以认为end值改变了,但最初传入的end没有变。
实际上是 erase 函数,对不同类型的容器,内部却做了截然不同的事情。
erase函数的原型如下:
string& erase ( size_t pos = 0, size_t n = npos ); iterator erase ( iterator position ); iterator erase ( iterator first, iterator last );
(1)erase(pos,n); 删除从pos开始的n个字符,比如erase(0,1)就是删除第一个字符
(2)erase(position);删除position处的一个字符(position是个string类型的迭代器)
(3)erase(first,last);删除从first到last之间的字符(first和last都是迭代器)
erase函数可以用于删除vector容器中的一个或者一段元素,在删除一个元素的时候,其参数为指向相应元素的迭代器,而在删除一段元素的时候,参数为指向一段元素的开头的迭代器以及指向结尾元素的下一个元素的迭代器。
在进行单个元素删除后,传入的迭代器指向不变,仍然指向被删除元素的位置,而被删除元素之后的所有元素都向前移动一位,也就是该迭代器实际上是指向了原来被删除元素的下一个元素。
【remove 函数】
iterator remove(iterator first, iterator last,val);
remove函数会将范围内所有等于val的值移动位置。
STL中remove()只是将待删除元素之后的元素移动到vector的前端,并相应的调整 last 指针,而不是删除。若要真正删除,需要搭配使用erase()。
static关键字
volatile关键字
C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量。
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。
volatile int i=10 int a = i; ... // 其他代码,并未明确告诉编译器,对 i 进行过操作 int b = i;
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从 i 的地址读取数据放在 b 中。
volatile 可以保证对特殊地址的稳定访问,防止被编译器优化
(优化:由于编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。)
【应用】
-
中断服务程序中修改的供其它程序检测的变量需要加 volatile;
-
多任务环境下各任务间共享的标志应该加 volatile;
-
存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;
【多线程下的 volatile】
C/C++多线程编程中不要指望 volatile 解决多线程竞争问题。
解决多线程的数据竞争问题应该使用原子操作或者互斥锁。
【volatile 和 const】
const 和 volatile 可以同时使用,volatile 作用是避免编译器优化,说明它是随时会变的,它不是 non-const,和 const 不矛盾。
const 修饰的变量只是在当前作用范围无法修改,但是可能被其它程序(如中断程序)修改。
volatile 标识一个变量意味着这个变量可能被非本程序的其他过程改变,例如某个访问这一变量的某中断程序。
final关键字
final 用来修饰类,让该类不能被继承。(绝代!终结!)
final 用来修饰类的虚函数,使得该虚函数在子类中,不能被重写。(即,使得该功能终结!)
哪些函数不能使用 virtual
虚函数,是一种特殊的成员函数,用来实现运行时多态。virtual 函数一定要通过对象来调用,有隐藏的this指针。
-
构造函数不能用;——不符合逻辑:构造函数调用时,Vtable没有建立,当然不能使用虚函数。
-
静态(static)函数不能用;—— static成员没有 this 指针。
-
virtual成员函数的关键是动态类型绑定的实例调用。然而,静态函数和任何类的实例都不相关,它是class的属性。
-
-
内联(inline)函数
进程和线程的区别
进程:是程序运行的一个实例,包括程序计数器、寄存器、变量等。
线程:一个进程中的更小粒度的执行单元。
【区别】
一个线程从属于一个进程,一个进程包含多个线程,实现并发执行任务。
线程是CPU资源调度的最小单位,进程是系统资源调度的最小单位。
线程需要的系统资源更少,线程的开销比进程要少。
进程在执行期间拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段。
线程除了共享这些内存,还拥有只属于自己的栈段、寄存器组。
进程切换时需要刷新快表获取新的地址空间,然后切换硬件上下文和内核栈;线程切换只需要切换 硬件上下文和内核栈。
什么时候用进程,什么时候用线程
多进程的优点:
①编程相对容易;通常不需要考虑锁和同步资源的问题。 ②更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。 ③有内核保证的隔离:数据和错误隔离。 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)。
多进程应用场景
-
nginx:主流的工作模式是多进程模式(也支持多线程模型)
-
几乎所有的 web server 服务器服务都有多进程的,至少有一个守护进程配合一个worker进程,例如apached,httpd等等以d结尾的进程包括init.d本身就是0级总进程,所有你认知的进程都是它的子进程;
-
chrome浏览器:也是多进程方式。 (原因:①可能存在一些网页不符合编程规范,容易崩溃,采用多进程一个网页崩溃不会影响其他网页;而采用多线程会。②网页之间互相隔离,保证安全,不必担心某个网页中的恶意代码会取得存放在其他网页中的敏感信息。)
-
redis:也可以归类到“多进程单线程”模型(平时工作是单个进程,涉及到耗时操作如持久化或aof重写时会用到多个进程)
多线程的优点:
①创建速度快,方便高效的数据共享 共享数据:多线程间可以共享同一虚拟地址空间;多进程间的数据共享就需要用到共享内存、信号量等IPC技术。 ②较轻的上下文切换开销 - 不用切换地址空间,不用更改寄存器,不用刷新TLB。 ③提供非均质的服务。如果全都是计算任务,但每个任务的耗时不都为1s,而是1ms-1s之间波动;这样,多线程相比多进程的优势就体现出来,它能有效降低“简单任务被复杂任务压住”的概率。
多线程应用场景
-
线程间有数据共享,并且数据是需要修改的(不同任务间需要大量共享数据或频繁通信时)。
-
提供非均质的服务(有优先级任务处理)事件响应有优先级。
-
单任务并行计算,在非CPU Bound的场景下提高响应速度,降低时延。
-
与人有IO交互的应用,良好的用户体验(键盘鼠标的输入,立刻响应)
怎么选
①需要频繁创建销毁的优先用线程(进程的创建和销毁开销过大)
最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的。
甚至——直接都用线程池也更大程度的减少线程频繁的创建、销毁开销。
②需要进行大量计算的优先使用线程(CPU频繁切换)
耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
③强相关的处理用线程,弱相关的处理用进程
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
④可能要扩展到多机分布的用进程,多核分布的用线程
⑤都满足需求的情况下,用你最熟悉、最拿手的方式
实际应用中基本上都是“进程+线程”的结合方式
进程通信方式的区别(共享内存、消息队列优缺点)
进程间通信就是两个 main 函数交互的过程,在程序之外开辟一片开阔的空间供两个程序交互使用根据这个区域的数据结构及开辟的方法有了消息队列、信号量、共享内存这三种方法。此外还有管道、信号两种方法。
【管道】
半双工。
pipe 函数创建:\#include <unistd.h> , int pipe(int filedes[2]);
调用 pipe 函数时在内核中开辟一块缓冲区(称为管道)用于通信,一个读端一个写端,然后通过 filedes 参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端。pipe 函数调用成功返回0, 调用失败返回-1。
【信号量semaphore】
一个共享资源的计数器,用来控制对共享资源的访问。PV操作实现。
【信号】
linux 系统内核定义的,用于通知接收进程某个事件的发生。
【消息队列】
先进先出,使用双向链表结构实现。用三个双链表来记录消息的本身、发送者和接收者。
操作:1、打开(获取)或创建消息队列;2、读写操作;3、获得或设置消息队列的属性。
linux 系统支持的最大消息队列数由msgque数组大小来决定。
常见应用场景:解耦、异步、削峰
优点:
-
独立于发送与接收进程;
-
进程终止,消息队列及其内容仍然存在;
-
可以实现消息的随机查询,可以按照消息类型读取。
缺点:
-
代码实现复杂度高;
-
系统可用性降低。
【共享内存】
享内存是在物理内存上开辟了一块空间。
不同的进程可以通过页表映射将开辟的物理空间内存映射到自己的共享区。
不同的进程在代码当中操作自己进程内存中的共享区的虚拟地址,达到操作物理内存的目的,从而实现不同进程之间的通信。
优点:
-
函数的接口也简单;
-
数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率;
-
也不像匿名管道那样要求通信的进程有一定的父子关系
缺点:
-
共享内存没有提供同步的机制,互斥访问需要额外的实现。
线程通信方式
【互斥量】
【信号量】
【条件变量】
【读写锁】
接口和抽象类的区别
【相同点】
接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
【区别】
抽象类表示是一个(IS-A)关系的抽象,接口表示能(CAN-DO)关系的抽象。
抽象类是不能实例化的类,但是其中的方法可以包含具体实现代码。
抽象类的使用动机是在不允许实例化的限制下复用代码
抽象类是本体的抽象,接口是行为的抽象
接口是一组方法声明的集合,其中应仅包含方法的声明,不能有任何实现代码。
接口又可以看作一组规则的集合,它是对调用者的保证,对被调用者的约束。
设计模式(单例和工厂)
数据库优化
MFC框架
对Linux系统的了解
驱动相关的问题;
循环队列的实现
可分为静态队列和链式队列。静态队列一般使用数组实现,数组需要预先定义内存大小,为了避免内存浪费,一般使用循环队列。
rtos的原理以及如何内存划分
RTOS 是实时操作系统。是一种通用的任务管理框架,用于控制任务的运行和任务之间的交互,保证任务或事件能够得到实时处理。
【原理】
将 CPU 通过虚拟化(分时复用),实现每个 Task 认为自己在独占CPU,提升CPU 利用率,避免绝大多数的等待,不可避免时让CPU 进入低功耗。
任务函数无需返回,简化代码。
通过RTOS控制任务的运行时机,事件处理的实时性得到有效保证。
【内存分配】
一般具有两种内存的分配方式,把它归纳为块内存和池内存。
-
块内存在每个内存池中分配的内存大小相同,由于应用需要不同大小的内存块,所以具有不同大小内存块的内存块池,如果所需的内存大小小于内存池中所有内存块的大小,则容易造成浪费,但是分配的效率较高;
-
池内存,初始化时是一个整体的内存块,分配时可以按需在该内存块中分配不同大小的内存,所以在分配一段时间后该原来一个整体的内存块变成某些部分分配,某些部分空闲,空闲的大小也不一致,容易造成内存碎片。
Linux 内存分配
用户态:malloc、calloc、realloc
malloc/calloc/realloc都是默认8字节对齐分配的
/* 分配SIZE字节的内存 */ void *malloc(size_t size); /* 分配NMEMB个元素,每个元素SIZE字节的内存,并全部初始化为0 */ void *calloc (size_t __nmemb, size_t __size); /* 将之前分配的PTR指针从新分配,使新的块为SIZE字节 */ void *realloc (void *__ptr, size_t __size); /* 释放由上述三个函数分配的内存块 */ void free(void *ptr);
在<malloc.h>中还有一个cfree函数,可以用来释放calloc分配的内存,man手册中写的是支持的平台不同,我们使用的时候就统一用free就好啦。
内核态
//获得页 struct page *alloc_pages(gfp_t gfp_mask, unsigned int order); //释放页 void free_pages(unsigned long addr, unsigned int order); //按字节获取内核内存,该内存在物理内存上是连续的 void *kmalloc(size_t size, gfp_t flags); void kfree(const void *); //按字节获取内核内存,该内存只在虚拟内存上是连续的 //主要用于获取大块内存 void *vmalloc(unsigned long size); void vfree(const void *addr);
和 new 的区别
malloc 与 free 是c++/c语言的标准函数,new/delete 是C++的运算符。
new/delete 比 malloc/free 更加智能,底层也是的 malloc/free。
malloc 实质
malloc 函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表(OS的堆空间)。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。
调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。
malloc 向 堆申请空间:
从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
free()到底释放了什么
free()释放的是指针指向的内存!不是指针!
并且注意:释放不可能释放一部分!且要确保指针是指向可用空间的首地址。
malloc 申请的空间有多大?
2个部分。
malloc()申请的空间实际分了两个不同性质的空间。一个就是用来记录管理信息的空间,另外一个就是用户实际可用空间了。而用来记录管理信息的实际上是一个结构体。
Linux系统的启动流程
-
BIOS启动引导阶段;
-
上电开机。主板BIOS运行上电自检(POST,Power on self test)代码,检测系统外围设备(CPU、内存、显卡、I/O);
-
系统启动自举程序,根据BIOS中设置的启动顺序搜索启动驱动器(硬盘、光驱、网络服务器等),获取第一个启动设备的代号,读取第一个启动设备的 MBR 的引导加载程序的启动信息,从 MBR 装载启动引导管理器(GRUB)并运行该启动引导管理。进入下一步:
-
-
GRUB启动引导阶段;
-
内核阶段;
-
init初始化阶段。
说说UART要配置什么参数,IIC,SPI 2.UART怎么传输浮点数,如果这个浮点数很大怎么办? \6. IIC总线协议原理,描述时序图,上拉电阻与下拉电阻的作用,IIC总线最多能挂载多少个设备; \7. SPI协议相关;
c语言中的malloc怎么使用,有哪些需要注意的,与calloc,realloc又有什么区别,与new有什么区别?
有进行过linux系统裁剪吗?用到哪些命令
shell脚本了解吗?说说怎么查找当前目录及其子目录下的某个文件
uboot的启动流程