又被百度捞起来了,能赢吗?
今天分享一波 C++ 服务器开发的面试题,目前这位同学经历了百度的一二面,把比较通用的面试问题抽离出来,大家可以感受一下,跟 Java 后端比起来,有什么区别?
这次主要面试涵盖的知识点:
MySQL:索引结构、索引应用、SQL调优 C++:特性、指针与引用、多态、sizeof、stl 计算机网络:tcp socket 编程、tcp 四次挥手过程 操作系统:虚拟内存、epoll、linux 线程、线程同步技术、Linux 命令
MySQL
SQL调优是怎样的一个历程?
答:通过mysql 慢查询日志找到慢 sql,然后通过 explain 去分析 sql 语句。根据生成的执行计划进一步的做优化, 比如是否是因为索引失效导致没有走索引,还是因为没有建立索引的导致没有走索引,并且还可以考虑通过简历联合索引来进行覆盖索引优化,减少回表。
MySQL索引简单讲一讲自己的理解?
答:MySQL索引常用的是B+树,也有B树和红黑树。B+树对比B树的好处在于,B+树是只有叶子点有数据,B树是非叶子节点也会有数据,B树相对于B+树,读取数据时,系统I/O调用的次数更多;还有红黑树也可以用作索引,但是红黑树是二叉树,当数据多的时候,高度会越来越高,相对B+树,系统I/O调用的次数更多了
那么在实际过程中,索引应该怎么用?
答:索引常用的是主键索引和联合索引,联合索引的话是将两个或者多个会一同查询,且需要频繁查询的键组成联合索引。
追问:还有吗?
答:还有普通索引,对某个常用的字段也可以进行普通索引。
操作系统
虚拟内存是个什么东西?
答:为了使得程序每次都是从地址0开始,而不用去查找实际地址,所以设置一个虚拟内存,使得程序可以都从0开始,如果用到了,再由虚拟内存和物理内存进行一一映射。
eopll水平测发和边缘测发的差距
答:(看到过,但是忘记了)对这个问题不是很清楚
补充:
epoll 支持两种事件触发模式,分别是边缘触发(*edge-triggered,ET*)**和**水平触发(*level-triggered,LT*)。
这两个术语还挺抽象的,其实它们的区别还是很好理解的。
使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完; 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。
这就是两者的区别,水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。
如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read
和 write
)返回错误,错误类型为 EAGAIN
或 EWOULDBLOCK
。
一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
那么你在Linux环境下有调用过系统接口去创建过线程什么的吗?
答:可以用 fork 创建进行,用 phtread 创建线程。
那你觉得同步技术有哪些技术?
答:
首先是匿名管道,但是有个缺点,所有文件都共享,并且取/写只能一个操作; 紧接着是命名管道,可以用于两个指定文件间进行同步; 然后是信号量,我认为信号量和锁类似,通过信号量,进程之间进行间接通信;信号和信号量相类似。 还有互斥锁、读写锁、自选锁
C++
C++特性介绍一下?
答:讲了封装继承多态.
封装是将一些数据和函数封装到类中,这样外层调用类只会调用到设计者想让他调用的方法; 继承的话,我常是设计一个基类,然后分别设置子类去继承基类的一些方法,尤其是虚函数,针对不同子类的特点对虚函数进行重写。 继承还有公有和私有两种方法,公有继承是将基类的成员都原封不动的继承下来,私有继承则会将其改为私有部分;多态的话,是有函数重载和之前提到的虚函数,函数重载是可以使得相同的函数面对不同的参数个数或者类型进行不同的方式实现。
讲一下多态的理解
答:多态的话,我的理解是函数重载和虚函数,函数重载的好处我认为是同一个函数名可以对不同的参数类型或者参数个数进行不同的实现;虚函数我认为是可以使得子类在继承父类的时候,基于子类的特点重写父类的一些函数。
实际上用的话,虚函数是怎么用的?
答:将子类指针赋给父类对象,然后通过父类对象调用子类的虚函数,也可以通过作用域去调用父类的虚函数。
除了指针,你认为引用可以实现吗?
答:我认为应该可以
为什么呢,你对引用的理解是什么?
答:因为我认为引用其实相当于变量的地址值,类似一个指针。
那么引用是不是可以理解为const的一个指针?
答:我认为是可以的
现在有一个类,用g++去编译它,编译器会给它生成哪些函数?
答:默认构造函数,析构函数,默认拷贝构造函数。
这时候用sizeof对这个类计算一下,得到的是多少?
答:1
为什么呢?
答:我就说了C++是固定地址的,如果是0的话,调用的时候会有地址冲突。
说到这个sizeof,你觉得它是函数吗?
答:它是运算符
运算符的话,一般在什么时候给它定好?
答:编译阶段
现在定义一个函数,函数的形参是B,是一个int数组,int b[10],现在传入一个int c[5]实参那么此时sizeof(b)的大小是多少?
答:一个指针的大小, 指针的大小通常是4或8字节,具体取决于操作系统和编译器的位数。如果是 32 位操作系统则是 4 字节, 64 位操作系统是 8 字节
sizeof和C的大小无关吗?
答:我认为是的
STL
STL用map去删除元素的时候,用迭代器去删除,要注意哪些东西?
答:
错误的写法:
map<string,int> testMap;
for(auto it = testMap.begin(); it != testMap.end(); ++it)
{
if(it->second == xxx)
{
testMap.erase(it); //这里会出问题
}
}
错误原因:it指针被erase之后会失效,for循环中对it操作其结果都是不可预料的,可能造成程序崩溃。
正确的写法:
map<string,int> testMap;
for(auto it = testMap.begin(); it != testMap.end();)
{
if(it->second == xxx)
{
testMap.erase(it++);
}
else
{
it++;
}
}
正确原因:新写法的迭代器的自增从for头部中取出,放在循环体中。it++返回了自增前的迭代器的一个临时拷贝。然后这个临时迭代器指向的内容被删除了,但是it本身已经自增到下一个位置了,不受影响。
list其实是一个链表,deque是一个队列,那么你认为list和deque的区别是什么?
答:一个内存空间是不连续的,一个是段连续的。
那么deque是不是可以用list去实现呢?
答:我认为是的
计算机网络
如果要实现一个TCP服务器要哪些(套接字)接口?
答:(一开始没有听到套接字三个字,给我干懵了,不知道是要什么接口,就直接答了不了解;然后面试官说你没用过Socket编程吗,我才反应过来时套接字)先是用bind函数绑定一个套接字,然后进行Listen监听,监听到连接请求后,调用connect函数,先将socket放到半连接队列,然后再通过三次握手到全连接队列,最后返回一个新的socket进行通信。(忘记了accept函数,connet函数是客户端的)
listen函数的第二个参数是什么?
答:(忘记了)
补充:
Linux内核中会维护两个队列:
半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态; 全连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
int listen (int socketfd, int backlog)
参数一 socketfd 为 socketfd 文件描述符 参数二 backlog,这参数在历史版本有一定的变化
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。
但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。
怎样设置一个套接字为非阻塞模式?
答:
默认创建的 socket 都是阻塞模式的,在 Linux 平台上,我们可以使用 fcntl() 函数或 ioctl() 函数给创建的 socket 增加 O_NONBLOCK 标志来将 socket 设置成非阻塞模式。示例代码如下:
int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(sockfd, F_SETFL, newSocketFlag);
ioctl() 函数 与 fcntl() 函数使用方式基本一致,这里就不再给出示例代码了。
当然,Linux 下的 socket() 创建函数也可以直接在创建时将 socket 设置为非阻塞模式,socket() 函数的签名如下:
int socket(int domain, int type, int protocol);
给 type 参数增加一个 SOCK_NONBLOCK 标志即可,例如:
int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
TCP四次挥手,什么时候处于CLOSE_WAIT状态?
答:被动关闭方在调用 close 函数,发送第三次挥手的时候, 也就是 fin 报文的时候, 被动关闭方会进入到 close_wait 状态。
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下图:
客户端打算关闭连接,此时会发送一个 TCP 首部 FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。服务端收到该报文后,就向客户端发送 ACK
应答报文,接着服务端进入CLOSE_WAIT
状态。客户端收到服务端的 ACK
应答报文后,之后进入FIN_WAIT_2
状态。等待服务端处理完数据后,也向客户端发送 FIN
报文,之后服务端进入LAST_ACK
状态。客户端收到服务端的 FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态服务端收到了 ACK
应答报文后,就进入了CLOSE
状态,至此服务端已经完成连接的关闭。客户端在经过 2MSL
一段时间后,自动进入CLOSE
状态,至此客户端也完成连接的关闭。
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
TCP四次挥手,什么时候处于TIME_WAIT状态?
答:主动关闭方在收到第三次挥手 fin 报文的时候, tcp 连接就会处于 TIME_WAIT 状态. 会在这个状态等待 2msl 的时间, 时间到了 ,就会进入到 close 状态.
Linux 命令
linux 用top命令你一般关注什么?
答:
CPU利用率:关注系统整体的CPU利用率以及各个进程的CPU占用情况,可以通过查看%CPU列来了解。 内存使用:关注系统的内存使用情况,包括总内存、已使用内存和空闲内存等,可以通过查看内存相关的统计信息来了解。 负载情况:关注系统的负载情况,包括1分钟、5分钟和15分钟的负载平均值,可以通过查看load average来了解。 I/O活动:关注系统的磁盘I/O活动情况,包括磁盘的读写速度、等待时间等,可以通过查看磁盘相关的统计信息来了解。
那你常用的linux命令 可以自己说一说?
答:
文件相关(mv mkdir cd ls) 进程相关( ps top netstate ) 权限相关(chmod chown useradd groupadd) 网络相关(netstat ip addr) 测试相关(测试网络连通性:ping 测试端口连通性:telnet)
算法
动态规划算法可以大概讲一讲吗?
答:只讲了怎么做动态规划的题,不知道应该怎么讲原理
项目
大概讲一讲项目 讲一下简历里的技术难点
反问
部门是做什么?
面试官答:互联网云平台相关的
👇🏻 点击下方阅读原文,获取鱼皮往期编程干货。
往期推荐