Android7.0 init进程源码分析

目的

看过一些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(
  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值