01Q请描述 TCP 三次握手建立连接、四次挥手断开连接的完整过程。为什么建立连接必须是三次两次/四次都不行主动关闭方为什么必须进入 TIME_WAIT为什么是 2MSL会带来什么问题有哪些优化手段一、三次握手过程第一次握手客户端调用connect向服务端发送 SYN 报文携带客户端随机生成的初始序列号 ISN(c)客户端进入SYN_SENT。第二次握手服务端收到 SYN 后回复 SYNACK 报文确认号为 ISN(c)1SYN 标志占用一个序号同时携带服务端自己的初始序列号 ISN(s)服务端为此连接创建 request_sock 放入半连接队列进入SYN_RCVD。第三次握手客户端收到 SYNACK 后回复 ACK确认号为 ISN(s)1该 ACK 报文允许携带数据也可以为空。服务端收到 ACK 后连接从半连接队列移入全连接队列双方都进入ESTABLISHED连接建立随后服务端accept()取走连接。二、四次挥手过程第一次挥手主动关闭方假设是客户端数据发送完毕调用close发送 FIN进入FIN_WAIT_1。第二次挥手被动方收到 FIN 后立即回复 ACK确认号为 FIN 序号1进入CLOSE_WAIT主动方收到 ACK 后进入FIN_WAIT_2。此时处于半关闭状态主动方不再发送数据但被动方若还有未发完的数据可以继续发主动方仍要接收。第三次挥手被动方数据全部发完后调用close发送自己的 FIN进入LAST_ACK。第四次挥手主动方收到 FIN 后回复 ACK随即进入TIME_WAIT被动方收到 ACK 后直接进入CLOSED主动方等待2MSL后进入 CLOSED。注意TIME_WAIT 永远在主动关闭方被动方没有 TIME_WAIT。三、为什么必须是三次握手两次为什么不行无法防止历史上过期的重复连接请求。假设客户端发出的 SYN1 在网络中滞留客户端超时重发了 SYN2 并正常完成通信、关闭了连接此后 SYN1 才到达服务端。两次握手下服务端发出 SYNACK 就单方面建立连接、分配 socket 和缓冲区资源但客户端根本不认这个连接会直接回复 RST 拒绝——在 RST 到达前服务端的资源已经白白占用攻击者只需不断发送这种过期 SYN就能耗尽服务端资源。三次握手下服务端必须再收到客户端的 ACK 才建立完整连接客户端发现自己没发过 SYN 却收到 SYNACK就知道是过期报文不会回 ACK而是回 RST连接无法建立。此外三次握手还能完整确认双向通道的收发能力并同步双方 ISN第一次握手证明客户端能发、服务端能收第二次握手让客户端确认服务端能收能发、自己能收第三次握手才让服务端确认客户端能收。两次握手缺少最后这一确认。四次为什么多余理论上告知我方 ISN和确认对方 ISN可以分成两个报文SYN、ACK 各一次双向共四次但建立连接时被动方通常没有数据需要先发送ACK 可以搭在 SYN 报文里合并发送所以三次是理论最小值再拆一次纯属冗余。形成对照的是挥手时被动方收到 FIN 的那一刻可能还有数据没发完ACK 必须立刻回否则对方会重传 FIN而 FIN 要等数据发完才能发二者无法合并所以挥手天然是四次。四、TIME_WAIT 的作用与 2MSL主动关闭方必须停留 2MSL原因有二保证最后一个 ACK 可靠到达如果第四次挥手的 ACK 丢失被动方会超时重传 FIN主动方停留在 TIME_WAIT 期间仍能收到重传的 FIN 并再次回复 ACK从而正常关闭。若主动方直接 CLOSED重传 FIN 到达时只能回 RST被动方会认为连接异常。让本连接的旧报文在网络中自然消亡等待期间本次连接残留的延迟报文都会超过 MSL 而被丢弃防止它们被误认为是下一个相同四元组新连接的数据。MSLMaximum Segment Lifetime是报文段在网络中的最大生存时间。2MSL 的计算是一来一回主动方发出的 ACK 最多 1 个 MSL 到达对端若丢失对端重传的 FIN 最多再 1 个 MSL 到达主动方合计 2MSL 足以覆盖最坏情况。Linux 中 MSL 固定为 30 秒TIME_WAIT 时长为 60 秒。五、TIME_WAIT 带来的问题与优化问题高并发短连接场景下主动关闭方会快速积累大量 TIME_WAIT 连接每个连接占用一个四元组槽位和少量内核内存当本机作为客户端向固定对端发起大量连接时可能耗尽本地端口导致connect失败Cannot assign requested address。常见优化手段改用长连接 连接池如 HTTP Keep-Alive从源头减少连接的建立与关闭次数是最根本的手段让 TIME_WAIT 落在对端服务端不主动关闭连接、由客户端先关TIME_WAIT 分散到海量客户机上SO_REUSEADDR允许 bind 处于 TIME_WAIT 的地址端口主要解决服务端程序重启时端口被占用的问题net.ipv4.tcp_tw_reuse1允许主动连接方安全复用 TIME_WAIT 的四元组依赖 TCP 时间戳防旧报文需同时开启 timestamps这才是解决客户端端口耗尽的关键参数02Q请说明 epoll 的水平触发LT和边缘触发ET的核心区别以及各自的使用场景。【参考答案】水平触发 LTLevel Triggered默认模式只要 fd 处于就绪状态——读缓冲区里还有数据可读或写缓冲区还有空间可写——每次调用epoll_wait通知相当于条件一直满足就一直通知。本次没处理完没关系下次epoll_wait还会再通知事件不会丢失。LT 对阻塞、非阻塞 fd 都兼容编程简单、不易出错代价是同一就绪状态可能反复唤醒、epoll_wait返回次数多系统调用开销略大。边缘触发 ETEdge Triggered只在 fd 状态发生跳变时通知一次——例如epoll_wait调用一次就会把该项从就绪链表移除此后即使缓冲区里还有数据没读完也不会再通知直到下一次新的数据到达产生新的边沿。因此 ET 模式下必须第一把 fd 设置为非阻塞第二收到通知后循环 read/write 直到返回 EAGAIN或 EWOULDBLOCK确保一次把数据读干、写满。如果用阻塞 fd最后一次没有数据的 read 会把线程永久阻塞导致其他所有连接饿死如果没读到 EAGAIN 就返回剩余数据不会再触发通知该连接会被饿死。使用场景LT 是默认且通用的选择适合连接数中等、逻辑复杂、追求稳妥的网络程序阻塞/非阻塞写法都能工作Redis 的事件循环就使用 LT。ET 适合高并发、连接数庞大、追求极致性能的场景它把缓冲区有数据这一状态的多次通知压缩为一次减少epoll_wait的返回次数和重复遍历但要求开发者完全掌控非阻塞 IO 的读写循环Nginx 即采用 ET。此外还有 EPOLLONESHOT 选项一个 fd 的事件只通知一次处理完后必须用epoll_ctl重新武装常用于多线程环境下保证同一个 fd 同一时刻只被一个线程处理。03Q请描述进程的六种状态及状态间的转换条件并说明 Linux 下父进程、子进程、僵尸进程、孤儿进程分别是什么关系。一、六种进程状态ps 中显示RRunning / Runnable运行或就绪正在 CPU 上运行或在运行队列中等待被调度两种情况都显示为 R。SInterruptible Sleep可中断睡眠进程在等待某个事件或资源如等待网络数据、管道输入、条件变量或执行sleep可以被信号唤醒。DUninterruptible Sleep不可中断睡眠通常因发起阻塞式磁盘 IO 等内核操作进入等待 IO 完成期间不响应任何信号SIGKILL 也杀不掉只能等内核唤醒这是为了保证内核 IO 流程不被打断。T / tStopped / Traced停止收到 SIGSTOP、SIGTSTP终端下 CtrlZ 发送的就是 SIGTSTP等信号后暂停执行小写 t 表示被调试器 ptrace 跟踪、停在断点处。ZZombie僵尸进程已经exit终止但父进程尚未调用wait/waitpid回收内核仍保留它的 task_struct、退出码和资源统计ps 中显示为 Z 或 defunct。XDead死亡父进程回收后资源彻底释放的终态瞬时存在正常观察不到。二、状态转换条件R → S进程主动等待某事件等待 IO、等待锁、sleep放弃 CPUR → D进程发起不可中断的阻塞 IO典型为磁盘读写R → T收到 SIGSTOP / SIGTSTP 等停止信号或被调试器拦截在断点R → Z → X进程调用exit或致命信号终止后先成为僵尸 Z父进程wait回收后变为 XS → R等待的事件发生数据到达、资源可用、信号到达被唤醒后回到运行队列D → RIO 完成后由内核唤醒D 状态期间信号不能中断它T → R收到SIGCONT信号恢复运行。特别时间片耗尽是 R→R从运行变为就绪仍在运行队列不是进入睡眠。三、父进程、子进程、僵尸进程、孤儿进程父进程与子进程父进程调用fork创建子进程父进程中fork返回子进程的 PID子进程中返回 0。fork 的瞬间子进程获得父进程虚拟地址空间的一份副本——代码、数据、堆、栈内容相同文件描述符表、环境变量、工作目录、信号处理方式等也被继承但二者拥有独立的页表和独立的地址空间底层物理页通过写时复制COW共享任一方修改数据时内核才复制对应物理页修改互不影响。PID、PPID、文件锁、未决闹钟等不被继承。僵尸进程子进程退出后内核必须保留它的退出状态等信息供父进程查询若父进程既不wait/waitpid也不处理 SIGCHLD子进程就一直处于 Z 状态。危害是每个僵尸仍占一个 task_struct 和一个 PID僵尸积累过多会耗尽 PID 资源导致系统无法创建新进程。处理方式父进程主动wait/waitpid或注册 SIGCHLD 信号处理函数在其中异步回收也可将 SIGCHLD 处置设为 SIG_IGN由内核自动回收若父进程本身有问题可杀掉父进程使僵尸变为孤儿进程由 PID 1 回收。孤儿进程父进程先退出、子进程仍在运行该子进程即为孤儿进程它会被 PID 1 的 init现代系统为 systemd容器内是其 1 号进程还存在 subreaper 机制领养此后由 1 号进程负责wait回收。孤儿进程本身无害因为有 1 号进程兜底它退出后不会变成僵尸。04Qepoll 为什么比 select/poll 性能高select/poll 的性能开销第一每次调用都要把完整的 fd 集合从用户态拷贝到内核态监听 n 个 fd 就是 O(n) 的拷贝第二内核每次都要把当前线程挂接到这 n 个 fd 各自的设备等待队列上然后线性遍历全部 fd检查就绪返回前再逐一摘除掉也是 O(n)第三用户态拿到结果后还得把 n 个 fd 全部遍历一遍才能找出就绪的又是 O(n)。此外select 额外有 1024 的 fd_set 硬上限且其位图会被内核改写、每轮必须重新设置。epoll 把注册监听和等待就绪拆成了两类操作从而避免了上述重复劳动fd 集合只注册一次通过epoll_ctl把 fd 包装成 epitem 插入内核中的红黑树并在该 fd 的设备等待队列上挂接一次回调函数增删改是 O(log n)此后无论epoll_wait调用多少次都不需要重新拷贝全量集合、不需要重复挂接等待队列。事件驱动代替轮询fd 就绪时设备驱动触发回调ep_poll_callback自动把对应 epitem 挂入就绪双向链表内核不需要在每次等待时扫描全部 fd。只返回就绪 fdepoll_wait直接把就绪链表中的 fd 拷贝给用户态返回复杂度是O(k)k 为本次就绪的 fd 数与监听总数 n 无关用户态也无需全量遍历。监听数量没有 1024 硬上限仅受内存和进程句柄数限制epoll_ctl还可以在其他线程中安全地增删 fd。需要注意边界情况epoll 的优势建立在连接总数大、但每次只有少量连接活跃的场景典型如高并发长连接。如果所有连接在每次等待时都有事件就绪例如局域网内流量被打满那么就绪处理本身就是 O(n)epoll 与 poll 的差距会明显缩小。05Q进程和线程之间的核心区别是什么多进程和多线程分别适合什么业务场景资源维度进程是资源分配的基本单位每个进程拥有独立的虚拟地址空间和页表、独立的文件描述符表、独立的堆、信号处理表以及用户/组身份线程隶属于某个进程同一进程的多个线程共享代码段、堆、全局数据、文件描述符表、信号处理函数和当前工作目录但每个线程私有自己的栈、寄存器上下文PC、SP、线程 ID、errno、信号掩码、调度优先级和线程局部存储TLS。调度维度线程是 CPU 调度的基本单位一个进程由一个或多个线程组成。Linux 在内核中统一用 task_struct 描述进程可理解为拥有独立地址空间的线程组。健壮性维度进程间资源隔离一个进程崩溃通常不影响其他进程线程共享地址空间一个线程发生非法内存访问会导致整个进程崩溃。切换开销维度进程创建、销毁要初始化和回收整套资源切换时需要更换地址空间、切换页表并刷新 TLB开销大同一进程内的线程切换不更换地址空间只需保存 PC、栈指针和通用寄存器等上下文开销小。通信维度进程间通信必须借助 IPC管道、消息队列、共享内存、信号、socket 等机制复杂但隔离清晰线程间可以直接读写同一进程的全局变量和堆通信天然高效但必须用互斥锁、条件变量、读写锁等同步手段解决竞态问题。适用场景多线程适合高并发、IO 密集、任务间需要频繁共享大量数据、追求低切换成本的场景如 Web 服务器的线程池、聊天服务、交易网关CPU 密集型任务在多核机器上也可用多线程并行C/C 无解释器锁限制。多进程适合对隔离性、容错性和安全性要求高的场景例如 Chrome 每个站点/标签页使用独立进程防止一个页面崩溃拖垮浏览器并隔离安全风险Nginx 使用多个 worker 进程一个 worker 异常不影响整体服务特权服务也常用独立子进程做权限沙箱。工程上常见二者混合多 worker 进程做隔离与多核利用进程内部再用事件驱动或多线程处理并发。06什么是虚拟内存它主要解决了直接使用物理内存的哪些问题虚拟内存是操作系统借助 CPU 的内存管理单元MMU在进程虚拟地址与物理内存地址之间建立的一层地址抽象。每个进程都拥有一个独立的、私有的、大小固定的连续虚拟地址空间32 位系统为 4GB64 位系统通常使用 48 位地址、用户态约 128TB/256TB进程访问的永远是虚拟地址由 MMU 通过多级页表翻译成物理地址翻译结果缓存在 TLB 中缺页时触发缺页中断由内核处理。进程完全不需要关心物理内存的实际分布。它解决了直接使用物理内存的以下问题地址冲突问题物理内存模式下多个程序直接操作物理地址链接和加载时必须互相避让无法方便地同时运行。有了虚拟内存每个进程都可以从相同的虚拟地址开始布局如代码段固定加载在某虚拟地址互不干扰编译器和链接器也无需关心物理位置共享库还能通过位置无关代码映射到各进程的任意虚拟地址。安全隔离问题每个进程的页表独立一个进程无法访问其他进程的物理内存页表项上的读写/执行权限位和内核态/用户态位还能保护内核空间和只读代码段越界访问未映射或无权限的页面会触发段错误从硬件层面实现了进程隔离与保护。内存利用率问题通过按需分页malloc 后首次访问才真正分配物理页、写时复制fork 后父子共享物理页写入才复制、页共享共享库、共享内存只存一份物理副本和swap 交换不活跃页换出到磁盘系统可以超售内存运行总需求超过物理内存容量的程序。物理碎片问题与编程简化连续的虚拟页可以映射到任意离散的物理页框物理内存的外部碎片对进程不可见配合 mmap还可以把文件、设备统一映射为内存进行访问。代价是地址翻译需要多级页表查询TLB 缺失时有开销、页表本身占用内存以及 swap 换入换出可能引起明显的性能抖动。07Q简述 TCP 粘包产生的原因和常见的解决方案。产生原因TCP 是面向字节流的协议协议栈只保证字节按序、可靠到达不保留应用层每次 write 的消息边界。具体来自两侧发送端Nagle 算法会把多个小数据包攒在一起合并发送而大于 MSS 的消息又会被分段成多个报文接收端数据先进入内核接收缓冲区若应用读取不及时多条消息会堆积在一起一次recv可能读出两条消息的拼接粘包也可能只读出一条消息的前半段半包/拆包。常见解决方案固定长度消息约定每条消息占固定字节数不足则补齐接收方每次按固定长度读取实现最简单但短消息浪费带宽适用消息长度高度一致的场景。特殊分隔符在消息末尾追加约定的分隔符如\r\n接收方逐字节扫描到分隔符即得到一条完整消息。HTTP 头部、Redis 的 RESP 协议、FTP 都采用这种方式要注意消息体内部出现分隔符时必须做转义。消息头 消息体长度前缀最常用定义固定长度的消息头头部中用固定字段如 4 字节大端整数标明消息体长度接收方先读满固定长度的头部、解析出长度再按长度读满整条消息体。接收缓冲区需要维护一个粘包/半包状态机缓冲区中字节数不足一条完整消息时保留残包等待下次数据到达处理半包字节数超过一条时截取第一条后继续解析剩余部分处理粘包。