中国领先的IT技术网站
|
|

Linux启动分析——init进程与app启动

本文通过简要分析init进程源码,梳理其处理流程,重点关注init进程如何启动应用程序,总结启动脚本文件的编写思路。

作者:awokezhou来源:segmentfault|2017-08-31 14:40

【沙龙】51CTO诚邀您9月23号和多位技术大咖一起聊智能CDN的优化之路,抓紧时间哦!


概述

本文通过简要分析init进程源码,梳理其处理流程,重点关注init进程如何启动应用程序,总结启动脚本文件的编写思路

init进程源码分析

init进程是linux内核启动的第一个进程,怎么知道的?从内核源码linux-2.6.xxx/init/main.c代码的kernel_init()函数分析,可以发现,内核会根据uboot传入的参数来启动第一个进程,一般都是init

怎么启动的呢,调用kernel_execve()函数完成的,猜测是从根文件系统的/sbin/init来启动的,linux的任何应用程序都是基于文件系统的,启动应用程序前提是根文件系统已经挂载好了。好,那么根文件系统又是从哪里来的呢,是由busybox这个工具配置编译生成的,所以要分析init源码,要去busybox里找init的源码

源码位置:/busybox/init/init.c,在其中查找main()函数,发现只有init_main(),没有main(),可以猜测busybox是通过一些方法将init进程的入口修改为init_main(),实际上所有busybox的命令工具都是一个到busybox程序的链接,

  1. cd /sbin 
  2. ls -l init 
  3. lrwxrwxrwx 1 root 0 14 Nov 16 2016 init -> ../bin/busybox  

可以看到,init进程其实是到busybox的链接,不用管它,知道init进程的入口是init_main()函数就行了

  1. #if DEBUG_SEGV_HANDLER 
  2.     { 
  3.         struct sigaction sa; 
  4.         memset(&sa, 0, sizeof(sa)); 
  5.         sa.sa_sigaction = handle_sigsegv; 
  6.         sa.sa_flags = SA_SIGINFO; 
  7.         sigaction(SIGSEGV, &sa, NULL); 
  8.         ...... 
  9.     } 
  10. #endif 
  11. ...... 
  12. console_init(); 
  13. set_sane_term(); 
  14. ...... 
  15. /* Make sure environs is set to something sane */ 
  16. putenv((char *) "HOME=/"); 
  17. putenv((char *) bb_PATH_root_path); 
  18. putenv((char *) "SHELL=/bin/sh"); 
  19. putenv((char *) "USER=root"); /* needed? why? */  

这一段是init进程最开始要做的事情,设置一些信号相关的东西,初始化console,然后设置环境变量,跟启动app似乎没有什么关系,不用管,继续往下看

  1. /* Check if we are supposed to be in single user mode */ 
  2. if (argv[1] 
  3.  && (strcmp(argv[1], "single") == 0 || strcmp(argv[1], "-s") == 0 || LONE_CHAR(argv[1], '1')) 
  4. ) { 
  5.     /* ??? shouldn't we set RUNLEVEL="b" here? */ 
  6.     /* Start a shell on console */ 
  7.     new_init_action(RESPAWN, bb_default_login_shell, ""); 
  8. else { 
  9.     /* Not in single user mode - see what inittab says */ 
  10.  
  11.     /* NOTE that if CONFIG_FEATURE_USE_INITTAB is NOT defined, 
  12.      * then parse_inittab() simply adds in some default 
  13.      * actions (i.e., INIT_SCRIPT and a pair 
  14.      * of "askfirst" shells) */ 
  15.     parse_inittab(); 
  16.  

这一段代码是一个if判断,注释说如果是single user mode,则走上半段代码,如果不是single user mode,则调用parse_inittab() 函数,因为内核启动init进程没有传入附加参数,所以argv[1]不存在,程序走parse_inittab()

注释还说如果没有定义CONFIG_FEATURE_USE_INITTAB 这个宏,程序会执行一些默认的action,那怎么知道这个宏定义了没有呢,猜测这个宏应该是对busybox配置时的选项,好,怎么查看busybox配置呢,和linux内核配置一样的道理,结合make menuconfig和各级config文件来看

是否定义了宏CONFIG_FEATURE_USE_INITTAB?

在busybox中执行make meunconfig,进入熟悉的配置界面

大概浏览一下,和init有关系的好像有个Init Utilities项,进去

这里面有一项“Support reading an inittab file”,这个配置项是选中的,描述中有“inittab”这个单词,和init源码中说到的parse_inittab()很相似,好,make menuconfig先放到一边,来看看配置文件,打开顶层目录的Config.in,全局搜一下"init",发现只有最下面有:

  1. source init/Config.in 

进入init文件夹,打开其中的Config.in文件,发现配置项

  1. config FEATURE_USE_INITTAB 
  2.     bool "Support reading an inittab file" 
  3.     default y 
  4.     depends on INIT 
  5.     help 
  6.       Allow init to read an inittab file when the system boot.  

猜测没错,CONFIG_FEATURE_USE_INITTAB这个宏确实定义了,回到init源码分析,进入parse_inittab()函数。首先看到这个函数前有一大段注释,看看它说什么

  1. /* NOTE that if CONFIG_FEATURE_USE_INITTAB is NOT defined, 
  2.  * then parse_inittab() simply adds in some default 
  3.  * actions (i.e., runs INIT_SCRIPT and then starts a pair 
  4.  * of "askfirst" shells).  If CONFIG_FEATURE_USE_INITTAB 
  5.  * _is_ defined, but /etc/inittab is missing, this 
  6.  * results in the same set of default behaviors. 
  7.  */  

前面的话和之前的if判断意思差不多,如果定义了XXX这个宏,但是/etc/inittab这个文件没有,也会走默认的action,好,大概猜想一下,parse_inttab()函数好像和要分析的app启动有点关系了,如果定义了XXX宏,就去解析/etc/inittab这个文件,执行里面的东西,如果没有定义XXX宏或者/etc/inittab文件不存在,执行一些默认的东西

好,搞清楚一件事,/etc/inittab这个文件很重要,可能需要自己来创建这个文件,往里面写东西,但是写什么内容呢?目前还不知道。那如果不走/etc/inittab这一条路呢,默认会执行的action又是什么意思?来分析一下parse_inittab()这个函数

  1. static void parse_inittab(void) 
  2. #if ENABLE_FEATURE_USE_INITTAB 
  3.     char *token[4]; 
  4.     parser_t *parser = config_open2("/etc/inittab", fopen_for_read); 
  5.  
  6.     if (parser == NULL
  7. #endif 
  8.     { 
  9.         /* No inittab file - set up some default behavior */ 
  10.         /* Sysinit */ 
  11.         new_init_action(SYSINIT, INIT_SCRIPT, ""); 
  12.         /* Askfirst shell on tty1-4 */ 
  13.         new_init_action(ASKFIRST, bb_default_login_shell, ""); 
  14. //TODO: VC_1 instead of """" is console -> ctty problems -> angry users 
  15.         new_init_action(ASKFIRST, bb_default_login_shell, VC_2); 
  16.         new_init_action(ASKFIRST, bb_default_login_shell, VC_3); 
  17.         new_init_action(ASKFIRST, bb_default_login_shell, VC_4); 
  18.         /* Reboot on Ctrl-Alt-Del */ 
  19.         new_init_action(CTRLALTDEL, "reboot"""); 
  20.         /* Umount all filesystems on halt/reboot */ 
  21.         new_init_action(SHUTDOWN, "umount -a -r"""); 
  22.         /* Swapoff on halt/reboot */ 
  23.         new_init_action(SHUTDOWN, "swapoff -a"""); 
  24.         /* Restart init when a QUIT is received */ 
  25.         new_init_action(RESTART, "init"""); 
  26.         return
  27.     } 
  28.  
  29. #if ENABLE_FEATURE_USE_INITTAB 
  30.     /* optional_tty:ignored_runlevel:action:command 
  31.      * Delims are not to be collapsed and need exactly 4 tokens 
  32.      */ 
  33.     while (config_read(parser, token, 4, 0, "#:"
  34.                 PARSE_NORMAL & ~(PARSE_TRIM | PARSE_COLLAPSE))) { 
  35.         /* order must correspond to SYSINIT..RESTART constants */ 
  36.         static const char actions[] ALIGN1 = 
  37.             "sysinit\0""wait\0""once\0""respawn\0""askfirst\0" 
  38.             "ctrlaltdel\0""shutdown\0""restart\0"
  39.         int action
  40.         char *tty = token[0]; 
  41.  
  42.         if (!token[3]) /* less than 4 tokens */ 
  43.             goto bad_entry; 
  44.         action = index_in_strings(actions, token[2]); 
  45.         if (action < 0 || !token[3][0]) /* token[3]: command */ 
  46.             goto bad_entry; 
  47.         /* turn .*TTY -> /dev/TTY */ 
  48.         if (tty[0]) { 
  49.             tty = concat_path_file("/dev/", skip_dev_pfx(tty)); 
  50.         } 
  51.         new_init_action(1 << action, token[3], tty); 
  52.         if (tty[0]) 
  53.             free(tty); 
  54.         continue
  55.  bad_entry: 
  56.         message(L_LOG | L_CONSOLE, "Bad inittab entry at line %d"
  57.                 parser->lineno); 
  58.     } 
  59.     config_close(parser); 
  60. #endif 
  61.  

首先去读了/etc/inittab这个文件,如果不存在,执行了很多new_init_action() ,如果存在,就走了一个while()循环,猜测应该是解析/etc/inittab文件的内容,根据文件的内容执行new_init_action() 。好,那么inittab文件到底写什么格式,什么东西呢,while()循环里面有一个static const char actions[]数组看起来像是和inittab的内容有关系,里面有“sysinit”等字符串,但是还是没办法搞清楚怎么写inittab文件

inittab文件怎么写

/busybox/examples/下面找到一个inittab脚本的例子,打开,看到一个类似格式说明的句子:

  1. Format for each entry: <id>:<runlevels>:<action>:<process> 

猜测inittab文件里可以有多条entry,每条entry格式中有id、runlevels、action和process这四项内容,这里也出现了action,和代码里的action数组很像。文件里又说id和runlevels无关紧要,好,要搞清楚inittab怎么写,关键在于理解action和process,继续看说明

action

action包括:sysinit、respawn、askfirst、wait、once、restart、ctrlaltdel、和shutdown共八种,

process

指定要运行的程序和它的参数

然后还说了如果没有inittab文件,则运行以下内容

  1. ::sysinit:/etc/init.d/rcS 
  2. ::askfirst:/bin/sh 
  3. ::ctrlaltdel:/sbin/reboot 
  4. ::shutdown:/sbin/swapoff -a 
  5. ::shutdown:/bin/umount -a -r 
  6. ::restart:/sbin/init 
  7. tty2::askfirst:/bin/sh         
  8. tty3::askfirst:/bin/sh 
  9. tty4::askfirst:/bin/sh  

这应该就是代码中如果读不到inittab文件,则执行的一系列net_init_action的内容

再往下看,出现的第一条示例entry

  1. ::sysinit:/etc/init.d/rcS 

是不是很熟悉,linux系统嵌入式设备里通常会有/etc/init.d/rcS这个文件,它是一个shell脚本,根据前面的格式,分析一下,id和runlevel为空,action为sysinit,process为/etc/init.d/rcS,所以第一件要干的事情是去执行rcS脚本,而rcS脚本里可以做自己想做的任何事情了

下一条示例是

  1. ::askfirst:-/bin/sh 

注释说的是启动shell到console口,不管,继续看

  1. tty4::respawn:/sbin/getty 38400 tty5 
  2.  
  3. tty5::respawn:/sbin/getty 38400 tty6  

开启getty

  1. ::restart:/sbin/init 

指定init进程的重启位置

  1. ::ctrlaltdel:/sbin/reboot 
  2.  
  3. ::shutdown:/bin/umount -a -r 
  4.  
  5. ::shutdown:/sbin/swapoff -a  

在重启之前要做的事情

再回到代码上,这个while()循环遍历了inittab文件的每一个entry,解析出entry的四个部分:id、runlevel、action和process,放到一个指针数组char *token[4]中,则token[2]和token[3]代表action和process,程序里调用index_in_strings()函数将token[2]转成字符串,即“sysinit”等值,再调用net_init_action(),分析net_init_action()源码可以看出,其实只是把这些action和process添加到一个链表中,并没有做实际的处理,真正的处理在后续的代码中,parse_inittab()函数返回,

  1. ...... 
  2.    /* Now run everything that needs to be run */ 
  3.    /* First run the sysinit command */ 
  4.    run_actions(SYSINIT); 
  5.    check_delayed_sigs(); 
  6.    /* Next run anything that wants to block */ 
  7.    run_actions(WAIT); 
  8.    check_delayed_sigs(); 
  9.    /* Next run anything to be run only once */ 
  10.    run_actions(ONCE); 
  11.  
  12.    /* Now run the looping stuff for the rest of forever. 
  13.     */ 
  14.    while (1) { 
  15.        ......  

这里调用run_action()运行链表中每一个entry,并且首先运行的是action为sysinit的动作

总结

到这里,大致搞清楚了init进程是怎么启动app的了,上流程图

简单来说,init进程首先分析/etc/inittab文件,当然,可以自己修改busybox源码,让它从任意文件开始分析,如果不存在inittab文件,则执行默认的action;如果inittab文件存在,则根据inittab文件中的条目执行,通常是去/etc/init.d/rcS文件中执行脚本命令,当然,修改源码,你也可以让它执行别的脚本

rcS脚本是以shell脚本语言编写,一般的套路是

  • 加载驱动模块
  • 配置网络,建桥、配网卡地址
  • 启动app 

【编辑推荐】

  1. Linux基本防火墙设置和开放端口命令
  2. 在Linux中分割和重组文件
  3. Linux Shell脚本执行超时怎么办?
  4. Linux程序设计的一些优化措施
  5. 六个优雅的Linux命令行技巧
【责任编辑:庞桂玉 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

Grails权威指南

本书译自Grails项目负责人Graeme Keith Rocher所著的“The Definitive Guide to Grails”一书,着重介绍了如何在Grails框架下使用Groovy语...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× 学习达标赢Beats耳机