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

 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是当前的状态机执行函数,全局变量,随时改变

  1. int main(int argc, char *argv[])
  2. {
  3.     int c;
  4. ……
  5.     while ((c = getopt(argc, argv, “sf”)) != -1)
  6.         switch (c) {
  7.         case ‘s’:
  8.             requested_transition = single_user;  //通过命令行传递参数,以使系统进入单用户模式。
  9.             break;
  10. ……
  11.         }
  12. ……
  13.     transition(requested_transition);  //开始维持状态机,bsd的init本质上就是一个状态机
  14.     /*
  15.      * Should never reach here.
  16.      */
  17.     exit(1);
  18. }

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

  1. void transition(state_t s)
  2. {
  3.     for (;;)                 //这就是无限循环状态机
  4.         s = (state_t) (*s)();
  5. }

所有的状态机状态处理函数就是:
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函数:

  1. state_func_t
  2. runcom(void)
  3. {
  4.     pid_t pid, wpid;
  5.     int status;
  6.     char *argv[4];
  7.     struct sigaction sa;
  8.     if ((pid = fork()) == 0) {   //执行/etc/rc脚本,没有System V那么复杂,就执行一个脚本,脚本内容是什么,鬼才知道
  9.         memset(&sa, 0, sizeof sa);
  10.         sigemptyset(&sa.sa_mask);
  11.         sa.sa_flags = 0;
  12.         sa.sa_handler = SIG_IGN;
  13.         (void) sigaction(SIGTSTP, &sa, NULL);
  14.         (void) sigaction(SIGHUP, &sa, NULL);
  15.         setctty(_PATH_CONSOLE);
  16.         argv[0] = “sh”;
  17.         argv[1] = _PATH_RUNCOM;  //_PATH_RUNCOM就是/etc/rc
  18.         argv[2] = runcom_mode == AUTOBOOT ? “autoboot” : 0;
  19.         argv[3] = 0;
  20.         sigprocmask(SIG_SETMASK, &sa.sa_mask, NULL);
  21.         setprocresources(RESOURCE_RC);
  22.         execv(_PATH_BSHELL, argv);
  23.         stall(“can’t exec %s for %s: %m”, _PATH_BSHELL, _PATH_RUNCOM);
  24.         _exit(1);
  25.     }
  26.     if (pid == -1) {  //fork都没有成功,说明根本没有机会执行/etc/rc,那么只好进入single_user了,此为状态机的一次迁移
  27.         emergency(“can’t fork for %s on %s: %m”,_PATH_BSHELL, _PATH_RUNCOM);
  28.         while (waitpid(-1, NULL, WNOHANG) > 0)
  29.             continue;
  30.         sleep(STALL_TIMEOUT);
  31.         return (state_func_t) single_user;
  32.     }
  33.     do {
  34.         if ((wpid = waitpid(-1, &status, WUNTRACED)) != -1)
  35.             collect_child(wpid);
  36. ……
  37.         if (wpid == pid && WIFSTOPPED(status)) {  //如果/etc/rc停止了,那么就开始它。
  38.             warning(“init: %s on %s stopped, restarting\n”,
  39.                 _PATH_BSHELL, _PATH_RUNCOM);
  40.             kill(pid, SIGCONT);
  41.             wpid = -1;
  42.         }
  43.     } while (wpid != pid);  //退出循环的可能性之一就是/etc/rc真的执行完了
  44.     if (WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM &&
  45.         requested_transition == catatonia) {
  46.         /* /etc/rc executed /sbin/reboot; wait for the end quietly */
  47.         sigset_t s;
  48.         sigfillset(&s);
  49.         for (;;)
  50.             sigsuspend(&s);  //等待重启
  51.     }
  52. ……
  53.     runcom_mode = AUTOBOOT;        
  54.     logwtmp(“~”“reboot”“”);
  55.     return (state_func_t) read_ttys;//初始化完毕,下面就要准备用户登录了,此为状态机的一次迁移。
  56. }

如果说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    0×1        /* session won’t be restarted */
#define    SE_PRESENT    0×2        /* session is in /etc/ttys */
#define    SE_DEVEXISTS    0×4        /* 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,具体的字段上面已经注释很清楚了。
  1. state_func_t read_ttys(void)
  2. {
  3.     int session_index = 0;
  4.     session_t *sp, *snext;
  5.     struct ttyent *typ;
  6.     for (sp = sessions; sp; sp = snext) {  //清除以前的会话。
  7.         if (sp->se_process)
  8.             clear_session_logs(sp);
  9.         snext = sp->se_next;
  10.         free_session(sp);
  11.     }
  12.     sessions = 0;                         //重置全局会话链表
  13.     if (start_session_db())
  14.         return (state_func_t) single_user;
  15.     while ((typ = getttyent()))           //重新初始化可能的会话,每个tty一个
  16.         if ((snext = new_session(sp, ++session_index, typ)))
  17.             sp = snext;
  18.     endttyent();
  19.     return (state_func_t) multi_user;     //进入多用户模式,注意,在进入多用户之前必须执行/etc/rc,这是由状态机控制的。
  20. }

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

  1. state_func_t multi_user(void)
  2. {
  3.     pid_t pid;
  4.     session_t *sp;
  5.     requested_transition = 0;
  6.     if (getsecuritylevel() == 0)
  7.         setsecuritylevel(1);
  8.     for (sp = sessions; sp; sp = sp->se_next) {
  9.         if (sp->se_process)
  10.             continue;
  11.         if ((pid = start_getty(sp)) == -1) {  //开启会话,开始准备多用户登录
  12.             /* 出现错误,不能再继续了,于是退回,清除ttys,这是在多用户初始化过程中,不允许有错误出现的*/
  13.             requested_transition = clean_ttys;
  14.             break;
  15.         }
  16.         sp->se_process = pid;
  17.         sp->se_started = time(NULL);
  18.         add_session(sp);
  19.     }
  20.     while (!requested_transition)             //比如上面没有出现严重错误,那么在此等待用户登录会话的退出
  21.         if ((pid = waitpid(-1, NULL, 0)) != -1)
  22.             collect_child(pid);       //搜集子进程退出信息。
  23.     return (state_func_t) requested_transition;  //如果出现了错误,则执行clean_ttys状态机处理函数
  24. }
  25. void collect_child(pid_t pid)
  26. {
  27.     session_t *sp, *sprev, *snext;
  28.     if (sessions == NULL)  //确保当前的会话链表非空,就是说确保当前有会话
  29.         return;
  30.     if ((sp = find_session(pid)) == NULL)   //确保这个pid的会话已经加入全局链表
  31.         return;
  32.     clear_session_logs(sp);
  33.     login_fbtab(sp->se_device + sizeof(_PATH_DEV) - 1, 0, 0);
  34.     del_session(sp);
  35.     sp->se_process = 0;
  36.     if (sp->se_flags & SE_SHUTDOWN) {  //如果设置了SE_SHUTDOWN标志代表此会话不必重启了
  37.         if ((sprev = sp->se_prev))
  38.             sprev->se_next = sp->se_next;
  39.         else
  40.             sessions = sp->se_next;
  41.         if ((snext = sp->se_next))
  42.             snext->se_prev = sp->se_prev;
  43.         free_session(sp);          //释放
  44.         return;
  45.     }
  46.     if ((pid = start_getty(sp)) == -1) {  //重新开启一个会话终端,这就是为何你在shell敲入exit后马上又出现了login的原因
  47.         requested_transition = clean_ttys;  //发生严重错误不是清理终端,而是重试,这样不影响别的终端
  48.         return;
  49.     }
  50.     sp->se_process = pid;
  51.     sp->se_started = time(NULL);
  52.     add_session(sp); //重启成功后加入新建的会话。
  53. }

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/archive/2008/11/12/3280477.aspx

原创文章,转载请注明: 转载自PT Ubuntu Blog

本文链接地址: http://www.ptubuntu.com/2008/12/778.html

  • Share/Bookmark

Leave a Reply


Verify Code   If you cannot see the CheckCode image,please refresh the page again!

Powered by WordPress | Buy free cell phones from at&t. | Thanks to PalmPreBlog.com, MMO Games and Conveyancing