Android+QC modem手机通信模块技术分析 (4)

2.3.6 rild.c

C 复制代码
hardware/ril/rild/rild.c

唯一的入口点,负责解析参数、加载动态库等顶层初始化工作

rild 守护进程的入口函数,负责解析命令行参数、加载厂商 RIL 库、初始化事件循环、获取 RIL 接口函数并注册到 libril,最后进入无限等待。

main 函数执行以下主要步骤:

  1. 解析参数:从命令行读取 -l (指定厂商 RIL 库路径)、-c (客户端 ID,用于多 SIM)、-- (传递给厂商库的参数)。

  2. 确定 RIL 库路径:如果未通过 -l 指定,则从系统属性 ro.telephony.ril.lib 中读取;若均无,则进入 done 标签直接睡眠(无 RIL 模式)。

  3. 模拟器特殊处理:如果检测到运行在 QEMU (Android 模拟器) 环境,则强制使用 libreference-ril.so并设置设备节点。

  4. 切换用户:调用 switchUser() 降低权限(通常切换为 radio 用户)。

  5. 动态加载厂商 RIL 库:使用 dlopen 加载 .so,并用 dlsym 获取 RIL_Init 函数指针。

  6. 启动事件循环:调用 RIL_startEventLoop() 创建事件分发线程。

  7. 调用厂商初始化:执行 rilInit(&s_rilEnv, argc, s_argv) 获得 RIL_RadioFunctions 函数表。

  8. 注册到 libril:调用 RIL_register(funcs),将函数表保存并创建监听 socket。

  9. 进入无限睡眠:守护进程保持存活,实际工作由事件循环线程完成。

C 复制代码
int main(int argc, char **argv)
{
    const char * rilLibPath = NULL;//要加载的厂商 RIL 库路径(如 libreference-ril.so)。
    char **rilArgv;
    static char * s_argv[MAX_LIB_ARGS] = {NULL};//用于传递给厂商 RIL_Init 的参数数组。
    void *dlHandle;                             //dlopen 返回的库句柄
    const RIL_RadioFunctions *(*rilInit)(const struct RIL_Env *, int, char **);
    //函数指针,指向厂商库的 RIL_Init
    const RIL_RadioFunctions *funcs;     //厂商返回的函数表。
    char libPath[PROPERTY_VALUE_MAX];
    unsigned char hasLibArgs = 0;
    int j = 0;
    int i;
    static char clientId[3] = {'0'};//客户端 ID(多 SIM 支持,默认为 "0")。

    RLOGD("**RIL Daemon Started**");
    RLOGD("**RILd param count=%d**", argc);
    memset(s_argv, 0, sizeof(s_argv));//打印启动日志,清空参数数组

    s_argv[0] = argv[0];//第一个参数设为程序名。
    //umask 设置文件创建掩码,移除组和其他用户的读写执行权限。
    
 /*
-l:指定 RIL 库路径(如 /system/lib/libreference-ril.so)。
-c:指定客户端 ID(用于多卡,如 -c 1 表示第二个 SIM)。
--:之后的所有参数将原样传递给厂商 RIL_Init(例如 -d /dev/ttyS0)。
不支持其他选项时调用 usage 退出。
 */   
    umask(S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH);
    
    //日志与参数解析
    for (i = 1, j = 1; i < argc ;) {
        if (0 == strcmp(argv[i], "-l") && (argc - i > 1)) {
            rilLibPath = argv[i + 1];
            i += 2;
        } else if (0 == strcmp(argv[i], "-c") && (argc - i > 1)) {
            strncpy(clientId, argv[i+1], strlen(clientId));
            i += 2;
        } else if (0 == strcmp(argv[i], "--")) {
            i++;
            hasLibArgs = 1;
            memcpy(&s_argv[j], &argv[i], argc-i);
            break;
        } else {
            usage(argv[0]);
        }
    }

/* 客户端 ID 校验
MAX_RILDS 通常为 2 或 4,限制并发 RIL 实例数。
若 clientId 非 "0",则调用 RIL_setRilSocketName 修改 socket 名称(如 rild1, rild2),用于多 SIM 分离。
*/
    if (atoi(clientId) >= MAX_RILDS) {
        RLOGE("Max Number of rild's supported is: %d", MAX_RILDS);
        exit(0);
    }
    RLOGD ("RIL Client Id:=%s", clientId);

    if (strncmp(clientId, "0", MAX_CLIENT_ID_LENGTH)) {
        RIL_setRilSocketName(clientId);
    }

/* 确定 RIL 库路径
若命令行未提供 -l,则读取系统属性 LIB_PATH_PROPERTY(通常为 "ro.telephony.ril.lib")。
若属性也未设置,则跳转到 done 标签(守护进程无 RIL 功能,仅睡眠)。
*/
    if (rilLibPath == NULL) {
        if ( 0 == property_get(LIB_PATH_PROPERTY, libPath, NULL)) {
            // No lib sepcified on the command line, and nothing set in props.
            // Assume "no-ril" case.
            goto done;
        } else {
            rilLibPath = libPath;
        }
    }

    /* special override when in the emulator 模拟器特殊处理(#if 1 块)*/
#if 1
    {
        static char   arg_device[32];
        int           done = 0;

#define  REFERENCE_RIL_PATH  "/system/lib/libreference-ril.so"

        /* first, read /proc/cmdline into memory */
        char          buffer[1024], *p, *q;
        int           len;
        int           fd = open("/proc/cmdline",O_RDONLY);

        if (fd < 0) {
            RLOGD("could not open /proc/cmdline:%s", strerror(errno));
            goto OpenLib;
        }

        do {
            len = read(fd,buffer,sizeof(buffer)); }
        while (len == -1 && errno == EINTR);

        if (len < 0) {
            RLOGD("could not read /proc/cmdline:%s", strerror(errno));
            close(fd);
            goto OpenLib;
        }
        close(fd);

        if (strstr(buffer, "android.qemud=") != NULL)
        {
            /* the qemud daemon is launched after rild, so
            * give it some time to create its GSM socket
            */
            int  tries = 5;
#define  QEMUD_SOCKET_NAME    "qemud"

            while (1) {
                int  fd;

                sleep(1);

                fd = qemu_pipe_open("qemud:gsm");
                if (fd < 0) {
                    fd = socket_local_client(
                                QEMUD_SOCKET_NAME,
                                ANDROID_SOCKET_NAMESPACE_RESERVED,
                                SOCK_STREAM );
                }
                if (fd >= 0) {
                    close(fd);
                    snprintf( arg_device, sizeof(arg_device), "%s/%s",
                                ANDROID_SOCKET_DIR, QEMUD_SOCKET_NAME );

                    memset(s_argv, 0, sizeof(s_argv));
                    s_argv[1] = "-s";
                    s_argv[2] = arg_device;
                    done = 1;
                    break;
                }
                RLOGD("could not connect to %s socket: %s",
                    QEMUD_SOCKET_NAME, strerror(errno));
                if (--tries == 0)
                    break;
            }
            if (!done) {
                RLOGE("could not connect to %s socket (giving up): %s",
                    QEMUD_SOCKET_NAME, strerror(errno));
                while(1)
                    sleep(0x00ffffff);
            }
        }

        /* otherwise, try to see if we passed a device name from the kernel */
        if (!done) do {
#define  KERNEL_OPTION  "android.ril="
#define  DEV_PREFIX     "/dev/"

            p = strstr( buffer, KERNEL_OPTION );
            if (p == NULL)
                break;

            p += sizeof(KERNEL_OPTION)-1;
            q  = strpbrk( p, " \t\n\r" );
            if (q != NULL)
                *q = 0;

            snprintf( arg_device, sizeof(arg_device), DEV_PREFIX "%s", p );
            arg_device[sizeof(arg_device)-1] = 0;
            memset(s_argv, 0, sizeof(s_argv));
            s_argv[1] = "-d";
            s_argv[2] = arg_device;
            done = 1;

        } while (0);

        if (done) {
            argc = 3;
            i    = 1;
            hasLibArgs = 1;
            rilLibPath = REFERENCE_RIL_PATH;
            RLOGD("overriding with %s %s", s_argv[1], s_argv[2]);
        }
    }
OpenLib:
#endif
    switchUser(); //切换用户 将进程的有效用户 ID 切换为 radio(通常为 AID_RADIO),降低权限。

//动态加载厂商 RIL 库。dlopen 加载厂商库(如 libreference-ril.so 或高通 libril-qc.so)。
    dlHandle = dlopen(rilLibPath, RTLD_NOW);

    if (dlHandle == NULL) {
        RLOGE("dlopen failed: %s", dlerror());
        exit(-1);
    }

*/* 启动事件循环线程。创建 libril 的事件分发线程(ril_event_loop),该线程会立即进入 select 循环等待事件。*/*
    RIL_startEventLoop(); 

//获取厂商库中的 RIL_Init 函数地址。
    rilInit = (const RIL_RadioFunctions *(*)(const struct RIL_Env *, int, char **))dlsym(dlHandle, "RIL_Init");

    if (rilInit == NULL) {
        RLOGE("RIL_Init not defined or exported in %s\n", rilLibPath);
        exit(-1);
    }

/* 构造参数并调用 RIL_Init
若命令行有 --,则使用其后的参数;否则尝试从系统属性 LIB_ARGS_PROPERTY(如 "rild.libargs")读取参数。
最后强制添加 -c <clientId> 参数,传递给厂商 RIL_Init。
调用 rilInit,传入 s_rilEnv(libril 提供的回调环境)和参数数组,获得函数表 funcs
*/
    if (hasLibArgs) {
        argc = argc-i+1;
    } else {
        static char * newArgv[MAX_LIB_ARGS];
        static char args[PROPERTY_VALUE_MAX];
        property_get(LIB_ARGS_PROPERTY, args, "");
        argc = make_argv(args, s_argv);
    }

    // Make sure there's a reasonable argv[0]
    s_argv[0] = argv[0];

    if (argc >= MAX_LIB_ARGS - 2) {
        RLOGE("Max arguments are passed for rild, args count = %d", argc);
        exit(0);
    }
    s_argv[argc++] = "-c";
    s_argv[argc++] = clientId;

    RLOGD("RIL_Init argc = %d clientId = %s", argc, s_argv[argc-1]);

    funcs = rilInit(&s_rilEnv, argc, s_argv);

/*  注册函数表
RIL_register 将厂商函数表保存到 libril 全局变量,并创建监听 socket /dev/socket/rild,将其加入事件循环。
*/
    RIL_register(funcs);

done:
/*主线程进入无限循环睡眠,避免进程退出。实际所有 I/O 操作都在 RIL_startEventLoop 创建的子线程中处理。*/
    while(1) {
        // sleep(UINT32_MAX) seems to return immediately on bionic
        sleep(0x00ffffff);
    }
}

2.3.6.1 代码片段分析

2.3.6.1.1 dlsym
C 复制代码
rilInit = (const RIL_RadioFunctions *(*)(const struct RIL_Env *, int, char **)) dlsym(dlHandle, "RIL_Init");
  1. dlsym 的返回值
C 复制代码
void *dlsym(void *handle, const char *symbol);
  • dlsym 返回 void*,表示符号在内存中的地址。
  1. 目标类型(函数指针类型)

将 void* 转换为一个函数指针,该函数具有以下签名:

  • 参数:(const struct RIL_Env *, int, char **)

  • 返回值:const RIL_RadioFunctions *

C 复制代码
const RIL_RadioFunctions *(*)(const struct RIL_Env *, int, char **)
  • (*) 表示这是一个函数指针。

  • 括号内是参数列表。

  • 左侧是返回值类型。

  1. 强制转换

将 dlsym 的结果强制转换为上述类型:

C 复制代码
(const RIL_RadioFunctions *(*)(const struct RIL_Env *, int, char **)) dlsym(...)
  1. 赋值给变量 rilInit

rilInit 变量的声明通常如下:

C 复制代码
const RIL_RadioFunctions *(*rilInit)(const struct RIL_Env *, int, char **);

即 rilInit 本身就是一个函数指针变量,类型与强制转换的目标类型相同(省去了外层括号和星号,因为变量名直接跟在类型之后)。

因此赋值后,rilInit 指向动态库中的 RIL_Init 函数,后续可通过 rilInit(&s_rilEnv, argc, s_argv) 调用。

  1. 语法解析图
  1. 等价分解(使用 typedef)
C++ 复制代码
typedef const RIL_RadioFunctions *(*RIL_InitFunc)(const struct RIL_Env *, int, char **);
RIL_InitFunc rilInit = (RIL_InitFunc) dlsym(dlHandle, "RIL_Init");
  • dlsym 返回的是原始地址,必须转换为正确的函数指针类型才能调用。

  • C 语言的函数指针类型声明语法比较晦涩:返回值 (*变量名)(参数列表)。

  • 强制转换时,类型描述符中变量名位置用 (*) 代替,表示"这是一个函数指针"。

  • 该行代码是 Android RIL 动态加载厂商库的核心,实现了运行时的多态性。

2.3.6.1.2 funcs

这行代码是 Android RIL 守护进程 rild 中调用厂商 RIL 库初始化函数的核心语句

C 复制代码
funcs = rilInit(&s_rilEnv, argc, s_argv);

目的:调用动态加载的厂商 RIL 库中的 RIL_Init 函数,完成 Modem 适配层的初始化,并获取厂商提供的 RIL 函数表。

结果:将返回的函数表指针保存到 funcs 变量中,后续通过 RIL_register(funcs) 注册到 libril。

变量与类型解析

变量 类型 含义
rilInit const RIL_RadioFunctions ()(const struct RIL_Env *, int, char **) 函数指针,指向厂商库中的 RIL_Init 函数。
s_rilEnv struct RIL_Env libril 提供的回调环境结构体(全局变量),包含 OnRequestComplete、OnUnsolicitedResponse 等函数指针。
argc int 传递给厂商 RIL_Init 的参数个数。
s_argv char ** 传递给厂商 RIL_Init 的参数数组(例如设备节点路径、调试选项等)。
funcs const RIL_RadioFunctions * 接收厂商返回的函数表指针。

参数详解

  1. &s_rilEnv:回调环境指针
  • s_rilEnv 是在 rild.c 中定义的全局变量,类型为 struct RIL_Env。

  • 该结构体由 libril 填充,提供了三个回调函数:

    • OnRequestComplete:厂商 RIL 处理完请求后调用,将结果返回给 libril。

    • OnUnsolicitedResponse:厂商 RIL 收到 Modem 主动上报时调用。

    • RequestTimedCallback:用于注册定时回调。

  • 厂商 RIL 内部会保存这个指针,以便后续回调 libril。

  1. argc 与 s_argv:厂商参数
  • 这些参数来自 rild 启动命令行中的 -- 之后的部分,或者来自系统属性 rild.libargs。

  • 典型参数示例:-d /dev/ttyS0(指定串口设备)、-s /dev/socket/qemud(模拟器 socket)。

  • 厂商 RIL_Init 可以根据这些参数打开正确的硬件接口。

返回值 funcs

rilInit 返回一个指向 RIL_RadioFunctions 结构体的常量指针,该结构体包含:

C++ 复制代码
typedef struct {
    int version;                     // 接口版本号
    RIL_RequestFunc onRequest;       // 处理上层请求的入口函数
    RIL_RadioStateRequest onStateRequest; // 查询无线状态
    RIL_Supports supports;           // 查询是否支持某请求
    RIL_Cancel onCancel;             // 取消请求
    RIL_GetVersion getVersion;       // 获取版本字符串
} RIL_RadioFunctions;

厂商 RIL 库内部必须实现这些函数,并将函数指针填入结构体,然后返回该结构体的地址。这个结构体通常是静态全局变量(生命周期与进程相同)。

执行流程上下文

C++ 复制代码
// 1. 动态加载厂商库
dlHandle = dlopen(rilLibPath, RTLD_NOW);
// 2. 获取 RIL_Init 函数指针
rilInit = (const RIL_RadioFunctions *(*)(const struct RIL_Env *, int, char **))
          dlsym(dlHandle, "RIL_Init");
// 3. 调用厂商 RIL_Init 获得函数表
funcs = rilInit(&s_rilEnv, argc, s_argv);   // <--- 本行代码
// 4. 将函数表注册到 libril
RIL_register(funcs);

语法与语义要点

  • 函数指针调用:rilInit 是函数指针,可以直接用 rilInit(...) 调用,等价于 (*rilInit)(...)。

  • 参数传递:&s_rilEnv 传递回调环境指针,厂商 RIL 必须将其保存到全局变量(如 static const RIL_Env *s_rilenv)。

  • 返回值赋值:返回的函数表指针赋值给 funcs,随后传递给 RIL_register。

C 复制代码
输入:libril 提供的回调环境 s_rilEnv 和启动参数。
处理:厂商 RIL_Init 初始化 Modem 接口,保存回调环境。
输出:厂商实现的函数表 RIL_RadioFunctions。
后续:函数表被注册到 libril,自此上下层通信通道正式建立。
通过这行代码,rild 成功地将厂商的硬件适配代码"插入"到了 Android 的电话栈中。

图示

2.3.6.2 RILD流程图

2.3.6.3 时序图(主线程与事件循环线程)

代码段 作用
参数解析 支持多 SIM、传递厂商参数
模拟器适配 自动检测设备节点,使用参考实现
RIL_startEventLoop 创建独立的事件循环线程,提前就绪
rilInit 调用 厂商 RIL 库的初始化入口,返回函数表
RIL_register 将厂商函数表注册到 libril,创建监听 socket
主线程睡眠 保持进程存活,实际工作由子线程完成

这种设计将库加载、参数初始化与事件驱动框架分离,主线程只负责初始化,事件循环线程独立处理所有 I/O,是典型的"反应器(Reactor)"模式在守护进程中的实现。

2.3.7 libreference-ril.so

C 复制代码
reference-ril.c
atchannel.c

Vendor RIL,厂家适配层。实现具体的AT命令收发,与Modem硬件交互

2.3.7.1 reference-ril.c

C 复制代码
hardware/ril/reference-ril/reference-ril.c

2.3.7.2 RIL_Init

RIL_Init 是厂商 RIL 库(参考实现)的入口函数,由 rild 主进程调用。它的职责是:

  1. 解析命令行参数:确定 Modem 通信方式(串口设备、TCP 端口或 socket)。

  2. 保存 RIL_Env 环境:以便后续调用 libril 的回调函数。

  3. 创建后台线程 mainLoop:负责处理 Modem 主动上报的消息(URC)。

  4. 返回函数表 s_callbacks:向 rild 提供 onRequest、onStateRequest 等函数指针。

C 复制代码
static pthread_t s_tid_mainloop; *// 后台线程 ID*
static const struct RIL_Env *s_rilenv; *// 保存的 RIL 环境指针*
/*
s_tid_mainloop:存储 mainLoop 线程 ID,用于可能的后续操作(本例未使用)。
s_rilenv:静态指针,保存传入的 env,供 mainLoop 或 onRequest 中调用 RIL_onRequestComplete 等回调。
*/

const RIL_RadioFunctions *RIL_Init(const struct RIL_Env *env, int argc, char **argv)
{
    int ret;
    int fd = -1;
    int opt;
    pthread_attr_t attr;

/* 保存 RIL 环境
将 libril 提供的回调环境指针保存到全局变量。后续厂商 RIL 可通过
s_rilenv->OnRequestComplete(...) 返回请求结果。
*/
    s_rilenv = env;

//解析命令行参数
    while ( -1 != (opt = getopt(argc, argv, "p:d:s:c:"))) {
        switch (opt) {
            case 'p':
                s_port = atoi(optarg);
                if (s_port == 0) {
                    usage(argv[0]);
                    return NULL;
                }
                RLOGI("Opening loopback port %d\n", s_port);
            break;

            case 'd':
                s_device_path = optarg;
                RLOGI("Opening tty device %s\n", s_device_path);
            break;

            case 's':
                s_device_path   = optarg;
                s_device_socket = 1;
                RLOGI("Opening socket %s\n", s_device_path);
            break;

            case 'c':
                ril_inst_id = optarg;
                RLOGI("ReferRil is using instance %s ", ril_inst_id);
            break;

            default:
                usage(argv[0]);
                return NULL;
        }
    }
/*
getopt 解析参数:
-p <port>:TCP 端口号(loopback 模式)。
-d <device>:串口设备路径(如 /dev/ttyS0)。
-s <socket>:Unix socket 路径(用于模拟器)。
-c <id>:RIL 实例 ID(多 SIM 场景)。

这些参数由 rild 启动命令行中的 -- 之后部分传递(例如 rild -l libreference-ril.so -- -d /dev/ttyS0)。解析结果存入全局变量:
s_port:端口号。
s_device_path:设备或 socket 路径。
s_device_socket:是否为 socket 模式。

若同时指定了 -p 和 -d,后指定的会覆盖?实际代码中 s_port 和 s_device_path 都会设置,但后续 mainLoop 中会优先使用设备路径?需要看具体逻辑。在参考实现中,通常只使用其中一种。
*/

//参数校验 必须指定至少一种通信方式(端口或设备路径),否则退出。
    if (s_port < 0 && s_device_path == NULL) {
        usage(argv[0]);
        return NULL;
    }

//分配 Modem 信息结构体 sMdmInfo 是全局结构体,用于存储 Modem 相关信息(如技术类型、状态等)。这里分配内存并置零。
    sMdmInfo = calloc(1, sizeof(ModemInfo));
    if (!sMdmInfo) {
        RLOGE("Unable to alloc memory for ModemInfo");
        return NULL;
    }
    
/*  创建 mainLoop 线程   
 设置线程属性为分离状态(PTHREAD_CREATE_DETACHED),线程结束自动回收资源,无需 pthread_join。
pthread_create 创建线程,入口函数为 mainLoop,无参数。
mainLoop 负责打开设备(串口/socket)、建立 AT 通道、读取 Modem 上报消息并调用 onUnsolicited 回调。*/
    pthread_attr_init (&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    ret = pthread_create(&s_tid_mainloop, &attr, mainLoop, NULL);

/*
s_callbacks 是静态的 RIL_RadioFunctions 结构体,已在文件开头定义,包含 onRequest、onStateRequest 等函数指针。
返回该结构体地址给 rild,rild 随后会调用 RIL_register(s_callbacks)。
*/
    return &s_callbacks;
}

流程图

时序图

步骤 说明
保存 env 后续 mainLoop 或 onRequest 可通过 s_rilenv->OnRequestComplete等回调 libril。
解析参数 支持串口、TCP 端口、Unix socket 三种 Modem 连接方式。
创建 mainLoop 独立线程读取 Modem 上报(URC),转换为 RIL_UNSOL_* 事件,调用 s_rilenv->OnUnsolicitedResponse 上送。
返回 &s_callbacks 向 rild 提供标准接口表,实现 onRequest、onStateRequest 等。

这个函数是厂商 RIL 适配层的"工厂",它根据启动参数选择硬件接口,初始化后台处理线程,并将自身的实现接口注册到框架中。

2.3.7.3 mainLoop

mainLoop 是厂商 RIL 参考实现的后台线程函数,由 RIL_Init 创建并运行。它的核心职责是:打开与 Modem 的通信通道(串口 / TCP / Unix socket),初始化 AT 命令处理器,然后循环等待通道关闭,并在断开后自动重连。它也是处理 Modem 主动上报(URC)的起点。

C++ 复制代码
static void *
mainLoop(void *param)
{
    int fd;                   //将要打开的通信通道文件描述符(串口、socket 等)。
    int ret;                  //保存函数返回值。

    AT_DUMP("== ", "entering mainLoop()", -1 );  //调试宏,打印日志。
    at_set_on_reader_closed(onATReaderClosed);//注册一个回调,当 AT 读取器关闭时调用(用于清理或重连)。
    at_set_on_timeout(onATTimeout);//注册超时回调(AT 命令超时时的处理)。

/* 外层无限循环(重连机制)
外层 for(;;) 保证 Modem 连接断开后能够自动重连。
while (fd < 0) 循环内根据不同配置尝试打开通道,直到成功。
*/
    for (;;) {
        fd = -1;
        while  (fd < 0) {
        //TCP 端口模式(s_port > 0)连接到本地环回地址的指定 TCP 端口(用于模拟 Modem 或调试)。
            if (s_port > 0) {
                fd = socket_loopback_client(s_port, SOCK_STREAM);
            } else if (s_device_socket) {//Unix socket 模式(s_device_socket 为真)
/*  处理模拟器专用的 qemud socket。
一般 Unix socket 通过 socket_local_client 连接。 */
                if (!strcmp(s_device_path, "/dev/socket/qemud")) {
                    /* Before trying to connect to /dev/socket/qemud (which is
                     * now another "legacy" way of communicating with the
                     * emulator), we will try to connecto to gsm service via
                     * qemu pipe. */
                    char qemuPipe[MAX_QEMU_PIPE_NAME_LENGTH] = "qemud:gsm";
                    if (strncmp(ril_inst_id, "0", MAX_CLIENT_ID_LENGTH)) {
                        strncat(qemuPipe, ril_inst_id ,MAX_QEMU_PIPE_NAME_LENGTH);
                    }
                    RLOGD("qemu pipe name : %s\n", qemuPipe);
                    fd = qemu_pipe_open(qemuPipe);

                    if (fd < 0) {
                        /* Qemu-specific control socket */
                        fd = socket_local_client( "qemud",
                                                  ANDROID_SOCKET_NAMESPACE_RESERVED,
                                                  SOCK_STREAM );
                    RLOGD("qemu pipe name : %d\n", fd);
                        if (fd >= 0 ) {
                            char  answer[2];

                            if ( write(fd, "gsm", 3) != 3 ||
                                 read(fd, answer, 2) != 2 ||
                                 memcmp(answer, "OK", 2) != 0)
                            {
                                close(fd);
                                fd = -1;
                            }
                       }
                    }
                }
                else
                    fd = socket_local_client( s_device_path,
                                            ANDROID_SOCKET_NAMESPACE_FILESYSTEM,
                                            SOCK_STREAM );
            } else if (s_device_path != NULL) {// 串口设备模式(s_device_path != NULL)
 /*打开串口设备(如 /dev/ttyS0)。
对于串口设备,禁用终端回显和规范输入模式(避免 AT 命令回显干扰)。*/
                fd = open (s_device_path, O_RDWR);
                if ( fd >= 0 && !memcmp( s_device_path, "/dev/ttyS", 9 ) ) {
                    /* disable echo on serial ports */
                    struct termios  ios;
                    tcgetattr( fd, &ios );
                    ios.c_lflag = 0;  /* disable ECHO, ICANON, etc... */
                    tcsetattr( fd, TCSANOW, &ios );
                }
            }
            
// 如果打开失败 :打印错误,休眠 10 秒后重试。
            if (fd < 0) {
                perror ("opening AT interface. retrying...");
                sleep(10);
                /* never returns */
            }
        }

//打开成功后的初始化
        s_closed = 0;  //全局标志,表示 AT 通道是否关闭(0 表示开启)。

        ret = at_open(fd, onUnsolicited);
        //初始化 AT 命令处理器,将文件描述符 fd 交给 AT 库,并注册
        //回调(用于处理 Modem 主动上报的 URC)。该函数会创建读取线程或使用事件循环监听 fd。

//若失败,线程退出(return 0)。
        if (ret < 0) {
            RLOGE ("AT error %d on at_open\n", ret);
            return 0;
        }

/* 发送初始化回调
RIL_requestTimedCallback:向 libril 请求一个定时回调,立即执行 initializeCallback(TIMEVAL_0 表示 0 毫秒后)。initializeCallback 通常用于上报 RIL 版本、查询 SIM 状态等初始化操作。
sleep(1):简单等待 1 秒,让初始化回调有机会执行(这是一种简陋的同步,实际工业级实现会使用更可靠的同步机制)。
*/
        RIL_requestTimedCallback(initializeCallback, NULL, &TIMEVAL_0);

        // Give initializeCallback a chance to dispatched, since
        // we don't presently have a cancellation mechanism
        sleep(1);

//等待通道关闭
/*
waitForClose():一个阻塞函数,它等待 AT 读取器关闭(例如串口断开、socket 对端关闭)。当 at_open 的读取线程检测到连接断开时,会调用之前注册的 onATReaderClosed,该回调通常会设置 s_closed 并唤醒 waitForClose。
一旦 waitForClose 返回,循环继续外层 for(;;),重新尝试打开设备,实现自动重连。
*/
        waitForClose();
        RLOGI("Re-opening after close");
    }
}
组件 说明
连接方式 支持 TCP 环回、Unix socket、串口设备三种,由全局变量 s_port、s_device_socket、s_device_path 决定。
重连机制 外层 for(;;) + while (fd<0) 循环,保证 Modem 断开后可自动重连。
AT 处理器 at_open 接管 fd,负责 AT 命令收发和 URC 解析,并回调 onUnsolicited 将主动上报传给 libril。
初始化 initializeCallback 通过 RIL_requestTimedCallback 调度,用于上报 RIL 版本、查询 SIM 状态等。
同步等待 waitForClose 阻塞线程,直到连接断开(如串口挂掉或 socket 关闭)。

这个函数体现了传统嵌入式系统中"守护线程"的典型模式:不断尝试建立连接,成功后运行主逻辑,断开后自动重连,保证服务的持久性。

C 复制代码
TCP 环回

TCP 环回(loopback)指的是使用 127.0.0.1 或 localhost 地址的 TCP 连接。这种连接不经过物理网络接口,而是由操作系统内核直接在内核中转发数据,用于同一台机器上不同进程间的 TCP 通信。

在 mainLoop 代码中,当 s_port > 0 时,调用 socket_loopback_client(s_port, SOCK_STREAM)。这个函数会创建一个 TCP socket,连接到本地环回地址 127.0.0.1 的指定端口。这种方式常用于:
与模拟器中的 Modem 模拟程序通信(例如 QEMU 将 Modem 模拟为一个 TCP 服务)。
调试时用 TCP 环回替代真实串口,方便在开发机上测试 RIL 逻辑。

环回的特点:
速度快(无网络延迟)
不受防火墙影响
支持流式可靠传输(TCP)
Bash 复制代码
传统上 AT 命令通过串口(RS-232)与 Modem 通信。在 Android 设备中,基带处理器与应用处理器之间往往通过物理串口(UART)连接,或使用共享内存(如高通 QMI)等更高效方式。参考实现使用串口是为了兼容性和简单性。

s_device_path:由命令行参数 -d 指定,例如 -d /dev/ttyS0。它可以是任意串口设备节点,如 /dev/ttyS0, /dev/ttyUSB0, /dev/ttyHS0 等。
/dev/ttyS:这是 Linux 内核中标准串口驱动的设备文件前缀。具体映射关系:
硬件 UART 端口(如 SoC 中的 UART1)在 Linux 设备树中注册,对应的驱动(如 8250 串口驱动)会创建设备节点 /dev/ttyS0, /dev/ttyS1 等。
用户空间程序打开 /dev/ttyS0,通过 read/write 系统调用与串口驱动交互;驱动通过内存映射或 DMA 将数据发送到硬件 UART 控制器,进而与外部 Modem 通信。

映射过程:
硬件层:SoC 的 UART 引脚连接到 Modem 的 RX/TX。
内核驱动:例如 drivers/tty/serial/8250_core.c,注册为 ttyS 设备。
设备节点:udev 或 devtmpfs 生成 /dev/ttyS0,权限通常为 root:dialout(在 Android 中可能是 root:radio)。
用户空间:RIL 以 radio 用户身份打开该节点,获得文件描述符。

/dev/ttyS0 就是对第一个串口硬件的软件抽象。

2.3.7.4 onRequest

onRequest 是厂商 RIL 库的核心函数,它实现了 RIL_RadioFunctions 接口中的 onRequest 函数指针。该函数负责处理上层 libril 传递下来的所有主动请求(例如拨号、挂断、获取 SIM 状态等),并根据当前无线状态(sState)和请求类型决定是直接返回错误还是调用具体的 AT 命令处理逻辑。

C 复制代码
/*
request:请求号(如 RIL_REQUEST_DIAL),定义在 ril.h 中。
data:指向特定请求参数的指针,类型取决于 request(例如 RIL_Dial 结构体)。
datalen:参数数据的长度。
t:令牌(RIL_Token),实际是 RequestInfo*,用于异步响应匹配。
p_response 和 err 是局部变量,本函数中未直接使用(可能用于某些 case 中调用 at_send_command后解析响应,但在给出的片段中并未展开)。
*/

static void
onRequest (int request, void *data, size_t datalen, RIL_Token t)
{
    ATResponse *p_response;
    int err;

    RLOGD("onRequest: %s", requestToString(request));

    /* Ignore all requests except RIL_REQUEST_GET_SIM_STATUS
     * when RADIO_STATE_UNAVAILABLE. 早期状态过滤
     */
    if (sState == RADIO_STATE_UNAVAILABLE
        && !(request == RIL_REQUEST_GET_SIM_STATUS || request == RIL_REQUEST_GET_DATA_CALL_PROFILE)
    ) {
        RIL_onRequestComplete(t, RIL_E_RADIO_NOT_AVAILABLE, NULL, 0);
        return;
    }
 /*
RADIO_STATE_UNAVAILABLE:表示 Modem 未就绪(例如飞行模式或初始化未完成)。此时只允许 GET_SIM_STATUS 和 GET_DATA_CALL_PROFILE 两个请求,其他请求直接返回 RIL_E_RADIO_NOT_AVAILABLE。
RADIO_STATE_OFF:表示 Modem 已关闭(通过 AT+CFUN=0 进入低功耗模式)。此时允许的请求多了 RIL_REQUEST_RADIO_POWER(用于重新开启射频),其他同样返回不可用错误。
这种早期过滤避免了在 Modem 不可用时无意义的 AT 命令交互,提高了效率。
 */   
    

    /* Ignore all non-power requests when RADIO_STATE_OFF
     * (except RIL_REQUEST_GET_SIM_STATUS)
     */
    if (sState == RADIO_STATE_OFF
        && !(request == RIL_REQUEST_RADIO_POWER
            || request == RIL_REQUEST_GET_SIM_STATUS
            || request == RIL_REQUEST_GET_DATA_CALL_PROFILE)
    ) {
        RIL_onRequestComplete(t, RIL_E_RADIO_NOT_AVAILABLE, NULL, 0);
        return;
    }

//请求分发(switch 语句)
    switch (request) {
        case RIL_REQUEST_GET_SIM_STATUS: { //获取 SIM 卡状态
            RIL_CardStatus_v6 *p_card_status;
            char *p_buffer;
            int buffer_size;

            int result = getCardStatus(&p_card_status);
            if (result == RIL_E_SUCCESS) {
                p_buffer = (char *)p_card_status;
                buffer_size = sizeof(*p_card_status);
            } else {
                p_buffer = NULL;
                buffer_size = 0;
            }
            RIL_onRequestComplete(t, result, p_buffer, buffer_size);
            freeCardStatus(p_card_status);
            break;
        }
/*
调用 getCardStatus(&p_card_status) 获取 SIM 卡状态(通过 AT 命令如 AT+CRSM、AT+CIMI等)。
若成功,将 p_card_status 指针和大小作为响应数据返回;否则返回空数据。
最后释放 p_card_status(由 getCardStatus 动态分配)。
*/       
        case RIL_REQUEST_GET_CURRENT_CALLS://获取当前通话列表
            requestGetCurrentCalls(data, datalen, t);
            break;
/*调用辅助函数 requestGetCurrentCalls,该函数会发送 AT+CLCC 命令查询当前通话列表,并调用 RIL_onRequestComplete 返回结果。*/   
        
        case RIL_REQUEST_DIAL:  //拨号
            requestDial(data, datalen, t);
            break;
//调用 requestDial,解析 RIL_Dial 结构体中的电话号码和 CLIR 等参数,发送 ATD 命令。
           
        case RIL_REQUEST_HANGUP:  //挂断
            requestHangup(data, datalen, t);
            break;
 //调用 requestHangup,解析挂断参数(通常是通话 ID),发送 AT+CHLD=1x 或 AT+CHUP 命令。       
            
        case RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND:  //挂断等待或背景通话
            // 3GPP 22.030 6.5.5
            // "Releases all held calls or sets User Determined User Busy
            //  (UDUB) for a waiting call."
            at_send_command("AT+CHLD=0", NULL);

            /* success or failure is ignored by the upper layer here.
               it will call GET_CURRENT_CALLS and determine success that way */
            RIL_onRequestComplete(t, RIL_E_SUCCESS, NULL, 0);
            break;
/*直接发送 AT+CHLD=0 命令(释放所有保持的通话或设置用户忙)。
立即返回成功(上层不依赖此命令的精确结果,后续会通过 GET_CURRENT_CALLS 同步状态)。*/      
            
            ......
            
            
            default:  //默认情况(不支持的请求)
            RLOGD("Request not supported. Tech: %d",TECH(sMdmInfo));
            RIL_onRequestComplete(t, RIL_E_REQUEST_NOT_SUPPORTED, NULL, 0);
            break;
//对于未实现的请求,返回 RIL_E_REQUEST_NOT_SUPPORTED。            
    }
}
  • 状态机过滤:根据 sState 提前拒绝无效请求,避免无效 AT 命令交互。

  • 同步处理:大多数请求的处理是同步的(发送 AT 命令并等待响应),然后调用 RIL_onRequestComplete返回。

  • 令牌传递:t 作为令牌贯穿始终,最终传给 RIL_onRequestComplete 以匹配请求。

  • 错误处理:对不支持的请求或状态不符的请求,直接返回相应的错误码。

onRequest 是参考 RIL 实现中处理所有上层请求的"总闸"。它通过状态过滤和 switch-case 分发,将不同类型的请求转换为对应的 AT 命令,并通过 RIL_onRequestComplete 将结果(或错误)返回给 libril。这种实现简单直观,适合教学和理解,但现代商用 RIL(如高通 qcril)已采用表驱动、异步消息等更高效的设计。

2.3.7.5 onUnsolicited

onUnsolicited 是 atchannel.c 中定义的静态函数,作为 AT 命令通道的主动上报处理回调。当后台 readerLoop 线程从 Modem 收到一行不以 OK、ERROR 等结尾的主动上报(Unsolicited Response,简称 URC)时,会调用该函数。

该函数的作用:

  • 识别不同类型的 URC 字符串(如 +CRING、RING、%CTZV:、+CMT: 等)。

  • 解析必要的数据(如 NITZ 时间、短信 PDU、网络状态等)。

  • 调用 RIL_onUnsolicitedResponse 将事件封装成 RIL 主动上报,通过 Socket 发送给 RILJ(上层 Java 电话框架)。

C++ 复制代码
/**
 * Called by atchannel when an unsolicited line appears
 * This is called on atchannel's reader thread. AT commands may
 * not be issued here
 * s:读取到的整行 URC 字符串(例如 "+CRING: VOICE" 或 "%CTZV: 16/01/01,00:00:00")。
 * sms_pdu:特殊参数,当收到 +CMT: 时,会携带后续短信 PDU 数据(多行处理的结果)。
 */
static void onUnsolicited (const char *s, const char *sms_pdu)
{
    char *line = NULL, *p;
    int err;

    /* Ignore unsolicited responses until we're initialized.
     * This is OK because the RIL library will poll for initial state
     * 如果当前无线状态为 UNAVAILABLE,则忽略所有主动上报,避免在 Modem 未就绪时处理不完整的数据。
     */
    if (sState == RADIO_STATE_UNAVAILABLE) {
        return;
    }

//识别并处理各种 URC
    if (strStartsWith(s, "%CTZV:")) {
/* TI specific -- NITZ time NITZ 时间(网络时间)
 %CTZV: 是某些 Modem(如 TI 平台)上报 NITZ(网络时间)的特定前缀。
使用 at_tok_nextstr 提取时间字符串(例如 "16/01/01,00:00:00"),然后调用 RIL_onUnsolicitedResponse 上报给上层。*/
        char *response;

        line = p = strdup(s);
        at_tok_start(&p);

        err = at_tok_nextstr(&p, &response);

        free(line);
        if (err != 0) {
            RLOGE("invalid NITZ line %s\n", s);
        } else {
            RIL_onUnsolicitedResponse (
                RIL_UNSOL_NITZ_TIME_RECEIVED,
                response, strlen(response));
        }
    } else if (strStartsWith(s,"+CRING:")//呼叫状态变化
                || strStartsWith(s,"RING")
                || strStartsWith(s,"NO CARRIER")
                || strStartsWith(s,"+CCWA")
    ) {
        RIL_onUnsolicitedResponse (
            RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED,
            NULL, 0);
#ifdef WORKAROUND_FAKE_CGEV
        RIL_requestTimedCallback (onDataCallListChanged, NULL, NULL); //TODO use new function
#endif /* WORKAROUND_FAKE_CGEV */
/*
来电(+CRING: / RING)、挂断(NO CARRIER)、呼叫等待(+CCWA)等都会触发通话状态变化。
上报 RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED,不带额外数据(上层会通过 RIL_REQUEST_GET_CURRENT_CALLS 获取具体通话列表)。
*/
    } else if (strStartsWith(s,"+CREG:")//网络注册状态变化
                || strStartsWith(s,"+CGREG:")
    ) {
        RIL_onUnsolicitedResponse (
            RIL_UNSOL_RESPONSE_VOICE_NETWORK_STATE_CHANGED,
            NULL, 0);
#ifdef WORKAROUND_FAKE_CGEV
        RIL_requestTimedCallback (onDataCallListChanged, NULL, NULL);
#endif /* WORKAROUND_FAKE_CGEV */
/*
+CREG:(电路交换网络注册)、+CGREG:(GPRS/数据网络注册)表明网络状态改变。
上报 RIL_UNSOL_RESPONSE_VOICE_NETWORK_STATE_CHANGED,上层会重新查询网络信息。
*/
    } else if (strStartsWith(s, "+CMT:")) { //新短信
        RIL_onUnsolicitedResponse (
            RIL_UNSOL_RESPONSE_NEW_SMS,
            sms_pdu, strlen(sms_pdu));
 /*
 +CMT: 表示收到新短信。sms_pdu 参数包含了短信的 PDU 数据(由 readerLoop 在多行读取后传递过来)。
上报 RIL_UNSOL_RESPONSE_NEW_SMS,携带 PDU 数据。
 */           
    } else if (strStartsWith(s, "+CDS:")) {
    
    ......
    
    }
2.3.7.5.1 结构图
  • 执行上下文:该函数在 readerLoop 线程中被调用,因此不能在其中执行可能阻塞的 AT 命令(避免死锁)。

  • 数据转换:将 Modem 上报的文本字符串转换为 RIL 定义的主动上报事件 ID 和二进制数据(如 NITZ 字符串、短信 PDU)。

  • 上层通信:通过 RIL_onUnsolicitedResponse 将事件传递给 libril.so,最终通过 Socket 发送给 RILJ。

  • 兼容性:支持多种 URC 格式(TI 的 %CTZV:、标准 GSM 的 +CRING、+CMT 等),体现了参考实现的历史兼容性。

  • 工作区宏:WORKAROUND_FAKE_CGEV 是一些历史平台上的特殊处理,用于主动触发数据通话列表更新。

这个函数是 RIL 将底层 AT 消息转化为上层 Java 框架所能理解的事件的关键"翻译器"。

2.3.7.5.2 onUnsolicited调用流程
C 复制代码
在 reference-ril 的 atchannel.c 实现中,onUnsolicited 函数是在 readerLoop 线程里被调用的。
调用路径是 readerLoop -> processLine -> handleUnsolicited -> s_unsolHandler,
而 s_unsolHandler 这个函数指针在 at_open 初始化时,就被设置成了 onUnsolicited。

第1步:readerLoop 线程从串口读取一行数据。

JavaScript 复制代码
static void *readerLoop(void *arg) {
    for (;;) {
        line = readline();              // ① 从串口读取一行
        processLine(line);              // ② 交给 processLine 处理
    }
}

第2步:processLine 通过 sp_response 判断是请求的回复,还是Modem主动上报(Unsolicited)。

C++ 复制代码
static void processLine(const char *line) {
    if (sp_response == NULL) {          // ③ 没有正在等待的命令,说明是主动上报
        handleUnsolicited(line);
    } else {
        // ... 处理命令回复的逻辑 ...
    }
}

第3步:handleUnsolicited 调用在 at_open 时注册的回调函数。

C++ 复制代码
static void handleUnsolicited(const char *line) {
    if (s_unsolHandler != NULL) {       // ⑤ s_unsolHandler 就是 onUnsolicited
        s_unsolHandler(line, NULL);     // ⑥ 调用 onUnsolicited
    }
}

第4步:在 at_open 函数中,s_unsolHandler 被赋值为参数 h。

C 复制代码
int at_open(int fd, ATUnsolHandler h) {
    s_fd = fd;
    s_unsolHandler = h;                // ⑦ 保存主动上报处理函数
    // ... 创建 readerLoop 线程 ...
}

第5步:onUnsolicited 是在 reference-ril.c 中定义的、符合 ATUnsolHandler 类型的回调函数。

C++ 复制代码
static void onUnsolicited (const char *s, const char *sms_pdu) {
    // ⑧ 区分主动上报消息的类型(如 +CRING, %CTZV: 等)
    // ⑨ 调用 RIL_onUnsolicitedResponse 上报给上层
}

第6步:在 reference-ril.c 的 mainLoop 函数中,初始化 AT 通道时注册了 onUnsolicited。

JavaScript 复制代码
static void * mainLoop(void *param) {
    // ...
    at_open(fd, onUnsolicited);        // ⑩ 将 onUnsolicited 注册为主动上报处理函数
    // ...
}
C 复制代码
RIL.java (RILJ)
   ↑↓ socket
rild main
   ↓
RIL_register
   ↓
reference-ril.c: onRequest
   ↓
reference-ril.c: mainLoop
   ↓ (at_open)
atchannel.c: at_open  // 注册 onUnsolicited 为 s_unsolHandler
   ↓ (pthread_create)
atchannel.c: readerLoop  // 在子线程中循环
   ↓ (readline)
readline()
   ↓ (processLine)
processLine()
   ↓
handleUnsolicited()
   ↓ (s_unsolHandler)
reference-ril.c: onUnsolicited
   ↓ (RIL_onUnsolicitedResponse)
ril.cpp: RIL_onUnsolicitedResponse
   ↓ (sendResponse)

2.3.7.6 atchannel.c

AT Channel专门负责 AT 命令的收发。

这个文件是 reference-ril(Google 提供的 RIL 参考实现)的一部分,其核心是构建了一个基于生产者-消费者模型的线程安全通道。上层业务通过它发送AT命令,同时一个专门的 readerLoop 线程负责接收Modem的回复和主动上报。

2.3.7.6.1 核心数据结构
数据结构 作用
ATResponse 存储AT命令执行结果的结构体,包含成功标志和回复字符串等。
s_fd 全局变量,存储与Modem通信的文件描述符(如串口fd)。
s_ATBuffer / s_ATBufferCur 静态缓冲区,用于高效地读取来自Modem的字节流。
s_reader_pid 线程ID,用于管理专门读取Modem数据的线程。
s_unsolHandler 函数指针,指向处理Modem主动上报的回调函数。
2.3.7.6.2 at_open

at_open 是 atchannel.c 中最重要的初始化函数之一。它的作用是:打开一个与 Modem 通信的 AT 命令通道,并启动一个后台读取线程(readerLoop),用于持续接收 Modem 的响应和主动上报。

C 复制代码
/**
 * Starts AT handler on stream "fd'
 * returns 0 on success, -1 on error
 * fd:已经打开的、指向通信设备(串口、socket 等)的文件描述符。
 * h:处理主动上报(Unsolicited Response)的回调函数指针,类型 ATUnsolHandler
 *(例如    onUnsolicited)。
 * 返回值:0 表示成功,-1 表示失败。
 */
int at_open(int fd, ATUnsolHandler h)
{
    int ret;
    pthread_t tid;
    pthread_attr_t attr;

    s_fd = fd;  //保存通信文件描述符(全局变量,供其他 AT 函数使用)。
    s_unsolHandler = h;  //保存主动上报回调函数,当 reader 线程收到 URC(Unsolicited Result Code)时调用。
    s_readerClosed = 0; //标志位,表示 reader 线程是否已关闭。

    s_responsePrefix = NULL;//用于解析 AT 响应时保存中间状态,这里初始化为 NULL。
    s_smsPDU = NULL;
    sp_response = NULL;

    /* Android power control ioctl Android 电源控制相关(可选)*/
#ifdef HAVE_ANDROID_OS
#ifdef OMAP_CSMI_POWER_CONTROL
    ret = ioctl(fd, OMAP_CSMI_TTY_ENABLE_ACK);
    if(ret == 0) {
        int ack_count;
                int read_count;
        int old_flags;
        char sync_buf[256];
        old_flags = fcntl(fd, F_GETFL, 0);
        fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);
        do {
            ioctl(fd, OMAP_CSMI_TTY_READ_UNACKED, &ack_count);
                        read_count = 0;
            do {
                ret = read(fd, sync_buf, sizeof(sync_buf));
                                if(ret > 0)
                                        read_count += ret;
            } while(ret > 0 || (ret < 0 && errno == EINTR));
            ioctl(fd, OMAP_CSMI_TTY_ACK, &ack_count);
         } while(ack_count > 0 || read_count > 0);
        fcntl(fd, F_SETFL, old_flags);
        s_readCount = 0;
        s_ackPowerIoctl = 1;
    }
    else
        s_ackPowerIoctl = 0;

#else // OMAP_CSMI_POWER_CONTROL
    s_ackPowerIoctl = 0;

#endif // OMAP_CSMI_POWER_CONTROL
#endif /*HAVE_ANDROID_OS*/
/*
该段代码针对老旧的 OMAP 平台(德州仪器芯片)的特殊电源控制机制。通过 ioctl 启用 ACK 并同步数据。
在现代设备(尤其是高通平台)中,这个条件通常未定义,s_ackPowerIoctl 会被设为 0。
核心目的:确保在开启 AT 通道前,串口缓冲区中没有残留的旧数据,避免干扰。
由于这是参考实现中历史遗留的部分,了解即可,不影响主流程理解。
*/

/*创建 reader 线程
初始化线程属性,设置 分离状态(PTHREAD_CREATE_DETACHED),线程终止后自动回收资源。
调用 pthread_create 创建新线程,入口函数为 readerLoop,参数为 &attr(实际上未使用,也可以传递 NULL,但这里传递了属性指针)。
创建失败时返回 -1,成功返回 0。
*/
    pthread_attr_init (&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    ret = pthread_create(&s_tid_reader, &attr, readerLoop, &attr);

    if (ret < 0) {
        perror ("pthread_create");
        return -1;
    }


    return 0;
}
  • 职责分离:at_open 只负责初始化通道并启动后台线程,实际的 AT 命令收发由其他函数(如 at_send_command)和 readerLoop 线程共同完成。

  • 异步架构:通过专用读取线程,避免阻塞主线程,提高了 RIL 的响应能力。

  • 回调注册:将 unsolHandler 保存下来,后续 Modem 主动上报的消息可以由 readerLoop 通过该回调及时传递到上层(如 reference-ril.c 中的 onUnsolicited)。

这个函数是整个 AT 命令通道的"开关"。

2.3.7.6.3 at_send_command/at_send_command_full/at_send_command_full_nolock

这三个函数是 reference-ril 中 AT 命令处理的核心,它们实现了向 Modem 发送 AT 命令并同步等待最终响应的完整机制。

2.3.7.6.3.1 函数层次关系
  • at_send_command 是对外最简单的接口,默认使用 NO_RESULT(只关心最终成功/失败)。

  • at_send_command_full 负责加锁、检查调用线程(不能从 readerLoop 中调用),然后调用 _nolock版本。

  • at_send_command_full_nolock 执行命令发送、条件变量等待、结果收集。

2.3.7.6.3.2 代码分析
C 复制代码
/**
 * Internal send_command implementation
 * Doesn't lock or call the timeout callback
 *
 * timeoutMsec == 0 means infinite timeout
command:AT 命令字符串(不含 \r)。
type:命令类型(NO_RESULT、SINGLELINE、MULTILINE 等),影响响应解析。
responsePrefix:预期响应行的前缀(如 "+CSQ:"),用于匹配中间行。
smspdu:短信 PDU 数据(用于 AT+CMGS 等命令)。
timeoutMsec:超时毫秒数,0 表示无限等待。
pp_outResponse:输出参数,指向 ATResponse 结构体指针,调用者负责释放。
 */

static int at_send_command_full_nolock (const char *command, ATCommandType type,
                    const char *responsePrefix, const char *smspdu,
                    long long timeoutMsec, ATResponse **pp_outResponse)
{
    int err = 0;
#ifndef USE_NP
    struct timespec ts;
#endif /*USE_NP*/

//检查是否有未完成的命令
    if(sp_response != NULL) {
        err = AT_ERROR_COMMAND_PENDING;
        goto error;
    }
//sp_response 是全局指针,非空表示上一个命令尚未完成(命令排队被禁止),直接返回错误。

    err = writeline (command);
    //发送命令 writeline 会将命令字符串加上 \r 后写入 s_fd(串口或 socket)。

    if (err < 0) {
        goto error;
    }

    s_type = type;
    s_responsePrefix = responsePrefix;
    s_smsPDU = smspdu;
    sp_response = at_response_new();
//保存命令类型、前缀、短信 PDU,并创建一个新的 ATResponse 结构体,用于收集中间行和最终响应。

#ifndef USE_NP //准备超时时间(非零超时)
    if (timeoutMsec != 0) {
        setTimespecRelative(&ts, timeoutMsec);
    }
#endif /*USE_NP*/
//将 timeoutMsec 毫秒转换为 struct timespec 的绝对时间,供 pthread_cond_timedwait 使用。

//等待最终响应
    while (sp_response->finalResponse == NULL && s_readerClosed == 0) {
        if (timeoutMsec != 0) {
#ifdef USE_NP
            err = pthread_cond_timeout_np(&s_commandcond, &s_commandmutex, timeoutMsec);
#else
            err = pthread_cond_timedwait(&s_commandcond, &s_commandmutex, &ts);
#endif /*USE_NP*/
        } else {
            err = pthread_cond_wait(&s_commandcond, &s_commandmutex);
        }

        if (err == ETIMEDOUT) {
            err = AT_ERROR_TIMEOUT;
            goto error;
        }
    }
/*
sp_response->finalResponse 由 handleFinalResponse 在收到 OK/ERROR 等最终行时设置。
循环条件:没有最终响应且 reader 未关闭。
如果超时时间非 0,使用带超时的等待;否则无限等待。
被唤醒后,若 err == ETIMEDOUT,则超时退出。
*/   

//处理响应结果
    if (pp_outResponse == NULL) {
        at_response_free(sp_response);
    } else {
        /* line reader stores intermediate responses in reverse order */
        reverseIntermediates(sp_response);
        *pp_outResponse = sp_response;
    }

    sp_response = NULL;
/*
若调用者不需要响应结构体,则立即释放;否则返回给调用者。
reverseIntermediates 将中间行列表反转(因为收集时是逆序添加的)。
*/

//错误处理和清理
    if(s_readerClosed > 0) {
        err = AT_ERROR_CHANNEL_CLOSED;
        goto error;
    }

    err = 0;
error:
    clearPendingCommand();

    return err;
}
/*
若 reader 线程已关闭,返回通道关闭错误。
clearPendingCommand 清空全局状态(sp_response、s_responsePrefix 等)。
*/

/**
 * Internal send_command implementation
 *
 * timeoutMsec == 0 means infinite timeout
 */
static int at_send_command_full (const char *command, ATCommandType type,
                    const char *responsePrefix, const char *smspdu,
                    long long timeoutMsec, ATResponse **pp_outResponse)
{
    int err;

    if (0 != pthread_equal(s_tid_reader, pthread_self())) {
        /* cannot be called from reader thread *不能从 reader 线程调用,防止死锁 **/
        return AT_ERROR_INVALID_THREAD;
    }
    
    //*加锁*
    pthread_mutex_lock(&s_commandmutex);

    err = at_send_command_full_nolock(command, type,
                    responsePrefix, smspdu,
                    timeoutMsec, pp_outResponse);

    pthread_mutex_unlock(&s_commandmutex);

    //* 超时回调*
    if (err == AT_ERROR_TIMEOUT && s_onTimeout != NULL) {
        s_onTimeout();
    }

    return err;
}
/*
线程检查:确保发送命令的线程不是 readerLoop,否则会死锁(因为 readerLoop 可能已经在处理之前的命令,再次等待条件变量会导致无法唤醒)。
加锁:保护全局变量(sp_response 等)的互斥访问。
调用 nolock 版本。
超时回调:若超时,调用注册的 s_onTimeout(例如可用于恢复 Modem 状态)。
*/

/**
 * Issue a single normal AT command with no intermediate response expected
 *
 * "command" should not include \r
 * pp_outResponse can be NULL
 *
 * if non-NULL, the resulting ATResponse * must be eventually freed with
 * at_response_free
 */
int at_send_command (const char *command, ATResponse **pp_outResponse)
{
    int err;

    err = at_send_command_full (command, NO_RESULT, NULL,
                                    NULL, 0, pp_outResponse);

    return err;
}
/*
最简化的接口:使用 NO_RESULT(不解析中间行),无限超时,不处理 SMS PDU。
上层(如 requestDial)通常调用此函数发送简单命令。
*/
2.3.7.6.3.3 图示

at_send_command/at_send_command_full/at_send_command_full_nolock调用关系图

函数 职责 锁状态 线程限制
at_send_command 最简 API,默认参数 通过 _full 加锁 不能是 reader 线程
at_send_command_full 加锁 + 线程检查 + 调用 nolock 加锁 同上
at_send_command_full_nolock 实际发送、条件等待 假定已加锁 无额外限制(但通常由 _full 调用)
  • 同步模型:每个 AT 命令必须等待最终响应(OK/ERROR)后才能发下一个,是典型的命令队列串行化。

  • 条件变量:在 readerLoop 中收到最终行时,会调用 pthread_cond_signal(&s_commandcond) 唤醒等待线程。

  • 超时处理:保证不会无限阻塞,并提供回调通知上层。

  • 线程安全:通过互斥锁保护全局状态,且禁止 reader 线程自己发送命令。

软件结构图

时序图

at_send_command_full 与 readerLoop 线程交互的时序图,展示了 AT 命令从发送到接收最终响应的完整流程。

  • 加锁与解锁:at_send_command_full 负责加锁,_nolock 版本在条件变量等待时会释放锁(pthread_cond_wait 原子性释放锁并阻塞),被唤醒后重新获取锁。

  • 线程隔离:命令发送和等待在调用者线程中执行;串口读取和 URC 处理在readerLoop 线程中执行。

  • 唤醒机制:当收到最终响应(OK/ERROR)时,handleFinalResponse 通过 pthread_cond_signal 唤醒等待中的业务线程。

  • 超时处理(未在图中画出):若设置了超时且超时先到,pthread_cond_timedwait 返回 ETIMEDOUT,业务线程会直接返回超时错误,不会等待 Modem 响应。

2.3.7.6.4 消息解析与分发 (Message Parsing & Dispatching)
2.3.7.6.4.1 readerLoop

readerLoop 是 AT 命令通道中独立运行的后台线程函数,由 at_open 创建并启动。它的核心职责是:持续从 Modem 串口(或 socket)读取数据,并对每一行数据进行分类处理:将命令响应交给 processLine 处理,将特殊的多行短信(+CMT)取出第二行 PDU 数据后一并交给 s_unsolHandler 回调。

该函数是 AT 命令通道中响应接收与主动上报的驱动引擎,确保 Modem 的消息能够被及时处理。

C++ 复制代码
static void *readerLoop(void *arg)
{
    for (;;) {
        const char * line;

        line = readline();       *//读取一行*
/*readline() 从 s_fd(串口或 socket)读取一行数据,以 \n 或 \r\n 结尾,返回动态分配的字符串(内部缓冲区复用)。
如果返回 NULL,表示串口已关闭或读取错误,线程退出。*/

        if (line == NULL) {      //*连接关闭则退出线程*
            break;
        }
//跳出循环,随后调用 onReaderClosed() 通知上层(reference-ril.c 中的 waitForClose 被唤醒)。


/*
isSMSUnsolicited(line) 检查 line 是否以 "+CMT:" 开头(新短信主动上报)。
AT 规范中,+CMT: 后面会紧跟一行短信 PDU 数据。因此需要额外再读取一行。
*/
        if(isSMSUnsolicited(line)) {  //*判断是否为 "+CMT:" 开头 *
        //*特殊处理:读取第二行 PDU 数据*
 /*
 line1 需要 strdup 复制,因为下一次 readline 会覆盖内部缓冲区。
line2 是直接返回的指针,在下次 readline 前有效,但 s_unsolHandler 会立即使用并复制(在 onUnsolicited 中会复制 PDU)。
s_unsolHandler 是在 at_open 时注册的 onUnsolicited 回调。
 */       
            char *line1;
            const char *line2;

            // The scope of string returned by 'readline()' is valid only
            // till next call to 'readline()' hence making a copy of line
            // before calling readline again.
            line1 = strdup(line); *// 复制第一行(因为下次 readline 会覆盖缓冲区)*
            line2 = readline();

            if (line2 == NULL) {
                break;
            }

            if (s_unsolHandler != NULL) {
                s_unsolHandler (line1, line2);  //*回调处理(含 PDU):传递给上层,line1 是第一行,line2 是 PDU*
            }
            free(line1);
        } else {
            processLine(line);        //*普通行处理*
        }
/*
非短信上报的行(包括命令响应、其他 URC)交给 processLine 处理。
processLine 根据当前是否有等待的命令(sp_response != NULL)来决定是作为命令响应还是主动上报。
*/        

#ifdef HAVE_ANDROID_OS。//*老旧的 OMAP 电源管理 ACK(通常忽略)*
        if (s_ackPowerIoctl > 0) {
            /* acknowledge that bytes have been read and processed */
            ioctl(s_fd, OMAP_CSMI_TTY_ACK, &s_readCount);
            s_readCount = 0;
        }
#endif /*HAVE_ANDROID_OS*/
    }

    onReaderClosed(); //*连接关闭回调*
/*
当 readline 返回 NULL 或发生错误时,线程退出循环,调用 onReaderClosed()。
该回调在 at_open 中通过 at_set_on_reader_closed(onATReaderClosed) 设置,最终会唤醒 mainLoop 中的 waitForClose(),触发重连。
*/
    return NULL;
}
特性 说明
独立线程 线程在 at_open 中创建,与业务线程分离,保证实时读取 Modem 数据。
行读取 使用 readline() 逐行读取,每行以 \n 结束。
短信特殊处理 +CMT: 是两行结构(第一行头,第二行 PDU),需要额外读取。
普通行分发 交给 processLine 区分命令响应或主动上报。
错误/关闭处理 读取失败时退出循环,调用 onReaderClosed() 触发重连机制。
遗留代码 OMAP ACK 部分仅对老平台有效,不影响主线逻辑。

这个函数是参考 RIL 实现中 AT 通道的数据接收中枢,它将串口读取、数据分类、回调分发等职责集中管理。

2.3.7.6.4.2 processLine

processLine 是 AT 命令通道中处理从 Modem 读取的每一行数据的核心函数。它由 readerLoop 线程调用,负责根据当前状态(是否有正在等待的命令、命令类型等)将一行数据分类处理:

  • 主动上报(Unsolicited):没有等待命令时,或不符合预期格式时,交给 handleUnsolicited。

  • 最终响应(Final Response):OK 或 ERROR,结束命令等待并唤醒业务线程。

  • 中间响应(Intermediate Response):符合预期前缀或数字格式的响应行,添加到 ATResponse 中。

  • 特殊命令提示符:如 "> "(短信发送提示),自动写入 PDU 数据。

C++ 复制代码
static void processLine(const char *line)
{
    pthread_mutex_lock(&s_commandmutex);//*加锁保护全局状态*
  //保护全局变量 sp_response、s_type、s_responsePrefix、s_smsPDU 的并发访问。  

//无等待命令(主动上报):如果 sp_response == NULL,表示当前没有正在等待响应的 AT 命令,那么这一行必定是 Modem 主动上报(URC),直接交给 handleUnsolicited。
    if (sp_response == NULL) {
        /* no command pending */
        handleUnsolicited(line);
    } else if (isFinalResponseSuccess(line)) {//检查行是否以 "OK" 开头
        sp_response->success = 1;
        handleFinalResponse(line);
    } else if (isFinalResponseError(line)) {//检查行是否以 "ERROR"或"+CME ERROR" 等开头
        sp_response->success = 0;
        handleFinalResponse(line);//设置 success 标志,调用 handleFinalResponse(line),该函数会保存最终响应字符串并调用 pthread_cond_signal 唤醒等待中的业务线程。
    } else if (s_smsPDU != NULL && 0 == strcmp(line, "> ")) {
        // See eg. TS 27.005 4.3
        // Commands like AT+CMGS have a "> " prompt  短信命令提示符("> ")
        writeCtrlZ(s_smsPDU);
        s_smsPDU = NULL;
/*当发送 AT+CMGS 等短信命令时,Modem 会返回 "> " 提示输入 PDU 数据。
如果 s_smsPDU 不为空,则调用 writeCtrlZ 将 PDU 数据写入(以 Ctrl+Z 结束),然后清空 s_smsPDU。
*/
    } else switch (s_type) {//根据命令类型处理中间响应
        case NO_RESULT://不期望任何中间响应,所有其他行都视为主动上报。
            handleUnsolicited(line);
            break;
        case NUMERIC:
/*期望一个数字开头的中间响应(如信号强度 +CSQ: 20,99,数字为 2?实际 AT 命令中 +CSQ: 以 + 开头,但此类型通常用于 AT+CMGS 等返回数字引用,这里以行首是否为数字判断)。
如果还没有添加过中间响应且行首是数字,则添加;否则视为主动上报。*/
            if (sp_response->p_intermediates == NULL
                && isdigit(line[0])
            ) {
                addIntermediate(line);
            } else {
                /* either we already have an intermediate response or
                   the line doesn't begin with a digit */
                handleUnsolicited(line);
            }
            break;
        case SINGLELINE:
/*期望一行以指定前缀(如 +CSQ:)开头的中间响应。
如果尚未添加且匹配前缀,则添加;否则视为主动上报。*/
            if (sp_response->p_intermediates == NULL
                && strStartsWith (line, s_responsePrefix)
            ) {
                addIntermediate(line);
            } else {
                /* we already have an intermediate response */
                handleUnsolicited(line);
            }
            break;
        case MULTILINE:
        //期望多行都以指定前缀开头,所有匹配行都添加为中间响应;不匹配的行视为主动上报。
            if (strStartsWith (line, s_responsePrefix)) {
                addIntermediate(line);
            } else {
                handleUnsolicited(line);
            }
        break;

        default: /* this should never be reached */
            RLOGE("Unsupported AT command type %d\n", s_type);
            handleUnsolicited(line);
        break;
    }

    pthread_mutex_unlock(&s_commandmutex);
}
场景 处理
无等待命令 视为主动上报,调用 handleUnsolicited
收到 OK / ERROR 设置成功标志,调用 handleFinalResponse 唤醒业务线程
收到 "> " 提示 如果有待发送的 PDU(短信),立即发送
中间响应 根据命令类型(NUMERIC, SINGLELINE, MULTILINE)判断是否匹配预期,匹配则添加到响应结构体;不匹配则视为主动上报
NO_RESULT 类型 任何非最终响应的行都视为主动上报

这个函数是 AT 命令响应解析的核心状态机,通过全局状态 sp_response、s_type、s_responsePrefix和 s_smsPDU 来区分命令回复和主动上报,确保上层业务线程能正确获得期望的响应数据,同时及时处理 Modem 主动上报的事件。

2.3.7.6.4.3 图示

时序图(完整AT命令交互流程)

atchannel.c 与 RIL 框架的交互流程

atchannel.c 在整个 RIL 架构中,是一个承上启下的关键模块。它通过精心设计的线程模型和状态机,将复杂的AT命令收发和管理,封装成一套简洁的API,为上层业务提供了一个稳定可靠的AT通信通道。

在更高通(Qualcomm)这种现代商用平台中,atchannel.c 已被更高效的 QMI (Qualcomm MSM Interface) 协议所取代,但它所体现的设计思想,至今仍是许多通讯模块的基石

2.3.8 高通特有组件

2.3.8.1 QCRIL

QCRIL(Qualcomm Communication Radio Interface Layer)是高通公司为Android系统实现的一套RIL(Radio Interface Layer)解决方案。它本质上是连接Android Telephony框架与高通BP(Baseband Processor,基带处理器)的软件中枢。

如果说RIL是Android为了屏蔽不同Modem硬件差异而定义的"标准接口",那么QCRIL就是高通对这个接口的"具体实现"。它的核心设计哲学可以概括为"模块化架构、表驱动分发、异步高效通信"。

2.3.8.1.1 软件结构

QCRIL的软件架构围绕"高效处理请求"和"与Modem通信"两个核心目标设计,主要包含以下模块:

  • 初始化与核心框架:通过 RIL_Init 函数作为入口,创建 qcril_event_main 事件循环线程。在此过程中,它会初始化 qcril_hash_table,这个表是请求分发的核心,存储了所有RIL请求ID与对应处理函数的映射关系。

  • 请求处理与分发:当上层请求到来时,onRequest 函数会通过查询 qcril_hash_table,将请求快速分发给对应的业务模块(如qcril_uim_request_get_sim_status)进行处理。

  • 核心业务模块:为了应对电话、数据、SIM卡等复杂业务,QCRIL内部进行了模块化拆分。主要包括:

    • Voice Module:处理呼叫控制、通话状态等。

    • Data Module:管理数据连接建立、维护和断开。

    • UIM Module:负责SIM卡状态、STK菜单等。

    • NAS Module:处理网络注册、信号强度等。

  • 通信与协议栈:这是QCRIL与BP通信的关键。它使用高通独有的 QMI(Qualcomm Message Interface) 协议。通过 QMUX(QMI Multiplexer) 在单一物理通道上复用出多个逻辑通道,并由 QCCI/QCSI 接口负责消息的封装与解封装。

2.3.8.1.2 设计哲学:分而治之,表驱动,异步高效
  1. 管理复杂度:分而治之与模块化

    • QCRIL通过将庞大、复杂的RIL请求,按照业务类型(语音、数据、SIM等)划分给不同的专职模块处理。这使得代码结构清晰,降低了耦合,也便于团队分工和维护。
  2. 快速路由请求:表驱动(Table-Driven)

    • QCRIL没有使用冗长的 switch-case 语句,而是采用 qcril_hash_table 这个静态映射表。当请求到来时,它就像一个高效的"路由表",通过查表直接定位到对应的处理函数,这种设计提升了代码的可读性和扩展性。
  3. 保证通信效率:异步与QMI

    • 相比传统的、基于文本且需等待回复的AT命令,QCRIL采用QMI协议进行通信。QMI是一种二进制、异步的协议。这意味着AP可以连续发送多个请求而无需同步等待,Modem处理完后会异步返回结果,极大提升了通信效率。同时,QMI基于共享内存的通信机制也比串口更快。
2.3.8.1.3 整体架构图

从整体架构上看,QCRIL作为承上启下的关键枢纽,向上对接libril.so的标准接口,向下通过QMI框架与BP通信。其模块化的设计使得各部分可以独立演进,而表驱动和异步通信机制则保证了系统的高效与可扩展性。

2.3.8.2 QMI

QMI(Qualcomm Messaging Interface)是高通为移动设备非对称多核(AP+BP)架构设计的高效进程间通信(IPC)协议框架。

应用处理器(AP)和 基带处理器(BP/Modem)

它的出现,是为了解决传统AT命令效率低、扩展性差等问题,其设计哲学是构建一条高效、灵活、可靠的"内部通信总线"。

QMI:高通提供的标准化接口,封装通信协议,包含QMI Framework、QMUX(消息队列复用)等组件

2.3.8.2.1 设计哲学与软件架构

QMI的设计,借鉴了TCP/IP的分层思想,但针对芯片内部通信进行了极致优化。

  • 分层架构:QMI是一个清晰的分层架构,每层各司其职,共同屏蔽底层复杂性。

  • 高效通信:采用二进制+TLV(Type-Length-Value)格式,相比AT命令的文本解析,数据处理效率极高。

  • 异步与并发:原生支持同步和异步两种模式,并通过QMUX(Qualcomm Multiplexer) 在单一物理通道上复用多个逻辑通道,实现多服务并发。

  • 模块化与可扩展:功能以服务(Service) 为单元模块化,新功能可通过添加新服务扩展,不影响现有架构。

这个图展示了 QMI 架构的核心:AP 和 BP 之间通过单一的物理总线(共享内存/HSIC/PCIe)连接,但在逻辑上通过 QMUX 层复用出多个虚拟通道,分别对应不同的服务(语音、数据、GPS、NAS 等)。每类服务的请求/响应/指示都在各自的逻辑通道中传输,实现了多路 I/O 的效果。

2.3.8.2.2 核心组件
  • 消息模型:基于客户端-服务器(C/S)模型,消息分为三种:

    • Request(请求):客户端发往服务端。

    • Response(响应):服务端对请求的应答。

    • Indication(指示):服务端主动发给客户端的异步消息。

  • 客户端与服务端:

    • QCCI (QMI Common Client Interface):客户端使用的通用API。

    • QCSI (QMI Common Service Interface):服务端使用的通用API。

    • 一个服务可以对应多个客户端,一个客户端只能连接一个服务。

  • 传输层:

    • IPC Router:基于共享内存(SMD) 的高效内部路由,是AP与BP通信的主力。

    • QMUX:在多物理通道上实现多路复用,也支持USB等外部连接。

  • IDL (Interface Definition Language):用于定义服务接口,并通过编译器自动生成代码,保证接口一致性。

  • 核心QMI服务 (Services):

    • CTL (Control Service):核心控制服务。

    • DMS (Device Management Service):设备管理。

    • NAS (Network Access Service):网络接入。

    • WDS (Wireless Data Service):无线数据。

    • UIM (User Identity Module Service):用户识别模块

2.3.8.2.3 工作流程
  1. 初始化:客户端通过CTL服务注册,获取服务句柄。

  2. 请求:客户端调用QCCI API发送Request消息,经IDL编码、QMUX封装后,通过IPC Router发送给BP。

  3. 处理:BP端QMUX解复用、IDL解码后,由对应服务处理。

  4. 响应:服务端发送Response消息,沿原路返回给客户端。

  5. 指示:服务端可随时发送Indication消息,通知客户端状态变化。

2.3.8.2.4 CTL 服务

CTL(Control Service)是 QMI 框架中最基础、最核心的控制服务,负责所有其他服务的注册、发现、客户端分配和生命周期管理。它是 AP 与 BP 之间建立任何 QMI 通信的第一道门槛,类似于计算机网络中的 DNS + 认证服务器。它是QMI 的"服务注册中心"与"认证中心"。

2.3.8.2.4.1 核心功能
  1. 服务注册:BP 侧的每个 QMI 服务(如 NAS、WDS、DMS)启动时,会向 CTL 服务注册其 服务类型(Service Type)、版本号和可用状态。

  2. 客户端分配:AP 侧的客户端在访问某个服务前,必须向 CTL 服务发送 Allocate Client 请求,CTL 会返回一个唯一的 客户端 ID(CID),用于后续所有与该服务的通信。

  3. 服务发现:客户端可以向 CTL 查询当前可用的服务列表。

  4. 服务通告:当 BP 侧服务状态变化(如上线、下线、重启),CTL 会主动向 AP 侧发送 Indication,通知相关客户端。

C 复制代码
解耦:客户端不需要硬编码服务端地址或端口,只需通过 CTL 动态获取。
安全管理:CTL 可以控制哪些客户端有权访问哪些服务(基于策略)。
热插拔/重启:支持服务动态重启或 Modem 重连后,客户端可重新通过 CTL 获取新的服务句柄。
2.3.8.2.4.2 图形分析

CTL 服务在 QMI 整体架构中的位置

CTL 服务工作流程时序图(首次访问服务)

CTL 服务的数据结构

CTL 服务是 QMI 的"总管家",它通过服务注册表和客户端分配机制,让 AP 侧能够动态、安全地访问 BP 侧的各种功能服务。

2.3.8.2.5 QMI结构图和时序图

结构图

总的来说,QMI以其分层架构、二进制协议、异步并发和模块化服务等设计,完美解决了AP-BP间高效通信的难题,是高通移动平台通信软件栈的基石。

时序图

初始化:客户端通过 CTL 服务获取目标服务的句柄,建立通信通道。

同步请求:使用 qmi_client_send_msg_sync,线程会阻塞直到收到响应。事务 ID 用于匹配请求与响应。

异步请求(图中未示):可使用 qmi_client_send_msg_async,响应通过回调返回,支持并发。

指示:服务端主动发送,客户端需预先注册回调函数。

物理传输:实际通过共享内存(SMD)进行零拷贝或低拷贝数据交换,效率极高。

2.3.8.3 QMUX

QMUX(QMI Multiplexing Protocol)是高通QMI框架中的核心复用协议。 可以理解为一个在单一物理通道上,为多个逻辑服务提供独立、高效数据通道的"多路复用器"。它的核心作用可以概括为:逻辑复用、数据封装、QoS保障和跨进程通信。

2.3.8.3.1 核心作用

它主要解决在AP与BP之间单一物理链路(如共享内存、USB等)上,如何同时承载电话、数据、GPS等多种服务的问题。

  • 逻辑复用:核心功能是在一个物理通道上,为不同的QMI服务(如NAS, WDS, UIM等)创建独立的逻辑通道。

  • 数据封装:为上层QMI业务数据添加统一的QMUX头部,形成标准数据帧,以便对端识别和分发。

  • QoS保障:通过给数据包打上优先级标签,确保关键任务(如通话信令)获得优先处理。

  • 跨进程通信:通过qmuxd守护进程管理多个用户空间程序的访问请求。

2.3.8.3.2 消息格式

一个标准的QMUX消息帧包含以下几个部分:

  1. I/F Type (1 byte):接口类型标识,通常为0x01,表示这是一个QMUX消息。

  2. Length (2 bytes):消息长度,表示后续数据的总长度。

  3. Control Flags (1 byte):控制标志,主要使用最高位(bit 7)标识消息方向。

    • bit7 = 0:由控制点 (Client) 发出的请求。

    • bit7 = 1:由服务端 (Service) 发出的响应或指示。

  4. Client ID (1 byte):客户端ID,用于标识不同的控制点。特殊值0xFF表示广播消息。

  5. QMUX SDU (Service Data Unit):实际承载的QMI业务数据,采用TLV(Type-Length-Value) 格式编码。

2.3.8.3.3 工作流程

一次典型的QMUX通信流程如下:

  1. 创建控制点:上层应用为所需服务创建一个控制点(Client),并分配一个Client ID。

  2. 请求与响应:

    • AP发起请求:应用(如RIL)通过控制点发起请求。QMI框架将请求封装并添加QMUX头部后,通过共享内存发送给BP。

    • BP处理并响应:BP侧的QMUX层解包,将数据交给对应的QMI服务处理。处理完成后,BP同样通过QMUX封装响应或主动上报的指示(Indication),发回给AP侧对应的控制点。

2.3.8.3.4 QMUX 在整体架构中的位置
C 复制代码
QMUX 的现代演进:QRTR

在高通更新的平台中,QMUX协议正逐渐被QRTR (Qualcomm IPC Router) 所取代或并存。QRTR是内核级的IPC路由器,性能更强,支持服务的热插拔。但理解QMUX依然是掌握高通平台通信架构的基础。

QMUX是高通平台实现AP与BP高效通信的关键技术。它通过统一的消息格式和灵活的多路复用机制,将单一的物理通道抽象为多个独立的逻辑通道,为上层复杂的通信业务提供了稳定、高效的支撑。

2.3.9 通信机制

2.3.9.1 RILJ与RILD之间的Socket通信

RILJ ↔ RILD:是 Android 系统内的本地进程间通信(本地IPC),走的是 Unix Domain Socket。它的核心目的是解耦,使上层 Java 框架不感知底层 Modem 是哪个厂商的。

Socket名称:/dev/socket/rild(SOCKET_NAME_RIL)

通信模型:RILD创建监听Socket,等待RILJ连接;RILJ通过RILSender线程发送命令,通过RILReceiver线程接收响应

  • 序列化:RILJ 将电话号码等参数打包成 Parcel 对象。

  • 写入 Socket:RILJ 通过 LocalSocket 将数据写入 /dev/socket/rild。

  • 事件唤醒:rild 主进程的 ril_event_loop 通过 select 监听到 s_fdCommand(客户端连接 fd)可读。

  • 接收与分发:processCommandsCallback 读取数据,processCommandBuffer 反序列化 Parcel,得到 RIL_REQUEST_DIAL

Binder

Binder 是 Android 的"灵魂"IPC 机制,Framework 层选择它,主要基于三点核心考量:

  • 卓越的性能:Binder 的数据拷贝只需 一次(从客户端到内核缓冲区,服务端通过 mmap 直接访问),而 Socket 等传统 IPC 需要 两次。这对于性能敏感的移动设备至关重要。

  • 强大的安全性:Binder 驱动会在内核层自动附加调用方的 UID/PID,服务端可据此进行精确的权限校验。这从根本上解决了 Socket 等传统方式身份信息易被伪造的安全隐患。

  • 自然的面向对象编程模型:Binder 支持跨进程的对象引用,将 IPC 调用抽象为 方法调用(RPC),实现了"本地调用即远程调用"的透明感。配合 AIDL 自动生成代码,极大地降低了开发门槛。

核心差异对比

对比维度 Binder Socket
核心定位 Android 专用、高性能 RPC 框架 通用的、面向网络的 IPC 机制
性能 (数据拷贝) 1次,效率极高 2次,开销较大
安全性 强,内核级身份校验 (UID/PID) 弱,依赖上层协议,身份易伪造
编程模型 面向对象,基于方法调用 (RPC) 面向数据流,基于字节流或数据报
开发复杂度 低,AIDL 自动生成大部分代码 高,需自行处理数据序列化、协议解析等

RILD 选择Socket的原因

  • 隔离风险,确保系统稳定性:RILD 作为核心系统服务,其稳定性至关重要。Binder 是一个复杂的框架,若用于 RILD,一旦 Binder 驱动或线程池出现问题,可能会影响整个系统。使用简单的 Socket 通信,可以将 RILD 与复杂的 Binder 框架解耦,即使 RILD 出现问题,也不太可能拖垮整个 Binder 系统。

  • 避免 fork() 死锁风险:rild 由 init 进程启动,而 Android 应用进程由 Zygote 通过 fork()创建。Zygote 使用 Socket 而非 Binder 的一个重要原因就是 fork() 在多线程环境下存在死锁风险。若 Zygote 使用 Binder,其内部的 Binder 线程池和锁状态会被子进程继承,极易引发死锁。RILD 作为系统守护进程,同样需要规避此类风险。

  • 保持轻量和简单:相比 Binder,Socket 的实现更简单、直接,资源占用也更少。对于 RILD 这种功能相对单一、通信模式明确的模块来说,用 Socket 构建一个轻量级的通信管道是更高效的选择。

  • 历史与兼容性原因:RILD 的设计早于 Android 许多上层 Framework 的定型,其核心接口和通信方式在早期就已确定。后续为了保持兼容性和稳定性,继续沿用 Socket 是更稳妥的方案。

Binder和Socket在内核中的数据传输方式

  • Socket的传统"两次拷贝":数据需要在"用户空间1 → 内核空间 → 用户空间2"之间完整地搬运两次。第一次:发送方将数据从自己的用户空间拷贝到内核的Socket缓冲区;第二次:接收方再从内核的Socket缓冲区拷贝到自己的用户空间。每次拷贝都需要CPU参与,对于频繁的IPC会带来不小的性能开销。

  • Binder的"一次拷贝":其核心是巧妙地利用了内存映射(mmap)。通信前,Binder驱动会在内核空间和接收进程的用户空间之间建立一块共享内存映射。发送方只需将数据从自己的用户空间拷贝一次到这块共享内存中,接收方即可通过之前建立的内存映射,直接访问这块内核空间的数据,无需再做拷贝。这就省去了一次数据拷贝和对应的CPU开销。

Framework 层使用 Binder:追求极致的性能、安全性和开发效率,以适应复杂多变的应用层需求。

RILD 等底层服务使用 Socket:优先保证系统的稳定性、隔离性和简洁性,以避免复杂的框架依赖影响系统底座的可靠性。

2.3.9.2 RILD与Modem之间的通信

RILD ↔ Modem:这是 AP(应用处理器)与 BP(基带处理器)之间的跨芯片通信。在量产手机上,它走的是共享内存(SMD)+ QMI 协议;在参考代码中,它走的是 UART 串口 + AT 命令。

物理连接方式:

  • AP与BP通信方式一(集成芯片):高通平台AP与BP集成在同一芯片,通过共享内存(Shared Memory)方式实现高速IPC通信

  • AP与BP通信方式二(独立芯片):采用字符设备(Character Devices)+ AT命令方式,通过串口/USB模拟串口进行通信

软件实现:

模式 A:参考代码(AOSP reference-ril)教学模式

  • 调用:dispatchDial 调用 s_callbacks.onRequest,进入 reference-ril.c。

  • 发送 AT:requestDial -> at_send_command("ATD...") -> writeline 写入 /dev/ttyS0(串口)。

  • Modem 响应:Modem 通过串口返回 OK 或 CONNECT。

  • 读取:readerLoop 线程从串口读到数据,processLine 识别到 OK,pthread_cond_signal 唤醒等待线程。

模式 B:高通量产模式(QMI)

  • 调用:dispatchDial 调用高通厂商库 qcril.so 的 onRequest。

  • 封装 QMI:qcril 将拨号参数填入 QMI 消息(TLV格式),调用 QCCI 接口。

  • 多路复用:qmuxd 或内核 QMUX 为消息添加 QMUX 头部,分配 Client ID。

  • 物理传输:通过 SMD(共享内存驱动) 将数据拷贝到 AP-BP 共享内存区域,并触发 BP 中断。

  • BP 处理:BP 侧的 QCSI 服务解包,交给语音服务,最终通过射频发出。

2.3.9.3 时序图

从 Java 层拨号,到 AT/QMI 指令发出,再返回结果的完整流转。

2.3.9.4 架构设计哲学:分层

对比维度 RILJ ↔ RILD (本地 IPC) RILD ↔ Modem (跨芯片通信)
物理介质 内存中的 Unix Socket (抽象命名空间) 量产:共享内存(SMD)/HSIC/PCIe 教学:物理串口 UART
通信协议 Android Parcel 序列化 + 自定义 RIL 命令 量产:QMI 二进制 TLV 教学:AT 文本命令
数据传输单位 完整的 RIL 命令帧 (request/token) QMI 消息帧 或 AT 命令行
主要驱动方 Linux Socket 协议栈 量产:SMD 驱动 + QMUX 教学:TTY 串口驱动
流量特点 短连接持久化、低频高内聚 量产:高速多路复用 教学:低速问答式
故障影响 RILJ 崩溃 -> 重连 Modem 复位 -> 需重新初始化 QMI/AT
  1. 标准化与私有化的分离:RILJ ↔ RILD 使用标准的 Android 协议,全球通用;RILD ↔ Modem 交由厂商(高通/MTK)私有实现,兼顾性能与保密。

  2. 同步与异步的适配:上层业务通常是异步(UI 不能卡),RILD 通过 Socket + 事件循环完美支撑;底层 Modem 可能是同步串口,RILD 通过 readerLoop 线程将同步转化为异步事件。

  3. 共享内存的效率:相比串口,共享内存极大减少了数据拷贝次数和 CPU 中断开销,这是 5G 高吞吐量的基石。

2.3.10 消息传递机制

  • 消息类型:Solicited Commands(请求-响应型)和Unsolicited Responses(主动上报型)

2.3.10.1 消息类型

在 RIL 架构中,消息根据流向和触发方式,被严格分为两种类型:

类型 方向 触发方 典型例子
主动请求 (Solicited Commands) AP → Modem RILJ(上层应用发起) 拨号、发短信、查询信号强度、挂断电话
主动上报 (Unsolicited Responses) Modem → AP Modem(硬件主动上报) 来电通知、信号强度变化、网络注册状态变化、短信到达

核心交互逻辑:

  1. 请求-响应(同步/异步):RILJ 发送一个请求(如 RIL_REQUEST_DIAL),并携带一个 token。RILD 处理完毕后,必须携带相同的 token 通过 RIL_onRequestComplete 返回结果,上层才能将响应与请求匹配。

  2. 主动上报(事件驱动):Modem 任何时候发生状态变化(如来电),都会通过 RIL_onUnsolicitedResponse 主动推送给 RILJ,不需要上层事先请求。

2.3.10.2 EventLoop机制

RILD 的消息处理不是靠多线程轮询,而是靠一个单线程的事件循环(EventLoop) 驱动的。这个 EventLoop 基于 select I/O 多路复用模型,在 ril_event.cpp 中实现。

2.3.10.2.1 核心原理
  • 将所有需要监听的文件描述符(fd)(如 Socket 的 s_fdListen/s_fdCommand、串口 fd、管道唤醒 fd)统一注册到 watch_table。

  • ril_event_loop 线程在 select 系统调用上阻塞,等待任一 fd 可读或定时器超时。

  • 一旦某个 fd 有数据到达,select 返回,EventLoop 调用对应的回调函数(如 listenCallback、processCommandsCallback、readerLoop 中的回调)。

2.3.10.2.2 为什么用 select 而非多线程?
  • 资源节省:一个线程即可管理所有 I/O 事件,无需为每个连接或串口创建线程。

  • 状态一致:避免复杂的锁竞争,所有消息处理在单线程中串行化,简化了状态管理。

  • 可预测性:事件驱动模型响应时间稳定,适合实时性要求较高的通信场景。

2.3.10.3 多路复用(MUX)

2.3.10.3.1 上层通信通道的复用(RILJ ↔ RILD)
  • s_fdListen:监听 Socket,仅用于接受 RILJ 的首次连接。连接成功后,s_fdListen 从事件循环移除,不再参与数据收发。

  • s_fdCommand:业务 Socket,用于所有实际请求与响应的传输。它是唯一的业务通道,但通过 token 机制复用,可以同时处理多个未完成的请求(异步并发)。

2.3.10.3.2 底层硬件通信的复用(RILD ↔ Modem)
  • 参考实现(AT 命令):通过 readerLoop 线程在同一个串口 fd 上循环读取,通过 sp_response 状态机区分"命令回复"与"主动上报",实现了同一个串口 fd 在时间上的复用。

  • 量产实现(QMI/CMUX):高通 QMI 通过 QMUX(Qualcomm Multiplexer) 在一个物理通道(共享内存)上,复用出多个逻辑通道(如 Voice、Data、GPS),每个逻辑通道有自己的 Client ID 和消息流,互不干扰。

这种设计的核心目的:避免因为某个慢速操作(如一个 AT 命令等待响应)而阻塞整个通信链路,保证紧急事件(如来电)可以优先处理。

2.3.10.3.3 结构图

消息传递全流程综合图(含数据流与线程模型):将消息类型、EventLoop、多路复用三者融为一体,展示了从 RILJ 到 Modem 的完整路径。

2.3.10.3.4 设计哲学
  • 同步请求,异步响应:上层发送请求后立即返回,不阻塞 UI;结果通过回调或事件通知,由 token 匹配。

  • 单线程事件驱动:RILD 主处理流程(processCommandBuffer → dispatch → onRequest)在单一线程中顺序执行,避免锁竞争;耗时的 Modem 读写由 readerLoop 线程处理,通过条件变量同步。

  • 协议栈分层清晰:

    • RILJ ↔ RILD:Android 标准 RIL 协议(Parcel 序列化)

    • RILD ↔ Vendor RIL:s_callbacks 函数表(解耦框架与厂商实现)

    • Vendor RIL ↔ Modem:厂商私有协议(AT / QMI),由厂商库封装

  • 多路复用思想贯穿始终:无论是 Socket 上的请求并发(通过 token),还是底层物理通道的逻辑复用(QMI/CMUX),都体现了"将有限资源虚拟化为多个独立通道"的系统设计智慧。

嵌入式通信系统架构的通用范式:如何用最少的资源(单线程 + select),处理最复杂的并发消息流。

2.3.11 TTY子系统

TTY(Teletypewriter,电传打字机)是 Linux 内核为了统一管理各类"终端"设备而设计的框架。它通过分层(核心层、线路规程、驱动层)和抽象(串口、控制台、伪终端)的设计,为上层应用提供了统一的字符设备接口。它是对整个"终端 I/O"的抽象。

在 RIL 架构中,无论参考实现使用 /dev/ttyS0(串口),还是通过伪终端(PTY)与模拟器通信,都离不开 TTY 子系统。

C 复制代码
"TTY"是 "Teletypewriter"(电传打字机)的缩写。在早期的大型计算机时代,它并没有自己的显示器和键盘。人们需要使用一种类似电报机的设备,通过串行电缆连接到计算机,用它来输入命令并打印输出结果。这台设备就是电传打字机,这也是计算机终端的最初形态。

随着技术演进,电传打字机被 "终端"(Terminal)取代,即带有键盘和显示器的专用设备,随后又被个人电脑的软件模拟所替代。但为了兼容性,Linux 内核一直沿用并发展了这套驱动模型,并统称为 TTY 子系统。

2.3.11.1 分层架构

TTY 设计哲学的核心是"分层与策略分离"。它不像普通驱动那样直接处理硬件,而是将"数据如何传输"(驱动层)与"数据如何被处理"(线路规程)彻底解耦。

2.3.11.2 核心层 (Core)

它是 TTY 子系统的核心管理层,负责提供一个抽象的、统一的接口规范。它定义了驱动必须遵循的规则和数据交换格式。无论是窗口、控制台还是伪终端,都必须通过 TTY 核心层来注册和操作。具体而言,它通过 file_operations 结构体定义了 open、read、write 等标准 API,用户空间程序可以通过这些接口,以操作普通文件的方式来与各种不同的终端设备进行交互。

2.3.11.3 线路规程层 (Line Discipline)

这是 TTY 子系统最精巧的部分,它就像一个"中间件"或"过滤器",可以动态地插入到 TTY 核心与 TTY 驱动之间,对传输中的数据进行实时的加工和处理。内核提供了多种类型的线路规程:

  • 标准规程 N_TTY:我们平时敲 ls 回车,它会把 \r 变成 \n;按退格键,它会从缓冲区删除字符。这是最常用的规程。

  • N_PPP:当用电话线拨号上网时,线路规程被切换为 PPP,不再处理字符,而是处理网络帧。

  • N_GSM0710:这正是 3GPP 标准中 CMUX(多路复用) 的规程。它允许在单一的物理串口上虚拟出多个逻辑通道(AT命令通道、GPS通道、PPP数据通道)。在 reference-ril 之外的高性能嵌入式方案中,CMUX 是标配。

TTY 中的线路规程就是具体的协议层。

2.3.11.4 驱动层 (Driver)

这一层位于 TTY 子系统的最底层,包含了与物理或虚拟硬件直接交互的驱动程序。每个具体的硬件接口都对应一个驱动程序。

  • 串口驱动 (UART Driver):驱动物理的串口设备,如 /dev/ttyS0、/dev/ttyUSB0 等,是嵌入式系统与外部模块通信的基石。

  • 虚拟终端驱动 (VT Driver):在本地显示器上提供文本终端,如 tty1 到 tty6,是系统启动后看到的命令行界面。

  • 伪终端驱动 (PTY Driver):用于实现终端模拟器、SSH 远程登录等,是理解 RIL 工作方式的关键。它以 主/从设备(Master/Slave) 对的形式工作。

2.3.11.5 PTY(伪终端):RIL 与模拟器的桥梁

在 Android 模拟器中,RILD 并非连接真实 Modem,而是连接模拟器服务。数据流如下:

  1. 主设备 (Master):模拟器进程(如 QEMU)打开 /dev/ptmx,得到主设备 fd。

  2. 从设备 (Slave):主设备创建后,内核自动生成一个从设备节点 /dev/pts/X。rild 的 mainLoop 通过 socket_local_client 最终映射到这个从设备上。

  3. 数据流:rild 写入从设备的数据,会立即出现在主设备的读取端;反之亦然。

2.3.11.6 关键设备节点和命令

mainLoop 里对串口或 socket 的操作,最终都对应到了 /dev/ 目录下的设备文件,这也是上层应用操作 TTY 设备的入口。

设备文件 驱动程序 应用场景 (以 RIL 为例)
/dev/ttyS0, /dev/ttyUSB0 串口驱动 mainLoop 可直接 open() 此类设备与 Modem 通信。
/dev/tty1, /dev/tty0 虚拟终端驱动 系统控制台,常用于显示内核日志,调试时监控 rild输出。
/dev/ptmx, /dev/pts/* 伪终端驱动 mainLoop 中通过 socket 方式连接时,实质是在与 PTY Master 通信。
/dev/console 系统控制台 系统控制台,接收所有 printk 输出,可设置为串口以便调试。
命令 作用
tty 查看当前终端对应的设备文件
stty -a 查看当前终端的线路规程设置
cat /proc/tty/drivers 查看系统中所有已注册的 TTY 驱动
ps -ef grep "rild" 查看 RIL 守护进程运行状态

TTY 子系统通过驱动层直接操控物理串口,并通过 PTY 统一了本地命令行、ADB 调试、Telnet 远程登录等多种 RIL 通信接口的输入/输出(I/O)模型。即,无论调试指令来自何方,rild 程序都可以用统一的方式,通过 open/read/write 这些标准的 POSIX 接口来与"终端"通信。

2.3.12 通信协议规范

RIL 架构中参考的协议规范:

功能领域 参考的协议/规范 简要说明
核心AT命令 3GPP TS 27.007 定义了控制移动电话(UE)和Modem的AT命令集。你在代码里见到的ATD(拨号)、AT+CSQ(信号质量)等命令都源于此。
短信服务 (SMS) 3GPP TS 27.005 专门定义了通过AT命令发送、接收和管理短信(SMS)及小区广播(CBS)的接口。AT+CMGS(发送短信)、AT+CMGR(读短信)等命令都定义于此。
数据连接 (GPRS) 3GPP TS 27.007 相关章节 在27.007中,有专门章节定义了用于GPRS(通用分组无线服务)的AT命令,如AT+CGATT(附着GPRS服务)、AT+CGDCONT(定义PDP上下文)等。
多路复用 (MUX) 3GPP TS 07.10 这就是我们刚才讨论的CMUX协议,用于在单一物理串口上虚拟出多个逻辑通道。
SIM卡管理 3GPP TS 11.11 / 51.011 规范了SIM卡(用户识别模块)的物理、电气特性和文件系统。RIL通过AT+CRSM等命令与之交互。
基础串行通信 ITU-T V.250 / V.24 定义了数据终端设备(DTE)和数据通信设备(DCE)之间串行通信的通用流程和控制信号。
网络选择 PCCA STD-101 用于选择无线网络的命令 AT+WS46,定义在此标准中。

2.3.13 RIL_Dial

跟随一个具体的请求(如 RIL_REQUEST_DIAL),跟踪从上层 Java 代码到 ril.cpp,最终调用 s_callbacks.onRequest 的完整路径。

Java 复制代码
@Override
    public void
    dial(String address, int clirMode, UUSInfo uusInfo, Message result) {
        RILRequest rr = RILRequest.obtain(RIL_REQUEST_DIAL, result);

        rr.mParcel.writeString(address);
        rr.mParcel.writeInt(clirMode);

        if (uusInfo == null) {
            rr.mParcel.writeInt(0); // UUS information is absent
        } else {
            rr.mParcel.writeInt(1); // UUS information is present
            rr.mParcel.writeInt(uusInfo.getType());
            rr.mParcel.writeInt(uusInfo.getDcs());
            rr.mParcel.writeByteArray(uusInfo.getUserData());
        }

        if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));

        send(rr);
    }
C++ 复制代码
static void onRequest (int request, void *data, size_t datalen, RIL_Token t);
static const RIL_RadioFunctions s_callbacks = {
    RIL_VERSION,
    onRequest,
    currentState,
    onSupports,
    onCancel,
    getVersion
};
C 复制代码
static void dispatchDial (Parcel& p, RequestInfo *pRI);

/**
 * Callee expects const RIL_Dial *
 * Payload is:
 *   String address
 *   int32_t clir
 */
static void
dispatchDial (Parcel &p, RequestInfo *pRI) {
    RIL_Dial dial;
    RIL_UUS_Info uusInfo;
    int32_t sizeOfDial;
    int32_t t;
    int32_t uusPresent;
    status_t status;

    memset (&dial, 0, sizeof(dial));

    dial.address = strdupReadString(p);

    status = p.readInt32(&t);
    dial.clir = (int)t;

    if (status != NO_ERROR || dial.address == NULL) {
        goto invalid;
    }

    if (s_callbacks.version < 3) { // Remove when partners upgrade to version 3
        uusPresent = 0;
        sizeOfDial = sizeof(dial) - sizeof(RIL_UUS_Info *);
    } else {
        status = p.readInt32(&uusPresent);

        if (status != NO_ERROR) {
            goto invalid;
        }

        if (uusPresent == 0) {
            dial.uusInfo = NULL;
        } else {
            int32_t len;

            memset(&uusInfo, 0, sizeof(RIL_UUS_Info));

            status = p.readInt32(&t);
            uusInfo.uusType = (RIL_UUS_Type) t;

            status = p.readInt32(&t);
            uusInfo.uusDcs = (RIL_UUS_DCS) t;

            status = p.readInt32(&len);
            if (status != NO_ERROR) {
                goto invalid;
            }

            // The java code writes -1 for null arrays
            if (((int) len) == -1) {
                uusInfo.uusData = NULL;
                len = 0;
            } else {
                uusInfo.uusData = (char*) p.readInplace(len);
            }

            uusInfo.uusLength = len;
            dial.uusInfo = &uusInfo;
        }
        sizeOfDial = sizeof(dial);
    }

    startRequest;
    appendPrintBuf("%snum=%s,clir=%d", printBuf, dial.address, dial.clir);
    if (uusPresent) {
        appendPrintBuf("%s,uusType=%d,uusDcs=%d,uusLen=%d", printBuf,
                dial.uusInfo->uusType, dial.uusInfo->uusDcs,
                dial.uusInfo->uusLength);
    }
    closeRequest;
    printRequest(pRI->token, pRI->pCI->requestNumber);

    s_callbacks.onRequest(pRI->pCI->requestNumber, &dial, sizeOfDial, pRI);

#ifdef MEMSET_FREED
    memsetString (dial.address);
#endif

    free (dial.address);

#ifdef MEMSET_FREED
    memset(&uusInfo, 0, sizeof(RIL_UUS_Info));
    memset(&dial, 0, sizeof(dial));
#endif

    return;
invalid:
    invalidCommandBlock(pRI);
    return;
}
C 复制代码
static qcril_dispatch_table_entry_type qcril_event_table[] ={
......
  { QCRIL_REG_ALL_ACTIVE_STATES( QCRIL_EVT_IMS_SOCKET_REQ_DIAL, qcril_qmi_voice_request_dial ) },
......  
  }
  
  
void qcril_qmi_voice_request_dial
(
  const qcril_request_params_type *const params_ptr,
  qcril_request_return_type *const ret_ptr /*!< Output parameter */
)
{
......
}

2.3.13.1 dispatchDial图形分析

dispatchDial在打电话流程中的位置

dispatchDial 是Android通用的"翻译官",qcril_qmi_voice_request_dial 是高通平台的"快递员"。它们通过 s_callbacks.onRequest 这个函数指针紧密协作,构成了高通平台上RIL层处理拨号请求的核心路径。

dispatchDial 内部结构图

dispatchDial 内部时序图

元素 说明
输入 Parcel 来自 RILJ 通过 Socket 发送的序列化数据,依次包含:地址字符串(以 -1 结尾)、CLIR 整型、可选的 UUS 信息。
版本兼容 s_callbacks.version 是厂商 RIL 报告的接口版本。若低于 3,则 UUS 信息不被支持,sizeOfDial 会减去指针大小,避免传递未使用的字段。
UUS 信息 用户间信令(User-to-User Signaling),用于在 ISDN 等网络中随呼叫传递额外数据。高通平台较常用。
RIL_Dial 结构体 定义为包含 address、clir、uusInfo 指针。注意 UUS 信息是动态分配的局部变量,调用 onRequest 时传递的是指针,因此 onRequest 必须在使用完数据前完成处理或复制。
内存管理 dial.address 是通过 strdupReadString 动态分配的,调用 onRequest 后需要 free。UUS 数据指向 parcel.readInplace 返回的缓冲区,该缓冲区在 dispatchDial 返回后不再有效,因此厂商 RIL 必须立即复制或使用。
令牌传递 最后一个参数 pRI 被直接作为 RIL_Token 传递,它是请求上下文的指针,用于异步响应匹配。

数据结构关系(类图)

dispatchDial 作为一个数据转换器,将序列化的 Parcel 数据转换为 C 结构体 RIL_Dial,并完成了版本兼容处理和内存分配后,最终将请求派发给厂商 RIL 的具体实现。

2.3.13.2 s_commands\[\]和qcril_event_table\[\]

C 复制代码
RILJ (Java)
   │ socket
   ▼
rild 守护进程
   │
   ▼
libril.so
   │
   ├── s_commands[]  ← 映射 RIL_REQUEST_XXX → dispatchDial 等
   │
   ▼
s_callbacks.onRequest  (函数指针)
   │
   ▼
qcril.so (Vendor RIL)
   │
   ├── qcril_event_table[]  ← 映射请求号 → qcril_qmi_voice_request_dial 等
   │
   ▼
Modem (QMI/AT)

s_commands\[\](libril 内部)

  • 位置:libril.so(ril.cpp)

  • 作用:将 RILJ 发来的请求号(如 RIL_REQUEST_DIAL)映射到 libril 内部的分发函数和响应函数。

  • 调用链: RILJ → (socket) → rild → libril::processCommandBuffer() → 查 s_commands\[\] → 调用对应的 dispatchFunction(如 dispatchDial)→ dispatchDial 再调用 s_callbacks.onRequest → 进入 Vendor RIL。

  • 本质:这是 RILJ ↔ libril 之间的协议适配层

qcril_event_table\[\](高通 Vendor RIL 内部)

  • 位置:高通 qcril 库(如 qcril_qmi_voice.c 等)

  • 作用:将 内部请求/事件 ID 映射到 具体的处理函数(如 qcril_qmi_voice_request_dial)。

  • 调用链: s_callbacks.onRequest → 进入高通的 onRequest_rid() → 高通内部根据请求号查 qcril_event_table\[\] → 调用具体的业务函数(例如语音、数据、短信等)。

  • 本质:这是 Vendor RIL 内部 的分发表,用于将 libril 下来的请求派发到不同的业务模块。

  • s_commands\[\] 是 libril 层的路由表,连接 RILJ 和 libril 内部的分发逻辑。

  • qcril_event_table\[\] 是 厂商 RIL 内部的路由表,连接 libril 回调与具体的 Modem 操作函数。

2.3.13.3 打电话流程全链路解析(从 Java 到 Modem)

2.3.13.3.1 总体架构分层
2.3.13.3.2 关键数据结构转换路径
阶段 数据结构 作用
Java 层 RILRequest + Parcel 封装请求号和参数(address, clir, UUS)
Native 分发层 RIL_Dial C 结构体,包含 address, clir, uusInfo 指针
Vendor RIL 层 QMI 消息(TLV) 高通私有二进制协议消息
Modem 层 内部信令(如 3GPP 呼叫控制) 最终执行呼叫
2.3.13.3.3 完整时序图(从 dial 调用到响应)
2.3.13.3.4 结构图:模块间数据流向

2.3.13.4 关键点分析

  1. Java 层 dial 方法
  • RILRequest.obtain(RIL_REQUEST_DIAL, result) 分配请求对象。

  • 按顺序写入 Parcel:

    • address(字符串)

    • clirMode(int)

    • uusInfo 存在标志(0/1),如果存在则写入 type、dcs、data。

  • send(rr) 将 Parcel 写入 Socket。

  1. dispatchDial 解析
  • 使用 strdupReadString 读取地址(动态分配)。

  • 读取 clir。

  • 检查 s_callbacks.version,如果 >= 3 则尝试读取 UUS。

  • UUS 数据:uusType、uusDcs、len,如果 len=-1 则无数据。

  • 将 dial.uusInfo 指向局部 uusInfo,注意该指针在函数返回后失效,但厂商 onRequest 必须立即使用或复制。

  1. s_callbacks.onRequest 调用
  • 传入 pRI->pCI->requestNumber(即 RIL_REQUEST_DIAL)。

  • 传入 &dial 和 sizeOfDial(版本兼容)。

  • 传入 pRI 作为令牌。

  1. 高通 qcril_qmi_voice_request_dial
  • 从 params_ptr->data 中取出 RIL_Dial*。

  • 将 address、clir 填入 QMI 消息结构体。

  • 若 UUS 存在,填充 QMI 的 UUS 字段。

  • 调用 qmi_client_send_msg_async 异步发送,等待 Modem 响应。

  1. 响应回调
  • Modem 返回后,QMI 回调 qcril_qmi_voice_dial_resp。

  • 调用 RIL_onRequestComplete(pRI, result, NULL, 0) 返回结果。

  1. 设计模式总结
模式 体现
分层解耦 Java RILJ ↔ Native rild ↔ Vendor RIL ↔ Modem,各层通过标准接口(Socket、s_callbacks、QMI)通信
异步回调 请求发送后不阻塞,通过 token(pRI)匹配响应
版本兼容 s_callbacks.version 控制是否解析 UUS,保证新旧 Vendor RIL 兼容
数据结构转换 Parcel(跨进程序列化)→ RIL_Dial(C 结构体)→ QMI TLV(二进制协议)
表驱动分发 s_commands\[\] 根据 request 号调用对应 dispatchFunction

从 Java dial 到 Modem 执行呼叫,经历了 三次数据转换(Parcel → RIL_Dial → QMI),两次跨进程通信(RILJ ↔ rild 通过 Socket,rild ↔ qcril 通过函数回调),最终通过 QMI 共享内存与基带交互。整个流程体现了 Android RIL 架构的 分层、异步、标准化 设计哲学。

相关推荐
raindesound1 小时前
Android+QC modem手机通信模块技术分析 (1)
架构
程序员cxuan4 小时前
读懂 Claude Code 架构分析系列,第一篇,开始!
人工智能·后端·架构
Yeats_Liao5 小时前
14:Servlet中的页面跳转-Java Web
java·后端·架构
raindesound5 小时前
计算机基础:ADT(Abstract Data Type)抽象数据类型 (2)
架构
武子康5 小时前
调查研究-201 Rust 里的 dev build 和 release build:为什么同一份代码性能差这么多?
后端·架构·rust
raindesound5 小时前
计算机基础:ADT(Abstract Data Type)抽象数据类型 (1)
架构
夕阳与风馨5 小时前
大文件(20GB+)SFTP 下载模块设计与实现
后端·架构
阳光是sunny16 小时前
Vue 项目怎么做用户行为全链路监控?轻量插件方案详解
前端·面试·架构
EMA1 天前
Docker虚拟化失败解决方案
架构