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

在 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变量,以传递任何必要的参数给你的程序根据需要添加停止和重启服务的命令,将其放在
stop和restart部分自启动程序根据名称前缀(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. 常见问题
自启动脚本执行失败的原因可能是什么?
常见原因包括脚本缺少可执行权限、路径不正确、依赖的服务未启动等。如何判断自启动脚本是否执行?
您可以通过查看系统日志或在脚本中添加日志输出,确认是否成功执行。