Android 进阶——系统启动之Android init进程的创建和启动

Posted CrazyMo_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 进阶——系统启动之Android init进程的创建和启动相关的知识,希望对你有一定的参考价值。

文章大纲

引言

前面两篇文章主要是总结关于Linux 内核启动阶段的主要内容,接下来将是android 系统层面的启动,严格上讲Android系统本质上是运行于Linux内核之上的一系列服务进程(结构有点类似“微内核”,用于维持Android设备正常工作和交互),并不是一个完整意义上的**“操作系统”**,而这一系列进程都是由Android init进程衍生的,系列文章链接如下:

aosp 代表系统源码根目录

一、Android init进程

Android init进程是Android系统启动的第一个进程。由上篇文章可知,Linux系统的init进程在内核初始化完成后,就直接执行init这个文件(因为Android系统一般会在根目录下放一个名为init的可执行文件,即调用aosp/system/core/init/init.cpp里的main函数),换言之,当**这个main函数就是Android init进程创建的起点,在函数里完成众多初始化工作,就包括通过解析init.rc脚本来构建出系统的初始形态,其他的"一系列"Android系统进程大部分也是通过init.rc**来启动的。

二、Android 中常见的Linux 内核函数

1、进程与进程调度

1.1 kernel_thread 创建启动进程

kernel_thread函数的作用是产生一个新的线程内核线程实际上就是一个共享父进程地址空间的进程,它有自己的系统堆栈。内核线程和进程都是通过do_fork()函数来产生的,系统中规定的最大进程数与线程数由fork_init来决定。

int kernel_thread (int ( * fn )( void * ), void * arg, unsigned long flags);
  • 第一参数是一个函数指针,表示新进程工作函数,相当于是调用kernel_thread时会执行这里的函数
  • 第二个参数是传入工作函数的参数
  • 第三个参数表示启动方式
参数名作用
CLONE_PARENT创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
CLONE_FS子进程与父进程共享相同的文件系统,包括root、当前目录、umask
CLONE_FILES子进程与父进程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS在新的namespace启动子进程,namespace描述了进程的文件hierarchy
CLONE_SIGHAND子进程与父进程共享相同的信号处理(signal handler)表
CLONE_PTRACE若父进程被trace,子进程也被trace
CLONE_UNTRACED若父进程被trace,子进程不被trace
CLONE_VFORK父进程被挂起,直至子进程释放虚拟内存资源
CLONE_VM子进程与父进程运行于相同的内存空间
CLONE_PID子进程在创建时PID与父进程一致
CLONE_THREADLinux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

1.2 sched_setscheduler_nocheck 设置进程调度策略

extern int sched_setscheduler_nocheck(struct task_struct *, int,const struct sched_param *)
  • 第一个参数是进程的task_struct 结构体
  • 第二个参数是进程调度策略
  • 第三个参数是进程优先级

进程调度策略如下:

  • SCHED_FIFO和SCHED_RR和SCHED_DEADLINE则采用不同的调度策略调度实时进程,优先级最高
  • SCHED_NORMAL和SCHED_BATCH调度普通的非实时进程,优先级普通
  • SCHED_IDLE则在系统空闲时调用idle进程,优先级最低

2、同步与锁

2.1 rcu_read_lock、rcu_read_unlock

	rcu_read_lock(); 
	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
	rcu_read_unlock();

RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)

3、内存与内存策略

3.1 numa_default_policy

设定NUMA系统的默认内存访问策略

3.2 mmap 内存映射

将一个文件或者其它对象映射进一个存储区域。即调用mmap函数可以告知内核将文件描述符fd对应的文件的__size个字节数据(起始位置是从__offset指定)映射到一个存储区域

void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset)
  • 第一个参数是用于指定映射存储区的起始地址,通常设置为0,表示由系统决定该映射区的起始地址,若自己指定的话需要与页长对齐。

  • 第二个参数是用于指定要映射文件的__size字节长度,如果指定的size小于一个页长,那么内核也会自动映射一个页长,总之是内核是以页长对齐来映射的,但是内核没有那么智能会去主动判断你给的值是否合理,比如说你的数据长度只有512如果你给2048那么就会造成把其他原本不应该被映射的区域映射了,此时就会造成把多余的字节数据写入到磁盘中。

  • 第三个参数用于指定文件的open模式的访问权限

  • 第四个参数用于指定映射存储区的多种属性

  • 第五个参数是用于指定要映射的文件描述符

  • 第六个参数是用于指定要映射文件里的起始位置的偏移量,0则表示从文件的第一个字节开始映射且必须与页长对齐

4、通信

4.1 int socketpair(int d, int type, int protocol, int sv[2])

创建一对socket,用于本机内的进程通信

  • 第一个参数,套接字的域 ,一般为AF_UNIX,表示Linux本机

  • 第二个参数,type 套接字类型,取值有:

    • SOCK_STREAM或SOCK_DGRAM——即TCP或UDP

    • SOCK_NONBLOCK ——read不到数据不阻塞,直接返回0

    • SOCK_CLOEXEC ——设置文件描述符为O_CLOEXEC

  • 第三个参数,protocol 使用的协议,值只能是0

  • 第四个参数,sv 指向存储文件描述符的指针

4.2 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

  • 第一个参数要操作的信号。
  • 第二个参数要设置的对信号的新处理方式。
  • dis按各参赛原来对信号的处理方式。
  • 返回值:0 表示成功,-1 表示有错误发生。

struct sigaction 类型用来描述对信号的处理,定义如下:

struct sigaction
{
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

其中sa_handler 是一个函数指针,其含义与 signal 函数中的信号处理函数类似,而sa_sigaction 则是另一个信号处理函数指针,可以获得关于信号的更详细的信息。当 sa_flags 成员的值包含了 SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号处理函数,否则使用 sa_handler 作为信号处理函数。在某些系统中,成员 sa_handler 与 sa_sigaction 被放在联合体中,因此使用时不要同时设置。sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。sa_flags 成员用于指定信号处理的行为,它可以是一下值的“按位或”组合。

参数名作用
SA_RESTART使被信号打断的系统调用自动重新发起
SA_NOCLDSTOP使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
SA_NOCLDWAIT使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
SA_NODEFER使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
SA_RESETHAND信号处理之后重新设置为默认的处理方式
SA_SIGINFO使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数

三、aosp/system/core/init/init.cpp#main函数执行

aosp/system/core/init/init.cpp#main 函数会执行多次

main函数里做了很多工作,从注释上看可以分为几个阶段(stage)来阅读:

1、Android init 进程运行第一阶段

1.1、第一阶段的前的准备阶段

  • ueventd、watchdogd跳转及环境变量设置相关
  • 初始化内核日志系统
  • 初始化重启系统的处理信号,内部通过sigaction 注册信号,当捕捉到该信号时重启

从代码中可以得知,并非每一次都会进行第一阶段的初始化工作(即if (is_first_stage)分支)

int main(int argc, char** argv) {
	/*
	* ueventd/watchdogd跳转及环境变量设置相关,其中strcmp是标准库中的函数,比较字符串,
	* 而basename 是用于获取指定路径中最后一个分拣分割符“/”后面的内容
	*/
    if (!strcmp(basename(argv[0]), "ueventd")) {
		//C中非0则为true,1表示true,ueventd_main函数,ueventd主要是负责设备节点的创建、权限设定等一些列工作
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
		//watchdogd看门狗,用于系统出问题时重启系统
        return watchdogd_main(argc, argv);
    }

    if (argc > 1 && !strcmp(argv[1], "subcontext")) {
        InitKernelLogging(argv);//初始化内核日志系统 
        const BuiltinFunctionMap function_map;
        return SubcontextMain(argc, argv, &function_map);
    }

    if (REBOOT_BOOTLOADER_ON_PANIC) {
		//注册捕捉各种信息,初始化重启系统的处理信号,内部通过sigaction 注册信号,当捕捉到该信号时重启
        InstallRebootSignalHandlers();
    }
	
    bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
    

其中aosp/system/core/init/ueventd.cpp#ueventd_main函数

int ueventd_main(int argc, char **argv)
{
    /*
     * init sets the umask to 077 for forked processes. We need to
     * create files with exact permissions, without modification by
     * the umask.
     */
    umask(000); //设置新建文件的默认值,这个与chmod相反,这里相当于新建文件后的权限为666,清空文件权限
    signal(SIGCHLD, SIG_IGN);//忽略子进程终止信号
    InitKernelLogging(argv); //初始化日志输出
    LOG(INFO) << "ueventd started!";
    selinux_callback cb;
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);//注册selinux相关的用于打印log的回调函数
    ueventd_parse_config_file("/ueventd.rc"); //解析.rc文件,这个后续再讲
    ueventd_parse_config_file("/vendor/ueventd.rc");
    ueventd_parse_config_file("/odm/ueventd.rc");

    std::string hardware = android::base::GetProperty("ro.hardware", "");
    ueventd_parse_config_file(android::base::StringPrintf("/ueventd.%s.rc", hardware.c_str()).c_str());

    device_init();//创建一个socket来接收uevent,再对内核启动时注册到/sys/下的驱动程序进行“冷插拔”处理,以创建对应的节点文件。

    pollfd ufd;
    ufd.events = POLLIN;
    ufd.fd = get_device_fd();//获取device_init中创建出的socket

    while (true) {//无限循环,随时监听驱动
        ufd.revents = 0;
        int nr = poll(&ufd, 1, -1);//监听来自驱动的uevent,poll机制原始版本的epoll
        if (nr <= 0) {
            continue;
        }
        if (ufd.revents & POLLIN) {
            handle_device_fd();//驱动程序进行“热插拔”处理,以创建对应的节点文件。
        }
    }

    return 0;
}

Android根文件系统的映像中不存在“/dev”目录,该目录是init进程启动后动态创建的。因此,建立Android中设备节点文件的重任,也落在了init进程身上。为此,init进程创建子进程ueventd,并将创建设备节点文件的工作托付给ueventd。

ueventd通过两种方式创建设备节点文件:

  • “冷插拔”(Cold Plug)——即以预先定义的设备信息为基础,当ueventd启动后,统一创建设备节点文件。这一类设备节点文件也被称为静态节点文件。

  • “热插拔”(Hot Plug)——即在系统运行中,当有设备插入USB端口时,ueventd就会接收到这一事件,为插入的设备动态创建设备节点文件。这一类设备节点文件也被称为动态节点文件。

另一个“看门狗”aosp/system/core/init/watchdogd.cpp#watchdogd_main

ioctl函数是设备驱动程序中对设备的I/O通道进行操作管理的函数,而WDIOC_SETTIMEOUT是设置超时时间

int watchdogd_main(int argc, char **argv) {
    InitKernelLogging(argv);

    int interval = 10;
    //atoi作用是将字符串转变为数值
    if (argc >= 2) interval = atoi(argv[1]);
    int margin = 10;
    if (argc >= 3) margin = atoi(argv[2]);

    LOG(INFO) << "watchdogd started (interval " << interval << ", margin " << margin << ")!";

    int fd = open(DEV_NAME, O_RDWR|O_CLOEXEC); //打开文件 /dev/watchdog
    if (fd == -1) {
        PLOG(ERROR) << "Failed to open " << DEV_NAME;
        return 1;
    }

    int timeout = interval + margin;
    int ret = ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
    if (ret) {
        PLOG(ERROR) << "Failed to set timeout to " << timeout;
        ret = ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
        if (ret) {
            PLOG(ERROR) << "Failed to get timeout";
        } else {
            if (timeout > margin) {
                interval = timeout - margin;
            } else {
                interval = 1;
            }
            LOG(WARNING) << "Adjusted interval to timeout returned by driver: "
                         << "timeout " << timeout
                         << ", interval " << interval
                         << ", margin " << margin;
        }
    }
    while (true) {//每间隔一定时间往文件中写入一个空字符,这就是看门狗的关键了
        write(fd, "", 1);
        sleep(interval);
    }
}

"看门狗"本身是一个定时器电路,内部会不断的进行计时(或计数)操作,计算机系统和"看门狗"有两个引脚相连接,正常运行时每隔一段时间就会通过其中一个引脚向"看门狗"发送信号,"看门狗"接收到信号后会将计时器清零并重新开始计时,而一旦系统出现问题,进入死循环或任何阻塞状态,不能及时发送信号让"看门狗"的计时器清零,当计时结束时,"看门狗"就会通过另一个引脚向系统发送“复位信号”,让系统重启。因此watchdogd_main主要是定时器作用,而DEV_NAME就是那个引脚。

1.2、正式开始进入第一阶段内进行初始化工作

1.2.1、挂载文件系统并创建各种相关的系统文件目录

在init初始化过程中,Android分别挂载了tmpfs、devpts、proc、sysfs、selinuxfs文件系统:

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

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

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

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

  • selinuxfs文件系统——虚拟文件系统,通常挂载在/sys/fs/selinux目录下,用来存放SELinux安全策略文件。

1.2.2、初始化内核日志系统,挂载分区设备

1.2.3、启用SELinux 策略和初始化相关

SELinux是「Security-Enhanced Linux」的简称,是美国国家安全局「NSA=The National Security Agency」和SCC(Secure Computing Corporation)开发的 Linux的一个扩张强制访问控制安全模块。在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。

 	bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);	
	//第一阶段首次就绪时
    if (is_first_stage) {
        boot_clock::time_point start_time = boot_clock::now();
        // Clear the umask.
        umask(0);
        clearenv();
        setenv("PATH", _PATH_DEFPATH, 1);
        // 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. 挂载各种文件系统,并创建相关系统文件目录
        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));
        // Don't expose the raw commandline to unprivileged processes.
        chmod("/proc/cmdline", 0440);
        gid_t groups[] = { AID_READPROC };
        setgroups(arraysize(groups), groups);
        mount("sysfs", "/sys", "sysfs", 0, NULL);
        mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);
			//创建Linux中的设备文件
        mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));

        if constexpr (WORLD_WRITABLE_KMSG) {
            mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11));
        }

        mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
        mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));

        // Mount staging areas for devices managed by vold
        // See storage config details at http://source.android.com/devices/storage/
        mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
              "mode=0755,uid=0,gid=1000");
        // /mnt/vendor is used to mount vendor-specific partitions that can not be
        // part of the vendor partition, e.g. because they are mounted read-write.
        mkdir("/mnt/vendor", 0755);

        // Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually talk to the outside world...
        InitKernelLogging(argv);

        LOG(INFO) << "init first stage started!";

        if (!DoFirstStageMount()) {
            LOG(FATAL) << "Failed to mount required partitions early ...";
        }

        SetInitAvbVersionInRecovery();

        // Enable seccomp if global boot option was passed (otherwise it is enabled in zygote).
        global_seccomp();

        // Set up SELinux, loading the SELinux policy. SELinux 相关
        SelinuxSetupKernelLogging();
        SelinuxInitialize();

        // We're in the kernel domain, so re-exec init to transition to the init domain now
        // that the SELinux policy has been loaded.
        if (selinux_android_restorecon("/init", 0) == -1) {
            PLOG(FATAL) << "restorecon failed of /init failed";
        }

        setenv("INIT_SECOND_STAGE", "true", 1);

        static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
        uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
        setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1);

        char* path = argv[0];
        char* args[] = { path, nullptr };
        execv(path, args);

        // execv() only returns if an error happened, in which case we
        // panic and never fall through this conditional.
        PLOG(FATAL) << "execv(\\"" << path << "\\") failed";
    }
    // At this point we're in the second stage of init.
    InitKernelLogging(argv);

2、Android init 进程运行第二阶段

从代码逻辑上看可能main函数会执行两次,第一此第一阶段执行setenv(“INIT_SECOND_STAGE”, “true”, 1)完毕后INIT_SECOND_STAGE的值为true,就直接开始第二阶段:

2.1.1、创建进程会话密

以上是关于Android 进阶——系统启动之Android init进程的创建和启动的主要内容,如果未能解决你的问题,请参考以下文章

Android 进阶——系统启动之SystemServer创建并启动Installer服务

Android 进阶——系统启动之SystemServer创建并启动Installer服务

Android 进阶——系统启动之Framework 核心ActivitityManagerService服务启动

Android 进阶——系统启动之Framework 核心ActivitityManagerService服务启动

Android 进阶——系统启动之Framework 核心ActivitityManagerService服务启动

Android 进阶——系统启动之核心SystemServer进程启动详解