System V的启动风格和bsd的启动风格不同,网上很多文章介绍这件事,但是那些文章仅仅从应用的角度来分析,这在理解上就有很大的困难,毕竟差异算什么呢?充其量仅仅算一种习惯,没有孰优孰劣的,如果让读者非得分出个所以然来,仅从应用角度来理解就不够了,最好的方法就是从源代码的角度看个究竟,几乎所有的人都 知道unix和类unix系统是从init程序开始生命历程的,但是却有了System V和BSD两种风格的启动,说什么/etc/inittab,/etc/rc.d/等等的不同,让人真的很难分辨清楚这到底是怎么一回事,实际上根本没有那么复杂,执行什么脚本以及怎么执行只是它们各自init程序的约定,从这个意义上讲这些启动脚本就是一种动态语言,而init程序就是解释器,本文就分 析System V和BSD的init源代码,然后试着分析它们有什么不同并试着指出它们各自的特点以及思想。
System V和BSD的启动程序都是/sbin/init,源代码都是init.c,我先分析System V的init.c文件(以下简称init)。init从main开始执行,最开始解析命令行参数,随后就进入了一个大循环,细节我就不说了,主要谈一下代 码逻辑进而指出为什么启动脚本的不同:
int main(int argc, char **argv)
{
...
init_main(dfl_level);
/*NOTREACHED*/
return 0;
}
init_main 函数就是一切的循环,在其内部主要解析了/etc/inittab脚本,在进一步分析这个主循环之前首先熟悉一个数据结构,这个数据结构非常重要,从 inittab中分析出的一一个要执行的程序就连同它的一些环境变量存储到这个结构里,然后所有这些结构链接成一个全局链表,这个结构就是
CHILD:
typedef struct _child_ {
int flags; /* inittab中指示进程的当前状态,比如正在执行,已退出,等待,已经执行过等等 */
int exstat; /* inittab中指示进程如果执行后退出的退出状态,init程序由此状态和下面的action字段采取不同的动作 */
int pid; /* 如果这个进程已经开始执行,那么这个字段表示该进程的pid */
time_t tm; /* When respawned last */
int count; /* Times respawned in the last 2 minutes */
char id[8]; /* Inittab中程序唯一的id号 */
char rlevel[12]; /* 运行级别,是System V抽象出的概念,BSD并没有运行级别的概念*/
int action; /* 程序执行的方式,比如wait就是等待这个进程结束在继续,respawn就是结束后重启 */
char process[128]; /* 命令行,实际就是要执行的程序或脚本的全路径和参数 */
struct _child_ *new; /* New entry (after inittab re-read) */
struct _child_ *next; /* For the linked list */
} CHILD;
下面开始分析主循环
int init_main()
{
CHILD *ch;
struct sigaction sa;
sigset_t sgt;
pid_t rc;
int f, st;
if (!reload) {
for(f = 1; f <= NSIG; f++)
SETSIG(sa, f, SIG_IGN, SA_RESTART);
}
SETSIG(sa, SIGALRM, signal_handler, 0);//设置信号处理器,这个信号处理器是很有意思的,仅仅搜集信号并不处理,具体处理工作由下面的循环来负责。
SETSIG(sa, SIGHUP, signal_handler, 0);
SETSIG(sa, SIGINT, signal_handler, 0);
SETSIG(sa, SIGCHLD, chld_handler, SA_RESTART);//这个chld_handler信号处理器很重要,inittab脚本中的程序的执行方式不同,有的结束后重新开始有的只执行一次,有的等待其结束才往下进行,这个处理器就是探测子程序结束的信号的。
SETSIG(sa, SIGPWR, signal_handler, 0);
SETSIG(sa, SIGWINCH, signal_handler, 0);
SETSIG(sa, SIGUSR1, signal_handler, 0);
SETSIG(sa, SIGSTOP, stop_handler, SA_RESTART);
SETSIG(sa, SIGTSTP, stop_handler, SA_RESTART);
SETSIG(sa, SIGCONT, cont_handler, SA_RESTART);
SETSIG(sa, SIGSEGV, (void (*)(int))segv_handler, SA_RESTART);
console_init();
if (!reload) {
/* Close whatever files are open, and reset the console. */
close(0);
close(1);
close(2);
console_stty();
setsid();
putenv(PATH_DFL);
(void) close(open(UTMP_FILE, O_WRONLY|O_CREAT|O_TRUNC, 0644));
initlog(L_CO, bootmsg, "booting");//打印字符,这是我们熟悉的
if (emerg_shell) {//如果有紧急的shell需要执行,那么马上执行,这个在救援模式下可能要用
SETSIG(sa, SIGCHLD, SIG_DFL, SA_RESTART);
if (spawn(&ch_emerg, &f) > 0) {
while((rc = wait(&st)) != f)
if (rc < 0 && errno == ECHILD)
break;
}
SETSIG(sa, SIGCHLD, chld_handler, SA_RESTART);
}
runlevel = '#';
read_inittab();//开始解析inittab脚本
} else {
initlog(L_CO, bootmsg, "reloading");
sigfillset(&sgt);
sigprocmask(SIG_UNBLOCK, &sgt, NULL);
}
start_if_needed();//解析完脚本后,脚本中的程序都已经被加入一个全局链表family,说是链表,实际上就是用next和prev串起来的一串要执行的程序结构。
while(1) { //可以认为init程序执行到这里,以下的任务就是不断地执行start_if_needed和收容僵尸进程了。
boot_transitions();
INITDBG(L_VB, "init_main: waiting..");
for(ch = family; ch; ch = ch->next)
if ((ch->flags & RUNNING) && ch->action != BOOT) break;
if (ch != NULL && got_signals == 0) check_init_fifo();
fail_check();
process_signals();
start_if_needed();
}
/*NOTREACHED*/
}
重头戏开始了,注意,解析inittab的逻辑是,一行一行解析,碰到id号重复的或者不合法的就跳过,如果没有错误则加入全局family链表
void read_inittab(void)
{
FILE *fp; /* The INITTAB file */
CHILD *ch, *old, *i; /* Pointers to CHILD structure */
CHILD *head = NULL; /* Head of linked list */
sigset_t nmask, omask; /* For blocking SIGCHLD. */
char buf[256]; /* Line buffer */
char err[64]; /* Error message. */
char *id, *rlevel,
*action, *process; /* 这四个字段代表的正是inittab文件中每一行的信息 */
char *p;
int lineNo = 0; /* Line number in INITTAB file */
int actionNo; /* Decoded action field */
int f; /* Counter */
int round; /* round 0 for SIGTERM, 1 for SIGKILL */
int foundOne = 0; /* No killing no sleep */
int talk; /* Talk to the user */
int done = 0; /* Ready yet? */
if ((fp = fopen(INITTAB, "r")) == NULL)
initlog(L_VB, "No inittab file found");
while(!done) {//循环读取inittab的每一个行。
if (fp == NULL || fgets(buf, sizeof(buf), fp) == NULL) { //将一行的信息存入buf
done = 1; //如果没有到行尾就说明没有结束
for(old = newFamily; old; old = old->next)
if (strpbrk(old->rlevel, "S")) break;
if (old == NULL)
snprintf(buf, sizeof(buf), "~~:S:wait:%s/n", SULOGIN);
else
continue;
}
lineNo++; //递增行号
for(p = buf; *p == ' ' || *p == '/t'; p++);//以下的解析逻辑就不细说了,挺好理解的,就是解析字符串,我最讨厌这件事了
if (*p == '#' || *p == '/n') continue;
id = strsep(&p, ":");
rlevel = strsep(&p, ":");
action = strsep(&p, ":");
process = strsep(&p, "/n");
err[0] = 0;
if (!id || !*id) strcpy(err, "missing id field");
if (!rlevel) strcpy(err, "missing runlevel field");
if (!process) strcpy(err, "missing process field");
if (!action || !*action)
strcpy(err, "missing action field");
if (id && strlen(id) > sizeof(utproto.ut_id))
sprintf(err, "id field too long (max %d characters)",
(int)sizeof(utproto.ut_id));
if (rlevel && strlen(rlevel) > 11)
strcpy(err, "rlevel field too long (max 11 characters)");
if (process && strlen(process) > 127)
strcpy(err, "process field too long");
if (action && strlen(action) > 32)
strcpy(err, "action field too long");
if (err[0] != 0) {
initlog(L_VB, "%s[%d]: %s", INITTAB, lineNo, err);
INITDBG(L_VB, "%s:%s:%s:%s", id, rlevel, action, process);
continue;
}
actionNo = -1;
for(f = 0; actions[f].name; f++) //actions代表一个结构数组,将一个说明运行方式的字符串映射到一个数字。这个循环就是匹配运行方式字符串并将它映射成数字。
if (strcasecmp(action, actions[f].name) == 0) {
actionNo = actions[f].act;
break;
}
if (actionNo == -1) {
initlog(L_VB, "%s[%d]: %s: unknown action field",
INITTAB, lineNo, action);
continue;
}
for(old = newFamily; old; old = old->next) {//这个循环检测唯一性,如果已经有这个id了,那么就不往下继续了,直接读取下一行。
if(strcmp(old->id, id) == 0 && strcmp(id, "~~")) {
initlog(L_VB, "%s[%d]: duplicate ID field /"%s/"",
INITTAB, lineNo, id);
break;
}
}
if (old) continue;
ch = imalloc(sizeof(CHILD));//分配一个CHILD,注意这个结构很重要,前面已经说过了。
ch->action = actionNo; //从这里开始就开始初始化这个新创建的CHILD结构了,同样,细节我就不说了。
strncpy(ch->id, id, sizeof(utproto.ut_id) + 1); /* Hack for different libs. */
strncpy(ch->process, process, sizeof(ch->process) - 1);
if (rlevel[0]) {
for(f = 0; f < sizeof(rlevel) - 1 && rlevel[f]; f++) {
ch->rlevel[f] = rlevel[f];
if (ch->rlevel[f] == 's') ch->rlevel[f] = 'S';
}
strncpy(ch->rlevel, rlevel, sizeof(ch->rlevel) - 1);
} else {
strcpy(ch->rlevel, "0123456789");
if (ISPOWER(ch->action))
strcpy(ch->rlevel, "S0123456789");
}
if (ch->action == SYSINIT) strcpy(ch->rlevel, "#");
if (ch->action == BOOT || ch->action == BOOTWAIT)
strcpy(ch->rlevel, "*");
if (ISPOWER(ch->action)) { //这个if-else判断就是简单地将新创建的CHILD先链接到一个局部链表newFamily,这个newFamily链表的意义在于检测id号的唯一性
ch->flags |= XECUTED;//XECUTED标志代表已经执行过,init程序不会再重新执行它了
old = NULL;
for(i = newFamily; i; i = i->next) {
if (!ISPOWER(i->action)) break;
old = i;
}
if (old) {
ch->next = i;
old->next = ch;
if (i == NULL) head = ch;
} else {
ch->next = newFamily;
newFamily = ch;
if (ch->next == NULL) head = ch;
}
} else {
if (ch->action == KBREQUEST) ch->flags |= XECUTED;
ch->next = NULL;
if (head)
head->next = ch;
else
newFamily = ch;
head = ch;
}
for(old = family; old; old = old->next) //全局意义上的分析,看是否有人两次解析inittab之间改动了inittab文件
if (strcmp(old->id, ch->id) == 0) { //如果有人改动了,那么以最近一次解析结果为准
old->new = ch;
break;
}
}
if (fp) fclose(fp);
INITDBG(L_VB, "Checking for children to kill");
...... // 这里省略了一个逻辑,该逻辑就是检测看是否有需要结束的进程,如果有的话就进一步判断能否结束,判断通过结束之。有一点要注意的是,并不是仅仅在系统启动的时候要解析inittab,而是系统运行中的时候通过给init进程发信号也可以让init进程解析inittab脚本文件
if (foundOne) do_sleep(1);
for(ch = family; ch; ch = ch->next)
if (ch->flags & KILLME) {
if (!(ch->flags & ZOMBIE))
initlog(L_CO, "Pid %d [id %s] seems to hang", ch->pid, ch->id);
else {
INITDBG(L_VB, "Updating utmp for pid %d [id %s]",ch->pid, ch->id);
ch->flags &= ~RUNNING;
if (ch->process[0] != '+')
write_utmp_wtmp("", ch->id, ch->pid, DEAD_PROCESS, NULL);
}
}
sigemptyset(&nmask);
sigaddset(&nmask, SIGCHLD);
sigprocmask(SIG_BLOCK, &nmask, &omask);
for(ch = family; ch; ch = old) {//释放掉原来的老的链表元素
old = ch->next;
free(ch);
}
family = newFamily; //终于将局部链表链入全局链表了,链入的肯定是没有错误的链表
for(ch = family; ch; ch = ch->next) ch->new = NULL;
newFamily = NULL;
sigprocmask(SIG_SETMASK, &omask, NULL);
}
解 析完毕inittab之后就要开始执行inittab中要求执行的程序了,现在全局链表family已经设置好,接下来需要做的就是遍历这个链表然后依次 执行链表中元素指定的程序,然后根据元素的一些字段设置一些参数,而这些参数进一步反馈到主循环,指示下一部的行为,还是看代码吧:
void start_if_needed(void)
{
CHILD *ch;
int delete;
INITDBG(L_VB, "Checking for children to start");
for(ch = family; ch; ch = ch->next) { //循环遍历family链表中的一切元素。
if (ch->flags & WAITING) break; //如果有等待标志,那么说明只有这个程序执行完才可以往下进行,故直接退出循环。
if (ch->flags & RUNNING) continue; //如果已经在运行了,那么忽略它。
delete = 1;
if (strchr(ch->rlevel, runlevel) ||((ch->flags & DEMAND) && !strchr("#*Ss", runlevel))) {
startup(ch); //启动一个进程,具体的代码就不分析了,startup中要做的就是设置一些CHILD结构的标志然后fork出一个进程并且exec这个要执行的程序,以上的if (ch->flags & WAITING)判断之类的就是在startup中被设置的。
delete = 0;
}
if (delete) { //如果需要删除,则将该CHILD从全局表删除,因为留着它没有任何意义
ch->flags &= ~(RUNNING|WAITING);
if (!ISPOWER(ch->action) && ch->action != KBREQUEST)
ch->flags &= ~XECUTED;
ch->pid = 0;
} else
if (ch->flags & WAITING) break; //如果新执行的程序在startup中其flags被设置了WAITING,那么就不继续循环了,原因同上。
}
}
以 上这个函数很短,就是一些判断然后执行,但是如果在startup中设置了WAITING,那么何时清除它呢?这就是信号处理的事情了,众所周知,一旦子进程结束就要向父进程发送一个SIGCHLD信号,那么我们就看一下init进程的SIGCHLD信号处理器chld_handler:
void chld_handler()
{
CHILD *ch;
int pid, st;
int saved_errno = errno;
while((pid = waitpid(-1, &st, WNOHANG)) != 0) { //找到结束进程的pid
if (errno == ECHILD) break;
for( ch = family; ch; ch = ch->next ) //遍历family链表,找到关于这个进程的CHILD
if ( ch->pid == pid && (ch->flags & RUNNING) ) {
ADDSET(got_signals, SIGCHLD); //将信号加入全局的got_signals,这个got_signals要在process_signals用
ch->exstat = st;
ch->flags |= ZOMBIE; //设置ZOMBIE标志,这个标志在init_main主循环中的process_signals要用来清除WAITING标志。
if (ch->new) {
ch->new->exstat = st;
ch->new->flags |= ZOMBIE;
}
break;
}
if (ch == NULL)
INITDBG(L_VB, "chld_handler: unknown child %d exited.", pid);
}
errno = saved_errno;
}
那么我们就来看一下got_signals,我们只关心我们关注的信号:
void process_signals()
{
CHILD *ch;
int pwrstat;
int oldlevel;
int fd;
char c;
......
if (ISMEMBER(got_signals, SIGCHLD)) { //子进程结束信号
INITDBG(L_VB, "got SIGCHLD");
DELSET(got_signals, SIGCHLD);
for(ch = family; ch; ch = ch->next) //找到结束的子进程
if (ch->flags & ZOMBIE) { //注意,init在chld_handler中设置了ZOMBIE标志
ch->flags &= ~(RUNNING|ZOMBIE|WAITING); //清除RUNNING,ZOMBIE,WAITING
if (ch->process[0] != '+')
write_utmp_wtmp("", ch->id, ch->pid, DEAD_PROCESS, NULL);
}
}
if (ISMEMBER(got_signals, SIGHUP)) {//这个信号大家一定很熟悉,我们经常在命令行敲的kill -1 1就是这个信号,重新载入inittab
{
oldlevel = runlevel;
if (runlevel == 'U') {
runlevel = oldlevel;
re_exec();
} else {
if (oldlevel != 'S' && runlevel == 'S') console_stty();
if (runlevel == '6' || runlevel == '0' ||
runlevel == '1') console_stty();
read_inittab();
fail_cancel();
setproctitle("init [%c]", runlevel);
DELSET(got_signals, SIGHUP);
}
}
}
......
}
上面函数不短,但是做事逻辑很清晰,比如清除了WAITING后,在start_if_needed中就可以继续往下进行了。
这样,整个代码大体逻辑就分析完了,通读代码,并没有发现/etc/rc.d等信息,实际上这些都是在inittab中约定好的,你只要按照 inittab的格式写脚本就可以了,如果不按inittab规则写的话系统就可能起不来了,因为init程序定义了inittab的解析规则,这是一种 机制和策略分离的体现,机制就是inittab的规则,包括运行级别的概念,而策略完全可以在inittab中调用的脚本中体现,但是有人会问定义如此一 个inittab规则是不是机制和策略的耦合性过大了,你完全可以这么说,耦合性过大可能不好,但是读完我的下一篇文章了解到bsd的init程序后你可 能会认为耦合性过小也不是什么好事,所以耦合性多大为宜是很难界定的。