面试知识点-1022

一.CAN总线

1.定义:

CAN(Controller Area Network,控制器局域网)是一种多主总线通信协议,最初由德国博世公司于1986年开发,主要用于汽车电子系统以减少线束复杂度。

2.核心特点

  • 多主结构:网络中任何节点都可以在任何时刻主动发送消息,没有主从之分。
  • 非破坏性位仲裁:当多个节点同时发送数据时,通过标识符(ID)进行仲裁。ID数值越小的报文,优先级越高,可以继续发送,而优先级低的节点会自动退出发送,不会破坏高优先级报文的传输。
  • 广播通信:所有节点都连接到同一总线上,一个节点发送的数据,所有其他节点都能接收到。节点通过报文ID过滤器来决定是否处理该报文。
  • 高可靠性与错误处理:内置多种强大的错误检测与处理机制,如循环冗余校验(CRC)、位填充、帧检查、错误帧等,确保数据传输的完整性。
  • 短帧结构:传统CAN的数据帧最多携带8字节数据,CAN FD(Flexible Data-rate)扩展至64字节,短帧结构使得传输时间短,受干扰概率低。
  • 差分信号传输:使用CAN_H和CAN_L双绞线进行差分信号传输,抗干扰能力强。

3.物理层与硬件连接

  • 电气特性
    • 显性电平(逻辑0):CAN_H和CAN_L之间的电压差约为2V。
    • 隐性电平(逻辑1):CAN_H和CAN_L的电压都接近2.5V,电压差接近0V。
    • 显性电平会覆盖隐性电平,这是仲裁机制的基础。
  • 终端电阻:总线的两端必须各接一个120欧姆的终端电阻,用于消除信号反射,保证信号完整性。
  • 传输介质:通常使用带屏蔽的双绞线。
  • 网络拓扑:主要采用总线型拓扑结构。

4.协议层与帧结构

  • 分层模型:遵循OSI模型,主要实现了物理层和数据链路层。
  • 帧类型
    • 数据帧:用于发送数据。
    • 远程帧:用于请求发送具有相同ID的数据帧。
    • 错误帧:当节点检测到总线错误时发出,以通知其他节点。
    • 过载帧:用于在前后两个数据帧或远程帧之间提供额外的延迟时间。
  • 数据帧结构:包含起始位(SOF)、仲裁段(控制段、数据段)、CRC段、ACK段和帧结束段(EOF)。

5.错误处理机制

  • 错误检测:通过CRC校验、位填充规则校验、帧格式检查、ACK应答检查等方式检测错误。
  • 错误计数:每个节点都有两个错误计数器:发送错误计数器(TEC)和接收错误计数器(REC)。
  • 节点状态
    • 主动错误状态:节点可以正常发送数据,检测到错误时会发送主动错误帧(6个显性位)。
    • 被动错误状态:错误计数器超过阈值(127),节点仍可通信,但检测到错误时只发送被动错误帧(6个隐性位),以减少对总线的影响。
    • 总线关闭状态:错误计数器超过255,节点会自动断开与总线的连接,不再发送或接收任何数据。

6.性能指标

  • 通信速率:最高可达1Mbps(在40米距离内),速率与距离成反比,最远通信距离可达10公里(在5kbps速率下)。
  • 节点数量:理论上最多可连接110个节点。

7. 协议发展与衍生

  • CAN FD (CAN with Flexible Data-rate)
    • 数据长度增加:每帧数据场长度从8字节扩展到64字节。
    • 可变速率:在数据传输阶段可以切换到更高的波特率,提高了数据传输效率。
  • CANopen:一种基于CAN总线的应用层协议,定义了设备模型(对象字典)、通信机制(PDO过程数据对象、SDO服务数据对象)等,用于实现不同厂商设备的互操作性。
  • CAN XL (CAN 3.0):新一代CAN协议,提供更高的数据吞吐率和更强的网络安全功能。

常见面试问题及解答思路

  • 问:CAN总线是如何解决多节点冲突的?

    • 答:通过非破坏性位仲裁机制。当多个节点同时发送时,它们会同时监视总线电平。如果某个节点发送的是隐性电平(1),但总线上检测到的是显性电平(0),则意味着有更高优先级的节点在发送,该节点会立即停止发送,转为接收状态。ID数值越小,优先级越高。
  • 问:为什么CAN总线两端要接120欧姆电阻?

    • 答:为了阻抗匹配消除信号反射。信号在电缆末端传输时,如果阻抗不连续会发生反射,反射波会与原始信号叠加,导致信号失真。终端电阻可以吸收能量,有效抑制反射,保证信号的完整性。
  • 问:CAN总线有哪些错误检测机制?

    • 答:主要有CRC校验位填充规则检查帧格式检查 (如固定的位域、CRC界定符、ACK界定符等)、ACK错误检查 以及位错误监控
  • 问:描述一下CAN节点从正常通信到离线的过程。

    • 答:节点初始处于主动错误状态 。当发送或接收错误频繁发生,对应的错误计数器(TEC/REC)会增加。当TEC或REC超过127时,节点进入被动错误状态 。如果错误持续,TEC超过255,节点则进入总线关闭状态,脱离总线通信,需要重新初始化或满足特定条件才能恢复。
  • 问:CAN和CAN FD的主要区别是什么?

    • 答:主要区别有三点:
      1. 数据场长度:CAN每帧最多8字节,CAN FD最多64字节。
      2. 波特率:CAN FD在数据传输阶段可以支持更高的波特率。
      3. 帧格式:CAN FD的帧格式有一些新的标志位以支持上述特性。

二.LIN总线协议

一、基础概念与定义

  • 全称:Local Interconnect Network,局域互联网。
  • 定位:一种低成本、低速(最高20kbps)的串行通信协议,主要用于汽车电子中的车身控制领域。
  • 目的:作为CAN等高速总线的辅助网络,连接对带宽、实时性要求不高的传感器、执行器,以降低成本和布线复杂度。

二、核心特点

  1. 主从架构:网络中必须有一个主节点和多个从节点。主节点负责控制总线访问、发起所有通信并管理调度表;从节点只能根据主节点的指令发送或接收数据,不能主动发起通信。
  2. 单线通信:仅需一根信号线(LIN Bus)即可完成数据传输,极大地降低了线束成本和重量。
  3. 低成本:硬件实现简单,从节点无需昂贵的晶振或专用的CAN收发器,通信控制器通常集成在MCU中。
  4. 自同步机制:从节点通过主节点发送的同步场来自动校准其内部波特率,保证了通信的可靠性。
  5. 确定性通信:主节点按照预设的调度表周期性地发送帧头,使得网络通信具有确定性和可预测性。

三、通信帧结构

一个完整的LIN帧由帧头响应两部分组成。

  • 帧头 :由主节点发送。
    • 间隔段:一个显性电平(低电平),用于标志一个新帧的开始。
    • 同步段 :包含同步字节0x55,用于从节点进行时钟同步。
    • 受保护标识符:6位的标识符和2位的奇偶校验位,共1个字节。用于标识消息的内容和用途。
  • 响应 :可以由主节点或从节点发送。
    • 数据场:1到8个字节的数据。
    • 校验和场:对数据场(或整个响应)进行计算得出的校验和,用于接收端验证数据的完整性。LIN 2.x版本引入了增强校验和,包含了PID。

四、节点类型

  • 主节点:包含主任务和从任务。主任务负责发送帧头,从任务负责发送或接收响应。
  • 从节点:只包含从任务,根据帧头中的PID来决定自己是发送响应还是接收响应。
  • 从-主节点:一种特殊的从节点,它的从任务可以发送响应,即它可以在主节点的调度下向其他节点发送数据。

五、与CAN总线的区别(面试高频题)

特性 LIN总线 CAN总线
成本 极低 较高
速度 低 (最高20 kbps) 高 (最高1 Mbps)
线缆 单线 双绞线 (CAN_H, CAN_L)
网络拓扑 主从架构,单主控 多主机,对等网络
仲裁机制 无冲突,主节点调度 CSMA/CA,非破坏性位仲裁
应用领域 车窗、座椅、后视镜、雨刷等车身舒适性电子 动力系统、底盘控制、安全气囊等关键系统

六、状态机与调度

  • 调度表:由主节点维护的一个消息发送序列,定义了在总线循环中各个帧的发送顺序和时机。主节点严格按照调度表发送帧头,保证了通信的实时性。
  • 帧的传输类型
    • 无条件帧:主节点发送帧头,指定的从节点立即响应。
    • 事件触发帧:多个从节点可以竞争响应同一个帧头,但需通过冲突解决机制(通常在下一个循环中单独查询)。
    • 偶发帧:主节点在特定条件下才发送的帧。
    • 诊断帧:用于节点诊断和数据配置。

三.Linux应用开发基础

1. 请描述一下在Linux下,一个C语言程序从源代码到可执行文件的完整过程。

参考回答: 这个过程主要分为四个阶段,由GCC编译器驱动完成:

  1. 预处理 :处理以#开头的指令。预处理器(cpp)会读取头文件(如#include <stdio.h>)、展开宏定义(#define)、处理条件编译(#ifdef)。这个阶段的输出是一个经过宏展开和头文件包含后的".i"为后缀的纯C代码文件。

    • 命令:gcc -E hello.c -o hello.i
  2. 编译 :将预处理后的C代码翻译成汇编代码。编译器(cc1)会进行词法分析、语法分析、语义分析,并生成与目标平台相关的汇编代码。这个阶段会检查代码的语法错误。

    • 命令:gcc -S hello.i -o hello.s
  3. 汇编 :将汇编代码翻译成机器可以理解的指令,即目标代码。汇编器(as)生成".o"为后缀的目标文件。这个文件是二进制格式,但还不能直接运行,因为它可能引用了其他文件中的函数或变量。

    • 命令:gcc -c hello.s -o hello.o
  4. 链接 :将一个或多个目标文件与所需的库文件(如C标准库)链接在一起,最终生成一个可执行文件。链接器(ld)负责解析和重定位符号引用,确保所有函数和变量的调用都能找到正确的地址。

    • 命令:gcc hello.o -o hello

最终,我们得到了一个可以在Linux系统上直接运行的hello可执行文件。


2. 什么是静态库和动态库?它们有什么区别和优缺点?

参考回答: 静态库(.a文件)和动态库(.so文件)都是代码库,用于将可复用的函数打包,但它们在链接和运行阶段的行为完全不同。

区别与优缺点:

特性 静态库 动态库
链接方式 在编译链接阶段,库中被用到的代码的完整副本会被复制到最终的可执行文件中。 在编译链接阶段,只在可执行文件中留下一个指向 动态库的"占位符"。代码在程序运行时才被加载。
可执行文件大小 较大,因为包含了库代码。 较小,只包含引用信息。
运行依赖 无需外部依赖,部署简单。 运行时必须系统能找到对应的.so文件,否则会报错。
内存使用 如果多个程序使用了同一个静态库,库代码会在每个程序的内存空间中都有一份副本,造成内存浪费。 多个进程可以共享同一份动态库在物理内存中的实例,节省内存。
更新与维护 库更新后,所有使用了它的程序都必须重新编译链接才能使用新版本,非常不便。 只需替换掉旧的.so文件即可,所有使用它的程序都会自动使用新版本(前提是接口兼容),便于升级和打补丁。

总结:

  • 静态库:适合对部署独立性要求高、不常更新的场景,或者在没有动态库支持的嵌入式系统中。
  • 动态库:是现代操作系统的主流选择,极大地提高了软件的模块化、可维护性和资源利用率。

3. Makefile的作用是什么?请简单写一个包含编译和清理功能的Makefile

参考回答: Makefile是一个用于管理项目编译和构建的配置文件。它定义了一系列的规则,指明了如何从源文件生成目标文件(可执行文件、库文件等)。make工具会根据Makefile的规则和文件的修改时间戳,智能地决定哪些文件需要重新编译,从而大大提高了大型项目的编译效率。

示例Makefile

复制代码

Makefile

cs 复制代码
# 定义变量
CC = gcc
CFLAGS = -Wall -g
TARGET = my_app
OBJS = main.o utils.o

# 默认目标
all: $(TARGET)

# 链接规则:生成最终可执行文件
$(TARGET): $(OBJS)
	@echo "Linking..."
	$(CC) $(CFLAGS) -o $@ $^

# 编译规则:生成目标文件
%.o: %.c
	@echo "Compiling $<..."
	$(CC) $(CFLAGS) -c $< -o $@

# 清理规则:删除生成的文件
clean:
	@echo "Cleaning up..."
	rm -f $(TARGET) $(OBJS)

# 声明伪目标
.PHONY: all clean
  • all是默认目标,执行make时会构建my_app
  • $(TARGET): $(OBJS)表示my_app依赖于main.outils.o
  • %.o: %.c是一个模式规则,告诉make如何从任何.c文件生成对应的.o文件。
  • clean是一个伪目标,用于清理生成的文件,通过make clean执行。

4. 在Linux下如何进行程序调试?gdb常用的命令有哪些?

参考回答: 在Linux下,最核心的调试工具是GNU Debugger(gdb)。使用它之前,编译程序时必须加上-g选项,以便在可执行文件中包含调试符号信息。

常用gdb命令:

  • gdb <executable>:启动gdb并加载要调试的程序。
  • run (或 r):开始运行程序。
  • break <location> (或 b):设置断点。location可以是函数名、行号或内存地址。例如:b mainb 25
  • next (或 n):单步执行 下一行代码,如果当前行是函数调用,则不会进入函数内部。
  • step (或 s):单步执行 下一行代码,如果当前行是函数调用,则会进入函数内部。
  • continue (或 c):继续运行程序,直到遇到下一个断点或程序结束。
  • print <variable> (或 p):打印指定变量的值。
  • backtrace (或 bt):查看当前函数的调用栈信息。
  • quit (或 q):退出gdb。

5. 在C/C++中,malloc/freenew/delete有什么区别?

参考回答: 它们分别是C和C++中用于动态内存管理的机制,主要区别在于:

  1. 语言与性质malloc/free是C语言的库函数 ,而new/delete是C++的操作符/关键字
  2. 构造/析构函数new在分配内存后会自动调用 对象的构造函数,delete在释放内存前会自动调用 对象的析构函数。而malloc/free只是简单地分配和释放原始内存,不会调用构造/析构函数。这是最核心的区别。
  3. 返回值类型malloc返回void*,需要强制类型转换。new直接返回指向特定类型的指针,无需转换。
  4. 内存分配失败处理malloc分配失败时返回NULLnew分配失败时会抛出std::bad_alloc异常(当然,可以使用new (std::nothrow)形式让其返回NULL)。
  5. 大小计算malloc需要手动指定要分配的内存字节数。new会根据类型自动计算所需大小。

总结 :在C++中,应优先使用new/delete来管理对象内存,因为它们能正确处理对象的构造和析构,更符合面向对象的思想。对于非对象类型的内存分配,两者都可以使用。

代码实现
  • malloc/free

    • malloc 是一个函数,需要你手动计算字节数,并强制转换类型。

      cs 复制代码
      // C 风格 
      int* p_int = (int*)malloc(sizeof(int) * 10); // 必须指定大小并强制转换 
      char* p_char = (char*)malloc(sizeof(char) * 100);
    • free 也是一个函数,只需要传入指针即可。

      cs 复制代码
      free(p_int);
      free(p_char);
  • new/delete

    • new 是一个操作符,无需计算大小,也无需强制转换,编译器会自动处理。它更智能、更安全。

      cs 复制代码
      // C++ 风格 
      int* p_int = new int;        // 分配一个 int 
      int* p_arr = new int[10];    // 分配一个包含10个 int 的数组 
    • delete 也是一个操作符,使用方式与 new 对应。

      cs 复制代码
      delete p_int;       // 释放单个对象 
      delete[] p_arr;     // 释放数组 (注意方括号!)

6. 什么是内存泄漏?如何检测和避免内存泄漏?

参考回答: 内存泄漏是指在程序运行过程中,动态分配的堆内存因为某种原因未被释放或无法被释放,导致这部分内存永久丢失,直到程序结束。随着时间推移,内存泄漏会耗尽系统资源,导致程序变慢甚至崩溃。

如何避免:

  1. 良好的编程习惯 :遵循"谁申请,谁释放"的原则。确保每一对malloc/freenew/delete都成对出现。
  2. 使用RAII(Resource Acquisition Is Initialization) :这是C++中最重要的思想。将资源的生命周期与对象的生命周期绑定。在对象构造时获取资源,在析构时自动释放资源。例如使用智能指针(std::unique_ptr, std::shared_ptr)。
  3. 代码审查:通过人工审查来发现潜在的内存泄漏点。

如何检测:

  1. 工具检测

    • Valgrind :Linux下非常强大的内存调试工具,可以检测内存泄漏、非法内存访问等问题。使用命令:valgrind --leak-check=full ./your_program
    • AddressSanitizer (ASan) :一个编译器选项(-fsanitize=address),在代码中插入检测指令,运行时能快速发现内存错误,包括泄漏。
  2. 静态分析工具:一些静态代码分析工具可以在编译前分析代码,发现潜在的泄漏风险。

什么是智能指针?它们解决了什么问题?

  1. 答案 : 智能指针是RAII(资源获取即初始化)思想在内存管理上的应用,用于自动管理动态分配的内存,防止内存泄漏。C++11主要提供了三种:
    1. std::unique_ptr: 独占资源所有权的指针,不可复制,只能移动。
    2. std::shared_ptr: 共享资源所有权的指针,通过引用计数来管理生命周期,当引用计数为0时释放资源。
    3. std::weak_ptr: shared_ptr的辅助工具,不增加引用计数,用于解决shared_ptr的循环引用问题

解释一下虚函数和多态的实现原理。

  1. 答案: 多态是通过虚函数实现的。包含虚函数的类会有一个隐藏的成员变量------虚函数表指针(vptr),该指针指向一个虚函数表(vtable)。vtable是一个函数指针数组,存储了该类的虚函数地址。当通过基类指针或引用调用虚函数时,程序会通过vptr找到对应的vtable,再从vtable中找到正确的函数地址进行调用,从而实现运行时的多态

mapunordered_map的区别是什么?

  1. 答案 : 两者都是键值对容器,但底层实现和性能不同。
    1. map: 基于红黑树实现,元素自动按键排序,查找、插入、删除的时间复杂度为O(log n)。
    2. unordered_map: 基于哈希表实现,元素无序,在理想情况下(无哈希冲突)查找、插入、删除的时间复杂度为O(1),最坏情况为O(n)。在不需要排序且追求高效查找的场景下,unordered_map通常是更好的选择

文件I/O (File I/O)

7. Linux标准I/O函数(如fopen, fread)和文件I/O系统调用(如open, read)有什么区别?

参考回答: 这是Linux I/O的两个层次,主要区别在于缓冲机制移植性

  • 文件I/O系统调用

    • 直接调用操作系统内核提供的函数,如open, read, write, close
    • 不带缓冲的I/O。每次调用都会直接触发内核操作,开销较大。
    • 依赖于操作系统,可移植性相对较差。
    • 操作对象是文件描述符(一个非负整数)。
  • 标准I/O库函数

    • 是C标准库提供的函数,如fopen, fread, fwrite, fclose
    • 带缓冲的I/O。在用户空间开辟了一个缓冲区,当读写数据时,会先操作这个缓冲区,只有当缓冲区满(对于写)或请求的数据量足够大(对于读)时,才会调用系统调用,从而减少了系统调用的次数,提高了效率。
    • 可移植性好,遵循ANSI C标准。
    • 操作对象是文件指针FILE*)。

关系 :标准I/O库函数的底层实现,最终还是通过调用文件I/O系统调用(如read/write)来与内核交互的。


8. 什么是文件描述符?Linux系统启动后,默认会为每个进程打开哪三个标准的文件描述符?

参考回答: 文件描述符在形式上是一个非负整数。它是一个索引,指向内核为每一个进程所维护的"打开文件表"。当进程打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符。后续对文件的所有操作(读、写、定位)都通过这个文件描述符来完成。这是Linux"一切皆文件"哲学的体现。

默认打开的三个标准文件描述符:

  1. 0: 标准输入,默认关联到键盘。
  2. 1: 标准输出,默认关联到终端屏幕。
  3. 2: 标准错误,默认也关联到终端屏幕。

这三个文件描述符使得程序可以像处理普通文件一样来处理输入、输出和错误流。


9. 调用readwrite函数时,返回值小于请求的字节数可能是什么原因?

参考回答: 这是一个非常实际的问题,表明候选人有处理真实I/O场景的经验。原因主要有:

  1. 读取时遇到文件末尾(EOF) :当读取普通文件时,如果已经读到了文件的末尾,再次调用read会返回0。如果文件剩余的字节数少于请求的字节数,read会返回实际读到的字节数。
  2. 网络或管道中的数据未就绪 :对于网络套接字或管道,如果当前可读的数据量小于请求的字节数,read会返回实际可读的数量,而不是等待更多数据(除非是阻塞模式且对方关闭了连接)。
  3. 被信号中断 :在I/O操作进行中,如果进程捕获到一个信号,readwrite可能会被中断,此时返回值可能是已读/写的字节数,并设置errnoEINTR。正确的做法是检查errno并重新调用。
  4. 缓冲区限制:对于某些底层实现,如网络协议栈,可能会受到内部缓冲区大小的限制,无法一次性处理大量数据。

10. lseek函数的作用是什么?请举例说明它的一个应用场景。

参考回答: lseek函数的作用是显式地设置一个打开文件的读写偏移量。它允许程序在文件中随机访问,而不必顺序读写。

函数原型off_t lseek(int fd, off_t offset, int whence);

  • whence参数决定了偏移量的基准:
    • SEEK_SET:从文件开头计算。
    • SEEK_CUR:从当前位置计算。
    • SEEK_END:从文件末尾计算。

应用场景举例:

  1. 获取文件大小off_t size = lseek(fd, 0, SEEK_END); 这会将偏移量移动到文件末尾,并返回新的偏移量,即文件的总字节数。
  2. 实现类似tail -f的功能 :可以先用lseek定位到文件末尾,然后在一个循环中不断尝试read,如果read返回0(没有新数据),就休眠一小段时间,从而实现追踪文件新增内容的效果。
  3. 数据库文件访问 :在简单的文件式数据库中,数据可能按固定长度存储。可以通过lseek(fd, record_id * record_size, SEEK_SET)来快速定位到任意一条记录的位置。
11.Linux文件系统中i节点是什么?
  • 答案: i节点是Unix/Linux文件系统中存储文件元信息的数据结构。每个文件(无论大小)都有一个唯一的i节点。它包含了文件的权限、所有者、组、大小、修改时间、以及指向磁盘上存储文件数据块的指针等关键信息,但不包含文件名。文件名是存储在目录文件中的,目录文件将文件名映射到对应的i节点号
select, poll, epoll的区别和优缺点是什么?

它们都是I/O多路复用的机制。

select:

缺点: 1) 单个进程可监控的文件描述符(fd)数量有限(通常1024)。2) 每次调用都需要将fd集合从用户空间拷贝到内核空间,开销大。3) 内核需要遍历所有fd来检查就绪状态,效率随fd数量增加而线性下降。

poll:

改进: 使用pollfd结构体,没有fd数量限制。

缺点: 仍存在拷贝和线性扫描的问题,效率与select类似。

epoll:

优点: 1) 没有fd数量限制。2) 内核维护一个"就绪列表",每次调用只需返回就绪的fd,无需线性扫描,效率为O(1)。3) 使用mmap实现用户空间和内核空间的共享,避免不必要的拷贝。

模式: 支持LT(水平触发,默认)和ET(边沿触发)模式。ET模式效率更高,但要求必须一次性读写完所有就绪数据,否则可能丢失事件,编程更复杂 1

什么是零拷贝技术?

零拷贝技术旨在减少或消除CPU在数据拷贝中的参与,从而提高数据传输效率。传统I/O操作需要数据在内核缓冲区和用户缓冲区之间来回拷贝。零拷贝通过sendfilemmap等系统调用,允许数据直接在内核空间或网卡之间传输,避免了用户空间和内核空间之间的数据拷贝,显著提升了文件传输和网络服务的性能 1

进程间通信 (IPC)

  1. 无名管道

    • 本质:内核中的一块缓冲区,是一种半双工的通信方式。
    • 特点:只能用于具有亲缘关系(如父子进程、兄弟进程)的进程间通信。数据以字节流形式传送,生命周期随进程结束而结束。
  2. 有名管道

    • 本质:一种在文件系统中可见的特殊类型文件(FIFO文件)。
    • 特点:克服了无名管道的亲缘关系限制,允许任何两个不相关的进程通过该文件路径进行通信。通信方式同样是半双工的字节流。
  3. 信号

    • 本质:Linux/Unix中最古老的通信方式,是一种异步的、软件层面的中断机制。
    • 特点 :携带的信息量非常少,只有一个信号编号。它不用于传输数据,而是用于通知接收进程某个事件已经发生(如终止进程SIGKILL、非法内存访问SIGSEGV)。
  4. 消息队列

    • 本质:一个由内核维护的消息链表,存放在内核中。
    • 特点:以结构化的消息(类型+数据)为单位进行通信,而不是字节流。可以实现任意进程间的通信,并且支持消息的异步收发。消息队列的生命周期随内核,除非被显式删除或系统关闭,否则一直存在。
  5. 共享内存

    • 本质:内核在物理内存中开辟一块区域,然后将其映射到多个进程的虚拟地址空间中。
    • 特点速度最快的IPC方式,因为它绕过了内核的数据拷贝,进程可以直接读写内存,就像访问自己的变量一样。但它本身不提供同步机制,需要配合信号量或互斥锁来防止数据冲突。
  6. 信号量

    • 本质:一个计数器,主要用于进程或线程间的同步与互斥,而不是数据传输。
    • 特点:它是一个原子操作,通过P操作(等待)和V操作(释放)来控制对共享资源的访问。常用于保护共享内存、临界区等,防止多个进程同时操作导致的数据不一致问题。
  7. 套接字

    • 本质:最通用的IPC机制,也是网络编程的基础。
    • 特点:既可以用于同一台主机上不同进程间的通信,也可以用于网络上不同主机间的进程通信。它提供了丰富的接口,支持多种协议(如TCP/UDP),是C/S架构应用的基础。

1.异同点对比总结

下面的表格从多个维度对这七种IPC方式进行了全面对比。

特性维度 无名管道 有名管道 信号 消息队列 共享内存 信号量 套接字
核心目的 数据传输 数据传输 事件通知 数据传输 数据传输 同步/互斥 数据传输
通信范围 亲缘进程 任意进程 任意进程 任意进程 任意进程 任意进程/线程 同主机或跨网络
数据形式 字节流 字节流 信号编号 结构化消息 任意数据块 计数器 字节流/数据报
工作方式 半双工 半双工 异步通知 异步收发 直接内存访问 原子操作(P/V) 全双工/半双工
速度/性能 较慢 较慢 极快(通知) 较慢 最快 极快(同步) 较慢(网络更慢)
生命周期 随进程 随文件系统 随进程 随内核 随内核 随内核 随进程
是否阻塞 默认阻塞 默认阻塞 不适用 可设为非阻塞 不适用 可设为非阻塞 可设为非阻塞
主要应用场景 父子进程简单数据传递(如`ls grep`) 无亲缘关系的进程间数据传递 异步事件处理(如Ctrl+C终止程序) 多进程间异步传递结构化数据 需要高速、大量数据交换的场景(如数据库、图像处理) 保护共享资源,实现进程同步

2.核心相同点与不同点提炼
相同点
  • 都是IPC机制:它们最根本的共同点是,都是操作系统内核提供给用户空间进程进行通信和同步的机制。
  • 依赖内核:除了套接字在网络上传输数据会经过网络设备外,所有IPC的实现都离不开内核的支持和管理。
核心不同点
  1. 功能定位不同

    • 数据传输型:管道(有名/无名)、消息队列、共享内存、套接字,它们的核心是搬移数据。
    • 同步通知型:信号、信号量,它们的核心是协调进程的执行时序或通知事件,而不是传输大量数据。
  2. 速度和效率差异巨大

    • 最快:共享内存,因为它避免了内核和用户空间之间的数据拷贝。
    • 最慢:管道和消息队列,因为数据需要多次在用户空间和内核空间之间拷贝。套接字在网络环境下则更慢。
  3. 适用范围不同

    • 最受限:无名管道,仅限亲缘进程。
    • 最通用:套接字,既能用于本机IPC,又能用于跨网络通信,是唯一支持跨主机的。
  4. 数据结构化程度不同

    • 字节流:管道,不关心数据边界,需要应用自己解析。
    • 结构化:消息队列,自带类型和数据长度,方便处理。
    • 无格式:共享内存,就是一块裸内存,格式完全由应用定义。
如何选择?
  • 需要高速数据交换? -> 共享内存(但别忘了加锁)。
  • 父子进程间简单通信? -> 无名管道
  • 两个不相关的进程在本机上通信? -> 有名管道消息队列
  • 需要跨网络通信? -> 套接字,别无选择。
  • 只是想让另一个进程做点什么(如关闭)? -> 信号
  • 多个进程/线程要安全地操作一个共享资源? -> 信号量
3. 共享内存为什么是最快的IPC方式?使用它时需要注意什么问题?

参考回答: 为什么最快? 共享内存之所以最快,是因为它绕过了内核的数据拷贝。其他IPC方式,如管道、消息队列,数据都需要在用户空间和内核空间之间来回拷贝。而共享内存机制允许内核将同一块物理内存映射到多个进程的虚拟地址空间中。当一个进程写入数据时,另一个进程可以立即看到,因为它们访问的是同一片内存,这个过程没有任何内核干预和数据拷贝,效率极高。

需要注意什么问题? 最核心的问题是同步 。由于共享内存允许多个进程同时读写,如果不加控制,就会产生竞态条件 ,导致数据不一致或程序崩溃。因此,必须使用其他的同步机制来保护共享内存区域,最常用的就是信号量互斥锁。进程在访问共享内存前,必须先获取信号量(加锁);访问完成后,再释放信号量(解锁),以确保任何时候只有一个进程在写入数据。


4. 什么是信号?请描述一下信号的处理过程。kill命令一定只能杀死进程吗?

参考回答: 信号 是Linux/Unix中一种非常古老的、异步的通信机制,可以理解为软件层面的中断。它用于通知接收进程发生了某个事件,打断进程当前的正常执行流程,转而去处理这个事件。

信号的处理过程:

  1. 信号产生 :某个事件导致信号的产生,如用户按下Ctrl+C(产生SIGINT信号)、硬件异常(如除零,产生SIGFPE)、或者通过kill命令/函数发送。
  2. 信号在内核中注册 :内核将信号递送到目标进程,通常会在进程的task_struct中记录下来。如果进程正在处理同类型的信号,新的信号可能会被阻塞或排队。
  3. 信号递达:在进程下一次被调度执行时,内核会检查它是否有待处理的信号。
  4. 信号处理 :进程必须决定如何处理这个信号。有三种方式:
    • 执行默认操作 :如SIGTERM的默认操作是终止进程。
    • 忽略信号 :通过signal()sigaction()函数设置处理方式为SIG_IGN
    • 捕获信号:提供一个自定义的信号处理函数,当信号到达时,跳转到该函数执行。

kill命令不一定只能杀死进程。 kill命令的通用功能是向指定进程发送一个信号 。其语法是kill -s <signal> <pid>。如果不指定信号,默认发送的是SIGTERM(信号15),这是一个请求终止的信号,可以被捕获或忽略。只有当发送SIGKILL(信号9)时,才是真正意义上的"杀死",该信号不能被捕获或忽略,进程会立即终止。


5. 什么是死锁?产生的四个必要条件是什么?如何预防和避免死锁?

参考回答: 死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生的四个必要条件(必须同时满足):

  1. 互斥条件:一个资源在同一时间内只能被一个进程使用。
  2. 占有并等待条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不可抢占条件:进程已获得的资源,在未使用完之前,不能被其他进程强行抢占,只能在使用完后由自己释放。
  4. 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

如何预防和避免: 预防死锁是通过破坏四个必要条件之一来实现的:

  1. 破坏"占有并等待":实行资源预分配策略,即进程在运行前一次性申请所有需要的资源,如果有一个申请不到,就等待,但不会占用已有资源。
  2. 破坏"不可抢占":允许进程抢占资源。当一个已持有部分资源的进程申请新资源失败时,它必须释放所有已占有的资源。
  3. 破坏"循环等待":实行资源有序分配策略。将所有资源按类型排序编号,规定所有进程必须按序号递增的顺序申请资源。这样就不会形成环路。
6.fork()之后,父子进程的关系是怎样的?如何避免僵尸进程?

fork()创建一个子进程,子进程复制父进程的地址空间。父进程和子进程都从fork()调用的下一条指令开始执行,但返回值不同:在父进程中返回子进程的PID,在子进程中返回0。如果子进程先于父进程结束,而父进程没有调用wait()waitpid()来获取子进程的退出状态,那么子进程的进程描述符(PCB)会保留在内核中,成为僵尸进程。避免方法:1) 父进程通过信号处理(如SIGCHLD)并在处理函数中调用wait。2) 父进程显式调用wait()waitpid()等待子进程结束。3) 父进程忽略SIGCHLD信号(某些系统下可行,但需注意) 1


多线程开发

1. 线程和进程的区别是什么?

参考回答: 线程和进程都是操作系统进行并发执行的单元,但它们在资源管理和调度上有着本质的区别。

  • 进程

    • 资源分配的基本单位。每个进程都有独立的地址空间、数据段、代码段,以及其他系统资源(如文件描述符)。进程间的资源是隔离的。
    • 创建、销毁和切换的开销大,因为需要重新分配和回收大量资源。
    • 进程间通信(IPC)复杂,需要借助专门的IPC机制。
  • 线程

    • CPU调度的基本单位。线程是进程内的一个执行流,它共享所属进程的全部资源(地址空间、文件描述符等)。
    • 每个线程只拥有自己必需的资源,如栈、寄存器和程序计数器。
    • 创建、销毁和切换的开销小,因为不涉及系统资源的重新分配。
    • 线程间通信简单方便,可以直接读写共享数据,但需要引入同步机制来避免冲突。

总结:进程是"容器",提供资源;线程是"工作者",负责执行。多进程程序更健壮(一个进程崩溃不影响其他),多线程程序并发性更高、通信更高效。


2. 在POSIX线程(pthread)中,如何创建和销毁一个线程?

参考回答: 在POSIX线程(pthread)库中,创建和销毁(等待)线程主要使用以下函数:

  1. 创建线程 - pthread_create:

    复制代码

    C

    复制

    #include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

    • thread: 指向线程标识符的指针,用于存储新线程的ID。
    • attr: 线程属性,通常传NULL表示使用默认属性。
    • start_routine: 线程启动后要执行的函数指针,该函数接收一个void*参数,返回一个void*
    • arg: 传递给start_routine函数的参数。
  2. 等待线程结束 - pthread_join:

    复制代码

    C

    复制

    int pthread_join(pthread_t thread, void **retval);

    • 调用此函数的线程会阻塞,直到ID为thread的线程执行完毕。
    • retval用于获取目标线程start_routine函数的返回值。这类似于进程中的wait
  3. 线程自我终止 - pthread_exit:

    复制代码

    C

    复制

    void pthread_exit(void *retval);

    • 线程可以调用此函数来终止自己的执行,并返回一个void*类型的值。

基本流程: 主线程调用pthread_create创建新线程,新线程开始执行start_routine函数。主线程可以继续执行或调用pthread_join等待新线程完成。新线程执行完毕或调用pthread_exit后,主线程的pthread_join返回,程序继续执行。

3.解释一下互斥锁、条件变量和读写锁的应用场景。

互斥锁: 用于保护临界区,确保同一时间只有一个线程可以访问共享资源。适用于任何需要独占访问的场景。

条件变量: 与互斥锁配合使用,允许线程在某个条件不满足时挂起等待,直到其他线程满足条件并发出信号。常用于生产者-消费者模型。

读写锁 : 一种更细粒度的锁,允许多个线程同时读取共享数据,但只允许一个线程写入数据。适用于读多写少的场景,能显著提高并发性能 1


4. 什么是互斥锁?什么是条件变量?请描述一个使用它们的生产者-消费者模型。

参考回答: 互斥锁 :是一种同步原语,用于保护临界区,确保在任何时刻只有一个线程可以访问共享资源。它就像一把锁,线程在进入临界区前"加锁",离开时"解锁"。如果锁已被占用,其他尝试加锁的线程会被阻塞,直到锁被释放。

条件变量 :也是一种同步原语,它允许线程在某个条件不满足时挂起等待,直到另一个线程通知它条件已经满足。它本身不是锁,通常要与互斥锁配合使用。

5.读写锁适用于什么场景?它相比于互斥锁有什么优势?

参考回答: 适用场景 :读写锁(也叫共享-独占锁)特别适用于读多写少的场景。在这种场景下,多个线程频繁地读取共享数据,而写操作相对较少。

优势: 相比于标准的互斥锁(Mutex),读写锁提供了更细粒度的锁控制,从而提高了并发性能。

  • 互斥锁 :无论是读操作还是写操作,都是独占的。任何线程要访问数据,都必须先获取锁,这导致即使是多个只想读取数据的线程,也必须串行执行,无法并发。

  • 读写锁:提供了两种加锁模式:

    • 读锁(共享锁) :如果一个线程获取了读锁,其他读线程 也可以同时获取读锁,并发地读取数据。但写线程会被阻塞。
    • 写锁(独占锁):如果一个线程获取了写锁,其他所有线程(无论是读还是写)都会被阻塞。

优势体现:在读多写少的场景中,大量的读操作可以并行进行,大大提高了系统的吞吐量和响应速度。只有在少数的写操作发生时,才会阻塞其他线程,保证了数据的一致性。

TCP和UDP协议

TCP通信流程与C++实现

TCP(Transmission Control Protocol,传输控制协议)是面向连接的、可靠的、基于字节流的传输层协议。通信双方必须先建立一个稳定的连接,然后才能进行数据传输。

1. 通信流程

服务器端流程(被动等待连接):

  1. 创建套接字 :调用socket()函数创建一个套接字。
  2. 绑定地址 :调用bind()函数,将套接字与服务器的IP地址和端口号绑定。
  3. 监听连接 :调用listen()函数,将套接字设置为监听模式,准备接收客户端的连接请求。
  4. 接受连接 :调用accept()函数,阻塞等待客户端的连接请求。当有客户端连接时,accept()会返回一个新的套接字("连接套接字"),专门用于与该客户端通信。原来的套接字("监听套接字")继续监听其他连接。
  5. 收发数据 :使用新的"连接套接字",调用read()(或recv())和write()(或send())与客户端进行多次数据交换。
  6. 关闭连接:通信结束后,关闭"连接套接字"。

客户端流程(主动发起连接):

  1. 创建套接字 :调用socket()函数创建一个套接字。
  2. 发起连接 :调用connect()函数,向服务器的IP地址和端口发起连接请求。
  3. 收发数据 :连接建立后,调用write()(或send())和read()(或recv())与服务器进行数据交换。
  4. 关闭连接:通信结束后,关闭套接字。
2. C++实现(Linux)

下面是一个简单的C++实现的Echo服务器(收到什么就发回什么)和客户端。

服务器端代码 (tcp_server.cpp)

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
const int PORT = 8080;
const int BUFFER_SIZE = 1024;
 
int main() {
    int server_fd, new_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE] = {0};
 
    // 1. 创建套接字 
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
 
    // 2. 绑定地址 
    server_addr.sin_family  = AF_INET;
    server_addr.sin_addr.s_addr  = INADDR_ANY;
    server_addr.sin_port  = htons(PORT);
 
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
 
    // 3. 监听连接 
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
 
    std::cout << "TCP Server is listening on port " << PORT << "..." << std::endl;
 
    // 4. 接受连接 
    if ((new_socket = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len)) < 0) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
 
    std::cout << "Client connected: " << inet_ntoa(client_addr.sin_addr)  << std::endl;
 
    // 5. 收发数据 
    int valread;
    while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
        std::cout << "Client: " << buffer;
        send(new_socket, buffer, valread, 0);
    }
 
    // 6. 关闭连接 
    close(new_socket);
    close(server_fd);
    return 0;
}

客户端代码 (tcp_client.cpp)

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
const int PORT = 8080;
const char* SERVER_IP = "127.0.0.1";
 
int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[1024] = {0};
    const char* message = "Hello from TCP client";
 
    // 1. 创建套接字 
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
 
    serv_addr.sin_family  = AF_INET;
    serv_addr.sin_port  = htons(PORT);
 
    // 转换IP地址 
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr)  <= 0) {
        perror("inet_pton failed");
        close(sock);
        exit(EXIT_FAILURE);
    }
 
    // 2. 发起连接 
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connect failed");
        close(sock);
        exit(EXIT_FAILURE);
    }
 
    // 3. 发送数据 
    send(sock, message, strlen(message), 0);
    std::cout << "Message sent to server" << std::endl;
 
    // 4. 接收数据 
    int valread = read(sock, buffer, 1024);
    std::cout << "Server response: " << buffer << std::endl;
 
    // 5. 关闭连接 
    close(sock);
    return 0;
}

UDP通信流程与C++实现

UDP(User Datagram Protocol,用户数据报协议)是无连接的、不可靠的、基于数据报的传输层协议。通信双方无需建立连接,直接发送数据即可。

1. 通信流程

服务器端流程:

  1. 创建套接字 :调用socket()函数,创建一个UDP套接字(SOCK_DGRAM)。
  2. 绑定地址 :调用bind()函数,将套接字与本地IP和端口绑定,以便客户端知道向哪里发送数据。
  3. 接收数据 :调用recvfrom()函数,阻塞等待并接收来自任何客户端的数据。该函数会同时获取发送方的地址信息。
  4. 发送数据 :调用sendto()函数,向指定的客户端地址(由recvfrom()获取)发送响应数据。
  5. 关闭套接字:通信结束后,关闭套接字。

客户端流程:

  1. 创建套接字 :调用socket()函数,创建一个UDP套接字。
  2. 发送数据 :调用sendto()函数,向服务器指定的IP地址和端口发送数据。
  3. 接收数据 :调用recvfrom()函数,接收服务器的响应数据。
  4. 关闭套接字:通信结束后,关闭套接字。
2. C++实现(Linux)

服务器端代码 (udp_server.cpp)

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
const int PORT = 8080;
const int BUFFER_SIZE = 1024;
 
int main() {
    int server_fd;
    char buffer[BUFFER_SIZE] = {0};
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
 
    // 1. 创建套接字 
    if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
 
    // 2. 绑定地址 
    server_addr.sin_family  = AF_INET;
    server_addr.sin_addr.s_addr  = INADDR_ANY;
    server_addr.sin_port  = htons(PORT);
 
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
 
    std::cout << "UDP Server is listening on port " << PORT << "..." << std::endl;
 
    while (true) {
        // 3. 接收数据 
        memset(buffer, 0, BUFFER_SIZE);
        int num_bytes = recvfrom(server_fd, buffer, BUFFER_SIZE, 0,
                                 (struct sockaddr *)&client_addr, &client_addr_len);
        if (num_bytes == -1) {
            perror("recvfrom failed");
            continue;
        }
 
        std::cout << "Received from " << inet_ntoa(client_addr.sin_addr)  << ": " << buffer << std::endl;
 
        // 4. 发送响应 
        sendto(server_fd, buffer, num_bytes, 0, (struct sockaddr *)&client_addr, client_addr_len);
    }
 
    close(server_fd);
    return 0;
}

客户端代码 (udp_client.cpp)

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
const int PORT = 8080;
const char* SERVER_IP = "127.0.0.1";
 
int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[1024] = {0};
    const char* message = "Hello from UDP client";
 
    // 1. 创建套接字 
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
 
    serv_addr.sin_family  = AF_INET;
    serv_addr.sin_port  = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr); 
 
    // 2. 发送数据 
    sendto(sock, message, strlen(message), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    std::cout << "Message sent to server" << std::endl;
 
    // 3. 接收数据 
    socklen_t serv_addr_len = sizeof(serv_addr);
    int num_bytes = recvfrom(sock, buffer, 1024, 0, (struct sockaddr *)&serv_addr, &serv_addr_len);
    std::cout << "Server response: " << buffer << std::endl;
 
    // 4. 关闭套接字 
    close(sock);
    return 0;
}

TCP与UDP实现的异同点

对比维度 TCP (Transmission Control Protocol) UDP (User Datagram Protocol)
核心区别 面向连接 无连接
连接过程 客户端通过connect(),服务器通过accept()建立连接。 无连接过程,直接发送数据。
可靠性 可靠。通过序列号、确认应答(ACK)、超时重传、数据校验等机制保证数据不丢、不重、不乱。 不可靠。不保证数据到达,不保证顺序,也不保证不重复。
数据形式 字节流。数据像流水一样没有边界,应用程序需要自己定义消息边界(如加消息头)。 数据报 。每个发送的数据包都是独立的,有明确的边界,接收方一次recvfrom正好接收一个sendto发送的数据报。
服务器API socket() -> bind() -> listen() -> accept() -> read()/write() socket() -> bind() -> recvfrom() -> sendto()
客户端API socket() -> connect() -> write()/read() socket() -> sendto() -> recvfrom()
效率与开销 开销大,效率低。需要维护连接状态,有复杂的拥塞控制和流量控制机制。 开销小,效率高。协议头小,无需维护连接状态。
应用场景 要求高可靠性的应用,如:文件传输(FTP)、网页浏览(HTTP)、电子邮件(SMTP) 对实时性要求高、能容忍少量丢包的应用,如:视频会议、在线游戏、DNS查询、语音通话
总结
  • TCP像打电话:先拨号(连接),确认对方在线后,才开始通话(传输数据),说完后挂断(关闭连接)。整个过程可靠、有序。
  • UDP像寄信:直接把信(数据报)扔进邮筒,不需要确认收信人是否在家。信可能丢失,也可能后寄的先到。

在选择使用TCP还是UDP时,根本的权衡在于可靠性效率。如果你的应用不能容忍任何数据丢失或乱序,选择TCP。如果你的应用对实时性要求极高,且能容忍偶尔的丢包,UDP是更好的选择。

请描述TCP的三次握手和四次挥手过程。

  • 答案 :

    • 三次握手 :
      1. 客户端发送SYN包(seq=x)到服务器,进入SYN_SENT状态。
      2. 服务器收到SYN包,回复SYN+ACK包(seq=y, ack=x+1),进入SYN_RCVD状态。
      3. 客户端收到SYN+ACK包,发送ACK包(ack=y+1),双方进入ESTABLISHED状态,连接建立。
    • 四次挥手 :
      1. 主动关闭方发送FIN包,进入FIN_WAIT_1状态。
      2. 被动关闭方收到FIN包,回复ACK包,进入CLOSE_WAIT状态。此时主动关闭方进入FIN_WAIT_2状态。
      3. 被动关闭方处理完数据后,发送FIN包,进入LAST_ACK状态。
      4. 主动关闭方收到FIN包,发送ACK包,进入TIME_WAIT状态,等待2MSL后连接关闭。被动关闭方收到ACK后直接关闭 1
  • TCP的粘包和拆包问题是什么?如何解决?

    • 答案 : TCP是面向字节流的,它不保证应用程序一次写操作的数据会作为一个独立的包进行发送。多个写操作的数据可能被合并成一个包发送(粘包),或者一个写操作的数据可能被拆分成多个包发送(拆包)。
      • 解决方案 :
        1. 固定长度消息: 每个消息大小固定,不足则补位。
        2. 特殊分隔符 : 使用特定字符(如\r\n)作为消息边界。
        3. 消息头+消息体 : 在消息头中声明消息体的长度,接收方先解析头获取长度,再按长度读取消息体。这是最常用和最可靠的方法 1
相关推荐
前端双越老师35 分钟前
前端面试常见的 10 个场景题
前端·面试·求职
Lee川16 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川20 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i1 天前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有1 天前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有1 天前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫1 天前
Looper.loop() 循环机制
面试
AAA梅狸猫1 天前
Handler基本概念
面试
Wect1 天前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼1 天前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试