网卡简介
网卡是一块通信硬件。属于数据链路层。用户可以通过电缆或无线相互连接。每一个网卡都有一个独一无二的MAC地址(48位),它被写在卡上的一块ROM中。IEEE负责为网卡销售商分配唯一的MAC地址。
可以在终端运行sudo lshw -C network
来查看网卡型号
可以在/lib/modules/$(uname -r)/kernel/drivers/net
路径下找到网卡驱动
网卡以前是作为扩展卡插到计算机总线上的,但是由于其价格低廉而且以太网标准普遍存在,大部分新的计算机都在主板上集成了网络接口。除非需要多接口,否则不再需要一块独立的网卡。甚至更新的主板可能含有内置的双网络(以太网)接口。
网卡的四种传输技术(和其他IO设备差不多)
- 轮询,即微处理器在程序控制下检查外设的状态
- 过程化I/O,即微处理器通过将地址送到系统地址总线上来通知制定的周边设备
- 中断驱动I/O,即当外设准备好传送数据时通知微处理器
- DMA,需要网卡上拥有一个独立的处理器
网卡接收数据
简单的内核源码分析
在电脑启动,加载内核时,网卡驱动程序就会申请一块共享内存作为缓冲区,称为Ring Buffer
- 由驱动程序使用内核的fifo数据结构创建的环形缓冲区,其空间通过内核分配,因此位于内核的数据段中
- Ring Buffer 不直接存储 Packet。初始状态下,Ring Buffer 队列每个槽中存放的 Packet Descriptor 指向 sk_buff ,状态均为 ready
当网卡接收到数据时,网卡做了两件事
- 通过DMA将数据保存到 sk_buff 中
- 通过硬中断通知内核,可以来处理数据了
CPU收到中断后,也做了两件事
- 系统切换为内核态,处理 Interrupt Handler,从RingBuffer 拿出一个 Packet, 并处理协议栈,填充 Socket 并交给用户进程
- CPU 切换为用户态,用户进程处理数据内容
一个简单的示意图
更为详细的说明,可以参考这位大佬的文章
性能瓶颈
一、用户进程阻塞
网卡接收到数据的延迟,取决于传输时延+传播时延,一般都是毫秒级别的,但应用程序处理数据的速度是纳秒级别的。
也就是说,CPU在内核态可能要花上500ms等待并移交数据,在用户态可能只要0.5ms就能处理完这些数据,在499.5ms这段时间内,用户进程无事可做,只能进入阻塞态。
二、频繁处理中断
如果频繁的收到数据包,NIC 可能频繁发出中断请求,这时CPU必须切换到内核态。在极端情况下,如果内核处理前一个协议栈,填充 Socket 还没结束,就要去处理下一个中断,会导致用户进程的饥饿。(即便是多核CPU也会面临这种情况)
处理瓶颈
一、解决频繁中断
解决思路:
最开始,内核每处理好一个数据,就向上层应用提交,CPU就要从内核切换到用户,如果处理10个数据,就要上下文切换20次。
但如果内核一次性处理完这10个数据,再一并提交,只需2次切换,节省开销。
在 NIC 上,解决频繁 IRQ 的技术叫做 New API(NAPI)
- napi_schedule(),专门快速响应 IRQ,只记录必要信息,并在合适的时机发出软中断 softirq
- netrxaction(),在另一个进程中执行,专门响应 napi_schedule 发出的软中断,批量的处理 RingBuffer 中的数据
二、解决用户进程频繁切换
这一条优化一般用于服务器端,需要连接多个信道,如果每个连接都由一个进程/线程管理,那么切换进程/线程的开销就会比较大。并且进程/线程可能会被阻塞,对其唤醒也需要开销。
解决思路:
- 只设置一个进程/线程统一管理全部连接
- 改为非阻塞
即创建一个监视进程/线程,内部采用IO多路复用管理所有连接
(1)select
每次 socket 状态变化,监视进程必须遍历一遍 socket 数组O(n),才知道哪些 socket 就绪了。
(2)epoll
底层采用红黑树 + 双向链表,可以实现O(1)查询