从init到shell start_kernel()->rest_init()->kernel_thread() 从此之后 ... schedule(); cpu_idle(); cpu开始空转(如果没有进程要运行的话),而kernel_thread()创建出了根内核进程init,init的内容如下 if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) printk(KERN_WARNING "Warning: unable to open an initial console.\n"); (void) sys_dup(0); (void) sys_dup(0); /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) run_init_process(execute_command); run_init_process("/sbin/init"); run_init_process("/etc/init"); run_init_process("/bin/init"); run_init_process("/bin/sh"); panic("No init found. Try passing init= option to kernel."); 当我们从命令行中传递过去init=xxx的字样的时候(不用initrd),那么execute_command中的内容就是 xxx 了,经实验,除了 "noinitrd root=/dev/mtdblock2 init=/linuxrc console=ttySAC0" 下面的init也是可以的 init=/sbin/init 或者/bin/sh 或者init=/sbin/getty 115200 console 其实linuxrc是链接到/bin/busybox的,而busybox就是首先执行的/sbin/init(我猜的), 注意sys_open((const char __user *) "/dev/console", O_RDWR, 0)这个句子,为什么呢? 那要从tty驱动谈起: 1。 其实每个tty设备(4,1)(4,2)(4,3)...(4,63)(4,64)...都想认领一个进程,作为与自己交互的对象,就是说,当我们往某个tty 设备上写入数据时,或者希望从某个tty设备上得到数据时,tty设备(驱动)都希望把这些数据流交给某个进程去处理,否则tty没有存在的意义 从tty驱动的角度来看,这个程序就是发起一次会话的那个进程。 2。 从这个发起对话的进程的角度来看,任何一个想使用tty驱动程序,来完成与用户交互的进程都希望能得到一个控制终端(键盘+vga或者串口), 以便能和用户互动。 所以open一个tty设备就要小心了,因为有些进程不想与用户交互(比如守护进程,他不希望给用户什么,也不希望从用户那里得到什么)。 扯回来,我们在init内核进程中,他是do_fork创建出来的第一个进程(或者叫内核线程?),kernel_thread->do_fork(),他的pid是1。 /dev/console (5,1)是一个终端设备(控制台),打开它有可能是本init内核线程拥有一个控制终端(控制台),而init此时并不需要得到一个 控制台,那么此时的open就得保证打开/dev/console(5,1)时不会成为其控制进程(controlling process)。怎么保证一个进程在打开tty 设备文件时不会成为其控制进程呢?那得看tty_open()了 if (!noctty && current->signal->leader && !current->signal->tty && tty->session == 0) { task_lock(current); current->signal->tty = tty; task_unlock(current); current->signal->tty_old_pgrp = 0; tty->session = current->signal->session; tty->pgrp = process_group(current); } 要想一个进程打开tty设备文件时不成为其控制进程只要满足下面的条件之一就可以了 1。open是指定O_NOCTTY。 2。本进程不是一个会话的首进程,就是说本进程不想开始一个对话,于用户交流。 3。本进程已经有了控制终端,就是说一个进程不能同时与两个tty设备交互 4。本tty设备(驱动)已经有一个进程认领了,就是说本tty设备已经与一个进程交互,开始和用户对话了。 initialize_tty_struct()将tty_struct结构的所有不关心的元素都设置成了0(memset()) 所以tty_open的时候,如果是第一次打开某个tty设备, 那么tty->session 肯定为 0,表示还没有开始一个对话 那么此时的 sys_open((const char __user *) "/dev/console", O_RDWR, 0)会不会将自己 变成(5,1)设备的控制进程呢? 答案是不会,这里的第2个条件被满足。 rest_init->kernel_thread->do_fork,而 do_fork->copy_process->copy_signal->sig->leader = 0; /* session leadership doesn't inherit */ 也就是说凡是do_fork函数创建的内核线程,他的leader初始值都是0,意义重大,表示他不想作为一个会话的的领导进程, 就是说他不想开始一次会话。那么到底是谁想开始会话,与用户交互呢? run_init_process(execute_command)->execve->do_execve最终把linuxrc文件load到内存并运行。 linuxrc是到busybox的符号连接,而busybox执行init.c文件(有待证实,如果从命令行传递init=/bin/init,跟init=/linuxrc 效果是一样的),加上do_execve()不改变进程pid,进程组id,等等,只要不出错返回,现在内核init线程已经消失,蜕变成了用户空间 的/bin/busybox程序。那么busybox进程会不会是对话首进程呢? 在好好看看这个execve() execve(init_filename, argv_init, envp_init); static char * argv_init[MAX_INIT_ARGS+2] = { "init", NULL, }; char * envp_init[MAX_INIT_ENVS+2] = { "HOME=/", "TERM=linux", NULL, }; 上面的init_filename就是/linuxrc 他增加了两个环境变量"HOME=/", "TERM=linux",并且传递给main()函数 的argv是"init", 经过两个dup,此时的内核线程init已经有了标准输入,输出,错误输出了。从而 execve()也继承了这三个已经打开的文件,即fd 0,1,2。没有执行时关闭标志 这样就到了busybox的main()中: 到了这里,其实我们相当于在运行 #init 这样的shell命令,表示此时的main()函数的argc=1,agrv[0]="init", 其实这跟 #busybox init 或者 #/bin/busybox busybox busybox init是一样的效果最终都会调用busybox里的init_main() 函数调用路径是 main()->run_applet_by_name()->...到具体的小程序中 现在应该到了 init_main() 时刻提醒自己,现在的串口控制台还没有认领对话进程呢。 到了这里 int init_main(int argc, char **argv) 此时的argc=1,argv[0]="init" if (argc > 1 && !strcmp(argv[1], "-q")) { return kill(1,SIGHUP); } 首先是对退出参数的检测,如果#init -q的话,肯定会执行成功的,不信你可以#echo $? if (getpid() != 1 && (!ENABLE_FEATURE_INITRD || !strstr(bb_applet_name, "linuxrc"))) { bb_show_usage(); //这个usage什么也不做,只是设置出错码并退出 } 这个逻辑的意思是: 1。如果本进程是linux的一号进程,就是经过蜕变的那个进程可以继续运行 2。如果不是一号进程,那么肯定是从shell中执行的init命令:这又分两种情况 1。如果只是单纯的敲入#init或者#busybox init,那么不可以继续运行 2。如果init的前面有linuxrc字样,恭喜你,init会继续为你服务 例如你敲上#/linuxrc init,此时^c会使linux重新启动 signal(SIGHUP, exec_signal); signal(SIGQUIT, exec_signal); signal(SIGUSR1, shutdown_signal); signal(SIGUSR2, shutdown_signal); signal(SIGINT, ctrlaltdel_signal); signal(SIGTERM, shutdown_signal); signal(SIGCONT, cont_handler); signal(SIGSTOP, stop_handler); signal(SIGTSTP, stop_handler); 安装信号处理函数,如果此时你按住^c,那么应该不会重启,因为tty驱动还没有开始对话呢,也就是说console现在没人认领呢。 /* Turn off rebooting via CTL-ALT-DEL -- we get a * SIGINT on CAD so we can shut things down gracefully... */ init_reboot(RB_DISABLE_CAD); static void init_reboot(unsigned long magic) { pid_t pid; /* We have to fork here, since the kernel calls do_exit(0) in * linux/kernel/sys.c, which can cause the machine to panic when * the init process is killed.... */ if ((pid = fork()) == 0) { reboot(magic); _exit(0); } waitpid (pid, NULL, 0); } 这里他通过系统调用sys_reboot()告诉linux不要把ctl+alt+del解释成重新启动,而是什么都不作。 /* Figure out where the default console should be */ console_init(); 这个函数用于确定init使用的控制台类型,有两类(serial和vga+键盘),我们的init在内核线程状态的时候就已经打开了三个 文件了,fd 0,1,2。(/dev/console),经execve(),init到了busybox,他还继承着那三个文件描述符呢。现在是时候 测试下这个/dev/console到底是什么终端的时候了(肯定是串口控制台),他测试/dev/console文件到底是什么控制台的方法是: 1。 首先确定控制台设备文件 1。 如果linux传递过来了console CONSOLE这样的环境变量,那么,就认定,这两个变量里就应该 是控制台设备文件了。 2。 如果linux没有传递过来这样的参数(当然没有,除非你自己putenv()),那么用ioctl(0, TIOCGSERIAL, &sr)去测试 /dev/console是不是串口控制台,如果ioctl返回0说明在串口核心层实现了这个ioctl,也就是说有串口tty驱动被安装,那么 确定设备文件为/dev/tts/0 3。 如果/dev/console不是串口控制台,那就测试ioctl(0, VT_GETSTATE, &vt)是否返回0,是,就说明/dev/console是 虚拟控制台(vga+键盘),确定设备文件为/dev/vc/0 4。 不是以上两中控制台,那么是用默认设备文件/dev/console 其实我的控制台设备文件这里被确定为/dev/tts/0 2。 确定了控制台设备文件还不够,如果打不开这个设备文件,也是徒劳。所以现在去打开它, 如果打开成功,说明存在此设备文件(/dev/tts/0),其实这个文件在初始化串口设备驱动的时候已经被创建了(/serial/s3c2410.c) 最坏的情况是串口控制台不能使用,虚拟控制台也不能使用,那就把设备文件改回/dev/console,这中情况发生在你用并口控制台? 如果是串口控制台,用putenv("TERM=vt102");将linux传递过来的TERM环境参数改成vt102,为以后作准备。 3。 最后关闭这个文件,到此,这个函数已经确定了一个可用的控制台设备文件了,即/dev/tts/0 注意这个函数里的打开文件操作,因为这个操作有可能开始一个对话,这里没有,因为。。。 继续init_main /* Close whatever files are open, and reset the console. */ close(0); close(1); close(2); if (device_open(console, O_RDWR | O_NOCTTY) == 0) { set_term(); close(0); } chdir("/"); setsid(); 关闭从init那里继承过来的三个文件描述符,此时如果在打开一个文件的话,毫无疑问返回的fd应该是0, device_open()打开某个文件,并返回他的文件fd,并且保证此文件不阻塞打开,因为init没时间等,也没必要等 然后调用set_term()设置此串口tty设备(/dev/tts/0)的驱动属性。这其实就确定使用串口的方法,注意 此函数只能从标准输入fd设置某个设备的驱动属性。 然后改变当前目录为根目录,并调用setsid(),这是一次伟大的历史转折点,因为一旦init进程调用了setsid() 就表示它要启动一次对话了,因为调用setsid()结果是该进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。 由于会话过程对控制终端的独占性,进程同时与控制终端脱离。 对于init进程,谈不上与控制终端脱离,因为他压根儿没有取得驱动控制终端哦!这就是每次open操作我所描述的。 其实这里有个问题,就是init能不能setsid()成功?答案当然是能。但是为什么呢?记得守护进程setsid()成功的秘诀是 父进程退出,在子进程中才能setsid(),意思是说,只有当一个进程不是组长进程的时候,才可以setsid(),问题是init是不是 进程组组长,当然不是,组长是0号进程,init的父进程也是0号进程,因为他是0号进程fork()过来的,所以敢肯定他不是组长哦。 在内核代码找了好久没找到,但是可以如下证明: lzd@lzd-laptop:~$ ps -axj |head -10 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 0 1 1 1 ? -1 Ss 0 0:02 /sbin/init 0 2 0 0 ? -1 S< 0 0:00 [kthreadd] 2 3 0 0 ? -1 S< 0 0:00 [migration/0] 2 4 0 0 ? -1 S< 0 0:00 [ksoftirqd/0] 2 5 0 0 ? -1 S< 0 0:00 [watchdog/0] 2 6 0 0 ? -1 S< 0 0:00 [events/0] ... 看到了吧,init的父亲是0号内核线程,就是start_kernel(),之所以进程组id现在成了1,对话组id成了1,是因为setsid的结果。 还可以发现 kthreadd也是0号进程字进程,他的对话id(无意义),组id都是0,说明这也是0号内核线程fork()出来的,只是没有setsid(), 呵呵,之后的进程都属于0号进程组,说明他们的祖宗是0号进程,但是父进程成了2,说明这些内核线程是2 fork出来的。当然这里的是x86的 情况,arm板子上跑的那个linux也应该相同的道理,只是busybox的ps是简化的,所以... ok,回到init,他现在已经setsid()了哦!!! 此时如果他不小心open一个终端设备,并且没有加上O_NOCTTY标志,那么毫无疑问本进程会成为他的控制进程,那个tty设备也会认领本 进程为自己的对话进程。 { const char * const *e; /* Make sure environs is set to something sane */ for(e = environment; *e; e++) putenv((char *) *e); } 把预定义的环境变量递交给内核。跳过启动swapon的操作,没有硬盘交换有什么用吗??? /* Check if we are supposed to be in single user mode */ if (argc > 1 && (!strcmp(argv[1], "single") || !strcmp(argv[1], "-s") || !strcmp(argv[1], "1"))) { /* Start a shell on console */ new_init_action(RESPAWN, bb_default_login_shell, ""); } 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., runs INIT_SCRIPT and then starts a pair * of "askfirst" shells */ parse_inittab(); } 如果传递过来的命令行参数里有“single“ 或 ”-s“ 或 ”1“ 字样的参数,比如 "noinitrd root=/dev/mtdblock2 init=/linuxrc single console=ttySAC0" 那么init为你确定一个默认的登录shell。否则就认为你不是忘了密码,那么解析/etc/inittab文件的内容。 先看看单用户模式,init为我们作了什么。 /* Allowed init action types */ #define SYSINIT 0x001 #define RESPAWN 0x002 #define ASKFIRST 0x004 #define WAIT 0x008 #define ONCE 0x010 #define CTRLALTDEL 0x020 #define SHUTDOWN 0x040 #define RESTART 0x080 new_init_action(RESPAWN,"-/bin/sh","") /* Set up a linked list of init_actions, to be read from inittab */ struct init_action { pid_t pid; //这个行为的进程号 char command[INIT_BUFFS_SIZE]; //这个行为的命令 char terminal[CONSOLE_BUFF_SIZE]; //使用的终端 struct init_action *next; //下个行为 int action; //行为的类型,上面8种之一 }; /* Static variables */ static struct init_action *init_action_list = NULL; 这里给以后需要的操作创建了一个单链表,新的脸表元素会追加到以init_action_list为链表头的最后。 如果没有指定最后的控制台参数,使用默认的console,就是前面确定的那个console设备(/dev/tts/0) 如果指定使用/dev/null作为控制台,并且是ASKFIRST的行为,那么不追加,当作不存在。 如果新加入的"行为"的命令和使用控制台与原来的某个行为一样,那么只覆盖原来的行为,也不追加。 parse_inittab(); 很重要,如果没有配置CONFIG_FEATURE_USE_INITTAB,那么使用init默认的各种行为, 如果配置了CONFIG_FEATURE_USE_INITTAB,但是文件系统中找不到/etc/inittab,那么也使用默认 友善的就是找不到/etc/inittab,使用的是默认的各种行为,如果你把/etc/inittab_改成inittab, 那么会发现login程序,否则就直接给shell了。 /* Make the command line just say "init" -- thats all, nothing else */ fixup_argv(argc, argv, "init"); 到了这里,像他所说的,只是保证argv第一个字符串是 "init",其他的都清空(wipe),使ps不会很凌乱(clutter)? 在下面的逻辑之前,先看看init默认的行为(就是不用inittab文件的情况)都在 init_action_list 链入了什么 CTRLALTDEL :/sbin/reboot :/dev/tts/0 SHUTDOWN :/bin/umount -a -r :/dev/tts/0 SHUTDOWN :/sbin/swapoff -a :/dev/tts/0 RESTART :/sbin/init :/dev/tts/0 ASKFIRST :-/bin/sh :/dev/tts/0 :-/bin/sh :/dev/vc/2 :-/bin/sh :/dev/vc/3 :-/bin/sh :/dev/vc/4 SYSINIT :/etc/init.d/rcS :/dev/tts/0 开始下面的逻辑分析: /* Now run everything that needs to be run */ /* First run the sysinit command */ run_actions(SYSINIT); /* Next run anything that wants to block */ run_actions(WAIT); /* Next run anything to be run only once */ run_actions(ONCE); #ifdef CONFIG_FEATURE_USE_INITTAB /* Redefine SIGHUP to reread /etc/inittab */ signal(SIGHUP, reload_signal); #else signal(SIGHUP, SIG_IGN); #endif /* CONFIG_FEATURE_USE_INITTAB */ 分析下run_actions()这个函数的逻辑。 /* Run all commands of a particular type */ 像他解释的那样,此函数将所有的8种命令行为细分成三组: 1:SYSINIT | WAIT | “CTRLALTDEL | SHUTDOWN | RESTART” 2:ONCE 3:RESPAWN | ASKFIRST 对于第一组中的行为,run_actions()->waitfor()->run() init(pid=1)在调用了run_actions()之后,将运行队列中的所有对应的行为。当运行的是1组中的行为时,run_actions() (pid=1)期望能够给run的子进程收尸,而在run()中,落实到最终的exec()调用时,父进程仍然期待给子进程收尸,并且 会在退出之前还原控制终端为未用状态(steal away)。也就是说,组1的命令不会是一次对话的开始。最后pid=1的进程会从列表 中删除组1,2中的命令行为,而对于组三中的命令行为,则保留在队列中,但是对他们的处理是,如果这些命令在运行,那么 在调用run_actions()将不会执行。 这里的逻辑意义似乎是对于组1,2中的东东,init都视为系统进入一个可用环境之前,必须要运行的初始化环境过程, 而组3被认为是发起一次会话的过程。 当run的是ASKFIRST时,会打印 Please press Enter to activate this console. 字样。可以发现init运行这些行为的顺序依次是 1。SYSINIT 2。WAIT 3。ONCE 4。解析“CTRLALTDEL | SHUTDOWN | RESTART”信号 5。进入无限循环。先RESPAWN,后ASKFIRST。 对于当找不到inittab文件,或者使用默认的配置的时候,只有1。SYSINIT被执行。然后进入了主循环 /* Now run the looping stuff for the rest of forever */ while (1) { /* run the respawn stuff */ run_actions(RESPAWN); /* run the askfirst stuff */ run_actions(ASKFIRST); /* Don't consume all CPU time -- sleep a bit */ sleep(1); /* Wait for a child process to exit */ wpid = wait(NULL); while (wpid > 0) { /* Find out who died and clean up their corpse */ for (a = init_action_list; a; a = a->next) { if (a->pid == wpid) { /* Set the pid to 0 so that the process gets * restarted by run_actions() */ a->pid = 0; message(LOG, "Process '%s' (pid %d) exited. " "Scheduling it for restart.", a->command, wpid); } } /* see if anyone else is waiting to be reaped */ wpid = waitpid (-1, NULL, WNOHANG); } } } init的主要任务就是给他的子进程收尸了,而子进程可以是RESPAWN ASKFIRST组中的执行命令,也可以是init收养的孤儿进程 如果是RESPAWN ASKFIRST组中的东东,那么a->pid = 0将导致while的前两句从新执行。对于默认,当你exit退出本次对话 的时候,此init将不断给你一个shell。 |
|
来自: astrotycoon > 《busybox》