从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。
阅读(2803) | 评论(0) | 转发(0) |