标签:__ 初始化 主程序 void memory init 源码 time linux
来自:https://in1t.top/2020/03/26/linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-%E5%88%9D%E5%A7%8B%E5%8C%96%E4%B8%BB%E7%A8%8B%E5%BA%8F/
main.c 功能描述
之前 setup 在 0x90000 ~ 0x901FF 保存了一些重要的机器参数,其中包括主内存区的开始地址,内存大小和作为高速缓冲区内存的末端地址,如果 Makefile 中还定义了 RAMDISK 虚拟盘,则主内存区还会减小。
高速缓冲是用于供磁盘等块设备临时存放数据的地方,以 1KB 为一个数据块单位,其中包含了显存及其 BIOS 占用的区域。现在 main.c 将会用这些参数来划分内存区域
之后调用一堆初始化函数对内存、陷阱门、块设备、字符设备、tty、时间、进程调度、缓冲区、硬盘、软驱进行初始化,并完成进程 0 的创建,从内核态切换为用户态。
此时第一次调用 fork 函数创建用于运行 init 函数的子进程 1,init 函数主要作用为
- 安装根文件系统
- 显示系统信息
- 执行资源配置文件
- 执行登录 shell 程序
流程图如下:
代码分析
*.h 头文件没有指明路径默认在 include 目录下
C
1 2 3 4
|
// Line 7 #define __LIBRARY__ // 定义了该符号表示会包含系统调用号及一些宏定义如 syscall0 等 #include <unistd.h> // 定义了各种符号常数与类型,声明了函数 #include <time.h> // 时间类型头文件
|
__always_inline 与 syscall 等在 include/unistd.h 中都有定义,_syscall 后面的数字表示定义的函数有几个参数,括号中的前两个参数为函数返回值类型及函数名,之后的每两个参数代表该函数参数类型及参数名
C
1 2 3 4 5
|
// Line 24 __always_inline _syscall0(int,fork) __always_inline _syscall0(int,pause) __always_inline _syscall1(int,setup,void *,BIOS) __always_inline _syscall0(int,sync)
|
比如 _syscall1(int,setup,void *,BIOS) 表示定义了 int setup(void *BIOS) 这么一个函数,具体代码:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
// include/unistd.h Line 267 #define __always_inline inline __attribute__((always_inline)) // 该函数需要内联处理
// Line 60 取部分用到的常数 #define __NR_fork 2 #define __NR_pause 29 #define __NR_setup 0 #define __NR_sync 36
// Line 147 #define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ // 将结果从 eax 寄存器赋值给变量 __res : "0" (__NR_##name)); \ // '0' 表示使用与第 0 条操作表达式相同的寄存器,即 eax = __NR_XXX if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ }
#define _syscall1(type,name,atype,a) \ type name(atype a) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(a))); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ }
|
对于 gcc AT&T 内嵌汇编不太熟悉的话,可以参考这篇 csdn 博文。以 _syscall0(int,fork) 为例,_syscall0(int,fork) 后产生了这么一个函数:
C
1 2 3 4 5 6 7 8
|
int fork(){ long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_fork)); if (__res >= 0) return (int) __res; errno = -__res; return -1; }
|
然后包含一堆头文件,引用一些初始化函数,定义一些常量
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
// Line 29 #include <linux/tty.h> // 定义有关 tty_ip,串行通信方面的常数等 #include <linux/sched.h> // 进程调度程序头文件 #include <linux/head.h> // 定义了段描述符的结构等 #include <asm/system.h> // 以宏的形式定义了许多有关设置或修改描述符/中断门等的嵌入式汇编子程序 #include <asm/io.h> // 定义了对 IO 端口的操作函数
#include <stddef.h> // 标准定义头文件 #include <stdarg.h> // 标准参数头文件 #include <fcntl.h> // 文件控制头文件 #include <sys/types.h> // 定义了基本的系统数据类型
#include <linux/fs.h> // 文件系统头文件
static char printbuf[1024]; // 内核显示信息的缓存
extern int vsprintf(); // vsprintf.c extern void init(void); // 就在本程序中 extern void blk_dev_init(void); // 块设备初始化 blk_drv/ll_rw_blk.c extern void chr_dev_init(void); // 字符设备初始化 chr_drv/tty_io.c extern void hd_init(void); // 硬盘初始化 blk_drv/hd.c extern void floppy_init(void); // 软驱初始化 blk_drv/floppy.c extern void mem_init(long start, long end); // 内存管理初始化 mm/memory.c extern long rd_init(long mem_start, int length); // 虚拟盘初始化 blk_drv/ramdisk extern long kernel_mktime(struct tm * tm); // 系统开机时间 kernel/mktime.c extern long startup_time;
#define EXT_MEM_K (*(unsigned short *)0x90002) // 1MB 后扩展内存的大小(KB) #define DRIVE_INFO (*(struct drive_info *)0x90080) // 硬盘参数表 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) // 根文件系统所在设备号
|
接下来定义读取 CMOS 时钟信息的宏及 time_init 函数,从 CMOS 中读出的信息都是 BCD 码的形式,用 4 bits(半个字节)表示一个 10 进制数。于是定义一个 BCD_TO_BIN 的宏,val&15 取 10 进制个位,val>>4 取 10 进制十位,乘 10 与个位相加得到实际的十进制对应的二进制数值
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
// Line 69 #define CMOS_READ(addr) ({ \ outb_p(0x80|addr,0x70); \ // 向 0x70 端口输出要读取 CMOS 的内存位置 inb_p(0x71); \ // 从 0x71 读取一个字节 })
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10) // BCD 码转二进制数值
static void time_init(void) { struct tm time; // tm 结构定义在 time.h 中
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)); // do-while 循环读取时钟信息,将误差锁定在 1s 内 BCD_TO_BIN(time.tm_sec); // 读出来都是 BCD 码,转成二进制数值 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--; // 让月份范围从 1 ~ 12 减为 0 ~ 11 startup_time = kernel_mktime(&time); // 计算开机时间 }
|
outb_p 与 inb_p 定义在 include/asm/io.h 中,分别表示向 io 端口输出信息及从 io 端口读取信息:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
// include/asm/io.h Line 11 #define outb_p(value,port) \ __asm__ ("outb %%al,%%dx\n" \ "\tjmp 1f\n" \ // jmp 1f 相当于 nop "1:\tjmp 1f\n" \ "1:"::"a" (value),"d" (port))
#define inb_p(port) ({ \ unsigned char _v; \ __asm__ volatile ("inb %%dx,%%al\n" \ "\tjmp 1f\n" \ "1:\tjmp 1f\n" \ "1:":"=a" (_v):"d" (port)); \ _v; \ })
|
main 之前最后还定义了一些用于内存划分的变量
C
1 2 3 4 5 6
|
// Line 98 static long memory_end = 0; // 机器的物理内存容量 static long buffer_memory_end = 0; // 高速缓冲区末端地址 static long main_memory_start = 0; // 主内存开始位置
struct drive_info { char dummy[32]; } drive_info; // 存放硬盘参数表信息
|
main 函数:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
// Line 104 void main(void) { ROOT_DEV = ORIG_ROOT_DEV; // 存储根设备号 drive_info = DRIVE_INFO; // 存储硬盘参数表 memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小 = 1MB + 扩展内存大小 * 1024 memory_end &= 0xfffff000; // 4K 对齐 if (memory_end > 16*1024*1024) // 内存大小超过 16 MB,限制为 16 MB memory_end = 16*1024*1024; if (memory_end > 12*1024*1024) // 内存大小在 12 ~ 16 MB 之间,缓冲区末端 = 4 MB buffer_memory_end = 4*1024*1024; else if (memory_end > 6*1024*1024) // 内存大小在 6 ~ 12 之间,缓冲区末端 = 2 MB buffer_memory_end = 2*1024*1024; else buffer_memory_end = 1*1024*1024;// 小于等于 6 MB,缓冲区末端 = 1 MB main_memory_start = buffer_memory_end; // 主内存区域起始位置设置为缓冲区结束位置 #ifdef RAMDISK // 如果定义了 RAMDISK,则初始化虚拟盘 main_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(); // 初始化完成,开启之前被屏蔽的中断 move_to_user_mode(); // 转移到用户态 if (!fork()) { // 先生成一个子进程 1,继续往下执行 init(); // 在进程 1 中继续初始化 } for(;;) pause(); // pause 调用进程调度函数,切换回进程 1 执行 }
|
之前有说过 pause 的进程需等待一个信号才能被激活,进程 0 是个例外,当没有其他进程在运行时,会激活进程 0
接下来声明了一个 printf 函数,为 init 中输出信息做准备,并设置了一些配置文件
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
// Line 152 static int printf(const char *fmt, ...) // 在屏幕上打印字符 { va_list args; int i;
va_start(args, fmt); write(1,printbuf,i=vsprintf(printbuf, fmt, args)); va_end(args); return i; } // 读取执行 /etc/rc 文件使用的命令行参数及环境参数 static char * argv_rc[] = { "/bin/sh", NULL }; // 命令行参数数组 static char * envp_rc[] = { "HOME=/", NULL, NULL }; // 环境数组 // 运行登录 shell 使用的命令行参数及环境参数 static char * argv[] = { "-/bin/sh",NULL }; // 命令行参数数组 static char * envp[] = { "HOME=/usr/root", NULL, NULL }; // 环境数组
|
进程 1 执行的 init 函数,该函数首先对第一个将要执行的 shell 程序的环境进行初始化,然后以登录 shell 的方式加载并执行。如果运行登录 shell 的进程结束了,进程 1 又会重新新建一个进程,继续运行登录 shell。这里的登录 shell 指的就是开机完成用户可以键入用户名密码登录系统的 shell
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
// Line 169 void init(void) { int pid,i;
setup((void *) &drive_info); // 读取硬盘参数,安装根文件系统设备,加载虚拟盘 (void) open("/dev/tty0",O_RDWR,0); // 以读写访问方式打开设备 /dev/tty0,句柄为 0(stdin) (void) dup(0); // 复制句柄 0,产生 1 号句柄,用于标准输出(stdout) (void) dup(0); // 复制句柄 0,产生 2 号句柄,用于标准出错输出(stderr) 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); // 打印主内存空闲内存字节数 if (!(pid=fork())) { // 新建进程 2(作为进程 1 的子进程) close(0); // 关闭标准输入流 if (open("/etc/rc",O_RDONLY,0)) //立即打开 /etc/rc 将 stdin 重定向到 /etc/rc 文件 _exit(1); execve("/bin/sh",argv_rc,envp_rc);// 执行 /bin/sh,从 /etc/rc 读入命令并执行 _exit(2); } if (pid>0) // 以下是父进程执行的代码,&i 是存放返回信息的位置 while (pid != wait(&i)) // 父进程(进程 1)等待进程 2 运行结束,环境初始化完毕 /* nothing */; while (1) { if ((pid=fork())<0) { // 创建进程 n,作为登录 shell printf("Fork failed in init\r\n"); continue; } if (!pid) { // 进程 n 执行的代码部分 close(0);close(1);close(2); // 关闭以前遗留的三个标准流句柄 setsid(); // 创建新的会话 (void) open("/dev/tty0",O_RDWR,0); // 重新打开 /dev/tty0 作为 stdin (void) dup(0); // stdout (void) dup(0); // stderr _exit(execve("/bin/sh",argv,envp)); // 打开登录 shell } while (1) // 对于进程 1,等待进程 n(登录 shell) 运行结束 if (pid == wait(&i)) break; // 子进程 n 结束了,进程 1 又会回到上面新建一个进程运行登录 shell printf("\n\rchild %d died with code %04x\n\r",pid,i); sync(); // 同步,刷新缓冲区 } _exit(0); }
|
文章作者:
in1t
参考:
标签:__,
初始化,
主程序,
void,
memory,
init,
源码,
time,
linux
From: https://www.cnblogs.com/rebrobot/p/18318712