JasonWang's Blog

JasonWang's Blog

由Policy Routing引发的一个奇怪问题

最近新的项目又开始了, 开始还算顺利, 却不料碰到了一个奇怪的问题. 先来了解下问题的背景. 这个项目里, Android中有两个以太网网口, 一个用于内网通讯, 不具备上外网的能力;一个用于外网通讯, 使用该网口可以访问互联网. 在网络管理模块的工作完成后, 提交了代码我原本以为可以高枕无忧, 前两天组内的同学跑过来告诉我, 他有个系统服务一直没法通过与内网的其他设备上的服务建立TCP链接, 但是网络却一直可以ping通; 而另外的一个开发板上却不存在这个问题.

开始我有点不相信竟然会有这样的问题, 但事实摆在面前, 我也不好抵赖, 于是自己找来一个板子, 看了下, 才逐渐找到答案. 问题的根源在于Android配置的策略路由规则隐含了一个针对系统默认网络的fwmark规则, 要解决问题, 只要我们将包含了内网路由表的路由规则的优先级提升到高于Android隐含的这条规则即可. 虽然找到了解决方案, 但是还是决定花点时间把整个事情的来龙去脉都理清楚.

大致分如下几个部分来讲一讲这个问题:

  • 介绍下什么是Policy Routing<策略路由>
  • 分析具体的问题, 并给出方案
  • 从源代码角度来分析下, 为何TCP无法建立, 但ping却可以
软件开发与BUG的那些事儿

Excellence in any department can be attained only by the labor of a lifetime;it is not to be purchased at a lesser price

Paul Graham

前段时间, 看新闻说“微软4万多的软件开发工程师, 每天产生进3万个BUG”, 当时感觉有点震惊, 然后就哑然失笑. 震惊的是连微软这样厉害的公司, 工程师应该都很优秀, 人才济济, 为何却会每天产生这么多的BUG了? 于是,再想想自己的开发经历, 才恍然明白: 开发人员一旦走进办公室,打开电脑写代码, 就不可避免的要写出BUG来. 与BUG纠缠不清似乎是每个开发人员的宿命.恰逢最近遇到了一个BUG, 让我纠结不已, 痛定思痛, 觉得有必要把自己的开发”心得经验”写下来, 权当是给自己一点警醒, 给自己一点回顾的资料, 分享下自己在开发过程中遇到的困难与挫折, 苦恼与迷惑.

从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> 查看本地进程的堆栈, 大概是这样的:

avatar
JasonWang
生命短暂,莫空手而归