网络开发面试真题答案整理(三):Linux 系统与内核 & C 语言基础
共 31 题,涵盖 Linux 进程线程、内核机制、eBPF/Netfilter,以及 C 语言关键字、内存模型、代码分析等方向。
一、Linux 系统与内核
1. 进程与线程的区别?线程间通信有哪些方法?
进程 vs 线程
线程间通信方法
- 互斥锁(pthread_mutex):保护共享资源
- 读写锁(pthread_rwlock):读多写少场景
- 自旋锁(spinlock):忙等待,锁持有时间短时用
2. 进程和线程在内核中的实现是什么?内核都用 task_struct,如何区分进程和线程?
task_struct 结构
Linux 内核统一用 task_struct 表示进程和线程:
区分方式
线程(轻量级进程):
进程:
- fork 时复制或 COW(写时复制)父进程地址空间
// 关键区分structtask_struct {structmm_struct *mm;// 进程地址空间structmm_struct *active_mm;// 指向当前使用的地址空间// ...};// 线程共享进程的 mm,进程拥有独立的 mm
3. 父进程 fork 一个子进程后,锁会被继承吗?如果继承会产生什么问题?
锁继承行为
- 锁本身不复制:fork 只复制锁的状态(locked/unlocked)
- pthread_atfork:可注册 fork 前后的锁处理回调
潜在问题
- 死锁:子进程可能在持有锁时 fork,此时锁状态被复制
- 资源泄露:某些锁(如文件锁 flock)会随 fork 继承但行为不同
正确做法
// fork 后在子进程中重新初始化锁pthread_atfork(NULL, NULL, child_after_fork);voidchild_after_fork(void){ pthread_mutex_init(&mylock, NULL);}
4. 如何使用 GDB 调试多线程程序?如何查看内存并将内存内容保存到本地磁盘?
GDB 多线程调试
# 启动gdb ./program# 查看线程(gdb) info threads# 切换线程(gdb) thread 2# 线程断点(gdb) break filename:linenum thread 2# 锁视图(gdb) info locks # 显示锁持有情况# 调试特定线程(gdb) thread apply 2 bt # 查看线程 2 的栈
查看内存
# 格式:x /<count><format><size>(gdb) x/16xb 0x1000 # 16 进制字节(gdb) x/4dw 0x1000 # 4 个有符号字(gdb) x/s 0x1000 # 字符串(gdb) x/8i 0x1000 # 指令# 打印变量地址(gdb) p &variable(gdb) p *ptr
保存内存到文件
(gdb) dump memory filename start_addr end_addr(gdb) restore filename binary 0x1000 # 恢复# 例子:保存 0x1000-0x2000 的内存(gdb) dump memory mem.bin 0x1000 0x2000
5. 阻塞 I/O 在内核中是如何实现的?
阻塞 I/O 流程
应用: read(fd, buf, len) ↓ syscall: sys_read() ↓ VFS: 检查文件描述符 ↓ 文件系统/设备驱动 ↓ 若无数据 ──→ 睡眠(加入等待队列) ↓ ↑ 若有数据 ◄─── 唤醒 ──────────┘ ↓ 复制数据到用户空间 ↓ 返回读取字节数
等待队列机制
// 驱动中的典型实现wait_queue_head_t wq;init_waitqueue_head(&wq);// 阻塞等待DEFINE_WAIT(wait);add_wait_queue(&wq, &wait);while (!condition) { prepare_to_wait(&wq, &wait, TASK_INTERRUPTIBLE);if (signal_pending(current))return -ERESTARTSYS; schedule();}finish_wait(&wq, &wait);// 数据到来时唤醒wake_up_interruptible(&wq);
6. 生产者-消费者模型如何用两个线程实现?消费者消费过快会发生什么?
C 语言实现
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<semaphore.h>#define BUFFER_SIZE 10int buffer[BUFFER_SIZE];int count = 0;// 信号量sem_t empty; // 空槽数量sem_t full; // 满槽数量pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *producer(void *arg){for (int i = 0; i < 20; i++) { sem_wait(&empty); // 申请空槽 pthread_mutex_lock(&mutex); buffer[count++] = i; // 生产数据printf("Producer: %d\n", i); pthread_mutex_unlock(&mutex); sem_post(&full); // 增加满槽 }returnNULL;}void *consumer(void *arg){for (int i = 0; i < 20; i++) { sem_wait(&full); // 申请满槽 pthread_mutex_lock(&mutex);int data = buffer[--count]; // 消费数据printf("Consumer: %d\n", data); pthread_mutex_unlock(&mutex); sem_post(&empty); // 增加空槽 }returnNULL;}intmain(){pthread_t p, c; sem_init(&empty, 0, BUFFER_SIZE); sem_init(&full, 0, 0); pthread_create(&p, NULL, producer, NULL); pthread_create(&c, NULL, consumer, NULL); pthread_join(p, NULL); pthread_join(c, NULL); sem_destroy(&empty); sem_destroy(&full);return0;}
消费过快问题
7. 说一下你知道的锁的类型(互斥锁、自旋锁、读写锁、RCU 锁),Linux 中锁是如何实现的?
锁类型对比
Linux 实现
互斥锁(mutex):
// 简化实现structmutex {atomic_t count; // 1=unlocked, 0=lockedwait_queue_head_t wait;};
自旋锁(spinlock):
// 底层基于 CASstaticinlinevoidspin_lock(spinlock_t *lock){while (atomic_swap(&lock->locked, 1) != 0) cpu_relax(); // pause 指令}
读写锁(rwlock):
// 读锁:计数器++read_lock(&rwlock);read_unlock(&rwlock);// 写锁:独占write_lock(&rwlock);write_unlock(&rwlock);
RCU(Read-Copy-Update):
8. iptables 和 Netfilter 的内核实现原理是什么?
Netfilter 框架
Incoming Routing Outgoing │ │ │ ▼ ▼ ▼┌───────┐ ┌───────┐ ┌───────┐│PREROUTING│ │ │ │POSTROUTING│└───────┘ ▼ ▼ └───────┘ │ ┌───────────────┐ ▲ │ │ INPUT │ │ │ │ (Local Proc) │ │ │ └───────────────┘ │ │ ▲ │ ▼ │ ┌───────┐┌───────┐ │ │OUTPUT ││ │ │ └───────┘└───────┘ │
钩子点(HOOK)
iptables 实现
// 规则链:PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING// 规则表:filter/nat/mangle/raw// 示例:阻止特定 IPiptables -A INPUT -s 192.168.1.100 -j DROP// 示例:NAT 规则iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
9. BPF、eBPF、XDP 分别是什么?它们的关系和应用场景是什么?
关系图
BPF (Classic BPF) ↓ eBPF (Extended BPF) ──── XDP (Express Data Path) ↓ ↓ Kernel Hooks Low-level Hook
BPF vs eBPF
XDP(Express Data Path)
应用场景
- 网络监控:tc (traffic control) 统计
10. 系统内存耗尽,如何定位问题?若各进程内存占用正常,可能是哪里的问题?如何处理?
定位方法
# 查看内存使用free -h# 查看进程内存占用ps aux --sort=-rss | head -n 10# 查看内存映射cat /proc/meminfo# slabtop 查看内核对象slabtop# vmalloc 泄漏检测cat /proc/vmallocinfo
常见原因
处理方法
# 手动触发 oomecho 1 > /proc/sys/vm/drop_caches# 调整 swapswapon/swapoff# 限制 cgroup 内存echo $$ > /sys/fs/cgroup/memory/your_group/tasksecho 10G > /sys/fs/cgroup/memory/your_group/memory.limit_in_bytes
11. Linux 线程调优有哪些工具?perf/trace 等工具如何使用?
perf
# CPU 性能分析perf top # 实时 top 风格perf record -g ./program # 记录perf report # 查看报告# 函数级分析perf record -F 99 -g ./programperf report --symbol-filter=my_func# 特定事件perf stat -e cache-misses ./programperf stat -e cycles:u ./program
strace
# 跟踪系统调用strace -tt -T ./program# 跟踪网络调用strace -e trace=network ./program# 统计系统调用strace -c ./program
其他工具
# 延迟追踪blktrace /dev/sda# 网络追踪tcpdump -i eth0# 内核追踪bpftrace -e 'tracepoint:syscalls/sys_enter_read { @[comm] = count(); }'
12. Linux 常用的文件系统有哪些?各自的特点是什么?
13. open/fopen、read/fread、write/fwrite 的区别是什么?
使用场景
- 底层驱动/系统编程:open/read/write
- 应用程序:fopen/fread/fwrite(缓冲减少系统调用)
14. ELF 文件格式是什么?包含哪些段?
ELF 结构
+------------------+| ELF Header | magic, 类型, 入口地址+------------------+| Program Header | 加载段信息+------------------+| .text | 代码段| .rodata | 只读数据| .data | 已初始化数据| .bss | 未初始化数据 (零)+------------------+| .symtab | 符号表| .strtab | 字符串表| .debug_* | 调试信息+------------------+| Section Header | 各段描述+------------------+
主要段
15. 如何在打开一个进程时检测该进程是否已经在运行?(进程单实例的实现方法)
方法一:文件锁
#include<stdio.h>#include<stdlib.h>#include<sys/file.h>#include<unistd.h>intcheck_single_instance(constchar *lockfile){int fd = open(lockfile, O_CREAT | O_RDWR, 0666);if (fd < 0) return-1;// 尝试获取文件锁,非阻塞if (flock(fd, LOCK_EX | LOCK_NB) != 0) {// 锁被占用,说明已有实例在运行 close(fd);return1; }// 写入 PIDchar pid[32];snprintf(pid, sizeof(pid), "%d\n", getpid()); ftruncate(fd, 0); lseek(fd, 0, SEEK_SET); write(fd, pid, strlen(pid));return0; // 成功获取锁}
方法二:PID 文件 + 信号
intcheck_instance(constchar *pidfile){ FILE *f = fopen(pidfile, "r");if (f) {pid_t old_pid;if (fscanf(f, "%d", &old_pid) == 1) {// 检查进程是否存在if (kill(old_pid, 0) == 0) { fclose(f);return1; // 进程存在 } } fclose(f); }// 写入当前 PID f = fopen(pidfile, "w");if (f) {fprintf(f, "%d\n", getpid()); fclose(f); }return0;}
二、C 语言基础
16. int id[sizeof(unsigned long)]; 这段代码是否正确?为什么?
答案
正确。
分析
sizeof(unsigned long) 返回 unsigned long 类型的字节大小- 在 64 位系统上,
unsigned long 是 8 字节 - 在 32 位系统上,
unsigned long 是 4 字节
代码等价于
// 64 位系统int id[8]; // 正确,声明了 8 个 int 元素的数组// 32 位系统int id[4]; // 正确,声明了 4 个 int 元素的数组
注意点
- 数组大小必须是常量表达式,C99 可用变长数组(VLA)
17. int i = 10; sizeof(i++); 执行后 i 的值是多少?为什么?
答案
i 的值仍然是 10。
分析
sizeof(i++) 只计算 i 的类型大小(int = 4 字节)- 表达式
i++ 不会被求值(sizeof 的操作数不计算)
验证
#include<stdio.h>intmain(){int i = 10;printf("%d\n", sizeof(i++)); // 输出 4printf("%d\n", i); // 输出 10,未执行 i++return0;}
18. 分析以下两段代码的性能差异,并说明原因(Cache 局部性原理)
性能差异
代码一(行优先):arr[i][j] 访问 → 快代码二(列优先):arr[j][i] 访问 → 慢
分析
// 代码一:行优先访问for(int i=0; i<1024; i++)for(int j=0; j<1024; j++) arr[i][j] = num++;// 访问模式:arr[0][0], arr[0][1], arr[0][2]... arr[0][1023],// arr[1][0], arr[1][1]...// 代码二:列优先访问for(int i=0; i<1024; i++)for(int j=0; j<1024; j++) arr[j][i] = num++;// 访问模式:arr[0][0], arr[1][0], arr[2][0]... arr[1023][0],// arr[0][1], arr[1][1]...
Cache 局部性原理
- Cache 行:CPU 按 Cache 行(通常 64 字节)从内存加载数据
- 空间局部性:访问某地址后,附近的地址可能被后续访问
- 代码一:访问同一行的连续元素,1 次加载可命中多个访问
- 代码二:跨行访问,每访问一个元素都可能触发 Cache miss
性能比
19. 以下联合体(union)的输出是什么?请说明原因
答案
输出取决于系统大小端,可能是 266(小端)或 266(大端)。
分析
union { int i; char x[2]; } a;a.x[0] = 10; // 低字节 = 0x0Aa.x[1] = 1; // 高字节 = 0x01
小端序(x86)
内存: [0x0A] [0x01] 低地址 高地址 = 10 + 256 = 266
大端序(网络字节序)
内存: [0x01] [0x0A] 低地址 高地址
验证方法
#include<stdio.h>intmain(){union { int i; char x[2]; } a; a.x[0] = 10; a.x[1] = 1;printf("%d\n", a.i);// 用系统函数验证printf("%s\n", __BYTE_ORDER__ == __LITTLE_ENDIAN__ ? "little" : "big");return0;}
20. 以下函数当 x = 9999 时返回多少?请说明解题思路
答案
返回 8。
分析
intfunc(int x){int ret = 0;while (x) { ret++; x = x & (x - 1); }return ret;}
x & (x-1) 规律
功能:消除 x 二进制表示中最右边的 1
推导
9999 = 10011100001111(二进制)
实际 9999 的二进制 1 的个数:8 个
21. 64 位操作系统下,以下结构体的大小是多少?
答案
48 字节。
分析
structstus1 {char* p; // 8 字节,偏移 0char arr[2]; // 2 字节,偏移 8// 此时偏移 10,需要对齐到 4 的倍数 → 偏移 12int c; // 4 字节,偏移 12// 偏移 16double f; // 8 字节,对齐到 8 → 偏移 16// 偏移 24 short d; // 2 字节,偏移 24// 偏移 26,需要对齐到 8 → 偏移 32long g; // 8 字节,偏移 32// 偏移 40float h[2]; // 8 字节,偏移 40// 总大小 48,但需检查末尾对齐};// 结构体对齐到最大成员(8) → 48 字节
计算过程
偏移计算:char* p : 偏移 0-7 (8字节)char arr[2]: 偏移 8-9 (2字节)padding : 偏移 10-11 (2字节,对齐到4)int c : 偏移 12-15 (4字节)double f : 偏移 16-23 (8字节,对齐到8)short d : 偏移 24-25 (2字节)padding : 偏移 26-31 (6字节,对齐到8)long g : 偏移 32-39 (8字节)float h[2] : 偏移 40-47 (8字节)-----------------------总大小 : 48 字节 (8的倍数)
22. struct 和 union 的区别是什么?
union 示例
union ip_addr {uint32_t int_addr;uint8_t bytes[4];};union ip_addr myip;myip.int_addr = 0xC0A80101; // 192.168.1.1printf("%d.%d.%d.%d", myip.bytes[0], myip.bytes[1], myip.bytes[2], myip.bytes[3]);
23. inline 关键字的作用是什么?内联函数和宏定义有什么区别?
inline 作用
vs 宏定义
示例
// 宏(容易出错)#define MAX(a, b) ((a) > (b) ? (a) : (b))// inline(安全)inlineintmax(int a, int b){return a > b ? a : b;}
注意
inline 只是建议,编译器可能忽略(优化级别、函数复杂度)
24. static 关键字修饰局部变量、全局变量、函数时分别有什么效果?
效果
| |
|---|
| 存储在 .data/.bss 段,生命周期为程序运行期间,只初始化一次 |
| |
| |
存储区域
示例
// 文件作用域staticint global_var; // 仅本文件可见staticvoidhelper(void); // 仅本文件可见// 函数内部voidfunc(void){staticint count = 0; // 只初始化一次,跨调用保持值 count++;}
25. volatile 关键字的作用是什么?是在编译阶段还是运行阶段起作用?寄存器如何对变量进行优化?
作用
何时使用
编译 vs 运行
寄存器优化问题
// 普通变量可能被优化int flag = 0;while (!flag) { } // 可能被优化为 while(1)// volatile 变量强制每次读取volatileint flag = 0;while (!flag) { } // 每次都从内存读取
26. register 关键字的作用是什么?
作用
注意事项
- 现代编译器:编译器比程序员更擅长优化,register 可能被忽略
- 不能取地址:
&var 对 register 变量是非法的 - 历史意义:在 C89 中有意义,C++11 后废弃
示例
registerint i;for (i = 0; i < 100; i++) {// 编译器可能将 i 放入寄存器}// 以下代码非法registerint j;printf("%p", &j); // error: address of register variable
27. 函数指针的调用和普通函数调用有什么区别?写出指针数组和数组指针的声明方式。
函数指针
// 普通函数调用intadd(int a, int b){ return a + b; }int result = add(1, 2);// 函数指针int (*func_ptr)(int, int) = add;int result = func_ptr(1, 2); // 间接调用int result = (*func_ptr)(1, 2); // 等价写法
区别
指针数组 vs 数组指针
// 指针数组(数组中每个元素是指针)int *arr[10]; // 10 个 int* 的数组char *strs[5]; // 5 个 char* 的数组// 数组指针(指向数组的指针)int (*ptr)[10]; // 指向 10 个 int 的数组的指针// 二维数组与数组指针int matrix[5][10];int (*row_ptr)[10] = matrix; // row_ptr 指向二维数组的一行
typedef 简化
typedefint(*Callback)(int, int);Callback handlers[3]; // 3 个回调函数指针的数组
28. gcc 的 -g 和 -o 参数分别是什么意思?
示例
# 编译并生成调试信息gcc -g -o program program.c# 仅生成调试信息(不优化)gcc -g -O0 -o program program.c# 指定输出gcc -o myapp main.c
29. C 语言编译的完整过程是什么?
四个阶段
源代码 (.c) ↓[1. 预处理] → .i 文件 ↓[2. 编译] → .s 汇编文件 ↓[3. 汇编] → .o 目标文件 ↓[4. 链接] → 可执行文件
各阶段详解
1. 预处理
2. 编译
3. 汇编
4. 链接
30. 大小为 0 的数组(柔性数组)在 Linux 内核中有什么用途?和使用指针有什么区别?
柔性数组(Flexible Array Member)
structpacket {int len;char data[0]; // 必须放在结构体末尾};// 使用structpacket *p = malloc(sizeof(structpacket) + 1024);p->len = 1024;memcpy(p->data, buffer, 1024);
优势
- 内存连续:data 与结构体连续,Cache 友好
vs 指针
内核示例
structiovec {void *iov_base;size_t iov_len;};// 类似的变长结构在网络协议栈中广泛使用
31. 了解 C 语言实现设计模式吗?(工厂模式、单例模式、观察者模式等)
单例模式
// 线程安全的单例(双重检查锁定)staticobj_t *instance = NULL;staticpthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;obj_t *get_instance(void){if (instance == NULL) { pthread_mutex_lock(&lock);if (instance == NULL) { instance = malloc(sizeof(obj_t)); init_obj(instance); } pthread_mutex_unlock(&lock); }return instance;}
工厂模式
typedefenum { TYPE_A, TYPE_B } ObjType;typedefstruct {void (*process)(void*);} Base;typedefstruct { Base base; int extra; } TypeA;typedefstruct { Base base; double data; } TypeB;Base *create_obj(ObjType type){ Base *obj = NULL;switch (type) {case TYPE_A: obj = malloc(sizeof(TypeA)); obj->process = process_a;break;case TYPE_B: obj = malloc(sizeof(TypeB)); obj->process = process_b;break; }return obj;}
观察者模式
// 观察者回调typedefvoid(*callback_t)(void *arg, int event);structobserver {callback_t cb;void *data;};structsubject {structobserver *observers[10];int count;};voidnotify(struct subject *s, int event){for (int i = 0; i < s->count; i++) { s->observers[i]->cb(s->observers[i]->data, event); }}
参考答案要点总结
Linux 部分
- 进程线程:task_struct 统一表示,共享 mm_struct 区分
- Netfilter:5 个钩子点(PREROUTING/INPUT/FORWARD/OUTPUT/POSTROUTING)
- 内存定位:/proc/meminfo、slabtop、vmallocinfo
C 语言部分
- **x & (x-1)**:消除最右边 1,统计 1 的个数
- static:局部变量持久化、全局/函数限制作用域
- inline vs 宏:inline 有类型检查、可调试