干货总结!Kafka 面试大全(万字长文,37 张图,28 个知识点)

Posted 大数据羊说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了干货总结!Kafka 面试大全(万字长文,37 张图,28 个知识点)相关的知识,希望对你有一定的参考价值。

关注并标星 Kafka 面试的连环问题,保证你看完后,对 Kafka 有了更深层次的了解。

全文总结的 题目之间的 关联性 很强,本文将通过 问答 + 图解 的形式 由浅入深 帮助大家进一步学习和理解 Kafka 分布式流式处理平台。

全文总计 负责保存 broker 集群元数据,并对控制器进行选举等操作。

(2)Producer:生产者负责创建消息,将消息发送到 Broker。

(3)Broker: 一个独立的 Kafka 服务器被称作 broker,broker 负责接收来自生产者的消息,为消息设置偏移量,并将消息存储在磁盘。broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。

(4)Consumer:消费者负责从 Broker 订阅并消费消息。

(5)Consumer GroupConsumer Group 为消费者组,一个消费者组可以包含一个或多个 Consumer

使用 多分区 + 多消费者 方式可以极大 提高数据下游的处理速度同一消费者组中的消费者不会重复消费消息,同样的,不同消费组中的消费者消费消息时互不影响。Kafka 就是通过消费者组的方式来实现消息 P2P 模式和广播模式。

(6)Topic:Kafka 中的消息 以 Topic 为单位进行划分,生产者将消息发送到特定的 Topic,而消费者负责订阅 Topic 的消息并进行消费。

(7)Partition:一个 Topic 可以细分为多个分区,每个分区只属于单个主题。同一个主题下不同分区包含的消息是不同的,分区在存储层面可以看作一个可追加的 日志(Log)文件,消息在被追加到分区日志文件的时候都会分配一个特定的 偏移量(offset)

(8)Offset:offset 是消息在分区中的唯一标识,Kafka 通过它来保证消息在分区内的顺序性,不过 offset 并不跨越分区,也就是说,Kafka保证的是分区有序性而不是主题有序性

(9)Replication副本,是 Kafka 保证数据高可用的方式,Kafka 同一 Partition 的数据可以在多 Broker 上存在多个副本,通常只有主副本对外提供读写服务,当主副本所在 broker 崩溃或发生网络异常,Kafka 会在 Controller 的管理下会重新选择新的 Leader 副本对外提供读写服务。

(10)Record:实际写入 Kafka 中并可以被读取的消息记录。每个 record 包含了 keyvaluetimestamp

(11)Leader: 每个分区多个副本的 "主" leader,生产者发送数据的对象,以及消费者消费数据的对象都是 leader。

(12)follower: 每个分区多个副本中的"从" follower,实时从 Leader 中同步数据,保持和 leader 数据的同步。Leader 发生故障时,某个 follow 会成为新的 leader。

⭐ 3、发布订阅的消息系统那么多,为啥选择kafka?

(1) 多个生产者

KafKa 可以无缝地支持多个生产者,不管客户端使用一个主题,还是多个主题。Kafka 适合从多个前端系统收集数据,并以统一的格式堆外提供数据。

(2)多个消费者

Kafka 支持多个消费者从一个单独的消息流中读取数据,并且消费者之间互不影响。这与其他队列系统不同,其他队列系统一旦被客户端读取,其他客户端就不能 再读取它。并且多个消费者可以组成一个消费者组,他们共享一个消息流,并保证消费者组对每个给定的消息只消费一次

(3)基于磁盘的数据存储

Kafka 允许消费者非实时地读取消息,原因在于 Kafka 将消息提交到磁盘上,设置了保留规则进行保存,无需担心消息丢失等问题。

(4)伸缩性

可扩展多台 broker。用户可以先使用单个 broker,到后面可以扩展到多个 broker

(5)高性能

Kafka 可以轻松处理百万千万级消息流,同时还能保证 亚秒级 的消息延迟。

⭐ 4、kafka 如何做到高吞吐量和性能的?

kafka 实现高吞吐量和性能,主要通过以下几点:

1、页缓存技术

Kafka 是基于 操作系统页缓存来实现文件写入的。

操作系统本身有一层缓存,叫做 page cache,是在 内存里的缓存,我们也可以称之为 os cache,意思就是操作系统自己管理的缓存。

Kafka 在写入磁盘文件的时候,可以直接写入这个 os cache 里,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把 os cache 里的数据真的刷入磁盘文件中。通过这一个步骤,就可以将磁盘文件写性能提升很多了,因为其实这里相当于是在写内存,不是在写磁盘,原理图如下:

2、磁盘顺序写

另一个主要功能是 kafka 写数据的时候,是以磁盘顺序写的方式来写的。也就是说,仅仅将数据追加到文件的末尾不是在文件的随机位置来修改数据

普通的机械磁盘如果你要是随机写的话,确实性能极差,也就是随便找到文件的某个位置来写数据。

但是如果你是 追加文件末尾 按照顺序的方式来写数据的话,那么这种磁盘顺序写的性能基本上可以跟写内存的性能相差无几。

基于上面两点,kafka 就实现了写入数据的超高性能

3、零拷贝

大家应该都知道,从 Kafka 里经常要消费数据,那么消费的时候实际上就是要从 kafka 的磁盘文件读取某条数据然后发送给下游的消费者,如下图所示。

那么这里如果频繁的从磁盘读数据然后发给消费者,会增加两次没必要的拷贝,如下图:

一次是从操作系统的 cache 里拷贝到应用进程的缓存里,接着又从应用程序缓存里拷贝回操作系统的 Socket 缓存里。

而且为了进行这两次拷贝,中间还发生了好几次上下文切换,一会儿是应用程序在执行,一会儿上下文切换到操作系统来执行。所以这种方式来读取数据是比较消耗性能的。

Kafka 为了解决这个问题,在读数据的时候是引入零拷贝技术

也就是说,直接让操作系统的 cache 中的数据发送到网卡后传输给下游的消费者,中间跳过了两次拷贝数据的步骤,Socket 缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到 Socket 缓存,如下图所示:

通过 零拷贝技术,就不需要把 os cache 里的数据拷贝到应用缓存,再从应用缓存拷贝到 Socket 缓存了,两次拷贝都省略了,所以叫做零拷贝。

对 Socket 缓存仅仅就是拷贝数据的描述符过去,然后数据就直接从 os cache 中发送到网卡上去了,这个过程大大的提升了数据消费时读取文件数据的性能

Kafka 从磁盘读数据的时候,会先看看 os cache 内存中是否有,如果有的话,其实读数据都是直接读内存的。

kafka 集群经过良好的调优,数据直接写入 os cache 中,然后读数据的时候也是从 os cache 中读。相当于 Kafka 完全基于内存提供数据的写和读了,所以这个整体性能会极其的高。

⭐ 5、 kafka 和 zookeeper 之间的关系

kafka 使用 zookeeper 来保存集群的元数据信息和消费者信息(偏移量),没有 zookeeper,kafka 是工作不起来。在 zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的点,节点路径为/brokers/ids

每个 Broker 服务器在启动时,都会到 Zookeeper 上进行注册,即创建 /brokers/ids/[0-N] 的节点,然后写入 IP,端口等信息,Broker 创建的是临时节点,所以一旦 Broker 上线或者下线,对应 Broker 节点也就被删除了,因此可以通过 zookeeper 上 Broker 节点的变化来动态表征 Broker 服务器的可用性。

⭐  6、生产者向 Kafka 发送消息的执行流程介绍一下?

如下图所示:

(1)生产者要往 Kafka 发送消息时,需要创建 ProducerRecoder,代码如下:

对象会包含目标 topic分区内容,以及指定的 keyvalue,在发送 ProducerRecoder 时,生产者会先把键和值对象序列化成字节数组,然后在网络上传输。

(3)生产者在将消息发送到某个 Topic ,需要经过拦截器序列化器分区器(Partitioner)。

(4)如果消息 ProducerRecord 没有指定 partition 字段,那么就需要依赖分区器,根据 key 这个字段来计算 partition 的值。分区器的作用就是为消息分配分区

  1. 若没有指定分区,且消息的 key 不为空,则使用 murmur 的 Hash 算法(非加密型 Hash 函数,具备高运算性能及低碰撞率)来计算分区分配。
  2. 若没有指定分区,且消息的 key 也是空,则用轮询的方式选择一个分区。

(5)分区选择好之后,会将消息添加到一个记录批次中,这个批次的所有消息都会被发送到相同的 Topicpartition 上。然后会有一个独立的线程负责把这些记录批次发送到相应的 broker 中。

(6)broker 接收到 Msg 后,会作出一个响应。如果成功写入 Kafka 中,就返回一个 RecordMetaData 对象,它包含 TopicPartition 信息,以及记录在分区的 offset

(7)若写入失败,就返回一个错误异常,生产者在收到错误之后尝试重新发送消息,几次之后如果还失败,就返回错误信息。

⭐  7、kafka 如何保证对应类型的消息被写到相同的分区?

通过 消息键分区器 来实现,分区器为生成一个 offset,然后使用 offset 对主题分区进行取模,为消息选取分区,这样就可以保证包含同一个键的消息会被写到同一个分区上。

  1. 如果 ProducerRecord 没有指定分区,且消息的 key 不为空,则使用 Hash 算法(非加密型 Hash 函数,具备高运算性能及低碰撞率)来计算分区分配。
  2. 如果 ProducerRecord 没有指定分区,且消息的 key 也是空,则用 轮询 的方式选择一个分区。

⭐ 8、kafka 文件存储机制了解吗?

如下图所示:

在 Kafka 中,一个 Topic 会被分割成多个 Partition,而 Partition 由多个更小的 Segment 的元素组成。

一个 Partition 下会包含下图的一些文件,由 log、index、timeindex 三个文件组成一个 Segment而文件名中的(0)表示的是一个 Segment 的起始 Offset

Kafka 会根据 log.segment.bytes 的配置来决定单个 Segment 文件(log)的大小,当写入数据达到这个大小时就会创建新的 Segment 。

(1)log 文件解析示意图

(2)index 文件解析示意图

(3)timeindex 文件解析示意图

log、index、timeindex 中存储的都是二进制的数据( log 中存储的是 BatchRecords 消息内容,而 index 和 timeindex 分别是一些索引信息。)

举例:现在创建一个 lyz topic,三个分区,一个副本。

文件中存储的是 OffsetPosition(Offset 对应的消息在 log 文件中的偏移量)的对应关系,这样当有 Offset 时可以快速定位到 Position 读取BatchRecord ,然后再从 BatchRecord 中获取某一条消息。

比如上述 Offset21 会被定位到 27 这个 BatchRecord,然后再从这个 BatchRecord 中取出第二个 Record(27 这个BatchRecord包含了 27 、28 两个 Record)。

Kafka 并不会为每个 Record 都保存一个索引,而是根据 log.index.interval.bytes 等配置 构建稀疏索引信息

Kafka 中还维护了 timeindex,保存了 Timestamp 和 Offset 的关系,在一些场景需要根据 timestamp 来定位消息。timeindex 中的一个( timestampX,offsetY )元素的含义是所有创建时间大于 timestampX 的消息的 Offset 都大于 offsetY

查找方式如下图所示

⭐ 10、 Producer 发送的一条 message 中包含哪些信息?

消息由 可变长度报头可变长度不透明密钥字节数组可变长度不透明值字节数组 组成。

RecordBatchKafka 数据的存储单元,一个 RecordBatch 中包含多个 Record(即我们通常说的一条消息)。RecordBatch 中各个字段的含义如下:

一个 RecordBatch 中可以包含多条消息,即上图中的 Record,而每条消息又可以包含多个 Header 信息,Header 是 Key-Value 形式的。

⭐  11、kafka 如何实现消息是有序的?

生产者:通过分区的 leader 副本负责数据以先进先出的顺序写入,来保证消息顺序性。

消费者:同一个分区内的消息只能被一个 group 里的一个消费者消费,保证分区内消费有序。

kafka 每个 partition 中的消息在写入时都是有序的,消费时, 每个 partition 只能被每一个消费者组中的一个消费者消费,保证了消费时也是有序的。

整个 kafka 不保证有序。如果为了保证 kafka 全局有序,那么设置一个生产者,一个分区,一个消费者。

⭐   12、kafka 有哪些分区算法?

kafka包含三种分区算法:

(1)轮询策略

也称 Round-robin 策略,即顺序分配。比如一个 topic 下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第四条消息时又会重新开始。

轮询策略是 kafka java 生产者 API 默认提供的分区策略。轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是平时最常用的分区策略之一。

(2)随机策略

也称 Randomness 策略。所谓随机就是我们随意地将消息放置在任意一个分区上,如下图:

(3)按 key 分配策略

kafka 允许为每条消息定义消息键,简称为 key。一旦消息被定义了 key,那么你就可以保证同一个 key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,如下图所示:

⭐  13、说说 kafka 的默认消息保留策略?

broker 默认的消息保留策略分为两种:

  1. 日志片段通过 log.segment.bytes 配置(默认是1GB)
  2. 日志片段通过 log.segment.ms 配置 (默认7天)

⭐  14、kafka 如何实现单个集群间的消息复制?

Kafka 消息负责机制只能在单个集群中进行复制,不能在多个集群之间进行。

kafka 提供了一个叫做 MirrorMaker 的核心组件,该组件包含一个生产者和一个消费者,两者之间通过一个队列进行相连,当消费者从一个集群读取消息,生产者把消息发送到另一个集群。

⭐  15、Kafka 消息确认(ack 应答)机制了解吗?

为保证 producer 发送的数据,能可靠的达到指定的 topic ,Producer 提供了消息确认机制。生产者往 Broker 的 topic 中发送消息时,可以通过配置来决定有几个副本收到这条消息才算消息发送成功。可以在定义 Producer 时通过 acks 参数指定,这个参数支持以下三种值:

(1)acks = 0:producer 不会等待任何来自 broker 的响应

特点:低延迟,高吞吐,数据可能会丢失。

如果当中出现问题,导致 broker 没有收到消息,那么 producer 无从得知,会造成消息丢失

(2)acks = 1(默认值):只要集群中 partition 的 Leader 节点收到消息,生产者就会收到一个来自服务器的成功响应。

如果在 follower 同步之前,leader 出现故障,将会丢失数据。

此时的吞吐量主要取决于使用的是 同步发送 还是 异步发送 ,吞吐量还受到发送中消息数量的限制,例如 producer 在收到 broker 响应之前可以发送多少个消息。

(3)acks = -1:只有当所有参与复制的节点全部都收到消息时,生产者才会收到一个来自服务器的成功响应

这种模式是最安全的,可以保证不止一个服务器收到消息,就算有服务器发生崩溃,整个集群依然可以运行。

根据实际的应用场景,选择设置不同的 acks,以此保证数据的可靠性。

另外,Producer 发送消息还可以选择同步或异步模式,如果设置成异步,虽然会极大的提高消息发送的性能,但是这样会增加丢失数据的风险。如果需要确保消息的可靠性,必须将 producer.type 设置为 sync。

版本开始引入了分区副本机制。在创建 topic 的时候指定 replication-factor,默认副本为 3 。

副本是相对 partition 而言的,一个分区中包含一个或多个副本,其中一个为leader 副本,其余为follower 副本,各个副本位于不同的 broker 节点中。

所有的读写操作都是经过 Leader 进行的,同时 follower 会定期地去 leader 上复制数据。当 Leader 挂掉之后,其中一个 follower 会重新成为新的 Leader。通过分区副本,引入了数据冗余,同时也提供了 Kafka 的数据可靠性

Kafka 的分区多副本架构是 Kafka 可靠性保证的核心,把消息写入多个副本可以使 Kafka 在发生崩溃时仍能保证消息的持久性。

⭐17、说一下 kafka 的 ISR 机制?

在分区中,所有副本统称为 AR ,Leader 维护了一个动态的 in-sync replica(ISR),ISR 是指与 leader 副本保持同步状态的副本集合。当然 leader 副本本身也是这个集合中的一员

当 ISR 中的 follower 完成数据同步之后, leader 就会给 follower 发送 ack ,如果其中一个 follower 长时间未向 leader 同步数据,该 follower 将会被踢出 ISR 集合,该时间阈值由 replica.log.time.max.ms 参数设定。当 leader 发生故障后,就会从 ISR 集合中重新选举出新的 leader。

⭐18、LEO、HW、LSO、LW 分别代表什么?

  • LEO :是 LogEndOffset 的简称,代表当前日志文件中下一条。

  • HW:水位或水印一词,也可称为高水位(high watermark),通常被用在流式处理领域(flink、spark),以表征元素或事件在基于时间层面上的进展。在 kafka 中,水位的概念与时间无关,而是与位置信息相关。严格来说,它表示的就是位置信息,即位移(offset)。取 partition 对应的ISR中最小的 LEO作为HWconsumer 最多只能消费到 HW 所在的上一条信息。

  • LSO: 是 LastStableOffset 的简称,对未完成的事务而言,LSO 的值等于事务中第一条消息的位置(firstUnstableOffset),对已完成的事务而言,它的值同HW 相同。

  • LW: Low Watermark 低水位,代表AR 集合中最小的 logStartOffset 值。

  • ⭐19、如何进行 Leader 副本选举?

    每个分区的 leader 会维护一个 ISR 集合,ISR 列表里面就是 follower 副本的 Borker 编号,只有“跟得上” Leader 的 follower 副本才能加入到 ISR 里面,这个是通过 replica.lag.time.max.ms 参数配置的。只有 ISR 里的成员才有被选为 leader 的可能。

    所以当 Leader 挂掉了,而且 unclean.leader.election.enable=false 的情况下,Kafka 会从 ISR 列表中选择 第一个 follower 作为新的 Leader,因为这个分区拥有最新的已经 committed 的消息。通过这个可以保证已经 committed 的消息的数据可靠性。

    ⭐ 20、如何进行 broker Leader 选举?

    (1) 在 kafka 集群中,会有多个 broker 节点,集群中第一个启动的 broker 会通过在 zookeeper 中创建临时节点 /controller 来让自己成为控制器,其他 broker 启动时也会在 zookeeper 中创建临时节点,但是发现节点已经存在,所以它们会收到一个异常,意识到控制器已经存在,那么就会在 zookeeper 中创建 watch 对象,便于它们收到控制器变更的通知。

    (2) 如果集群中有一个 broker 发生异常退出了,那么控制器就会检查这个 broker 是否有分区的副本 leader ,如果有那么这个分区就需要一个新的 leader,此时控制器就会去遍历其他副本,决定哪一个成为新的 leader,同时更新分区的 ISR 集合。

    (3) 如果有一个 broker 加入集群中,那么控制器就会通过 Broker ID 去判断新加入的 broker 中是否含有现有分区的副本,如果有,就会从分区副本中去同步数据。

    (4) 集群中每选举一次控制器,就会通过 zookeeper 创建一个 controller epoch,每一个选举都会创建一个更大,包含最新信息的 epoch,如果有 broker 收到比这个 epoch 旧的数据,就会忽略它们,kafka 也通过这个 epoch 来防止集群产生“脑裂”。

    ⭐ 21、kafka 事务了解吗?

    Kafka 在 0.11版本引入事务支持,事务可以保证 Kafka 在 Exactly Once 语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。

    Producer 事务

    为了实现跨分区跨会话事务,需要引入一个全局唯一的 Transaction ID,并将 Producer 获取的 PID 和 Transaction ID  绑定。这样当 Producer 重启后就可以通过正在进行的 Transaction ID 获取原来的 PID。

    为了管理 Transaction,Kafka 引入了一个新的组件 Transaction CoordinatorProducer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

    **Consumer 事务 **

    上述事务机制主要是从Producer 方面考虑,对于 Consumer 而言,事务的保证就会相对较弱,尤其是无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过 offset 访问任意信息,而且不同的Segment File 生命周期不同,同一事务的消息可能会出现重启后被删除的情况。

    ⭐ 22、kafka的消费者组跟分区之间有什么关系?

    (1) 在 kafka 中,通过消费者组管理消费者,假设一个主题中包含 4 个分区,在一个消费者组中只要一个消费者。那消费者将收到全部 4 个分区的消息。

    (2) 如果存在两个消费者,那么四个分区将根据分区分配策略分配个两个消费者,如下图所示:

    (3)如果存在四个消费者,将平均分配,每个消费者消费一个分区。

    (4)如果存在5个消费者,就会出现消费者数量多于分区数量,那么多余的消费者将会被闲置,不会接收到任何信息。

    ⭐ 23、如何保证每个应用程序都可以获取到 Kafka 主题中的所有消息,而不是部分消息?

    为每个应用程序创建一个消费者组,然后往组中添加消费者来伸缩读取能力和处理能力,每个群组消费主题中的消息时,互不干扰。

    ⭐ 24、如何实现 kafka 消费者每次只消费指定数量的消息?

    写一个队列,把 consumer 作为队列类的一个属性,然后增加一个消费计数的计数器,当到达指定数量时,关闭 consumer。

    ⭐ 25、kafka 如何实现多线程的消费?

    kafka 允许同组的多个 partition 被一个 consumer 消费,但不允许一个 partition 被同组的多个 consumer 消费。

    实现多线程步骤如下:

    1. 生产者随机分区提交数据(自定义随机分区)。
    2. 消费者修改单线程模式为多线程,在消费方面得注意,得遍历所有分区,否则还是只消费了一个区。

    ⭐ 26、 kafka 消费支持几种消费模式?

    kafka消费消息时支持三种模式:

  • at most once 模式 最多一次。保证每一条消息 commit 成功之后,再进行消费处理。消息可能会丢失,但不会重复。

  • at least once 模式 至少一次。保证每一条消息处理成功之后,再进行commit。消息不会丢失,但可能会重复。

  • exactly once 模式 精确传递一次。将 offset 作为唯一 id 与消息同时处理,并且保证处理的原子性。消息只会处理一次,不丢失也不会重复。但这种方式很难做到。

  • kafka 默认的模式是 at least once ,但这种模式可能会产生重复消费的问题,所以在业务逻辑必须做幂等设计。

    在业务场景保存数据时使用了 INSERT INTO ...ON DUPLICATE KEY UPDATE语法,不存在时插入,存在时更新,是天然支持幂等性的。

    ⭐ 27、kafka 如何保证数据的不重复和不丢失?

  • exactly once 模式 精确传递一次。将 offset 作为唯一 id 与消息同时处理,并且保证处理的原子性。消息只会处理一次,不丢失也不会重复。但这种方式很难做到。
  • kafka 默认的模式是 at least once ,但这种模式可能会产生重复消费的问题,所以在业务逻辑必须做幂等设计。

    使用 exactly Once + 幂等操作,可以保证数据不重复,不丢失。

    ⭐ 28、kafka 是如何清理过期数据的?

    kafka 将数据持久化到了硬盘上,允许你配置一定的策略对数据清理,清理的策略有两个,删除和压缩

    数据清理的方式

    1、删除

    log.cleanup.policy=delete 启用删除策略

    直接删除,删除后的消息不可恢复。可配置以下两个策略:

    形式的实现,删除操作进行时,读取操作的二分查找功能实际是在一个静态的快照副本上进行的,这类似于 Java 的 CopyOnWriteArrayList。

    2、压缩

    将数据压缩,只保留每个 key 最后一个版本的数据。

    首先在 broker 的配置中设置 log.cleaner.enable=true 启用 cleaner,这个默认是关闭的。

    在 topic 的配置中设置 log.cleanup.policy=compact 启用压缩策略。

    读者福利


    机械工业出版社赞助
    书籍: 共「5」本, 《精通Linux内核:智能设备开发核心技术 》《人工智能程序员面试笔试宝典》二选一


    精通Linux内核:智能设备开发核心技术  编辑推荐

    经验:作者曾就职于华为/Intel/AMD,多位x86专家担当顾问
    实用:内核重点、工作疑惑、常见陷阱、关键数据结构全收录
    深入:作者阅读代码量数百万行,透视模块间关联和背后逻辑
    动手:配有大量实例,复杂机制均以图表形式帮读者厘清脉络

    工智能程序员面试笔试宝典 编辑推荐

    这是一本人工智能相关领域面试、笔试、经验心得、算法、考点、难点、真题解析一应俱全,获取高薪必备。
    作者:算法工程师,清华大学博士,在前沿会议和期刊上发表论文多篇,对机器学习和深度学习有比较深入的研究。主要研究方向为图卷积神经网络和强化学习。目前在某互联网企业从事算法研发工作

    好评奖1: 「转发本文到朋友圈」+ 「留言」,留言点赞数前「6」名   挑选「3位优质留言每人获得一本。「必须是转发了朋友圈」。

    好友奖2:  在朋友圈点赞 再送「2」本 

    当然,不差钱的朋友也可以直接通过下面小程序直接购买本书

    End


    本文原创作者:土哥、一名大数据算法工程师。

    文章首发平台:微信公众号【3分钟秒懂大数据】

    需要 PDF 版本的小伙伴们,可以加我微信:youzhiqiangshou_02,备注:Kafka,进行领取。

    原创不易,各位觉得文章不错的话,不妨点赞(在看)、留言、转发三连走起!谢谢大家!

    重磅干货 | 五万字长文总结 C/C++ 知识(下)


    置顶/星标公众号????,硬核文章第一时间送达!

    链接 | https://github.com/huihut/interview

    回顾上篇:《重磅干货 | 五万字长文总结 C/C++ 知识(上)

    网络层

    • IP(Internet Protocol,网际协议)是为计算机网络相互连接进行通信而设计的协议。

    • ARP(Address Resolution Protocol,地址解析协议)

    • ICMP(Internet Control Message Protocol,网际控制报文协议)

    • IGMP(Internet Group Management Protocol,网际组管理协议)

    IP 网际协议

    IP 地址分类:

    • IP 地址 ::= {<网络号>,<主机号>}

    IP 地址类别网络号网络范围主机号IP 地址范围
    A 类8bit,第一位固定为 00 —— 12724bit1.0.0.0 —— 127.255.255.255
    B 类16bit,前两位固定为 10128.0 —— 191.25516bit128.0.0.0 —— 191.255.255.255
    C 类24bit,前三位固定为 110192.0.0 —— 223.255.2558bit192.0.0.0 —— 223.255.255.255
    D 类前四位固定为 1110,后面为多播地址


    E 类前五位固定为 11110,后面保留为今后所用


    应用:

    • PING(Packet InterNet Groper,分组网间探测)测试两个主机之间的连通性

        • TTL(Time To Live,生存时间)该字段指定 IP 包被路由器丢弃之前允许通过的最大网段数量

    内部网关协议

    • RIP(Routing Information Protocol,路由信息协议)

    • OSPF(Open Sortest Path First,开放最短路径优先)

    外部网关协议

    • BGP(Border Gateway Protocol,边界网关协议)

    IP多播

    • IGMP(Internet Group Management Protocol,网际组管理协议)

    • 多播路由选择协议

    VPN 和 NAT

    • VPN(Virtual Private Network,虚拟专用网)

    • NAT(Network Address Translation,网络地址转换)

    路由表包含什么?

    1. 网络 ID(Network ID, Network number):就是目标地址的网络 ID。

    2. 子网掩码(subnet mask):用来判断 IP 所属网络

    3. 下一跳地址/接口(Next hop / interface):就是数据在发送到目标地址的旅途中下一站的地址。其中 interface 指向 next hop(即为下一个 route)。一个自治系统(AS, Autonomous system)中的 route 应该包含区域内所有的子网络,而默认网关(Network id: 0.0.0.0, Netmask: 0.0.0.0)指向自治系统的出口。

    根据应用和执行的不同,路由表可能含有如下附加信息:

    1. 花费(Cost):就是数据发送过程中通过路径所需要的花费。

    2. 路由的服务质量

    3. 路由中需要过滤的出/入连接列表

    运输层

    协议:

    • TCP(Transmission Control Protocol,传输控制协议)

    • UDP(User Datagram Protocol,用户数据报协议)

    端口:

    应用程序FTPTELNETSMTPDNSTFTPHTTPHTTPSSNMP
    端口号212325536980443161

    受限于公众号文章字数限制,后续部分请看【今天的第二篇推文】,

    TCP

    • TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,其传输的单位是报文段。

    特征:

    • 面向连接

    • 只能点对点(一对一)通信

    • 可靠交互

    • 全双工通信

    • 面向字节流

    TCP 如何保证可靠传输:

    • 确认和超时重传

    • 数据合理分片和排序

    • 流量控制

    • 拥塞控制

    • 数据校验

    TCP 首部

    TCP:状态控制码(Code,Control Flag),占 6 比特,含义如下:

    • URG:紧急比特(urgent),当 URG=1 时,表明紧急指针字段有效,代表该封包为紧急封包。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据), 且上图中的 Urgent Pointer 字段也会被启用。

    • ACK:确认比特(Acknowledge)。只有当 ACK=1 时确认号字段才有效,代表这个封包为确认封包。当 ACK=0 时,确认号无效。

    • PSH:(Push function)若为 1 时,代表要求对方立即传送缓冲区内的其他对应封包,而无需等缓冲满了才送。

    • RST:复位比特(Reset),当 RST=1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。

    • SYN:同步比特(Synchronous),SYN 置为 1,就表示这是一个连接请求或连接接受报文,通常带有 SYN 标志的封包表示『主动』要连接到对方的意思。

    • FIN:终止比特(Final),用来释放一个连接。当 FIN=1 时,表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。

    UDP

    • UDP(User Datagram Protocol,用户数据报协议)是 OSI(Open System Interconnection 开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务,其传输的单位是用户数据报。

    特征:

    • 无连接

    • 尽最大努力交付

    • 面向报文

    • 没有拥塞控制

    • 支持一对一、一对多、多对一、多对多的交互通信

    • 首部开销小

    TCP 与 UDP 的区别

    1. TCP 面向连接,UDP 是无连接的;

    2. TCP 提供可靠的服务,也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付

    3. TCP 的逻辑通信信道是全双工的可靠信道;UDP 则是不可靠信道

    4. 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信

    5. TCP 面向字节流(可能出现黏包问题),实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的(不会出现黏包问题)

    6. UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话,实时视频会议等)

    7. TCP 首部开销20字节;UDP 的首部开销小,只有 8 个字节

    TCP 黏包问题

    原因

    TCP 是一个基于字节流的传输服务(UDP 基于报文的),“流” 意味着 TCP 所传输的数据是没有边界的。所以可能会出现两个数据包黏在一起的情况。

    解决
    • 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。

    • 包头加上包体长度。包头是定长的 4 个字节,说明了包体的长度。接收对等方先接收包头长度,依据包头长度来接收包体。

    • 在数据包之间设置边界,如添加特殊符号 \\r\\n 标记。FTP 协议正是这么做的。但问题在于如果数据正文中也含有 \\r\\n,则会误判为消息的边界。

    • 使用更加复杂的应用层协议。

    TCP 流量控制

    概念

    流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收。

    方法

    利用可变窗口进行流量控制

    TCP 拥塞控制

    概念

    拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。

    方法
    • 慢开始( slow-start )

    • 拥塞避免( congestion avoidance )

    • 快重传( fast retransmit )

    • 快恢复( fast recovery 


    TCP 传输连接管理

    因为 TCP 三次握手建立连接、四次挥手释放连接很重要,所以附上《计算机网络(第 7 版)-谢希仁》书中对此章的详细描述:https://github.com/huihut/interview/blob/master/images/TCP-transport-connection-management.png

    TCP 三次握手建立连接
    UDP 报文

    【TCP 建立连接全过程解释】

    1. 客户端发送 SYN 给服务器,说明客户端请求建立连接;

    2. 服务端收到客户端发的 SYN,并回复 SYN+ACK 给客户端(同意建立连接);

    3. 客户端收到服务端的 SYN+ACK 后,回复 ACK 给服务端(表示客户端收到了服务端发的同意报文);

    4. 服务端收到客户端的 ACK,连接已建立,可以数据传输。

    TCP 为什么要进行三次握手?

    【答案一】因为信道不可靠,而 TCP 想在不可靠信道上建立可靠地传输,那么三次通信是理论上的最小值。(而 UDP 则不需建立可靠传输,因此 UDP 不需要三次握手。)

    Google Groups . TCP 建立连接为什么是三次握手?{技术}{网络通信}

    【答案二】因为双方都需要确认对方收到了自己发送的序列号,确认过程最少要进行三次通信。

    知乎 . TCP 为什么是三次握手,而不是两次或四次?

    【答案三】为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

    《计算机网络(第 7 版)-谢希仁》

    【TCP 释放连接全过程解释】

    1. 客户端发送 FIN 给服务器,说明客户端不必发送数据给服务器了(请求释放从客户端到服务器的连接);

    2. 服务器接收到客户端发的 FIN,并回复 ACK 给客户端(同意释放从客户端到服务器的连接);

    3. 客户端收到服务端回复的 ACK,此时从客户端到服务器的连接已释放(但服务端到客户端的连接还未释放,并且客户端还可以接收数据);

    4. 服务端继续发送之前没发完的数据给客户端;

    5. 服务端发送 FIN+ACK 给客户端,说明服务端发送完了数据(请求释放从服务端到客户端的连接,就算没收到客户端的回复,过段时间也会自动释放);

    6. 客户端收到服务端的 FIN+ACK,并回复 ACK 给客户端(同意释放从服务端到客户端的连接);

    7. 服务端收到客户端的 ACK 后,释放从服务端到客户端的连接。

    TCP 为什么要进行四次挥手?

    【问题一】TCP 为什么要进行四次挥手?/ 为什么 TCP 建立连接需要三次,而释放连接则需要四次?

    【答案一】因为 TCP 是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四次挥手)。所以 TCP 释放连接时服务器的 ACK 和 FIN 是分开发送的(中间隔着数据传输),而 TCP 建立连接时服务器的 ACK 和 SYN 是一起发送的(第二次握手),所以 TCP 建立连接需要三次,而释放连接则需要四次。

    【问题二】为什么 TCP 连接时可以 ACK 和 SYN 一起发送,而释放时则 ACK 和 FIN 分开发送呢?(ACK 和 FIN 分开是指第二次和第三次挥手)

    【答案二】因为客户端请求释放时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端 FIN 请求(服务端发送 ACK),然后数据传输,传输完成后,服务端再提出 FIN 请求(服务端发送 FIN);而连接时则没有中间的数据传输,因此连接时可以 ACK 和 SYN 一起发送。

    【问题三】为什么客户端释放最后需要 TIME-WAIT 等待 2MSL 呢?

    【答案三】

    1. 为了保证客户端发送的最后一个 ACK 报文能够到达服务端。若未成功到达,则服务端超时重传 FIN+ACK 报文段,客户端再重传 ACK,并重新计时。

    1. 防止已失效的连接请求报文段出现在本连接中。TIME-WAIT 持续 2MSL 可使本连接持续的时间内所产生的所有报文段都从网络中消失,这样可使下次连接中不会出现旧的连接报文段。



    应用层

    DNS

    • DNS(Domain Name System,域名系统)是互联网的一项服务。它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS 使用 TCP 和 UDP 端口 53。当前,对于每一级域名长度的限制是 63 个字符,域名总长度则不能超过 253 个字符。

    域名:

    • 域名 ::= {<三级域名>.<二级域名>.<顶级域名>},如:blog.huihut.com

    FTP

    • FTP(File Transfer Protocol,文件传输协议)是用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式,使用 TCP 数据报,提供交互式访问,双向传输。

    • TFTP(Trivial File Transfer Protocol,简单文件传输协议)一个小且易实现的文件传输协议,也使用客户-服务器方式,使用UDP数据报,只支持文件传输而不支持交互,没有列目录,不能对用户进行身份鉴定

    TELNET

    • TELNET 协议是 TCP/IP 协议族中的一员,是 Internet 远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。

    • HTTP(HyperText Transfer Protocol,超文本传输协议)是用于从 WWW(World Wide Web,万维网)服务器传输超文本到本地浏览器的传送协议。

    • SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    • Socket 建立网络通信连接至少要一对端口号(Socket)。Socket 本质是编程接口(API),对 TCP/IP 的封装,TCP/IP 也要提供可供程序员做网络开发所用的接口,这就是 Socket 编程接口。

    WWW

    • WWW(World Wide Web,环球信息网,万维网)是一个由许多互相链接的超文本组成的系统,通过互联网访问

    URL
    • URL(Uniform Resource Locator,统一资源定位符)是因特网上标准的资源的地址(Address)

    标准格式:

    • 协议类型:[//服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]

    完整格式:

    • 协议类型:[//[访问资源需要的凭证信息@]服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]

    其中【访问凭证信息@;:端口号;?查询;#片段ID】都属于选填项  
    如:https://github.com/huihut/interview#cc

    HTTP

    HTTP(HyperText Transfer Protocol,超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网的数据通信的基础。

    请求方法

    方法意义
    OPTIONS请求一些选项信息,允许客户端查看服务器的性能
    GET请求指定的页面信息,并返回实体主体
    HEAD类似于 get 请求,只不过返回的响应中没有具体的内容,用于获取报头
    POST向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改
    PUT从客户端向服务器传送的数据取代指定的文档的内容
    DELETE请求服务器删除指定的页面
    TRACE回显服务器收到的请求,主要用于测试或诊断

    状态码(Status-Code)

    • 1xx:表示通知信息,如请求收到了或正在进行处理

      • 100 Continue:继续,客户端应继续其请求

      • 101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到 HTTP 的新版本协议

    • 2xx:表示成功,如接收或知道了

      • 200 OK: 请求成功

    • 3xx:表示重定向,如要完成请求还必须采取进一步的行动

      • 301 Moved Permanently: 永久移动。请求的资源已被永久的移动到新 URL,返回信息会包括新的 URL,浏览器会自动定向到新 URL。今后任何新的请求都应使用新的 URL 代替

    • 4xx:表示客户的差错,如请求中有错误的语法或不能完成

      • 400 Bad Request: 客户端请求的语法错误,服务器无法理解

      • 401 Unauthorized: 请求要求用户的身份认证

      • 403 Forbidden: 服务器理解请求客户端的请求,但是拒绝执行此请求(权限不够)

      • 404 Not Found: 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置 “您所请求的资源无法找到” 的个性页面

      • 408 Request Timeout: 服务器等待客户端发送的请求时间过长,超时

    • 5xx:表示服务器的差错,如服务器失效无法完成请求

      • 500 Internal Server Error: 服务器内部错误,无法完成请求

      • 503 Service Unavailable: 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的 Retry-After 头信息中

      • 504 Gateway Timeout: 充当网关或代理的服务器,未及时从远端服务器获取请求

    更多状态码:菜鸟教程 . HTTP状态码

    其他协议
    • SMTP(Simple Main Transfer Protocol,简单邮件传输协议)是在 Internet 传输 Email 的标准,是一个相对简单的基于文本的协议。在其之上指定了一条消息的一个或多个接收者(在大多数情况下被确认是存在的),然后消息文本会被传输。可以很简单地通过 Telnet 程序来测试一个 SMTP 服务器。SMTP 使用 TCP 端口 25。

    • DHCP(Dynamic Host Configuration Protocol,动态主机设置协议)是一个局域网的网络协议,使用 UDP 协议工作,主要有两个用途:

      • 用于内部网络或网络服务供应商自动分配 IP 地址给用户

      • 用于内部网络管理员作为对所有电脑作中央管理的手段

    • SNMP(Simple Network Management Protocol,简单网络管理协议)构成了互联网工程工作小组(IETF,Internet Engineering Task Force)定义的 Internet 协议族的一部分。该协议能够支持网络管理系统,用以监测连接到网络上的设备是否有任何引起管理上关注的情况。

    网络编程

    Socket

    Socket 中的 read()、write() 函数

    ssize_t read(int fd, void *buf, size_t count);
    ssize_t write(int fd, const void *buf, size_t count);
    
    read()
    • read 函数是负责从 fd 中读取内容。

    • 当读成功时,read 返回实际所读的字节数。

    • 如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。

    • 如果错误为 EINTR 说明读是由中断引起的;如果是 ECONNREST 表示网络连接出了问题。

    write()
    • write 函数将 buf 中的 nbytes 字节内容写入文件描述符 fd。

    • 成功时返回写的字节数。失败时返回 -1,并设置 errno 变量。

    • 在网络程序中,当我们向套接字文件描述符写时有俩种可能。

    • (1)write 的返回值大于 0,表示写了部分或者是全部的数据。

    • (2)返回的值小于 0,此时出现了错误。

    • 如果错误为 EINTR 表示在写的时候出现了中断错误;如果为 EPIPE 表示网络连接出现了问题(对方已经关闭了连接)。

    Socket 中 TCP 的三次握手建立连接

    我们知道 TCP 建立连接要进行 “三次握手”,即交换三个分组。大致流程如下:

    1. 客户端向服务器发送一个 SYN J

    2. 服务器向客户端响应一个 SYN K,并对 SYN J 进行确认 ACK J+1

    3. 客户端再想服务器发一个确认 ACK K+1

    只有就完了三次握手,但是这个三次握手发生在 Socket 的那几个函数中呢?请看下图:

    socket 中发送的 TCP 三次握手

    从图中可以看出:

    1. 当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态; 

    2. 服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求向客户端发送 SYN K ,ACK J+1,这时 accept 进入阻塞状态; 

    3. 客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认; 

    4. 服务器收到 ACK K+1 时,accept 返回,至此三次握手完毕,连接建立。

    Socket 中 TCP 的四次握手释放连接

    上面介绍了 socket 中 TCP 的三次握手建立过程,及其涉及的 socket 函数。现在我们介绍 socket 中的四次握手释放连接的过程,请看下图:

    socket 中发送的 TCP 四次握手

    图示过程如下:

    1. 某个应用进程首先调用 close 主动关闭连接,这时 TCP 发送一个 FIN M;

    2. 另一端接收到 FIN M 之后,执行被动关闭,对这个 FIN 进行确认。它的接收也作为文件结束符传递给应用进程,因为 FIN 的接收意味着应用进程在相应的连接上再也接收不到额外数据;

    3. 一段时间之后,接收到文件结束符的应用进程调用 close 关闭它的 socket。这导致它的 TCP 也发送一个 FIN N;

    4. 接收到这个 FIN 的源发送端 TCP 对它进行确认。

    这样每个方向上都有一个 FIN 和 ACK。

    数据库

    • 数据库事务四大特性:原子性、一致性、分离性、持久性

    • 数据库索引:顺序索引、B+ 树索引、hash 索引
      MySQL 索引背后的数据结构及算法原理

    • SQL 约束 (Constraints)

    范式

    • 第一范式(1NF):属性(字段)是最小单位不可再分

    • 第二范式(2NF):满足 1NF,每个非主属性完全依赖于主键(消除 1NF 非主属性对码的部分函数依赖)

    • 第三范式(3NF):满足 2NF,任何非主属性不依赖于其他非主属性(消除 2NF 主属性对码的传递函数依赖)

    • 鲍依斯-科得范式(BCNF):满足 3NF,任何非主属性不能对主键子集依赖(消除 3NF 主属性对码的部分和传递函数依赖)

    • 第四范式(4NF):满足 3NF,属性之间不能有非平凡且非函数依赖的多值依赖(消除 3NF 非平凡且非函数依赖的多值依赖)

    设计模式

    各大设计模式例子参考:CSDN专栏 . C++ 设计模式 系列博文

    设计模式工程目录

    单例模式

    单例模式例子

    抽象工厂模式

    抽象工厂模式例子

    适配器模式

    适配器模式例子

    桥接模式

    桥接模式例子

    观察者模式

    观察者模式例子

    设计模式的六大原则

    • 单一职责原则(SRP,Single Responsibility Principle)

    • 里氏替换原则(LSP,Liskov Substitution Principle)

    • 依赖倒置原则(DIP,Dependence Inversion Principle)

    • 接口隔离原则(ISP,Interface Segregation Principle)

    • 迪米特法则(LoD,Law of Demeter)

    • 开放封闭原则(OCP,Open Close Principle)

    链接装载库

    内存、栈、堆

    一般应用程序内存空间有如下区域:

    • 栈:由操作系统自动分配释放,存放函数的参数值、局部变量等的值,用于维护函数调用的上下文

    • 堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收,用来容纳应用程序动态分配的内存区域

    • 可执行文件映像:存储着可执行文件在内存中的映像,由装载器装载是将可执行文件的内存读取或映射到这里

    • 保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称,如通常 C 语言讲无效指针赋值为 0(NULL),因此 0 地址正常情况下不可能有效的访问数据

    栈保存了一个函数调用所需要的维护信息,常被称为堆栈帧(Stack Frame)或活动记录(Activate Record),一般包含以下几方面:

    • 函数的返回地址和参数

    • 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量

    • 保存上下文:包括函数调用前后需要保持不变的寄存器

    堆分配算法:

    • 空闲链表(Free List)

    • 位图(Bitmap)

    • 对象池

    “段错误(segment fault)” 或 “非法操作,该内存地址不能 read/write”

    典型的非法指针解引用造成的错误。当指针指向一个不允许读写的内存地址,而程序却试图利用指针来读或写该地址时,会出现这个错误。

    普遍原因:

    • 将指针初始化为 NULL,之后没有给它一个合理的值就开始使用指针

    • 没用初始化栈中的指针,指针的值一般会是随机数,之后就直接开始使用指针

    编译链接

    各平台文件格式

    平台可执行文件目标文件动态库/共享对象静态库
    Windowsexeobjdlllib
    Unix/LinuxELF、outosoa
    MacMach-Oodylib、tbd、frameworka、framework

    编译链接过程

    1. 预编译(预编译器处理如 #include#define 等预编译指令,生成 .i.ii 文件)

    2. 编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)

    3. 汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)

    4. 链接(连接器进行地址和空间分配、符号决议、重定位,生成 .out 文件)

    现在版本 GCC 把预编译和编译合成一步,预编译编译程序 cc1、汇编器 as、连接器 ld

    MSVC 编译环境,编译器 cl、连接器 link、可执行文件查看器 dumpbin

    目标文件

    编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。

    可执行文件(Windows 的 .exe 和 Linux 的 ELF)、动态链接库(Windows 的 .dll 和 Linux 的 .so)、静态链接库(Windows 的 .lib 和 Linux 的 .a)都是按照可执行文件格式存储(Windows 按照 PE-COFF,Linux 按照 ELF)

    目标文件格式
    • Windows 的 PE(Portable Executable),或称为 PE-COFF,.obj 格式

    • Linux 的 ELF(Executable Linkable Format),.o 格式

    • Intel/Microsoft 的 OMF(Object Module Format)

    • Unix 的 a.out 格式

    • MS-DOS 的 .COM 格式

    PE 和 ELF 都是 COFF(Common File Format)的变种

    目标文件存储结构
    功能
    File Header文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等)
    .text p代码段,执行语句编译成的机器代码
    .data p数据段,已初始化的全局变量和局部静态变量
    .bss pBSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间)
    .rodata p只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量
    .comment p注释信息段,存放编译器版本信息
    .note.GNU-stack p堆栈提示段

    其他段略

    链接的接口————符号

    在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。

    如下符号表(Symbol Table):

    Symbol(符号名)Symbol Value (地址)
    main0x100
    Add0x123
    ......

    Linux 的共享库(Shared Library)

    Linux 下的共享库就是普通的 ELF 共享对象。

    共享库版本更新应该保证二进制接口 ABI(Application Binary Interface)的兼容

    命名

    libname.so.x.y.z

    • x:主版本号,不同主版本号的库之间不兼容,需要重新编译

    • y:次版本号,高版本号向后兼容低版本号

    • z:发布版本号,不对接口进行更改,完全兼容

    路径

    大部分包括 Linux 在内的开源系统遵循 FHS(File Hierarchy Standard)的标准,这标准规定了系统文件如何存放,包括各个目录结构、组织和作用。

    • /lib:存放系统最关键和最基础的共享库,如动态链接器、C 语言运行库、数学库等

    • /usr/lib:存放非系统运行时所需要的关键性的库,主要是开发库

    • /usr/local/lib:存放跟操作系统本身并不十分相关的库,主要是一些第三方应用程序的库

    动态链接器会在 /lib/usr/lib 和由 /etc/ld.so.conf 配置文件指定的,目录中查找共享库

    环境变量

    • LD_LIBRARY_PATH:临时改变某个应用程序的共享库查找路径,而不会影响其他应用程序

    • LD_PRELOAD:指定预先装载的一些共享库甚至是目标文件

    • LD_DEBUG:打开动态链接器的调试功能

    so 共享库的编写

    使用 CLion 编写共享库

    创建一个名为 MySharedLib 的共享库

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.10)
    project(MySharedLib)
    
    set(CMAKE_CXX_STANDARD 11)
    
    add_library(MySharedLib SHARED library.cpp library.h)
    

    library.h

    #ifndef MYSHAREDLIB_LIBRARY_H
    #define MYSHAREDLIB_LIBRARY_H
    
    // 打印 Hello World!
    void hello();
    
    // 使用可变模版参数求和
    template <typename T>
    T sum(T t)
    {
        return t;
    }
    template <typename T, typename ...Types>
    T sum(T first, Types ... rest)
    {
        return first + sum<T>(rest...);
    }
    
    #endif
    

    library.cpp

    #include <iostream>
    #include "library.h"
    
    void hello() {
        std::cout << "Hello, World!" << std::endl;
    }
    

    so 共享库的使用(被可执行项目调用)

    使用 CLion 调用共享库

    创建一个名为 TestSharedLib 的可执行项目

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.10)
    project(TestSharedLib)
    
    # C++11 编译
    set(CMAKE_CXX_STANDARD 11)
    
    # 头文件路径
    set(INC_DIR /home/xx/code/clion/MySharedLib)
    # 库文件路径
    set(LIB_DIR /home/xx/code/clion/MySharedLib/cmake-build-debug)
    
    include_directories(${INC_DIR})
    link_directories(${LIB_DIR})
    link_libraries(MySharedLib)
    
    add_executable(TestSharedLib main.cpp)
    
    # 链接 MySharedLib 库
    target_link_libraries(TestSharedLib MySharedLib)
    

    main.cpp

    #include <iostream>
    #include "library.h"
    using std::cout;
    using std::endl;
    
    int main() {
    
        hello();
        cout << "1 + 2 = " << sum(1,2) << endl;
        cout << "1 + 2 + 3 = " << sum(1,2,3) << endl;
    
        return 0;
    }
    

    执行结果

    Hello, World!
    1 + 2 = 3
    1 + 2 + 3 = 6
    

    Windows 应用程序入口函数

    • GUI(Graphical User Interface)应用,链接器选项:/SUBSYSTEM:WINDOWS

    • CUI(Console User Interface)应用,链接器选项:/SUBSYSTEM:CONSOLE

    _tWinMain 与 _tmain 函数声明

    Int WINAPI _tWinMain(
        HINSTANCE hInstanceExe,
        HINSTANCE,
        PTSTR pszCmdLine,
        int nCmdShow);
    
    int _tmain(
        int argc,
        TCHAR *argv[],
        TCHAR *envp[]);
    
    应用程序类型入口点函数嵌入可执行文件的启动函数
    处理ANSI字符(串)的GUI应用程序_tWinMain(WinMain)WinMainCRTSartup
    处理Unicode字符(串)的GUI应用程序_tWinMain(wWinMain)wWinMainCRTSartup
    处理ANSI字符(串)的CUI应用程序_tmain(Main)mainCRTSartup
    处理Unicode字符(串)的CUI应用程序_tmain(wMain)wmainCRTSartup
    动态链接库(Dynamic-Link Library)DllMain_DllMainCRTStartup

    Windows 的动态链接库(Dynamic-Link Library)

    知识点来自《Windows核心编程(第五版)》

    用处

    • 扩展了应用程序的特性

    • 简化了项目管理

    • 有助于节省内存

    • 促进了资源的共享

    • 促进了本地化

    • 有助于解决平台间的差异

    • 可以用于特殊目的

    注意

    • 创建 DLL,事实上是在创建可供一个可执行模块调用的函数

    • 当一个模块提供一个内存分配函数(malloc、new)的时候,它必须同时提供另一个内存释放函数(free、delete)

    • 在使用 C 和 C++ 混编的时候,要使用 extern "C" 修饰符

    • 一个 DLL 可以导出函数、变量(避免导出)、C++ 类(导出导入需要同编译器,否则避免导出)

    • DLL 模块:cpp 文件中的 __declspec(dllexport) 写在 include 头文件之前

    • 调用 DLL 的可执行模块:cpp 文件的 __declspec(dllimport) 之前不应该定义 MYLIBAPI

    加载 Windows 程序的搜索顺序

    1. 包含可执行文件的目录

    2. Windows 的系统目录,可以通过 GetSystemDirectory 得到

    3. 16 位的系统目录,即 Windows 目录中的 System 子目录

    4. Windows 目录,可以通过 GetWindowsDirectory 得到

    5. 进程的当前目录

    6. PATH 环境变量中所列出的目录

    DLL 入口函数

    DllMain 函数

    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
    {
        switch(fdwReason)
        {
        case DLL_PROCESS_ATTACH:
            // 第一次将一个DLL映射到进程地址空间时调用
            // The DLL is being mapped into the process' address space.
            break;
        case DLL_THREAD_ATTACH:
            // 当进程创建一个线程的时候,用于告诉DLL执行与线程相关的初始化(非主线程执行)
            // A thread is bing created.
            break;
        case DLL_THREAD_DETACH:
            // 系统调用 ExitThread 线程退出前,即将终止的线程通过告诉DLL执行与线程相关的清理
            // A thread is exiting cleanly.
            break;
        case DLL_PROCESS_DETACH:
            // 将一个DLL从进程的地址空间时调用
            // The DLL is being unmapped from the process' address space.
            break;
        }
        return (TRUE); // Used only for DLL_PROCESS_ATTACH
    }
    

    载入卸载库

    FreeLibraryAndExitThread 函数声明

    // 载入库
    HMODULE WINAPI LoadLibrary(
      _In_ LPCTSTR lpFileName
    );
    HMODULE LoadLibraryExA(
      LPCSTR lpLibFileName,
      HANDLE hFile,
      DWORD  dwFlags
    );
    // 若要在通用 Windows 平台(UWP)应用中加载 Win32 DLL,需要调用 LoadPackagedLibrary,而不是 LoadLibrary 或 LoadLibraryEx
    HMODULE LoadPackagedLibrary(
      LPCWSTR lpwLibFileName,
      DWORD   Reserved
    );
    
    // 卸载库
    BOOL WINAPI FreeLibrary(
      _In_ HMODULE hModule
    );
    // 卸载库和退出线程
    VOID WINAPI FreeLibraryAndExitThread(
      _In_ HMODULE hModule,
      _In_ DWORD   dwExitCode
    );
    

    显示地链接到导出符号

    GetProcAddress 函数声明

    FARPROC GetProcAddress(
      HMODULE hInstDll,
      PCSTR pszSymbolName  // 只能接受 ANSI 字符串,不能是 Unicode
    );
    

    DumpBin.exe 查看 DLL 信息

    VS 的开发人员命令提示符 使用 DumpBin.exe 可查看 DLL 库的导出段(导出的变量、函数、类名的符号)、相对虚拟地址(RVA,relative virtual address)。如:

    DUMPBIN -exports D:\\mydll.dll
    

    DLL 头文件

    // MyLib.h
    
    #ifdef MYLIBAPI
    
    // MYLIBAPI 应该在全部 DLL 源文件的 include "Mylib.h" 之前被定义
    // 全部函数/变量正在被导出
    
    #else
    
    // 这个头文件被一个exe源代码模块包含,意味着全部函数/变量被导入
    #define MYLIBAPI extern "C" __declspec(dllimport)
    
    #endif
    
    // 这里定义任何的数据结构和符号
    
    // 定义导出的变量(避免导出变量)
    MYLIBAPI int g_nResult;
    
    // 定义导出函数原型
    MYLIBAPI int Add(int nLeft, int nRight);
    

    DLL 源文件

    // MyLibFile1.cpp
    
    // 包含标准Windows和C运行时头文件
    #include <windows.h>
    
    // DLL源码文件导出的函数和变量
    #define MYLIBAPI extern "C" __declspec(dllexport)
    
    // 包含导出的数据结构、符号、函数、变量
    #include "MyLib.h"
    
    // 将此DLL源代码文件的代码放在此处
    int g_nResult;
    
    int Add(int nLeft, int nRight)
    {
        g_nResult = nLeft + nRight;
        return g_nResult;
    }
    

    DLL 库的使用(运行时动态链接 DLL)

    DLL 库的使用(运行时动态链接 DLL)

    // A simple program that uses LoadLibrary and 
    // GetProcAddress to access myPuts from Myputs.dll. 
    
    #include <windows.h> 
    #include <stdio.h> 
    
    typedef int (__cdecl *MYPROC)(LPWSTR); 
    
    int main( void ) 
    { 
        HINSTANCE hinstLib; 
        MYPROC ProcAdd; 
        BOOL fFreeResult, fRunTimeLinkSuccess = FALSE; 
    
        // Get a handle to the DLL module.
    
        hinstLib = LoadLibrary(TEXT("MyPuts.dll")); 
    
        // If the handle is valid, try to get the function address.
    
        if (hinstLib != NULL) 
        { 
            ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts"); 
    
            // If the function address is valid, call the function.
    
            if (NULL != ProcAdd) 
            {
                fRunTimeLinkSuccess = TRUE;
                (ProcAdd) (L"Message sent to the DLL function\\n"); 
            }
            // Free the DLL module.
    
            fFreeResult = FreeLibrary(hinstLib); 
        } 
    
        // If unable to call the DLL function, use an alternative.
        if (! fRunTimeLinkSuccess) 
            printf("Message printed from executable\\n"); 
    
        return 0;
    }
    

    运行库(Runtime Library)

    典型程序运行步骤

    1. 操作系统创建进程,把控制权交给程序的入口(往往是运行库中的某个入口函数)

    2. 入口函数对运行库和程序运行环境进行初始化(包括堆、I/O、线程、全局变量构造等等)。

    3. 入口函数初始化后,调用 main 函数,正式开始执行程序主体部分。

    4. main 函数执行完毕后,返回到入口函数进行清理工作(包括全局变量析构、堆销毁、关闭I/O等),然后进行系统调用结束进程。

    一个程序的 I/O 指代程序与外界的交互,包括文件、管程、网络、命令行、信号等。更广义地讲,I/O 指代操作系统理解为 “文件” 的事物。

    glibc 入口

    _start -> __libc_start_main -> exit -> _exit

    其中 main(argc, argv, __environ) 函数在 __libc_start_main 里执行。

    MSVC CRT 入口

    int mainCRTStartup(void)

    执行如下操作:

    1. 初始化和 OS 版本有关的全局变量。

    2. 初始化堆。

    3. 初始化 I/O。

    4. 获取命令行参数和环境变量。

    5. 初始化 C 库的一些数据。

    6. 调用 main 并记录返回值。

    7. 检查错误并将 main 的返回值返回。

    C 语言运行库(CRT)

    大致包含如下功能:

    • 启动与退出:包括入口函数及入口函数所依赖的其他函数等。

    • 标准函数:有 C 语言标准规定的C语言标准库所拥有的函数实现。

    • I/O:I/O 功能的封装和实现。

    • 堆:堆的封装和实现。

    • 语言实现:语言中一些特殊功能的实现。

    • 调试:实现调试功能的代码。

    C语言标准库(ANSI C)

    包含:

    • 标准输入输出(stdio.h)

    • 文件操作(stdio.h)

    • 字符操作(ctype.h)

    • 字符串操作(string.h)

    • 数学函数(math.h)

    • 资源管理(stdlib.h)

    • 格式转换(stdlib.h)

    • 时间/日期(time.h)

    • 断言(assert.h)

    • 各种类型上的常数(limits.h & float.h)

    • 变长参数(stdarg.h)

    • 非局部跳转(setjmp.h)

    海量数据处理

    • 海量数据处理面试题集锦

    • 十道海量数据处理面试题与十个方法大总结

    音视频

    • 最全实时音视频开发要用到的开源工程汇总

    • 18个实时音视频开发中会用到开源项目

    其他

    • Bjarne Stroustrup 的常见问题

    • Bjarne Stroustrup 的 C++ 风格和技巧常见问题

    书籍

    语言

    • 《C++ Primer》

    • 《Effective C++》

    • 《More Effective C++》

    • 《深度探索 C++ 对象模型》

    • 《深入理解 C++11》

    • 《STL 源码剖析》

    算法

    • 《剑指 Offer》

    • 《编程珠玑》

    • 《程序员面试宝典》

    系统

    • 《深入理解计算机系统》

    • 《Windows 核心编程》

    • 《Unix 环境高级编程》

    网络

    • 《Unix 网络编程》

    • 《TCP/IP 详解》

    其他

    • 《程序员的自我修养》

    往期推荐

    ☞ 趣味设计模式

    ☞ 音视频开发

    ☞ C++ 进阶

    ☞ 超硬核 Qt

    ☞ 玩转 Linux

    ☞ GitHub 开源推荐

    ☞ 程序人生

    关注公众号「高效程序员」????,一起优秀!

    回复“1024”,送你一份程序员大礼包。

    以上是关于干货总结!Kafka 面试大全(万字长文,37 张图,28 个知识点)的主要内容,如果未能解决你的问题,请参考以下文章

    (建议收藏)万字长文,帮你一招搞定产品经理面试-详解产品经理面试大全

    重磅干货 | 五万字长文总结 C/C++ 知识(下)

    万字长文,整理到吐血!Linux最全命令总结

    重磅干货 | 五万字长文总结 C/C++ 知识(上)

    重磅干货 | 五万字长文总结 C/C++ 知识(上)

    五万字长文:Java面试知识点总结

    (c)2006-2024 SYSTEM All Rights Reserved IT常识