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

本文详细解析了BSD系统的init程序工作机制,包括其状态机模型、会话管理和终端控制等关键特性,并对比了与System V init的不同之处。

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的所有东西一向以复杂著称,但是有的时候确实方便了用户。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值