init 进程是 Android 系统中用户空间的第一个进程,它被赋予了很多极其重要的工作职责,init 进程相关源码位于 system/core/init,本篇博客我们就一起来学习 init 进程(基于 Android 7.0)。
init 的入口函数为 main,位于 system/core/init/init.cpp
- intmain(intargc,char** argv) {if(!strcmp(basename(argv[0]),"ueventd")) {returnueventd_main(argc, argv);
- }if(!strcmp(basename(argv[0]),"watchdogd")) {returnwatchdogd_main(argc, argv);
- }// Clear the umask.umask(0);
- add_environment("PATH", _PATH_DEFPATH);boolis_first_stage = (argc ==1) || (strcmp(argv[1],"--second-stage") !=0);// Get the basic filesystem setup we need put together in the initramdisk
- // on / and then we'll let the rc file figure out the rest.
- //1.创建一些文件夹,并挂载设备,这些都是与Linux相关
- if(is_first_stage) {
- mount("tmpfs","/dev","tmpfs", MS_NOSUID,"mode=0755");
- mkdir("/dev/pts",0755);
- mkdir("/dev/socket",0755);
- mount("devpts","/dev/pts","devpts",0, NULL);#define MAKE_STR(x) __STRING(x)mount("proc","/proc","proc",0,"hidepid=2,gid="MAKE_STR(AID_READPROC));
- mount("sysfs","/sys","sysfs",0, NULL);
- }// We must have some place other than / to create the device nodes for
- // kmsg and null, otherwise we won't be able to remount / read-only
- // later on. Now that tmpfs is mounted on /dev, we can actually talk
- // to the outside world.
- //2.重定向标准输入,输出,错误输出到/dev/_null_open_devnull_stdio();3.初始化内核log系统
- klog_init();
- klog_set_level(KLOG_NOTICE_LEVEL);
- NOTICE("init %s started!\n", is_first_stage"first stage":"second stage");if(!is_first_stage) {// Indicate that booting is in progress to background fw loaders, etc.close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC,0000));//4.初始化和属性相关资源property_init();// If arguments are passed both on the command line and in DT,
- // properties set in DT always have priority over the command-line ones.process_kernel_dt();
- process_kernel_cmdline();// Propagate the kernel variables to internal variables
- // used by init as well as the current required properties.export_kernel_boot_props();
- }// Set up SELinux, including loading the SELinux policy if we're in the kernel domain.
- 5.完成SELinux相关工作
- selinux_initialize(is_first_stage);// If we're in the kernel domain, re-exec init to transition to the init domain now
- // that the SELinux policy has been loaded.
- if(is_first_stage) {6.重新设置属性if(restorecon("/init") == -1) {
- ERROR("restorecon failed: %s\n", strerror(errno));
- security_failure();
- }char* path = argv[0];char* args[] = { path,const_cast<char*>("--second-stage"),nullptr};if(execv(path, args) == -1) {
- ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno));
- security_failure();
- }
- }// These directories were necessarily created before initial policy load
- // and therefore need their security context restored to the proper value.
- // This must happen before /dev is populated by ueventd.NOTICE("Running restorecon...\n");
- restorecon("/dev");
- restorecon("/dev/socket");
- restorecon("/dev/__properties__");
- restorecon("/property_contexts");
- restorecon_recursive("/sys");7.创建epoll句柄
- epoll_fd = epoll_create1(EPOLL_CLOEXEC);if(epoll_fd == -1) {
- ERROR("epoll_create1 failed: %s\n", strerror(errno));exit(1);
- }8.装载子进程信号处理器
- signal_handler_init();
- property_load_boot_defaults();
- export_oem_lock_status();//9.启动属性服务start_property_service();constBuiltinFunctionMap function_map;
- Action::set_function_map(&function_map);
- Parser& parser = Parser::GetInstance();
- parser.AddSectionParser("service",std::make_unique());
- parser.AddSectionParser("on",std::make_unique());
- parser.AddSectionParser("import",std::make_unique());
- //10.解析init.rc配置文件parser.ParseConfig("/init.rc");
- ActionManager& am = ActionManager::GetInstance();
- am.QueueEventTrigger("early-init");// Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...am.QueueBuiltinAction(wait_for_coldboot_done_action,"wait_for_coldboot_done");// ... so that we can start queuing up actions that require stuff from /dev.am.QueueBuiltinAction(mix_hwrng_into_linux_rng_action,"mix_hwrng_into_linux_rng");
- am.QueueBuiltinAction(set_mmap_rnd_bits_action,"set_mmap_rnd_bits");
- am.QueueBuiltinAction(keychord_init_action,"keychord_init");
- am.QueueBuiltinAction(console_init_action,"console_init");// Trigger all the boot actions to get us started.am.QueueEventTrigger("init");// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
- // wasn't ready immediately after wait_for_coldboot_doneam.QueueBuiltinAction(mix_hwrng_into_linux_rng_action,"mix_hwrng_into_linux_rng");// Don't mount filesystems or start core system services in charger mode.
- std::stringbootmode = property_get("ro.bootmode");if(bootmode =="charger") {
- am.QueueEventTrigger("charger");
- }else{
- am.QueueEventTrigger("late-init");
- }// Run all property triggers based on current state of the properties.am.QueueBuiltinAction(queue_property_triggers_action,"queue_property_triggers");while(true) {if(!waiting_for_exec) {
- am.ExecuteOneCommand();
- restart_processes();
- }inttimeout = -1;if(process_needs_restart) {
- timeout = (process_needs_restart - gettime()) *1000;if(timeout <0)
- timeout =0;
- }if(am.HasMoreCommands()) {
- timeout =0;
- }
- bootchart_sample(&timeout);
- epoll_event ev;intnr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev,1, timeout));if(nr == -1) {
- ERROR("epoll_wait failed: %s\n", strerror(errno));
- }else if(nr ==1) {
- ((void(*)()) ev.data.ptr)();
- }
- }return 0;
- }
从上面代码中可以精简归纳 init 的 main 方法做的事情: 1. 创建文件系统目录并挂载相关的文件系统 2. 屏蔽标准的输入输出 3. 初始化内核 log 系统 4. 调用 property_init 初始化属性相关的资源 5. 完成 SELinux 相关工作 6. 重新设置属性 7. 创建 epoll 句柄 8. 装载子进程信号处理器 9. 通过 property_start_service 启动属性服务 10. 通过 parser.ParseConfig("/init.rc") 来解析 init.rc 接下来对上述部分步骤,进行详细解析。
- //清除屏蔽字(file mode creation mask),保证新建的目录的访问权限不受屏蔽字影响。umask(0);
- add_environment("PATH", _PATH_DEFPATH);boolis_first_stage = (argc ==1) || (strcmp(argv[1],"--second-stage") !=0);// Get the basic filesystem setup we need put together in the initramdisk
- if(is_first_stage) {
- mount("tmpfs","/dev","tmpfs", MS_NOSUID,"mode=0755");
- mkdir("/dev/pts",0755);
- mkdir("/dev/socket",0755);
- mount("devpts","/dev/pts","devpts",0,NULL);#define MAKE_STR(x) __STRING(x)mount("proc","/proc","proc",0,"hidepid=2,gid="MAKE_STR(AID_READPROC));
- mount("sysfs","/sys","sysfs",0,NULL);
- }
该部分主要用于创建和挂载启动所需的文件目录。 需要注意的是,在编译 Android 系统源码时,在生成的根文件系统中,并不存在这些目录,它们是系统运行时的目录,即当系统终止时,就会消失。
在 init 初始化过程中,Android 分别挂载了 tmpfs,devpts,proc,sysfs 这 4 类文件系统。
- open_devnull_stdio();
前文生成 / dev 目录后,init 进程将调用 open_devnull_stdio 函数,屏蔽标准的输入输出。 open_devnull_stdio 函数会在 / dev 目录下生成 null 设备节点文件,并将标准输入、标准输出、标准错误输出全部重定向到 null 设备中。
- voidopen_devnull_stdio(void)
- {// Try to avoid the mknod() call if we can. Since SELinux makes
- // a /dev/null replacement available for free, let's use it.
- intfd = open("/sys/fs/selinux/null", O_RDWR);if(fd == -1) {// OOPS, /sys/fs/selinux/null isn't available, likely because
- // /sys/fs/selinux isn't mounted. Fall back to mknod.
- static const char*name ="/dev/__null__";if(mknod(name, S_IFCHR |0600, (1<<8) |3) ==0) {
- fd = open(name, O_RDWR);
- unlink(name);
- }if(fd == -1) {exit(1);
- }
- }
- dup2(fd,0);
- dup2(fd,1);
- dup2(fd,2);if(fd >2) {
- close(fd);
- }
- }
open_devnull_stdio 函数定义于 system/core/init/util.cpp 中。
这里需要说明的是,dup2 函数的作用是用来复制一个文件的描述符,通常用来重定向进程的 stdin、stdout 和 stderr。它的函数原形是:
int dup2(int oldfd, int targetfd)
该函数执行后,targetfd 将变成 oldfd 的复制品。
因此上述过程其实就是:创建出 null 设备后,将 0、1、2 绑定到 null 设备上。因此 init 进程调用 open_devnull_stdio 函数后,通过标准的输入输出无法输出信息。
- if(!is_first_stage) {.......property_init();.......}
调用 property_init 初始化属性域。在 Android 平台中,为了让运行中的所有进程共享系统运行时所需要的各种设置值,系统开辟了属性存储区域,并提供了访问该区域的 API。
需要强调的是,在 init 进程中有部分代码块以 is_first_stage 标志进行区分,决定是否需要进行初始化,而 is_first_stage 的值,由 init 进程 main 函数的入口参数决定。 其原因在于,在引入 selinux 机制后,有些操作必须要在内核态才能完成; 但 init 进程作为 android 的第一个进程,又是运行在用户态的。 于是,最终设计为用 is_first_stage 进行区分 init 进程的运行状态。init 进程在运行的过程中,会完成从内核态到用户态的切换。
- voidproperty_init(){
- if (__system_property_area_init()) {ERROR("Failed to initialize property area\n");exit(1);
- }
- }
property_init 函数定义于 system/core/init/property_service.cpp 中,如上面代码所示,最终调用_system_property_area_init 函数初始化属性域。
- // Set up SELinux, including loading the SELinux policyifwe're inthe kernel domain.
- selinux_initialize(is_first_stage);
init 进程进程调用 selinux_initialize 启动 SELinux。从注释来看,init 进程的运行确实是区分用户态和内核态的。
- static voidselinux_initialize(boolin_kernel_domain) {
- Timer t;
- selinux_callback cb;//用于打印log的回调函数cb.func_log = selinux_klog_callback;
- selinux_set_callback(SELINUX_CB_LOG, cb);//用于检查权限的回调函数cb.func_audit = audit_callback;
- selinux_set_callback(SELINUX_CB_AUDIT, cb);if(in_kernel_domain) {//内核态处理流程INFO("Loading SELinux policy...\n");//用于加载sepolicy文件。该函数最终将sepolicy文件传递给kernel,这样kernel就有了安全策略配置文件,后续的MAC才能开展起来。
- if(selinux_android_load_policy() <0) {
- ERROR("failed to load policy: %s\n", strerror(errno));
- security_failure();
- }//内核中读取的信息
- boolkernel_enforcing = (security_getenforce() ==1);//命令行中得到的数据
- boolis_enforcing = selinux_is_enforcing();if(kernel_enforcing != is_enforcing) {//用于设置selinux的工作模式。selinux有两种工作模式:
- //1、"permissive",所有的操作都被允许(即没有MAC),但是如果违反权限的话,会记录日志
- //2、"enforcing",所有操作都会进行权限检查。在一般的终端中,应该工作于enforing模式
- if(security_setenforce(is_enforcing)) {
- ........//将重启进入recovery modesecurity_failure();
- }
- }if(write_file("/sys/fs/selinux/checkreqprot","0") == -1) {
- security_failure();
- }
- NOTICE("(Initializing SELinux %s took %.2fs.)\n",
- is_enforcing"enforcing":"non-enforcing", t.duration());
- }else{
- selinux_init_all_handles();
- }
- }
- // If we're in the kernel domain, re-exec init to transition to the init domain now that the SELinux policy has been loaded.
- if (is_first_stage) {
- //按selinux policy要求,重新设置init文件属性
- if (restorecon("/init") == -1) {
- ERROR("restorecon failed: %s\n", strerror(errno));
- security_failure();
- }
- char * path = argv[0];
- char * args[] = {
- path,
- const_cast < char * >("--second-stage"),
- nullptr
- };
- //这里就是前面所说的,启动用户态的init进程,即second-stage
- if (execv(path, args) == -1) {
- ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno));
- security_failure();
- }
- }
- // These directories were necessarily created before initial policy load
- // and therefore need their security context restored to the proper value.
- // This must happen before /dev is populated by ueventd.
- INFO("Running restorecon...\n");
- restorecon("/dev");
- restorecon("/dev/socket");
- restorecon("/dev/__properties__");
- restorecon_recursive("/sys");
上述文件节点在加载 Sepolicy 之前已经被创建了,因此在加载完 Sepolicy 后,需要重新设置相关的属性。
- start_property_service();
init 进程在共享内存区域中,创建并初始化属性域。其它进程可以访问属性域中的值,但更改属性值仅能在 init 进程中进行。这就是 init 进程调用 start_property_service 的原因。其它进程修改属性值时,要预先向 init 进程提交值变更申请,然后 init 进程处理该申请,并修改属性值。在访问和修改属性时,init 进程都可以进行权限控制。
- voidstart_property_service() {
- //创建了一个非阻塞socket
- property_set_fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,0666,0,0, NULL);if(property_set_fd == -1) {
- ERROR("start_property_service socket creation failed: %s\n", strerror(errno));exit(1);
- }
- //调用listen函数监听property_set_fd, 于是该socket变成一个server
- listen(property_set_fd,8);
- //监听server socket上是否有数据到来
- register_epoll_handler(property_set_fd, handle_property_set_fd);
- }
我们知道,在 create_socket 函数返回套接字 property_set_fd 时,property_set_fd 是一个主动连接的套接字。此时,系统假设用户会对这个套接字调用 connect 函数,期待它主动与其它进程连接。
由于在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接,于是需要调用 listen 函数使用主动连接套接字变为被连接套接字,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。
因此,调用 listen 后,init 进程成为一个服务进程,其它进程可以通过 property_set_fd 连接 init 进程,提交设置系统属性的申请。
listen 函数的第二个参数,涉及到一些网络的细节。
在进程处理一个连接请求的时候,可能还存在其它的连接请求。因为 TCP 连接是一个过程,所以可能存在一种半连接的状态。有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。
因此,内核会在自己的进程空间里维护一个队列,以跟踪那些已完成连接但服务器进程还没有接手处理的用户,或正在进行的连接的用户。这样的一个队列不可能任意大,所以必须有一个上限。listen 的第二个参数就是告诉内核使用这个数值作为上限。因此,init 进程作为系统属性设置的服务器,最多可以同时为 8 个试图设置属性的用户提供服务。
在启动配置属性服务的最后,调用函数 register_epoll_handler。该函数将利用之前创建出的 epoll 句柄监听 property_set_fd。当 property_set_fd 中有数据到来时,init 进程将利用 handle_property_set_fd 函数进行处理。
- staticvoidhandle_property_set_fd() {..........
- if((s=accept(property_set_fd, (struct sockaddr*)&addr,&addr_size))< 0) {return;
- }........r=TEMP_FAILURE_RETRY(recv(s,&msg, sizeof(msg), MSG_DONTWAIT));.........switch(msg.cmd) {.........}.........}
handle_propery_set_fd 函数实际上是调用 accept 函数监听连接请求,接收 property_set_fd 中到来的数据,然后利用 recv 函数接受到来的数据,最后根据到来数据的类型,进行设置系统属性等相关操作,在此不做深入分析。
介绍一下系统属性改变的一些用途。 在 init.rc 中定义了一些与属性相关的触发器。当某个条件相关的属性被改变时,与该条件相关的触发器就会被触发。举例来说,如下面代码所示,debuggable 属性变为 1 时,将执行启动 console 进程等操作。
- on property:ro.debuggable=1# Give writestoanyoneforthe trace folderondebug builds.
- # The folderisusedtostoremethod traces.chmod0773 /data/misc/trace
- start console
总结一下,其它进程修改系统属性时,大致的流程如下图所示:其它的进程像 init 进程发送请求后,由 init 进程检查权限后,修改共享内存区。
init.rc 是系统配置文件,位于 system/core/rootdir/init.rc,Android 7.0 中对 init.rc 文件进行了拆分,每个服务一个 rc 文件。
init.rc 文件是在 init 进程启动后执行的启动脚本,文件中记录着 init 进程需执行的操作。在 Android 系统中,使用 init.rc 和 init.{hardware}.rc 两个文件。
其中 init.rc 文件在 Android 系统运行过程中用于通用的环境设置与进程相关的定义,init.{hardware}.rc(例如,高通有 init.qcom.rc,MTK 有 init.mediatek.rc)用于定义 Android 在不同平台下的特定进程和环境设置等。
init.rc 文件大致分为两大部分,一部分是以 "on" 关键字开头的动作列表(action list):
- on early-init
- # Set init and its forked children's oom_adj.
- write/proc/1/oom_score_adj -1000.........
- start ueventd
另一部分是以 "service" 关键字开头的服务列表(service list):
- service ueventd /sbin/ueventdclass corecritical
- seclabelu:r:ueventd:s0
动作列表用于创建所需目录,以及为某些特定文件指定权限,而服务列表用来记录 init 进程需要启动的一些子进程。如上面代码所示,service 关键字后的第一个字符串表示服务(子进程)的名称,第二个字符串表示服务的执行路径。
接下来,我们从 ParseConfig 函数入手,逐步分析整个解析过程 (函数定义于 system/core/init/ Init_parser.cpp 中):
- boolParser::ParseConfig(const std::string& path) {if(is_dir(path.c_str())) {//传入参数为目录地址
- returnParseConfigDir(path);
- }//传入参数为文件地址
- returnParseConfigFile(path);
- }
- bool Parser::ParseConfigDir(const std::string&path) {...........std::unique_ptr<DIR, int(*)(DIR*)>config_dir(opendir(path.c_str()), closedir);..........
- //看起来很复杂,其实就是递归目录
- while((current_file=readdir(config_dir.get()))) {
- std::stringcurrent_path=android::base::StringPrintf("%s/%s", path.c_str(), current_file->d_name);if(current_file->d_type==DT_REG) {//最终还是靠ParseConfigFile来解析实际的文件
- if(!ParseConfigFile(current_path)) {.............}
- }
- }
- }
从上面的代码可以看出,解析 init.rc 文件的函数是 ParseConfigFile:
- bool Parser::ParseConfigFile(const std::string&path) {
- INFO("Parsing file %s...\n", path.c_str());
- Timer t;
- std::string data;//读取路径指定文件中的内容,保存为字符串形式
- if(!read_file(path.c_str(),&data)) {return false;
- }data.push_back('\n');// TODO: fix parse_config.
- //解析获取的字符串ParseData(path,data);
- for (const auto&sp : section_parsers_) {
- sp.second->EndFile(path);
- }// Turning this on and letting the INFO logging be discarded adds 0.2s to
- // Nexus 9 boot time, so it's disabled by default.
- if(false) DumpState();
- NOTICE("(Parsing %s took %.2fs.)\n", path.c_str(), t.duration());return true;
- }
ParseData 函数定义于 system/core/init/init_parser.cpp 中,根据关键字解析出服务和动作。动作与服务会以链表节点的形式注册到 service_list 与 action_list 中,service_list 与 action_list 是 init 进程中声明的全局结构体
- voidParser::ParseData(const std::string& filename,const std::string& data) {
- .......
- parse_state state;
- .......std::vector<std::string>args;for(;;) {//next_token以行为单位分割参数传递过来的字符串
- //最先走到T_TEXT分支
- switch(next_token(&state)) {caseT_EOF:if(section_parser) {//EOF,解析结束section_parser->EndSection();
- }return;caseT_NEWLINE:
- state.line++;if(args.empty()) {break;
- }//创建parser时,会为init.rc中以service,on,import开头的都定义了对应的解析parser
- //这里就是根据第一个参数,判断是否有对应的parser
- if(section_parsers_.count(args[0])) {if(section_parser) {//结束上一个parser的工作,将构造出的对象加入到对应的service_list与action_list中section_parser->EndSection();
- }//获取参数对应的parsersection_parser = section_parsers_[args[0]].get();std::stringret_err;//调用实际parser的ParseSection函数
- if(!section_parser->ParseSection(args, &ret_err)) {
- parse_error(&state,"%s\n", ret_err.c_str());
- section_parser =nullptr;
- }
- }else if(section_parser) {std::stringret_err;//如果第一个参数不是service,on,import
- //则调用前一个parser的ParseLineSection函数
- //这里相当于解析一个参数块的子项
- if(!section_parser->ParseLineSection(args, state.filename, state.line, &ret_err)) {
- parse_error(&state,"%s\n", ret_err.c_str());
- }
- }//清空本次解析的数据args.clear();break;caseT_TEXT://将本次解析的内容写入到args中args.emplace_back(state.text);break;
- }
- }
- }
这里的解析看起来比较复杂,在 6.0 以前的版本中,整个解析是面向过程的。init 进程统一调用一个函数来进行解析,然后在该函数中利用 switch-case 的形式,根据解析的内容进行相应的处理。 在 Android 7.0 中,为了更好地封装及面向对象,对于不同的关键字定义了不同的 parser 对象,每个对象通过多态实现自己的解析操作。
在 init 进程 main 函数中,创建各种 parser 的代码如下:
- ...........Parser& parser =Parser::GetInstance();
- parser.AddSectionParser("service",std::make_unique<ServiceParser>());
- parser.AddSectionParser("on",std::make_unique<ActionParser>());
- parser.AddSectionParser("import",std::make_unique<ImportParser>());
- ...........
看看三个 Parser 的定义:
- class ServiceParser: public SectionParser {......
- }
- class ActionParser: public SectionParser {......
- }
- class ImportParser: public SectionParser {.......
- }
可以看到三个 Parser 均是继承 SectionParser,具体的实现各有不同,我们以比较常用的 ServiceParser 和 ActionParser 为例
ServiceParser ServiceParser 定义于 system/core/init/service.cpp 中。从前面的代码,我们知道,解析一个 service 块,首先需要调用 ParseSection 函数,接着利用 ParseLineSection 处理子块,解析完所有数据后,调用 EndSection。 因此,我们着重看看 ServiceParser 的这三个函数:
- boolServiceParser::ParseSection(.....) {
- .......const std::string& name = args[1];
- .......std::vector<std::string>str_args(args.begin() +2, args.end());//主要根据参数,构造出一个service对象service_ =std::make_unique(name, "default", str_args);return true;
- }
- //注意这里已经在解析子项了bool ServiceParser::ParseLineSection(......) const {//调用service对象的HandleLine
- returnservice_?service_->HandleLine(args, err) :false;
- }
- boolService::HandleLine(.....) {
- ........//OptionHandlerMap继承自keywordMap<OptionHandler>
- static constOptionHandlerMap handler_map;//根据子项的内容,找到对应的handler函数
- //FindFunction定义于keyword模块中,FindFunction方法利用子类生成对应的map中,然后通过通用的查找方法,即比较键值找到对应的处理函数
- autohandler = handler_map.FindFunction(args[0], args.size() -1, err);if(!handler) {return false;
- }//调用handler函数
- return(this->*handler)(args, err);
- }
- class Service: :OptionHandlerMap: public KeywordMap < OptionHandler > {...........Service: :OptionHandlerMap: :Map & Service: :OptionHandlerMap: :map() const {
- constexpr std: :size_t kMax = std: :numeric_limits < std: :size_t > ::max();
- static const Map option_handlers = {
- {
- "class",
- {
- 1,
- 1,
- &Service: :HandleClass
- }
- },
- {
- "console",
- {
- 0,
- 0,
- &Service: :HandleConsole
- }
- },
- {
- "critical",
- {
- 0,
- 0,
- &Service: :HandleCritical
- }
- },
- {
- "disabled",
- {
- 0,
- 0,
- &Service: :HandleDisabled
- }
- },
- {
- "group",
- {
- 1,
- NR_SVC_SUPP_GIDS +
来源: http://blog.csdn.net/u012124438/article/details/70990816