当前位置:首页>Linux>面试宝典(三):Linux系统编程与IPC

面试宝典(三):Linux系统编程与IPC

  • 2026-09-12 21:25:45
面试宝典(三):Linux系统编程与IPC

目录

  1. 从 hello 到进程:程序的执行流程
  2. 进程创建:fork 与写时复制(COW)
  3. exec
     族与进程替换
  4. 进程等待:wait / waitpid 与僵尸进程
  5. 信号处理:异步事件的双刃剑
  6. 文件 I/O:阻塞、非阻塞、O_DIRECT
  7. mmap
     内存映射与零拷贝思想
  8. 进程间通信总览:管道 / FIFO
  9. 共享内存:最快的 IPC
  10. 消息队列、信号量
  11. 域套接字(Unix Domain Socket)
  12. 守护进程(daemon)的编写
  13. IPC 选型对比与面试真题

1. 从 hello 到进程:程序的执行流程

一个 C++ 程序从源码到进程运行,经历:

  1. 预处理 → 编译 → 汇编 → 链接
     生成 ELF 可执行文件。
  2. 用户执行 ./a.out,shell 调用 fork() + execve()。
  3. 内核 execve 读 ELF,建立虚拟地址空间,映射代码/数据段。
  4. 加载动态链接器(如 /lib64/ld-linux-x86-64.so.2),重定位共享库。
  5. 跳到 _start → __libc_start_main → 调用 main。
  6. 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 回收,不会变僵尸。
  • 子先死、父不 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)的编写

守护进程是后台长期运行、脱离终端的进程。标准步骤:

  1. fork
     后父进程退出,子进程成为孤儿被 init 收养。
  2. 子进程调用 setsid() 创建新会话,脱离控制终端、成为组长。
  3. 再次 fork(可选,确保非会话首进程,永不获取终端)。
  4. 重设 umask、关闭/重定向 0/1/2 到 /dev/null 或日志。
  5. 切换工作目录(如 /)避免占用挂载点。
#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 选型对比与面试真题

IPC 方式
速度
跨机
亲缘要求
同步
典型场景
匿名管道
中
否
有
自带
shell 流水线
命名管道 FIFO
中
否
无
自带
简单本地通信
共享内存
最快
否
无
需自加
大数据共享
消息队列
中
否
无
自带
结构化消息
信号量
—
否
无
是
同步原语
域套接字
快
否
无
自带
本机服务通信
TCP 环回
较慢
是
无
自带
跨机/通用

高频真题:

  1. 父子进程共享哪些资源?(fd、信号处理、地址空间映射(COW)、但栈/堆独立副本)
  2. 僵尸进程怎么产生、怎么避免?(父不 wait;SIGCHLD + waitpid(WNOHANG) 或二次 fork)
  3. fork
     后多线程程序为什么危险?(只复制调用线程,其他线程锁状态丢失,信号安全函数受限)
  4. 最快的 IPC 是什么?需要注意什么?(共享内存;要自己加跨进程锁)
  5. mmap
     和 read 的区别?什么时候用 sendfile?(零拷贝;大文件/静态服务)
  6. 进程间能传 fd 吗?怎么传?(能,UDS + SCM_RIGHTS)
  7. SIGKILL
     和 SIGTERM 区别?(前者不可捕获,强制;后者可优雅处理)
  8. 守护进程为什么要 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
    :记录锁(见第 15 节)。

面试点: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 题(系统编程闪电战)

  1. 程序到进程流程?
     预处理→编译→汇编→链接→execve 建地址空间→动态链接→_start→main。
  2. fork 后父子共享什么?
     fd、信号处理、地址空间映射(COW)、但栈堆各自副本。
  3. 写时复制(COW)作用?
     让 fork 极快,写入时才真正复制页。
  4. 僵尸进程怎么产生?
     子进程退出、父进程不 wait 回收。
  5. 孤儿进程?
     父先死,被子进程被 init(pid 1)收养并回收。
  6. SIGCHLD 怎么异步回收?
     处理器里 while(waitpid(-1,&st,WNOHANG)>0)。
  7. 信号处理函数能调 printf 吗?
     不能(非 async-signal-safe),只能调 write/_exit/waitpid 等。
  8. SIGKILL 能捕获吗?
     不能,强制杀;SIGTERM 可捕获用于优雅退出。
  9. SIGPIPE 为何常忽略?
     写已关闭对端会触发,默认杀进程,网络服务多忽略。
  10. fork 在多线程程序为什么危险?
     只复制调用线程,其他线程锁状态丢失,子进程调 malloc 可能死锁。
  11. O_NONBLOCK 作用?
     读写无数据立即返回 EAGAIN,配合 epoll 做事件驱动。
  12. pread/pwrite 好处?
     带偏移原子读写,多线程并发不同区域安全。
  13. mmap 相比 read?
     免用户态↔内核态拷贝,适合随机访问大文件、共享内存。
  14. 零拷贝 sendfile?
     内核内直接文件→socket,不经用户态。
  15. 最快 IPC?
     共享内存(需自加锁)。
  16. UDS 比 TCP 环回快在哪?
     走内核缓冲不进协议栈,支持传 fd/凭证。
  17. 共享内存同步?
     必须配信号量/跨进程互斥锁(PTHREAD_PROCESS_SHARED)。
  18. 守护进程为何 setsid?
     脱离控制终端、创建新会话。
  19. fsync vs fdatasync?
     前者刷数据+元数据,后者只刷数据(性能更好)。
  20. 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(避坑必看)

  1. 子进程崩溃变僵尸
    :父进程忘了 wait/waitpid 回收,pid 耗尽后无法创建新进程。
  2. fork 后多线程调 malloc/printf
    :其他线程持有的锁状态在子进程丢失,导致死锁。
  3. 共享内存不加锁
    :共享内存本身不提供同步,多进程并发写同一区域必数据竞争。
  4. read 不循环读满
    :一次 read 未必返回请求的全部字节,需用 readn 循环读满 n 字节。
  5. 忽略 SIGPIPE
    :写已关闭对端触发该信号默认杀进程,网络/管道程序应 SIG_IGN 或 MSG_NOSIGNAL。
  6. fd 忘关 / 没 FD_CLOEXEC
    :导致 fd 泄漏给子进程,管道写端不关读端永远 read 不返回 0。

最新文章

随机文章