分享

从init到shell

 astrotycoon 2013-09-18
从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。
       

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多