前言

uevent是一个经常看见但是从来没有去研究过的一个东西。正好,这次重构一加第三方系统tri-state-key驱动的时候,发现里面也有相关的玩意儿,那就,不得不弄明白了

uevent

是什么

uevent是一种内核通知用户空间关于/sys目录下的kobject的更改的一种方式,比如在/sys目录下的节点创建、移除和更新时

uevent存在的意义在于:用户空间不再需要花费大量的时间和性能遍历sysfs,去寻找各种细微的变化,这对linux下设备的自动发现和挂载有重要作用,将在稍后阐述

当然,uevent也并非仅限于sysfs中节点的创建和删除,内核空间也可以向用户空间发送含有指定信息的自定义uevent,触发用户空间的指定事件

如何发送

uevent的发送方式主要分为三种(我目前只见过这三种)
一种是由内核空间自动发送:当sysfs中创建的kobject符合一定条件时,内核空间会自动发送其add与remove甚至还有change事件
另一种时内核空间手动发送:比如使用函数kobject_uevent或者kobject_uevent_env,第二个函数可以自定义环境变量,向用户空间发送含有指定信息的uevent
还有一种是由用户空间触发:往指定节点中写入事件名称即可,比如echo change > /sys/devices/**/uevent

内核中也有一些驱动,封装了通过往uevent里装数据来与用户空间进行通信的操作,比如extcon

menuconfig EXTCON
    tristate "External Connector Class (extcon) support"
    help
      Say Y here to enable external connector class (extcon) support.
      This allows monitoring external connectors by userspace
      via sysfs and uevent and supports external connectors with
      multiple states; i.e., an extcon that may have multiple
      cables attached. For example, an external connector of a device
      may be used to connect an HDMI cable and a AC adaptor, and to
      host USB ports. Many of 30-pin connectors including PDMI are
      also good examples.

当然,并不是所有/sys目录下的条目都可以拥有uevent节点并自动发送uevent,这个似乎和内核中相关数据结构的定义方式有关,此处不展开

如何接收

uevent的接收分为两种方式

第一种,用户空间可以通过打开一个特定的netlink socket,然后poll在那里休眠等待事件唤醒,在获得uevent事件后解析并按照要求进行处理

另一种,内核可以直接调用用户空间的可执行文件,并把信息作为环境变量的方式发送过去?(未验证)(类似usermodehelper?)

安卓使用的是第一种,并在cutils/uevent.h中提供了开启这个socket的封装int uevent_open_socket(int buf_sz, bool passcred),用户空间也可以直接通过ssize_t uevent_kernel_multicast_recv(int socket, void* buffer, size_t length)来接收uevent message

安卓在platform/system/extras/tests/uevents提供了一个名为uevents的测试工具,可以使用mmm命令编译二进制文件并安装到system

在没有写sepolicy的情况下,这个二进制文件只能在root / permissive下使用

这个测试工具的内容非常简单,但同时也提供了使用uevent的不错的参考

/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <cutils/uevent.h>
#include <stdio.h>

#define UEVENT_MSG_LEN  1024

int main(int argc, char *argv[])
{
    int device_fd;
    char msg[UEVENT_MSG_LEN+2];
    int n;
    int i;

    device_fd = uevent_open_socket(64*1024, true);
    if(device_fd < 0)
        return -1;

    while ((n = uevent_kernel_multicast_recv(device_fd, msg, UEVENT_MSG_LEN)) > 0) {
        msg[n] = '\0';
        msg[n+1] = '\0';

        for (i = 0; i < n; i++)
            if (msg[i] == '\0')
                msg[i] = ' ';

        printf("%s\n", msg);
    }

    return 0;
}

安装到系统后,在adb root环境下输入uevents即可获得所有关于uevent事件的汇报

p

常见的uevent使用

比如电池电量/电流等信息的更新就是通过uevent传递的,硬件设备的插拔也是通过uevent通知用户空间的,当然,还有文章开篇的一加三段式按键驱动,也是通过uevent通知用户空间按键的切换状态的

除了某些非常secure的信息传递需要自建节点来poll外,对别的来说,uevent确实是一个非常不错的封装。毕竟,uevent是个广播,具有一定权限的进程都能拿到其内容,虽然有selinux的保护,但也别对它的安全性太有期待

ueventd

你看呐,这个东西啊,以d结尾,那很显然,它是一个daemon(守护进程)
没错,它是安卓的一个非常核心也非常有特色的进程,和uevent有关,你猜它是一个uevent的转发器?那就错了

从devfs说起

从网上的一些历史资料来看,devfs出现的很早,大约是在Linux 2.4的时候,嗯,它就是最最最原始的/dev目录,里面显示了内核中挂载的各种设备的信息,但是,它却因为混乱、难管理、性能开销巨大等问题,在Linux 2.6被取代
取代的方式是,引入sysfs(没错,devfs出现的比sysfs早),结构化的存储各所挂载的总线等详细信息,然后使用一个用户空间程序根据sysfs中的设备信息来创建和更新/dev目录,从而解决上述问题
所以说啊,uevent的原教旨其实就是在这里——向用户空间程序通知sysfs的更改,即时在dev中创建/删除/更新设备,并通过这种可睡眠的被动的方式(指poll)来有效降低开销

而这个用户空间程序,在正宗的Linux上,叫udev,在迷你嵌入式设备上,叫mdev,而在安卓上,叫ueventd

我们可以通过df指令清晰的看到,现代Linux下,挂载到/dev的究竟是什么

p

别致的/dev

但是在安卓上,挂载到/dev下的东西就非常有意思了

p

什么?tmpfs?没错,就是一个非常非常普通的tmpfs,这意味着它下面的内容甚至是可以随意创建删除修改的(只要权限够)

p

怎么样,把/dev/null给删了,再创建了一个fuck节点🤣

所以说,安卓的/dev目录,本质上就是个内存盘,而这个内存盘的内容,需要专门的程序去创建和维护,而这个程序,就是ueventd

设备文件是如何被创建的

这确实是一个值得好奇的内容,既然/dev目录是一个内存盘,也就意味着一定有适当的操作可以使这些看似遥不可及的设备文件被创建并和指定设备关联,这也意味着我们可以通过一些骚操作在用户看得见的地方(比如/sdcard下)创建魔幻的字符设备

函数创建

上面已经说了,维护/dev目录的东西是ueventd,这也意味着,我们只需要看一看ueventd的源码就知道这些设备是怎么来的了

不出所料,在platform/system/core/init/first_stage_init.cpp中,很快就找到了这些关键字符设备的创建方式

p

而mknod这个函数,则在bionic中被指向posix api mknodat,然后应该就是靠syscall进内核了?

命令创建

既然有函数,也该有对应的命令
Linux提供了mknod这个命令来创建这种特殊的设备文件

usage: mknod [-m MODE] NAME TYPE [MAJOR MINOR]

Create a special file NAME with a given type. TYPE is b for block device,
c or u for character device, p for named pipe (which ignores MAJOR/MINOR).

-m      Mode (file permissions) of new device, in octal or u+x format

其中NAME是文件名,TYPE是类型,比如字符设备是c,块设备是b,管道设备是b等,支持多个类型同时使用,比如{b|c}
而最后的两个参数,则需要指定设备的主设备号和次设备号,其中主设备号一般和驱动有关,次设备号是挂载在这个驱动下的设备?
这个设备号似乎并没有那么好搞到,一些特殊的设备比如/dev/null/dev/random的设备号都是写死在创建时的(可以看上面的图),而那些挂在总线上的设备的设备号的获取方式还未研究过

但是,我们可以通过ls -l命令来直接看到设备号,并根据其来造设备😆

p

日期前的两个数字,就分别是主设备号和次设备号。对于一般文件,这个日期前的位置,显示的是文件大小

于是,我们就可以仿制一个/dev/random了

OnePlus8T:/dev # ls -l | grep random
crw------- 1 root        root             10, 183 1970-09-21 01:06 hw_random
crw-rw-rw- 1 root        root              1,   8 1970-09-21 01:06 random
crw-rw-rw- 1 root        root              1,   9 1970-09-21 01:06 urandom

首先,获得random的主设备号为1,次设备号为8

然后,尝试在用户可以看到的地方仿制一个random

OnePlus8T:/data/media/0 # mknod myrandom c 1 8

这个目录会被fuse映射到/sdcard作为用户可以看到的存储空间(直接在/sdcard创建会被拒绝)

然后,见证奇迹的时候

OnePlus8T:/data/media/0 # cat myrandom
cat: myrandom: Permission denied

啊,看来安卓做了某些保护....

不过,在一般的Linux发行版下,这种操作是可以正常运行的

$cat myrandom

p

ueventd还会干啥

除了根据uevent创建和维护devfs,ueventd还会根据uevent向内核发送固件

比如高通的qcacld-3.0驱动中就有定义了固件路径

#define WLAN_MAC_FILE              "wlan/qca_cld/" PREFIX "wlan_mac.bin"

并在驱动启动时加载固件

status = request_firmware(&fw, WLAN_MAC_FILE, hdd_ctx->parent_dev);

在固件从未被加载过时,这一函数会在最终创建一个固件的device并发送uevent事件,等待用户空间把固件塞进来

system/core/init/firmware_handler.cpp,我们可以看到ueventd对此的相关实现
就不粘贴完整代码了,看个头文件知大概

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <grp.h>
#include <pwd.h>

#include <functional>
#include <string>
#include <vector>

#include "result.h"
#include "uevent.h"
#include "uevent_handler.h"

namespace android {
namespace init {

struct ExternalFirmwareHandler {
    ExternalFirmwareHandler(std::string devpath, uid_t uid, std::string handler_path);
    ExternalFirmwareHandler(std::string devpath, uid_t uid, gid_t gid, std::string handler_path);

    std::string devpath;
    uid_t uid;
    gid_t gid;
    std::string handler_path;

    std::function<bool(const std::string&)> match;
};

class FirmwareHandler : public UeventHandler {
  public:
    FirmwareHandler(std::vector<std::string> firmware_directories,
                    std::vector<ExternalFirmwareHandler> external_firmware_handlers);
    virtual ~FirmwareHandler() = default;

    void HandleUevent(const Uevent& uevent) override;

  private:
    friend void FirmwareTestWithExternalHandler(const std::string& test_name,
                                                bool expect_new_firmware);

    Result<std::string> RunExternalHandler(const std::string& handler, uid_t uid, gid_t gid,
                                           const Uevent& uevent) const;
    std::string GetFirmwarePath(const Uevent& uevent) const;
    void ProcessFirmwareEvent(const std::string& root, const std::string& firmware) const;
    bool ForEachFirmwareDirectory(std::function<bool(const std::string&)> handler) const;

    std::vector<std::string> firmware_directories_;
    std::vector<ExternalFirmwareHandler> external_firmware_handlers_;
};

}  // namespace init
}  // namespace android

我们也可以看到ueventd相关日志

[    7.535361] ueventd: firmware: loading 'adsp.mdt' for '/devices/platform/soc/17300000.qcom,lpass/firmware/adsp.mdt'
[    7.536249] ueventd: firmware: loading 'cdsp.mdt' for '/devices/platform/soc/8300000.qcom,turing/firmware/cdsp.mdt'
[    7.537424] ueventd: loading /devices/platform/soc/8300000.qcom,turing/firmware/cdsp.mdt took 1ms
[    7.538441] ueventd: loading /devices/platform/soc/17300000.qcom,lpass/firmware/adsp.mdt took 3ms
[    7.548819] ueventd: firmware: loading 'cdsp.b03' for '/devices/platform/soc/8300000.qcom,turing/firmware/cdsp.b03'
[    7.549305] ueventd: firmware: loading 'cdsp.b02' for '/devices/platform/soc/8300000.qcom,turing/firmware/cdsp.b02'
[    7.549760] ueventd: loading /devices/platform/soc/8300000.qcom,turing/firmware/cdsp.b03 took 1ms
[    7.550898] ueventd: loading /devices/platform/soc/8300000.qcom,turing/firmware/cdsp.b02 took 1ms
[    7.554015] ueventd: firmware: loading 'cdsp.b05' for '/devices/platform/soc/8300000.qcom,turing/firmware/cdsp.b05'
[    7.556356] ueventd: firmware: loading 'cdsp.b06' for '/devices/platform/soc/8300000.qcom,turing/firmware/cdsp.b06'
[   15.276763] ueventd: firmware: loading 'venus.mdt' for '/devices/platform/soc/aab0000.qcom,venus/firmware/venus.mdt'
[   15.278572] ueventd: loading /devices/platform/soc/aab0000.qcom,venus/firmware/venus.mdt took 1ms
[   15.285652] ueventd: firmware: loading 'venus.b04' for '/devices/platform/soc/aab0000.qcom,venus/firmware/venus.b04'
[   15.286386] ueventd: firmware: loading 'venus.b05' for '/devices/platform/soc/aab0000.qcom,venus/firmware/venus.b05'
[   15.287040] ueventd: firmware: loading 'venus.b06' for '/devices/platform/soc/aab0000.qcom,venus/firmware/venus.b06'
[   15.287131] ueventd: loading /devices/platform/soc/aab0000.qcom,venus/firmware/venus.b05 took 0ms
[   15.287164] ueventd: loading /devices/platform/soc/aab0000.qcom,venus/firmware/venus.b04 took 1ms
[   15.287879] ueventd: firmware: loading 'venus.b07' for '/devices/platform/soc/aab0000.qcom,venus/firmware/venus.b07'
[   15.288253] ueventd: loading /devices/platform/soc/aab0000.qcom,venus/firmware/venus.b07 took 0ms
[   15.288693] ueventd: loading /devices/platform/soc/aab0000.qcom,venus/firmware/venus.b06 took 1ms

当然,由于这些发生在系统的早期阶段,也很难拦截和调试,这里就不再深入研究了

总结

通过上述分析研究,也算是对uevent机制在安卓中扮演的角色有了一个大致的了解
但,也只是个大概。世界,充满了未解之谜