JasonWang's Blog

从NAPI说一说Linux内核数据的接收流程

NAPI(New API)是Linux内核针对网络数据传输做出的一个优化措施,其目的是在高负载的大数据传输时,网络驱动收到硬件中断后,通过poll(轮询)方式将传输过来的数据包统一处理, 在poll时通过禁止网络设备中断以减少硬件中断数量(Interrupt Mitigation),从而实现更高的数据传输速率。

基于NAPI接口, 一般的网络传输(接收)有如下几个步骤:

  • 网络设备驱动加载与初始化(配置IP等)
  • 数据包从网络侧发送到网卡(Network Interface Controller, NIC)
  • 通过DMA(Direct Memory Access),将数据从网卡拷贝到内存的环形缓冲区(ring buffer)
  • 数据从网卡拷贝到内存后, NIC产生硬件中断告知内核有新的数据包达到
  • 内核收到中断后, 调用相应中断处理函数, 此时就会调用NAPI接口__napi_schedule开启poll线程(实际是触发一个软中断NET_RX_SOFTIRQ)(常规数据传输, 一般在处理NIC的中断时调用netif_rx_action处理网卡队列的数据)
  • ksoftirqd(每个CPU上都会启动一个软中断处理线程)收到软中断后被唤醒, 然后执行函数net_rx_action, 这个函数负责调用NAPI的poll接口来获取内存环形缓冲区的数据包
  • 解除网卡ring buffer中的DMA内存映射(unmapped), 数据由CPU负责处理, netif_receive_skb传递回内核协议栈
  • 如果内核支持数据包定向分发(packet steering)或者NIC本身支持多个接收队列的话, 从网卡过来的数据会在不同的CPU之间进行均衡, 这样可以获得更高的网络速率
  • 网络协议栈处理数据包,并将其发送到对应应用的socket接收缓冲区
Android是如何实现流量统计的?

使用Android手机时, 我们不仅可以看到当前系统的流量使用情况, 还可以查看每个应用消耗了多少流量, 借此我们可以发现有那些流氓APP在偷偷在背后消耗流量.那么, Android是具体如何实现流量统计的? 又是如何对每个应用的流量使用进行监控? 这篇文章我们就来看看Android流量统计的具体实现原理.

大致说来, Android从如何几个方面进行流量统计:

  • 统计每个网口当前发送/接收的流量数据
  • 监控每个应用(对应唯一的UID)所消耗的流量
  • 支持对总的流量配额进行限制, 如达到一定的流量阈值后, 会对网络进行限制

而具体到每个应用(比如system应用, UID=1000), Android还支持对应用内的每个socket进行标记(tag), 用于区分每个应用(UID)内部具体使用了那些流量.后面, 我们会讲到如何通过标签来区分UID内部的流量.

利用SSH隧道访问局域网

SSH(Secure SHell)是一种基于加密算法的网络安全协议, 其在TCP/IP协议的基础上通过非对称公钥算法对用户身份进行验证. SSH在网络中有广泛的应用, 比如平常在远程登录时就会用到SSH, Github的代码仓库提交也会基于SSH协议来验证提交者的合法性, 而对常年生活在局域网内的人来说, SSH更多的用途则是搭建穿越防火墙的VPN实现网络自由.

一台服务器如果有公共域名或者IP地址, 只需要事先将客户端的公钥放到服务器上就可以正常登录, 但如果服务器本身位于防火墙之外(比如某个端口被禁)或者位于NAT(Network Address Translation)网关之后, 这个方法就不起作用了. 用过VPN的同志应该比较清楚, 穿透防火墙或者某个局域网的NAT网关, 一般要用到SSH隧道技术(SSH tunneling);SSH隧道技术也被称为端口转发(port forwarding).简单来说, 建立SSH隧道大致有两个流程:

Recovery模式如何支持ADB

这两天顺着recovery模式下一个网络需求, 为了便于调试又在recovery下做了ADB功能. 在Android早期的如4.4版本, recovery模式下支持ADB配置起来比较简单(支持adb devices/adb reboot/adb pull/push等常用指令), 但在Android 9.0下USB辅助设备一般都通过configfs的方式来配置了, 因此相对来说要适配的东西就多一些, 如果额外要适配adb shell命令, 则要修改adbd的源代码了.这篇文章就来看看如何在Recovery模式下解决这几个问题.

在进入正题之前, 先了解下USB相关的基础知识.

USB全称是Universal Serial Bus, 是一种广泛用于主机与外设之间的连接的串行总线.USB设备使用的是一种层级的结构, 最多可支持多达127个设备, 每个USB设备对应一个功能(function), 比如USB打印机提供了打印服务; 存储设备则提供了存储数据的功能.

USB system architecture

SELinux在Android中的应用

SELinux(Security Enhanced Linux)是Linux下的安全控制机制, 为进程访问系统资源提供了访问控制(access control)策略. 早期, Linux基于用户身份/用户组的DAC(Discretionary Access Control作为访问控制策略: 每个进程都有所属的UID, 每个文件都有所属的UID/GID以及文件模式(读写执行等), 一个进程是否可以访问某个文件就是基于UID/GID/文件模式来管理的.换句话说,只要某个资源序属于该用于或该用户组, 则该用户对该资源具有绝对控制权力, 这样一旦用户获得了root权限, 那么整个系统就成了肉鸡. 可见, DAC的安全控制策略比较粗放.

SELinux最初是由美国Utah大学与NSA(National Security Agency)的安全小组研究出来的安全框架FLASK演变而来, 后被合入到Linux 2.6版本.相较于DAC, SELinux采用的是更细粒度的MAC(Mandatory Access Control).对于DAC而言, 资源的权限是由每个用户自己控制的, 而MAC则将所有的权限收拢, 由一个统一的管理者(SELinux)统一来分配所有的资源权限, 如果访问者没有事先分配到某个资源的权限, 则不会允许访问.这样即使是root用户也要收到安全策略的约束. Android在4.3开始引入SELinux, 到了5.0版本之后, 则开始全面支持了.

BPF与eBPF

最近了解Linux的性能优化时, 接触到了BPF(Berkeley Packet Filter)。很有意思也很强大的功能;想把学到的一些基本原理与知识记录下来, 算是一个初步的总结. 这篇文章主要从如下几个方面介绍下BPF:

  • BPF的原理
  • 什么是eBPF
  • 如何在Linux中使用BPF

用过tcpdump的同学应该都了解pcap, 实际上pcap就是基于BPF来实现网络数据包的过滤的. tcpdump的原理如下图所示: tcpdump将包过滤的表达式, 如查看某个网口所有udp包, 输入tcpdump -n -i eth0 udp, 这个表达式通过PCAP库编译成伪机器字节码后, 通过系统调用发送给内核(内核中有对应的机器码解释器)解释执行, 这样只要系统有udp包, 内核都会过滤出来转发给用户进程tcpdump:

how tcpdump works

说说Process.waitfor()引起的进程阻塞问题

最近碰到一个看似很怪异的问题, 在两个APP上调用同样的本地指令得到的结果却大相径庭; 看源代码, 这个本地进程做的事情其实并不复杂:

  • 从一个串口/dev/ttyUSBX读取数据
  • 将数据写入到本地目录(读缓存大小为1KB)

本地进程的代码逻辑其实相当简单: 主线程起来后主动创建一个负责读/写的子线程, 然后通过pthread_join主动等待子线程完成后退出.

问题是, 应用A调用的时保存的日志大小雷打不动的停留在不到4M就停止了, 而应用B可以一直写数据. 看应用A调用时, 通过debuggerd -b <tid> 查看本地进程的堆栈, 大概是这样的:

聊一聊TCP协议

TCP(Transmission Control Protocol)即传输控制协议, 位于TCP/IP协议栈的第三层传输层, 与UDP不同的是, TCP号称提供有链接的(connection-oriented), 可靠的(reliable)字节流服务, 很多其他应用层协议如HTTP/SMTP/MQTT都是基于TCP协议实现.

这篇文章我们就从定义的角度来看一看TCP协议的具体工作原理. 首先看下有链接的(connection-oriented)具体含义.

TCP在发送数据之前, 第一件事情就是要在通信的双方建立一个通信的链路, 这个有点像日常生活中的打电话: A向B发起通话请求, B确认后双方建立通信链接才能正式通话. TCP也一样, 在发送任何数据之前必须要建立链接(connection), 这个建立通信链接的过程就是我们常说的”三次握手”;同样, 如果要想结束通信, 也需要有一个挥手的过程(四次挥手).有关TCP链接的建立与关闭可以参考之前的一篇文章(TCP的链接建立与状态迁移). 那么, TCP建立链接主要完成哪几件事情了?

TCP链接建立与状态迁移

学习TCP协议的第一步是要了解熟悉TCP的三次握手/四次挥手以及状态迁移图. 这篇文章用三个图展示TCP链接的建立与关闭以及状态的迁移.

TCP状态迁移

根据TCP协议的文档RFC793, 一个TCP链接有下图中的几个状态(图中实粗线为Client端的正常情况下状态迁移图, 虚线为Server端正常情况下的状态迁移图):

  • CLOSED: TCP链接的初始状态, 表示没有任何链接
  • LISTEN: (服务端)等待来自远程客户端的请求
  • SYN_SENT: 发送了一个建立TCP链接的SYN请求, 等待对端返回结果
  • SYN_RCVD: 收到了TCP建立链接的SYN包, 等待对方的回应(ACK)
  • ESTABLISHED: TCP链接建立成功, 从这里开始可以交换数据包了
  • FIN_WAIT1: 应用进程关闭了TCP链接(发送FIN包), 并等待对端的响应
  • FIN_WAIT2: 接收到关闭回应后, 等待对端结束TCP链接(等待FIN包)
  • CLOSING: 如果两端同时接收到了FIN包, 则进入该状态
  • CLOSE_WAIT:处于被动关闭一端接受到FIN请求后, 等待本地进程的关闭TCP链接
  • LAST_ACK: 服务端本地进程关闭TCP链接后, 发送FIN包, 等待回应
  • TIME_WAIT: 等待2*MSL(Maximum Segment Lifetime, TCP包的最大存活时间)后关闭该TCP链接, 等待足够长的时间是为了确保最后关闭链接的ACK包有足够长的时间达到对端, 如果对端未能收到该包, 则会重传FIN包, 这样对端也可以重传ACK包, 一般MSL为60s。有关更多TIME_WAIT状态的解释可以参考TIME-WAIT State.

TCP state transition