System V的启动风格和BSD的启动风格(2)---代码角度

晨曦之光 发布于 2012/04/10 15:01
阅读 236
收藏 1

BSD 中没有运行级别的概念,一些文章上说的bsd运行级别是错误的。bsd的init进程通篇维持一个状态机,该状态机在不同状态间迁移,比如用户在 shell敲入init 3(实际上这种情况不会发生,bsd不允许init第二次运行,这里仅仅通过System V的方式举个例子),那么就有可能引起状态机的迁移,再比如用户给init进程发送了一个信号,也有可能引起状态机迁移。
typedef long (*state_func_t)(void);   
typedef state_func_t (*state_t)(void);  //从字面上理解state_t就是一个状态机,它实质上是一个函数指针,这个函数指针在很多地方会改变,比如信号处理,比如命令行参数,比如出错...从而引起状态机的状态迁移。
#define DEFAULT_STATE        runcom  //默认的状态机初始状态函数就是runcom
state_t requested_transition = DEFAULT_STATE;   //requested_transition是当前的状态机执行函数,全局变量,随时改变

int main(int argc, char *argv[])

{

    int c;

......

    while ((c = getopt(argc, argv, "sf")) != -1)

        switch (c) {

        case 's':

            requested_transition = single_user;  //通过命令行传递参数,以使系统进入单用户模式。

            break;

......

        }

......

    transition(requested_transition);  //开始维持状态机,bsd的init本质上就是一个状态机

    /*

     * Should never reach here.

     */

    exit(1);

}

看看到底transition函数是怎么一回事:

void transition(state_t s)

{

    for (;;)                 //这就是无限循环状态机

        s = (state_t) (*s)();

}

所有的状态机状态处理函数就是:
state_func_t single_user(void); //单用户
state_func_t runcom(void);      //此为初始化时的初始状态,要读/etc/rc脚本并执行之的
state_func_t read_ttys(void);   
state_func_t multi_user(void);  //多用户
state_func_t clean_ttys(void);  //清理ttys以便重新开始
state_func_t catatonia(void);
state_func_t death(void);
state_func_t nice_death(void);
现在看一下runcom函数:

state_func_t runcom(void)

{

    pid_t pid, wpid;

    int status;

    char *argv[4];

    struct sigaction sa;

    if ((pid = fork()) == 0) {   //执行/etc/rc脚本,没有System V那么复杂,就执行一个脚本,脚本内容是什么,鬼才知道

        memset(&sa, 0, sizeof sa);

        sigemptyset(&sa.sa_mask);

        sa.sa_flags = 0;

        sa.sa_handler = SIG_IGN;

        (void) sigaction(SIGTSTP, &sa, NULL);

        (void) sigaction(SIGHUP, &sa, NULL);

        setctty(_PATH_CONSOLE);

        argv[0] = "sh";

        argv[1] = _PATH_RUNCOM;  //_PATH_RUNCOM就是/etc/rc

        argv[2] = runcom_mode == AUTOBOOT ? "autoboot" : 0;

        argv[3] = 0;

        sigprocmask(SIG_SETMASK, &sa.sa_mask, NULL);

        setprocresources(RESOURCE_RC);

        execv(_PATH_BSHELL, argv);

        stall("can't exec %s for %s: %m", _PATH_BSHELL, _PATH_RUNCOM);

        _exit(1);

    }

    if (pid == -1) {  //fork都没有成功,说明根本没有机会执行/etc/rc,那么只好进入single_user了,此为状态机的一次迁移

        emergency("can't fork for %s on %s: %m",_PATH_BSHELL, _PATH_RUNCOM);

        while (waitpid(-1, NULL, WNOHANG) > 0)

            continue;

        sleep(STALL_TIMEOUT);

        return (state_func_t) single_user;

    }

    do {

        if ((wpid = waitpid(-1, &status, WUNTRACED)) != -1)

            collect_child(wpid);

......

        if (wpid == pid && WIFSTOPPED(status)) {  //如果/etc/rc停止了,那么就开始它。

            warning("init: %s on %s stopped, restarting/n",

                _PATH_BSHELL, _PATH_RUNCOM);

            kill(pid, SIGCONT);

            wpid = -1;

        }

    } while (wpid != pid);  //退出循环的可能性之一就是/etc/rc真的执行完了

    if (WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM &&

        requested_transition == catatonia) {

        /* /etc/rc executed /sbin/reboot; wait for the end quietly */

        sigset_t s;

        sigfillset(&s);

        for (;;)

            sigsuspend(&s);  //等待重启

    }

......

    runcom_mode = AUTOBOOT;       

    logwtmp("~", "reboot", "");

    return (state_func_t) read_ttys;//初始化完毕,下面就要准备用户登录了,此为状态机的一次迁移。

}

如果说CHILD是System V的init程序中至关重要的结构的话,那么BSD中至关重要的结构就是session_t了,它代表了一个登录会话。
typedef struct init_session {
    int    se_index;        /* tty的索引 */
    pid_t    se_process;        /* 此会话的控制进程,比如getty */
    time_t    se_started;        
    int    se_flags;        /* 会话的状态 */
#define    SE_SHUTDOWN    0x1        /* session won't be restarted */
#define    SE_PRESENT    0x2        /* session is in /etc/ttys */
#define    SE_DEVEXISTS    0x4        /* open does not result in ENODEV */
    char    *se_device;        /* filename of port */
    char    *se_getty;        /* what to run on that port */
    char    **se_getty_argv;    /* pre-parsed argument array */
    char    *se_window;        /* window system (started only once) */
    char    **se_window_argv;    /* pre-parsed argument array */
    struct    init_session *se_prev;  //链表结构,这个和System V的很类似,只不过System V关注的是子进程,而BSD根本不管子进程具体的事,只操心会话,子进程的具体信息包含在会话中。实际上BSD中的init的子进程根本没有那么复杂那么 多,/etc/rc在runcom中就执行完毕了,所有的初始化工作也就执行完毕,余下的只是用户登录的控制了,所以没有那么多的类似“结束之后重启”之 类的控制。
    struct    init_session *se_next;
} session_t;
函数getttyent 在bsd中是很重要的一个函数,新的会话就是通过这个函数初始化的,因为一个tty一个会话,所以这是很显然的,在bsd中/etc/ttys文件包含了 所有的要启动会话的终端信息getttyent函数就是读取这个文件然后由这个文件指示的tty信息采取相应的行为,文件中每一行包含一个tty,可以映 射到一个结构体:

struct ttyent {
char *ty_name; /* terminal device name */
char *ty_getty; /* command to execute, usually getty */
char *ty_type; /* terminal type for termcap */
int ty_status; /* status flags */
char *ty_window; /* command to start up window manager */
char *ty_comment; /* comment field */
};
这个结构体就是下面函数中读取到的typ,具体的字段上面已经注释很清楚了。

state_func_t read_ttys(void)

{

    int session_index = 0;

    session_t *sp, *snext;

    struct ttyent *typ;

    for (sp = sessions; sp; sp = snext) {  //清除以前的会话。

        if (sp->se_process)

            clear_session_logs(sp);

        snext = sp->se_next;

        free_session(sp);

    }

    sessions = 0;                         //重置全局会话链表

    if (start_session_db())

        return (state_func_t) single_user;

    while ((typ = getttyent()))           //重新初始化可能的会话,每个tty一个

        if ((snext = new_session(sp, ++session_index, typ)))

            sp = snext;

    endttyent();

    return (state_func_t) multi_user;     //进入多用户模式,注意,在进入多用户之前必须执行/etc/rc,这是由状态机控制的。

}

在 进入单用户之后,fork一个子进程执行shell,而父进程循环等待子进程结束或者等待requested_transition被重置,一旦 requested_transition被重置就说明状态机要求迁移了,那么就有可能跳出单用户模式进入别的模式,因此一旦 requested_transition状态改变,父进程循环结束,开始执行requested_transition状态机处理函数,但是如果子进程结束了requested_transition也没有置位,那么就要重新执行runcom进而重新加载行/etc/rc了,这是由状态机控制的。
下面看一下multi_user(连同一起看一下collect_child):

state_func_t multi_user(void)

{

    pid_t pid;

    session_t *sp;

    requested_transition = 0;

    if (getsecuritylevel() == 0)

        setsecuritylevel(1);

    for (sp = sessions; sp; sp = sp->se_next) {

        if (sp->se_process)

            continue;

        if ((pid = start_getty(sp)) == -1) {  //开启会话,开始准备多用户登录

            /* 出现错误,不能再继续了,于是退回,清除ttys,这是在多用户初始化过程中,不允许有错误出现的*/

            requested_transition = clean_ttys;

            break;

        }

        sp->se_process = pid;

        sp->se_started = time(NULL);

        add_session(sp);

    }

    while (!requested_transition)             //比如上面没有出现严重错误,那么在此等待用户登录会话的退出

        if ((pid = waitpid(-1, NULL, 0)) != -1)

            collect_child(pid);       //搜集子进程退出信息。

    return (state_func_t) requested_transition;  //如果出现了错误,则执行clean_ttys状态机处理函数

}

void collect_child(pid_t pid)

{

    session_t *sp, *sprev, *snext;

    if (sessions == NULL)  //确保当前的会话链表非空,就是说确保当前有会话

        return;

   if ((sp = find_session(pid)) == NULL)//确保这个pid的会话已经加入全局链表

        return;

    clear_session_logs(sp);

    login_fbtab(sp->se_device + sizeof(_PATH_DEV) - 1, 0, 0);

    del_session(sp);

    sp->se_process = 0;

    if (sp->se_flags & SE_SHUTDOWN) {  //如果设置了SE_SHUTDOWN标志代表此会话不必重启了

        if ((sprev = sp->se_prev))

            sprev->se_next = sp->se_next;

        else

            sessions = sp->se_next;

        if ((snext = sp->se_next))

            snext->se_prev = sp->se_prev;

        free_session(sp);          //释放

        return;

    }

    if ((pid = start_getty(sp)) == -1) {  //重新开启一个会话终端,这就是为何你在shell敲入exit后马上又出现了login的原因

        requested_transition = clean_ttys;  //发生严重错误不是清理终端,而是重试,这样不影响别的终端

        return;

    }

    sp->se_process = pid;

    sp->se_started = time(NULL);

    add_session(sp); //重启成功后加入新建的会话。

}

start_getty就是执行ttyent中ty_getty指示的命令行程序,一般为getty。clean_ttys就是给所有的会话发送终止信号,然后开启新的会话。start_getty就是开启新会话的函数,它运行getty程序。
还有很多状态机处理函数我就不一一讨论了,可以自己看代码。
以上就是BSD的init程序的大体框架,从中可以看出它和System V的有太大的不同,仅仅执行一遍/etc/rc脚本,至于这个脚本怎么写的,根本不管,如果说有人在此脚本写了一个死循环,那么很抱歉,玩大了!而且 bsd不允许执行init x,只能给init进程发送信号,而重新加载并执行/etc/rc的机会也不大。按照机制和策略的观点考虑,bsd的init完全实现了机制而丝毫不管策 略,完全由/etc/rc全权负责,init进程和启动脚本完全解耦,init进程关心的只是终端会话,根本不管别的。
还有一个话题就是/etc/rc.d下面的文件以及目录问题,其实这些无论System V还是BSD都是启动脚本的策略,和init程序本身没有关系了。
  linux的很多发行版为何用System V的启动风格呢?我想这是因为bsd的太容易出错的缘故,System V的所有东西一向以复杂著称,但是有的时候确实方便了用户。


原文链接:http://blog.csdn.net/dog250/article/details/5303620
加载中
返回顶部
顶部