目录
exec- 进程等待:
wait / waitpid 与僵尸进程 mmap
1. 从 hello 到进程:程序的执行流程
一个 C++ 程序从源码到进程运行,经历:
- 预处理 → 编译 → 汇编 → 链接
- 用户执行
./a.out,shell 调用 fork() + execve()。 - 内核
execve 读 ELF,建立虚拟地址空间,映射代码/数据段。 - 加载动态链接器(如
/lib64/ld-linux-x86-64.so.2),重定位共享库。 - 跳到
_start → __libc_start_main → 调用 main。 main 返回后执行 __exit 系统调用,内核回收资源。
面试加分:能说出 ELF 的段(segment)与节(section)区别;能描述"静态链接 vs 动态链接"的取舍;知道 LD_PRELOAD 可劫持符号(常用于诊断/打桩)。
2. 进程创建:fork 与写时复制(COW)
fork() 创建子进程,是 Unix 哲学的核心。子进程是父进程的几乎完整副本。
写时复制(Copy-On-Write):fork 后父子共享同一物理页,页表标记为只读;任一方写入时才真正复制该页。这让 fork 极快(无需立刻复制全部内存)。
#include<unistd.h>#include<sys/types.h>#include<iostream>intmain(){ pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { // 子进程:fork 返回 0 std::cout << "child, pid=" << getpid() << ", ppid=" << getppid() << "\n"; } else { // 父进程:fork 返回子进程 pid std::cout << "parent, child pid=" << pid << "\n"; } return 0;}
高频考点:
fork 后父子的文件描述符(fd)是复制的,指向同一打开文件表项,共享文件偏移量。这就是为什么管道需要在 fork 前创建。fork 只复制调用线程;其他线程在子进程中"消失"(这是 fork 后不能直接调用非异步信号安全函数的原因,详见信号节)。vfork 已废弃(父子共享地址空间,子进程必须先 exec),面试了解即可。
fork 炸弹(面试可能问如何防范)::(){ :|:& };: 无限递归 fork,耗尽进程表。防护:ulimit -u 限制用户进程数,或用 cgroup 限制。
3. exec 族与进程替换
exec 族(execl/execv/execlp/execve 等)用新程序替换当前进程的映像,PID 不变、fd 默认保留(除非设 FD_CLOEXEC)。通常 fork + exec 组合实现"启动另一个程序"。
#include<unistd.h>intmain(){ pid_t pid = fork(); if (pid == 0) { // 子进程执行 ls -l execlp("ls", "ls", "-l", (char*)nullptr); perror("exec"); // 只有 exec 失败才会执行到这里 _exit(1); } return 0;}
exec 失败处理:exec 成功不会返回;一旦返回说明失败,必须用 _exit(不是 exit,避免冲刷父进程 stdio 缓冲区造成重复输出)。
system() 与 popen():system 内部就是 fork+exec+wait,会启动 shell,有命令注入风险;popen 建立单向管道读/写子进程。面试建议:高性能场景不要滥用 system,避免 shell 开销与注入。
4. 进程等待:wait / waitpid 与僵尸进程
子进程结束后,内核保留其退出状态(pid、退出码、资源用量),成为僵尸进程(zombie),直到父进程 wait 回收。父进程不回收 → 僵尸堆积 → 耗尽 pid。
#include<sys/wait.h>pid_t pid = fork();if (pid == 0) { /* 子 */ _exit(0); }int status;waitpid(pid, &status, 0); // 阻塞等待if (WIFEXITED(status)) printf("exit=%d\n", WEXITSTATUS(status));
僵尸 vs 孤儿:
- 父先死 → 子成为孤儿进程,被
init(pid 1,现代 systemd)收养,init 会 wait 回收,不会变僵尸。
SIGCHLD 信号处理 + waitpid(..., WNOHANG) 是异步回收僵尸的正确姿势:
#include<signal.h>#include<sys/wait.h>voidon_sigchld(int){ while (waitpid(-1, nullptr, WNOHANG) > 0) { /* 回收所有已结束子进程 */ }}// signal(SIGCHLD, on_sigchld); 或在 sigaction 中设置 SA_RESTART
面试陷阱:signal() 行为在不同 Unix 不完全一致,且信号处理函数里只能调用**异步信号安全(async-signal-safe)**函数(如 write、_exit、waitpid),不能调用 printf、malloc 等(可能死锁)。推荐用 sigaction。
5. 信号处理:异步事件的双刃剑
信号是软件中断。面试要区分:
- 可靠信号(实时信号 SIGRTMIN~SIGRTMAX)
- 不可靠信号(1~31)
常用信号:SIGINT(Ctrl+C)、SIGTERM(优雅终止,可被捕获)、SIGKILL(强制杀死,不可捕获/阻塞/忽略)、SIGSEGV(段错误)、SIGPIPE(写已关闭的 socket,默认杀进程,网络编程常忽略)、SIGCHLD、SIGUSR1/2(自定义)。
#include<signal.h>#include<unistd.h>#include<cstdio>volatile sig_atomic_t g_flag = 0; // 必须用 sig_atomic_t,跨信号安全voidhandler(int sig){ g_flag = 1; }intmain(){ struct sigaction sa{}; sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, nullptr); while (!g_flag) { pause(); } // 等信号 printf("got SIGINT\n");}
高频考点:
- 全局变量在信号处理器里访问要用
volatile sig_atomic_t(或 C++11 std::atomic 的合适特化)。 SIGPIPE:往已被对端关闭的管道/socket 写会触发,默认终止进程。网络服务常在启动早期 signal(SIGPIPE, SIG_IGN),或 send 时设 MSG_NOSIGNAL。fork 后子进程在多线程程序中只能调用 async-signal-safe 函数——因为另一个线程可能正持有 malloc 锁,子进程继承这个状态,调用 malloc 会死锁。这就是 fork + 多线程的坑。- 可重入(reentrant)与线程安全的区别:可重入函数可在信号/中断中安全调用(不依赖静态状态);线程安全靠锁,但不一定可重入。
6. 文件 I/O:阻塞、非阻塞、O_DIRECT
最基础的五个系统调用:open / read / write / lseek / close。
#include<fcntl.h>#include<unistd.h>int fd = open("a.txt", O_RDWR | O_CREAT, 0644);char buf[4096];ssize_t n = read(fd, buf, sizeof(buf));write(fd, buf, n);close(fd);
阻塞 vs 非阻塞:
- 默认 fd 阻塞:
read 没数据就睡,直到有数据或 EOF。 O_NONBLOCK:无数据立即返回 -1 且 errno==EAGAIN,常用于配合 epoll 的事件驱动(见下一篇网络编程)。
O_DIRECT:绕过页缓存,直接读写磁盘。用于自管理缓存的数据库(如 MySQL InnoDB、RocksDB),避免双缓存。注意:缓冲区需对齐(通常 512 字节或页对齐),且 I/O 大小对齐。
文件描述符与 fcntl:fcntl(fd, F_SETFL, flags | O_NONBLOCK) 可把阻塞 fd 改为非阻塞;F_GETFD/F_SETFD + FD_CLOEXEC 控制 exec 时是否关闭(防泄漏 fd 给子进程)。
pread/pwrite:带偏移的原子读写,不移动文件偏移,多线程并发读写不同区域安全。
dup/dup2:复制 fd,常用于重定向标准输入/输出(dup2(fd, STDOUT_FILENO))。
7. mmap 内存映射与零拷贝思想
mmap 把文件/设备映射到进程虚拟地址空间,访问内存即访问文件,免去 read/write 的用户态↔内核态拷贝。
#include<sys/mman.h>#include<fcntl.h>#include<unistd.h>int fd = open("data.bin", O_RDWR);size_t len = 4096;char* p = (char*)mmap(nullptr, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);// 直接读写 p,修改通过页缓存回写p[0] = 'A';msync(p, len, MS_SYNC); // 强制刷盘munmap(p, len);
mmap vs read/write:
mmap 适合随机访问大文件、进程间共享(MAP_SHARED)、零拷贝加载。- 小文件用
read 更简单;mmap 映射本身有页表开销,频繁映射/解除小文件反而慢。 MAP_ANONYMOUS 可匿名映射,用作大块堆内存(如 jemalloc 底层)。
零拷贝(zero-copy):sendfile(out_fd, in_fd, ...) 在内核态直接把文件数据搬到 socket,不经过用户态,是高性能文件服务器的关键(如 Nginx 静态文件)。splice / tee 也是零拷贝相关系统调用。面试能讲清"传统 read+write 四次拷贝(磁盘→内核页缓存→用户态→socket 缓冲区→网卡),sendfile 减到两次(DMA)"就是满分点。
8. 进程间通信总览:管道 / FIFO
匿名管道 pipe:半双工,仅用于有亲缘关系的进程(父子),fork 前创建,靠共享 fd 通信。
#include <unistd.h>int pipefd[2];pipe(pipefd); // pipefd[0] 读,pipefd[1] 写pid_t pid = fork();if (pid == 0) { close(pipefd[1]); // 子关闭写端 char buf[64]; read(pipefd[0], buf, sizeof(buf));} else { close(pipefd[0]); // 父关闭读端 write(pipefd[1], "hi", 2);}
命名管道 FIFO:通过文件系统路径(mkfifo)存在,无亲缘关系进程也能通信用,本质仍是内核缓冲区(默认 64KB,PIPE_BUF 保证原子写边界)。
管道容量与原子性:write 小于等于 PIPE_BUF(通常 4096)时原子;超过则可能交错。管道满时写阻塞(除非 O_NONBLOCK);读空时读阻塞。
9. 共享内存:最快的 IPC
共享内存让多个进程映射同一段物理内存,无需内核中转,是速度最快的 IPC,适合海量数据交换(如数据库共享缓冲、游戏状态同步)。
#include<sys/shm.h>#include<sys/ipc.h>key_t key = ftok("/tmp", 'x');int shmid = shmget(key, 4096, IPC_CREAT | 0644);char* p = (char*)shmat(shmid, nullptr, 0); // 映射到本进程// 读写 p ...shmdt(p);// shmctl(shmid, IPC_RMID, nullptr); // 删除
现代推荐用 mmap + MAP_SHARED/shm_open 替代 System V 共享内存(shmget 那套),更符合 POSIX、可用文件语义管理、生命周期更清晰:
#include<sys/mman.h>#include<fcntl.h>int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0644);ftruncate(fd, 4096);char* p = (char*)mmap(nullptr, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
面试重点:共享内存本身不提供同步,必须配信号量/互斥锁(注意锁要放共享内存里且用 PTHREAD_PROCESS_SHARED 属性),否则数据竞争。这是共享内存最大的坑。
10. 消息队列、信号量
System V 消息队列(msgget/msgsnd/msgrcv):按类型收发结构化消息,自带边界与缓冲,但接口老旧、受内核参数限制。POSIX 消息队列(mq_open 等)更现代。
信号量(semaphore):用于进程间同步/互斥。System V 信号量(semget)和 POSIX 命名信号量(sem_open)都可用。
#include<semaphore.h>#include<fcntl.h>sem_t* sem = sem_open("/my_sem", O_CREAT, 0644, 1); // 初值 1(互斥量)sem_wait(sem); // P 操作// 临界区sem_post(sem); // V 操作sem_close(sem);
面试对比:信号量 vs 互斥锁——信号量可跨进程、可计数(控制 N 个资源);pthread_mutex 默认进程内,需 PTHREAD_PROCESS_SHARED 才能跨进程。
11. 域套接字(Unix Domain Socket)
UDS 是同一台机器上进程间通信的最佳选择:像网络 socket 一样用 socket/bind/connect,但走内核缓冲区、不进协议栈、不丢数据、支持传 fd(SCM_RIGHTS)、支持凭证(SCM_CREDENTIALS),且比 TCP 环回快得多。
// server 端(简化)#include<sys/socket.h>#include<sys/un.h>#include<unistd.h>int srv = socket(AF_UNIX, SOCK_STREAM, 0);struct sockaddr_un addr{};addr.sun_family = AF_UNIX;strcpy(addr.sun_path, "/tmp/demo.sock");bind(srv, (sockaddr*)&addr, sizeof(addr));listen(srv, 5);int cli = accept(srv, nullptr, nullptr);// read/write(cli, ...)
面试点:
- UDS 支持传递文件描述符(通过
sendmsg + SCM_RIGHTS 控制消息),这是容器、dbus、systemd 跨进程传递 socket 的底层机制。 - 对比共享内存:UDS 自带同步、有边界、易用;共享内存更快但需自己加锁。实际架构常组合:UDS 做控制信道,共享内存传大数据。
12. 守护进程(daemon)的编写
守护进程是后台长期运行、脱离终端的进程。标准步骤:
fork- 子进程调用
setsid() 创建新会话,脱离控制终端、成为组长。 - 再次
fork(可选,确保非会话首进程,永不获取终端)。 - 重设
umask、关闭/重定向 0/1/2 到 /dev/null 或日志。
#include<unistd.h>#include<sys/stat.h>#include<fcntl.h>voiddaemonize(){ if (fork() > 0) _exit(0); // 父退出 setsid(); // 新会话 if (fork() > 0) _exit(0); // 再 fork 防获取终端 umask(0); chdir("/"); int fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO);}
现代 Linux 推荐用 systemd 托管服务,而非手写 daemon(但面试要懂原理)。daemon() 库函数也能一行完成,但旧式。
13. IPC 选型对比与面试真题
高频真题:
- 父子进程共享哪些资源?(fd、信号处理、地址空间映射(COW)、但栈/堆独立副本)
- 僵尸进程怎么产生、怎么避免?(父不 wait;SIGCHLD + waitpid(WNOHANG) 或二次 fork)
fork 后多线程程序为什么危险?(只复制调用线程,其他线程锁状态丢失,信号安全函数受限)- 最快的 IPC 是什么?需要注意什么?(共享内存;要自己加跨进程锁)
mmap 和 read 的区别?什么时候用 sendfile?(零拷贝;大文件/静态服务)- 进程间能传 fd 吗?怎么传?(能,UDS + SCM_RIGHTS)
SIGKILL 和 SIGTERM 区别?(前者不可捕获,强制;后者可优雅处理)- 守护进程为什么要
setsid、为什么要二次 fork?(脱离终端、确保不重获终端)
小结
Linux 系统编程是 C++ 工程师的底层功力。核心记住三点:进程靠 fork+exec+wait,靠 COW 高效,靠 SIGCHLD/waitpid 清僵尸;文件与内存靠 mmap/sendfile 实现零拷贝;IPC 按"速度/跨机/易用"权衡选型,本机高频用 UDS 或共享内存,跨机用 TCP。
14. 补充:fork + 多线程的经典灾难
这是大厂高频深挖题:在多线程程序中调用 fork,子进程会怎样?
答案:fork 只复制调用 fork 的那个线程,父进程的其他线程在子进程中"消失"。但父进程中那些"消失的线程"可能正持有 malloc 的全局锁、某个 mutex,或正处于写操作中间。子进程继承了这些锁的"被持有状态",却没有任何线程去释放它们。于是子进程一旦调用 malloc、printf(内部加锁)等,就会永久死锁。
// 危险示例std::mutex m;void* worker(void*){ std::lock_guard<std::mutex> lk(m); sleep(100); return nullptr; }intmain(){ pthread_t t; pthread_create(&t, nullptr, worker, nullptr); sleep(1); if (fork() == 0) { // 子进程里,worker 线程不存在,但 m 已被"持有" printf("child\n"); // 可能死锁:printf 内部可能用锁或 malloc _exit(0); } wait(nullptr);}
正确做法:
- 仅在单线程阶段
fork,之后在子进程立刻 exec(exec 会替换地址空间,锁状态随之清除)——这是绝大多数库的约定。 - 或子进程
fork 后只调用 async-signal-safe 函数,随即 exec/_exit,绝不调用 malloc/printf/cout。 - 真正需要"多线程 + 子进程干活",用
posix_spawn 或 vfork + 立即 exec,避免复制多余状态。
面试加分:能讲清这是 POSIX 的已知坑,pthread_atfork 可用于注册 prepare/parent/child 钩子在 fork 前后加解锁,但实践中很难完全正确,故"fork 后只 exec"是铁律。
15. 补充:fcntl 文件锁 flock 与 fcntl 锁的区别
flock(fd, LOCK_EX):对整个文件加建议锁,基于打开文件描述(相同 fd 副本共享锁状态),fork 后子进程继承同一锁,关闭任一副本即释放。不支持锁范围(只能整文件)。fcntl(fd, F_SETLK, ...)(POSIX 记录锁):可锁文件某一段字节区间,基于进程(不同 fd 也按进程区分)。支持读锁/写锁。注意 fcntl 锁是"建议性"的——不阻止无视锁的进程直接读写。
#include<fcntl.h>struct flock fl{};fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 512;fcntl(fd, F_SETLK, &fl); // 锁前 512 字节
面试点:两者语义不同(锁粒度、继承行为),混用会出意外;NFS 等网络文件系统上锁不可靠,生产多用外部协调(如 Redis/ZooKeeper)代替文件锁。能指出"建议性锁 vs 强制性锁(mandatory lock,需挂载 mand 且文件 setgid 位)"体现深度。
16. 补充:inotify 与文件事件监控
监控文件/目录变化的经典机制,inotify_init + inotify_add_watch + read 事件。常用于配置热加载、日志轮转检测、构建工具(如 webpack watch)。
#include<sys/inotify.h>int fd = inotify_init();int wd = inotify_add_watch(fd, "/etc/app", IN_MODIFY | IN_CREATE);// read(fd, buf, size) 读取 inotify_event 结构,得到被改文件与事件类型
面试点:inotify 默认不递归监控子目录;有队列上限(/proc/sys/fs/inotify/max_user_watches),监控大量文件需调大;事件可能合并(同一文件多次修改只报一个事件);文件名在事件里只给名字不给完整路径,需自己拼接。对比 epoll:inotify 的 fd 本身可以用 epoll 监听,集成进事件循环。
17. 真题演练(系统编程)
Q1:open 一个不存在的路径返回什么?errno 是什么? A:返回 -1,并设置 errno(如 ENOENT 文件不存在、EACCES 权限不足)。面试要养成"检查返回值 + perror/strerror(errno)"的习惯,这是系统编程的基本素养。
Q2:read 从 socket 读,返回 0 意味着什么?返回 -1 且 errno==EINTR 呢? A:返回 0 表示对端关闭连接(EOF);返回 -1 且 errno==EINTR 表示被信号中断,不是错误,应重试;errno==EAGAIN/EWOULDBLOCK 表示非阻塞下暂时无数据;errno==ECONNRESET 表示连接被对端重置。
Q3:如何避免 fork 后文件描述符泄漏给子进程? A:对不希望子进程继承的 fd 设置 FD_CLOEXEC(fcntl(fd, F_SETFD, FD_CLOEXEC) 或 open(O_CLOEXEC)),这样 exec 时该 fd 自动关闭。否则子进程继承了不该有的 fd,会导致父进程无法及时关闭(如管道写端不关闭,读端永远 read 不返回 0)。
Q4:mmap 映射文件后,另一个进程删了原文件会怎样? A:映射仍有效——文件数据在 unlink 后只是目录项消失,inode 与数据块在最后一个引用(含 mmap)释放前不会被回收。这正是"删除大文件后磁盘空间不立即释放"的常见原因:某进程仍 mmap 或打开着它。
Q5:进程 core dump 是什么?如何开启? A:core dump 是进程崩溃时内核把内存映像写到磁盘(core 文件),用于 gdb 事后调试。开启:ulimit -c unlimited + echo "/tmp/core.%p" > /proc/sys/kernel/core_pattern。面试能说出"生产环境保留 core 是定位段错误的利器"体现运维素养。
18. 深度专题:文件描述符本质与 dup/fcntl/select 的协作
文件描述符(fd)是进程级的小整数索引,指向内核的"打开文件表项"(open file description),后者含文件偏移、状态标志、inode 指针。多个 fd(同一进程或不同进程)可指向同一打开文件表项——这就是 dup、父子继承、共享内存键集的本质。
int fd = open("a.txt", O_RDWR);int fd2 = dup(fd); // fd2 与 fd 共享同一打开文件表项// 通过 fd2 改偏移,fd 读也会受影响(共享 offset)
fcntl 的常见魔法:
F_GETFD/F_SETFD + FD_CLOEXEC:控制 exec 时是否关闭(防泄漏)。F_GETFL/F_SETFL + O_NONBLOCK:把阻塞 fd 改非阻塞(网络编程关键)。F_SETLK
面试点:fork 后父子 fd 共享偏移(所以管道要先关一端);但 dup 的两个 fd 也共享偏移。O_APPEND 标志保证写永远追加到末尾(原子),多进程写日志安全——面试能讲清 O_APPEND 的原子性区别于"先 lseek 再 write"(非原子,多进程会覆盖)。
19. 真题再加码(系统编程收官)
Q1:read 和 fread 有什么区别?哪个更快? A:read 是系统调用,直接进内核,无缓冲;fread(stdio)带用户态缓冲区,减少系统调用次数,通常更快(尤其小量多次读)。但 fread 有缓冲区,与 read 混用同一 fd 会导致数据错位(两套缓冲不互通)。面试能说出"stdio 缓冲 vs 系统调用"是基本功。
Q2:lseek 到文件末尾之后、甚至超过末尾写入会怎样? A:超过末尾写会在原末尾与新数据间形成"空洞"(hole),不占实际磁盘块(或全零块),读取返回 0。这就是稀疏文件(sparse file),常用于数据库/VMDK 节省空间。可用 lseek(fd, offset, SEEK_END) 创建大稀疏文件而不真占空间。
Q3:如何回收所有子进程避免僵尸,且不被信号打断? A:在 SIGCHLD 处理器里用 while (waitpid(-1, &st, WNOHANG) > 0); 循环回收(WNOHANG 非阻塞,一次信号可能多个子进程结束)。配合 sigaction 的 SA_RESTART 让慢系统调用自动重启。注意处理器里只调 async-signal-safe 函数。
Q4:mmap 的 MAP_PRIVATE 和 MAP_SHARED 区别? A:MAP_SHARED 修改会写回文件、对其他映射/进程可见(用于 IPC 共享内存);MAP_PRIVATE 是写时复制的私有视图,修改只对本进程有效、不影响文件(常用于加载可执行文件/只读映射)。这是"共享内存 IPC"与"私有映射"的分水岭。
Q5:sync / fsync / fdatasync 区别? A:sync 把内核所有脏页刷盘(全局、异步返回);fsync(fd) 保证该文件数据与元数据都落盘(持久化关键,如数据库 commit);fdatasync(fd) 只刷数据、跳过不必要的元数据(如大小没变时少一次写),性能更好。面试能区分三者体现 I/O 正确性与性能意识。
Q6:为什么网络服务常把 SIGPIPE 忽略? A:往已关闭的对端写会触发 SIGPIPE(默认杀进程),但网络程序更希望 write 返回 -1/EPIPE 自己优雅处理。所以启动早期 signal(SIGPIPE, SIG_IGN) 或用 send(..., MSG_NOSIGNAL)。这是网络编程的"保命"设置。
20. 高频速答 20 题(系统编程闪电战)
- 程序到进程流程? 预处理→编译→汇编→链接→execve 建地址空间→动态链接→
_start→main。 fork 后父子共享什么? fd、信号处理、地址空间映射(COW)、但栈堆各自副本。- 写时复制(COW)作用?
- 僵尸进程怎么产生?
- 孤儿进程? 父先死,被子进程被 init(pid 1)收养并回收。
SIGCHLD 怎么异步回收? 处理器里 while(waitpid(-1,&st,WNOHANG)>0)。- 信号处理函数能调
printf 吗? 不能(非 async-signal-safe),只能调 write/_exit/waitpid 等。 SIGKILL 能捕获吗? 不能,强制杀;SIGTERM 可捕获用于优雅退出。SIGPIPE 为何常忽略?fork 在多线程程序为什么危险? 只复制调用线程,其他线程锁状态丢失,子进程调 malloc 可能死锁。O_NONBLOCK 作用? 读写无数据立即返回 EAGAIN,配合 epoll 做事件驱动。pread/pwrite 好处?mmap 相比 read? 免用户态↔内核态拷贝,适合随机访问大文件、共享内存。- 零拷贝
sendfile? - 最快 IPC?
- UDS 比 TCP 环回快在哪?
- 共享内存同步? 必须配信号量/跨进程互斥锁(PTHREAD_PROCESS_SHARED)。
- 守护进程为何
setsid? fsync vs fdatasync?SO_REUSEADDR 作用? 允许绑定处于 TIME_WAIT 的地址,避免 “Address already in use”。
21. 临场速记清单(系统编程篇面试前过一遍)
- 进程靠
fork+exec+wait;COW 让 fork 快;SIGCHLD+waitpid(WNOHANG) 清僵尸。 - 僵尸=父不 wait;孤儿=被 init 收养;
CLOSE_WAIT 堆积=连接泄漏 bug。 - 信号处理函数只调 async-signal-safe 函数;
SIGKILL 不可捕获,SIGTERM 可优雅处理。 fork 在多线程程序危险(只复制调用线程,锁状态丢失),铁律是"fork 后只 exec"。O_NONBLOCK + epoll 才能高并发;O_APPEND 写日志原子;O_DIRECT 绕过页缓存。mmap MAP_SHARED 用于共享内存 IPC,MAP_PRIVATE 是写时复制私有视图。sendfile/splice 零拷贝;read/fread 是系统调用 vs 带缓冲之差。- 最快 IPC 是共享内存(需自加锁);本机服务优先 UDS(可传 fd)。
fcntl 设 FD_CLOEXEC/O_NONBLOCK/F_SETLK;dup 共享文件偏移。fsync 刷数据+元数据,fdatasync 只刷数据;稀疏文件用 lseek 造洞。- 守护进程:
fork→setsid→再 fork→chdir→重定向 0/1/2。 - core dump(
ulimit -c unlimited)是排查段错误的利器。
22. 补充:系统编程易错 Top 6(避坑必看)
- 子进程崩溃变僵尸:父进程忘了
wait/waitpid 回收,pid 耗尽后无法创建新进程。 fork 后多线程调 malloc/printf- 共享内存不加锁:共享内存本身不提供同步,多进程并发写同一区域必数据竞争。
read 不循环读满:一次 read 未必返回请求的全部字节,需用 readn 循环读满 n 字节。- 忽略
SIGPIPE:写已关闭对端触发该信号默认杀进程,网络/管道程序应 SIG_IGN 或 MSG_NOSIGNAL。 - fd 忘关 / 没
FD_CLOEXEC:导致 fd 泄漏给子进程,管道写端不关读端永远 read 不返回 0。