1. 基础概念:先把进程和线程的“人设”搞清楚
1.1 进程是资源分配单位,线程是调度执行单位
面试聊到进程和线程,几乎每次都是从“区别”开场。很多人能背出这句标准答案——“进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位”,但真被追问下去就露馅了。所以别急着背,先把这句话拆开看。
进程是操作系统里面真正“拥有财产”的那一方。每个进程都有独立的虚拟地址空间、独立的页表、独立的文件描述符表、独立的信号处理机制。你开一个程序,系统就给它划分一块地盘,这块地盘里代码段、数据段、堆、栈各归各位,进程之间默认谁也碰不到谁的私有财产。这就是为什么一个进程崩溃了,通常不会直接搞挂另一个进程——它们的地盘是隔离的。
线程就不一样了。线程是进程里面的“干活的人”,同一个进程下的所有线程共享这个进程的地址空间、文件描述符、全局变量、堆内存等资源。线程自己只保留一小块独立的东西:线程ID、栈、程序计数器、寄存器上下文、线程局部存储(TLS)等。也就是说,线程是个“轻装上阵”的执行流,它不占有系统资源,只跟兄弟线程共享一个家里的财产。
面试官问到这个层次还不够,我一般会接着问一句:既然线程只多了栈和寄存器,那你觉得线程切换和进程切换,谁更贵?答案是进程切换更贵,因为要切换页表、刷新TLB,还要保存和恢复整个地址空间相关的上下文。线程切换虽然也要切换上下文,但不需要切换地址空间,代价小很多。这也就是为什么高并发场景都喜欢用多线程而不是多进程,省的就是这份开销。
1.2 从fork到写时复制:概念背后的底层机制
光会背定义远远不够,面试官会往深处扎。最典型的问题就是:fork之后,父子进程到底共享什么?很多人答“共享内存”,这对了一半,但底层实现比你想象的更巧妙。
现代Linux的fork用的是写时复制(Copy-On-Write,COW)机制。fork出来的子进程并不会立刻复制父进程的整个地址空间,而是让父子进程的虚拟地址空间映射到同一批物理页面上,并把这些页面标记为只读。如果大家都只读,那谁都不需要复制,省了内存也省了拷贝时间。一旦有一方要写这个页面,就会触发缺页异常,内核这才让出一份物理页给写的那一方,然后解除共享。所以“共享”是一种惰性的、按需的共享,不是一上来就把所有东西复制一份。
理解了COW,很多追问就能应付了。比如:fork之后子进程先exec,性能会怎么样?答案是exec会直接把子进程的地址空间替换成新的程序镜像,所以fork之后紧接着exec,COW的“按需复制”几乎不会生效,效率很高。这也是历史上vfork出现的原因——vfork就是专门为了fork之后马上exec这种场景做的优化,父子进程共享地址空间,父进程还要等子进程exec完才能继续跑。虽然现在Linux下vfork已经比较边缘了,但面试里偶尔还会被拎出来问。
再往下追一层,就是clone系统调用。Linux下创建线程用的其实是clone,不是fork。fork和clone的区别在于flag参数——fork是复制完整进程环境,而clone可以精确控制哪些资源要共享、哪些要复制。创建线程时传入CLONE_VM、CLONE_FS、CLONE_FILES等标志,表示地址空间、文件系统信息、文件描述符表全都共享。这也是为什么Linux的线程有时候被叫做“轻量级进程”(Lightweight Process,LWP),因为它本质上是由clone创建出来的、共享了大部分资源的特殊进程。面试中能把这个层次讲清楚,基本就让面试官看到你读过内核相关的东西了。
1.3 线程到底有多少独享的东西
聊清楚“共享什么”之后,最好把“独享什么”也一并讲透。前面说线程独享栈、寄存器、程序计数器、TLS,那“栈”到底是多大?这其实是个很实际的问题。Linux下面默认的线程栈大小通常是8MB(通过ulimit -s查看),但这不是绝对的,不同发行版、不同架构会有差异。写代码的时候如果开递归特别深的函数,或者在线程栈上分配大数组,很容易把栈压爆,直接段错误,而且这种bug很难排查,core文件都经常看不出名堂。
线程独享的寄存器上下文也好理解——每个线程都有一套自己的寄存器状态,特别是栈指针SP和指令指针PC,这俩决定了线程“现在执行到哪了”。线程切换的时候,内核把当前线程的寄存器状态保存到它的内核栈里,再把下一个线程的寄存器状态恢复出来,让CPU接着跑。这个过程非常底层,但理解了它,你就理解了为什么线程调度看起来像“同时跑”但其实是“轮流跑”——尤其在单核机器上,所谓并发就是时间片轮转的结果。
TLS(Thread Local Storage)是个容易被忽略的点。面试问“怎么让多个线程各保存各的数据”,答案不是加锁,而是用TLS。C/C++里是__thread关键字,Java里是ThreadLocal。底层实现思路类似——编译器把TLS变量放到特殊的段里,每个线程通过自己的线程控制块找到对应的偏移地址,这样同名变量在不同线程里就指向不同的存储位置。这个机制在中间件、链路追踪SDK里面非常常见,我面试的时候很爱问这个,因为能答上来的人说明他真写过服务器程序,而不只是背过八股。
2. 生命周期:从创建到退出,状态的每一步都是考点
2.1 进程状态机:从创建到退出
进程的经典三态是“运行、就绪、阻塞”,但真实的Linux进程状态远不止这三种。ps命令里看到的R(running)、S(sleeping)、D(disk sleep)、T(stopped)、Z(zombie)、X(dead),每一个都对应具体的内核状态。
面试中我最常问的就是D状态——不可中断睡眠(uninterruptible sleep)。这个状态的特点是,进程在等待某些内核资源或磁盘I/O,期间不能被信号打断,甚至kill -9都杀不掉。很多运维新手遇到D状态进程就慌了,以为系统被什么东西卡死了。其实D状态的进程只要I/O完成就会自动恢复,真正要担心的是大量进程长期处于D状态,那基本说明磁盘子系统出问题了,比如NFS连接挂了、磁盘硬件故障、内核I/O路径卡死。答这个问题的时候能主动提到D状态,说明你真的在业务环境里处理过故障,而不是只看了教科书里的三态图。
T状态也值得一说。Ctrl+Z把前台作业挂起,进程就进入T状态。面试官可能顺嘴问一句“怎么把后台挂起的进程恢复”,答案有两个:fg恢复到前台继续跑,或者bg放到后台继续跑。jobs命令可以查看当前shell挂起了哪些任务。这些基础知识虽然简单,但在真实的“怎么管理开发机上的多个任务”场景里非常实用,属于那种“不问觉得简单,一追问又容易嘴瓢”的题。
2.2 僵尸进程和孤儿进程:面试必考的两兄弟
僵尸进程(zombie)和孤儿进程(orphan)是Linux面试里百分之百会碰到的CP,很多人在这里翻车。我先把定义说清楚:
- 僵尸进程:子进程已经退出了,但父进程还没有调用wait/waitpid来收尸,子进程的进程描述符(task_struct)仍然保留在系统里。此时子进程的状态就是Z,它不占CPU也不占内存,但占着一个进程表项(PID资源)。
- 孤儿进程:父进程先于子进程退出,子进程变成孤儿,被init进程(PID为1)收养。init会负责回收这些孤儿的退出状态,所以孤儿进程不会变成僵尸。
我见过的最大误解是:把僵尸进程当成“还能跑起来的进程”,或者以为僵尸进程会持续消耗大量内存。真实情况是,僵尸进程已经死了,它只是“尸体”没被收走。僵尸的问题不在性能,而在PID——每个PID都是有限的资源,如果父进程一直不收尸,僵尸进程越积越多,最终会耗尽系统的PID上限,导致无法创建新进程。
怎么排查僵尸进程?一条命令搞定:
ps -ef | grep defunct或者看统计:
ps -e -o stat,ppid,pid,cmd | grep '^Z'处理僵尸进程的常规手段是杀掉它的父进程,让init接管并回收。但生产环境里直接kill父进程风险很大,更好的思路是从代码层面解决:父进程用waitpid配合SIGCHLD信号处理来主动回收子进程,或者在fork之后设置signal(SIGCHLD, SIG_IGN),让内核自动帮父进程回收子进程资源。不过要提醒一句,设置SIG_IGN虽然省事,但某些场景下你会因此拿不到子进程的退出码,属于“按下葫芦浮起瓢”,用之前得想清楚。
2.3 线程也有自己的“生命周期”
线程的生命周期和进程类似,但问法不同。Java那边爱问“线程的六种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED”,C/C++那边爱问“join和detach的区别”。
join的本质也是“等待收尸”——主线程调用thread.join()会阻塞在那里,直到目标线程执行完毕并释放其资源。detach则把线程变成“后台运行”的,与主线程脱离关系,线程结束后由运行时库自动回收资源。我面试时喜欢加一个追问:detach之后,线程执行到一半,主线程退出,会怎么样?答案是进程一旦退出,所有线程都会被强制终止。detach只是让你不再需要join它,不代表它能脱离进程存活。
还有一个跟热词相关的点——“java线程等待都完成”。这个需求在实际开发里太常见了。Thread.join()可以等单个线程,CountDownLatch可以等一批线程到达某个点,ExecutorService.shutdown()配合awaitTermination()可以优雅关闭线程池并等待任务全部完成,CompletableFuture.allOf()则是把多个异步任务组合起来统一等待。面试答这个题的时候,一定要带上使用场景:比如主线程要等所有子线程把结果算完再做汇总,用哪个最合适?这个问题的标准答法是分业务来看,简单等齐用join、要控制并发数用线程池、要聚合结果用CompletableFuture,不能一个API打天下。
3. 并发编程核心问题:同步、互斥与死锁
3.1 竞争条件:多线程编程里最隐蔽的坑
进程和线程最大的不同在于“共享”,而共享带来的最大问题就是“竞争”。面试官聊到这里,通常会拿一个经典例子开刀:两个线程同时对全局变量count执行count++,循环一万次,最终结果一定是20000吗?
答案是否定的。因为count++不是一个原子操作,它至少要经历三条指令:读取count到寄存器、寄存器加1、写回count。两个线程可能同时在“读-改-写”的中间状态里交错,导致一次累加被覆盖。最终结果可能只有一万多点。这个例子虽然老掉牙了,但它背后藏着一个面试官真正想听的点——临界区(critical section)的概念。同一时刻只允许一个线程进入的那段代码区域,就是临界区,而保护临界区的机制就是各种同步原语。
应对竞争条件,入门方案是加锁。互斥锁(mutex)保证同一时刻只有一个线程能进入临界区;读写锁(rwlock)区分读多写少的场景,允许读锁共享、写锁独占;自旋锁(spinlock)则是在锁被占用时忙等待,不切换线程,适合临界区特别短的场景。很多人以为这是理论,但在Linux内核代码里,spinlock的使用比mutex还频繁,因为内核临界区要求不能睡眠,忙等待反而是正的。
我建议面试答题时带上一句话:锁不是银弹,锁竞争本身会降低并发度。能用原子操作解决的问题不要用锁,能用无锁设计规避的问题不要引入锁。这种表达会让面试官觉得你对并发有大局观,而不是只会背API。
3.2 实用同步机制:mutex、条件变量、信号量怎么选
聊完竞争条件,自然会追问:有哪些同步手段,它们各自适合什么场景?我一般建议把这三种拆开讲。
互斥锁(mutex)是最基础的“令牌”,同一时刻只允许一个线程持有。它的特点是排他性特别强,适合保护临界区,但对“等某个条件满足再执行”这种场景无能为力。条件变量(condition variable)就是补这个空缺的:线程A等待某个条件(比如队列非空),线程B往队列里放数据后通知A,A被唤醒后再去做进一步判断。这跟“生产者-消费者”模型强绑定。
信号量(semaphore)是个容易混的概念。它本质上是个计数器,可以允许多个线程同时访问有限数量的资源(比如数据库连接池里有5个连接)。POSIX信号量分有名信号量和无名信号量两种,进程间通信和线程同步都能用。面试有一个高频陷阱题:mutex和binary semaphore(二值信号量)有什么区别?很多人答不上来,其实关键区别有两个:一是mutex有所有权概念,谁加的锁只能由谁解锁,semaphore没有;二是mutex支持优先级继承,可以缓解优先级反转问题,semaphore不行。能答到这两个层次的,基本可以秒杀大多数候选人。
我刚入行的时候做C++服务端,曾经把一个应该用mutex的地方写成了semaphore,结果线程之间互相释放对方的“锁”,整个程序的行为完全随机。从那以后我就记住了一个原则:编程技巧可以少炫一点,但同步原语的语义必须搞懂,否则线上出问题时连排查方向都没有。
3.3 死锁的四个必要条件与排查实操
死锁是面试里最经典的“背题点”,因为考纲太清晰了——死锁的四个必要条件是互斥、持有并等待、不可剥夺、循环等待。但光背出这四个名词只能说及格,面试官紧接着就会问:怎么防止死锁?
防止死锁的思路就是破坏四个条件中的任意一个:互斥条件在业务上往往没法破坏(临界区就是要互斥);持有并等待可以通过一次性申请所有资源来破坏;不可剥夺可以通过锁超时来破坏;循环等待可以通过固定锁的顺序来破坏。代码里最常用的手段是“锁排序”——所有线程必须按照同一个顺序加锁,比如先锁A再锁B,禁止出现线程1锁A等B、线程2锁B等A的情况。这个原理简单,但工程上真正执行起来超级容易被忽视,尤其在大型项目里,调用链一深,排锁顺序很容易失守。
真遇到线上死锁了怎么排查?我给你一套实战路径。如果是Java服务,用jstack pid,grep一下“Found one Java-level deadlock”,它能直接给出一段环状提示,标明哪个线程持有哪把锁、在等哪把锁,非常方便。如果是C/C++服务,可以用gdb -p pid,thread apply all bt打印所有线程的调用栈,然后人工观察有没有互相等待的迹象;也可以用pstack pid快速打印栈信息。另外,top -H -p pid可以先看每个线程的CPU占用,死锁的线程通常CPU是0,但状态是阻塞。
热词里“线程死锁”排得很靠前,说明大家都被这块折磨过。我想补充一个容易被忽略的点:Linux下的文件锁(flock/fcntl)也可能造成死锁。两个进程分别锁住文件A和文件B,然后各自尝试去锁对方的文件,一样会死锁。内核检测到这种情况时,fcntl会返回EDEADLK错误,但前提是你得检查返回值——很多人忽略了这点,导致错误信息被吞掉了。
4. 进程间通信(IPC):六种武器全解析
4.1 进程间通信为什么比线程同步更“麻烦”
聊完线程同步,就该聊进程间通信(IPC)了。面试官对这块的要求不只是懂原理,更看重你能不能根据场景选对方案。Linux下的IPC手段大概有:管道(pipe/FIFO)、信号(signal)、消息队列(message queue)、共享内存(shared memory)、信号量(semaphore)、Socket,以及比较新的AIO(异步I/O)和基于netlink的机制。
为什么进程间通信比线程同步麻烦?原因在于进程地址空间是隔离的。线程间共享内存,写一个全局变量对方就能看到,但进程之间不能直接访问对方的内存地址,必须“绕道”在内核里走一圈,由内核充当信使或者中间人。这就带来两个代价:一是数据要拷贝(从用户态拷贝到内核态,再拷贝到目标进程的用户态),二是要进行一次系统调用,性能远不如线程同步那么轻量。
4.2 管道与共享内存:性能的两端
管道(pipe)是最古老的IPC方式了,它本质上是内核里的一块环形缓冲区。匿名管道只能在有亲缘关系的进程间使用,典型的场景就是shell命令里的管道符,比如ps aux | grep java。命名管道(FIFO)则通过文件系统里的一个特殊文件打通不相干进程之间的通道,两个进程只要约好打开同一个FIFO文件,就能双向或单向传数据。
管道的特点是“流式”,没有消息边界。什么意思?就是写入端的字节流是拼在一起的,读端无法知道消息从哪里断哪里续。这个特性导致管道适合传“连续的数据流”,但不太适合传“结构化消息”。另一个大坑是阻塞问题——默认情况下,读取一个没有数据的管道会阻塞,写入一个已满的管道也会阻塞。这些细节在面试中都是很好的追问点,能给面试官留下“真做过”的印象。
共享内存(shared memory)是性能最极端的IPC方式:它绕过了“用户态-内核态-用户态”的两次数据拷贝,让两个进程直接映射到同一块物理内存。读写共享内存就跟读写自己的内存一样快,因此它是性能要求最高的场景的首选方案,比如大流量下的日志缓冲、跨进程传图片/视频帧等。但共享内存有两个麻烦:第一,它是裸的,没有任何同步机制,必须配合信号量或互斥锁来保护;第二,生命周期是内核级的,进程退出后共享内存段不会自动消失,需要手动shmctl(IPC_RMID)回收,否则系统里会积累一堆僵死的共享内存段。用ipcs -m可以查看当前系统的共享内存段,ipcrm -m shmid可以删除,这两个命令对运维排查很有用。
4.3 信号、消息队列、Socket:各自的主场
信号(signal)是最“轻”的IPC,但它不适合传数据,更适合做“事件通知”。比如Ctrl+C产生SIGINT终止前台进程,修改终端窗口大小产生SIGWINCH,kill -9 pid发送SIGKILL。由于信号处理函数是异步执行的,里面能做的事非常有限——不能调用非异步信号安全的函数(比如printf、malloc),否则可能引发死锁或数据损坏。这个话题在“多线程环境下信号如何处理”里会进一步放大,比较复杂,面试中能说出“信号和处理函数的异步安全问题”就比只说“kill命令”高级很多。
消息队列(message queue)是有消息边界的IPC,每个消息有类型和长度,读端可以选择按类型读取。它比管道更好控制,但也有经典局限:单条消息有大小上限、消息队列总容量有上限,而且拷贝依旧发生两次。POSIX消息队列还提供了一种“接收时可以指定优先级”的能力,这在某些实时场景很有用,比如优先级高的报警消息必须插队先读。
Socket是一把万能钥匙,它不只是网络通信的IPC,还能在同一台机器的不同进程间走本地socket(Unix domain socket)通信。本地socket比TCP socket更快,因为不走网络协议栈,纯内核内部的数据拷贝。很多高性能中间件内部都用本地socket来组合多进程架构,比如某些数据库的代理进程和存储引擎进程之间。面试遇到“两个不相关进程在同一个机器上要大量数据交互,怎么选”这样的问题,答本地socket或共享内存都是不错的答案,区别在于你愿不愿意接受共享内存的同步复杂度。
我在这块有一个经验心得:面试官问IPC的目的,通常是考察你是否理解“不同IPC的成本差异”和“不同业务的读写模型”。所以答题的时候不要对着列表背,而是先说需求场景再选方案。比如:要传几十GB的日志文件,选共享内存还是socket?真正合理的做法往往是落地到磁盘再批量处理,或者直接走共享内存的分片+流式传输方案,单纯选一个单词回答,反而显得浅。
5. 实战技能:用命令排查进程线程问题
5.1 top、ps、/proc:三板斧看穿系统
面试一般不会只考理论,多少会夹带一两个“现场排障”的问题。最经典的就是:线上CPU飙高,怎么定位到具体是哪个进程的哪个线程在搞事?
第一步,用top查看全局负载。按1可以展开每个CPU核心的使用率,看看是不是只有单核被打满。如果是单核100%而其他核空闲,那多半是“单线程脚本/单线程循环”的典型症状;如果所有核都接近100%,那可能是计算密集型任务同时吃满了所有CPU,也可能是一个多线程程序把所有核都调度满了。
第二步,找到嫌疑进程后用top -H -p <pid>查看该进程下所有线程的CPU占用,这时候你能看到线程级的使用率。记下占用最高的线程PID(注意是LWP,不是进程PID),用printf "%x\n" <tid>转成十六进制,因为这个值是要用在jstack里的nid。
第三步,如果Java服务,jstack <pid> | grep <十六进制线程id> -A 20,就能定位到是哪个业务代码的哪一行在烧CPU。如果是C/C++服务,pstack <pid>打印线程栈,或者用gdb attach再thread apply all bt。这套组合拳在面试现场说出来,面试官基本就知道你实战过。
另外,/proc文件系统也是Linux排障的核心工具。/proc/<pid>/status可以看进程状态、线程数、内存情况;/proc/<pid>/task目录下列出所有线程;/proc/<pid>/fd目录列出所有打开的文件描述符,排查句柄泄漏非常有用。有一次线上出现进程频繁报“too many open files”,我就是靠ls -l /proc/<pid>/fd | wc -l来确认文件描述符数量暴涨,再逐个比对fd指向的路径,最终定位到一个没有关闭的日志文件句柄。
5.2 线程数与系统资源的量化关系
面试经常问一个很现实的问题:一台机器上到底能创建多少线程?这个问题没有固定答案,但有几个硬性指标可以用来推导。
第一个指标是内存。每个线程都要一块独立栈空间,默认8MB(ulimit -s查看),如果机器可用内存只剩4GB,那理论上最多也就500来个线程的余量。当然实际不会全部分配满,线程栈按页分配,进程启动时先虚拟占位,真正用到哪些页才物理分配。但即便如此,线程数一旦上千,内存和上下文切换开销都是非常可观的。
第二个指标是pid上限。Linux默认的pid_max通常是32768,也就是说系统同时存在的进程/线程数大概在三万左右。你可能会说“线程不是进程,也占pid吗?”答案是占。Linux线程也是clone创建的进程,同样占用一个PID。所以sysctl kernel.pid_max这个值决定了整个系统能存活的最大线程总数。
第三个指标是单个进程能开的线程数,受ulimit -u(max user processes)限制。如果这个值设得太小,线程池疯狂扩张时就会碰到Cannot create new thread的报错。实际生产环境里,线程数并不是越多越好,因为CPU核心数有限,线程多了就是疯狂地切换上下文,CPU时间全耗在线程调度上了。这也是线程池和协程存在的价值所在——线程池控制线程数量上限,协程则把“调度”这件事搬到用户态,用更轻量的方式承载并发。想在这块给面试官留下印象,可以提一嘴自己用top观察过单机上万线程时sys/irq占比高得吓人的经历。
5.3 常见进程相关故障排查实录
结合热搜词里的“u盘无法弹出,请先结束占用进程”“vmware另一个程序已锁定文件一部分”这两个典型问题,我分享一下类似故障的排查思路。
“设备被占用无法弹出/卸载”的本质是:某个进程打开了设备或文件,并且没有释放文件描述符。Linux下有个神器叫lsof,可以列出所有打开文件的进程。排查步骤很直接:lsof /media/usb或者lsof | grep sd,看到哪个PID还霸占着设备,再用ps -fp <pid>确认进程,确认安全后kill掉。如果是文件被占用,还有一个办法:fuser -km /path可以强制杀死所有访问这个路径的进程,但生产环境用之前必须确认杀哪些进程。
VMware那个“文件被锁定”的报错,本质上是同一份虚拟机磁盘文件被两个进程同时打开导致冲突,或者上一次异常退出之后锁文件没有被清理掉。解决方案通常是删除工作目录下的.lck目录,或者确保只有一个VMware实例运行在同一份vmdk文件上。这类问题在面试里一般不会单独考,但它能体现出一个人的“排障思维”——先确认是什么文件、被谁打开、为什么锁住,再决定是清理锁文件还是先杀占用进程。
热词里还有一条“mate-indicators进程可关闭吗”,这属于桌面环境组件。这类系统级小组件进程通常对应桌面的托盘图标、输入法状态等,手动杀掉会暂时消失,但桌面环境会自动把它拉起来,一般不建议随意kill。面试谈到“哪些进程不能乱杀”的时候,可以顺带提一嘴:从内核线程到桌面环境守护进程,再到业务服务进程,优先级和风险等级完全不同,不能一“杀”了事。
6. 面试高频真题与有效答题思路
6.1 概念与原理题:怎么说才能显得有深度
我把面试常见的概念题整理成一张速查表,方便你模拟自测。答这些题不要只答一句话,最好做到“一句话结论+两个细节+一个场景”。
| 问题 | 一句话结论 | 加分细节 | 推荐场景 |
|---|---|---|---|
| 进程和线程的区别 | 进程是资源分配单位,线程是调度执行单位 | Linux下线程也是clone创建的特殊进程 | 容器隔离为什么用进程,高并发为什么用线程 |
| 线程和协程的区别 | 线程由内核调度,协程由用户态调度 | 协程切换不陷入内核,成本极低 | 高并发IO密集型服务为什么用协程 |
| 上下文切换是什么 | 保存当前任务状态、恢复下一个任务状态的过程 | 进程切换要换页表,线程切换不需要 | 为什么多线程比多进程更适合高并发 |
| 什么是死锁 | 多个任务互相持有资源并循环等待 | 死锁四要素、锁顺序、超时机制 | 分布式锁如何避免死锁 |
| 什么是僵尸进程 | 子进程退出后父进程未收尸 | waitpid/SIGCHLD信号处理 | 高并发服务为何要避免僵尸进程堆积 |
面试官问“进程和线程的区别”的时候,你可以在基础答案上加一句:“在Linux上,线程其实是clone系统调用创建出来的,它和进程的本质区别是资源和地址空间是否共享。因此严格说,Linux线程和进程是同一套调度实体,区别在于资源共享程度。”这句话一出来,就和其他候选人拉开了差距。
6.2 场景设计题:用业务去驱动技术方案
面试最爱出的是场景题,因为这种题目不是靠背能过的,需要你真正理解技术选型背后的trade-off。比如:你要设计一个高并发的日志采集系统,进程间传日志应该用什么方案?
合理思路是先分场景。如果日志量大、实时性要求高、且业务都在同一台机器上,共享内存是最优解——性能最好,但需要自己解决同步和崩溃恢复问题。如果日志要送到远程服务器,那本地socket或消息队列更合适,因为涉及到网络传输,进程内IPCS的性能优势反而不重要了。如果各业务模块之间关联性不强、只想做一次异步解耦,直接走磁盘中间文件或者消息中间件(比如Kafka)反而是最稳的。答案没有唯一标准,但面试官想听的是:你知不知道各种IPC的取舍,会不会根据业务场景做技术决策。
再看一个典型的线程池场景题:给一个IO密集型任务设计一个线程池,核心参数怎么定?IO密集型的经验法则是线程数可以大于CPU核心数,一般推荐2 * CPU核心数甚至更多,因为大部分线程在等IO,CPU闲着也是闲着。而CPU密集型任务线程数最好保持在CPU核心数 + 1,避免过多线程互相争抢CPU时间片导致切换开销增大。当然这只是经验值,真正上线前要用压测拉曲线。这种题能结合具体业务场景说清楚推理过程,面试官一般就比较满意了。
6.3 手撕题与命令题:代码和命令一个都不能少
最后一种题型是和“输出代码/命令”强相关的。这里最常见的坑是:手写生产者-消费者模型。线程池、消息队列、任务调度很多场景都由这个模型派生出来。关键点有三处:
第一,必须定义一个有限容量的缓冲区(队列);第二,生产者往队列放数据、消费者从队列取数据,两边都要加锁;第三,队列满时生产者要等待,队列空时消费者要等待——这就用到了条件变量。我在面试中见过太多人把线程安全的任务队列写成了无锁的裸队列,一并发就丢数据。写完之后最好自己画一遍:队列满时生产者阻塞在wait上,消费者消费后调用notify,生产者被唤醒后要重新检查队列状态(为什么?因为可能被虚假唤醒,所以必须用while循环重新判断)。
命令题也很关键。面试官给你一台线上Linux机器,你能说出哪些命令用于排查进程和线程?我列个最常用的清单:
ps -ef # 查看进程列表 ps -eLf # 查看线程列表(带LWP) top -H -p <pid> # 按线程维度看CPU和内存 htop # 交互式查看进程/线程 pstree -p <pid> # 看进程树 lsof -p <pid> # 查看进程打开的文件 strace -p <pid> # 跟踪进程系统调用 jstack <pid> # 打印Java线程栈 pstack <pid> # 打印C/C++线程栈 cat /proc/<pid>/status # 查看进程详细信息 cat /proc/<pid>/stack # 查看内核栈(需要权限)面试到最后可以主动总结一句“工具只能帮我们缩小排查范围,真正定位根因还得靠对进程模型和内核机制的理解”。这句话既显得谦逊,又暗示自己不仅仅停留在玩命令的层面。
我个人在截了很多次线上故障之后的一个体会是:面试官其实并不指望你把所有命令都背下来,更想看到的是你的“问题定位路径”。比如CPU高的时候,你的第一反应是什么?是直接kill掉进程重来,还是先看线程栈、看请求量、看GC日志、看慢日志?这个思考顺序才是区分老手和新手的地方。把你平时在真实环境里排障的路径,用口语化的方式讲出来,比背十道八股都管用。