Linux内核的是如何实现环形缓冲区机制的?
Posted 一口Linux
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核的是如何实现环形缓冲区机制的?相关的知识,希望对你有一定的参考价值。
Why is printk() so complicated?
https://linuxreviews.org/Why_is_printk()_so_complicated%3F
特点
- 它将允许在没有死锁风险的任何情况下存储和读取消息
它是一个多生产者,单个或多个消费者组成的循环缓冲区。
struct kfifo {
unsigned char *buffer;
unsigned int size;
unsigned int in;
unsigned int out;
spinlock_t *lock;
};
其中buffer指向存放数据的缓冲区,
size是缓冲区的大小,
in是写指针下标,
out是读指针下标,
lock是加到struct kfifo上的自旋锁(上面说不加锁不是这里的锁),
防止多个进程并发访问此数据结构。
当in==out时,说明缓冲区为空;
当(in-out)==size时,说明缓冲区已满。
- 循环缓冲区日志记录由一个固定大小的内存缓冲区构成,进程使用这个内存缓冲区进行日志记录。
当然现在我们面对的大多是多线程的协同工作,
对于日志记录来说,倘若采取传统的加锁机制访问我们的存储文件,这些线程将在获得和释放锁上花费了大部分的时间,
所以采取循环缓冲区会是一个不错的办法。
通过使得每个线程将数据写入到它自己的内存块,就可以完全避免同步问题 【不懂这个】
- 生产者不能被缓慢的消费者所阻挡。取而代之的是,生产者只是简单地按照他们想要的速度写作,而消费者则尽其所能跟上
在通常情况下,环形缓冲区的读用户仅仅会影响读指针,而写用户仅仅会影响写指针。
如果仅仅有一个读用户和一个写用户,那么不需要添加互斥保护机制就可以保证数据的正确性。
如果有多个读写用户访问环形缓冲区,那么必须添加互斥保护机制来确保多个用户互斥访问环形缓冲区。
# 资料 https://www.cnblogs.com/dodng/p/4367791.html
.为多线程数据通信提供了一种高效的机制。
在最典型的生产者消费者模型中,如果引入环形队列,那么生成者只需要生成“东西”然后放到环形队列中即可,而消费者只需要从环形队列里取“东西”并且消费即可,没有任何锁或者等待,巧妙的高效实现了多线程数据通信。
.环形队列的工作场景
一般应用于需要高效且频繁进行多线程通信传递数据的场景,例如:linux捕包、发包等等,(linux系统中对PACKET_RX_RING和PACKET_TX_RING的支持实质就是内核实现的一种环形队列)
https://hedzr.com/algorithm/golang/ringbuf-01-intro/
https://segmentfault.com/a/1190000016329997
环形队列是一种 FIFO 数据结构。
它和普通 FIFO 队列数据结构的不同就在于队尾连接着队首,
当入列元素位于队尾时,该元素将被回绕并放在队首的位置,
从而完成有界性限定。而普通的 FIFO 队列总是增长队尾以入列新元素,
并不限制队列的有效长度。
环形队列具有先天优势,无需数据搬移
https://github.com/smallnest/ringbuffer
## Linux 网络协议栈收消息过程-Ring Buffer
https://ylgrgyq.github.io/2017/07/23/linux-receive-packet-1/
Linux kernel里面从来就不缺少简洁,优雅和高效的代码
. 2的次幂
- 判断一个数是不是2的次幂
kfifo要保证其缓存空间的大小为2的次幂,如果不是则向上取整为2的次幂。其对于2的次幂的判断方式也是很巧妙的。如果一个整数n是2的次幂,则二进制模式必然是1000...,而n-1的二进制模式则是0111...,也就是说n和n-1的每个二进制位都不相同,例如:8(1000)和7(0111);n不是2的次幂,则n和n-1的二进制必然有相同的位都为1的情况,例如:7(0111)和6(0110)。这样就可以根据 n & (n-1)的结果来判断整数n是不是2的次幂,实现如下
Linux内核中kfifo
实现技巧,主要集中在放入数据的put
方法和取数据的get方法。代码如下:
为什么kfifo实现的单生产/单消费模式的共享队列是不需要加锁同步的呢?
https://blog.csdn.net/chen19870707/article/details/39899743
https://blog.csdn.net/chen19870707/article/details/39899743
# 资料
https://unix.stackexchange.com/questions/198178/what-are-the-concepts-of-kernel-ring-buffer-user-level-log-level
https://zhuanlan.zhihu.com/p/118158992
https://www.jianshu.com/p/e6162bc984c8
想看能不能完整梳理一下收消息过程。从 NIC 收数据开始,到触发软中断,交付数据包到 IP 层再经由路由机制到 TCP 层,最终交付用户进程
课堂练习如果你对 TCP 协议的结构不太熟悉,可以使用 tcpdump 命令截取一个 TCP 的包,看看里面的结构。
UDP 是面向数据报的,一个一个地发送。
TCP 是可以提供流量控制和拥塞控制的,既防止对端被压垮,也防止网络被压垮
在内核中,为每个 Socket 维护两个队列。
一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于 established 状态;
一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态。
struct sk_buff 是存储网络包的重要的数据结构,在应用层数据包叫 data,
在 TCP 层我们称为 segment,
在 IP 层我们叫 packet,
在数据链路层称为 frame。
在 struct sk_buff,首先是一个链表,将 struct sk_buff 结构串起来。
网卡作为一个硬件,接收到网络包,应该怎么通知操作系统,这个网络包到达了呢?
没错,我们可以触发一个中断
如果一个网络包到来,触发了硬件中断,就会调用 ixgb_intr,这里面会调用 __napi_schedule。
我们知道,软中断 NET_RX_SOFTIRQ 对应的中断处理函数是 net_rx_action。
这两个工作都在 poll 中完成,上面说过 poll 是个 driver 实现的函数,所以每个 driver 实现可能都不相同。但 poll 的工作基本是一致的就是:
- 从 Ring Buffer 中将收到的 sk_buff 读取出来
- 对 sk_buff 做一些基本检查,可能会涉及到将几个 sk_buff 合并因为可能同一个 Frame 被分散放在多个 sk_buff 中
- 将 sk_buff 交付上层网络栈处理
- 清理 sk_buff,清理 Ring Buffer 上的 Descriptor 将其指向新分配的 sk_buff 并将状态设置为 ready
- 更新一些统计数据,比如收到了多少 packet,一共多少字节等
以上是关于Linux内核的是如何实现环形缓冲区机制的?的主要内容,如果未能解决你的问题,请参考以下文章