1.操作系统真相还原 2.Linux内核完全剖析:基于0.12内核 3.x86汇编语言 从实模式到保护模式 4.Linux内核设计的艺术 ps:基于x86硬件的pc系统
此时系统已经加载了/etc/rc中的命令进行了执行,我们继续往下分析。
execve("/bin/sh",argv_rc,envp_rc); // 系统调用执行命令 _exit(2);
当execve执行完成后,此时就会调用_exit(2)这个函数执行,
volatile void _exit(int exit_code) { __asm__("int $0x80"::"a" (__NR_exit),"b" (exit_code)); }
该函数直接调用了系统调用来处理,此时继续查找sys_exit函数,
int sys_exit(int error_code) { do_exit((error_code&0xff)<<8); //调用do_exit函数error_code左移八位 }
继续查看do_exit函数;
volatile void do_exit(long code) // 程序退出函数 { struct task_struct *p; int i; free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); // 释放当前进程代码段和数据段所占内存页 free_page_tables(get_base(current->ldt[2]),get_limit(0x17)); for (i=0 ; i<NR_OPEN ; i++) // 遍历当前进程打开文件的描述符数组 if (current->filp[i]) // 如果文件打开了则依次关闭打开文件 sys_close(i); iput(current->pwd); // 放回工作目录inode current->pwd = NULL; // 并置空 iput(current->root); // 放回根目录inode current->root = NULL; // 并置空 iput(current->executable); // 放回执行文件的inode current->executable = NULL; // 并置空 iput(current->library); // 放回库文件 current->library = NULL; // 并置空 current->state = TASK_ZOMBIE; // 设置当前进程为僵死状态 current->exit_code = code; // 设置进程退出状态码 /* * Check to see if any process groups have become orphaned * as a result of our exiting, and if they have any stopped * jobs, send them a SIGUP and then a SIGCONT. (POSIX 3.2.2.2) * * Case i: Our father is in a different pgrp than we are * and we were the only connection outside, so our pgrp * is about to become orphaned. */ if ((current->p_pptr->pgrp != current->pgrp) && (current->p_pptr->session == current->session) && is_orphaned_pgrp(current->pgrp) && // 如果父进程所在进程组与当前进程的不同,但都处于同一个会话 has_stopped_jobs(current->pgrp)) { // 并且当前进程所在进程组将要变成孤儿进程并当前进程的进程组中含有处于停止状态的作业进程 kill_pg(current->pgrp,SIGHUP,1); // 向当前进程组发送SIGHUP,SIGCONT信号 kill_pg(current->pgrp,SIGCONT,1); } /* Let father know we died */ current->p_pptr->signal |= (1<<(SIGCHLD-1)); // 通知父进程当前进程将终止 /* * This loop does two things: * * A. Make init inherit all the child processes * B. Check to see if any process groups have become orphaned * as a result of our exiting, and if they have any stopped * jons, send them a SIGUP and then a SIGCONT. (POSIX 3.2.2.2) */ if (p = current->p_cptr) { // 当前进程有子进程 while (1) { p->p_pptr = task[1]; // 让进程1(init)成为父进程 if (p->state == TASK_ZOMBIE) // 如果子进程已经是僵死状态 task[1]->signal |= (1<<(SIGCHLD-1)); // 则向父进程发送子进程已终止信号 /* * process group orphan check * Case ii: Our child is in a different pgrp * than we are, and it was the only connection * outside, so the child pgrp is now orphaned. */ if ((p->pgrp != current->pgrp) && (p->session == current->session) && is_orphaned_pgrp(p->pgrp) && has_stopped_jobs(p->pgrp)) { // 孤儿进程组检查,子进程在不同的进程组中,而本进程是唯一与外界的连接,子进程所在进程将变成孤儿进程组 kill_pg(p->pgrp,SIGHUP,1); kill_pg(p->pgrp,SIGCONT,1); } if (p->p_osptr) { // 如果该子进程有兄弟进程,则继续循环处理兄弟进程 p = p->p_osptr; continue; } /* * This is it; link everything into init's children * and leave */ p->p_osptr = task[1]->p_cptr; // 将p的兄弟进程加入init子进程链表中 task[1]->p_cptr->p_ysptr = p; // 设置init最年轻的子进程为p task[1]->p_cptr = current->p_cptr; current->p_cptr = 0; // 置空 break; } } if (current->leader) { // 如果当前进程是会话进程 struct task_struct **p; struct tty_struct *tty; if (current->tty >= 0) { // 若有控制终端 tty = TTY_TABLE(current->tty); // 向该控制终端的进程组发送挂断信号 if (tty->pgrp>0) kill_pg(tty->pgrp, SIGHUP, 1); tty->pgrp = 0; // 释放该会话 tty->session = 0; } for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) // 需要任务组 if ((*p)->session == current->session) // 将属于当前进程会话中进程的中断置空 (*p)->tty = -1; } if (last_task_used_math == current) // 检查上次是否使用了协处理器 last_task_used_math = NULL; // 如果使用则置空 #ifdef DEBUG_PROC_TREE // 如果调试模式则打印进程树 audit_ptree(); #endif schedule(); // 重新调度 }
该函数主要是释放当前进程代码段和数据段所占的内存页,关闭对应打开文件,放回相应当前进程的根目录、工作目录等,并设置当前进程为僵死状态、设置进程退出码,最后设置相关子进程相关操作,最后执行完成后调用调度函数重新调度。其中相关使用函数备注如下;
int is_orphaned_pgrp(int pgrp) // 判断是否是孤儿进程 { struct task_struct **p; for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) { // 扫描任务数组 if (!(*p) || // 如果任务为空 ((*p)->pgrp != pgrp) || // 进程的组号与指定的不同 ((*p)->state == TASK_ZOMBIE) || // 进程处于僵死状态 ((*p)->p_pptr->pid == 1)) // 或者进程的父进程是init continue; // 继续循环 if (((*p)->p_pptr->pgrp != pgrp) && // 如果该父进程的父进程的组号不等于指定的组号 ((*p)->p_pptr->session == (*p)->session)) // 并且父进程的会话号等于进程的会话号 return 0; // 则返回不是 } return(1); /* (sighing) "Often!" */ // 返回是 } static int has_stopped_jobs(int pgrp) { struct task_struct ** p; for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) { // 循环查找查找当前任务是否包含已经处于停止的任务 if ((*p)->pgrp != pgrp) continue; if ((*p)->state == TASK_STOPPED) return(1); } return(0); }
其中有关kill_pg函数的分析留待后文分析。
至此,由init生成的执行/etc/rc进程执行完成后就调用_exit函数退出。此时,查看init进程的执行。
此时父进程一直在执行;
if (pid>0) while (pid != wait(&i)) // 父进程等待子进程执行完成 /* nothing */;
此时我们查看wait函数,
pid_t wait(int * wait_stat) { return waitpid(-1,wait_stat,0); // 系统调用waitpid函数 }
调用waitpid就会进行系统调用,sys_waitpid函数
int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options) // 挂起当前进程,直到pid指定的子进程退出 { int flag; struct task_struct *p; unsigned long oldblocked; verify_area(stat_addr,4); // 验证将要存放状态信息的位置处内存空间是否足够 repeat: flag=0; // 复位标志 for (p = current->p_cptr ; p ; p = p->p_osptr) { // 当前进程中最年轻子进程开始扫描子进程兄弟链表 if (pid>0) { // 如果pid大于0 if (p->pid != pid) // 如果当前进程Pid不等于传入pid则继续循环 continue; } else if (!pid) { // 如果传入pid为0表示正在等待进程组号等于当前进程组号的任何子进程 if (p->pgrp != current->pgrp) // 如果此时找到的p的进程组号与当前进程的组号不等则跳过 continue; } else if (pid != -1) { // 如果传入不等于-1,表示正在等待进程组号等于pid绝对值的任何子进程 if (p->pgrp != -pid) // 如果此时找到的进程p的组号不等于pid的绝对值则跳过 continue; } switch (p->state) { // 此时传入pid值为-1,此时找到进程的状态 case TASK_STOPPED: // 如果是停止状态 if (!(options & WUNTRACED) || !p->exit_code) // 如果程序无需立即返回或者找到进程没有退出吗 continue; // 继续查找 put_fs_long((p->exit_code << 8) | 0x7f, stat_addr); // 把退出码移入高字节 p->exit_code = 0; // 重置当前进程的退出码 return p->pid; // 返回当前进程的pid case TASK_ZOMBIE: // 如果当前进程是僵死状态 current->cutime += p->utime; // 将子进程的用户态和内核态运行时间累加到父进程中 current->cstime += p->stime; flag = p->pid; // 将找到进程的pid赋值给标志位flag put_fs_long(p->exit_code, stat_addr); release(p); // 释放该进程 #ifdef DEBUG_PROC_TREE // 如果定义了调试 audit_ptree(); // 显示进程树 #endif return flag; // 返回pid default: flag=1; // 默认flag为1继续循环 continue; } } if (flag) { // 如果flag为1则表示有符合等待要求的子进程并没有处于退出立刻或僵死状态 if (options & WNOHANG) // 如果已设置则立刻返回 return 0; current->state=TASK_INTERRUPTIBLE; // 设置当前进程为可中断等待状态 oldblocked = current->blocked; // 保留当前进程信号阻塞位图 current->blocked &= ~(1<<(SIGCHLD-1)); // 重置进程信号位图 schedule(); // 重新调度 current->blocked = oldblocked; // 重置阻塞位图 if (current->signal & ~(current->blocked | (1<<(SIGCHLD-1)))) // 如果本进程收到处SIGCHLD以外的其它未屏蔽信号则返回退出码 return -ERESTARTSYS; else // 否则继续repeat goto repeat; } return -ECHILD; // 若flag为0表示没有找到符合要求的子进程,返回出错码 }
该函数有三个传入参数,如果传入pid>0,表示等待进程号为pid的子进程,如果传入pid=0,表示等待进程组号等于当前进程组号的任何子进程,当pid<-1,表示等待进程组号等于pid绝对值的任何子进程,如果pid=-1,表示等待任何子进程,如果传入options=WUNTRACED,表示如果子进程是停止的,马上返回,如果options=WNOHANG,如果没有子进程退出或终止就马上返回。主要实现了挂起当前进程,直到pid指定的子进程退出或者收到要求终止该进程的信号,或者是需要调用一个信号句柄,如果pid所指的子进程已退出则立刻返回,子进程使用的资源将释放。
此时当使用waitpid(-1,wait_stat,0)函数时,则需要等待子进程执行完成则返回,此处又使用了循环while (pid != wait(&i)),所以此处会一直等待子进程执行完成后,才会退出循环。当子进程执行完成后退出循环后,此时就需要加载shell了
if (pid>0) while (pid != wait(&i)) // 父进程等待子进程执行完成 /* nothing */; while (1) { if ((pid=fork())<0) { printf("Fork failed in init\r\n"); continue; // 如果fork出错则继续fork } if (!pid) { // 新的子进程 close(0);close(1);close(2); setsid(); // 创建一组会话 (void) open("/dev/tty1",O_RDWR,0); // 以读写的方式打开终端 (void) dup(0); (void) dup(0); _exit(execve("/bin/sh",argv,envp)); // 执行shell程序 } while (1) if (pid == wait(&i)) // 如果子进程退出则继续循环 break; // 停止循环 printf("\n\rchild %d died with code %04x\n\r",pid,i); sync(); // 同步操作,刷新缓冲区 }
此时就会进入该循环,然后调用fork函数,让子进程执行加载shell终端的任务。
其中close函数系统调用的是sys_close,setsid()对应sys_setsid的系统调用。
int sys_close(unsigned int fd) { struct file * filp; if (fd >= NR_OPEN) // 如果大于设置进程打开文件数的个数则返回出错码 return -EINVAL; current->close_on_exec &= ~(1<<fd); // 复位进程的执行时关闭文件句柄位图 if (!(filp = current->filp[fd])) // 如果当前描述符对应的文件结构为空则返回出错码 return -EINVAL; current->filp[fd] = NULL; // 设置当前文件结构为空 if (filp->f_count == 0) // 如果文件引用计数已经为0则出错 panic("Close: file count is 0"); if (--filp->f_count) // 否则对应文件结构的引用计数减1,如果此时引用计数为0 return (0); iput(filp->f_inode); // 此时放入该节点 return (0); } ... int sys_setsid(void) { if (current->leader && !suser()) // 如果当前进程已是会话首领并不是超级用户则返回错误码 return -EPERM; current->leader = 1; // 设置当前进程为新会话首领 current->session = current->pgrp = current->pid; // 设置当前进程会话号和组号都为进程号 current->tty = -1; // 设置当前进程没有控制终端 return current->pgrp; // 返回进程pid号 }
至此,Linux0.12的初始化过程已经完成。