1 关键字

power_supply; battery; 设备属性文件;CPU占用;

2 问题描述

拔掉电池使用USB供电重启系统后,power_host进程的CPU占用较高,每5秒钟CPU的占用在0%~17%之间跳跃。对比插上电池重启系统的场景,power_host进程的CPU占用反而比较低,CPU占用不超过0.6%。

3 问题原因

电池内核驱动每隔5秒钟会通过uevent上报电池的状态信息,power_host中的battery_thread线程专门负责接受驱动上报的uevent事件。当接收到uevent事件时,会读取/sys/class/power_supply/battery/下的capacity、voltage_now、temp、health、status等11个属性的文件,通知给关注电池状态的上层应用。

在读取这些属性文件时共耗时100ms以上,而插入电池场景下读取文件仅耗时8ms。根本原因是由于内核电池驱动挂接的读取capacity属性的函数中,当电池未插上时会调用mdelay函数延迟100ms。内核的mdelay函数会让CPU进入忙等状态,因此用户态进程读取/sys/class/power_supply/battery/capacity文件需要100ms以上才能返回,并且期间一直占用CPU。

4 解决方案

电池内核驱动当电池未插上时,在读取/sys/class/power_supply/battery/capacity属性的处理函数中,将mdelay改为msleep避免一直占用CPU。

5 定位过程

1、使用top命令查看CPU占用率,发现power_host进程CPU占用达到17%。

执行top命令

Tasks: 267 total,   1 running, 266 sleeping, 0 stopped, 0 zombie
  Mem: 911368K total,  487644K used,  423724K free,  880640 buffers
 Swap: 524284K total, 44040192 used,  481276K free, 140064K cached
400%cpu  7%user  0%nice 46%sys  346%idle  0%iow  0%irq  0%sirq  0%host 
  PID USER       PR  NI VIRT  RES  SHR S[%CPU] %MEM   TIME+  ARGS
  659 power_host 20   0 7.9M 4.2M 3.6M S 17.0   0.4 0:02.61  power_host
  ....

2、进一步查看power_host进程的CPU占用,发现battery_thread和IPC_1_1298线程CPU占用率呈周期性跳跃状态,二个线程CPU共占用10%上下。

使用top -Hp 659 查看power_host进程CPU占用具体信息

Threads: 8 total,  0 running,  8 sleeping,  0 stopped,  0 zombie
  Mem:   911368K total,  491324K used,  420044K free,  880640 buffers
 Swap:   524284K total, 44040192 used,  481276K free, 141508K cached
400%cpu  65%user  0%nice  151%sys  184%idle  0%iow  0%irq  0%sirq  0%host
 TID USER       PR  NI VIRT  RES  SHR  S[%CPU] %MEM    TIME+ THREAD            PROCESS 
 741 power_host 20   0 8.0M 4.2M 3.6M  S  7.0   0.4  0:27.07 battery_thread power_host
1298 power_host 20   0 8.0M 4.2M 3.6M  S  3.3   0.4  0:12.34 IPC_3_1298     power_host
1296 power_host  0 -20 8.0M 4.2M 3.6M  S  0.3   0.4  0:00.03 IPC_1_678      power_host
1755 power_host  0 -20 8.0M 4.2M 3.6M  S  0.0   0.4  0:00.00 IPC_1_678      power_host
1293 power_host 20   0 8.0M 4.2M 3.6M  S  0.0   0.4  0:00.00 IPC_2_1293     power_host
 678 power_host 20   0 8.0M 4.2M 3.6M  S  0.0   0.4  0:10.65 IPC_1_678      power_host
 673 power_host 20   0 8.0M 4.2M 3.6M  S  0.0   0.4  0:00.00 IPC_0_673      power_host
 659 power_host 20   0 8.0M 4.2M 3.6M  S  0.0   0.4  0:00.07 power_host     power_host

3、查看代码,看看battery_thread线程主要是做什么的。

battery_thread线程实现的代码/drivers/peripheral/battery/interfaces/hdi_service/src/battery_thread.cpp。

// StartThread在BatteryInterfaceImpl::Init中调用
void BatteryThread::StartThread(void* service)
{
    Init(service);
    Run(service);
} 

void BatteryThread::Run(void* service)
{
    std::thread batteryThread(&BatteryThread::LoopingThreadEntry,
                              this, service);
    // 创建battery_thread线程
    pthread_setname_np(batteryThread.native_handle(), "battery_thread");
    batteryThread.detach();
}

int32_t BatteryThread::Init([[maybe_unused]] void* service)
{
    ......

    // 初始化uevent的事件接收
    InitUevent();

    return HDF_SUCCESS;
}

int32_t BatteryThread::InitUevent()
{
    ueventFd_ = OpenUeventSocket();
    if (ueventFd_ == INVALID_FD) {
        BATTERY_HILOGE(COMP_HDI, "open uevent socket failed, fd is invalid");
        return HDF_ERR_BAD_FD;
    }

    // 注册uenent事件的回调处理函数
    fcntl(ueventFd_, F_SETFL, O_NONBLOCK);
    callbacks_.insert(std::make_pair(ueventFd_, 
                      &BatteryThread::UeventCallback));

    if (RegisterCallback(ueventFd_, EVENT_UEVENT_FD)) {
        BATTERY_HILOGE(COMP_HDI, "register Uevent event failed");
        return HDF_ERR_BAD_FD;
    }

    return HDF_SUCCESS;
}

int32_t BatteryThread::OpenUeventSocket()
{
    int32_t bufferSize = UEVENT_BUFF_SIZE;
    struct sockaddr_nl address = {
        .nl_family = AF_NETLINK,
        .nl_pid = getpid(),
        .nl_groups = 0xffffffff
    };

    // 创建接收uevent的socket
    int32_t fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, 
                        NETLINK_KOBJECT_UEVENT);
    
    ......

    return fd;
} 

// 接收uevent事件并进行处理,主要处理函数UpdateBatteryInfo
void BatteryThread::UeventCallback(void* service)
{
    char msg[UEVENT_MSG_LEN + UEVENT_RESERVED_SIZE] = { 0 };

    ssize_t len = recv(ueventFd_, msg, UEVENT_MSG_LEN, 0);
    if (len < 0 || len >= UEVENT_MSG_LEN) {
        BATTERY_HILOGI(COMP_HDI, "recv return msg is invalid, len: %{public}zd", len);
        return;
    }

    // msg separator
    msg[len] = '\0';
    msg[len + 1] = '\0';
    if (!IsPowerSupplyEvent(msg)) {
        return;
    }
    UpdateBatteryInfo(service);
} 

void BatteryThread::UpdateBatteryInfo(void* service)
{
    ......

    // UpdateInfoByReadSysFile读取/sys/class/power_supply/battery/
    // 目录下的属性文件
    provider_->UpdateInfoByReadSysFile(batteryInfo.get());

    event.capacity = batteryInfo->capacity_;
    .......
    event.totalEnergy = batteryInfo->totalEnergy_;

    // 调用框架层battery service注册的callback函数
    // BatteryService::HandleBatteryCallbackEvent,
    // 通知关注电池信息的模块更新电池信息
    if (g_callback != nullptr) {
        g_callback->Update(event);
    } else {
        BATTERY_HILOGI(FEATURE_BATT_INFO, "g_callback is nullptr");
    }
}

battery_thread线程是power_host进程创建的线程,最主要的任务是接收内核驱动发送的uevent,并通知上层battery service电池信息的变化。而BatteryThread::UpdateBatteryInfo就是最核心的处理函数。

通过在BatteryThread::UpdateBatteryInfo中增加hilog调试发现,该函数在插电池和不插电池重启后所耗费的时间差异较大。不插电池重启后执行该函数大约花费103ms,而插上电池重启后只花费8ms,特别是调用ParseCapacity函数最为耗时。

ParseCapacity函数实际就是读取/sys/class/power_supply/battery/capacity文件中的值,为什么不同的场景下耗时差异这么大?

// 文件/drivers/peripheral/battery/interfaces/hdi_service/src/power_supply_provider.cpp 

void PowerSupplyProvider::UpdateInfoByReadSysFile(struct BatterydInfo* info) const
{
    ParseCapacity(&info->capacity_);
    ParseVoltage(&info->voltage_);
    ParseTemperature(&info->temperature_);
    ParseHealthState(&info->healthState_);
    ParseChargeState(&info->chargeState_);
    ParseChargeCounter(&info->chargeCounter_);
    ParseCurrentNow(&info->curNow_);
    ParseCurrentAverage(&info->curAverage_);
    ParseRemainEnergy(&info->remainEnergy_);
    ParseTotalEnergy(&info->totalEnergy_);
    ParsePresent(&info->present_);

    ......
} 

int32_t PowerSupplyProvider::ParseCapacity(int32_t* capacity) const
{
    char buf[MAX_BUFF_SIZE] = {0};

    // 读取/sys/class/power_supply/battery/capacity文件
    int32_t ret = ReadBatterySysfsToBuff(batterySysfsInfo_.capacityPath.c_str(), buf, sizeof(buf));
    if (ret != HDF_SUCCESS) {
        return ret;
    }

    int32_t value = ParseInt(buf);
    *capacity = value;

    return HDF_SUCCESS;
}

4、找到读取capacity文件耗时的原因。

/sys/目录是sysfs文件系统,一种虚拟文件系统,与proc文件系统有些类似。用于对系统设备进行管理,可以产生一个包括所有系统硬件的层级视图,向用户空间导出内核对象并且能对其进行读写操作。

/sys/class/power_supply/battery/目录下属性文件的读写由电池内核驱动实现,通过向linux内核注册power supply设备将读取电池属性文件的函数挂接到内核。

// 电池充放电驱动probe函数
static int battery_charger_probe(struct platform_device *pdev)
{
    ......
    // battery_monitor_work用于周期性上报电池状态信息
    INIT_DELAYED_WORK(&chip->battery_monitor_work,
                      battery_monitor_func);
    ......
    chip->batt_psy_d.name = "battery";
    chip->batt_psy_d.type = POWER_SUPPLY_TYPE_BATTERY;
    chip->batt_psy_d.properties = the_batt_power_props;
    chip->batt_psy_d.num_properties =
        ARRAY_SIZE(the_batt_power_props);
    // 挂接power supply设备属性读、写等接口函数
    chip->batt_psy_d.get_property = batt_power_get_property;
    chip->batt_psy_d.set_property = batt_power_set_property;
    chip->batt_psy_d.property_is_writeable =
        batt_property_is_writeable;
    chip->batt_psy_d.external_power_changed =
        batt_external_power_changed;

    batt_psy_cfg.drv_data = chip;

    // 注册power supply设备
    chip->batt_psy = devm_power_supply_register(chip->dev,
                                                &chip->batt_psy_d,
                                                &batt_psy_cfg);
    ......

    // probe函数中开始延迟执行battery_monitor_work
    schedule_delayed_work(&chip->battery_monitor_work,
                          msecs_to_jiffies(2000));
    ......
}

// 读取电池所有属性的入口函数
static int batt_power_get_property(struct power_supply *psy,
                                   enum power_supply_property psp,
                                   union power_supply_propval *val)
{
    struct the_chip *chip = power_supply_get_drvdata(psy);

    switch (psp) {
    case POWER_SUPPLY_PROP_STATUS:
        val->intval = get_prop_batt_status(chip);
        break;
    case POWER_SUPPLY_PROP_CAPACITY: // 读取capacity文件
        val->intval = get_prop_capacity(chip);
        break;

    ......
    }

    return 0;
}

static int get_prop_capacity(struct the_chip *chip)
{
    ......

    // 电池没有插上时,会执行if分支,其中调用mdelay导致CPU忙等100ms,
    // 用户态读取capacity要100+ms才能返回。
    if (!get_prop_batt_present(chip) ||
        (chip->battery_status_change != 0)) {
        mdelay(100);

        if (!get_prop_batt_present(chip)) {
            last_soc = DEFAULT_CAPACITY;
            return DEFAULT_CAPACITY;
        }
    } 

    ......
}

5、充放电内核驱动程序每5s会调用battery_monitor_func,周期性向用户态发送uevent事件通知电池状态信息。

// 电池充放电驱动probe函数
static int battery_charger_probe(struct platform_device *pdev)
{
    ......
    // battery_monitor_work用于周期性上报电池状态信息
    INIT_DELAYED_WORK(&chip->battery_monitor_work,
                      battery_monitor_func);
    ......

    // probe函数中延迟2s执行battery_monitor_work
    schedule_delayed_work(&chip->battery_monitor_work,
                          msecs_to_jiffies(2000));
    ......
}

static void battery_monitor_func(struct work_struct *work)
{
    struct delayed_work *dwork = to_delayed_work(work);
    struct the_chip *chip = container_of(dwork, struct the_chip, 
                                         battery_monitor_work);

    ......

    // 查看电池状态等操作,并通知到用户态
    user_action_func(chip);
    // 5s后再次执行battery_monitor_work
    schedule_delayed_work(&chip->battery_monitor_work,
                          msecs_to_jiffies(5000));
}

static int user_action_func(struct the_chip *chip)
{
    ......

    // 调用power supply的接口通知用户态电池状态信息
    power_supply_changed(chip->batt_psy);

    ......
}

6、从上述分析看,battery_thread线程周期性CPU占用高,是因为内核电池充放电管理的驱动程序每5秒钟通过uevent通知用户态电池状态信息,battery_thread接收到uevent后读取设备的属性文件。因为电池驱动挂接的get_prop_capacity函数在电池未插入的情况下会delay 100ms导致读取文件时间长并一直占用CPU,这样引起的CPU占用高的问题。

6 知识分享

  • sysfs是基于内存的文件系统,用于向用户空间导出内核对象并且能对其进行读写。如果读写sysfs下的文件有异常,就需要分析设备驱动中向内核注册的读写等函数。
  • 内核延时函数mdelay与msleep的区别:mdelay是忙等待函数,会占用CPU资源,延迟时间是准确的。msleep是休眠函数,不占用CPU资源,延迟时间通常高于给定值。

 

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐