腾讯 云平台架构 二面
1.你这个项目是哪来的,网上找的吗
2. 你的网络框架大致架构是怎样的
3. epoll 能监听磁盘文件吗,为什么普通文件和 socket 在事件模型上表现不同
答案:epoll 从接口上看可以把很多 fd 加进去,但并不是所有 fd 都能像 socket 一样带来有意义的事件驱动收益。普通磁盘文件通常总是“可读”“可写”,因为它们不具备网络连接那种等待对端、等待缓冲区状态变化的语义,所以即使加入 epoll,也不会像网络 fd 那样随着数据到达不断产生值得等待的边缘事件。真正适合 epoll 的通常是 socket、pipe、eventfd、timerfd 这类“状态会变化”的 fd。如果业务是“收到网络请求后把数据写到某个文件”,一般不会去监听文件本身,而是监听网络连接,把文件写入放到异步线程里处理。
4. 如果一个服务既处理网络连接又处理大量文件写入,这两类 IO 应该怎么协同
答案:更合理的做法是把网络 IO 和磁盘 IO 解耦。网络侧继续走 epoll + 非阻塞 socket + 事件循环,磁盘写入侧走异步任务队列或者专门的刷盘线程。网络线程只负责收包、拆包、协议校验和把待写数据组织好,不直接做重磁盘操作。如果磁盘压力大,就通过高低水位做背压,比如落盘队列积压到一定程度后暂停连接读事件、拒绝低优先级请求或者做批量刷盘。真正线上容易出问题的不是“能不能写文件”,而是把慢磁盘路径混进了快网络路径,最后把整个事件循环拖慢。
5. ET 模式下读写一般为什么都要配合非阻塞
答案:因为 ET 只在状态从“不就绪”变成“就绪”的时候通知一次。如果某次收到可读事件后没有把内核缓冲区里的数据读到 EAGAIN,后面可能不会再提醒你;写也是类似,如果发送缓冲区有空间时你没尽量写完,后面不一定再收到新的边缘通知。所以 ET 模式通常要求 fd 设为非阻塞,然后在一次事件处理中循环读或循环写,直到返回 EAGAIN 为止。如果还是阻塞 IO,一旦读写卡住,线程就会被挂住,ET 的优势基本就没了。
代码:
char buf[4096];
while (true) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n > 0) {
// 处理数据
} else if (n == 0) {
// 对端关闭
close(fd);
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
close(fd);
break;
}
}
6. 阻塞 IO 和非阻塞 IO 的区别,为什么很多高并发场景不直接用阻塞读写
答案:阻塞 IO 的特点是当前系统调用在条件不满足时会一直等,比如没数据就阻塞在 recv,发送缓冲区满了就阻塞在 send。非阻塞 IO 则是在条件不满足时立刻返回,通常配合 epoll 这类多路复用机制使用。高并发场景下如果每个连接都让线程阻塞等待,线程数、栈空间、上下文切换和调度成本都会很高,而且一个慢连接就可能长期占住一个线程。所以更常见的做法是少量 IO 线程用非阻塞 + 事件驱动管理大量连接,把真正耗时的逻辑再拆到业务线程池里。
7. 如果 send 返回了成功,是否说明这次一定把用户数据发到对端了
答案:不一定。send 成功通常只表示数据已经从用户态拷到了内核发送缓冲区,或者至少有一部分已经被内核接收准备发送,并不等于对端应用已经收到,更不等于已经处理完成。如果是阻塞 socket,也不代表一次调用一定把所有数据都写完;如果是非阻塞 socket,更可能只写了一部分,需要自己维护发送缓冲区和写偏移。所以网络编程里不能把一次 send 当成“一条消息发送完成”,而是要自己处理半包发送、重试和写事件续传。
代码:
ssize_t send_all(int fd, const char* data, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, data + sent, len - sent, 0);
if (n > 0) {
sent += n;
} else if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
break;
} else {
return -1;
}
}
return sent;
}
8. 如果连接很多,但服务已经处理不过来了,读事件还要不要继续开着
答案:一般不会无脑继续读。如果业务线程池、发送队列或者落盘队列已经明显积压,继续从 socket 把数据读进用户态,只会把压力从内核缓冲区转移到应用缓冲区,最后导致内存继续上涨。比较常见的做法是做背压:达到高水位后临时关闭该连接或该批连接的读关注,等消费回到低水位再恢复;严重时还会配合连接级限流、请求降级或者直接拒绝新请求。系统设计上更重要的是能在超载时“有序变慢”,而不是把所有流量都接进来然后一起拖死。
9. 手写一个循环队列,核心要注意什么
答案:循环队列最核心的是固定容量、头尾下标推进和判空判满条件。为了区分空和满,最简单的方式是浪费一个槽位:head == tail 表示空,(tail + 1) % cap == head 表示满。如果需要支持泛型对象,还要考虑对象构造析构、异常安全和拷贝成本。如果只是面试手写,通常先把整数版或模板版的 push/pop/front/empty/full 写清楚就够了。
代码:
#include <vector>
using namespace std;
class RingQueue {
private:
vector<int> buf;
int head, tail, cap;
public:
RingQueue(int n) : buf(n + 1), head(0), tail(0), cap(n + 1) {}
bool empty() const { return head == tail; }
bool full() const { return (tail + 1) % cap == head; }
bool push(int x) {
if (full()) return false;
buf[tail] = x;
tail = (tail + 1) % cap;
return true;
}
bool pop() {
if (empty()) return false;
head = (hea
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.