Skip to content

面经

自我介绍

面试官您好,很高兴能够参加这场面试,我叫刘志鸿,目前就读于南昌航空大学,所学专业是计算机科学与技术,本科毕业于江西农业大学,GPA4.0,获得多次专业奖学金和“校级优秀学生”荣誉。研究生阶段,我参加了几个实验室的项目,包括地理信息服务器和遥感影像切片系统,地理信息服务器主要是为仿真客户端提供地理信息数据支撑以及为客户端提供计算支持,用到的技术栈包括C/C++、TCP、UDP、MySQL、Redis、Qt等。在空闲时间,我还参加了江西省数学建模竞赛和全国数学建模竞赛并担任队长分别取得了三等奖、一等奖的成绩,考取软件设计师中级证书,我的介绍差不多就这些,谢谢。

技术面反问

咱们这边的主要业务有哪些?

咱们这边一个项目大概是多少人协同开发?

咱们这边有没有什么定期的分享会或者是技术交流的渠道?

对于代码的规范、质量还有性能优化有什么样的要求和标准?

项目

项目中用到了哪些设计模式

  1. 单例模式

项目中碰到了哪些困难

LRU缓存

项目做了哪些优化

  1. KCP协议

KCP = UDP + 自己实现的可靠性与拥塞控制机制。首先在数据接收逻辑中,UDP网络数据先交付给KCP算法,然后在KCP中对数据序号进行检查并发送确认应答包ACK,如果该过程中有数据丢失则触发快速重传,等待所有数据接收完成后再将完成数据包交付给应用。其次在数据发送逻辑中,用户层数据会交付给KCP,KCP将完整数据包分割发送,在发送中跟踪各数据包的ACK响应,如果丢包则需进行重传,最终数据通过UDP传输接口对外发送数据。

  1. Hash LRU缓存

影像和高程数据总量有TB级别,无法全部加载到内存中,用户在特定时段内,只会频繁访问某块特定区域,这意味着数据访问具有很强的时间局部性和空间局部性,如果每次都从磁盘中读取数据存在I/O瓶颈。

LRU缓存缺点:当客户端大量请求地理信息数据时,如果缓存空间有限,可能会把缓存中的所有数据都换出去,导致热点数据被淘汰,当热点数据再次被访问时,由于缓存未命中,会引发大量的磁盘IO,造成缓存污染

所以采用Hash LRU缓存,目标是降低锁竞争、提升并发读写性能,为每个层级设计独立的缓存结构,22级。

采用了两个LRU链表来存储地理信息数据,并将缓存区域划分为两个部分。在该算法中,预读的页只需放置在 old 区域的头部。当这些页被真正访问时,才会将它们插入到 young 区域的头部。如果预读的页一直没有被访问,它们将从 old 区域移除,这样就不会影响到 young 区域中的热点数据。此外,为了解决缓存污染问题,该算法引入了一个时间判断机制。只有同时满足被访问和在 old 区域停留时间超过设定秒数的条件,数据才会被插入到 young 区域的头部。这样就有效地解决了缓存污染问题。

如何设计一个项目/模块

明确需求->拆解功能->选型架构->定义借口->设计数据->考虑扩展->迭代交付

gdb常用调试命令

  • run arg1 arg2 运行带参程序
  • quit 退出GDB
  • break func/file.c:10 在函数入口/文件第10行设端点
  • info breakpoints 查看所有端点
  • delete x 删除编号为x的断点
  • disable x 禁用断点x
  • enable x 启用断点x
  • clear 清除当前行断点
  • continue 执行到下一个断点
  • next 单步跳过
  • step 单步进入
  • finish 执行当前函数并返回
  • until 执行到当前循环结束或指定行
  • print var 打印变量值
  • backtrace 显示函数调用栈

valgrind内存检测工具

valgrind底层基于二进制插桩,在虚拟CPU上运行程序程序,监控每条指令的内存访问,通过追踪内存分配/释放、检查地址有效性、记录未初始化值来源检查内存泄露、越界访问等错误;

valgrind --参数 可执行文件名

可以检测的内存错误:内存泄漏、非法内存访问、未初始化的值、重复释放/释放非法指针、内存重叠拷贝

参数:

  • tool=memcheck
  • leak-check
  • show-leak-kinds

网络

在socket中主要使用了那些函数

socket可以看作是TCP/IP模型中应用层于传输层之间的接口层。

  1. 创建和关闭:socket(int domain, int type, int protocol)close(int sockfd)shutdown(int sockfd, int how)

  2. 绑定和监听:bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen)accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)

  3. 连接:connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen)

  4. 数据传输:

    • TCP常用:发送数据send(int sockfd, const void* buf, size_t len, int flags),接收数据recv(int sockfd, void* buf, size_t len, int flags)或者发送数据write(int sockfd, void* buf, size_t len),接收数据read(int sockfd, void* buf, size_t len)
    • UDP常用:发送数据sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen),接收数据recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen)
  5. I/O多路复用:select(int nfds, fd_set *readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout)poll(struct pollfd* fds, nfds_t nfds, int timeout)epoll_create/epoll_ctl_epoll_wait

  6. 套接字选项:getsockopt(int sockfd, int level, int optname, void* optval, socklen_t* optlen)setsockopt(int sockfd, int level, int optname, const void* optval, socklen_t optlen),常用选项有端口复用SO_REUSEADDR,缓冲区大小SO_RCVBUF/SO_SNDBUF,长连接检测SO_KEEPALIVE

  7. 地址转换:主机序->网络序htons/htonl,网络序->主机序ntohs/ntohl,IPv4字符串与二进制转换inet_addr/inet_ntoa,IPv4与IPv6地址转换inet_pton/inet_ntop

键入网址到网页显示期间发生流程

  1. HTTP解析URL,确定服务器地址和请求资源的路径后,生成HTTP请求信息;
  2. DNS解析查询服务器真实IP地址
  3. 三次握手建立TCP连接,如果是HTTPS,还需要进行TLS握手,并发送HTTP请求
  4. 服务器处理请求,构造HTTP响应信息
  5. 返回响应信息到浏览器
  6. 浏览器接收响应,解析响应信息并渲染页面
  7. 如果页面包含其他资源,浏览器会发起额外的HTTP请求加载这些资源

发送网络数据,涉及几次内存拷贝操作

  1. 调用发送数据的系统调用时,内核会申请一个内核态的sk_buff内存,将用户待发送的数据拷贝到sk_buff内存,并将其加入到发送缓冲区;
  2. 在TCP传输协议下,从传输层到网络层时,每个sk_buff都会克隆一个新的副本,副本sk_buff发到网络层,等待数据包发送后回ACK时,原始sk_buff才会释放;
  3. IP层如果出现sk_buff大于MTU时,会申请额外的sk_buff,将原来的 sk_buff拷贝为多个小的sk_buff

select、poll、epoll区别

  • select使用位图、有1024fd限制,每次需重置参数
  • poll使用链表、无fd限制、需要遍历所有fd
  • epoll使用红黑树+就绪链表、支持边缘/水平触发、性能不随fd数量下降

epoll如何体现多路复用

一个线程同时监控多个文件描述符的I/O事件,当任一fd就绪时,内核通知应用程序,避免每个fd创建独立线程或轮询等待

为什么握手3次,挥手4次

三次握手原因:

  • 防止重复历史连接的初始化
  • 同步双方初始序列号
  • 避免浪费资源

四次挥手原因:

  • TCP是全双工的,每个方向需要独立关闭
  • 被动方收到FIN后可能还有数据要发送,不能立即回FIN

为什么四次挥手后要等2MSL

  • 确保最后一个ACK到达对端,如果ACK丢失,会重发FIN
  • 防止旧连接数据干扰新连接:2MSL确保旧报文在网络中小时。

大量TIME_WAIT有什么危害,如何解决?

危害:占用端口和内存,导致无法建立新连接

解决方案:

  • 复用连接 HTTP Keep-Alive
  • 服务端:避免主动关闭
  • 启用TIME_WAIT复用,tcp_tw_reuse参数

ping命令

ping命令发送ICMP echo 请求报文到目标主机,等待其恢复ICMP echo 应答报文来检测网络连通性。

TCP和UDP的区别

  • 连接:TCP是面向连接的,UDP是无连接的
  • 服务对象:TCP是一对一两点服务,UDP支持一对一、一对多、多对多
  • 可靠性:TCP是可靠交付数据,UDP是尽最大努力交付,不保证可靠交付,但是可以基于UDP实现可靠传输协议
  • 拥塞控制、流量控制:TCP有拥塞控制和流量控制,UDP没有
  • 首部开销:TCP首部更长,最短20字节,UDP首部8字节
  • 传输方式:TCP流式传输,无边界,UDP是按包发送的,但可能会出现丢包和乱序。

TTL是什么,解决了什么问题

TTL是IP报头中的一个字段,限制数据包的最大跳数,防止数据报无限循环导致网络拥塞

http头部有哪些字段,项目里用到了哪些?

  • Host字段:指定服务器域名
  • Content-length字段:表明数据长度
  • Content-type字段:告诉客户端本次数据的格式

TCP流量控制和拥塞控制解决什么问题?

流量控制解决“接收方处理不过来”的问题,拥塞控制解决“网络过载”的问题,二者都通过“窗口”调节发送速率(接收窗口,拥塞窗口)

IP是解决什么问题的? 为什么要用IP地址,直接mac地址不行吗? 为什么链路层上面搞个网络层?

IP解决跨网络、异构设备寻址和路由问题

如何判断大小端字节序

定义一个整数,取首字节,看首字节地址

操作系统

内核态和用户态的区别

区别主要在于权限和可执行的操作,内核态下CPU可以执行所有的指令和访问所有的硬件资源,权限高,主要用于操作系统内核的运行;用户态下CPU只能执行部分指令集,无法直接访问硬件资源,权限低,主要用于运行用户程序。

内核态的底层操作主要包括:内存管理、进程管理、设备驱动程序控制、系统调用等,这些操作涉及到操作系统的核心功能,需要高权限执行。

为什么区分内核态和用户态?

  • 安全性:划分权限可以避免用户程序直接访问操作系统核心功能,避免对系统资源的破坏;
  • 稳定性:用户态程序出现问题不会影响到整个系统;
  • 隔离性:使内核和用户程序具有明确边界,便于系统模块化和维护;

进程的状态

  1. 创建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
  5. 结束状态

进程间通讯方式

  1. 管道
  2. 消息队列
  3. 内存共享
  4. 信号
  5. 信号量
  6. 套接字

线程间通讯方式

  • 互斥锁:对共享资源访问时,使用互斥锁获取对资源的绝对访问权,其他线程在获取该资源时会进入阻塞状态,待当前线程释放互斥锁后,其他线程才能获取资源;
  • 条件变量:线程的同步机制,线程等待“条件变量成立”而挂起,另一个线程使“条件变量成立”。条件变量通常和互斥锁一起使用。
  • 自旋锁:一种忙等待锁,线程在获取锁时不会进行阻塞状态,而是循环忙等待直到获取锁,适用于锁的持有时间短的场景,避免线程频繁切换带来开销。
  • 信号量:通常表示资源的数量,有两个原子操作的系统调用控制信号量,PV操作。
  • 读写锁:适用于能区分读写操作的场景,允许多个进程同时读,仅允许一个线程写

什么是僵尸进程,如何避免

僵尸进程指的是子进程已退出但父进程尚未调用wait或waitpid回收其退出状态的进程,它不占用内存和CPU,但会占用pid。

使用pa aux | grep 'Z'top

避免方法是父进程及时调用wait、忽略SIGCHLD信号或者让子进程被init进程收养。

孤儿进程:父进程先于子进程退出,子进程被init收养

僵尸进程:子进程先退出,父进程不wait

使用管道和套接字有什么信号是一定要处理的

SIGPIPE是唯一必须处理的信号(写入关闭读端的管道或断开连接的套接字时,内核发送SIGPIPE),其默认行为会杀死进程,必须处理,处理方式是忽略该信号signal(SIGPIPE, SIG_IGN)

在网络编程中,涉及到SIGCHLD;SIGINT/SIGTERM

为什么要有虚拟内存,使用虚拟内存有什么好处

虚拟内存就是让每个进程都以为自己独占了一大块连续的内存空间,这些“虚拟地址”是由操作系统和硬件动态映射到物理内存或磁盘交换空间上的。

好处:让进程之间内存隔离,保障安全;简化程序开发,提供连续地址空间;支持超过物理内存大小的程序运行;支持内存共享,提高资源利用率;避免外部碎片,提高内存分配效率;支持按需加载和懒分配,提升系统性能;

虚拟内存和物理内存如何映射

通过页表由内存管理单元动态映射,操作系统将虚拟地址空间划分为页,物理内存划分为页框,页表记录虚拟页对应哪个页框。

linux启动过程

BIOS/UEFI固件初始化->Bootloader加载linux内核->内核解压与初始化->init进程启动->启动系统服务->启动登录管理器->用户登录与shell启动

C在Linux上运行,会调用哪些资源

CPU时间、内存、文件描述符、进程控制块、系统调用、信号、环境变量、共享库

Linux上如何查找一个文件、查找要给字符串

用find、grep

硬中断和软中断的区别

硬中断由外部硬件设备出发的异步中断,如网卡收到数据报,CPU必须立即响应;软中断由软件指令主动触发同步终端,常用于系统调用或内核延迟任务处理。

C/C++

C与Python的区别

  1. 语言类型和执行方式角度:C是编译型语言,源代码通过预处理、编译、汇编、连接生成机器码,直接运行在硬件上,执行效率高;Python是解释性语言,由解释器逐行执行,开发效率高但运行速度慢;
  2. 内存管理角度:C需要手动管理内存,需要注意内存泄漏和野指针问题;Python有自动回收机制,安全性更高,但会带来额外的性能开销;
  3. 类型系统角度:C是静态类型语言,变量类型在编译时确定,不能随意转换,类型错误会在编译时报错;Python是动态强类型语言,变量类型在运行时确定,支持灵活赋值,但类型错误在运行时才暴露;
  4. 应用领域角度:C主要应用于操作系统、嵌入式系统、驱动开发、游戏引擎、高性能计算等底层或性能敏感领域;Python主要用于Web开发、数据分析、人工智能、脚本自动化等
  5. 生态和库支持角度:C标准库较小,第三方库少,许多功能需要自己实现或者依赖系统API;Python有强大的第三方库生态;

new/delete和malloc/free区别

new/delete是C++的操作符,malloc/free是C语言的标准库函数;调用new/delete的时候会自动调用对象的构造/析构函数,而malloc/free只分配/释放内存,不调用构造/析构函数;new/delete返回对应类型的指针,而malloc/free返回无类型指针void*,需要强制类型转换;malloc需要手动传入字节数,new会自动计算大小;malloc失败时返回NULL,new失败返bac_alloc异常

malloc最大能申请多大的内存

理论上32位系统能申请4GB空间,但实际情况受限于虚拟地址空间和系统可用物理内存+交换空间、内核限制、内存碎片情况

32位系统中int和double如何对齐

int按4字节对齐,double根据平台4或8字节对齐

strcpy的缺陷,strncpy安全吗

strcpy的缺陷是不检查目标缓冲区大小,容易导致缓冲区溢出。strncpy可限制拷贝长度,但不能保证字符串以\0结尾,使用不当仍然会导致未定义行为,可以使用strlcpy、snprintf或string

sizeof和strlen区别

sizeof是运算符,编译时计算变量占用内存字节数,包括\0,strlen是函数,运行时计算字符串实际长度,不包括\0

什么是内存泄漏?如何避免?

内存泄露就是程序未能释放掉不再使用的内存的情况,造成内存的浪费;

内存泄露分类:堆内存泄露、系统资源泄露、基类析构函数未定义为虚函数

智能指针避免内存泄漏。

C++内存分区

栈区、堆区、静态区、常量区、代码区

堆和栈的区别

栈用于存储局部变量、函数调用信息,堆是动态分配的内存区域,由程序运行时动态分配;栈上的变量生命周期和他的函数执行周期相同,堆上的变量由程序员显式控制; 栈上的内存分配释放是自动的,且速度快,堆上的内存分配和释放是手动的,速度慢;

如何用两个栈实现队列

用两个栈(stack_in和stack_out)实现队列:入队时压入stack_in,出队时若 stack_out 为空,则将stack_in全部弹出并压入stack_out,再弹出stack_out栈顶,均摊时间复杂度O(1)

函数内部静态局部变量和全局静态变量的区别

静态局部变量的作用域仅限于函数内部,生命周期贯穿整个程序,全局静态变量作用域限于当前文件,用于实现文件内封装。

main函数执行之前都干了什么

在main函数执行之前,加载可执行文件、建立进程环境、调用C运行时库初始化、构造全局/静态对象、设置标准I/O流等。

C++三大特性

  • 封装:把数据和操作打包在一起,使用public、private、protected保护数据
  • 继承:复制+扩展父类内容,使得代码复用
  • 多态:同一种方式操作不同对象,分为静态多态和动态多态,静态多态比如函数重载和运算符重载,动态多态就是虚函数+继承,根据程序运行时的实际对象类型来决定调用什么函数

虚函数

虚函数在C++中实现多态机制,允许在派生类中重新定义基类中定义的函数,通过基类指针或引用调用的函数在运行时根据实际对象确定。

虚函数的实现依赖虚函数表,每个类都有一个虚函数表,它包含了该类的虚函数地址,每个对象都包含一个指向其类的虚表的指针,叫虚指针;

调用虚函数时,编译器使用对象的虚指针查找虚表,并通过虚表的函数地址执行响应的虚函数。

哈希表使用场景

需要快速查找、插入、删除,且不需要有序的场景,如缓存、字典、去重、计数、路由表等。

数据结构与算法

常用的二叉树

大(小)顶堆、二叉搜索树、AVL平衡树、红黑树

快速排序思路

选取一个基准值,然后同时从两边向中间遍历,左指针遍历到比基准值大的,右指针遍历到比基准值小的,交换二者,直到指针相遇,此时基准值的位置就唯一确定了,然后左右两边继续分治。基准值的选取策略可以使用随机选轴或中位选轴,为了防止快排在极端情况下时间复杂度退化到O(n^2),如降序数组选取第一个元素为基准值。

堆排序思路

先把数组构建成要给大顶堆/小顶堆,然后依次把堆顶原始和末尾元素交换

数组和链表的区别

介绍一下extern关键字

extern关键字用于声明一个在其他文件中的变量或函数,用于跨文件共享全局变量或函数,还可以用extern "C"禁用修饰,调用C库

如何优化链表遍历

deque

哈希表冲突怎么办?一直冲突怎么办?

使用链地址法或开放寻址法,若一直冲突就优化哈希函数、扩容哈希表或者改用更高级结构

为什么两个结构体里面的成员是一样的,但是使用sizeof的时候大小不一样

内存对齐填充导致,在成员变量之间插入空字节

外排序

分块排序+多路归并,用磁盘换内存,将数据分块,每块单独排序,然后使用多路归并

数据库相关

讲一下索引

索引是用来加速数据库数据检索的数据结构,索引和数据都存储在磁盘中。 使用B+树作为索引结构原因如下:

  • 只有叶子节点才存放数据,非叶子节点存放的是键,这保证了非叶子节点中能存放更多的键,使得树更矮,有效减少IO次数;
  • B+树叶子节点之间用(双向)链表连接,天然支持范围查询和顺序遍历;
  • 在mysql中B+树每个节点都是一个页,充分利用磁盘预读,性能好

索引的分类有哪些

  • 按数据结构分类:B+树索引、HASH索引、Full-Text索引;
  • 按物理存储分类:聚簇索引(主键索引)、二级索引(辅助索引);
  • 按字段特性分类:主键索引、唯一索引、普通索引、前缀索引;
  • 按字段个数分类:单列索引、联合索引(复合索引);

MySQL分页查询有什么性能问题?怎么优化?

SQL事务

事务启动方式:begin/start 、commt、rollback

redo log和binlog有何区别?

  • 适用对象不同:binlog是MySQL的Server层实现的,所有存储引擎都能适用;redo log是InnoDB存储引擎实现的;
  • 文件格式不同:binlog中有3种格式类型:STATEMENT、ROW、MIXED;redo log是物理日志,记录某个数据页做了哪些修改;
  • 写入方式不同:binlog是追加写,写满一个文件就继续写;redo log是循环写,日志大小固定,边写边擦除;
  • 用途不同:binlog用于备份恢复、主从复制;redo log用于故障恢复

mysql怎么实现主从复制?

  • 写入binlog:事务提交后,主库写binlog日志,提交事务,更新本地存储数据;
  • 同步binlog:将binlog复制到从库上,从库把binlog写到暂存日志中;
  • 回放binlog:回放binlog,更新存储引擎数据;

两阶段提交

redo log和binlog都需要持久化到磁盘上,但二者的持久化过程是独立的,可能出现半成功状态导致日志不一致,由于redo log影响主库数据,binlog影响从库数据,又会导致主从库数据不一致问题;

使用两阶段提交来解决日志不一致问题,将单个事务提交分为2个阶段:准备(prepare)阶段和提交(commit)阶段,每个阶段由协调者和参与者共同完成(在这里binlog为协调者,存储引擎是参与者);

客户端执行commit语句或自动提交事务时,MySQL内部会开启一个XA事务,分两阶段来完成XA事务的提交

  • prepare阶段:将XID(内部XA事务的ID)写入到redo log,同时及那个redo log对应的事务状态设置为prepare,然后将redo log持久化到磁盘;
  • commit阶段:将XID写入到binlog,然后将binlog持久化到磁盘,接着调用引擎提交事务接口,将redo log状态设置为commit,只要binlog刷盘成功,redo log无论是prepare还是commit阶段,都能认定事务执行成功;

缺点:

  • 磁盘I/O次数高:每次事务提交都会进行两次刷盘;
  • 锁竞争激烈:在多事务情况下,需要加锁保证提交原子性以保证多事务下两个日志提交顺序的一致;

组提交

为解决两阶段提交导致的磁盘I/O次数高的问题,MySQL引入了binlog组提交机制,当多个事务提交时,会将多个binlog刷盘操作合并为一个;

引入组提交后,两阶段中的commit阶段拆分为3个过程:

  • flush阶段:多个事务按顺序将binlog从cache写入文件(不刷盘);
  • sync阶段:对binlog做fsynnc操作(合并为一次刷盘);
  • commit阶段:各事务按顺序做InnoDB commit操作;

每个阶段都有一个队列,每个阶段都有锁进行保护,第一个进入队列的事务会称为leader,leader领导队列中的所有事务进行操作;针对队列进行加锁,使得锁粒度变小,从而提升效率;

MySQL磁盘I/O很高,有什么优化方法?

想尽办法“延迟”binlog和redo log刷盘时机:

  • 设置组提交参数binlog_group_commit_sync_delaybinlog_group_sync_no_delay_count,延迟binlog刷盘时机,减少binlog刷盘次数;缺点是由于”故意等待”而导致的语言响应时间的增加,但只要系统不宕机,就不会又丢失数据的风险;
  • sync_binlog设置为大于1的值,表示每次提交事务都会write,但积累N个事务后才fsync,相当于延迟了binlog刷盘时机;缺点是主机掉电会丢失N个事务的binlog日志
  • innodb_flush_log_at_trx_commit设置为2,表示每次事务提交后,将redo log buffer中的redo log写入redo log文件,交由操作系统控制持久化磁盘时机。

怎么优化数据库表

索引、缓存、主写从读、读写分离、分库分表等

上次更新于: