本文参考书籍
1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言 从实模式到保护模式
4.Linux内核设计的艺术
ps:基于x86硬件的pc系统
系统调用
系统调用(通常称为syscalls)接口是Linux内核与上层应用程序进行交互通信的唯一接口,如图所示;
用户程序通过直接或间接(通过库函数)调用中断int 0x80,并在eax寄存器中指定系统调用功能号,即可使用内核资源,包括系统资源等。通常系统调用使用函数形式进行调用,因此可带一个或多个参数,对于系统调用执行的结果,它会在返回值中表示出来,通常负值表示错误,而0则表示成功,在出错的情况下,错误的类型码被存放在全局变量errno中,通过调用库函数perror(),打印出该错误码对应的出错字符串信息。在Linux内核中,每个系统调用都具有唯一的一个系统调用功能号,这些功能号定义在文件include/unistd.h中,这些系统调用功能号实际上对应于include/linux/sys.h中定义的系统调用处理程序指针数组表sys_call_table[]中项的索引值。
当应用程序经过库函数向内核发出一个中断调用int 0x80时,就开始执行一个系统调用,其中寄存器eax中存放着系统调用号,而携带的参数可依次存在寄存器ebx、ecx和edx中,因此Linux0.12内核中用户程序能够最多直接传递3个参数,处理系统调用中断int 0x80的过程是程序kernel/system_call.S中的system_call。为了方便执行系统调用,在include/unistd.h中定义了宏函数_syscalln(),其中n代表携带的参数个数,可以使0~3,因此最多可以直接传递三个参数。例如fork函数;
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
根据static inline _syscall0(int,fork) 宏展开后
int fork(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_fork)); \
if (__res >= 0) \
return (int) __res; \
errno = -__res; \
return -1; \
}
如上代码就是fork根据宏展开的代码,该中断调用在eax(__res)寄存器中返回了实际读取的字节数。如果某个系统调用需要多于3个参数,那么内核通常采用的方法是直接把这些参数作为一个参数缓冲块,并把这个缓冲块的指针作为一个参数传递给内核,因此对于3个参数的系统调用,我们只需要使用带一个参数的宏_syscall1(),把第一个参数的指针传递给内核即可。当进入内核中的系统调用处理程序kernel/sys_call.S后,system_call的代码会首先检查eax中的系统调用功能号是否在有效系统调用号范围内,然后根据sys_call_table[]函数指针表调用执行相应的系统调用处理程序,
call _sys_call_table(, %eax, 4)
这句汇编操作数的含义是间接调用地址在_sys_call_table+%eax*4处的函数,由于sys_call_table指针每项4字节,因此这里需要给系统调用功能号乘上4,然后调用所得到的值从表中获取被调用处理函数的地址。
Linux初始化流程分析
在main初始化的代码如下;
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
ROOT_DEV = ORIG_ROOT_DEV; // 根文件系统设备号
SWAP_DEV = ORIG_SWAP_DEV; // 交换文件设备号
sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);
envp[1] = term;
envp_rc[1] = term;
drive_info = DRIVE_INFO; // 赋值内存0x90080处的硬盘参数
memory_end = (1<<20) + (EXT_MEM_K<<10); // 内存大小1MB+扩展内存(k)*1024
memory_end &= 0xfffff000; // 忽略不到4KB的内存数
if (memory_end > 16*1024*1024) // 如果内存量超过16MB,按照16MB来
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) // 如果内存大于12MB,则设置缓冲区末端4MB
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024) // 如果内存大于6MB,则设置缓冲区大小2MB
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024; // 否则设置缓冲区末端为1MB
main_memory_start = buffer_memory_end; // 主内存起始位置等于缓冲区末端
#ifdef 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()) { /* we count on this going ok */ // 在新建的子进程中执行init()函数
init();
}
for(;;)
__asm__("int $0x80"::"a" (__NR_pause):"ax"); // 任务0倍调度时执行系统调用pause
}
虚拟盘的初始化
此时我们查看一下rd_init初始化的过程;
long rd_init(long mem_start, int length)
{
int i;
char *cp;
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // 此时MAJOR_NR为1,为虚拟磁盘
rd_start = (char *) mem_start; // 内存的开始位置
rd_length = length; // 虚拟盘的长度
cp = rd_start;
for (i=0; i < length; i++) // 将虚拟盘的数据设置为'\0'
*cp++ = '\0';
return(length); // 返回长度
}
其中blk_dev定义在blk.h中;
struct blk_dev_struct {
void (*request_fn)(void); // 请求处理的函数指针
struct request * current_request; // 当前处理的请求结构
};
extern struct blk_dev_struct blk_dev[NR_BLK_DEV]; // 块设备表数组
由于在ramdisk.c中包含了;
#define MAJOR_NR 1
#include "blk.h"
此时DEVICE_REQUEST就是
#if (MAJOR_NR == 1)
/* ram disk */
#define DEVICE_NAME "ramdisk" // 设备名称
#define DEVICE_REQUEST do_rd_request // 设备请求处理函数
#define DEVICE_NR(device) ((device) & 7) // 设备号
#define DEVICE_ON(device) // 开启设备
#define DEVICE_OFF(device) // 关闭设备
此后虚拟盘的操作函数都是通过do_rd_request来进行操作。
内存的初始化
mem_init函数的初始化
void mem_init(long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem; // 设置内存最高端地址16MB
for (i=0 ; i<PAGING_PAGES ; i++) // 将1MB到16MB范围内所有内存页面对应的内存映射字节数组项置为USED,mem_map大小为15MB/4KB=3840
mem_map[i] = USED;
i = MAP_NR(start_mem); // 主内存区起始位置处页面号
end_mem -= start_mem;
end_mem >>= 12; // 主内存区所有的页面总数
while (end_mem-->0)
mem_map[i++]=0; // 主内存区页面对应字节值清零
}
系统对1MB以上的内存进行分页管理的,系统就通过一个mem_map的数组记录每一个页面的适用次数,先将所有的内存页面适用次数均设置成100,然后再依据主内存的其实位置和终止位置将处于主内存中的所有页面的适用次数全部清零,系统以后只把适用次数为0的页面视为空闲页面。为什么不把1MB以内的内存当作用户空间进行分页管理呢,这是因为,这样管理并记录页面使用次数本身就意味着进程可以对这部分被管理的内存进行操作,而系统是不能允许对1MB以下的内核代码区进行操作,所以也就不能对1MB以下的内存进行管理。
陷阱门的初始化
trap_init() 函数的初始化
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error); // 设置出错的中断向量处理函数
set_trap_gate(1,&debug); // 设置调试的中断处理函数
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS); // 设置TSS异常处理函数
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault); // 设置
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
for (i=18;i<48;i++) // 初始化int17~47的陷阱门设置成reserved
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21); // 允许8259A主芯片的IRQ2中断请求
outb(inb_p(0xA1)&0xdf,0xA1); // 允许8259A从芯片的IRQ13中断请求
set_trap_gate(39,¶llel_interrupt); // 设置并行口1的中断0x27陷阱门描述符
}
设置的函数都定义在了kernel/asm.s中,其中set_trap_gate代码如下;
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
该函数为一个宏定义,其中又调用了_set_gate,
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
参数分别是处理函数的addr,门描述符类型type和特权级信息dpl,设置位于地址gate_addr处的门描述符。
块设备初始化
blk_dev_init() 函数的初始化
void blk_dev_init(void)
{
int i;
for (i=0 ; i<NR_REQUEST ; i++) { // 将32项请求数组设置成空闲项
request[i].dev = -1;
request[i].next = NULL;
}
}
此时NR_REQUEST在blk.h的头文件中已经初始化为32。
tty初始化
tty_init()函数初始化
void tty_init(void)
{
...
con_init();
...
rs_init();
...
}
tty初始化时,主要是初始化所有终端缓冲队列,初始化串口终端和控制台终端。
con_init()函数是主要是根据系统数据中提供的显卡属性信息来设置其他属性信息,设置完成后将键盘中断服务程序与中断描述符表相关联。
rs_init函数是串口初始化函数主要是把串行口中断服务程序与中断描述符表相连,然后根据tty_table数据结构中的内容对这两个串口进行初始化设置,包括设置线路控制寄存器、设置发送的波特率等。
开机时间初始化
time_init函数主要是开机时获取时间。
具体的执行步骤是:CMOS是主板上一个只读的芯片,系统通过调用time_init函数,先对它上面记录的时间数据进行采集,提取不同等级的时间数据,然后再对这些数据进行整合,并计算得到开机启动时间。
static void time_init(void) // 读取CMOS实时时钟信息作为开机时间,并保持到全局变量中
{
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--; // 月份范围0~11
startup_time = kernel_mktime(&time); // 计算开机时间
}
调度程序初始化
sched_init主要是调度程序的初始化。
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); // 设置进程0的任务状态描述符
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); // 设置进程0的局部数据描述符
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i<NR_TASKS;i++) { // 清理除进程0之外的所有任务的描述符
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); // 复位NT标志位
ltr(0); // 加载进程0的任务描述符
lldt(0);
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */ // 初始化定时器中断操作
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt); // 设置定时器中处理程序
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call); // 设置系统调用0x80的处理程序
}
主要设置了进程0的任务运行,设置了时间定时器,并设置了系统调用中断入口函数。
缓冲管理初始化
buffer_init函数主要是缓冲区初始化
void buffer_init(long buffer_end) // 缓冲区初始化函数,对于具有16MB内存的系统,缓冲区末端被设置成4MB,对于具有8MB的内存,缓冲区被设置成2MB
{ // 从缓冲区开始位置和缓冲区末端处同时设置缓冲块头结构和对应的数据块,直到缓冲区内存被分配完毕。
struct buffer_head * h = start_buffer;
void * b;
int i;
if (buffer_end == 1<<20) // 如果缓冲区高端为1MB,从640KB~1MB被显示内存和BIOS占用,所以实际可用的缓冲区大小为640KB
b = (void *) (640*1024);
else
b = (void *) buffer_end;
while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) { // 双向建立缓冲区大小
h->b_dev = 0; // 使用该缓冲块的设备号
h->b_dirt = 0; // 脏标志
h->b_count = 0; // 缓冲块引用计数
h->b_lock = 0; // 缓冲块锁定标志
h->b_uptodate = 0; // 缓冲块更新标志
h->b_wait = NULL; // 指向等待该缓冲块解锁的进程
h->b_next = NULL; // 指向下一个缓冲头
h->b_prev = NULL; // 指向上一个缓冲头
h->b_data = (char *) b; // 指向数据块
h->b_prev_free = h-1; // 指向链表中前一项
h->b_next_free = h+1; // 指向链表中下一项
h++; // 指向下一个缓冲块头部
NR_BUFFERS++; // 数据区块数据累加
if (b == (void *) 0x100000) // 若递减到1MB则跳过384KB
b = (void *) 0xA0000;
}
h--; // 让h指向最后一个有效缓冲块头部
free_list = start_buffer; // 让空闲链表表头指向头一个缓冲块
free_list->b_prev_free = h; // 链表头的b_prev_free指向前一项
h->b_next_free = free_list; // h的下一项指针指向第一项,形成环链
for (i=0;i<NR_HASH;i++)
hash_table[i]=NULL; // 初始化hash表,置表中所有指针为NULL
}
硬盘初始化与软驱初始化
hd_init和floppy_init初始化的过程都比较类型
void hd_init(void)
{
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // 初始化硬盘的处理函数
set_intr_gate(0x2E,&hd_interrupt); // 设置硬盘中断的处理程序
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xbf,0xA1);
}
void floppy_init(void)
{
blk_size[MAJOR_NR] = floppy_sizes; // 设置软驱的大小
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // 设置软驱的请求处理函数
set_trap_gate(0x26,&floppy_interrupt); // 设置软驱的中断处理函数
outb(inb_p(0x21)&~0x40,0x21);
}
至此主要的初始化过程,分析流程如上,其中的细节内容,待后续继续分析,下一篇文章将会继续分析进程0 的初始化过程。