什么是SocketPair呢
socketpair是一个Linux系统调用,用户创建一对无名、相互连接的本地套接字int socketpair(int domain, int type, int protocol, int sv[2]);- 就像你买一对对讲机。当你调用这个函数的时候,内核直接给你两个文件描述符(FD), 你往sv[0]里面写数据,sv[1]就能读到;反之亦然
那SocketPair是可以支持两端同时读写的嘛
- 是的,
socketpair是完全支持两端同时读写的。他是全双工通信 - 核心原理
- 当你调用
socketpair得到两个文件描述符fd[0] 和 fd[1]时,内核并不是只开辟了一块内存。实际上,内核在底层为这对Socket维护了两块独立的缓存区(Buffer) - 缓冲区(A):专门用于fd[0]发送数据,fd[1]接受数据
- 缓冲区(B):专门用于fd[1]发送数据,fd[0]接受数据
- 当你调用
- 为什么可以同时呢
- 因为这两条路径在内核中是物理隔离(逻辑独立)的
- 当系统进程(SystemServer)在往fd[0]写点击事件时,它占用的是缓冲区A
- 与此同时,App进程往往可以往
fd[1]写"处理完成"的反馈信息,它占用的是缓存区B - 互不干扰 两个进程在多核CPU上甚至可以真正意义上的物理同时操作
- 为什么必须要
socketpair- 异步分发 高吞吐
InputDispatcher(系统段)不需要等待App回复,就可以连续不断地发送多个触摸事件,它只管往fd[0]里塞数据
- 及时反馈
- App处理完一个事件之后,立刻往
fd[1]写回一个Finished信号 - 如果
socketpair像管道(Pipe)那样是单向的,或者像半双工(Half-Duplex,如对讲机)那样同一时间只能一个人说话,那么:- 系统发事件时,App 就没法回信号。
- App 回信号时,系统就得停下分发。 这会导致严重的掉帧和延迟。全双工保证了“下发”和“上报”可以并线执行。
- App处理完一个事件之后,立刻往
- 异步分发 高吞吐
SocketPair的核心工作原理
全双工通信
- 与传统的"管道"不同(管道通常是单向的,即一个只能写,一个只能读),SocketPair是全双工的
- 每一个FD既可以读也可以写
- 系统通过它来分发事件(Dispatcher -> App),App通过它回传处理完成的信号(App -> Dispatcher)
内核缓冲区映射
- 当你调用
socketpair的时候,Linux内核会在内存中开辟一块缓冲区 - 这两个FD实际指向的是内核中同一个套接字对象的两个端点
- 零网络协议栈:因为是本地通信,他不经过TCP/IP协议栈,没有包头,没有校验和计算等等。本质上就是内核空间的内存拷贝
跨进程的双向奔赴
- 创建:系统进程创建一对FD
- 分发:系统进程保留
fd[0],通过Binder把fd[1]发送给App进程 - 重映射:Binder驱动在传输FD的时候,会把App进程的“文件描述符”里找个空位,把
fd[1]映射进去 - 直连:从此以后,两个进程就像通过一根电线连在一起,直接读写,不再需要Binder的介入
代码模拟
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd[2];
char buf[100];
// 1. 创建对讲机
if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fd) < 0) {
return -1;
}
// 模拟跨进程(这里用 fork)
if (fork() == 0) {
// 子进程 (模拟 App)
close(fd[0]);
read(fd[1], buf, sizeof(buf)); // 等待系统消息
printf("App 收到事件: %s\n", buf);
write(fd[1], "Done", 4); // 回传:我处理完了
} else {
// 父进程 (模拟 SystemServer)
close(fd[1]);
write(fd[0], "Touch Event", 11); // 发送点击事件
read(fd[0], buf, sizeof(buf)); // 等待 App 回复
printf("Dispatcher 收到反馈: %s\n", buf);
}
return 0;
}
Android中的应用
- InputChannel里面封装了ScoketPair
- InputPublisher:运行在
system_server的InputDispatcher线程。负责把NotifyKeyArgs这种原始数据打包称为InputMessage写入Scoket - InputConsumer:运行在App的主线程(由
WindowInputEventReceiver调用)。它负责从Socket中读出信息,转换成MotionEvent给View
- InputPublisher:运行在
Android中由Binder这种IPC机制,为什么InputManager还要引入SocketPair呢
-
其中的核心矛盾就是通用性 和 极致性能的冲突
-
1. 通信模型的差异:RPC vs 数据流
- Binder (寄快递):
Binder 设计初衷是 RPC(远程过程调用)。你调用另一个进程的一个函数,把参数打包(Parcel)发过去,对方处理完可能还给你个返回值。它有极其严格的权限检查、接口定义(AIDL)和对象生命周期管理。
- 开销: 每次 Binder 通信都要经历完整的事务流程(Transaction),包括复杂的内核对象映射、安全校验等。
- SocketPair (自来水管):
SocketPair 是 字节流(Stream) 通信。一旦建立连接,数据就像水一样从一头流向另一头。它没有复杂的协议头,没有繁琐的权限反复校验。
- 开销: 极低。直接操作内核缓冲区,非常适合高频数据。
2. “全双工”与“半双工”的效率对决
- Binder 的局限: 虽然 Binder 支持异步调用(oneway),但在逻辑上它通常是“一问一答”或“单向投递”。如果系统要发事件给 App,App 同时要回传状态,Binder 需要开启两个事务,或者在一次事务中等待返回。
- SocketPair 的优势: 它是天然的全双工。系统在疯狂往 App 塞触摸坐标的同时,App 可以毫不干扰地在同一条“通道”上回传“处理完毕”的信号。这种并行能力是丝滑触控的前提。
3. 实时性与“ANR”监控的需求
输入系统有一个极高的要求:不能丢包,且必须知道 App 是否处理完了。
- Binder 的问题: 如果 App 进程的主线程卡死了,Binder 驱动虽然能把消息投递到目标进程的 Binder 线程池,但逻辑层面的响应会变慢。系统很难精准地通过 Binder 实时监控“这条管道是不是堵了”。
- SocketPair + epoll: 系统(InputDispatcher)将 FD 放入 epoll 监听。如果 Socket 的发送缓冲区满了(写不进去了),或者 App 迟迟不回传“Finish”信号,epoll 能在毫秒级感知到阻塞。这是 Android 系统判定 ANR(应用程序无响应)最直接的物理依据。
4. 传输频率的压力测试
- 数据量对比:
- 普通操作:一秒钟可能有 120 次、240 次甚至 360 次(高采样率手机)的触控数据。
- Binder: 每一帧都要创建一个 Transaction,经历内核的查找、映射、拷贝。在高频场景下,Binder 的上下文切换开销会显著消耗 CPU,导致掉帧。
- SocketPair: 只需要一次 write 系统调用。数据就在内核缓冲区静静地等着 App 的 Looper 把它读走。
- Binder (寄快递):
Binder 设计初衷是 RPC(远程过程调用)。你调用另一个进程的一个函数,把参数打包(Parcel)发过去,对方处理完可能还给你个返回值。它有极其严格的权限检查、接口定义(AIDL)和对象生命周期管理。
总结
-
“为什么有了 Binder 还要 SocketPair?”
-
因为 Binder 负责的是“秩序”,它确保了跨进程调用的安全和规范;而 SocketPair 负责的是“速度”,它追求的是在 120Hz 甚至更高的刷新率下,触摸事件能以最短的延迟从系统内核抵达你的手指。
-
在 Android 输入系统中,它们不是替代关系,而是协作关系:
-
Binder 负责开路: WindowManager 调起 IMS,通过 Binder 把 Socket 的一头传给 App。
-
SocketPair 负责干活: 建立连接后,Binder 功成身退,剩下的高频触控数据全部通过 SocketPair 这条“私密专线”极速传输。
-
如果没有 SocketPair,你的 Android 手机在滑动时,CPU 可能会被 Binder 的事务处理占满,屏幕也会感到明显的迟钝和卡顿。