4.4.1. 自启动设置

4.4.1.1. 概述与介绍

在嵌入式系统或轻量级 Linux 环境中,自启动服务的管理通常使用 Busybox init。通过在 /etc/init.d 目录下创建自启动的 Shell 脚本,可以实现系统启动时自动执行特定任务或服务的功能。本文将详细介绍自启动机制的系统启动过程、其实现原理以及如何自定义自启动脚本。

4.4.1.2. 功能详细介绍

系统启动过程

从 Kernel 到 init

start_bring_up.png

在 Linux 内核启动的最后阶段,start_kernel() 函数调用 reset_init() 函数以创建第一个进程,即 pid=0 的 idle 进程。这个进程在内核态运行,且是唯一一个不通过 fork() 或 kernel_thread() 创建的进程。该进程通过一系列调用最终进入cpu_idle_loop(),在其中生成两个重要的进程:

  • init 进程: 所有用户空间进程的祖先,负责管理和创建用户空间进程。

  • kthreadd 进程: 所有内核线程的祖先,负责内核线程的管理。

通过对下面代码的分析,清晰地看出 pid=0 是所有进程和线程的祖先,而 init 进程和 kthreadd 进程分别负责不同的任务。

static noinline void __ref rest_init(void)
{
...
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
...
	cpu_startup_entry(CPUHP_ONLINE);
}

static int __ref kernel_init(void *unused)
{
...
	/* 通过设置 bootargs 的 "rdinit=/sbin/init" 来指定,如果指定则启动ramdisk。*/
    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
...
    }

	/* 通过设置 bootargs 的 "init=/sbin/init" 来指定,包括启动参数 argv_init[]。*/
    if (execute_command) {
        ret = run_init_process(execute_command);
...
}
	/* 如果没有指定rdinit和init,那么依次尝试下面几个固定路径init程序。*/
    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
        return 0;
...
}

int kthreadd(void *unused)
{
...
    /* Setup a clean context for our children to inherit. */
    /* 修改内核线程名为kthreadd。*/
    set_task_comm(tsk, "kthreadd");
...
    cgroup_init_kthreadd();

    for (;;) {
...
        spin_lock(&kthread_create_lock);
        /* 内核线程的创建是由kthreadd遍历kthread_create_list列表 */
        /* 然后取出成员,通过create_kthread()创建内核线程。*/
        while (!list_empty(&kthread_create_list)) {
            struct kthread_create_info *create;

            create = list_entry(kthread_create_list.next,
                        struct kthread_create_info, list);
            list_del_init(&create->list);
            spin_unlock(&kthread_create_lock);

            create_kthread(create);

            spin_lock(&kthread_create_lock);
        }
        spin_unlock(&kthread_create_lock);
    }

    return 0;
}

init 进程分析

init_main() 是 Busybox 中的 init 进程入口。该进程负责配置用户空间的工作环境,并根据 /etc/inittab 文件的设置决定启动流程。

int init_main(int argc UNUSED_PARAM, char **argv)
{
... ...
	/* Make sure environs is set to something sane */
	/* 设置环境变量,SHELL指向/bin/sh */
	putenv((char *) "HOME=/");
	putenv((char *) bb_PATH_root_path);
	putenv((char *) "SHELL=/bin/sh");
	putenv((char *) "USER=root"); /* needed? why? */
... ...

	/* Check if we are supposed to be in single user mode */
	if (argv[1]
	 && (strcmp(argv[1], "single") == 0 || strcmp(argv[1], "-s") == 0 || LONE_CHAR(argv[1], '1'))
	) {
... ...
	} else {
		/* Not in single user mode - see what inittab says */

		/* NOTE that if CONFIG_FEATURE_USE_INITTAB is NOT defined,
		 * then parse_inittab() simply adds in some default
		 * actions (i.e., INIT_SCRIPT and a pair
		 * of "askfirst" shells) */
		/* 解析/etc/inittab文件,下面按照SYSINIT->WAIT->ONCE->RESPAWN|ASKFIRST顺序执行inittab内容。 */
		parse_inittab();
	}
... ...
	/* Now run the looping stuff for the rest of forever */
	while (1) {
... ...
		/* Wait for any child process(es) to exit */
		while (1) {
			/* -1表示等待任一子进程。若成功则返回状态改变的子进程ID,若出错则返回-1,
			 * 若指定了WNOHANG选项且pid指定的子进程状态没有发生改变则返回0。
			 */
			wpid = waitpid(-1, NULL, WNOHANG);
			if (wpid <= 0)
				break;

			/* 将进程的init_action->pid改成0. */
			a = mark_terminated(wpid);
			if (a) {
				message(L_LOG, "process '%s' (pid %u) exited. "
						"Scheduling for restart.",
						a->command, (unsigned)wpid);
			}
		}
... ...
	} /* while (1) */
}

在 init_main() 函数中会调用 parse_inittab(void) 函数,当 /etc/inittab 没有配置时,parse_inittab(void) 函数可以使用一些默认的配置。

/* NOTE that if CONFIG_FEATURE_USE_INITTAB is NOT defined,
 * then parse_inittab() simply adds in some default
 * actions (i.e., runs INIT_SCRIPT and then starts a pair
 * of "askfirst" shells).  If CONFIG_FEATURE_USE_INITTAB
 * _is_ defined, but /etc/inittab is missing, this
 * results in the same set of default behaviors.
 */
static void parse_inittab(void)
{
#if ENABLE_FEATURE_USE_INITTAB
	char *token[4];
	parser_t *parser = config_open2("/etc/inittab", fopen_for_read);

	if (parser == NULL)
... ...
		/* No inittab file - set up some default behavior */
		/* Sysinit */
		new_init_action(SYSINIT, INIT_SCRIPT, "");
... ...
	}
... ...
}

在 parse_inittab 里面会调用 new_init_action(SYSINIT, INIT_SCRIPT, “”),决定了接下去初始化的脚本是 INIT_SCRIPT 所定义的值。默认的 SYSINIT 脚本为 /etc/init.d/rcS

/* Default sysinit script. */
#ifndef INIT_SCRIPT
# define INIT_SCRIPT  "/etc/init.d/rcS"
#endif

查看系统中的 /etc/inittab 内容,表明在启动时会执行 /etc/init.d/rcS。

rcS:12345:wait:/etc/init.d/rcS

/etc/init.d/rcS

/etc/init.d/rcS 脚本的作用是按顺序执行 /etc/init.d/S??* 文件(?? 代表数字),启动系统所需的服务。从实现上看,启动脚本有分以.sh结尾的脚本程序和其他类型的程序。两种程序的执行区别如下:

  • .sh后缀的程序:对于以 .sh 结尾的文件,会使用 source 的方式(. filename)执行,脚本中的命令将在当前 shell 进程中执行,而不是在一个子 shell 中执行。这样可以更快地执行,并且允许脚本中的变量和环境在当前 shell 中保持。

  • .sh后缀的程序:对于没有 .sh 结尾的文件,直接执行 $i start,将在一个单独的进程中执行。

#!/bin/sh


# Start all init scripts in /etc/init.d
# executing them in numerical order.
#
for i in /etc/init.d/S??* ;do

     # Ignore dangling symlinks (if any).
     [ ! -f "$i" ] && continue

     case "$i" in
        *.sh)
            # Source shell script for speed.
            (
                trap - INT QUIT TSTP
                set start
                . $i
            )
            ;;
        *)
            # No sh extension, so fork subprocess.
            $i start
            ;;
    esac
done

4.4.1.3. 移植与开发

在开发与移植过程中,用户可以轻松地添加或修改自启动脚本,而无需重新构建系统镜像。这在调试和开发阶段尤其有用,可以快速验证自启动程序的功能。总结以上分析情况,用户可以在 /etc/init.d 下添加以 S 开头加 两位数字 的可执行程序(例如:S99auto_startup, 或者 S99auto_startup.sh),该程序即会在系统启动时自动运行。

示例:创建初始化脚本

步骤 1:创建脚本文件

vi /etc/init.d/S99my_custom_service

步骤 2:编辑脚本

将以下内容粘贴到编辑器中,并保存文件。

#!/bin/sh

# Path to your program
PROG="/path/to/your/program"

# Optional arguments for your program
ARGS=""

start() {
    echo "Starting my_custom_service"
    $PROG $ARGS &
}

stop() {
    echo "Stopping my_custom_service"
    # Add commands to stop your service gracefully
}

restart() {
    stop
    sleep 1
    start
}

case "$1" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  restart)
    restart
    ;;
  *)
    echo "Usage: $0 {start|stop|restart}"
    exit 1
    ;;
esac

exit 0

步骤 3:设置权限

确保脚本具备可执行权限:

chmod +x /etc/init.d/S99my_custom_service

用法提示

  • 启动服务:sudo /etc/init.d/S99my_custom_service start

  • 停止服务:sudo /etc/init.d/S99my_custom_service stop

  • 重启服务:sudo /etc/init.d/S99my_custom_service restart

注意事项

  • 替换 /path/to/your/program 为实际程序的路径

  • 根据需要编辑 ARGS 变量,以传递任何必要的参数给你的程序

  • 根据需要添加停止和重启服务的命令,将其放在stoprestart部分

  • 自启动程序根据名称前缀(S??)的数字可以设定执行优先级,数字越小优先级越高,取值范围0-99。

在当前的系统中已经预置了 /etc/init.d/S99auto_startup 自启动程序,该程序会在启动时自动去 /app/userdata/ 目录下寻找 startup.sh ,如果 startup.sh 程序存在,并且拥有可执行权限则会自动执行。

#!/bin/bash
... ...
start()
{
        APP_STARTUP="/app/startup.sh"
        USERDATA_STARTUP="/userdata/startup.sh"

        if [ -x "$APP_STARTUP" ]; then
                echo -n "<$LOG_INFO>Starting custom script: $APP_STARTUP" > /dev/kmsg
                $APP_STARTUP &
        fi

        if [ -x "$USERDATA_STARTUP" ]; then
                echo -n "<$LOG_INFO>Starting custom script: $USERDATA_STARTUP" > /dev/kmsg
                $USERDATA_STARTUP &
        fi
}
... ...

用户在调试和开发应用软件过程中,如果想要在不重新生成系统镜像的条件下添加自启动程序,可以利用这个自启动程序启动机制。

4.4.1.4. 常见问题

  • 自启动脚本执行失败的原因可能是什么?
    常见原因包括脚本缺少可执行权限、路径不正确、依赖的服务未启动等。

  • 如何判断自启动脚本是否执行?
    您可以通过查看系统日志或在脚本中添加日志输出,确认是否成功执行。