# 自启动设置


## 概述与介绍

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

## 功能详细介绍

### 系统启动过程

#### 从 Kernel 到 init

![start_bring_up.png](./_static/_images/35-Auto-start/start_bring_up.png)

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

- init 进程: 所有用户空间进程的祖先，负责管理和创建用户空间进程。
- kthreadd 进程: 所有内核线程的祖先，负责内核线程的管理。

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

```C
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 文件的设置决定启动流程。

```C
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) 函数可以使用一些默认的配置。

```C
/* 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`。

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

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

```C
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`，将在一个单独的进程中执行。

```SHELL
#!/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

```

## 移植与开发

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

### 示例：创建初始化脚本

#### 步骤 1：创建脚本文件

```Bash
vi /etc/init.d/S99my_custom_service
```

#### 步骤 2：编辑脚本

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

```Bash
#!/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：设置权限

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

```Bash
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` 变量，以传递任何必要的参数给你的程序
- 根据需要添加停止和重启服务的命令，将其放在`stop`和`restart`部分
- 自启动程序根据名称前缀(S??)的数字可以设定执行优先级，数字越小优先级越高，取值范围0-99。

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

```SHELL
#!/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
}
... ...

```

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

## 常见问题

- 自启动脚本执行失败的原因可能是什么？ <br/>
常见原因包括脚本缺少可执行权限、路径不正确、依赖的服务未启动等。
- 如何判断自启动脚本是否执行？ <br/>
您可以通过查看系统日志或在脚本中添加日志输出，确认是否成功执行。