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 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,具体的字段上面已经注释很清楚了。
- 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/archive/2008/11/12/3280477.aspx
原创文章,转载请注明: 转载自PT Ubuntu Blog