广西建设厅网站在线服务,wordpress建站优缺点,东莞网络营销网络推广软件,哪有网站建设的[272页]
第7章 初始化程序
1、main.c主要内核初始化工作。 2、如果能完全理解这里调用的所有程序#xff0c;那么看完这张内容后应该对Linux内核有了大致的了解。 3、 有一定的C语言知识 4、 需要GNU gcc手册在身边作为参考#xff0c;因为在内核代码很多地方使用gcc的扩展…[272页]
第7章 初始化程序
1、main.c主要内核初始化工作。 2、如果能完全理解这里调用的所有程序那么看完这张内容后应该对Linux内核有了大致的了解。 3、 有一定的C语言知识 4、 需要GNU gcc手册在身边作为参考因为在内核代码很多地方使用gcc的扩展特性。 例如内联(inline)函数、内联(内嵌)汇编语句等。
7-1 main.c程序
7-1-1 功能描述
1、
(a)main.c程序首先利用前面setup.s程序取得的系统参数设置、系统的根文件设备
以及一些内存全局变量。这些内存变量指明了主内存的开始地址、系统所拥有的内存容量 和作为高速缓冲区内存的末端地址。
如果还定义了虚拟盘则主内存将适当减少。
整个内存的映像示意图如图7-1所示 内核程序高速缓冲虚拟盘主内存区。
(b)高速缓冲部分还要扣除被显示和ROM BIOS占用的部分。
高速缓冲区是用于磁盘等块设备临时存放数据的地方 以1K1024字节为一个数据块单位。
©主内存区域的内存由内存管理模块mm通过分页机制进行管理分配
以4K字节为一个内存页面单位。
(d)内核程序可以自由访问高速缓冲中的数据但需要通过mm才能使用分配到内存页面。
2、main.c进行所有方面的硬件初始化工作
包括陷阱门、块设备、字符设备和tty还包括人工设置第一个任务task 0。 待所有初始化工作完成后程序就设置中断允许标志以开启中断 并切换到任务0中运行。 作者建议深入的看遇到看不懂暂时先放一放。
3、整个内核完成初始化后内核将执行权切换到用户模式(任务0)即CPU从0特权级
切换到了第3特权级。此时main.c的主程序就工作在任务0中。然后系统第一次调用进程创建 函数fork(),创建出一个用于运行init()的子进程通常被称为init进程。
4、 系统整个初始化过程如图7-2所示
看作者 (a)main.c程序首先确定如何分配使用系统物理内存 (b)调用内核各部分的初始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘等硬件进行初始化处理。 ©程序把自己手工移动到任务0(进程0)中运行并使用fork()调用首次创建出进程1(init进程) (d)init()函数将继续进行应用环境的初始化并执行shell登陆程序。 (e)而原进程0则会在系统空闲时被调度执行因此进程0通常也被称为idle进程。
5、 init()函数可分为4个部分安装根文件系统、显示系统信息、
运行系统初始资源配置文件rc中的命令、执行用户登陆shell程序。
(a)代码首先调用系统调用setup((void *) drive_info);用来收集硬盘设备分区表信息并安装根文件系统。
在安装根文件系统之前系统会先判断是否需要建立虚拟盘。若编译内核时设置了虚拟盘的大小 并在前面内核初始化过程中已经开辟了一块内存用在虚拟盘则内核就会首先尝试把根文件系统加载到内存的虚拟盘区中。
(b)打开一个终端设备tty0并复制其文件描述符以产生标准输入stdin、标准输出stdout和错误输出stderr设备。
内核随后利用这些描述符在终端上显示一些系统信息例如高速缓冲区中缓冲块总数、注内存区空闲内存总字节等。
©新建一个进程2并在其中为建立用户交互使用环境而执行一些初始配置操作即在用户可以使用shell命令行环境之前
内核调用/bin/sh程序运行了配置文件etc/rc中设置的命令。rc文件的作业与DOS系统根目录中的AUTOEXEC.BAT文件类似。 这段代码首先通过关闭文件描述符0并立刻打开文件/etc/rc从而把标志输入stdin定向到/etc/rc文件上。 这样所有的标准输入数据都将从该文件中读取。然后内核以非交互形式执行/bin/sh从而实现执行/etc/rc文件中的命令。 当该文件中的命令执行完毕后/bin/sh就会立刻退出。因此进程2也就随之结束。
(d)init()函数的最后一部分等待进程2退出创建新的进程执行参数_exit(execve(“/bin/sh”,argv,envp));等待新进程退出如此循环。
带有’-标志会在/bin/sh执行时通知它这不是一次普通的运行而是作为登陆shell运行/bin/sh的。
6、fork()是内联函数因为创建进程1之前要求进程0的用户堆栈干净所以fork()不能以函数形式进行调用。
作者展开代码且解释。 static inline int fork(void) { long __res; asm volatile(“int $0x80”:“a”(__res):“0”(__NR_fork)); if (__res0) return (int)__res; errno -__res; return -1; } 进程init和进程0实际上同时使用着内核代码区内(小于1MB的物理内存)相同的代码和数据物理内存页面640KB 只是执行的代码不在一处。当进程init操作用户堆栈时内核才会分配内存页给进程init。
当进程init或进程2执行过execve()调用后进程2的代码和数据区位于系统的主内存。
7-1-2 代码注释
佩服赵老师保姆式注释如果学不会只能怪您自己了。-
/** linux/init/main.c** (C) 1991 Linus Torvalds*/
//是为了包括定义在unistd.h中的内嵌汇编代码等信息。
#define __LIBRARY__
//*.h头文件所在的默认目录是include/则在代码中就不必明确指明其位置。如果不是UNIX的
//标准头文件则需要指明所在的目录并用双引号括住。unistd.h是标准符号常数与类型文件。
//其中定义了各种符号常数和类型并声明了各种函数。如果还定义了符号__LIBRARY__,则还会
//包含系统调用号和内嵌汇编代码syscall0()等。
#include unistd.h
#include time.h//时间类型头文件。其中主要定义了tm结构和一些有关时间的函数原形。/*
我们需要下面这些内嵌语句-从内核空间创建进程将导致没有写时复制COPY ON WRITE!!!
知道执行一个execve调用。这对堆栈可能带来问题。处理方法是在fork()调用后不让main()
使用任何堆栈。因此就不能由函数调用-这意味着fork也要使用内嵌的代码否则我们在从
fork()退出时就要使用堆栈了。实际上只要pause和fork需要使用内嵌方式以保证从main()中不会弄乱堆栈但是我们同时还
定义了其他一些函数。
*/
//fork()展开后
static inline _syscall0(int,fork)
//pause()系统调用暂停进程的执行直到收到一个信号。
static inline _syscall0(int,pause)
//setup(void *BIOS)系统调用仅用于linux初始化仅在这个程序中被调用。
static inline _syscall1(int,setup,void *,BIOS)
//sync()系统调用更新文件系统。
static inline _syscall0(int,sync)#include linux/tty.h
#include linux/sched.h
#include linux/head.h
#include asm/system.h
#include asm/io.h#include stddef.h
#include stdarg.h
#include unistd.h
#include fcntl.h
#include sys/types.h#include linux/fs.h#include string.hstatic char printbuf[1024];//静态字符串数组用作内核显示信息的缓存。extern char *strcpy();
extern int vsprintf();//送格式化输出到一字符串中
extern void init(void);//函数原形初始化
extern void blk_dev_init(void);//块设备初始化子程序
extern void chr_dev_init(void);//字符设备初始化
extern void hd_init(void);//硬盘初始化程序
extern void floppy_init(void);//软驱初始化程序
extern void mem_init(long start, long end);//内存管理初始化
extern long rd_init(long mem_start, int length);//虚拟盘初始化
extern long kernel_mktime(struct tm * tm);//计算系统开机启动时间秒。
//内核专用sprintf()函数。该函数用于产生格式化信息并输出到指定缓冲区str中。参数*fmt
//指定输出将采用的格式参见标志C语言书籍。该子程序正好是vsprintf如何使用的一个简单
//例子。函数使用vsprintf()将格式化字符串放入str缓冲区参见第179行上的printf()函数。
static int sprintf(char * str, const char *fmt, ...)
{va_list args;int i;va_start(args, fmt);i vsprintf(str, fmt, args);va_end(args);return i;
}/** 以下这些数据是在内核引导期间由setup.s程序设置的。*///下面三行分别将指定的线性地址强行转换为给定数据类型的指针并获取指针所指内容。由于内核代码//被映射到从物理地址零开始的地方因此这些线性地址正好也是对应的物理地址。//这些指定地址处内存值的含义请参见表6-3(setup程序读取并保存的参数)。//drive_info结构请参见下面第125行。
#define EXT_MEM_K (*(unsigned short *)0x90002)//1MB以后的扩展内存大小KB
#define CON_ROWS ((*(unsigned short *)0x9000e) 0xff)//选的的控制台屏幕行、列数。
#define CON_COLS (((*(unsigned short *)0x9000e) 0xff00) 8)//
#define DRIVE_INFO (*(struct drive_info *)0x90080)//硬盘参数表32字节内容。
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)//根文件系统所在设备号。
#define ORIG_SWAP_DEV (*(unsigned short *)0x901FA)//交换文件所在设备号。/*
是啊是啊下面这段程序很差劲但我不知道如何正确地实现而且好像
它还能运行。如果有关于实时时钟更多资料那我很感兴趣。这些都是试探
出来的另外还看了一些bios程序,呵*/
//这段宏读取CMOS实时时钟信息。outb_p和inb_p是include/asm/io.h中定义的端口输入输出宏。
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \ //0x70是写地址端口号。
inb_p(0x71); \ //0x71是读数据端口号。
})
//定义宏。将BCD码转换二进制数据值。
#define BCD_TO_BIN(val) ((val)((val)15) ((val)4)*10)//该函数取CMOS时钟信息作为开机时间保存到全局变量startup_time(秒)中。参见后面
//CMOS内存列表说明。其中调用的函数kernel_mktime()用于计算从1970年1月1日0时起到
//开机当日经过的秒数作为开机时间。
static void time_init(void)
{struct tm time;do {time.tm_sec CMOS_READ(0);time.tm_min CMOS_READ(2);time.tm_hour CMOS_READ(4);time.tm_mday CMOS_READ(7);time.tm_mon CMOS_READ(8);time.tm_year CMOS_READ(9);} while (time.tm_sec ! CMOS_READ(0));BCD_TO_BIN(time.tm_sec);BCD_TO_BIN(time.tm_min);BCD_TO_BIN(time.tm_hour);BCD_TO_BIN(time.tm_mday);BCD_TO_BIN(time.tm_mon);BCD_TO_BIN(time.tm_year);time.tm_mon--;startup_time kernel_mktime(time);
}
//下面定义一些局部变量。
static long memory_end 0; //机器具有的物理内存容量字节数。
static long buffer_memory_end 0; //告诉缓冲区末端地址。
static long main_memory_start 0; //主内存将用于分页开始的位置。
static char term[32]; //终端设置字符串环境参数。//读取并执行/etc/rc文件时所使用的命令行参数和环境参数。
static char * argv_rc[] { /bin/sh, NULL };
static char * envp_rc[] { HOME/, NULL ,NULL };
//运行登陆shell时所使用的命令行参数和环境参数。
//-是传递给shell程序sh的一个标志。通过识别该标志sh程序
//会作为登陆shell执行。其执行过程与在shell提示符下执行sh不一样。
static char * argv[] { -/bin/sh,NULL };
static char * envp[] { HOME/usr/root, NULL, NULL };struct drive_info { char dummy[32]; } drive_info;//用于存放硬盘参数表信息。//内核初始化主程序。初始化结束后将以任务0(idle任务即空闲任务)的身份运行。
void main(void) /* 这里确实是void,没错。 */
{ /* 在startup程序(head.s)中就是这样假设的 */
/*
此时中断仍被禁止着做完必要的设置后就将其开启。*/
//首先保存根文件系统设备号和交换文件设备号并根据setup.s程序中获取的信息设置控制台终端
//屏幕行、列数环境变量TERM并用其设置初始init进程中执行etc/rc文件和shell程序使用的
//环境变量以及复制内存0x90080处的硬盘参数表。
//其中ROOT_DEV已在前面包含进的include/linux/fs.h文件第206行上被声明为extern int,
//而SWAP_DEV在include/linux/mm.h文件内也作了相同声明。这里mm.h文件并没有显示地列在
//本程序前面因为前面包含进的include/linux/sched.h文件中已经包含有它。 ROOT_DEV ORIG_ROOT_DEV;//声明在fs/super.c 定义在bootsetup.sSWAP_DEV ORIG_SWAP_DEV;//声明在mm/swap.c 定义在bootsetup.ssprintf(term, TERMcon%dx%d, CON_COLS, CON_ROWS);envp[1] term; envp_rc[1] term;drive_info DRIVE_INFO;//复制内存0x90080处的硬盘参数表。
//接着根据机器物理内存容量设置高速缓冲区和主内存区的位置和范围。
//高速缓存末端地址-buffer_memory_end;机器内存容量-memory_end;
//主内存开始地址-main_memory_start;memory_end (120) (EXT_MEM_K10);//内存大小1MB扩展内存(k)*1024字节。memory_end 0xfffff000;//忽略不到4KB1页的内存数。if (memory_end 16*1024*1024)//如果内存量超过16MB则按16MB计。memory_end 16*1024*1024;if (memory_end 12*1024*1024) //如果内存12MB,则设置缓冲区末端4MBbuffer_memory_end 4*1024*1024;else if (memory_end 6*1024*1024)//如果内存6MB,则设置缓冲区末端2MBbuffer_memory_end 2*1024*1024;else//否则设置缓冲区末端1MBbuffer_memory_end 1*1024*1024;main_memory_start buffer_memory_end;//主内存起始位置缓冲区末端。//如果在Makefile文件中定义了内存虚拟盘符号RAMDISK则初始化虚拟盘。此时主内存将减少。
//参见kernel/blk_drv_ramdisk.c。
#ifdef RAMDISKmain_memory_start rd_init(main_memory_start, RAMDISK*1024);
#endif
//以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看
//若实在看不下去了就先放一放继续看下一个初始化调用。--作者保姆式的注释和关系。mem_init(main_memory_start,memory_end);//主内存区初始化。trap_init();//陷阱门硬件中断量初始化。blk_dev_init();//块设备初始化。chr_dev_init();//字符设备初始化。tty_init();//tty初始化。time_init();//设置开机启动时间。sched_init();//调度程序初始化buffer_init(buffer_memory_end);//缓冲管理初始化建内存链表等。hd_init();//硬盘初始化。floppy_init();//软驱动初始化sti();//所有初始化工作都完了于是开启中断。
//下面过程通过在堆栈中设置的参数利用中断返回指令启动任务0执行。 move_to_user_mode();//移到用户模式下执行。if (!fork()) { /* 在新建的子进程任务1即init进程中执行。 */init();}
//下面代码开始以任务0的身份运行。
/*
注意对于任何其他的任务pause()将意味着我们必须等待收到一个信号才会返回就绪态但
任务0(task0)是唯一例外情况因为任务0在任何空闲时间里都会被激活
因此对于任务0pause()仅意味着我们返回来查看是否有其他任务可以运行如果没有的话我们就回到合理
一直循环执行pause()*/for(;;)__asm__(int $0x80::a (__NR_pause):ax);
}//下面函数产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数*fmt
//指定输出将采用的格式参见标准C语言书籍。该子程序正好是vsprintf如何使用的一个简单
//例子。该程序使用vsprintf()将格式化的字符串放入printbuf缓冲区然后用write()将缓冲
//去的内容输出到标准设备(1--stdout)。vsprintf()函数的实现建kernel/vsprintf.c
static int printf(const char *fmt, ...)
{va_list args;int i;va_start(args, fmt);write(1,printbuf,ivsprintf(printbuf, fmt, args));va_end(args);return i;
}
//在main()中已经进行了系统初始化包括内存管理、各种硬件设备和驱动程序。init()函数
//运行在任务0第1次创建的子进程(任务1)中。它首先对第一个将要执行的程序(shell)
//的环境进行初始化然后以登陆shell方式加载该程序并执行之。
void init(void)
{int pid,i;
//setup()是一个系统调用。用于读取硬盘参数包括分区表信息并加载虚拟盘(若存在的话)和
//安装根文件系统设备。该函数用25行上的宏定义对应函数时sys_setup(),在块设备子目录
//kernel/blk_drv/hd.c,74行。setup((void *) drive_info);
//下面以读写访问方式打开设备/dev/tty0它对应终端控制台。由于这是第一次打开文件
//操作因此产生的文件句柄号文件描述符肯定是0.该句柄是UNIX类操作系统默认的控制
//台标志输入句柄stdin。这里再把它以读和写的方式分别打开是为了复制产生标准输出写
//句柄stdou和标志出差输出句柄stderr。函数前面的(void)前缀用于表示强制函数无需返回值。(void) open(/dev/tty1,O_RDWR,0);(void) dup(0);//复制句柄产生句柄1号--stdout标准输出设备。(void) dup(0);//复制句柄产生句柄2号--stderr标志出错输出设备。//下面打印缓冲区块数和总字节数每块1024字节以及主内存区空闲内存字节数。 printf(%d buffers %d bytes buffer space\n\r,NR_BUFFERS,NR_BUFFERS*BLOCK_SIZE);printf(Free mem: %d bytes\n\r,memory_end-main_memory_start);//函数_exit()退出时的出错码 1-操作未允许2--文件或目录不存在。if (!(pidfork())) {//if语句里面是task2//关闭句柄0(stdin)并立刻打开/etc/rc文件的作业是把标志输入stdin重定向到/etc/rc文件。//这样shell程序/bin/sh就可以运行rc文件中的设置的命令。close(0);if (open(/etc/rc,O_RDONLY,0))_exit(1);//若打开文件失败则退出。execve(/bin/sh,argv_rc,envp_rc);//替换成/bin/sh程序并执行。_exit(2);//若execve()执行失败则退出。}//下面还是父进程(1)执行语句。wait()等待子进程停止或终止返回值应是子进程的进程号//(pid)。这三局的作用是父进程等待子进程的结束。i是存放返回状态信息的位置。//如果wait()返回值不等于子进程号则继续等待。if (pid0)while (pid ! wait(i))/* nothing */;//如果执行到这里说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子进程
//如果出错则显示初始化程序创建子进程失败信息并继续执行。对于所创建的子进程将关闭所有
//以前还遗留的句柄(stdin,stdou,stderr),新创建一个会后并设置进程组号
//然后重新打开/dev/tty0作为stdin并复制成stdou和stderr。再次执行系统解释程序/bin/sh。但这
//次执行所选用的参数和环境数组另选了一套。然后父进程再次运行wait()等待。
//如果子进程又停止了执行则在标准输出上显示出错信息子进程pid停止了运行返回码时i,
//然后继续重试下……,形成大死循环。while (1) {if ((pidfork())0) {printf(Fork failed in init\r\n);continue;}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));}while (1)if (pid wait(i))break;printf(\n\rchild %d died with code %04x\n\r,pid,i);sync();//同步操作刷新缓冲区。}_exit(0); /* 注意 是_exit(),非exit() *///_exit()和exit()都用于正常终止一个函数。但_exit()直接是一个sys_exit系统调用而//exit()则通常是普通函数库中的一个函数。它会先执行一些清除操作例如调用执行各终止处理程序、//关闭所有标准IO等然后调用sys_exit。
}7-1-3 其他信息
1、 CMOS信息
(a)0x70是地址端口0x71是数据端口。 (b)表7-1 CMOS 64字节信息简表
2、 调用fork()创建新进程
(a)fork是创建新进程但需要用exec()簇函数取执行其他不同的程序。 (b)子进程pid0,父进程pid子进程的pid号。 ©当程序执行完或有必要终止时就可以调用exit()来退出 而父进程则可以使用wait()调用来查看或等待子进程的退出并获取被终止进程的退出状态信息。
3、 关于会话期的概念
_不理解进程组和会话期。