ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux进程与线程深度解析:从fork到IPC的工程实践

Linux进程与线程深度解析:从fork到IPC的工程实践 1. 先搞清楚进程和线程到底谁“重”谁“轻”我经常跟刚接触 Linux 的同学聊一个话题你天天写代码启动一个程序、开一个线程但你有没有想过进程和线程在 Linux 内核里本质上都是同一个东西——task_struct对你没看错。Linux 内核从来不区分“这是进程”“这是线程”它只认“任务”task。你通过fork()创建的是任务通过pthread_create()创建的也是任务。区别只是它们对资源的可见范围不一样有的任务有自己的独立地址空间有的任务共享别人的地址空间。这个理解特别关键。很多面试题上问“进程和线程的区别”标准答案张口就来进程是资源分配的最小单位线程是 CPU 调度的最小单位。这么说没错但你要是真在 Linux 下用ps -eLf看过你会发现线程和进程都有 PID、都有状态、都有优先级看起来一模一样。你甚至可以用kill单独杀掉一个线程tgkill系统调用干的就是这事。所以你要是只背教科书答案实际排查问题时会很懵。我平时排查线上问题最常用的三个命令组合是ps -eLf、top -H -p pid、/proc/pid/status。看什么看主进程和它的子线程各自占了多少 CPU、处于什么状态、缺不缺少线程。但这里有个坑top -H默认只展示线程的“轻量级进程号”LWP你不会直接看到线程名对应的主进程是谁。你得自己知道这个进程组的关系再去/proc/pid/task/目录下面翻每个线程的状态。这篇我就把“父子进程、进程中的线程、不同进程、不同的线程”这四组关系一次讲透。包括 fork 写的时复制到底怎么回事、为什么多线程程序里你千万别随便 fork 之后再调用非 async-signal-safe 的函数、线程之间怎么同步最靠谱、跨进程通信到底选哪套方案。最后我会分享几个实战里踩过的坑每个都是能直接拿去用的经验。2. 父子进程之间恩断义绝的复制品2.1 fork() 到底复制了什么先看一个最基础的场景你在 Linux 写了个 C 程序调一次fork()内核返回两次一次在父进程返回子进程 PID一次在子进程返回 0。是不是觉得特别魔法其实本质上就是复制当前任务子进程刚出生时跟父进程一模一样相同的地址空间内容、相同的打开文件描述符表、相同的环境变量、相同的信号处理器设置。但这里有个绝对误区复制不等于共享。父进程后来改了自己的变量、关闭了自己的文件描述符子进程完全感知不到。反过来也一样。因为它们俩各有各的地址空间各有各的文件描述符表。所以“父子进程”的关系用一句大白话概括就是曾经双胞胎出生即分家此后两家人。而且分家分得很彻底——内存页都给你抠成两份实际上有写时复制优化见下节文件描述符表也独立了。它们之间唯一的联系就是子进程的 PPID父进程 ID指向父进程以及一些残留的资源比如父进程退出后子进程变成孤儿被 init 收养。我还记得第一次用fork()写并发服务器时的困惑我在父进程里 accept 了一个连接fork 出子进程去处理。子进程里我关掉监听 socket只保留连接 socket这段操作没问题。但我一个同事在子进程里写日志时发现日志文件的行 offset 总是跟不上原因是父子进程共享了同一个 file 结构体的 offset因为 fork 复制了文件描述符表但 file 结构体是共享的引用计数加了 1。如果你在子进程里用 C 标准库的fwrite写日志又没调用fsync父进程的偏移量都不会变。这就是著名的“fork 之后文件偏移共享”陷阱。你想让子进程从文件当前位置继续写它确实是继续写但父进程再写两边就可能互相覆盖因为它们的用户态缓冲区是独立的内核里 file 结构体的 f_pos 也是同一个。解决办法是什么要么子进程重新打开文件各写各的 fd要么在 fork 之前把所有该写的写完。2.2 写时复制COW和它带来的性能真相Linux 的fork()之所以没有成为性能灾难全靠写时复制Copy-On-Write。fork()刚创建子进程时并不会把父进程整个地址空间拷贝一份而是让父子进程共享同一批物理内存页并且把这些页统统标记为只读。任何一方想要往内存页里写数据CPU 就会触发缺页异常内核这才把这一页复制一份改好页表权限让那个写操作落在新页上。听哥们儿一句话理解 COW 是看懂 fork 性能曲线的钥匙。一个典型的 fork exec 场景比如 shell 执行一个外部命令父进程内存哪怕有 1 GBfork 也只花不到 1 ms因为 exec 马上就要把子进程地址空间整个换掉那些共享页根本派不上用场。你如果真把内存从 1 GB 扩大到 8 GBfork 的性能差不多还是不变的。真正让它变慢的是页面表本身的复制和缓存刷新的开销而不是内存内容拷贝的开销。不过 COW 也带来一个坑如果你 fork 之后子进程和父进程都疯狂对同一大片内存区域写数据每个页都要复制一次那开销就非常爆炸。实际经验是不要 fork 之后让两边在高频共享内存上同时写。有这需求就该用 mmap 共享内存或换多线程模型。另外一个 fork 相关的老生长谈问题是多线程程序里调用 fork 极容易出幺蛾子glibc 2.25 之后有所缓解。比如你在一个多线程进程里线程 A 持有某个锁然后线程 B 调用fork()。子进程出生时其他线程没了但锁状态还是“被持有着”的——子进程里没有任何线程能释放这个锁后面谁再拿这把锁就死锁。标准做法是什么在fork()的子进程分支里只做_exit()或者调用exec()里面别再用任何复杂的库。如果你必须用锁就调用pthread_atfork()注册回调在 fork 之前把锁全锁上、fork 之后在子进程里全解锁。道理虽然简单实际项目里不少人就是在这里翻车。2.3 父子进程的同步wait/僵尸进程进程间通信轮不到父子进程之间秀什么花活但父子进程之间的“同步”有一件非常朴素的事父进程要等子进程结束。如果你fork()出了子进程从不在子进程上调用waitpid()子进程正常结束后它的 task_struct 和退出码还必须留在内核里等着父进程来“收尸”。这个状态的进程就是僵尸进程Zombie。僵尸进程不占 CPU但占一个 PID 槽位也占一小块内存。积累多了系统可用的 PID 就变少最终表现是 fork 返回错误 “cannot fork”。我见过一个实际案例有个服务主进程每接收一个任务就 fork 一个子进程去处理处理完子进程退出。运维反馈系统偶尔会有一批僵尸进程持续一两天。排查时发现主进程里根本没有 waitpid 循环而是注册了 SIGCHLD 信号处理器。诡异的是用signal()注册 SIGCHLD子进程退出的信号来了处理器也执行了waitpid(-1, status, WNOHANG)但之后又发现很多子进程“死而无尸”。后来查了 glibc 文档才发现用signal()注册的 SIGCHLD 在某些发行版上是带 SA_RESTART 的但waitpid在 EINTR 时可能被信号处理器打断。最后改成sigaction()显式设置 SA_RESTART并循环调用 waitpid问题就没了。所以给一个非常实用的建议fork 之后主进程一定要及时 waitpid 回收每一个子进程除非子进程里的逻辑简单到可以忽略退出码。用信号配合 waitpid 时循环加WNOHANG保证把队列里所有僵尸都收干净。3. 进程中的线程同居室友与资源博弈3.1 线程到底共享了什么同一个进程内部的多个线程共享的东西远比你想象得多地址空间、打开的文件描述符表、当前工作目录、信号处理器、用户 ID/组 ID。不共享的东西其实归结为“执行上下文”每个线程有自己的栈、寄存器现场、线程局部存储TLS、信号掩码。这里有个好处线程之间通信根本不需要任何“跨进程机制”。你定义一个全局变量线程 A 改了线程 B 下次读的时候自然就是新值。性能上这个通信代价为零级。但代价来了——数据竞争。你要是读过一个没有加锁的全局变量在开启-fsanitizethread的编译器下跑一遍测试会在意想不到的角落发现一堆 data race 警告。线程之间的“同步”核心工作永远是围绕在共享数据上排队的。线程的本质是 _clone() 系统调用加一堆共享参数的产物。pthread_create()底层调用 clone 时带上 CLONE_VM、CLONE_FS、CLONE_FILES、CLONE_SIGHAND 这类标志表示新任务要共享地址空间、文件系统信息、文件描述符表、信号处理器表。所以线程也被叫做“轻量级进程”LWP。但从内核调度视角看它又确确实实是一个独立的 task独立参与调度、独立占 CPU 时间。去/proc/pid/status看线程数你会看到“Threads: N”这一行那里统计的就是该进程下所有线程的数目。如果你跑一个 Java 应用里面线程池开了 200 个线程那你/proc/pid/status的 Threads 就是 200 左右别惊讶这是正常的。3.2 线程之间为什么必须锁一个不加锁的教训我记得有次帮人排查一个日志系统。逻辑很简单多线程每处理一条日志就把一个全局计数器加 1最后落盘。最开始用的代码是// 全局变量 unsigned long g_counter 0; void onEvent(void) { g_counter; // 这里没有同步 }在单线程下没有任何问题但多线程一跑计数就对不上甚至出现小于真实值的情况。原因很简单g_counter不是原子操作它底层是“从内存读值、寄存器加一、写回内存”三步。线程 A 和线程 B 同时读到旧值各自加一写回时后写回的覆盖先写回的计数就少了。正确的姿势是至少用 C11 的atomic_fetch_add或者用互斥锁保护起来或者干脆用 GCC 内置的__atomic_add_fetch(g_counter, 1, __ATOMIC_SEQ_CST)。看起来只是一个小问题但线上日志量一大错误的计数直接会导致告警误报和报表不准。我给团队一条死规定所有跨线程共享的变量要么加锁要么声明为原子变量不允许裸写裸读。代码 Review 时看到任何私有的非 const 全局变量必须能说清楚被谁读、被谁写、有没有同步机制。这条规则看似严格但它实实在在减少了线上 bug。3.3 线程之间的同步三大件锁、条件变量、原子操作线程之间除了共享更重要的是有序协作。锁只是互斥——保护临界区不让两个线程同时写。但更常见的情形是一个线程等另一个线程完成某件事例如生产者-消费者模型消费者要等队列非空才能取数据。这里推荐用互斥锁 条件变量而不是自旋锁硬忙等。典型代码结构pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER; unsigned int queue_len 0; // 生产者 void produce(void) { pthread_mutex_lock(mtx); queue_len; pthread_cond_signal(cond); pthread_mutex_unlock(mtx); } // 消费者 void consume(void) { pthread_mutex_lock(mtx); while (queue_len 0) { pthread_cond_wait(cond, mtx); } queue_len--; pthread_mutex_unlock(mtx); }关键点有三个条件变量 wait 时要配 while 而不是 if防止虚假唤醒。pthread_cond_signal最好在持有锁的时候调用免得丢唤醒。等待条件的线程醒来之后会自动重新获取互斥锁所以排队逻辑都在锁内完成。原子操作则适合更简单的场景——一个计数器、一个标志位。atomic_compare_exchange可以做到无锁编程里的状态转移比如 CAS 循环更新链表头。但我的建议是别轻易在大型共享结构上自造无锁算法你以为是内存屏障的万能实际线上一跑ABA、内存序、缓存行伪共享全来了。真要无锁用现成库比如 liburcu、boost.lockfree它们的坑都被削平了。3.4 线程“调度的公平性”问题不同线程之间听起来大家都在同一个进程里应该风平浪静。但实际上CFS 调度器完全公平调度器是对 task 调度的不是对“进程”调度的。线程和进程在调度器眼里都是 task。如果一个进程里开了 8 个线程另一个进程只有 1 个线程那前者能拿到更多 CPU 时间吗默认的 CFS 有一个“nice 值”分摊机制大体上多线程进程会占便宜。不过如果你开了 CPU 亲和性设置taskset或sched_setaffinity线程被固定到某些 CPU 核心上之后调度策略又不一样了。还有一个和调度相关的经典问题线程优先级反转。低优先级线程持有锁高优先级线程在等锁中优先级线程一直抢跑导致高优先级线程迟迟拿不到 CPU。严格说这是实时系统话题但在普通 Linux 上也有影响。解决办法通常是pthread_mutexattr_setprotocol(attr, PTHREAD_PRIO_INHERIT)给互斥锁设优先级继承让持有锁的低优先级线程暂时提高优先级。项目里要是做音视频处理这类延迟敏感的服务这一条是必须考虑的。4. 不同进程之间通信靠 IPC内存各占一亩三分地4.1 进程间为什么非要 IPC进程地址空间完全隔离这是操作系统安全性和稳定性的基石。A 进程崩了B 进程不该跟着崩。但业务上进程之间需要通信Nginx 的 worker 进程从 master 进程读配置、数据库进程通过网络和客户端进程交换数据……所以就得有通信机制。Linux 下 IPC 的选择非常多我列一个实用对照表通信方式特点典型场景性能量级管道pipe/FIFO简单单向适合流式数据shell 命令串联、父子进程传数据内存拷贝中等共享内存shm/mmap最快但要自己同步高吞吐、低延迟数据交换近乎零拷贝最快消息队列POSIX mq带优先级、持久化但系统调用开销生产者消费者解耦中等偏慢SocketUnix domain/TCP语义丰富跨网络可用分布式服务、多机通信来回拷贝较慢信号控制面不适合传数据通知退出、中断极轻不传数据你去看微服务里的进程间通信绝大多数走的是 TCP 或者 Unix domain socket。本地进程通信用 Unix domain socket 比 TCP 省去协议栈开销性能能提升 30% 上下。不过你说到底高吞吐场景比如 Redis Cluster 节点间同步数据还是会考虑共享内存方式。4.2 共享内存 信号量Linux 最快的内核 IPC 组合如果你在做数据采集、中间件开发进程之间要传几个 GB 的吞吐共享内存 POSIX 信号量是你的第一选择。做法很简单用shm_open创建或打开一个 POSIX 共享内存对象然后ftruncate设置大小再mmap映射到进程地址空间。用sem_open创建或打开一个信号量抢到信号量的进程操作共享内存操作完释放。用munmap映射解除最后shm_unlink清理对象。我调过的一版代码大致是// 进程A int fd shm_open(/myshm, O_CREAT | O_RDWR, 0666); ftruncate(fd, 4096); void* addr mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); sem_t* sem sem_open(/mysem, O_CREAT, 0666, 1); // 写入前拿锁 sem_wait(sem); sprintf((char*)addr, hello from A); sem_post(sem);这样的模式能拿到单机原生速度一条 20 字节消息走共享内存从进程 A 到进程 B往返延迟可能不到 1 微秒同一条走 Unix domain socket大概要 3~5 微秒走 TCP loopback可能就要 20 微秒以上。差距非常直观。但共享内存有个补丁要打你要小心翼翼地设计环形缓冲区否则生产者消费者会互相踩。最简单的是做一个“单生产者单消费者”的无锁环形队列用两个内存屏障实现。如果要多个生产者多个消费者那还是要上信号量或自旋锁。4.3 fork 之后的 exec 和子进程独立性的联系刚才说的“父子进程各过各的”其实还有一个终止路径exec()。fork()出来跑到一半进程可以调用exec()系列函数把当前进程的地址空间全部替换成新程序。这个时候进程的 PID 不变巧了exec 不创建新进程但它的代码段、数据段、堆栈全部换成新程序的。这解释了为什么 shell 执行外部命令时先 fork 一个子进程、再在子进程里 exec 目标命令父 shell 还在原地等着。exec 和父子进程有什么关系对“子进程”这个身份来说exec 是它摆脱父亲代码逻辑、变成全新程序的办法。fork exec 这对组合是现代 Unix 派生新程序的法宝。底层的系统调用是execve()shell 里的bash -c、编程语言里的subprocess.Popen(/bin/ls)走的全是这一套路。这时候子进程和父进程之间除了 PID/PPID 这层血缘基本就没有任何代码级共享了。4.4 守护进程、孤儿进程和会话的关系长时间跑的进程比如后台服务一般都会 fork 一次、setsid、再 fork 一次把自己变成守护进程。第一次 fork 可以让未来会话首进程不再拥有控制终端setsid()创建新会话脱离原来的进程组和会话第二次 fork 确保不再次成为会话首进程避免重新获取控制终端。这是经典的“双 fork 守护进程法”。线程有没有类似的“会话”线程本身没有 session 概念只有进程有。当你说“杀掉一个进程组”的时候kill(-pgid, SIGTERM)会把所有属于该进程组的进程都杀掉但不会自动把某个进程里的线程也杀掉——不过 SIGTERM 给进程的默认动作是终止整个进程所以该进程的线程也确实会一起消失。这里有个容易混淆的点线程死了进程不一定死进程死了线程必死无疑。因为进程是线程的容器内核销毁进程时会把它所有线程一并销毁。所以你排查问题时看到某个线程崩溃导致 core dump实际挂掉的是整个进程。5. 不同线程之间互斥、死锁和调试的艺术5.1 同进程线程之间不需要 IPC但需要纪律线程之间因为是共享内存通信基本靠读写变量根本没有“IPC”这套概念。但这也带来了一个隐患共享变量的同步纪律必须自己管。内核不会替你检查编译器不会替你纠错只有人管人。写多线程程序的先决条件就是想清楚“状态归属”。哪些状态属于具体某个线程放栈上、放线程局部存储里哪些状态属于进程全局加锁保护哪些状态是不可变的只读安全共享。我见过很多出 bug 的项目都是把所有东西一锅端声明成全局变量。实际项目里更常见的其实不是“控制共享”而是“并发调度”线程池大小怎么定、任务队列怎么喂、线程怎么优雅退出。这些不是纯语言层面的东西但要会设计。5.2 死锁四条件与破解思路线程同步里最经典的老难题是死锁。要死锁必须同时满足四个条件互斥资源同一时刻只能被一个线程占用。持有并等待线程占着一个资源又等着别的资源。不可剥夺资源只能主动释放。循环等待线程 A 等线程 B 的资源线程 B 等线程 A 的资源。破解思路基本就是破坏其中一个条件。最简单的是锁顺序约定所有线程加锁的顺序必须一致比如先锁 A 再锁 B。这样只要有一个人拿到 A它一定会再去拿 B不会出现 A 等待 B、B 等待 A 的循环。实践上的陷阱在于代码一多锁的层级关系经常被破坏。我用过一个工具叫lockdepLinux 内核里也有同名机制是死锁检测的神器。用户态可以跑 Thread Sanitizer 或者 Helgrind它们可以发现运行路径上的潜在死锁。不过静态分析工具能覆盖的场景有限关键还是设计时别用多把锁交叉保护同一批资源。5.3 原子指令和内存序无锁编程的底线如果你做高并发计数器、状态机这类“窄临界区”完全可以用原子操作替代锁。GCC 提供的__atomic_*内置函数、C11 的stdatomic.h、C11 的atomic都是映射到 CPU 的原子指令比如 x86 的LOCK CMPXCHG。原子操作的好处是不阻塞线程不会因为锁而让线程睡眠和唤醒延迟最低。但原子操作一旦涉及顺序就得小心内存序memory order。默认的memory_order_seq_cst可以保证全局顺序但开销略高memory_order_acquire/release则适合“我写完数据你再读”的场景release 写保证之前的所有普通写操作同步到其他线程acquire 读保证之后的所有普通读操作不会越过这个读。举个例子std::atomicbool ready{false}; std::string payload; // 线程1 payload hello; ready.store(true, std::memory_order_release); // 线程2 if (ready.load(std::memory_order_acquire)) { // 这里读 payload 一定是 hello }如果你乱用 relaxed 内存序在某些架构比如 ARM上线程 2 有可能在ready变成 true 之后读到payload的旧值。这种 bug 很难复现也难定位。所以我常用的经验是非极端性能需求默认全用 seq_cst要优化了才换 acquire/releaserelaxed 只用在“只统计不依赖顺序”的场景。5.4 线程调试三板斧strace / gdb / gstack多线程 bug 排查时下面这几个工具我都是常驻现场的strace -f -p pid跟踪进程所有线程的系统调用看哪个线程卡在哪个 syscall 上。gdb -p pid直接 attach 正在跑的进程thread apply all bt看所有线程的调用栈。pstack/gstackgstack更推荐快速打印所有线程的栈比 gdb attach 更省事。perf top、perf record看各线程的 CPU 占比和热点函数。现场排查线程问题的核心思路先定位是哪个线程出问题再判断它挂在什么调用上最后查上下文。有一次一个服务响应卡住top -H里看不出异常CPU 也不高。我用gstack一看发现一堆线程都卡在__lll_lock_wait上进一步看发现拿到锁的线程堵在了一个磁盘 I/O 的read上磁盘慢导致锁一直不释放。最后我们给磁盘加缓存把 I/O 挪到独立线程池问题解决。这类问题只有结合“锁等待 系统调用”一起看才能定位。6. 实战排查四个经典案例复盘6.1 场景一多线程进程频繁 fork 导致锁残留背景是个监控 agent它有一个主线程负责采集指标另一个线程定时执行用户命令。命令执行走的是fork exec。监控线程用的是多线程库内部有自己的锁。上线一周后agent 频繁出现 exec 出来的子进程启动到一半卡死CPU 正常但任务不推进。排查过程用strace -f挂子进程发现子进程卡在futex等待一个锁上但没有任何其他线程持有。后来查代码确认是 fork 在 exec 前那一刻其他线程的某个锁恰好处于持锁状态而子进程除了调用 fork 的线程以外其他线程都没有了于是直接死锁。修复方案给 fork 调用准备pthread_atfork在 prepare 阶段把进程内比较关键的库锁都锁上在 parent 阶段解锁child 阶段重置锁。更重要的是后来我们把采集功能改成不用多线程而是用主循环 非阻塞 I/O彻底规避 fork 加锁的问题。结论多线程程序别轻易 fork除非你非常清楚哪些锁在 fork 瞬间是干净的。6.2 场景二线程池里有线程“消失”了另一回一个 C 网关服务日志显示客户端连接数在涨但处理线程池可用线程数在降。top -H -p pid一看线程总数确实少了说明有线程退出后没有被重新创建。看代码线程池的 worker 循环里某个异常导致线程直接退出而线程池的“补充线程”逻辑没有覆盖这种退出路径。修复很简单worker 函数外面包一层循环子线程因异常退出后重新投递一个任务到线程池或者干脆设置一个监控线程定期检查线程数并补足。教训线程池必须定义“线程意外死亡”后的恢复策略否则你再多的哨兵监控也白搭。6.3 场景三僵尸进程堆积导致 PID 耗尽经典案例前面提过主进程 fork 了子进程但没有 waitpid子进程退出后变成僵尸。僵尸进程多了PID 空间被占满后面 fork 直接返回EAGAIN。排查命令就三招# 数一数僵尸进程 ps -eo stat,ppid,pid,comm | awk $1Z {print $2} | sort | uniq -c # 看是不是有父进程忘了回收 ps -eo pid,ppid,stat,cmd | grep defunct # 如果是 init 的僵尸多半是孤儿不用慌张修复就一行父进程加 waitpid 循环或者直接忽略 SIGCHLDsignal(SIGCHLD, SIG_IGN)这样子进程退出后内核会把它自动回收。对很多“不关心子进程状态码”的后台程序忽略 SIGCHLD 是最省心的一招。6.4 场景四线程里全局变量计数不准前面讲过了但这里补一个实际操作经验如果你用gcc编译直接裸跑g_counter在默认优化级别下编译出的汇编确实是普通内存读改写但如果你开-O2编译器有可能把它优化成寄存器操作再写回。不管怎么优化多核下都会丢更新。修的最终版是用 C11 原子操作#include stdatomic.h atomic_ulong g_counter; void onEvent(void) { atomic_fetch_add_explicit(g_counter, 1, memory_order_relaxed); }这里用 relaxed 就够因为计数只做统计、不依赖顺序relaxed 的性能最好。把内存序吃透以后你就能知道哪里该严、哪里可以松。7. 面试考点与自我检测清单这篇文章本身就是 Linux 面试高频区。我按自己招人时喜欢问的方向给你整理几个必须能答上来的点进程和线程在内核里的本质区别都是 task区别在资源可见范围。fork 之后父子进程共享什么不共享什么共享文件偏移、打开的文件表不共享地址空间、锁状态。线程共享什么不共享什么共享地址空间、文件描述符表、信号处理器不共享栈、寄存器现场、线程局部存储。僵尸进程怎么产生、怎么处理子进程退出无人回收父进程 waitpid 或忽略 SIGCHLD。线程间同步手段互斥锁、条件变量、原子操作、读写锁、信号量。进程间通信手段管道、共享内存、消息队列、Socket、信号。写时复制原理fork 共享只读页写时分裂页表。多线程进程 fork 风险锁残留导致死锁需要 pthread_atfork 或避免 fork。死锁四条件、破解思路、如何验证是否死锁4 个条件同时满足才会死锁破解任一条件即可。原子操作和锁的性能差异前者无阻塞后者可能睡眠窄临界区用原子宽临界区用锁。CFS 如何调度线程对 task 调度多线程进程会相对多占 CPU但受负载均衡影响。守护进程怎么创建fork setsid 再 fork 改工作目录 重定向 fd。孤儿进程怎么办被 initPID 1收养。线程池和进程池怎么设计任务队列、工作线程循环、优雅退出。每一条背后都有真实工程场景支撑能结合案例讲清楚绝对比死背概念强得多。面试官问“进程和线程的区别”你直接抛内核 task_struct 的角度马上就跟普通候选人的答案拉开了层次。8. 个人经验我最终怎么看待这组关系Linux 上这些概念的掌握不在于背熟定义而在于调试时能不能第一时间反应出问题属于哪个层面。比如服务卡了你第一反应是看进程状态还是线程状态如果现象是整个服务回复变慢多半是锁竞争或者磁盘 I/O如果现象是一个请求卡住另一些请求正常多半是某个线程死锁或者等待条件变量。如果ps -ef看到一堆 defunct说明是 fork 生命周期管理出了问题。如果 shm 服务异常重启问题多半是共享内存映射、权限、或者信号量残留。我在实际项目里体会最深的一件事用“进程/线程”这种二元思维理解 Linux永远差着一层。更准确的模型是“资源容器 执行单元”。进程是资源的容器线程是容器里跑的执行单元。fork 创建了一个新的容器clone 创建了一个新的执行单元可以把它放到别人的容器里也可以放到新容器里。理解了这层面试题、线上排查、系统设计全都通了。另外一个小技巧多线程程序的线程名在 Linux 下是可以自定义的用pthread_setname_np()设置后top -H或htop里看起来一目了然。项目进到一个阶段别只停留在“会写并发代码”要学会“让代码自己在进程列表里说话”。最后再补一个细节如果你用 JavaJVM 里的用户线程和内核线程是 1:1 映射的所以 Java 线程池的线程数在系统层面就是等量 LWP。调优时别光看 JVM 指标也该看系统线程状态。Python 的 GIL 又是另一种故事但原理还是同一套共享资源要同步多线程不一定更快多进程也未必更稳关键看你的瓶颈在哪里。
返回列表