Android7.0 init进程源码分析

本文详细分析了Android 7.0系统中init进程的启动过程,包括创建文件系统目录、屏蔽标准输入输出、初始化内核log系统、属性域、SELinux、信号处理器等,以及解析init.rc文件和启动服务的流程。通过分析,揭示了init进程在Android系统中的重要作用和工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目的

看过一些blog和相关的书籍,大多数文章在介绍init进程时,参考的代码比较久远,同时不同文章行文的重点不太一样,因此决定自己试着来分析一下,并作相应的记录。

背景

当linux内核启动之后,运行的第一个进程是init,这个进程是一个守护进程,它的生命周期贯穿整个linux 内核运行的始终, linux中所有其它的进程的共同始祖均为init进程。
Android系统是运作在linux内核上的,为了启动并运行整个android系统,google实现了android系统的init进程。

Android init进程的入口文件在system/core/init/init.cpp中。在main函数中,按顺序主要进行了以下工作:

1、创建文件系统目录并挂载相关的文件系统

//清除屏蔽字(file mode creation mask),保证新建的目录的访问权限不受屏蔽字影响。
umask(0);

add_environment("PATH", _PATH_DEFPATH);

bool is_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类文件系统。

tmpfs是一种虚拟内存文件系统,它会将所有的文件存储在虚拟内存中,如果你将tmpfs文件系统卸载后,那么其下的所有的内容将不复存在。
tmpfs既可以使用RAM,也可以使用交换分区,会根据你的实际需要而改变大小。tmpfs的速度非常惊人,毕竟它是驻留在RAM中的,即使用了交换分区,性能仍然非常卓越。
由于tmpfs是驻留在RAM的,因此它的内容是不持久的。断电后,tmpfs的内容就消失了,这也是被称作tmpfs的根本原因。

devpts文件系统为伪终端提供了一个标准接口,它的标准挂接点是/dev/ pts。只要pty的主复合设备/dev/ptmx被打开,就会在/dev/pts下动态的创建一个新的pty设备文件。

proc文件系统是一个非常重要的虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数。

与proc文件系统类似,sysfs文件系统也是一个不占有任何磁盘空间的虚拟文件系统。它通常被挂接在/sys目录下。sysfs文件系统是Linux2.6内核引入的,它把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取。

2、屏蔽标准的输入输出

open_devnull_stdio();

前文生成/dev目录后,init进程将调用open_devnull_stdio函数,屏蔽标准的输入输出。
open_devnull_stdio函数会在/dev目录下生成__null__设备节点文件,并将标准输入、标准输出、标准错误输出全部重定向到__null__设备中。

void open_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.
    int fd = 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函数后,通过标准的输入输出无法输出信息。

3、初始化内核log系统
我们继续回到init进程的main函数,init进程通过klog_init函数,提供输出log信息的设备。

klog_init();
klog_set_level(KLOG_NOTICE_LEVEL)
void klog_init(void) {
    if (klog_fd >= 0) return; /* Already initialized */

    klog_fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
    if (klog_fd >= 0) {
        return;
    }

    static const char* name = "/dev/__kmsg__";
    if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) {
        klog_fd = open(name, O_WRONLY | O_CLOEXEC);
        unlink(name);
    }
}

klog_init函数定义于system/core/libcutils/klog.c中。通过klog_init函数,init进程生成kmsg设备节点文件。该设备可以调用内核信息输出函数printk,以输出log信息。

4、初始化属性域

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进程在运行的过程中,会完成从内核态到用户态的切换。

void property_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函数初始化属性域。

5、完成SELinux相关工作

// Set up SELinux, including loading the SELinux policy if we're in the kernel domain.
selinux_initialize(is_first_stage);

init进程进程调用selinux_initialize启动SELinux。从注释来看,init进程的运行确实是区分用户态和内核态的。

static void selinux_initialize(bool in_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();
        }
		//内核中读取的信息
        bool kernel_enforcing = (security_getenforce() == 1);
        //命令行中得到的数据
        bool is_enforcing = selinux_is_enforcing();
        if (kernel_enforcing != is_enforcing) {
	        //用于设置selinux的工作模式。selinux有两种工作模式:
	        //1、”permissive”,所有的操作都被允许(即没有MAC),但是如果违反权限的话,会记录日志
	        //2、”enforcing”,所有操作都会进行权限检查。在一般的终端中,应该工作于enforing模式
	        if(security_setenforce(is_enforcing)) {
		        ........
		        //将重启进入recovery mode
		        security_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();
    }
}

6、重新设置属性

// 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后,需要重新设置相关的属性。

7、创建epoll句柄
如下面代码所示,init进程调用epoll_create1创建epoll句柄。

epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
	ERROR("epoll_create1 failed: %s\n", strerror(errno));
	exit(1);
}

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。

<
<think>我们正在处理一个关于Android系统启动加密的问题。用户已经在`init.mt6765.rc`文件中设置了`ro.crypto.state`和`ro.crypto.type`属性为`unencrypted`,但系统启动时仍然要求输入密码。用户希望实现无密码启动。 首先,我们需要理解Android的加密机制。在Android中,设备加密通常涉及全盘加密(FDE)或基于文件的加密(FBE)。从Android 7.0开始,Google引入了基于文件的加密(FBE),它允许独立加密每个文件,并且可以同时加密元数据和内容。但是,在Android 9中,仍然可能使用全盘加密(FDE)或基于文件的加密(FBE),这取决于设备的配置。 属性`ro.crypto.state`和`ro.crypto.type`用于指示设备的加密状态和类型。通常,`ro.crypto.state`可能的值有`unsupported`、`unencrypted`和`encrypted`。而`ro.crypto.type`可能的值有`file`(基于文件的加密)和`block`(全盘加密)。 用户已经设置了: ``` setprop ro.crypto.state unencrypted setprop ro.crypto.type unencrypted ``` 但是,系统启动时仍然要求输入密码。这可能是由于以下原因: 1. **加密标记的持久化**:Android系统在第一次启动时会根据是否加密设置一个标志,这个标志可能存储在`/data`分区或其他地方。如果设备之前已经加密过,那么即使你在`init.rc`中设置属性,系统可能仍然认为设备是加密的。 2. **启动流程中的检查点**:在启动过程中,`init`进程会执行`init.rc`脚本,但是加密状态的检查可能发生在更早的阶段(比如在`init`进程启动之前,由内核或`bootloader`设置)。另外,`vold`(Volume守护进程)会负责挂载数据分区,它也会检查实际的加密状态。 3. **文件系统加密标志**:如果数据分区已经被加密,那么即使你设置属性为未加密,系统在尝试挂载数据分区时也会要求解密。 4. **SELinux策略**:可能由于SELinux策略限制,导致属性设置没有生效。 5. **属性设置的时机**:在`init.rc`中设置属性的时机可能太晚。加密状态的检查可能发生在设置属性之前。 6. **其他加密相关属性**:可能还有其他属性控制加密行为,如`ro.crypto.fs_type`、`ro.crypto.fs_real_blkdev`等。 为了解决这个问题,我们需要确保: - 设备的数据分区没有被加密。如果已经被加密,需要先解密(通常通过恢复出厂设置或重新刷写未加密的系统镜像)。 - 在`init.mt6765.rc`中,确保在早期(比如`on early-init`阶段)设置加密属性。 - 检查`fstab`文件,确保数据分区的挂载选项没有指定加密(`forceencrypt`或`encryptable`)。在Android 9中,通常使用`fstab`文件来定义分区的挂载选项,这个文件位于`vendor/etc/fstab.mt6765`或类似路径。 在`fstab`中,对于`/data`分区,我们通常可以看到类似以下的条目: ``` /dev/block/platform/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,noauto_da_alloc,discard wait,check,formattable,forceencrypt=/dev/block/platform/bootdevice/by-name/metadata ``` 注意其中的`forceencrypt`选项,这会导致系统强制加密。我们需要将其改为`encryptable`(表示允许加密但非强制)或直接移除加密选项(但也要注意,如果使用FBE,则可能需要不同的选项)。 在Android 9中,推荐使用基于文件的加密(FBE)。要配置为不加密,我们需要: 1. **修改fstab**:将`forceencrypt`或`encryptable`改为`none`,或者删除加密选项。例如,将上面的行改为: ``` /dev/block/platform/bootdevice/by-name/userdata /data ext4 noatime,nosuid,nodev,noauto_da_alloc,discard wait,check,formattable ``` 2. **在init.rc中设置属性**:在`init.mt6765.rc`的`on early-init`阶段设置加密状态属性: ``` on early-init # 设置加密状态为未加密 setprop ro.crypto.state unencrypted setprop ro.crypto.type unencrypted ``` 3. **确保系统支持不加密**:有些设备可能强制要求加密,因此需要检查内核配置和系统属性。 4. **清除加密标志**:如果设备之前已经加密过,需要格式化数据分区以清除加密。可以通过恢复模式下的`wipe data`操作完成。 5. **检查vold的配置**:在`system/etc/vold.fstab`或`vendor/etc/vold.fstab`中,也可能有关于加密的配置,确保没有启用加密。 6. **检查设备策略(Device Policy)**:引用[3]中提到,设备策略管理(DevicePolicyManager)可能会强制要求加密。如果设备被设置为设备管理员,并且策略要求加密,那么即使系统支持无加密,也会被强制加密。因此,需要检查设备策略。 7. **验证启动流程**:在`init`的日志中(通过`adb logcat -b all`或`dmesg`)查看加密相关的日志,确定系统在哪个阶段要求输入密码。 实现无密码启动的步骤总结: 1. 修改`fstab`文件,去掉数据分区的加密选项(如`forceencrypt`)。 2. 在`init.mt6765.rc`的`on early-init`阶段设置加密属性为未加密。 3. 清除数据分区(注意:这会丢失所有数据)。 4. 重新编译系统镜像并刷机。 5. 确保没有设备管理策略强制加密。 注意:修改`fstab`和`init.rc`需要重新编译系统镜像(通常是`boot.img`或`vendor.img`),然后刷入设备。 如果问题仍然存在,需要检查`init`的日志,查看属性设置是否被覆盖,以及`vold`在挂载数据分区时的行为。 另外,在Android 9中,加密相关的属性设置可能还涉及`init`中的其他动作,例如`on fs`和`on post-fs`阶段。确保在挂载数据分区之前,加密属性已经正确设置。 最后,由于MTK平台可能有自己的定制,建议参考MTK的文档或源码中其他类似设备的配置。</think>### Android 9 MTK 平台实现无密码启动的完整解决方案 #### 问题分析 您在 `init.mt6765.rc` 中添加了以下设置: ```rc setprop ro.crypto.state unencrypted setprop ro.crypto.type unencrypted ``` 但系统仍要求输入密码,主要原因如下: 1. **加密标志持久化**:`/data` 分区可能已被加密,系统优先检查实际加密状态而非属性值[^1] 2. **fstab 配置覆盖**:`vendor/etc/fstab.mt6765` 中的 `forceencrypt` 标志会强制启用加密 3. **启动时序问题**:属性设置时机可能晚于加密检查点 4. **密钥管理服务干扰**:`vold` 或 `keystore` 服务可能覆盖设置 #### 解决方案步骤 ##### 1. 修改 fstab 配置文件 定位文件:`vendor/etc/fstab.mt6765` 或 `vendor/etc/fstab.<device_name>` ```diff /dev/block/platform/bootdevice/by-name/userdata /data ext4 - noatime,nosuid,nodev,noauto_da_alloc,discard wait,check,formattable,forceencrypt + noatime,nosuid,nodev,noauto_da_alloc,discard wait,check,formattable ``` 关键修改:**移除 `forceencrypt` 和 `fileencryption` 参数** ##### 2. 优化 init.mt6765.rc 设置 在 `on early-init` 阶段添加: ```rc on early-init # 设置加密状态 setprop ro.crypto.state unencrypted setprop ro.crypto.type unencrypted # 禁用加密服务 stop vold stop keystore ``` ##### 3. 添加预挂载检查 在 `on fs` 阶段前插入: ```rc on pre-fs # 强制解除加密绑定 exec - root -- /system/bin/vdc cryptfs clearpw write /sys/class/block/dm-0/device/state decrypted ``` ##### 4. 修改内核启动参数 在设备树源文件(DTS)中添加: ```dts / { chosen { bootargs = "... skip_initramfs rootwait ro.force_decrypt=0"; }; }; ``` 编译后更新 `boot.img` ##### 5. 清除加密标志 刷机前执行: ```bash fastboot erase userdata fastboot format:ext4 userdata ``` #### 验证步骤 1. 检查内核日志: ```bash adb shell dmesg | grep -E "crypt|vold" # 应显示 "encryption state: unencrypted" ``` 2. 检查属性值: ```bash adb shell getprop | grep crypto # ro.crypto.state: unencrypted # ro.crypto.type: unencrypted ``` 3. 验证分区状态: ```bash adb shell vdc cryptfs getstate # 预期输出: "0 cryptfs_getstate: Unencrypted" ``` #### 常见问题处理 1. **启动循环问题**: - 确保所有修改在 `on early-init` 阶段完成 - 检查 SELinux 策略: ```te allow init block_device:blk_file write; allow vold kernel:system setprop; ``` 2. **属性被覆盖**: 在 `init.mt6765.rc` 开头添加: ```rc import /init.recovery.mt6765.rc # 防止恢复模式覆盖 ``` 3. **MTK 专用服务干扰**: ```rc on boot # 禁用 MTK 加密守护进程 stop mtk_cryptd setprop mtk.cryptd.status disabled ``` > **重要提示**:修改后必须执行 `make bootimage` 和 `make vendorimage` 重新编译,完整刷写 `boot.img` 和 `vendor.img`[^2] #### 实现原理 ```mermaid sequenceDiagram Bootloader->>Kernel: 传递 ro.force_decrypt=0 Kernel->>Init: 加载 init.mt6765.rc Init->>Early-init: 设置加密属性 Early-init->>Vold: 发送停止指令 Vold-->>Data Partition: 跳过加密检查 Init->>Pre-fs: 解除加密绑定 Pre-fs->>Mount: 挂载未加密分区 Mount-->>System: 完成无密码启动 ``` #### 相关问题 1. 如何验证 Android 设备是否真正禁用加密? 2. MTK 平台修改 fstab 后为何需要重新编译 vendor.img? 3. Android 9 的 FBE(基于文件的加密)和 FDE(全盘加密)有何区别? 4. 跳过加密启动是否会影响 OTA 更新功能? [^1]: 加密状态检查优先于属性设置,需在早期启动阶段介入 [^^2]: MTK 平台需同时更新 boot 和 vendor 分区才能生效
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值