Android+QC modem手机通信模块技术分析
1 Android手机软件结构图
1.1 整体分层架构
-
Application层(应用层):Dialer(拨号盘)、SMS(短信应用)、MMS(彩信应用)、Browser(浏览器)等APP层应用
-
Framework层(应用程序框架层):Telephony Framework,提供TelephonyManager、SmsManager、SubscriptionManager等API
-
HAL层(硬件抽象层):RILD(Radio Interface Layer Daemon),负责与modem硬件通信的守护进程
-
Linux Kernel层(内核驱动层):Modem驱动程序、USB/HSIC/共享内存驱动等,负责底层数据传输

1.2 高通平台AP+BP双处理器架构
-
AP(Application Processor,应用处理器):运行Android系统,采用ARM11/Cortex-A系列处理器
-
BP(Baseband Processor,基带处理器):运行AMSS(Advanced Mobile Subscriber Software),采用ARM9处理器,负责通信协议栈处理、射频管理、GPIO等
-
AP与BP的通信方式:通过共享内存(Shared Memory)进行高速数据交换
1.3 高通Modem软件架构
-
L4微内核:提供基础系统功能(地址空间管理、进程间通信IPC、任务调度)
-
REX(实时扩展操作系统):抢占式多任务RTOS,提供任务创建、同步、互斥、定时器和中断控制
-
AMSS(高级移动用户软件):运行于REX之上,包含3GPP协议栈等无线通信核心功能
2 RIL(Radio Interface Layer)结构图
2.1 RIL整体架构(四层结构)
-
APP层:通话、短信、SIM卡管理等应用程序
-
RILJ(Java层):包含RIL模块(负责与RILD通信)和Phone模块(GSMPhone/CDMAPhone,电话功能接口)
-
RILD(C++层,HAL层):Init进程启动的本地服务,通过Socket与上层通信
-
Vendor RIL(厂商定制层):高通对应实现为QCRIL(Qualcomm Communication RIL)
2.2 RIL核心组件详解
-
RILJ核心类:RILRequest(命令请求)、RIL.RILSender(命令发送线程)、RIL.RILReceiver(响应接收线程)
-
RILD组成:rild(守护进程入口)、libril.so(事件处理库)、libreference-ril.so(AT指令转换库)
-
请求-响应型命令(Solicited Commands):DIAL(拨号)、HANGUP(挂断)、SEND_SMS(发短信)、GET_CURRENT_CALLS等60+种命令
-
主动上报型命令(Unsolicited Responses/Indication):CALL_STATE_CHANGED(通话状态变化)、NEW_SMS(新短信通知)、CALL_RING(来电响铃)等

2.2.1 主要命令类型
Solicited Commands(请求‑响应型命令)
-
发起方:上层(RILJ、Telephony 框架)主动发起请求。
-
执行流程:上层调用 RIL_REQUEST_XXX → Vendor RIL 执行(如发 AT 命令给 Modem)→ 必须调用 RIL_onRequestComplete 返回结果(成功或失败)。
-
例子:RIL_REQUEST_DIAL、RIL_REQUEST_SETUP_DATA_CALL、RIL_REQUEST_SEND_SMS。
Unsolicited Responses(主动上报)
-
发起方:Modem 或 Vendor RIL 主动向上层通知事件,无需上层预先请求。
-
执行流程:Modem 主动上报(如来电、信号变化)→ Vendor RIL 调用 RIL_onUnsolicitedResponse → RILJ 接收并处理。
-
例子:RIL_UNSOL_CALL_RING(来电振铃)、RIL_UNSOL_RESPONSE_NETWORK_STATE_CHANGED、RIL_UNSOL_NEW_SMS。
2.2.2 命令时序图
主叫(MO):Solicited Commands

被叫(MT):Unsolicited Responses

2.2.3 RILD设计思想
RILD(Radio Interface Layer Daemon)的核心是 单线程事件驱动模型,其设计思想源自典型的 Reactor 模式,目标是在一个线程内高效处理多个并发 I/O 事件(与 RILJ 的 socket 通信、与 Modem 的串口/IPC 通信、定时器事件),同时保证状态一致性和避免复杂的线程同步。
C
通信框架经典设计:异步 IO 实现 + 同步业务语义:
RIL 底层通信**是非阻塞异步 IO**:
RILJ ↔ RILD:Socket 非阻塞
RILD 内部:EventLoop 异步事件驱动
QCRIL ↔ Modem:QMI 异步消息
**业务模型是同步请求**:
上层发请求后,会**挂起等待对应响应**
收到响应后,才唤醒并返回结果
超时无响应则直接报失败
//----------------------------------------------------------
Solicited 有请求、有响应(可能同步或异步返回)
可以是同步:Vendor RIL 在 onRequest 函数内直接完成操作(如读取缓存、同步 AT 命令),
在返回前就调用 RIL_onRequestComplete。此时对上层来说像是同步调用。
也可以是异步:Vendor RIL 将 token 保存,启动异步任务(如等待 Modem 响应),
稍后才调用 RIL_onRequestComplete。这属于异步请求。绝大多数耗时操作(拨号、数据建立)都是异步实现,
避免阻塞事件循环。
Unsolicited 无请求、有通知(纯异步事件)
没有同步/异步之分:它只是下层主动通知上层的单向消息,不涉及"请求等待响应"的范式。
硬要说的话,它是异步事件,因为没有任何同步阻塞。
//----------------------------------------------------------
每个 Solicited 请求发出时,RILJ 会分配**唯一 Token**:
上层发:Token=123 + RIL_REQUEST_DIAL
基带回:Token=123 + 响应结果
这种 "一对一等待配对" 的模型,就是典型**同步语义**。
异步上报(Unsolicited)**没有 Token**,不需要配对,谁来就处理谁。
C
AP->BP:RILJ 发请求 → RILD 转发 → QCRIL → Modem
BP->AP:Modem 处理 → QCRIL 回包 → RILD → RILJ 收响应
| 路径 | 同步/异步 | 阻塞/非阻塞 | 原因 | 命令 |
|---|---|---|---|---|
| Solicited Commands | 上层同步,底层异步 | 业务上:同步(一问一答,必须配对) 底层实现:非阻塞(Socket 非阻塞 + 事件循环 + 回调) 线程:不阻塞,靠 Token 匹配结果 上层阻塞,底层非阻塞 | 上层 RILJ 线程 wait() 等待 Modem 回复;底层 rild 通过 epoll 监听 fd,不阻塞在 read() 上。 | 上层 APP/Framework 主动发起的有问有答命令: 拨号 RIL_REQUEST_DIAL 查 IMSI RIL_REQUEST_GET_IMSI 激活 PDN RIL_REQUEST_SETUP_DATA_CALL 发送短信 RIL_REQUEST_SEND_SMS |
| Indication | 异步 | 业务:异步(基带主动推) 实现:非阻塞事件通知 无配对、无等待 上层阻塞,底层非阻塞 | 上层 RILJ 线程 wait() 等待 Modem 回复;底层 rild 通过 epoll 监听 fd,不阻塞在 read() 上。 | 基带主动推送、上层无请求的无问自答消息: 来电 RIL_UNSOL_CALL_STATE_CHANGED 信号变化 RIL_UNSOL_SIGNAL_STRENGTH 网络注册 RIL_UNSOL_NETWORK_REGISTRATION |
2.2.4 RIL三大模块
| 模块文件 | 编译产物 | 主要作用 |
|---|---|---|
| rild.c | rild守护进程 | 唯一的入口点,负责解析参数、加载动态库等顶层初始化工作。 |
| ril.cpp ril_event.cpp | libril.so | LibRIL,核心逻辑层。建立事件循环,管理上层通信,协调下层通信。 |
| reference-ril.c atchannel.c | libreference-ril.so | Vendor RIL,厂家适配层。实现具体的AT命令收发,与Modem硬件交互。 |
整个 RIL 主要分为三个部分,它们之间通过函数指针或动态加载的方式交互。
rild是主程序,通过dlopen方式加载libreference-ril.so;
libril.so是rild编译时就链接的依赖库。
2.2.5 主要线程
| 线程 (Thread) | 初始化时机 | 初始化位置 (函数/库) | 主要职责 |
|---|---|---|---|
| eventLoop | rild 主进程启动初期 | rild.c 的 main() 函数中调用 RIL_startEventLoop() | 事件分发中枢。监听所有可读/可写的文件描述符(如与 RILJ 的 Socket 和内部通知管道),通过 select()/poll() 循环进行非阻塞的事件处理。 |
| mainLoop | Vendor RIL 库初始化时 | Vendor RIL 库的 RIL_Init() 函数内部 | Modem 交互核心。负责建立与 Modem 的通信链路(如打开串口),并创建 readerLoop 线程。 |
| readerLoop | mainLoop 初始化过程中 | Vendor RIL 库的 at_open() 函数内部 | Modem 响应监听器。持续、阻塞地读取 Modem 发来的所有消息,并将它们传递给上层处理。 |

2.2.5.1 eventLoop 线程初始化
在 rild.c 的 main() 函数中,会调用 RIL_startEventLoop()。该函数实现在 ril.cpp 中,内部通过 pthread_create 创建了一个入口为 eventLoop() 的新线程,这个线程就是 eventLoop。eventLoop() 最终会进入 ril_event_loop(),这是一个基于 select/poll 的无限循环,负责监听和分发所有 I/O 事件。
2.2.5.2 mainLoop 线程初始化
接下来,main() 函数会动态加载厂商的 RIL 库,并调用其 RIL_Init 函数。在 RIL_Init 内部,会创建一个 mainLoop 线程。例如在 reference-ril.c 中,其入口函数是 mainLoop()。
2.2.5.3 readerLoop 线程初始化
mainLoop 线程的首要任务是打开与 Modem 的物理通信通道(如 /dev/ttyUSB0),然后调用 at_open() 函数来建立基于 AT 命令的通信机制。在 at_open() 内部,会再创建一个新的线程 readerLoop,该线程的入口函数为 readerLoop()。readerLoop 会进入一个循环,持续阻塞读取来自 Modem 的消息,并使用特定的回调函数(如 onUnsolicited)进行处理。对于同步命令的最终响应,则通过 processLine() 等函数匹配到相应的请求。
2.2.5.4 RIL_register 注册
在 RIL_Init 返回函数表后,main() 函数会调用 RIL_register(),将这个函数表注册到 libril.so,并建立与上层 RILJ 通信的 Socket。这些 Socket 的文件描述符随后会被加入到 eventLoop 线程的监听集合中,从而完成整个 rild 初始化流程。
C
//rild 各线程初始化过程:
main 线程创建 eventLoop 用于上层通信,
通过厂商的 RIL_Init 创建 mainLoop 用于建立 Modem 连接,
mainLoop 再调用 at_open 创建阻塞式读取线程 readerLoop来专门处理 Modem 的各类消息。
2.2.5.5 worker 线程
worker 线程并非 RILD 框架的强制组件,而是 Vendor RIL 实现中常见的优化/并发手段。
-
主要用于 AP → BP 方向:当上层下发请求(如 RIL_REQUEST_DIAL)时,eventLoop 可以将该请求交给 Worker 线程异步处理,避免阻塞 eventLoop。
-
BP → AP 方向(主动上报)不使用 Worker 线程,因为主动上报由 readerLoop 线程读取并直接放入主动上报队列,然后唤醒 eventLoop 发送。Worker 线程不参与上行主动上报的处理(除非厂商实现特别复杂,但典型设计中不涉及)。
2.2.5.1 为什么需要 worker 线程?
-
避免阻塞 eventLoop:如果 onRequest 中直接发送 AT 命令并同步等待响应(几秒),会阻塞整个 eventLoop,导致无法处理其他请求或主动上报。
-
管理复杂命令队列:Modem 可能同时接收多个命令,worker 线程可以负责排队、优先级、重试等。
-
解耦发送与接收:有些 Vendor RIL 将命令发送和响应解析放在不同线程中,提高效率。
2.2.5.2 worker 线程的工作流程

2.2.5.3 worker 线程与 readerLoop 的关系
-
独立实现:有些 Vendor RIL 将 发送 放在 worker 线程,接收 单独由 readerLoop 处理(阻塞读取)。两者通过共享队列或 notify pipe 协调。
-
合并实现:少数简单实现中,mainLoop 既负责发送也负责读取,但这样容易阻塞。
2.2.5.4 例子
-
高通 RIL (QCRIL):有多个 worker 线程,分别处理不同的请求类型(如数据、语音、SMS),每个 worker 有自己的命令队列,通过 QMI 或 AT 与 Modem 交互。
-
参考 RIL (reference-ril):没有独立的 worker 线程,而是在 mainLoop 中同步发送 AT 命令并等待响应(阻塞),但这仅适用于简单场景。
MO Call + Data + SMS (并发场景)

-
在典型的 Vendor RIL(如高通 QCRIL)中,存在一个 worker 线程池(大小可配置,例如 5~10 个线程)。
-
当 eventLoop 收到多个请求时,会将每个请求分配给 空闲的 worker 线程。因此:
-
MO Call、Data、SMS 可能被分配到三个不同的 worker 线程,从而实现真正的并发处理。
-
如果线程池繁忙,也可能两个请求被分配到同一个线程(串行处理),但现代 RIL 通常配置足够多的 worker 线程以避免阻塞。
-
2.2.5.5 worker 线程的优缺点
| 优点 | 缺点 |
|---|---|
| 提高并发响应能力 | 增加线程同步复杂性(需保护队列) |
| 避免 eventLoop 阻塞 | 可能引入竞态条件(需用互斥锁) |
| 可优先处理紧急命令 | 调试难度增加 |
2.2.5.6 通知管道notify pipe
管道是 Unix/Linux 系统提供的一种进程间通信(IPC)机制,它允许数据在内核中单向流动。在代码中,通过 pipe(int pipefd2) 系统调用创建:
-
pipefd0:读端(read end),用于从管道读取数据。
-
pipefd1:写端(write end),用于向管道写入数据。
-
数据在内核缓冲区中缓存,先进先出(FIFO)。
notify pipe 是 RILD 内部用于跨线程唤醒事件循环的机制。
创建:在 RIL_startEventLoop() 中通过 pipe() 或 eventfd() 创建一对文件描述符(读端 fdRead,写端 fdWrite)。
注册:事件循环线程(eventLoop)将 fdRead 加入到 epoll 或 poll 的监听集合中,监听其可读事件。
使用场景:当 Vendor RIL 的其他线程(如 readerLoop 或工作线程)收到 Modem 异步响应,需要通知事件循环 去调用 RIL_onRequestComplete 或处理主动上报时,向 fdWrite 写入任意一个字节(例如 'A')。
效果:eventLoop 的 epoll_wait 会立即返回,因为 fdRead 变为可读。事件循环读取这个字节, 然后执行预先注册的回调函数(如 processWatchtable()),从而触发对 RILJ 的响应发送。
2.2.5.6.1 为什么需要 notify pipe?
RILD 的核心线程 eventLoop 是一个单线程事件循环,它大部分时间阻塞在 epoll_wait()(或 poll())上,等待感兴趣的文件描述符(fd)发生事件。这些 fd 包括:
-
与 RILJ 通信的 socket
-
定时器 timerfd
-
其他内部 fd
然而,eventLoop 并不知道 Vendor RIL 的其他线程(如 readerLoop)何时会收到 Modem 的响应。当 readerLoop 读取到 Modem 响应后,它需要立即通知 eventLoop 去发送响应给 RILJ。但直接向 eventLoop 发送信号是很困难的,因为 eventLoop 正阻塞在内核中。
notify pipe 提供了一个优雅的解决方案:它是一个可以被 epoll 监听的 fd,任何其他线程都可以通过向 pipe 的写端写入数据来唤醒 eventLoop。
C
下发命令本身不需要 notify pipe。因为下发命令是由 eventLoop 收到 RILJ 请求后,
直接调用 Vendor RIL 的 onRequest 函数,该调用发生在 eventLoop 线程上下文中,不需要唤醒。
异步命令的响应返回时需要。如果 Vendor RIL 将命令交给另一线程(如 worker 线程)异步发送给 Modem,
那么当该线程收到 Modem 响应后,必须通过 notify pipe 唤醒 eventLoop,
再由 eventLoop调用 RIL_onRequestComplete 将结果发回 RILJ。
2.2.5.6.2 notify pipe 的初始化
notify pipe 通常在 RIL_startEventLoop() 函数中创建。以下是简化的初始化流程:
C++
// ril.cpp 中简化代码
static int s_fdWakeupRead = -1;
static int s_fdWakeupWrite = -1;
void RIL_startEventLoop() {
// 1. 创建管道
int ret = pipe(s_fdWakeupFds); // s_fdWakeupFds[0] 读端, [1] 写端
if (ret < 0) {
RLOGE("pipe creation failed");
return;
}
s_fdWakeupRead = s_fdWakeupFds[0];
s_fdWakeupWrite = s_fdWakeupFds[1];
// 2. 将读端设为非阻塞(可选,但推荐)
fcntl(s_fdWakeupRead, F_SETFL, O_NONBLOCK);
// 3. 创建事件循环线程
pthread_create(&s_tid_dispatch, NULL, eventLoopThread, NULL);
}
// eventLoop 线程入口
static void* eventLoopThread(void* param) {
// 注册 pipe 读端到 epoll
ril_event_set(&s_wakeup_event, s_fdWakeupRead, true, wakeupCallback, NULL);
ril_event_add(&s_wakeup_event);
// 进入 epoll 循环
ril_event_loop();
}
经过初始化后:
-
写端 s_fdWakeupWrite 可以被任意线程访问。
-
读端 s_fdWakeupRead 已被 eventLoop 监控。
2.2.5.6.3 软件结构图
SQL
+-------------------+ +-------------------+
| 写端 | | 读端 |
| (fd[1]) | | (fd[0]) |
| int fd_write | | int fd_read |
+--------+----------+ +----------+--------+
| ^
| write() | read()
v |
+----------------------------------------------+
| 内核管道缓冲区 (FIFO) |
| +-----+-----+-----+-----+-----+-----+ |
| |字节1|字节2| ... | ... | ... | ... | |
| +-----+-----+-----+-----+-----+-----+ |
| 写入顺序 ──────────────────────> 读出顺序 |
+----------------------------------------------+
C
readerLoop 线程 eventLoop 线程
│ │
│ write(1 字节) │ epoll_wait(监听 fd_read)
▼ ▼
┌───────┐ ┌─────────┐
│写端 fd│ ──── 字节流 (1 byte) ────►│读端 fd │
└───────┘ └─────────┘
│
│ read(1 字节)
▼
唤醒,处理队列
notify pipe 在 RILD 架构中的位置及与各线程的关系。

2.2.5.6.4 时序图
readerLoop 收到 Modem 数据后,通过管道唤醒 eventLoop 的过程。

下图展示了一个完整的跨线程唤醒流程,以 readerLoop 收到 Modem 响应为例。

来电MT(+CRING)的处理流程

2.2.5.6.5 为什么用 pipe,而不是信号或条件变量?
| 方案 | 问题 |
|---|---|
| 信号 (如 pthread_kill) | 信号处理函数受限,且容易与 epoll 产生竞争条件。 |
| 条件变量 + 互斥锁 | 无法直接唤醒 epoll_wait,需要先退出 epoll_wait(比如设置超时轮询),效率低。 |
| eventfd | 更现代的选择,效果类似 pipe(Linux 专用)。Android RIL 早期使用 pipe,后续版本可能使用 eventfd。 |
| pipe | 简单、可靠、跨平台(所有 POSIX 系统支持),可被 epoll 直接监听。 |
C
写入的数据内容重要吗?
不重要。写端通常只写入一个任意字节(如 'A' 或 '\0'),读端也只是读取并丢弃。
其目的仅仅是让 epoll_wait 返回,从而 eventLoop 有机会检查是否有新的响应需要发送。
如何避免惊群或数据残留?
读端设为非阻塞,并在回调中循环读取直到返回 EAGAIN/EWOULDBLOCK,确保 pipe 缓冲区被清空。
写入时若 pipe 满(概率极低),可选择忽略或阻塞,但通常不会发生,因为一次唤醒只需写 1 字节,
pipe 缓冲区默认 4KB~64KB。
C
典型应用场景
readerLoop 收到 Modem 主动上报(如来电):解析后写入 pipe,
eventLoop 调用 RIL_onUnsolicitedResponse。
readerLoop 收到某个请求的同步响应:匹配 token 后,
通过 pipe 唤醒 eventLoop 去调用 RIL_onRequestComplete。
定时器超时:虽然 timerfd 本身会唤醒 epoll,但某些老实现可能需要 worker 线程写入 pipe。
mainLoop 完成耗时的初始化:通知 eventLoop 可以开始处理 RILJ 请求。
notify pipe 是 RILD 实现线程安全唤醒事件循环的核心机制。它通过一个简单的、可被 epoll 监控的文件描述符,允许任意线程(readerLoop、mainLoop 等)安全地唤醒 eventLoop,从而将 Modem 响应及时传递给上层。这一设计既保持了单线程事件循环的简单性,又解决了跨线程通知的难题。
2.2.5.7 主要线程之间的关系
- eventLoop 处理 RILJ 与 rild 的通信
-
职责:监听与 RILJ 通信的 socket 文件描述符(也包括内部 notify pipe、定时器事件等)。
-
机制:使用 select/poll/epoll 实现 I/O 多路复用,当 socket 有数据可读时唤醒,读取 RILJ 发来的请求并分发处理。
-
目的:非阻塞、低延迟响应上层请求,同时避免多线程锁竞争。
- mainLoop 处理 rild 与 Modem 通信(含 Vendor RIL 部分)
-
归属:mainLoop 线程通常在 Vendor RIL 库内部 创建(如高通 libqcril.so 中的 mainLoop 函数)。
-
职责:
-
打开与 Modem 的物理通道(串口 /dev/ttyUSB0、共享内存、QMI 等)。
-
初始化 Modem 状态(如设置 APN、注册网络)。
-
创建 readerLoop 线程用于读取 Modem 消息。
-
发送 AT 命令或 QMI 请求给 Modem(实际发送常在 mainLoop 或专用工作线程中完成)。
-
-
关系:mainLoop 负责 Vendor RIL 与 Modem 的底层通信。
- at_open 处理 Vendor RIL 到 Modem 的 AT 命令
-
作用:at_open 是 Vendor RIL 中用于初始化 AT 通道的函数(常见于 reference-ril 或高通 RIL 的 AT 模块)。
-
工作:打开串口设备,设置串口属性(波特率、流控等),注册回调函数(处理主动上报、命令响应),然后创建 readerLoop 线程。
-
结果:通过此通道,Vendor RIL 可以下发 AT 命令并接收 Modem 响应。
2.2.5.8 rild-rilj 与VendorRIL-Modem接口分析
-
阻塞 read:线程调用 read(fd, ...) 时,如果 fd 中没有数据,内核会让该线程进入睡眠状态(不占用 CPU),直到有数据到达或发生错误才唤醒。
-
非阻塞 read 是指当文件描述符(fd)被设置为非阻塞模式(O_NONBLOCK)后,调用 read() 系统调用时,如果当前没有数据可读,不会让线程睡眠等待,而是立即返回,并返回错误码 EAGAIN 或 EWOULDBLOCK(二者通常值相同,表示"资源暂时不可用")。
-
select:
-
select/poll/epoll 是同步 I/O 多路复用:应用程序可以同时监控多个 fd,当其中任意 fd 就绪(如可读)时,select 返回,然后应用程序可以调用 read 读取(一般设置为非阻塞,但也可以阻塞)。
-
FD_ISSET 是宏,用于在 select 返回后判断哪个 fd 就绪。
-
-
为何 readerLoop 用阻塞 read:因为串口是低速设备且响应时间不确定,专用线程阻塞读取不影响其他线程,简单可靠。
| 特性 | AT open 阻塞 read (Vendor RIL 与 Modem) | rild-rilj 非阻塞 epoll (rild 与 RILJ) |
|---|---|---|
| I/O 模式 | 同步阻塞 I/O | 同步非阻塞 I/O + I/O 多路复用 |
| 线程模型 | 专用线程(readerLoop)独占读取一个 fd | 单线程(eventLoop)监控多个 fd |
| 等待机制 | 线程调用 read(),无数据时内核挂起线程 | 线程调用 epoll_wait(),无事件时内核挂起线程;fd 设置为非阻塞,read 立即返回 |
| 并发能力 | 只能读取一个串口,但线程可被唤醒后解析并分发 | 可以同时监听 socket、notify pipe、timer fd 等多个源 |
| 典型场景 | 从 Modem 串口读取 AT 响应或主动上报 | 接收 RILJ 发送的请求,以及接收 notify pipe 的唤醒信号 |
2.2.5.8.1 I/O 多路复用(I/O Multiplexing) 对比
select, poll, epoll 核心对比:
| 特性 | select | poll | epoll |
|---|---|---|---|
| 监控方式 | 轮询,每次调用都需遍历所有监控的 fd (文件描述符)。 | 轮询,遍历 pollfd 数组检查 revents 字段。 | 事件驱动,内核通过回调机制将就绪的 fd 直接放入就绪链表。 |
| 时间复杂度 | O(n),性能随监控 fd 数量增加而线性下降。 | O(n),同样需要遍历整个数组。 | O(1),epoll_wait() 只返回已就绪的 fd,无需全局遍历。 |
| 最大连接数 | 1024 (FD_SETSIZE 宏限制)。 | 无硬性限制,受系统内存限制。 | 无硬性限制,受系统内存和 /proc/sys/fs/file-max 限制。 |
| 数据拷贝 | 每次调用都需将 fd 集合从用户态拷贝到内核态。 | 同样需要将整个 pollfd 数组从用户态拷贝到内核态。 | 零拷贝,通过 mmap 共享内存,减少数据拷贝开销。 |
| 触发模式 | 仅支持水平触发(LT)。 | 仅支持水平触发(LT)。 | 支持 LT 和 ET,边缘触发(ET)更高效。 |
| API 接口 | select() 单一函数。 | poll() 单一函数。 | epoll_create(), epoll_ctl(), epoll_wait() 三组函数。 |
| 可移植性 | 跨平台 (POSIX 标准),几乎所有 Unix-like 系统都支持。 | 跨平台 (POSIX 标准),几乎所有 Unix-like 系统都支持。 | Linux 特有,不可移植。 |
| 实时性 | 高,timeout 精度为微秒级。 | 一般,timeout 精度为毫秒级。 | 一般,timeout 精度为毫秒级。 |
| API 友好度 | 简单,接口简洁,易于理解。 | 中等,比 select 稍复杂,但仍是单一函数。 | 复杂,API 更底层,编程需要更多技巧。 |
epoll的优势:在面对高并发场景时(比如上万个同时连接),epoll 的性能优势极其明显。
| 核心优势 | select / poll 的不足 | epoll 的高效方案 |
|---|---|---|
| 事件驱动,O(1)复杂度 | 每次调用都需 线性扫描 全部文件描述符(fd),复杂度 O(n)。 | 仅返回就绪的fd,复杂度 O(1)。 |
| 突破 fd 数量限制 | select 默认限制 1024 个 fd。 | 无硬性上限,仅受系统内存限制。 |
| 零拷贝,减少系统调用 | 每次调用都需要 将整个fd集合从用户态拷贝到内核态,开销大。 | 仅一次注册,通过内存映射(mmap)在内核与用户态共享数据,避免拷贝。 |
| 数据高效管理 | 使用线性数组或链表,增删和查找效率低。 | 内核使用 红黑树 来高效管理fd的增删改查(O(log n))。 |
| 灵活的触发模式 | 仅支持 水平触发(LT)。 | 同时支持高效的 边缘触发(ET) 和简单的 水平触发(LT)。 |
选择 select 还是 epoll?
在高端、通用的嵌入式Linux领域,如 智能手机、主流平板、车机系统 等,epoll 已替代 select,成为事实标准。在航空航天、军事、金融交易等需要绝对安全、稳定和确定性的系统领域,select凭借其简单、确定性的实现,仍是一个不可或缺的备选方案,通常用于处理少量、简单的I/O。
在实际开发中,当监控的 fd 数量很少(如少于10个) 时,使用 epoll 的优势并不明显,而 select 的简单性反而降低了开发和维护成本。此外,如果程序需要跨平台运行(如兼容 BSD、macOS 等),select或 poll 是更稳妥的选择。同时,对于生命周期长、必须保持代码稳定性的 "长寿"旧项目,为避免不必要的系统升级风险,开发者也会选择沿用 select。
选择I/O模型的本质是在 性能、复杂度、可移植性 之间寻找最佳平衡点,并没有绝对的"最好",只有针对特定场景的"最合适"。
2.2.5.8.2 行为图解
- rild-rilj 非阻塞 epoll(rild 侧)

- 非阻塞 epoll 的场景:rild 需要同时与 RILJ(一个或多个 socket)、notify pipe、定时器等多个 fd 交互。使用 epoll 可以将所有事件源统一到单线程中处理,避免多线程锁,且能高效应对高频率请求(比如 RILJ 短时间内下发几十个命令)。
- AT open 阻塞 read(Vendor RIL 侧)

- 阻塞 read 的场景:串口设备通常不支持 select/epoll 的高效事件驱动(尤其老旧驱动),但使用专用线程阻塞读取并不会拖累整体性能,因为挂起的线程不占用 CPU。同时,Modem 的响应速率相对较慢(几十到几百毫秒),阻塞读取足够应对。
C
为什么不能反过来?
如果把与 RILJ 通信改为阻塞 read:则需要为每个 RILJ 连接开一个线程,且无法同时监听 notify pipe,
会导致线程爆炸和唤醒困难。
如果把与 Modem 通信改为非阻塞 epoll:需要 Modem 驱动支持非阻塞且能产生 POLLIN 事件,
但部分嵌入式串口驱动不可靠,且 Modem 响应需要独立线程处理长时间操作(如拨号等待连接),
会增加状态管理复杂度。
- 综合架构图

上述设计利用了两种 I/O 模型的优点:事件循环的非阻塞多路复用保证上层通信的高效与可扩展,而专用线程的阻塞读取简化了与 Modem 的字符流交互。
2.2.5.9 rild 架构中的回调函数
| 方向 | 回调函数提供方 | 用途 | 回调函数 |
|---|---|---|---|
| AP → BP | Vendor RIL 提供的回调函数表(RIL_RadioFunctions),通过 RIL_Init 返回给 LibRIL, | eventLoop 收到 RILJ 请求后,调用 Vendor RIL 的这些函数,将命令下发给 Modem。 | onRequest:LibRIL 调用此函数来下发一个请求(如 DIAL、DATA CALL)。 |
| onStateRequest:查询 RIL 状态。 | |||
| onCancel:取消请求等。 | |||
| onSupports:查询是否支持某请求。 | |||
| BP → AP | LibRIL 提供的回调函数,通过 RIL_Env 结构体注册给 Vendor RIL | Vendor RIL 在收到 Modem 响应或主动上报后,调用这些函数,由 LibRIL 负责封装并发送给 RILJ。 | OnRequestComplete:Vendor RIL 调用此回调来响应一个请求(发送结果给 RILJ)。 |
| OnUnsolicitedResponse:Vendor RIL 调用此回调上报主动事件(如信号变化、来电)。 | |||
| RequestTimedCallback:用于请求超时处理。 |
注意:eventLoop 并不直接处理 Modem 原始数据,而是通过 notify pipe 被唤醒后,调用 LibRIL 提供的接口(如 RIL_onUnsolicitedResponse)将已解析好的数据发送给 RILJ。真正的 Modem 数据读取和解析由 readerLoop 或 worker 线程完成。
2.2.5.10 ril结构图,时序图


rild 使用单线程事件循环(Reactor 模式) + 辅助阻塞读线程,实现高效、可靠的电话栈通信。
2.2.5.11 队列
| 队列名称 | 核心作用 | 归属/操作者 |
|---|---|---|
| 请求队列 (Request Queue) | Vendor RIL 的命令缓冲池。用于缓存待发送到 Modem 的 AT 命令,实现命令的异步化处理。mainLoop 和 worker 线程都可能是它的生产者和消费者。 | Vendor RIL |
| 命令队列 (s_commands) | LibRIL 的命令路由表。一个静态数组,定义了所有 RIL_REQUEST_* 对应的分发函数(dispatchFunction)和响应函数(responseFunction)。 | LibRIL |
| 主动上报表 (s_unsolResponses) | LibRIL 的 URC 路由表。另一个静态数组,定义了 RIL_UNSOL_* 对应的响应函数(responseFunction),用于处理 Modem 的主动上报消息。 | LibRIL |
| 挂起列表 (pending_list) | eventLoop 的"待办事项"列表。当 I/O 事件(如 socket 数据到来)或定时器超时发生时,其对应的事件会被移至此队列,随后由 eventLoop 统一调度执行。 | eventLoop |

2.2.5.11.1 请求队列 (Request Queue)
-
位置:Vendor RIL 内部(例如 reference-ril.c 或高通 QCRIL 中)。
-
作用:当上层请求(如拨号、发短信)到来时,eventLoop 会调用 Vendor RIL 的 onRequest 函数。为了不阻塞 eventLoop,onRequest 将请求参数封装后放入 请求队列,然后立即返回。后台的 worker 线程池 从队列中取出请求,转换成 AT 命令发送给 Modem。
-
生产者:mainLoop(处理部分简单命令)或 onRequest 直接入队(通常由 eventLoop 触发的上下文)。
-
消费者:worker 线程。
-
同步机制:需要互斥锁保护队列,以及条件变量通知 worker 有新任务。
2.2.5.11.2 命令队列 (s_commands)
-
位置:LibRIL 内部,定义在 ril_commands.h 中,由 ril.cpp 引用。
-
作用:静态数组,每一行对应一个 RIL_REQUEST_XXX,包含:
-
request:请求号
-
dispatchFunction:将请求参数从 Parcel 解析出来的函数
-
responseFunction:将响应数据打包成 Parcel 发送给 RILJ 的函数
-
-
操作:只读。eventLoop 收到 RILJ 请求后,根据请求号索引该数组,调用 dispatchFunction 解析参数,再调用 Vendor RIL 的 onRequest。
2.2.5.11.3 主动上报表 (s_unsolResponses)
-
位置:LibRIL 内部,定义在 ril_unsol_commands.h。
-
作用:静态数组,每一行对应一个 RIL_UNSOL_XXX(如 RIL_UNSOL_CALL_RING),包含:
-
unsolResponse:主动上报号
-
responseFunction:将主动上报数据打包成 Parcel 发送给 RILJ
-
-
操作:只读。当 Vendor RIL 调用 RIL_onUnsolicitedResponse 时,LibRIL 根据上报号查表,调用对应的 responseFunction 封装数据并通过 socket 发送给 RILJ。
2.2.5.11.4 挂起列表 (pending_list)
-
位置:ril_event.cpp 中定义,事件循环的核心数据结构。
-
作用:存放已经被触发但尚未执行回调的事件节点(ril_event)。例如:
-
RILJ socket 有数据可读 → socket 事件从 watch_table 移到 pending_list。
-
定时器超时 → 超时事件从 timer_list 移到 pending_list。
-
其他线程向 notify pipe 写字节 → pipe 读端事件移入 pending_list。
-
-
操作者:eventLoop 线程。主循环中先调用 processTimeouts() 和 processReadReadies() 将就绪事件移入 pending_list,然后调用 firePending() 遍历 pending_list 并执行每个事件的回调函数。处理完后清空列表。
2.2.5.11.5 总结
| 请求队列 (Request Queue) | 缓存待发送到 Modem 的 AT 命令,实现异步化处理。 | Vendor RIL(生产者:mainLoop/worker;消费者:worker 线程) | 队列(链表/环形缓冲) | 需要互斥锁 |
|---|---|---|---|---|
| 命令队列 (s_commands) | LibRIL 的命令路由表,映射 RIL_REQUEST_* → 分发函数 + 响应函数。 | LibRIL(只读,事件循环查询) | 静态数组 | 只读,无需锁 |
| 主动上报表 (s_unsolResponses) | LibRIL 的 URC 路由表,映射 RIL_UNSOL_* → 响应函数。 | LibRIL(只读,事件循环查询) | 静态数组 | 只读,无需锁 |
| 挂起列表 (pending_list) | eventLoop 的待办事项列表,存放已就绪的 I/O 事件或定时器事件,等待回调执行。 | eventLoop 线程(内部管理) | 双向链表 | 单线程内部使用,无需锁 |
这四个队列/表分别解决了:
-
请求队列:异步命令发送(生产者-消费者模型)。
-
s_commands / s_unsolResponses:请求/上报的路由与序列化/反序列化。
-
pending_list:事件循环的就绪事件管理。
2.2.6 同步机制
RILD 是一个多线程守护进程,需要处理:
-
跨进程通信:与上层 RILJ(Java 进程)通过 socket 交互。
-
跨线程同步:保护共享队列(如请求队列)的并发访问。
-
跨线程通知:唤醒事件循环处理异步响应。
| 机制名称 | 关键特点 | 主要用途 | RILD 中的具体应用 |
|---|---|---|---|
| 互斥锁 (Mutex) | 提供基本的互斥访问。 | 保护共享资源。确保在多线程环境下,如 Vendor RIL 内部的请求队列等共享资源的访问是线程安全的。 | 保护 Vendor RIL 的请求队列(Request Queue),防止多线程同时入队/出队 |
| 条件变量 (Condition Variable) | 需配合互斥锁使用。 | 实现线程等待/通知。当某个条件不满足时,线程可以在此等待,直到被其他线程通知唤醒,常用于实现生产者-消费者模式。 | worker 线程等待队列非空;生产者(eventLoop)放入请求后通知 worker |
| 信号量 (Semaphore) | 可允许多个线程同时访问。 | 控制资源访问数量。可用于限制同时访问某一资源的线程数量,相当于一个"计数器锁"。 | 较少使用,可用互斥锁+条件变量替代 |
| select/poll/epoll | 单线程处理多路 I/O。 | 多路I/O复用。eventLoop 的核心机制,让单个线程能同时"监控"多个文件描述符(如 socket、pipe),从而高效处理并发 I/O。 | eventLoop 同时监听 RILJ socket、notify pipe、timerfd |
| Pipe (管道) | 简单、可靠的唤醒机制。 | 线程间通知。主要用于 notify pipe 机制,是一种轻量级的跨线程唤醒方式。 | notify pipe:readerLoop 写管道唤醒 eventLoop |
| Timerfd (定时器文件描述符) | 将定时器事件与 I/O 事件统一。 | 集成定时器到事件循环。允许将定时器事件像普通文件句柄一样被 select/poll 监听,从而在 eventLoop 中统一处理超时。 | 处理请求超时(如拨号 5 秒未响应) |
| 信号 (Signal) | 用于进程间通信。 | 进程间通信。用于进程间的简单通知,但通常不用于 rild 内部的多线程同步,因其异步特性可能带来复杂性。 | NA |
同步的语境
| 语境 | 含义 | 反义词 |
|---|---|---|
| 线程/进程同步 | 协调多个执行流对共享资源的访问顺序,避免竞态条件。 | 异步(指无协调,不保证顺序) |
| I/O 同步/异步 | 调用方是否等待操作完成:同步 I/O 阻塞直到完成;异步 I/O 不阻塞,通过回调或事件通知。 | 异步 I/O |
在 RILD 源码分析中,"同步"常指 I/O 模型。
-
同步阻塞:线程调用 read(),无数据则挂起,直到有数据返回。例如 readerLoop 阻塞读串口。
-
同步非阻塞:调用 read() 立即返回(有数据或 EAGAIN),需要配合 select/poll 轮询。
-
异步 I/O:调用 aio_read 或使用 io_uring,内核完成后再通知。
同步 I/O:mainLoop 中 at_send_command 等待 Modem 响应。
场景:reference-ril 中 mainLoop 线程直接调用 AT 命令发送函数,该函数内部阻塞等待 Modem 响应(例如 OK 或 ERROR),在此期间 mainLoop 线程被挂起,无法处理任何其他请求。

特点:
-
编程简单,顺序执行。
-
线程利用率低:在等待 Modem 响应的数百毫秒甚至数秒内,
mainLoop无法处理其他请求。 -
仅适用于简单调试或并发要求极低的场景。
异步 I/O:请求队列 + worker 线程池,eventLoop 不等待 AT 命令完成。
场景:现代 Vendor RIL(如 QCRIL)中,eventLoop 收到请求后将其放入请求队列,立即返回;worker 线程池取出请求并异步发送 AT 命令,完成后通过 notify pipe 唤醒 eventLoop 发送响应。

特点:
-
主线程 (eventLoop) 不被阻塞,可以同时处理多个来自 RILJ 的请求。
-
通过队列解耦,worker 线程池并行执行 AT 命令,提升吞吐量。
-
需要互斥锁保护队列,条件变量实现等待/通知。
总结
| 特性 | 同步 I/O (at_send_command) | 异步 I/O (队列 + worker) |
|---|---|---|
| 调用是否阻塞 | 是,线程挂起直到响应返回 | 否,enqueue 后立即返回 |
| 并发处理能力 | 差,一次只能处理一个请求 | 高,多个 worker 并行处理 |
| 线程模型 | 单线程(mainLoop)处理所有 | 事件循环 + 线程池 |
| 适用场景 | 简单调试、低负载 | 生产环境(手机、车载等) |
| 实现复杂度 | 低 | 较高(需管理队列、锁、条件变量) |
2.2.7 进程间通信(IPC)
IPC(Inter-Process Communication) 是操作系统提供的机制,允许不同进程之间交换数据或信号。每个进程拥有独立的地址空间,因此需要内核协助才能安全通信。
2.2.7.1 常见 IPC 方式
| 方式 | 特点 | RILD 中的使用 |
|---|---|---|
| Socket(Unix Domain / TCP) | 双向、流式或数据报,支持跨网络 | RILJ 与 rild 通过 /dev/socket/rild 通信 |
| Pipe / FIFO | 单向字节流,亲缘进程或同一主机进程 | notify pipe 是线程间通信,也可用于进程间 |
| 共享内存 (shm) | 最快,需同步机制(如信号量) | RIL 中较少直接使用 |
| 信号 (Signal) | 异步事件通知,携带信息有限 | rild 可能处理 SIGPIPE 避免崩溃 |
| 消息队列 (msg queue) | 有类型消息,内核维护 | 不常用 |
2.2.7.2 RIL 中最重要的 IPC:Unix Domain Socket
特点:
-
同一主机上高效,类似 TCP 但不经过网络协议栈。
-
文件系统路径作为地址(如 /dev/socket/rild)。
-
支持流式(SOCK_STREAM)或数据报(SOCK_DGRAM)。
在 RIL 中的角色:
-
RILJ(Java 进程)与 rild 守护进程之间的唯一 IPC 通道。
-
RILJ 将请求(RIL_REQUEST_*)序列化为 Parcel,通过 socket 发送。
-
rild 的 eventLoop 监听该 socket,收到数据后反序列化并处理。
简化通信过程
C
// rild 端 (C++)
int listen_fd = android_get_control_socket("rild");
listen(listen_fd, 4);
int client_fd = accept(listen_fd, ...);
// 将 client_fd 加入 epoll
Java
// RILJ 端 (Java)
LocalSocket s = new LocalSocket();
s.connect(new LocalSocketAddress("rild", LocalSocketAddress.Namespace.RESERVED));
OutputStream os = s.getOutputStream();
// 写入 Parcel 数据
os.write(data);
2.2.7.3 线程间通信 vs 进程间通信
注意:notify pipe、互斥锁、条件变量 等属于线程间同步与通信,它们用于同一进程内的多个线程。而 IPC 特指不同进程之间。
| 类型 | 示例 | 是否需要内核介入 |
|---|---|---|
| 线程间 | 互斥锁、条件变量、线程局部变量 | 部分需要(锁可能用原子操作) |
| 进程间 | socket、pipe、共享内存、信号 | 必须通过内核 |
2.2.7.4 软件结构图
跨进程通信(RILJ ↔ rild)和线程间通信(notify pipe)。

| 元素 | 类型 | 说明 |
|---|---|---|
| RILJ ↔ Socket | 跨进程 IPC | Unix Domain Socket,文件系统路径 /dev/socket/rild,全双工 |
| Socket → eventLoop | 进程内数据流 | rild 主线程监听 socket fd,使用 select/poll/epoll 接收数据 |
| readerLoop / worker 线程 → notify pipe | 线程间通信 | 向管道写端写入任意字节,唤醒 eventLoop |
| notify pipe → eventLoop | 线程间通知 | 读端被 epoll 监听,可读时触发事件循环处理队列 |
-
跨进程通信(Socket)------ RILJ 与 rild 之间的唯一通道。
-
线程间通信(notify pipe)------ 用于其他线程唤醒 eventLoop,不涉及进程边界。