Android12系统环境变量设置

Android12中环境变量设置与使用
Views: 706
4 0
Read Time:9 Minute, 29 Second

最近在移植百度Apollo Cyber通信框架至安卓系统中,发现Cyber本身依赖于环境变量来实现服务的初始化配置。相应地,我也需要在安卓系统中引入这些环境变量,并确保在Native服务启动时这些环境变量已经准备就绪。

由于此前我对环境变量的了解并不多,于是研究学习了一下Android系统中关于环境变量的相关配置,这篇博文即对此做了一个记录。

背景

首先我们需要探究一下为什么我们需要环境变量。环境变量是一组动态的,可手动编辑、设置的值。一般而言,我们程序的运行往往会依赖于各种各样的配置,这些配置我们可以以配置文件的形式存放于本地文件系统中,再让程序在运行时进行解析从而获取配置,这是一种比较通用的做法;而另一种比较通用的做法是将这些配置写入环境变量,程序在运行时自动读取环境变量即可。前者适合配置比较多的情况,而后者则适合配置少且配置可能需要多进程共享的情况,比较灵活。

可以说环境变量是非常重要的,没有环境变量的支持,无论是Windows系统中的程序还是Linux系统中的程序都无法正常运行。

环境变量分为两类:全局环境变量和局部环境变量。全局环境变量是全局可见的,所有进程可见;而局部变量则针对部分进程可见。

安卓系统环境变量

如何查看系统的环境变量呢,这里提供两个命令:env或者printenv命令,如下所示:

_=/system/bin/env
ANDROID_DATA=/data
HOME=/
ANDROID_TZDATA_ROOT=/apex/com.android.tzdata
ANDROID_STORAGE=/storage
ANDROID_ASSETS=/system/app
TERM=xterm-256color
ANDROID_SOCKET_adbd=19
ANDROID_ART_ROOT=/apex/com.android.art
CYBER_DOMAIN_ID=71
CYBER_PATH=/data/cyber
EXTERNAL_STORAGE=/sdcard
DOWNLOAD_CACHE=/data/cache
LOGNAME=root
SYSTEMSERVERCLASSPATH=/system/framework/com.android.location.provider.jar:/system/framework/services.jar:/system/framework/ethernet-service.jar:/system/framework/car-frameworks-service.jar:/apex/com.android.appsearch/javalib/service-appsearch.jar:/apex/com.android.media/javalib/service-media-s.jar:/apex/com.android.permission/javalib/service-permission.jar
DEX2OATBOOTCLASSPATH=/apex/com.android.art/javalib/core-oj.jar:/apex/com.android.art/javalib/core-libart.jar:/apex/com.android.art/javalib/okhttp.jar:/apex/com.android.art/javalib/bouncycastle.jar:/apex/com.android.art/javalib/apache-xml.jar:/system/framework/framework.jar:/system/framework/framework-graphics.jar:/system/framework/ext.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/apex/com.android.i18n/javalib/core-icu4j.jar:/system/framework/android.car.jar
BOOTCLASSPATH=/apex/com.android.art/javalib/core-oj.jar:/apex/com.android.art/javalib/core-libart.jar:/apex/com.android.art/javalib/okhttp.jar:/apex/com.android.art/javalib/bouncycastle.jar:/apex/com.android.art/javalib/apache-xml.jar:/system/framework/framework.jar:/system/framework/framework-graphics.jar:/system/framework/ext.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/apex/com.android.i18n/javalib/core-icu4j.jar:/system/framework/android.car.jar:/apex/com.android.appsearch/javalib/framework-appsearch.jar:/apex/com.android.conscrypt/javalib/conscrypt.jar:/apex/com.android.ipsec/javalib/android.net.ipsec.ike.jar:/apex/com.android.media/javalib/updatable-media.jar:/apex/com.android.mediaprovider/javalib/framework-mediaprovider.jar:/apex/com.android.os.statsd/javalib/framework-statsd.jar:/apex/com.android.permission/javalib/framework-permission.jar:/apex/com.android.permission/javalib/framework-permission-s.jar:/apex/com.android.scheduling/javalib/framework-scheduling.jar:/apex/com.android.sdkext/javalib/framework-sdkextensions.jar:/apex/com.android.tethering/javalib/framework-connectivity.jar:/apex/com.android.tethering/javalib/framework-tethering.jar:/apex/com.android.wifi/javalib/framework-wifi.jar
SHELL=/bin/sh
ANDROID_BOOTLOGO=1
ASEC_MOUNTPOINT=/mnt/asec
HOSTNAME=trout_x86
USER=root
TMPDIR=/data/local/tmp
PATH=/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin
ANDROID_ROOT=/system
ANDROID_I18N_ROOT=/apex/com.android.i18n
trout_x86:/ # 

除了这个命令以外,我们还可以使用echo来获取某个具体环境变量的值,如下所示:

TMPDIR=/data/local/tmp
PATH=/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin
ANDROID_ROOT=/system
ANDROID_I18N_ROOT=/apex/com.android.i18n
trout_x86:/ # echo $USER
root
trout_x86:/ # echo $ANDROID_SOCKET_adbd
19

需要说明的是,上述方法仅针对全局环境变量有效,如果是查看局部环境变量,我们可以使用set命令,如下所示:

trout_x86:/ $ set
DOWNLOAD_CACHE=/data/cache
EPOCHREALTIME=1682412013.723827
EXTERNAL_STORAGE=/sdcard
HOME=/
HOSTNAME=trout_x86
IFS=$' \t\n'
KSHEGID=2000
KSHGID=2000
KSHUID=2000
KSH_VERSION='@(#)MIRBSD KSH R59 2020/05/16 Android'
LINES
LOGNAME=shell
OPTIND=1
PATH=/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin
PATHSEP=:
PGRP=24010
PIPESTATUS[0]=1
PPID=574
PS1=$'${|\n\tlocal e=$?\n\n\t(( e )) && REPLY+="$e|"\n\n\treturn $e\n}$HOSTNAME:${PWD:-?} $ '
PS2='> '
PS3='#? '
PS4='[$EPOCHREALTIME] '
PWD=/
RANDOM=30514
SECONDS=3878
SHELL=/bin/sh
SYSTEMSERVERCLASSPATH=/system/framework/com.android.location.provider.jar:/system/framework/services.jar:/system/framework/ethernet-service.jar:/system/framework/car-frameworks-service.jar:/apex/com.android.appsearch/javalib/service-appsearch.jar:/apex/com.android.media/javalib/service-media-s.jar:/apex/com.android.permission/javalib/service-permission.jar
TERM=xterm-256color
TMOUT=0
TMPDIR=/data/local/tmp
USER=shell
USER_ID=

环境变量加载

那么Android系统是如何加载环境变量的呢,我们可以先看看/system/core/rootdir/Android.mk文件中的内容(节选):

$(LOCAL_BUILT_MODULE): $(LOCAL_PATH)/init.environ.rc.in
	@echo "Generate: $< -> $@"
	@mkdir -p $(dir $@)
	$(hide) cp $< $@
	$(hide) sed -i -e 's?%EXPORT_GLOBAL_ASAN_OPTIONS%?$(EXPORT_GLOBAL_ASAN_OPTIONS)?g' $@
	$(hide) sed -i -e 's?%EXPORT_GLOBAL_GCOV_OPTIONS%?$(EXPORT_GLOBAL_GCOV_OPTIONS)?g' $@
	$(hide) sed -i -e 's?%EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS%?$(EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS)?g' $@
	$(hide) sed -i -e 's?%EXPORT_GLOBAL_HWASAN_OPTIONS%?$(EXPORT_GLOBAL_HWASAN_OPTIONS)?g' $@

Android.mk中这部分其实就引入了一个关键的文件:init.environ.rc.in,在Android12.1中其内容定义如下:

# set up the global environment
on early-init
    export ANDROID_BOOTLOGO 1
    export ANDROID_ROOT /system
    export ANDROID_ASSETS /system/app
    export ANDROID_DATA /data
    export ANDROID_STORAGE /storage
    export ANDROID_ART_ROOT /apex/com.android.art
    export ANDROID_I18N_ROOT /apex/com.android.i18n
    export ANDROID_TZDATA_ROOT /apex/com.android.tzdata
    export EXTERNAL_STORAGE /sdcard
    export ASEC_MOUNTPOINT /mnt/asec
    %EXPORT_GLOBAL_ASAN_OPTIONS%
    %EXPORT_GLOBAL_GCOV_OPTIONS%
    %EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS%
    %EXPORT_GLOBAL_HWASAN_OPTIONS%

该文件内定义的内容其实就是全局的环境变量设置,通过在rc启动文件中使用export命令进行导入。但这只是初始定义的文件,是无法直接生效的。

在上面Android.mk展示的片段中,会将 init.environ.rc.in 中的中%EXPORT_GLOBAL_ASAN_OPTIONS%、%EXPORT_GLOBAL_GCOV_OPTIONS%、%EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS%、%EXPORT_GLOBAL_HWASAN_OPTIONS%使用sed命令进行替换,替换的值来源于Android.mk中获取到的EXPORT_GLOBAL_ASAN_OPTIONS、EXPORT_GLOBAL_GCOV_OPTIONS、EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS、EXPORT_GLOBAL_HWASAN_OPTIONS变量,这部分变量的赋值逻辑如下:

# init.environ.rc

include $(CLEAR_VARS)
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE := init.environ.rc
LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
LOCAL_LICENSE_CONDITIONS := notice
LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)

EXPORT_GLOBAL_ASAN_OPTIONS :=
ifneq ($(filter address,$(SANITIZE_TARGET)),)
  EXPORT_GLOBAL_ASAN_OPTIONS := export ASAN_OPTIONS include=/system/asan.options
  LOCAL_REQUIRED_MODULES := asan.options $(ASAN_OPTIONS_FILES) $(ASAN_EXTRACT_FILES)
endif

EXPORT_GLOBAL_HWASAN_OPTIONS :=
ifneq ($(filter hwaddress,$(SANITIZE_TARGET)),)
  ifneq ($(HWADDRESS_SANITIZER_GLOBAL_OPTIONS),)
    EXPORT_GLOBAL_HWASAN_OPTIONS := export HWASAN_OPTIONS $(HWADDRESS_SANITIZER_GLOBAL_OPTIONS)
  endif
endif

EXPORT_GLOBAL_GCOV_OPTIONS :=
ifeq ($(NATIVE_COVERAGE),true)
  EXPORT_GLOBAL_GCOV_OPTIONS := export GCOV_PREFIX /data/misc/trace
endif

EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS :=
ifeq ($(CLANG_COVERAGE),true)
  EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS := export LLVM_PROFILE_FILE /data/misc/trace/clang-%20m.profraw
endif

由此,在编译阶段对 init.environ.rc.in 进行处理,得到init.environ.rc,打包后位于根目录;init进程在启动时就会读取该rc启动文件,进行环境变量的设置。

在我的环境中, init.environ.rc 文件内容如下:

# set up the global environment
on early-init
    export ANDROID_BOOTLOGO 1
    export ANDROID_ROOT /system
    export ANDROID_ASSETS /system/app
    export ANDROID_DATA /data
    export ANDROID_STORAGE /storage
    export ANDROID_ART_ROOT /apex/com.android.art
    export ANDROID_I18N_ROOT /apex/com.android.i18n
    export ANDROID_TZDATA_ROOT /apex/com.android.tzdata
    export EXTERNAL_STORAGE /sdcard
    export ASEC_MOUNTPOINT /mnt/asec

除了init.environ.rc.in中定义的全局环境变量,还有部分环境变量是在init.rc中进行导入的,如DOWNLOAD_CACHE环境变量。如下所示:


    # enable armv8_deprecated instruction hooks
    write /proc/sys/abi/swp 1

    # Linux's execveat() syscall may construct paths containing /dev/fd
    # expecting it to point to /proc/self/fd
    symlink /proc/self/fd /dev/fd

    export DOWNLOAD_CACHE /data/cache                                                                                                                                                                       

    # This allows the ledtrig-transient properties to be created here so
    # that they can be chown'd to system:system later on boot 
    write /sys/class/leds/vibrator/trigger "transient"

那么环境变量具体是如何加载的呢,加载的过程其实就在init进程中进行。在rc文件中定义的export导入环境变量会被init进程以command的形式执行,从而导入到系统环境中,具体代码细节可以参考—— system/core/init/action_manager.cpp与system/core/init/action.cpp:

void ActionManager::ExecuteOneCommand() {
    {   
        auto lock = std::lock_guard{event_queue_lock_};
        // Loop through the event queue until we have an action to execute
        while (current_executing_actions_.empty() && !event_queue_.empty()) {
            for (const auto& action : actions_) {
                if (std::visit([&action](const auto& event) { return action->CheckEvent(event); },
                               event_queue_.front())) {
                    current_executing_actions_.emplace(action.get());
                }   
            }   
            event_queue_.pop();
        }   
    }   

    if (current_executing_actions_.empty()) {
        return;
    }   

    auto action = current_executing_actions_.front();

    if (current_command_ == 0) {
        std::string trigger_name = action->BuildTriggersString();
        LOG(INFO) << "processing action (" << trigger_name << ") from (" << action->filename()
                  << ":" << action->line() << ")";
    }   

    action->ExecuteOneCommand(current_command_);

    // If this was the last command in the current action, then remove
    // the action from the executing list.
    // If this action was oneshot, then also remove it from actions_.
    ++current_command_;
    if (current_command_ == action->NumCommands()) {
        current_executing_actions_.pop();
        current_command_ = 0;
        if (action->oneshot()) {
            auto eraser = [&action](std::unique_ptr<Action>& a) { return a.get() == action; };
            actions_.erase(std::remove_if(actions_.begin(), actions_.end(), eraser),
                           actions_.end());
        }   
    }   
}    
void Action::ExecuteCommand(const Command& command) const {
    android::base::Timer t;
    auto result = command.InvokeFunc(subcontext_);
    auto duration = t.duration();

    // Any action longer than 50ms will be warned to user as slow operation
    if (!result.has_value() || duration > 50ms ||
        android::base::GetMinimumLogSeverity() <= android::base::DEBUG) {
        std::string trigger_name = BuildTriggersString();
        std::string cmd_str = command.BuildCommandString();

        LOG(INFO) << "Command '" << cmd_str << "' action=" << trigger_name << " (" << filename_
                  << ":" << command.line() << ") took " << duration.count() << "ms and "
                  << (result.ok() ? "succeeded" : "failed: " + result.error().message());
    }   
}     

除了export导入环境变量,rc启动文件中定义的action都会通过类似的方式执行。

现在我们可以明确知道的是,Android系统中的环境变量可以通过rc来进行配置,但若我们观察得更仔细一点,会发现还有一些环境变量是没有通过rc配置的,那么这部分环境变量是怎么加载的呢——其实是通过setenv系统调用进行配置的,这里我们以PATH环境变量为例子:

86_64:/ # echo $PATH
/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin

我们通过在rc中进行搜索,会发现没有地方去export该环境变量,但倘若我们在init进程源码中使用搜索setenv,我们就会有惊喜的发现:

// Update $PATH in the case the second stage init is newer than first stage init, where it is first set.
    if (setenv("PATH", _PATH_DEFPATH, 1) != 0) {
        PLOG(FATAL) << "Could not set $PATH to '" << _PATH_DEFPATH << "' in second stage";
    }

很明显,PATH环境变量是通过setenv去进行设置的,这属于在代码内硬编码;我们看看_PATH_DEFPATH宏定义:

/** Default shell search path. */
#define _PATH_DEFPATH "/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin"

这与我们观察到的环境变量是一致的。

如何添加环境变量

前面我们已经了解到环境变量是如何加载的了,现在假设我们需要在Android中添加自己的环境变量,需要怎么做呢,这里列举几种办法:

1.在 init.environ.rc.in中进行添加,这种方式适合全局环境变量;可以在不修改init.rc的情况下添加环境变量,比较推荐使用。此时可以参考rc启动文件的语法规则使用export命令导入即可。

2.在init.rc中进行添加,这种方式与上述第一种方式类似,适合添加全局环境变量,不过这种方式会直接修改init.rc,可能会导致其他问题,如果不小心修改错误,可能会导致系统无法开机。这里给一个AOSP Android12中的示例:

// system/etc/init/hw/init.rc 
# Linux's execveat() syscall may construct paths containing /dev/fd
# expecting it to point to /proc/self/fd
symlink /proc/self/fd /dev/fd

export DOWNLOAD_CACHE /data/cache

# This allows the ledtrig-transient properties to be created here so
# that they can be chown'd to system:system later on boot
write /sys/class/leds/vibrator/trigger "transient"

# This is used by Bionic to select optimized routines.
write /dev/cpu_variant:${ro.bionic.arch} ${ro.bionic.cpu_variant}
chmod 0444 /dev/cpu_variant:${ro.bionic.arch}
write /dev/cpu_variant:${ro.bionic.2nd_arch} ${ro.bionic.2nd_cpu_variant}

3.如果我们需要的并非全局环境变量,如只是某些进程需要使用的环境变量,可以在对应进程的rc启动文件内通过setenv进行导入,如下所示:

service myservice /system/bin/myservice
        user system
        group root
        setenv MY_VARIABLE  value

其中MY_VARIABLE为环境变量名,value为对应的值。

4.在init.rc中使用单独的服务来进行导入,示例如下:

service setenv /system/bin/sh -c "export FOO=bar; exec sleep 3600"
    class main
    oneshot

这部分定义了一个名为setenv的服务,其执行的内容为:通过sh终端export名为FOO,值为bar的环境变量,之后sleep 3600s;其中oneshot表示只在开机阶段执行一次。

5.直接修改init进程源码,通过setenv系统调用设置环境变量

程序内使用环境变量

在程序内使用环境变量,我们会用到两个函数:setenv与getenv,两者皆需要引用stdlib.h头文件。

getenv函数:

char *getenv(const char *name);

使用示例:

   const char* reject_kill_server = getenv("ADB_REJECT_KILL_SERVER");
    if (reject_kill_server && strcmp(reject_kill_server, "1") == 0) {
        adb_set_reject_kill_server(true);
    }

返回值为指针,空指针则表明没有匹配的环境变量存在。

setenv函数:

int setenv(const char name, const char value, int overwrite);

返回值0代表成功,返回-1代表失败,同时会设置errno标志位。

需要注意的是setenv函数设置的环境变量不是全局的,只对对应进程以及子进程有效。这也是为什么Android系统所有的全局环境变量都是放在rc启动文件内,因为只有init进程会去解析这些rc启动文件导入环境变量,并传递给后续的所有进程,自然也就成为“全局环境变量”。

Happy
Happy
100 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %
FranzKafka95
FranzKafka95

极客,文学爱好者。如果你也喜欢我,那你大可不必害羞。

文章: 86

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注

zh_CNCN