i007.cc

i007.cc

优先队列-降维打击

05.价值资料

讲讲Linux平台开发经验

 一、进程与线程模型

进程基础

cpp
// fork() — 创建子进程
pid_t pid = fork();
if (pid == 0) {
    // 子进程:完整拷贝父进程内存(CoW,写时复制)
    execv("/usr/bin/game_server", args);  // 替换进程映像
    _exit(1);  // exec 失败才到这里,用 _exit 不用 exit
} else if (pid > 0) {
    // 父进程
    int status;
    waitpid(pid, &status, 0);  // 等待子进程,避免僵尸进程
} else {
    // fork 失败
}

 

写时复制(Copy-on-Write):fork 后父子进程共享内存页,只有某方写入时才真正复制该页。游戏服务器用 fork 做快照备份(Redis BGSAVE 就是这个原理)。

僵尸进程:子进程退出后父进程没有 wait,进程表项残留。解决:

  • 父进程调用 waitpid
  • 或忽略 SIGCHLDsignal(SIGCHLD, SIG_IGN)

线程 vs 进程

线程共享:堆、代码段、全局变量、文件描述符
线程私有:栈、寄存器、errno、线程局部存储(TLS)

 

__thread / thread_local — 线程局部存储

cpp
thread_local int g_player_id = -1;  // 每个线程独立一份,无锁访问

// 游戏服务器常用:在工作线程处理请求时存当前 player_id,
// 避免层层传参

 


二、文件描述符与 I/O 多路复用

epoll — 高并发网络的核心

cpp
// 创建 epoll 实例
int epfd = epoll_create1(EPOLL_CLOEXEC);

// 注册感兴趣的 fd
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;  // ET 边缘触发
ev.data.fd = server_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);

// 事件循环
struct epoll_event events[MAX_EVENTS];
while (true) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        if (events[i].data.fd == server_fd) {
            // 新连接
            int client_fd = accept4(server_fd, nullptr, nullptr, 
                                    SOCK_NONBLOCK | SOCK_CLOEXEC);
            // 注册 client_fd 到 epoll...
        } else {
            // 可读数据
            handle_client(events[i].data.fd);
        }
    }
}

 

ET vs LT(面试必考)

水平触发 LT(默认) 边缘触发 ET
触发条件 fd 可读就一直通知 fd 状态变化时通知一次
未读完 下次 epoll_wait 还会通知 不再通知,必须一次读完
实现要求 简单 必须循环读到 EAGAIN
性能 较低 更高(事件更少)

ET 模式下必须这样读:

cpp
// ET 模式:必须循环读到 EAGAIN,否则丢数据
while (true) {
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
            break;  // 读完了
        // 真正的错误
        break;
    }
    if (n == 0) {
        // 对端关闭
        close(fd);
        break;
    }
    process(buf, n);
}

 

select vs poll vs epoll 对比

select poll epoll
fd 上限 1024 无限制 无限制
遍历方式 线性扫描全部 线性扫描全部 只返回就绪 fd
时间复杂度 O(n) O(n) O(1)
内核拷贝 每次全量 每次全量 注册一次,增量通知
适用场景 已基本淘汰 少量连接 高并发服务器标配

三、内存管理

虚拟内存布局

高地址
┌─────────────────┐
│   内核空间       │  ← 用户程序不可直接访问
├─────────────────┤
│   栈(向下增长) │  ← 局部变量、函数调用帧
│        ↓        │
├─────────────────┤
│   内存映射区     │  ← mmap、共享库、匿名映射
│        ↑        │
├─────────────────┤
│   堆(向上增长) │  ← malloc/new
├─────────────────┤
│   BSS 段         │  ← 未初始化全局变量
│   数据段         │  ← 已初始化全局变量
│   代码段(只读) │  ← 程序文本
└─────────────────┘
低地址

 

mmap 的实际用途

cpp
// 1. 内存映射文件(零拷贝读大文件)
int fd = open("game_assets.pak", O_RDONLY);
struct stat st;
fstat(fd, &st);

void* data = mmap(nullptr, st.st_size, PROT_READ, 
                  MAP_PRIVATE, fd, 0);
// 直接访问文件内容,操作系统按需换页,避免 read() 的内核→用户拷贝

// 2. 进程间共享内存
void* shm = mmap(nullptr, 4096, PROT_READ | PROT_WRITE,
                 MAP_SHARED | MAP_ANONYMOUS, -1, 0);
// fork 后父子进程共享这段内存

 

内存问题排查工具

bash
# Valgrind:检测内存泄漏、越界访问(慢,开发阶段用)
valgrind --leak-check=full --track-origins=yes ./game_server

# AddressSanitizer(ASan):编译期插桩,性能损耗小得多
g++ -fsanitize=address -fno-omit-frame-pointer -g ./game_server.cpp

# 查看进程内存映射
cat /proc/<pid>/maps
cat /proc/<pid>/smaps_rollup  # 内存汇总

# 堆使用情况
jemalloc / tcmalloc 的 heap profiler(C++ 游戏服务器常用 tcmalloc)

 


四、信号处理

cpp
#include <signal.h>

// 优雅关机处理
volatile sig_atomic_t g_running = 1;  // 信号安全类型

void signal_handler(int sig) {
    if (sig == SIGTERM || sig == SIGINT) {
        g_running = 0;  // 通知主循环退出
    }
}

int main() {
    struct sigaction sa{};
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // 系统调用被信号中断后自动重启
    sigaction(SIGTERM, &sa, nullptr);
    sigaction(SIGINT, &sa, nullptr);
    
    // 忽略 SIGPIPE(对端关闭时写入会触发,必须忽略)
    signal(SIGPIPE, SIG_IGN);
    
    while (g_running) {
        // 游戏服务器主循环
    }
    
    // 优雅关机:保存状态、通知客户端
    graceful_shutdown();
}

 

SIGPIPE 是游戏服务器必须处理的:客户端断开后,服务器再往 socket 写数据会收到 SIGPIPE,默认行为是进程终止。忽略它,改为检查 write 返回值。


五、性能分析工具

perf — Linux 性能分析神器

bash
# CPU 热点采样(采样 30 秒)
perf record -g -p <pid> -- sleep 30
perf report

# 查看缓存未命中、分支预测失败
perf stat -e cache-misses,cache-references,branch-misses ./game_server

# 火焰图(最直观)
perf record -F 99 -g -p <pid> -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg

 

strace — 系统调用追踪

bash
# 追踪进程所有系统调用
strace -p <pid> -e trace=network,file

# 统计每个系统调用耗时
strace -c ./game_server

# 常见发现:
# - 频繁小 read/write(应该用 writev / 批量)
# - 意外的 stat 调用(配置文件热重载失控)
# - futex 争用(锁竞争严重)

 

其他工具

bash
top / htop         # CPU、内存实时
vmstat 1           # 内存、IO、CPU 综合
iostat -x 1        # 磁盘 IO 详情
netstat / ss -tuln # 网络连接
lsof -p <pid>      # 打开的文件描述符
/proc/<pid>/fd/    # 文件描述符列表

 


六、游戏后端常见 Linux 优化技巧

socket 调优

cpp
// 1. TCP_NODELAY — 禁用 Nagle 算法,降低延迟(游戏必开)
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

// 2. SO_REUSEPORT — 多线程各自 accept,避免惊群,提升吞吐
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &flag, sizeof(flag));

// 3. SO_KEEPALIVE — 检测死连接
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &flag, sizeof(flag));

// 4. 发送/接收缓冲区
int buf_size = 4 * 1024 * 1024;  // 4MB
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));

 

系统参数调优(/etc/sysctl.conf

bash
# 最大文件描述符(默认 1024,游戏服务器要大得多)
fs.file-max = 1000000
ulimit -n 1000000

# TCP 连接队列
net.core.somaxconn = 65535           # listen() backlog 上限
net.ipv4.tcp_max_syn_backlog = 65535 # SYN 半连接队列

# TIME_WAIT 快速复用(高并发短连接服务)
net.ipv4.tcp_tw_reuse = 1

# 端口范围
net.ipv4.ip_local_port_range = 1024 65535

 

CPU 亲和性绑核

cpp
// 将线程绑定到指定 CPU 核,减少调度抖动和缓存失效
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(2, &cpuset);  // 绑定到 CPU 2
pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);

 

游戏服务器常见架构:网络 I/O 线程绑 CPU 0-1,游戏逻辑线程绑 CPU 2-7,避免跨核迁移导致 L1/L2 缓存失效。


七、面试高频问题

Q:fork 之后 malloc 是否安全?

不一定。fork 在多线程程序中有”async-signal-safe”问题:如果 fork 时另一个线程持有 malloc 的内部锁,子进程拿到的是锁定状态的 heap,再调用 malloc 会死锁。安全做法:fork 后立即 exec,或只 fork 单线程程序。

Q:epoll 的 fd 可以跨线程使用吗?

可以。epoll fd 是内核对象,多个线程可以同时调用 epoll_wait,结合 SO_REUSEPORT 实现多线程并行 accept,这是高性能服务器的标准做法。

Q:为什么 write 返回成功不代表对方收到数据?

write 只是把数据写入内核发送缓冲区,返回成功意味着内核已接受这些字节。实际网络传输、对方 TCP 栈接收、应用层 read 是后续步骤。这就是为什么游戏协议需要应用层 ACK 来确认关键操作。

Q:如何定位 C++ 程序的 coredump?

bash
# 开启 core dump
ulimit -c unlimited
echo "/tmp/core.%p" > /proc/sys/kernel/core_pattern

# 用 gdb 分析
gdb ./game_server /tmp/core.12345
(gdb) bt full      # 查看完整调用栈
(gdb) info threads # 查看所有线程状态
(gdb) thread 3     # 切换到线程 3

 


总结

对于 EA 游戏后端面试,Linux 开发经验的考察重点是:epoll 网络模型 → 内存管理与调试 → 多线程同步 → 性能调优工具链。能说出 ET 模式为什么必须循环读、SIGPIPE 为什么要忽略、TCP_NODELAY 是什么效果,就已经超过大部分候选人了。


继续深挖某个方向?比如 网络编程实战(自己实现一个简单 TCP 服务器)、性能优化案例,还是转向别的 EA 面试知识点?

发表回复