Linux

最后更新于 22 天前 20643 字 预计阅读时间: 2 小时


前言

在了解内核PWN的过程中同时越来越难理解各个结构体和exp的编写,于是我将目标瞄向了Linux源码,希望在理解源码的同时对内核PWN有更加深入的理解,本文根据《Linux源码趣读》这本书的排版进行学习,同时会加入自己的想法和理解。

Bootsect.s

前置知识

  • RAM:Random Access Memory,随机存取存储器,也叫主存,是与CPU直接交换数据的内部存储器。
  • ROM:Read-Only Memory,只读存储器,只能读出无法写入信息。信息一旦写入后就固定下来,即使切断电源,信息也不会丢失,用来存储BIOS信息,嵌入式设备控制程序等。
  • 硬盘:和ROM是两个不同的概念(我老是记错),但是都用于存储,磁盘可以重复读写,CPU不可以直接访问,要通过内存作为媒介。
  • 主引导扇区(MBR):位于整个磁盘的第一个扇区,即0柱面0磁头1扇区,用于存储计算机启动的引导代码,只要磁盘的0柱面0磁头1扇区最后两字节为0x55和0xAA BIOS就会认为该区域是一个启动区。


在按下开机键后,将PC寄存器初始化为0xFFFF0。CPU地址线连接的有RAM,ROM和IO端口,其中0xFFFF0地址为BIOS程序所在的ROM区域,CPU会执行此处的代码检查各计算机的自检以及将主引导扇区的数据(512字节)加载到内存0x7c00处并跳转到此处执行,主引导扇区部分代码如下。

.text
    BOOTSEG  = 0x07c0			! original address of boot-sector
    INITSEG  = 0x9000			! we move boot here - out of the way
    SETUPSEG = 0x9020			! setup starts here
    SYSSEG   = 0x1000			! system loaded at 0x10000 (65536).
.code 
	mov	ax,#BOOTSEG             
	mov	ds,ax                   
	mov	ax,#INITSEG
	mov	es,ax
	mov	cx,#256
	sub	si,si
	sub	di,di
	rep
	movw
	jmpi	go,INITSEG
	

以上汇编代码将 ds赋值为0x07c0,es赋值为0x9000,清空si和di并执行movw,将ds:si复制到es:di,一次复制一个字,进行256次,共512字节,jmpi将cs置为0x9000,ip置为go处的偏移执行。

总结如下:将0x7c00地址的512字节复制到0x90000处,并直接执行go地址的指令

以下部分在0x90000处执行

go:	mov	ax,cs
	mov	ds,ax
	mov	es,ax
! put stack at 0x9ff00.
	mov	ss,ax
	mov	sp,#0xFF00	

初始化ds,es,ss等于0x9000,sp等于0xFF00。


load_setup:
	mov	dx,#0x0000		! drive 0, head 0
	mov	cx,#0x0002		! sector 2, track 0
	mov	bx,#0x0200		! address = 512, in INITSEG
	mov	ax,#0x0200+SETUPLEN	! service 2, nr of sectors
	int	0x13			! read it
	jnc	ok_load_setup		! ok - continue
	mov	dx,#0x0000
	mov	ax,#0x0000		! reset the diskette
	int	0x13
	j	load_setup

int 0x13中断向量所指向的中断服务程序实质上就是磁盘服务程序,以上代码读取了,从第二个扇区(cx)读取四个扇区(al=4),放到0x200偏移处(bx)即0x90200处后重置磁盘。


ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

	mov	dl,#0x00
	mov	ax,#0x0800		! AH=8 is get drive parameters
	int	0x13
	mov	ch,#0x00
	seg cs
	mov	sectors,cx
	mov	ax,#INITSEG
	mov	es,ax

! Print some inane message

	mov	ah,#0x03		! read cursor pos
	xor	bh,bh
	int	0x10
	
	mov	cx,#24
	mov	bx,#0x0007		! page 0, attribute 7 (normal)
	mov	bp,#msg1
	mov	ax,#0x1301		! write string, move cursor
	int	0x10

读取取磁盘驱动器的参数并打印字符串“Loading system …”


! ok, we've written the message, now
! we want to load the system (at 0x10000)

	mov	ax,#SYSSEG
	mov	es,ax		! segment of 0x010000
	call	read_it
	call	kill_motor
        .......................
        jmpi	0,SETUPSEG

read_it是定义在bootsect.s中的一个函数,其作用是将第六个扇区后的240扇区加载到内存0x10000处(es),汇编太长了不看了,最后跳转到0x90200处执行代码,也就是执行setup.s处的代码。

其中2-6扇区存放的是setup.s的数据,后240扇区存放的是操作系统system的数据,分布如下

扩展:中断

整个操作系统就是一个中断驱动的死循环。当没有中断时,操作系统就会在循环中等待中断,中断到来时会去处理中断。

中断的分类

  • 中断:是一个异步事件,通常由IO设备触发
  • 异常(故障,陷阱,终止):同步事件,CPU检测到反常条件时触发

可编程中断控制器

他存在很多IRQ引脚线,接入能发出中断请求的硬件设备,可编程中断控制器提前设置了IRQ和中断号的对应关系,IO设备给IRQ发送信号时会转化为对应的终端号给CPU的INTR引脚发送一个信号,CPU收到后会去对应的端口读取中断值。

CPU收到中断信号的三种方式

  • 通过中断控制器给CPU的INTR引脚发信号
  • CPU执行到某段指令发现了异常
  • 执行int n指令

CPU处理中断信号

CPU收到中断信号后n后会去中断描述符表(IDT)中寻找对应的中断描述符,在中断描述符中存在一个中断处理程序的段选择子和偏移,根据段选择子找到对应的全局描述符(GDT)得到基址后加上偏移找到中断处理程序。

操作系统初始化中断描述符表

在traps.c中的trap_init初始化了中断描述符表。

void trap_init(void)
{
	int i;

    // 设置除操作出错的中断向量值。
	set_trap_gate(0,&divide_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);
	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);
    // 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。
	for (i=17;i<48;i++)
		set_trap_gate(i,&reserved);
    // 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。
	set_trap_gate(45,&irq13);
	outb_p(inb_p(0x21)&0xfb,0x21);  // 允许8259A主芯片的IRQ2中断请求。
	outb(inb_p(0xA1)&0xdf,0xA1);    // 允许8259A从芯片的IRQ3中断请求。
	set_trap_gate(39,&parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
}

CPU进入中断处理程序的流程

在进入中断处理程序之前会进行一系列的环境保护阶段

  • 若特权级发生变化则将SS和ESP压入栈中,并使用TSS中的SS和ESP
  • 压入EFLAGS
  • 压入当前的CS和IP
  • 如果存在错误码则压入错误码

进行以上的操作后会将得到的段描述符和偏移装载到CS:IP上执行中断处理程序。

返回指令

使用iret和iretd从中断处理程序中返回,iret和iretd的作用是相同的,iretd比iret好用,原文

IRET and IRETD are mnemonics for the same opcode. The IRETD mnemonic (interrupt return double) is intendedfor use when returning from an interrupt when using the 32-bit operand size; however, most assemblers use theIRET mnemonic interchangeably for both operand sizes.

iret会将进入中断处理程序推入栈的顺序反向执行

在NT位(EFLAGS)为0的情况下使用栈存储ESP和SS,伪代码如下

  • pop EIP
  • pop CS
  • pop EFLAGS
  • pop ESP
  • pop SS

为1的情况下则不会有pop ESP和pop SS,而是从TSS中得到。

软中断

软中断是由软件实现的。

  • 宏观上软中断会打断当前运行的进程转而执行软中断处理程序
  • 微观来看,存在一个守护进程(后台运行的特殊进程,用于执行特定的系统任务,不受控于任意终端)不断地轮询软中断标志位,如果有标志位被置为1则转去执行改软中断处理程序。

在linux 2.6 main.c中start_kernel存在rest_init()


asmlinkage void __init start_kernel(void)
{
...................
rest_init();
}

rest_init()开启了内核线程init

static void rest_init(void)
{
	kernel_thread(init, NULL, CLONE_KERNEL);
	unlock_kernel();
 	cpu_idle();
} 

在init中存在do_pre_smp_initcalls();

static int init(void * unused)
{
	lock_kernel();
	/*
	 * Tell the world that we're going to be the grim
	 * reaper of innocent orphaned children.
	 *
	 * We don't want people to have to make incorrect
	 * assumptions about where in the task array this
	 * can be found.
	 */
	child_reaper = current;

	/* Sets up cpus_possible() */
	smp_prepare_cpus(max_cpus);

	do_pre_smp_initcalls();
.............
}

do_pre_smp_initcalls执行了spawn_ksoftirqd函数即(spawn kernel soft irq daemon)开启软中断守护进程

static void do_pre_smp_initcalls(void)
{
extern int spawn_ksoftirqd(void);
.........................
}

spawn_ksoftirqd调用了CPU回调函数

__init int spawn_ksoftirqd(void)
{
	cpu_callback(&cpu_nfb, CPU_ONLINE, (void *)(long)smp_processor_id());
	register_cpu_notifier(&cpu_nfb);
	return 0;
}

cpu_callback开启内核线程调用ksoftirqd

static int __devinit cpu_callback(struct notifier_block *nfb,
				  unsigned long action,
				  void *hcpu)
{
.........................
		if (kernel_thread(ksoftirqd, hcpu, CLONE_KERNEL) < 0) {
			printk("ksoftirqd for %i failed\n", hotcpu);
			return NOTIFY_BAD;
		}
.......................
}

ksoftirqd死循环执行do_softirq

static int ksoftirqd(void * __bind_cpu)
{
........................
	for (;;) {
		if (!local_softirq_pending())
			schedule();
		__set_current_state(TASK_RUNNING);
		while (local_softirq_pending()) {
			do_softirq();
			cond_resched();
		}
		__set_current_state(TASK_INTERRUPTIBLE);
	}
........................
}

do_softirq通过local_softirq_pending()获取软中断向量表内容,检查每一位,如果为1则跳转执行对应的中断处理程序。

asmlinkage void do_softirq(void)
{
..........................
	pending = local_softirq_pending();
		do {
			if (pending & 1)
				h->action(h);
			h++;
			pending >>= 1;
		} while (pending);
..........................
}

Setup.s

获取设备信息

从上到下分别是获取光标位置,内存信息,显卡信息,检查显示方式并获取参数,获取第一第二个硬盘的信息。

! posterity.

	mov	ax,#INITSEG	! this is done in bootsect already, but...
	mov	ds,ax
	mov	ah,#0x03	! read cursor pos
	xor	bh,bh
	int	0x10		! save it in known place, con_init fetches
	mov	[0],dx		! it from 0x90000.
! Get memory size (extended mem, kB)

	mov	ah,#0x88
	int	0x15
	mov	[2],ax

! Get video-card data:

	mov	ah,#0x0f
	int	0x10
	mov	[4],bx		! bh = display page
	mov	[6],ax		! al = video mode, ah = window width

! check for EGA/VGA and some config parameters

	mov	ah,#0x12
	mov	bl,#0x10
	int	0x10
	mov	[8],ax
	mov	[10],bx
	mov	[12],cx

! Get hd0 data

	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x41]
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0080
	mov	cx,#0x10
	rep
	movsb

! Get hd1 data

	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x46]
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
	rep
	movsb

内存布局如下所示

内存地址长度(Byte)名称
0x900002光标位置
0x900022扩展内存数
0x900042显示页面
0x900061显示方式
0x900071字符列数
0x900082未知
0x9000A1显示内存
0x9000B1显示状态
0x9000C2显卡特征参数
0x9000E1屏幕行数
0x9000F1屏幕列数
0x9008016硬盘1参数表
0x9009016硬盘2参数表
0x901FC2根设备号

接着使用cli汇编关闭中断,因为下面要对中断向量表进行重写。

! now we want to move to protected mode ...

	cli			! no interrupts allowed !

再接着进行复制操作,es=x,ds=es+0x1000进行0x8000次的复制操作即0x10000-0x90000的数据都往下移0x10000到0地址处。movsw每次复制1word,进行0x8000次,一次rep复制0x10000字节,movsw是以es:di-->ds:si,在实模式下段寄存器寻址都要乘上0x10,即(0x10000+0x10000*i)复制到(0+0x10000*i),正好全部复制完。

 first we move the system to it's rightful place

	mov	ax,#0x0000
	cld			! 'direction'=0, movs moves forward
do_move:
	mov	es,ax		! destination segment
	add	ax,#0x1000
	cmp	ax,#0x9000
	jz	end_move
	mov	ds,ax		! source segment
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000
	rep
	movsw
	jmp	do_move

而在0x10000的数据正好是system所在地址,内存布局变成了,0x90000地址原本是bootsect.s的数据,但是被自身存储的部分变量所破坏。

设置描述符表

在32位保护模式下,段寄存器存放的内容不再叫做段基址,而是叫做段选择子。段寄存器0-15位装载段选择子

16-79位是不可见的,用来缓存对应描述符的基址,界限和属性,实现用16位段选择子装载80位段寄存器。

CPU会根据段选择子从全局/局部描述符(GDT,IDT)表中找到一个段描述符。在段描述符中存放着与该段有关的信息,包括段基址,段权限,类型等。数据结构如下所示

如何找到GDT和IDT?名为GDTR和IDTR的寄存器记录着全局/局部描述符的基址,需要用lgdt/lidt汇编指令对其进行加载地址。 Setup.s结束对0x10000-0x90000的复制后设置了全局/局部描述符的基址。

end_move:
	mov	ax,#SETUPSEG	! right, forgot this at first. didn't work :-)
	mov	ds,ax
	lidt	idt_48		! load idt with 0,0
	lgdt	gdt_48		! load gdt with whatever appropriate

GDTR和IDTR结构如下。

gdt_48和idt_48都是设置在Setup.s中的数据,因为Setup.s在0x90200处,故gdt的地址为0x90200+gdt处的偏移。idt未初始化。

idt_48:
	.word	0			! idt limit=0
	.word	0,0			! idt base=0L

gdt_48:
	.word	0x800		! gdt limit=2048, 256 GDT entries
	.word	512+gdt,0x9	! gdt base = 0X9xxxx

在gdt处设置好了三个段描述符分别为

  • 代码段描述符
  • 数据段描述符
gdt:
	.word	0,0,0,0		! dummy

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9A00		! code read/exec
	.word	0x00C0		! granularity=4096, 386

	.word	0x07FF		! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000		! base address=0
	.word	0x9200		! data read/write
	.word	0x00C0		! granularity=4096, 386

值得注意的是,代码段描述符和数据段描述符段基址都为0,这意味着完全依靠ip和地址进行寻址。

进入保护模式

打开A20地址线

	call	empty_8042
	mov	al,#0xD1		! command write
	out	#0x64,al
	call	empty_8042
	mov	al,#0xDF		! A20 on
	out	#0x60,al
	call	empty_8042
  • 向端口0x64发送命令0xD1,通知键盘控制器准备写入输出端口‌。
  • 向端口0x60写入控制字节0xDF,将A20地址线置为有效‌

打开A20地址线意味着突破地址线20位的宽度,变成32位可用,寻址空间来到4GB。

接着就是对可编程中断控制器8259芯片的编程,有能力了再看这部分

! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.

	mov	al,#0x11		! initialization sequence
	out	#0x20,al		! send it to 8259A-1
	.word	0x00eb,0x00eb		! jmp $+2, jmp $+2
	out	#0xA0,al		! and to 8259A-2
	.word	0x00eb,0x00eb
	mov	al,#0x20		! start of hardware int's (0x20)
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x28		! start of hardware int's 2 (0x28)
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x04		! 8259-1 is master
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x02		! 8259-2 is slave
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x01		! 8086 mode for both
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0xFF		! mask off all interrupts for now
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al

正式开启保护模式

	mov	ax,#0x0001	! protected mode (PE) bit
	lmsw	ax		! This is it!
	jmpi	0,8		! jmp offset 0 of segment 8 (cs)

LMSW(加载机器状态字)汇编用于快速配置Cr0的低4位,lmsw ax即将Cr0低四位设置为ax的数值。

Cr0寄存器内容如下所示

展开:Cr0寄存器FLag各个位的作用
PE:保护使能(Protection Enable),该位用于控制处理器的保护模式。

    当 PE = 1 时,处理器运行在保护模式下,可以使用内存保护等功能。
    当 PE = 0时,处理器运行在实模式下。

MP:监视协处理器(Monitor Coprocessor),该位用于控制对协处理器的监控。

    当 MP = 1 时,处理器监视协处理器的使用情况,当发生对协处理器的操作时,会触发异常。
    当 MP = 0 时,处理器不监视协处理器。

EM:模拟(Emulation),该位用于控制协处理器的模拟。

    当 EM = 1 时,处理器不支持协处理器指令,会将协处理器指令转为软件模拟执行。
    当 EM = 0 时,处理器支持协处理器指令。

TS:任务切换(Extension Type),该位用于指示处理器是否支持处理器扩展。

    当 ET = 1 时,表示处理器支持处理器扩展。
    当 ET = 0 时,表示不支持处理器扩展。

ET:扩展类型(Extension Type),该位用于指示处理器是否支持处理器扩展。

    当 ET = 1 时,表示处理器支持处理器扩展。
    当 ET = 0 时,表示不支持处理器扩展。

NE:数值错误(Numeric Error),该位用于控制浮点异常的处理方式。

    当 NE = 1 时,处理器会将浮点异常的错误码保存到浮点异常状态寄存器中。
    当 NE = 0 时,处理器在浮点异常发生时不保存错误码。

WP:写保护(Write Protect),该位用于控制写保护。

    当 WP = 1 时,处理器会禁止用户态程序向只读页面写数据。
    当 WP = 0 时,处理器不会执行写保护。

AM:对齐掩码(Alignment Mask),该位用于控制内存对齐检查。

    当 AM = 1 时,处理器会执行内存对齐检查。
    当 AM = 0 时,处理器不会执行对齐检查。

NW:不直写(Not Write-through),该位用于控制写缓冲的写策略。

    当 NW = 1 时,处理器执行不通过写缓冲进行写操作,而直接写入内存。
    当 NW = 0 时,处理器使用写缓冲进行写操作。

CD:缓存禁用(Cache Disable),该位用于控制处理器的缓存。

    当 CD 为 1 时,处理器禁用数据缓存。
    当 CD 为 0 时,处理器启用数据缓存。

PG:分页(Paging),该位用于控制分页功能。

    当 PG = 1 时,处理器启用分页机制。
    当 PG = 0 时,处理器禁用分页机制。

第0位的PE位表示CPU允许的模式,将其置1表示进入保护模式。

设置完Cr0后跳转cs-->8,ip-->0,其中cs=8,段选择子为0000000000001000,3-15位位描述符索引,第2位为0表示该描述符载GDT中,该操作装载上述初始化的代码段描述符。

jmpi	0,8		! jmp offset 0 of segment 8 (cs)

段基址0+ip=0即跳转到0地址(system代码)处。

System

system模块是由head.s和main.o生成的,在makefile中可以找到。

tools/system:	boot/head.o init/main.o 

head.s

在开始部分的pg_dir表示页目录,以后页表将存放在这里。使用段选择子0x10装填ds,es,fs,gs,16位比特0000000000010000,index部分为0000000000010,装载第二个描述符即数据段描述符。

pg_dir:
.globl startup_32
startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs

lss(Load Stack Segment)用于设置SS和SP,从内存中读取32位数据,低16位给sp,高16位给ss

lss stack_start,%esp

stack_start这个变量在sched.c中。高16位位0x10,低16位为user_stack 最后一个元素的地址。加载完后栈的基址为0,sp指向stack_start最后一个元素。

long user_stack [ PAGE_SIZE>>2 ] ;

struct {
	long * a;
	short b;
	} 
stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

初始化中断描述符和重建段描述符

结束对栈的转移后开始了idt的初始化和gdt的重建。

call setup_idt
call setup_gdt

IDT的初始化

setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

上述执行完后eax=0x00080000|offset ignore_int,edx=0x00008E00。

初始化255个相同的idt。

	lea idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr
	ret

idt的结构如下所示,edx是高32位,offset[31,16]=0

eax是低32位,offset[15,0]=offset ignore_int

上述汇编初始化了255个相同的中断描述符,中断处理程序都指向了ignore_int

ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg
	call printk
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

GDT的重建,因为最开始的GDT实在setup.s中,在后续会被覆盖需要重新设置一个GDT,汇编重新设置了GDTR。

setup_gdt:
	lgdt gdt_descr
	ret

gdt_descr

gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */

初始化完后进行了一些检查最后跳转到了after_page_tables

xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b

/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 */
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP
	movl %eax,%cr0
	call check_x87
	jmp after_page_tables

开启分页机制

页式内存管理的原理不在此讲述。

分页以4KB即(1024bit)划分,在Linux-0.11中约定使用的内存不超过16MB,故总共有4个页目录表项,4x1024*4KB=16MB。

movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */
	cld;rep;stosl
	movl $pg0+7,pg_dir		/* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */

页目录表项/页表项结构如下

$pg1+7,$pg+7,$pg3+7,$pg4+7以及后面的0xfff007都是在设置页属性,其中值得关注的是

  • P位:表示该页是否存在,1表示该页存在。
  • RW位:表示用户是否可读可写,1表示用户可读可写。
  • US位:表示特权状态,1表示为用户态。

四个页目录表项的内容分别是分别是pg0,pg1,pg2,pg3的地址。.org关键字的作用是告诉编译器将该变量放在那个地址。故pg0,pg1,pg2,pg3四个页表在内存中的地址分别为0x1000,0x2000,0x3000,0x4000

.org 0x1000
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

结束对页目录表的初始化后对页表的初始化。将dirctor设置为1使edi递减后stosl将eax的值复制到es:edi处。对$pg3+4092地址赋值即pg3末尾开始进行4,095(0xfff007/0x1000)次赋值,将pg3 pg2 pg1 pg0初始化。

	movl $pg3+4092,%edi
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1

结束对页表的初始化后将Cr3置为0,Cr3中用于页目录表物理内存基地址的存放,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)。在head.s开头的pg_dir位于system的开头也就是0地址(页目录表物理内存)处。并将Cr0第31位PG:分页(Paging)置1开启分页模式。

	xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */

内容布局如下

跳转到main

在进入页目录表/页表初始化前往栈内推入了一些数值,并在页表设置完后执行ret直接跳到main处。

	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $main
	jmp setup_paging

Main.c

展开: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;高速缓存末端地址->buffer_memory_end;
    // 机器内存数->memory_end;主内存开始地址->main_memory_start;
    // 其中ROOT_DEV已在前面包含进的fs.h文件中声明为extern int
 	ROOT_DEV = ORIG_ROOT_DEV;
 	drive_info = DRIVE_INFO;        // 复制0x90080处的硬盘参数
	memory_end = (1<<20) + (EXT_MEM_K<<10);     // 内存大小=1Mb + 扩展内存(k)*1024 byte
	memory_end &= 0xfffff000;                   // 忽略不到4kb(1页)的内存数
	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;
    // 如果在Makefile文件中定义了内存虚拟盘符号RAMDISK,则初始化虚拟盘。此时主内存将减少。
#ifdef RAMDISK
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
    // 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,若实在
    // 看不下去了,就先放一放,继续看下一个初始化调用。——这是经验之谈。o(∩_∩)o 。;-)
	mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c
	trap_init();                            // 陷阱门(硬件中断向量)初始化,kernel/traps.c
	blk_dev_init();                         // 块设备初始化,kernel/blk_drv/ll_rw_blk.c
	chr_dev_init();                         // 字符设备初始化, kernel/chr_drv/tty_io.c
	tty_init();                             // tty初始化, kernel/chr_drv/tty_io.c
	time_init();                            // 设置开机启动时间 startup_time
	sched_init();                           // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c)
    // 缓冲管理初始化,建内存链表等。(fs/buffer.c)
	buffer_init(buffer_memory_end);
	hd_init();                              // 硬盘初始化,kernel/blk_drv/hd.c
	floppy_init();                          // 软驱初始化,kernel/blk_drv/floppy.c
	sti();                                  // 所有初始化工作都做完了,开启中断
    // 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。
	move_to_user_mode();                    // 移到用户模式下执行
	if (!fork()) {		/* we count on this going ok */
		init();                             // 在新建的子进程(任务1)中执行。
	}
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
    // pause系统调用会把任务0转换成可中断等待状态,再执行调度函数。但是调度函数只要发现系统中
    // 没有其他任务可以运行是就会切换到任务0,而不依赖于任务0的状态。
	for(;;) pause();
}

我们拆开几部分来看

        #define EXT_MEM_K (*(unsigned short *)0x90002)
        #define DRIVE_INFO (*(struct drive_info *)0x90080)
        #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
        ROOT_DEV = ORIG_ROOT_DEV;
 	drive_info = DRIVE_INFO;        // 复制0x90080处的硬盘参数
	memory_end = (1<<20) + (EXT_MEM_K<<10);     // 内存大小=1Mb + 扩展内存(k)*1024 byte
	memory_end &= 0xfffff000;                   // 忽略不到4kb(1页)的内存数

定义了几个全局变量指向Setup.s保存的参数中(回顾历史喵),获取扩展内存数,硬盘参数和根设备号。并计算内存大小。

再看下一部分根据内存的大小计算main_memory_startbuffer_memory_end,当内存大小大于16MB时将内存设定为16MB。

  • 内存<6MB,main_memory_start=buffer_memory_end=1MB
  • 6MB<内存<12MB,main_memory_start=buffer_memory_end=2MB
  • 12MB<内存,main_memory_start=buffer_memory_end=4MB
        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;

设置了缓冲区内存和主内存的位置(main_memory_start向上,buffer_memory_end向下)

接着就是后面一堆的初始化函数了分段来看

mem_init

mem_init在/mem/memory.c中将16MB按照4kb分页,将每个页块设置使用标志初始化,设置一个全局静态变量mem_map,记录哪些内存被占用,将其全部置为USED 100,将main_memory_start以上的内存在mem_map对应的位全部置0表示未被占用。

// 内存低端(1MB)
#define LOW_MEM 0x100000
// 分页内存15 MB,主内存区最多15M.
#define PAGING_MEMORY (15*1024*1024)
// 分页后的物理内存页面数(3840)
#define PAGING_PAGES (PAGING_MEMORY>>12)
// 指定地址映射为页号
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
// 页面被占用标志.
#define USED 100
void mem_init(long start_mem, long end_mem)
{
	int i;
	HIGH_MEMORY = end_mem;               
	for (i=0 ; i<PAGING_PAGES ; i++)
            mem_map[i] = USED;
	i = MAP_NR(start_mem);     
	end_mem -= start_mem;
	end_mem >>= 12;            
	while (end_mem-->0)
		mem_map[i++]=0;        
}

注意mem_map的大小为3840(15MB>>12),该结构体只记录2-16共15MB的内存页,低1MB给内核使用,i = MAP_NR(start_mem); 实际上是((main_memory_start-1MB)>>12)即main_memory_start地址的内存页在mem_map的位置。当main_memory_start为4MB时。(我理解这块花了点时间。。。)

  • 如果mem_map记录的是16MB的内存页则main_memory_start在4MB的位置。
  • 如果mem_map记录的是15MB的内存页则main_memory_start实际上在mem_map是3MB内存页的位置。

一句话:mem_init初始化哪些内存被使用(低1MB和缓冲区内存)和哪些内存未被使用(主内存)。

在memory.c中存在一个get_free_page函数用于获取空闲页块,使用的是扩展内联汇编语法(AT&T)。

unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");

__asm__("std ; repne ; scasb\n\t"   // 置方向位,al(0)与对应每个页面的(di)内容比较
	"jne 1f\n\t"                    // 如果没有等于0的字节,则跳转结束(返回0).
	"movb $1,1(%%edi)\n\t"          // 1 => [1+edi],将对应页面内存映像bit位置1.
	"sall $12,%%ecx\n\t"            // 页面数*4k = 相对页面其实地址
	"addl %2,%%ecx\n\t"             // 再加上低端内存地址,得页面实际物理起始地址
	"movl %%ecx,%%edx\n\t"          // 将页面实际其实地址->edx寄存器。
	"movl $1024,%%ecx\n\t"          // 寄存器ecx置计数值1024
	"leal 4092(%%edx),%%edi\n\t"    // 将4092+edx的位置->dei(该页面的末端地址)
	"rep ; stosl\n\t"               // 将edi所指内存清零(反方向,即将该页面清零)
	"movl %%edx,%%eax\n"            // 将页面起始地址->eax(返回值)
	"1:"
	:"=a" (__res)
	:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),"D" (mem_map+PAGING_PAGES-1)
	);
return __res;           // 返回空闲物理页面地址(若无空闲页面则返回0).
}
扩展:扩展内联汇编基础语法

扩展内联汇编的基本语法如下,其中asm声明和asm code部分与内联汇编一样故不在多述,在每段汇编的结尾要加上分号或\n\t让编译器区分每句汇编。

asm [volatile] 
("assembly code" 
: output 
: input 
: clobber/modify
)
  • ouput:用来指定汇编中的数据如给C使用,在此部分可以给C变量赋值。
  • input:用来指定C变量如何给汇编代码使用,在此部分可以将C变量的值放入寄存器或内存。
  • clobber/modify:用来声明该内联汇编破坏了上下文环境。如rax的内容遭到修改则编译器会在该内联汇编前保存rax用于后续的恢复。

output和input还可以加上各种约束

在output中约束可以分为两种,表示属性的

  • =表述该变量可写
  • +表示该变量可读可写
  • &表示该变量约束的寄存器只能被output使用

表示装填方式?的

  • a:表示寄存器 eax/ax/al
  • b:表示寄存器 ebx/bx/bl
  • c:表示寄存器 ecx/cx/cl
  • d:表示寄存器 edx/dx/dl
  • D:表示寄存器 edi/di
  • S:表示寄存器 esi/si
  • q:表示任意这 4 个通用寄存器之一:eax/ebx/ecx/edx
  • r:表示任意这 6 个通用寄存器之一:eax/ebx/ecx/edx/esi/edi
  • g:表示可以存放到任意地点(寄存器和内存)。相当于除了同 q 一样外,还可以让 gcc 安排在内存中
  • f:表示浮点寄存器
  • t:表示第 1 个浮点寄存器
  • u:表示第 2 个浮点寄存器

上述两者可用配合使用

asm(
"addl %%ebx, %%eax"
:"=a"(out_sum)
:"a"(in_a),"b"(in_b)
); 
  • output:"=a"(out_sum)表示out_sum可写并最后用rax/eax进行赋值
  • input : "a"(in_a),"b"(in_b),表示用rax/eax存储in_a的数据,rbx/ebx存储in_b的数据
  • clobber/modify为空

AT&T语法下用%%来表示寄存器,单个%用来引用数据,例如%0表示output中的out_sum,%1表示in_a。

好了你已经学会扩展内联汇编基本语法了,来看看Linux内核源码吧(悲)

__asm__("std ; repne ; scasb\n\t"  
	"jne 1f\n\t"                  
	"movb $1,1(%%edi)\n\t"          
	"sall $12,%%ecx\n\t"          
	"addl %2,%%ecx\n\t"           
	"movl %%ecx,%%edx\n\t"         
	"movl $1024,%%ecx\n\t"        
	"leal 4092(%%edx),%%edi\n\t"    
	"rep ; stosl\n\t"              
	"movl %%edx,%%eax\n"           
	"1:"
	:"=a" (__res)
	:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),"D" (mem_map+PAGING_PAGES-1)
	);
  • 先看input,"0" (0)表示用0赋值给%0(__res)即rax寄存器,PAGING_PAGES放在rcx/ecx中,mem_map+PAGING_PAGES-1放在rdi/edi中,有一个立即数LOW_MEM。
  • std ; repne ; scasb
    std 设置direction位,反向传送。
    repne(repeat no equal)根据CF位判断是否重复下一个操作,每执行一次cx加/减1(由direction位决定)
    scasb(scan string byte)相当于cmp al,ds:di; inc/dec di用于寻找和al相同的内存
    意思是找到与al数值相同的内存,其中al=0,di=mem_map+PAGING_PAGES-1,即寻找空闲页块,ecx=空闲页块的索引
  • jne 1f:未找到空闲内存则返回
  • movb $1,1(%%edi):在mem_map中将其置1表示该内存块被占用。
  • sall $12,%%ecx:将获取到空闲页块的索引左移12位得到该内存页块的地址
  • addl %2,%%ecx:加上低1MB内存地址
  • movl %%ecx,%%edx:保存该内存页块的地址到edx
  • movl $1024,%%ecx;leal 4092(%%edx),%%edi;rep ; stosl:将该内存页清空
  • movl %%edx,%%eax:将该内存页块的基址给eax作为返回值返回。

trap_init

trap_init定义在kernel/traps.c中。

展开:在trap_init中调用许多set_xxxx_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))

#define set_intr_gate(n,addr) \
	_set_gate(&idt[n],14,0,addr)

#define set_trap_gate(n,addr) \
	_set_gate(&idt[n],15,0,addr)

#define set_system_gate(n,addr) \
	_set_gate(&idt[n],15,3,addr)

void trap_init(void)
{
	int i;

    // 设置除操作出错的中断向量值。
	set_trap_gate(0,&divide_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);
	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);
    // 下面把int17-47的陷阱门先均设置为reserved,以后各硬件初始化时会重新设置自己的陷阱门。
	for (i=17;i<48;i++)
		set_trap_gate(i,&reserved);
    // 设置协处理器中断0x2d(45)陷阱门描述符,并允许其产生中断请求。设置并行口中断描述符。
	set_trap_gate(45,&irq13);
	outb_p(inb_p(0x21)&0xfb,0x21);  // 允许8259A主芯片的IRQ2中断请求。
	outb(inb_p(0xA1)&0xdf,0xA1);    // 允许8259A从芯片的IRQ3中断请求。
	set_trap_gate(39,&parallel_interrupt); // 设置并行口1的中断0x27陷阱门的描述符。
}

这些set_xxxx_gate都指向了同一个宏定义_set_gate,只是传的参数不一样,分析_set_gate之前先了解以下中断描述符的结构,如下图,和段描述符差不多,只不过段基址变成了偏移。

_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))
  • %1和%2分别是中断描述符的低32位和高32位,%0是根据传进来的特权级(DPL),和描述符的类型(type)来初始化。%4指定的段选择子
  • 赋值后eax=0x00000000|%0,高16位(offset[31:16])全为0,低16位是属性
  • edx=0x00080000|addr,段选择子为GDT表的第一个并将offset[15:0]设置为传进来的中断处理程序的偏移。

blk_dev_init

定义在kernel/blk_dev/ll_rw/blk.c中,翻译为块设备初始化。代码很简单,就是往request结构体中对应项写入值。

#define NR_REQUEST	32
struct request request[NR_REQUEST];
void blk_dev_init(void)
{
	int i;

	for (i=0 ; i<NR_REQUEST ; i++) {
		request[i].dev = -1;
		request[i].next = NULL;
	}
}

request结构体定义在kernel/blk_dev/ll_rw/blk.h中

struct request {
	int dev;		/* -1 if no request */
	int cmd;		/* READ or WRITE */
	int errors;
	unsigned long sector;
	unsigned long nr_sectors;
	char * buffer;
	struct task_struct * waiting;
	struct buffer_head * bh;
	struct request * next;
};
  • dev:表示设备号,如果等于-1则表示空闲
  • cmd:表示本次操作是read还是write
  • errors:表示操作时产生的错误数
  • sector:表示起始的扇区
  • nr_sectors:表示要读取/写入的扇区数量
  • buffer:缓冲区,读取磁盘时数据存放处
  • waiting:是一个task_struct结构,可以表示一个进程,表示哪个进程发起了请求
  • bh:缓冲区头指针,在缓冲区部分会讲到
  • next:指向下一个request(在blk.c中的request是一个request结构体数组,在blk.h中的request是结构体

目前只需要知道blk_dev_init做了什么和request 结构体即可。

tty_init

tty_init在kernel/chr_drv/tty_io.c中。只存在两个函数

void tty_init(void)
{
    // 初始化串行中断程序和串行接口1和2(serial.c)
	rs_init();
	con_init();     // 初始化控制台终端(console.c文件中)
}

rs_init打开了串口中断并设置中断处理程序,但是串口在现在的计算机很少用到,可以忽略。。。

void rs_init(void)
{
    // 下面两句用于设置两个串行口的中断门描述符。rsl_interrupt是串口1的中断处理过程指正。
    // 串口1使用的中断是int 0x24,串口2的是int 0x23.
	set_intr_gate(0x24,rs1_interrupt);      // 设置串行口1的中断门向量(IRQ4信号)
	set_intr_gate(0x23,rs2_interrupt);      // 设置串行口2的中断门向量(IRQ3信号)
	init(tty_table[1].read_q.data);         // 初始化串行口1(.data是端口基地址)
	init(tty_table[2].read_q.data);         // 初始化串行口2
	outb(inb_p(0x21)&0xE7,0x21);            // 允许主8259A响应IRQ3、IRQ4中断请求
}
展开:con_init
void con_init(void)
{
    // 寄存器变量a为了高效的访问和操作。
    // 若想指定存放的寄存器(如eax),则可以写成:
    // register unsigned char a asm("ax");。
	register unsigned char a;
	char *display_desc = "????";
	char *display_ptr;

    // 首先根据setup.s程序取得系统硬件参数初始化几个本函数专用的静态全局变量。
	video_num_columns = ORIG_VIDEO_COLS;    // 显示器显示字符列数
	video_size_row = video_num_columns * 2; // 每行字符需要使用的字节数
	video_num_lines = ORIG_VIDEO_LINES;     // 显示器显示字符行数
	video_page = ORIG_VIDEO_PAGE;           // 当前显示页面
	video_erase_char = 0x0720;              // 擦除字符(0x20是字符,0x07属性)
	
    // 根据显示模式是单色还是彩色分别设置所使用的显示内存起始位置以及显示寄存器
    // 索引端口号和显示寄存器数据端口号。如果原始显示模式等于7,则表示是单色显示器。
	if (ORIG_VIDEO_MODE == 7)			/* Is this a monochrome display? */
	{
		video_mem_start = 0xb0000;      // 设置单显映象内存起始地址
		video_port_reg = 0x3b4;         // 设置单显索引寄存器端口
		video_port_val = 0x3b5;         // 设置单显数据寄存器端口
        // 接着我们根据BIOS中断int 0x10 功能0x12获得的显示模式信息,判断显示卡是
        // 单色显示卡还是彩色显示卡。若使用上述中断功能所得到的BX寄存器返回值不等于
        // 0x10,则说明是EGA卡。因此初始显示类型为EGA单色。虽然EGA卡上有较多显示内存,
        // 但在单色方式下最多只能利用地址范围在 0xb0000-0xb8000 之间的显示内存。
        // 然后置显示器描述字符串为 'EGAm'. 并会在系统初始化期间显示器描述字符串将
        // 显示在屏幕的右上角。
        // 注意,这里使用了 bx 在调用中断 int 0x10 前后是否被改变的方法来判断卡的类型。
        // 若BL在中断调用后值被改变,表示显示卡支持 Ah=12h 功能调用,是EGA或后推出来的
        // VGA等类型的显示卡。若中断调用返回值未变,表示显示卡不支持这个功能,则说明
        // 是一般单色显示卡。
		if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
		{
			video_type = VIDEO_TYPE_EGAM;       // 设置显示类型(EGA单色)
			video_mem_end = 0xb8000;            // 设置显示内存末端地址
			display_desc = "EGAm";              // 设置显示描述字符串
		}
		else    // 如果 BX 寄存器的值等于 0x10,则说明是单色显示卡MDA。
		{
			video_type = VIDEO_TYPE_MDA;        // 设置显示类型(MDA单色)
			video_mem_end	= 0xb2000;          // 设置显示内存末端地址
			display_desc = "*MDA";              // 设置显示描述字符串
		}
	}
    // 如果显示模式不为7,说明是彩色显示卡。此时文本方式下所用的显示内存起始地址为0xb8000;
    // 显示控制索引寄存器端口地址为 0x3d4;数据寄存器端口地址为 0x3d5。
	else								/* If not, it is color. */
	{
		video_mem_start = 0xb8000;              // 显示内存起始地址
		video_port_reg	= 0x3d4;                // 设置彩色显示索引寄存器端口
		video_port_val	= 0x3d5;                // 设置彩色显示数据寄存器端口
        // 再判断显示卡类别。如果 BX 不等于 0x10,则说明是EGA/VGA 显示卡。此时实际上我们
        // 可以使用32KB显示内存(0xb8000 -- 0xc0000),但该程序只使用了其中16KB显示内存。
		if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
		{
			video_type = VIDEO_TYPE_EGAC;       // 设置显示类型(EGA彩色)
			video_mem_end = 0xbc000;            // 设置显示内存末端地址
			display_desc = "EGAc";              // 设置显示描述字符串
		}
		else    // 如果 BX 寄存器的值等于 0x10,则说明是CGA显示卡,只使用8KB显示内存
		{
			video_type = VIDEO_TYPE_CGA;        // 设置显示类型(CGA彩色)
			video_mem_end = 0xba000;            // 设置显示内存末端地址
			display_desc = "*CGA";              // 设置显示描述字符串
		}
	}

	/* Let the user known what kind of display driver we are using */

    // 然后我们在屏幕的右上角显示描述字符串。采用的方法是直接将字符串写到显示内存
    // 相应位置处。首先将显示指针display_ptr 指到屏幕第1行右端差4个字符处(每个字符
    // 需2个字节,因此减8),然后循环复制字符串的字符,并且每复制1个字符都空开1个属性字节。
	display_ptr = ((char *)video_mem_start) + video_size_row - 8;
	while (*display_desc)
	{
		*display_ptr++ = *display_desc++;
		display_ptr++;                      // 空开属性字节
	}
	
	/* Initialize the variables used for scrolling (mostly EGA/VGA)	*/
	
	origin	= video_mem_start;              // 滚屏起始显示内存地址
	scr_end	= video_mem_start + video_num_lines * video_size_row;   // 结束地址
	top	= 0;                                // 最顶行号
	bottom	= video_num_lines;              // 最底行号

    // 最后初始化当前光标所在位置和光标对应的内存位置pos,并设置键盘中断0x21陷阱门
    // 描述符,&keyboard_interrupt是键盘中断处理过程地址。取消8259A中对键盘中断的
    // 屏蔽,允许响应键盘发出的IRQ1请求信号。最后复位键盘控制器以允许键盘开始正常工作。
	gotoxy(ORIG_X,ORIG_Y);
	set_trap_gate(0x21,&keyboard_interrupt);
	outb_p(inb_p(0x21)&0xfd,0x21);          // 取消对键盘中断的屏蔽,允许IRQ1。
	a=inb_p(0x61);                          // 读取键盘端口0x61(8255A端口PB)
	outb_p(a|0x80,0x61);                    // 设置禁止键盘工作(位7置位)
	outb(a,0x61);                           // 再允许键盘工作,用以复位键盘
}

代码很多,但是逻辑很简单。先从0x90000取到保存的参数(setup.s还在发力),取到显示器显示字符列数当前显示页面显示模式

在内存中存在图像视频缓冲区,其中单色显示器使用黑白区域,如果是彩色显示卡,则使用文本区域

con_init根据显示模式判断

  • 如果为彩色显示卡如果是则
    1.设置显示内存起始地址设置为0xb8000
    2.设置彩色显示索引寄存器端口和彩色显示数据寄存器端口为0x3d4和0x3d5
  • 如果为单色显示卡
    1.设置显示内存起始地址设置为0xb0000
    2.设置彩色显示索引寄存器端口和彩色显示数据寄存器端口为0x3b4和0x3b5

以下变量都是全局静态变量,可以被kernel/chr_drv/tty_io.c中任何函数使用

还有一些设置就不细看了

结束对显示参数的初始化后这部分设置了键盘的中断描述符和中断处理程序并取消对键盘的中断屏蔽和允许键盘工作,从这开始就可以用键盘输入了

        gotoxy(ORIG_X,ORIG_Y);
	set_trap_gate(0x21,&keyboard_interrupt);
	outb_p(inb_p(0x21)&0xfd,0x21);          // 取消对键盘中断的屏蔽,允许IRQ1。
	a=inb_p(0x61);                          // 读取键盘端口0x61(8255A端口PB)
	outb_p(a|0x80,0x61);                    // 设置禁止键盘工作(位7置位)
	outb(a,0x61);                           // 再允许键盘工作,用以复位键盘

在gotoxy函数中传入光标的X,Y坐标计算光标在对应显示缓冲区的地址

static inline void gotoxy(unsigned int new_x,unsigned int new_y)
{
    // 首先检查参数的有效性。如果给定的光标列号超出显示器列数,
    // 或者光标行号不低于显示的最大行数,则退出。否则就更新当前
    // 光标变量和新光标位置对应在显示内存中位置pos.
	if (new_x > video_num_columns || new_y >= video_num_lines)
		return;
	x=new_x;
	y=new_y;
	pos=origin + y*video_size_row + (x<<1);     // 1列用2个字节表示,x<<1.
}

键盘输入过程,在键盘中断处理程序中调用了do_tty_interrupt

keyboard_interrupt:
	。。。。。。。
	call do_tty_interrupt

在do_tty_interrupt调用了copy_to_cooked

void do_tty_interrupt(int tty)
{
	copy_to_cooked(tty_table+tty);
}

copy_to_cooked省略一万行中可以看到调用了一个tty结构体的write函数

void copy_to_cooked(struct tty_struct * tty)
{
              。。。。。。。
			tty->write(tty);
              。。。。。。。
}

tty的write函数一般都是con_write,kernel/chr_drv/console.c的con_write会向光标在对应显示缓冲区的地址写入对应字符。

void con_write(struct tty_struct * tty)
{
	
	__asm__("movb attr,%%ah\n\t"
		"movw %%ax,%1\n\t"
		::"a" (c),"m" (*(short *)pos)
		);
	pos += 2;
	x++;
}

在console.c中还有一系列函数

//定位光标
static inline void gotoxy(unsigned int new_x,unsigned int new_y)
//滚屏
static void scrup(void)
//将光标移动到下一列
static void lf(void)
//将光标移回第一列
static void cr(void)
。。。。。。

time_init

不感兴趣。。。有时间再看

sched_init

本来打算4/8写的,拖到4/14也是鸽了好久,终于到最期待的一个部分了。

sched_init定义在/kernel/sched.c中。

展开:sched_init
void sched_init(void)
{
	int i;
	struct desc_struct * p;                 // 描述符表结构指针

    // Linux系统开发之初,内核不成熟。内核代码会被经常修改。Linus怕自己无意中修改了
    // 这些关键性的数据结构,造成与POSIX标准的不兼容。这里加入下面这个判断语句并无
    // 必要,纯粹是为了提醒自己以及其他修改内核代码的人。
	if (sizeof(struct sigaction) != 16)         // sigaction 是存放有关信号状态的结构
		panic("Struct sigaction MUST be 16 bytes");
    // 在全局描述符表中设置初始任务(任务0)的任务状态段描述符和局部数据表描述符。
    // FIRST_TSS_ENTRY和FIRST_LDT_ENTRY的值分别是4和5,定义在include/linux/sched.h
    // 中;gdt是一个描述符表数组(include/linux/head.h),实际上对应程序head.s中
    // 全局描述符表基址(_gdt).因此gtd+FIRST_TSS_ENTRY即为gdt[FIRST_TSS_ENTRY](即为gdt[4]),
    // 也即gdt数组第4项的地址。
	set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
	set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
    // 清任务数组和描述符表项(注意 i=1 开始,所以初始任务的描述符还在)。描述符项结构
    // 定义在文件include/linux/head.h中。
	p = gdt+2+FIRST_TSS_ENTRY;
	for(i=1;i<NR_TASKS;i++) {
		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 */
    // NT标志用于控制程序的递归调用(Nested Task)。当NT置位时,那么当前中断任务执行
    // iret指令时就会引起任务切换。NT指出TSS中的back_link字段是否有效。
	__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");        // 复位NT标志
	ltr(0);
	lldt(0);
    // 下面代码用于初始化8253定时器。通道0,选择工作方式3,二进制计数方式。通道0的
    // 输出引脚接在中断控制主芯片的IRQ0上,它每10毫秒发出一个IRQ0请求。LATCH是初始
    // 定时计数值。
	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */
	outb(LATCH >> 8 , 0x40);	/* MSB */
    // 设置时钟中断处理程序句柄(设置时钟中断门)。修改中断控制器屏蔽码,允许时钟中断。
    // 然后设置系统调用中断门。这两个设置中断描述符表IDT中描述符在宏定义在文件
    // include/asm/system.h中。
	set_intr_gate(0x20,&timer_interrupt);
	outb(inb_p(0x21)&~0x01,0x21);
	set_system_gate(0x80,&system_call);
}

在开头执行了set_***_desc

#define FIRST_TSS_ENTRY 4
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));

在gdt表中第四位和第五位设置了ldt表和tss表,该ldt和tss保存的是init_task(初始进程)的idt和tss,每个进程对应一个ldt和tss

这两个函数都指向了都一个函数只是传入的参数不同。

#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \
	"movw %%ax,%2\n\t" \
	"rorl $16,%%eax\n\t" \
	"movb %%al,%3\n\t" \
	"movb $" type ",%4\n\t" \
	"movb $0x00,%5\n\t" \
	"movb %%ah,%6\n\t" \
	"rorl $16,%%eax" \
	::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
	 "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
	)

#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),((int)(addr)),"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),((int)(addr)),"0x82")

TSS即状态任务段,用于保存任务上下文,即在任务切换时保存寄存器信息。

随后将task1-64的位置和got表的6-255全部置NULL为后续进程的加载做准备。

#define NR_TASKS 64	
//struct task_struct * task[NR_TASKS] = {&(init_task.task), };
        p = gdt+2+FIRST_TSS_ENTRY;
	for(i=1;i<NR_TASKS;i++) {
		task[i] = NULL;
		p->a=p->b=0;
		p++;
		p->a=p->b=0;
		p++;
	}

其中task数值的类型为task_struct,该结构体保存的进程的各个信息,目前只有一个init_task进程。

struct task_struct {
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	long signal;
	struct sigaction sigaction[32];
	long blocked;	/* bitmap of masked signals */
/* various fields */
	int exit_code;
	unsigned long start_code,end_code,end_data,brk,start_stack;
	long pid,father,pgrp,session,leader;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	long alarm;
	long utime,stime,cutime,cstime,start_time;
	unsigned short used_math;
/* file system info */
	int tty;		/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	struct m_inode * executable;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];
/* tss for this task */
	struct tss_struct tss;
};

在后面复位NT位,使中断可以得到响应。

	__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");        // 复位NT标志

在汇编中tr,ldtr分别是指向TSS和ldt的寄存器,分别用ltr和lldt进行赋值,但在这里只是一个宏定义,最终调用了lldt和ltr汇编设置了正确的值。

ltr(0);
lldt(0);

#define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
#define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
#define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))

outb_p开启时钟中断,,set_intr_gate和set_system_gate分别用时钟中断中断处理程序系统调用中断处理程序设置了中断门和调用门

	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */
	outb(LATCH >> 8 , 0x40);	/* MSB */
    // 设置时钟中断处理程序句柄(设置时钟中断门)。修改中断控制器屏蔽码,允许时钟中断。
    // 然后设置系统调用中断门。这两个设置中断描述符表IDT中描述符在宏定义在文件
    // include/asm/system.h中。
	set_intr_gate(0x20,&timer_interrupt);
	outb(inb_p(0x21)&~0x01,0x21);
	set_system_gate(0x80,&system_call);

关于时钟中断

时钟中断与进程调度资源监控与统计内核定时任务有关

  • 进程调度:时钟中断强制任务发生上下文转换,防止某一进程站用cpu时间过长而发生的一系列调度。
  • 资源监控与统计:计算开机时长,CPU平均负载等信息
  • 内核定时任务:定时回收线程,检查设备等

buffer_init

buffer_init定义在fs/buffer.c中

展开:buffer_init
void buffer_init(long buffer_end)
{
	struct buffer_head * h = start_buffer;
	void * b;
	int i;

    // 首先根据参数提供的缓冲区高端位置确定实际缓冲区高端位置b。如果缓冲区高端等于1Mb,
    // 则因为从640KB - 1MB被显示内存和BIOS占用,所以实际可用缓冲区内存高端位置应该是
    // 640KB。否则缓冲区内存高端一定大于1MB。
	if (buffer_end == 1<<20)
		b = (void *) (640*1024);
	else
		b = (void *) buffer_end;
    // 这段代码用于初始化缓冲区,建立空闲缓冲区块循环链表,并获取系统中缓冲块数目。
    // 操作的过程是从缓冲区高端开始划分1KB大小的缓冲块,与此同时在缓冲区低端建立
    // 描述该缓冲区块的结构buffer_head,并将这些buffer_head组成双向链表。
    // h是指向缓冲头结构的指针,而h+1是指向内存地址连续的下一个缓冲头地址,也可以说
    // 是指向h缓冲头的末端外。为了保证有足够长度的内存来存储一个缓冲头结构,需要b所
    // 指向的内存块地址 >= h 缓冲头的末端,即要求 >= h+1.
	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;                   // 指向具有相同hash值的下一个缓冲头
		h->b_prev = NULL;                   // 指向具有相同hash值的前一个缓冲头
		h->b_data = (char *) b;             // 指向对应缓冲块数据块(1024字节)
		h->b_prev_free = h-1;               // 指向链表中前一项
		h->b_next_free = h+1;               // 指向连表中后一项
		h++;                                // h指向下一新缓冲头位置
		NR_BUFFERS++;                       // 缓冲区块数累加
		if (b == (void *) 0x100000)         // 若b递减到等于1MB,则跳过384KB
			b = (void *) 0xA0000;           // 让b指向地址0xA0000(640KB)处
	}
	h--;                                    // 让h指向最后一个有效缓冲块头
	free_list = start_buffer;               // 让空闲链表头指向头一个缓冲快
	free_list->b_prev_free = h;             // 链表头的b_prev_free指向前一项(即最后一项)。
	h->b_next_free = free_list;             // h的下一项指针指向第一项,形成一个环链
    // 最后初始化hash表,置表中所有指针为NULL。
	for (i=0;i<NR_HASH;i++)
		hash_table[i]=NULL;
}	

在main函数中将buffer_memory_end作为参数传进buffer_init

buffer_init(buffer_memory_end);

开头的h变量指向了end变量的地址,end为外部变量,是由ld进行写入的,由于在写代码的过程中不知道内核代码究竟占多大内存,于是就将该工作交给ld在链接的过程中确定内核代码的界限。

//extern int end;
//struct buffer_head * start_buffer = (struct buffer_head *) &end;
struct buffer_head * h = start_buffer;

如果buffer_end =1mb则b指向640kb处,640kb-1mb留给显存和BIOS使用,如大于1mb则b指向buffer_end,并在递减过程中到达1mb后自动指向640kb,后面会讲到。

	if (buffer_end == 1<<20)
		b = (void *) (640*1024);
	else
		b = (void *) buffer_end;

以1kb为单位对buffer分块,在一次循环内建立的两个结构体(buffer块(1kb)和buffer head(h))

#define BLOCK_SIZE 1024
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;                   // 指向具有相同hash值的下一个缓冲头
		h->b_prev = NULL;                   // 指向具有相同hash值的前一个缓冲头
		h->b_data = (char *) b;             // 指向对应缓冲块数据块(1024字节)
		h->b_prev_free = h-1;               // 指向链表中前一项
		h->b_next_free = h+1;               // 指向连表中后一项
		h++;                                // h指向下一新缓冲头位置
		NR_BUFFERS++;                       // 缓冲区块数累加
		if (b == (void *) 0x100000)         // 若b递减到等于1MB,则跳过384KB
			b = (void *) 0xA0000;           // 让b指向地址0xA0000(640KB)处
	}

主要逻辑如下

b -= BLOCK_SIZE

h->b_data = (char *) b;

h->b_prev_free = h-1; 

h->b_next_free = h+1;

得到了如图的结果,每个buffer头管理一个buffer块

链表头free_list 指向第一个buffer头并将第一个buffer头与最后一个链接形成双向链表。

	h--;                                    // 让h指向最后一个有效缓冲块头
	free_list = start_buffer;               // 让空闲链表头指向头一个缓冲快
	free_list->b_prev_free = h;             // 链表头的b_prev_free指向前一项(即最后一项)
	h->b_next_free = free_list;             // h的下一项指针指向第一项,形成一个环链

初始化hash表。

for (i=0;i<NR_HASH;i++)
    hash_table[i]=NULL;

该hash表是为了避免重复读取块设备,当读取块设备的时候先要去buffer块中寻找是否已经被读取到缓存中,利用

dev^block%307  (设备号^逻辑块号%307)

得到hash表中的索引,在遍历双向链表查找即可,双向链表+hash表=LRU算法

hd_init

硬盘初始化

void hd_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;      // do_hd_request()
	set_intr_gate(0x2E,&hd_interrupt);
	outb_p(inb_p(0x21)&0xfb,0x21);                      // 复位接联的主8259A int2的屏蔽位
	outb(inb_p(0xA1)&0xbf,0xA1);                        // 复位硬盘中断请求屏蔽位(在从片上)
}

因为存在很多块设备,则需要一个数值用于存储这些设备的信息,该数组即blk_dev,在index=3时正好是hd的块设备信息,将该index的request_fn 赋值为do_hd_request(),每个块设备进行读写请求时都会有自己的函数,第一句的赋值即初始化hd的读写操作函数为do_hd_request()

struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
	{ NULL, NULL },		/* no_dev */
	{ NULL, NULL },		/* dev mem */
	{ NULL, NULL },		/* dev fd */
	{ NULL, NULL },		/* dev hd */
	{ NULL, NULL },		/* dev ttyx */
	{ NULL, NULL },		/* dev tty */
	{ NULL, NULL }		/* dev lp */
};

设置硬盘读写的中断门

set_intr_gate(0x2E,&hd_interrupt);

允许硬盘控制器发送中断

outb_p(inb_p(0x21)&0xfb,0x21);   
outb(inb_p(0xA1)&0xbf,0xA1); 

一个新进程的诞生

Kernel_to_user

结束各个初始化后main函数还剩下几行

	move_to_user_mode();                    // 移到用户模式下执行
	if (!fork()) {		/* we count on this going ok */
		init();                             // 在新建的子进程(任务1)中执行。
	}
	for(;;) pause();

执行完初始化函数后CPU就从内核态变成用户态,iret弹出的顺序是rip->cs->rflags->rsp->,则下面cs寄存器的段选择子是0x0f,即0x00001111其中

  • CPL=0x11=3为用户态
  • TI=0x1,表示段描述符从ldt表中选择
  • index=0x00001
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
	"pushl $0x17\n\t" \
	"pushl %%eax\n\t" \
	"pushfl\n\t" \
	"pushl $0x0f\n\t" \
	"pushl $1f\n\t" \
	"iret\n" \
	"1:\tmovl $0x17,%%eax\n\t" \
	"movw %%ax,%%ds\n\t" \
	"movw %%ax,%%es\n\t" \
	"movw %%ax,%%fs\n\t" \
	"movw %%ax,%%gs" \
	:::"ax")

fork一个子进程后用子进程执行init函数,父进程则进入死循环

	if (!fork()) {		/* we count on this going ok */
		init();                             // 在新建的子进程(任务1)中执行。
	}
        for(;;) pause();

进程调度

保存状态

进程调度由时钟中断控制,在sched.c中设置了时钟中断的中断处理程序,在task_struct结构体中这几个成员与进程调度相关

struct task_struct {
	struct tss_struct tss;
};

其中tss为任务状态段,用于存储发生进程调度时本进程的寄存器状态,其中在tss中存在cr3寄存器,在进程调度中每更换一个进程cr3也会跟着变化,内存映射不相互干扰,这正是所谓的每个进程都有独自的页表

struct tss_struct {
	long	back_link;	/* 16 high bits zero */
	long	esp0;
	long	ss0;		/* 16 high bits zero */
	long	esp1;
	long	ss1;		/* 16 high bits zero */
	long	esp2;
	long	ss2;		/* 16 high bits zero */
	long	cr3;
	long	eip;
	long	eflags;
	long	eax,ecx,edx,ebx;
	long	esp;
	long	ebp;
	long	esi;
	long	edi;
	long	es;		/* 16 high bits zero */
	long	cs;		/* 16 high bits zero */
	long	ss;		/* 16 high bits zero */
	long	ds;		/* 16 high bits zero */
	long	fs;		/* 16 high bits zero */
	long	gs;		/* 16 high bits zero */
	long	ldt;		/* 16 high bits zero */
	long	trace_bitmap;	/* bits: trace 0, bitmap 16-31 */
	struct i387_struct i387;
};

进程时间片

counter即时间片,即每次时钟中断该进程最长能占用CPU的时间。

struct task_struct {
	long counter;
};

counter会随着占用CPU的时间而递减,每次触发时钟中断后会检查占用CPU的进程的counter是否为0,若为0则进行调度,不为0则继续运行。此事在shed.c的do_timer函数中亦有记载。

void do_timer(long cpl)
{
.......................................
    // 如果进程运行时间还没完,则退出。否则置当前任务计数值为0.并且若发生时钟中断
    // 正在内核代码中运行则返回,否则调用执行调度函数。
	if ((--current->counter)>0) return;
	current->counter=0;
	if (!cpl) return;                       // 内核态程序不依赖counter值进行调度
	schedule();
}

进程优先级

priority为优先级,表示进程能占用CPU的时长,优先级越高能占用CPU的时长就越多,counter在归零后都会被赋值为priority同等数值。

struct task_struct {
	long priority;
}

进程状态

state表示该进程的状态。

struct task_struct {
	long state;	
}

总所周知进程可以分为以下几种状态,但是只要

#define TASK_RUNNING		0
#define TASK_INTERRUPTIBLE	1
#define TASK_UNINTERRUPTIBLE	2
#define TASK_ZOMBIE		3
#define TASK_STOPPED		4

1. TASK_RUNNING (0)

  • 含义:进程正在运行就绪等待运行
  • 特点
    • 进程可能在 CPU 上执行,也可能在就绪队列中等待被调度。
    • 是进程的活跃状态,随时可以被操作系统分配 CPU 时间片。
  • 常见场景:用户程序正常执行时的状态。

2. TASK_INTERRUPTIBLE (1)

  • 含义:进程处于可中断的睡眠状态
  • 特点
    • 进程在等待某个事件(如 I/O 完成、信号量释放等)。
    • 可以被信号(Signal)唤醒,比如用户按下 Ctrl+C
  • 常见场景:读取键盘输入、等待网络数据等。

3. TASK_UNINTERRUPTIBLE (2)

  • 含义:进程处于不可中断的睡眠状态
  • 特点
    • 进程在等待某些关键资源(如硬件 I/O 操作),不能被信号唤醒
    • 操作系统会强制进程等待,直到资源就绪。
    • ps 命令中显示为 D 状态,可能表明潜在问题(如硬件故障)。
  • 常见场景:磁盘 I/O 操作、内核关键路径操作。

4. TASK_ZOMBIE (3)

  • 含义:进程处于僵尸状态
  • 特点
    • 进程已终止,但其退出状态尚未被父进程读取(通过 wait() 系统调用)。
    • 内核保留进程描述符(PID、退出码等),直到父进程回收。
    • 长期存在的僵尸进程可能表明程序逻辑缺陷。
  • 常见场景:父进程未正确处理子进程退出。

5. TASK_STOPPED (4)

  • 含义:进程被暂停执行
  • 特点
    • 通常由信号(如 SIGSTOPSIGTSTP)触发,可通过 SIGCONT 信号恢复运行。
    • 在调试器中暂停进程时会进入此状态。
  • 常见场景:用户按下 Ctrl+Z 暂停进程、调试器中断进程。

进程调度详细过程

从时钟中断的中断处理函数入手。主要的部分如下,该处理函数会将jiffies(系统滴答数)+1后调用do_timer。

timer_interrupt:
        ........................
	incl jiffies
        ........................
	call do_timer		
	........................

do_timer判断当前进程的counter,不为0直接返回不进行调度,为0则执行schedule调度

void do_timer(long cpl)
{
        ..........
	if ((--current->counter)>0) return;
	current->counter=0;
	if (!cpl) return;                       // 内核态程序不依赖counter值进行调度
	schedule();
}

schedule函数

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
        // 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较
        // 每个就绪状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还
        // 不长,next就值向哪个的任务号。
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
        // 如果比较得出有counter值不等于0的结果,或者系统中没有一个可运行的任务存在(此时c
        // 仍然为-1,next=0),则退出while(1)_的循环,执行switch任务切换操作。否则就根据每个
        // 任务的优先权值,更新每一个任务的counter值,然后回到while(1)循环。counter值的计算
        // 方式counter=counter/2 + priority.注意:这里计算过程不考虑进程的状态。
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
    // 用下面的宏把当前任务指针current指向任务号Next的任务,并切换到该任务中运行。上面Next
    // 被初始化为0。此时任务0仅执行pause()系统调用,并又会调用本函数。
	switch_to(next);     // 切换到Next任务并运行。
}

在task数组中寻找处于TASK_RUNNING 并且counter不为0的task

while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}

如果找到就退出(break),未找到则代表所有task的counter都为0了,重置所有task的counter并再次进行上述操作寻找可用task。

        if (c) break;
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p)
	        (*p)->counter = ((*p)->counter >> 1) +(*p)->priority;

将找到可用进程的下标index传进switch_to并进行调用。

switch_to(next); 

switch_to函数如下,定义了一个结构体64位__tmp分为高32位 a,低32位 b。

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,current\n\t" \
	"je 1f\n\t" \
	"movw %%dx,%1\n\t" \
	"xchgl %%ecx,current\n\t" \
	"ljmp *%0\n\t" \
	"cmpl %%ecx,last_task_used_math\n\t" \
	"jne 1f\n\t" \
	"clts\n" \
	"1:" \
	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
	"d" (_TSS(n)),"c" ((long) task[n])); \
}

rdx存储着对应下标进程的tss,rcx存储着对应进程的task_struct指针

在sched_init()函数中我们提到init进程,其中current(当前进程)则指向init。

struct task_struct *current = &(init_task.task);

无论是正在运行的进程还是在等待队列的进程都属于TASK_RUNNING状态,故先比较当前选中的进程是否与当前正在运行的进程一样,若一样则不进行调度。

cmpl %%ecx,current
je 1f

若不一样,则将该进程的tss段选择子(rdx)放入__tmp的低16位中,高32位全为0,并将current指向要调度的进程。

        movw %%dx,%1
	xchgl %%ecx,current
	ljmp *%0

ljmp(长跳转指令)在 x86 架构中的作用是改变代码段寄存器(CS)和指令指针(EIP)的值,实现跨代码段的跳转。ljmp会将64位的__tmp当作48位使用即(cs(16位):rip(32位)),将会把__tmp.a视为rip,__tmp.b视为cs长跳转。

当ljmp的操作数存放原本CS段选择子的是一个TSS描述符时,cpu会无视rip的部分,将当前寄存器的状态全部保存到当前进程的TSS段并加载要调度进程的TSS描述符到对应寄存器

最后几行判断该进程是否调用数学库,若是则将cr0第三位(TF位),该位用于执行浮点指令(如 x87 FPU、SSE 等),为了避免混乱,进程调度会将其置0

cmpl %%ecx,last_task_used_math
jne 1f
clts

通过fork函数看系统调用

经过main.c的初始化和从内核态变成用户态后,main使用fork创建子进程来调用init函数。

	if (!fork()) {		/* we count on this going ok */
		init();                             // 在新建的子进程(任务1)中执行。
	}

我们可以在main.c开头看到有个内联函数_syscall0

static inline _syscall0(int,fork)

_syscall0其实是在unistd.h下的一个宏定义。

#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; \
}

该宏定义将传入的返回值类型,函数名称构造成一个函数即

int fork(void)
{
long __res; 
__asm__ volatile ("int $0x80" 
	: "=a" (__res) \
	: "0" (__NR_##name)); 
if (__res >= 0) \
	return (type) __res; 
errno = -__res; 
return -1; 
}

##简化后

int fork(void)
{
long __res; 
__asm__ volatile (
"mov eax,__NR_fork"
"int $0x80" 
"mov __res,eax"

if (__res >= 0) \
	return (type) __res; 
errno = -__res; 
return -1; 
}

在unistd.h开头定义了很多系统调用号,根据传进来的名称进行替换。其中__NR_fork=2

展开:系统调用号
#define __NR_setup	0	/* used only by init, to get system going */
#define __NR_exit	1
#define __NR_fork	2
#define __NR_read	3
#define __NR_write	4
#define __NR_open	5
#define __NR_close	6
#define __NR_waitpid	7
#define __NR_creat	8
#define __NR_link	9
#define __NR_unlink	10
#define __NR_execve	11
#define __NR_chdir	12
#define __NR_time	13
#define __NR_mknod	14
#define __NR_chmod	15
#define __NR_chown	16
#define __NR_break	17
#define __NR_stat	18
#define __NR_lseek	19
#define __NR_getpid	20
#define __NR_mount	21
#define __NR_umount	22
#define __NR_setuid	23
#define __NR_getuid	24
#define __NR_stime	25
#define __NR_ptrace	26
#define __NR_alarm	27
#define __NR_fstat	28
#define __NR_pause	29
#define __NR_utime	30
#define __NR_stty	31
#define __NR_gtty	32
#define __NR_access	33
#define __NR_nice	34
#define __NR_ftime	35
#define __NR_sync	36
#define __NR_kill	37
#define __NR_rename	38
#define __NR_mkdir	39
#define __NR_rmdir	40
#define __NR_dup	41
#define __NR_pipe	42
#define __NR_times	43
#define __NR_prof	44
#define __NR_brk	45
#define __NR_setgid	46
#define __NR_getgid	47
#define __NR_signal	48
#define __NR_geteuid	49
#define __NR_getegid	50
#define __NR_acct	51
#define __NR_phys	52
#define __NR_lock	53
#define __NR_ioctl	54
#define __NR_fcntl	55
#define __NR_mpx	56
#define __NR_setpgid	57
#define __NR_ulimit	58
#define __NR_uname	59
#define __NR_umask	60
#define __NR_chroot	61
#define __NR_ustat	62
#define __NR_dup2	63
#define __NR_getppid	64
#define __NR_getpgrp	65
#define __NR_setsid	66
#define __NR_sigaction	67
#define __NR_sgetmask	68
#define __NR_ssetmask	69
#define __NR_setreuid	70
#define __NR_setregid	71

进入到int 0x80 syscall的中断处理函数中,两行重要的代码如下,该代码调用了sys_call_table[eax]的函数

system_call:
	call sys_call_table(,%eax,4)        # 间接调用指定功能C函数
	pushl %eax                          # 把系统调用返回值入栈

sys_call_table定义如下,该数组定义了很多函数,我们可以看到index=2时的函数为sys_fork即fork的内部实现

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };

sys_fork在system_calls.s中定义

sys_fork:
	call find_empty_process
	testl %eax,%eax             # 在eax中返回进程号pid。若返回负数则退出。
	js 1f
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax
	call copy_process
	addl $20,%esp               # 丢弃这里所有压栈内容。
1:	ret

fork的调用如下

fork->int80(2,0,0)->sys_call_table[2](sys_fork)

sys_fork的实现等下再写,先看看_syscallx,可以看到int 0x80的参数顺序如下

  • arg1->rbx
  • arg2->rcx
  • arg3->rdx
#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; \
}

#define _syscall2(type,name,atype,a,btype,b) \
type name(atype a,btype b) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \
if (__res >= 0) \
	return (type) __res; \
errno = -__res; \
return -1; \
}

#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
	return (type) __res; \
errno=-__res; \
return -1; \
}

fork实现

sys_fork代码如下

sys_fork:
	call find_empty_process
	testl %eax,%eax             # 在eax中返回进程号pid。若返回负数则退出。
	js 1f
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax
	call copy_process
	addl $20,%esp               # 丢弃这里所有压栈内容。
1:	ret

find_empty_process定义在fork.c中,用于获取空闲的pid和task数组中空闲位置的index

int find_empty_process(void)
{
	int i;

    // 首先获取新的进程号。如果last_pid增1后超出进程号的整数表示范围,则重新从1开始
    // 使用pid号。然后在任务数组中搜索刚设置的pid号是否已经被任何任务使用。如果是则
    // 跳转到函数开始出重新获得一个pid号。接着在任务数组中为新任务寻找一个空闲项,并
    // 返回项号。last_pid是一个全局变量,不用返回。如果此时任务数组中64个项已经被全部
    // 占用,则返回出错码。
	repeat:
		if ((++last_pid)<0) last_pid=1;
		for(i=0 ; i<NR_TASKS ; i++)
			if (task[i] && task[i]->pid == last_pid) goto repeat;
	for(i=1 ; i<NR_TASKS ; i++)         // 任务0项被排除在外
		if (!task[i])
			return i;
	return -EAGAIN;
}

将寄存器推入栈后进入copy_process函数

展开:copy_process源码
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx,
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;

    // 首先为新任务数据结构分配内存。如果内存分配出错,则返回出错码并退出。
    // 然后将新任务结构指针放入任务数组的nr项中。其中nr为任务号,由前面
    // find_empty_process()返回。接着把当前进程任务结构内容复制到刚申请到
    // 的内存页面p开始处。
	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
    // 随后对复制来的进程结构内容进行一些修改,作为新进程的任务结构。先将
    // 进程的状态置为不可中断等待状态,以防止内核调度其执行。然后设置新进程
    // 的进程号pid和父进程号father,并初始化进程运行时间片值等于其priority值
    // 接着复位新进程的信号位图、报警定时值、会话(session)领导标志leader、进程
    // 及其子进程在内核和用户态运行时间统计值,还设置进程开始运行的系统时间start_time.
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;              // 新进程号。也由find_empty_process()得到。
	p->father = current->pid;       // 设置父进程
	p->counter = p->priority;       // 运行时间片值
	p->signal = 0;                  // 信号位图置0
	p->alarm = 0;                   // 报警定时值(滴答数)
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;        // 用户态时间和和心态运行时间
	p->cutime = p->cstime = 0;      // 子进程用户态和和心态运行时间
	p->start_time = jiffies;        // 进程开始运行时间(当前时间滴答数)
    // 再修改任务状态段TSS数据,由于系统给任务结构p分配了1页新内存,所以(PAGE_SIZE+
    // (long)p)让esp0正好指向该页顶端。ss0:esp0用作程序在内核态执行时的栈。另外,
    // 每个任务在GDT表中都有两个段描述符,一个是任务的TSS段描述符,另一个是任务的LDT
    // 表描述符。下面语句就是把GDT中本任务LDT段描述符和选择符保存在本任务的TSS段中。
    // 当CPU执行切换任务时,会自动从TSS中把LDT段描述符的选择符加载到ldtr寄存器中。
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;     // 任务内核态栈指针。
	p->tss.ss0 = 0x10;                      // 内核态栈的段选择符(与内核数据段相同)
	p->tss.eip = eip;                       // 指令代码指针
	p->tss.eflags = eflags;                 // 标志寄存器
	p->tss.eax = 0;                         // 这是当fork()返回时新进程会返回0的原因所在
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;                // 段寄存器仅16位有效
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);                  // 任务局部表描述符的选择符(LDT描述符在GDT中)
	p->tss.trace_bitmap = 0x80000000;       // 高16位有效
    // 如果当前任务使用了协处理器,就保存其上下文。汇编指令clts用于清除控制寄存器CRO中
    // 的任务已交换(TS)标志。每当发生任务切换,CPU都会设置该标志。该标志用于管理数学协
    // 处理器:如果该标志置位,那么每个ESC指令都会被捕获(异常7)。如果协处理器存在标志MP
    // 也同时置位的话,那么WAIT指令也会捕获。因此,如果任务切换发生在一个ESC指令开始执行
    // 之后,则协处理器中的内容就可能需要在执行新的ESC指令之前保存起来。捕获处理句柄会
    // 保存协处理器的内容并复位TS标志。指令fnsave用于把协处理器的所有状态保存到目的操作数
    // 指定的内存区域中。
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    // 接下来复制进程页表。即在线性地址空间中设置新任务代码段和数据段描述符中的基址和限长,
    // 并复制页表。如果出错(返回值不是0),则复位任务数组中相应项并释放为该新任务分配的用于
    // 任务结构的内存页。
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
    // 如果父进程中有文件是打开的,则将对应文件的打开次数增1,因为这里创建的子进程会与父
    // 进程共享这些打开的文件。将当前进程(父进程)的pwd,root和executable引用次数均增1.
    // 与上面同样的道理,子进程也引用了这些i节点。
	for (i=0; i<NR_OPEN;i++)
		if ((f=p->filp[i]))
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
    // 随后GDT表中设置新任务TSS段和LDT段描述符项。这两个段的限长均被设置成104字节。
    // set_tss_desc()和set_ldt_desc()在system.h中定义。"gdt+(nr<<1)+FIRST_TSS_ENTRY"是
    // 任务nr的TSS描述符项在全局表中的地址。因为每个任务占用GDT表中2项,因此上式中
    // 要包括'(nr<<1)'.程序然后把新进程设置成就绪态。另外在任务切换时,任务寄存器tr由
    // CPU自动加载。最后返回新进程号。
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	return last_pid;
}

首先copy_process函数调用了get_free_page获取了一个空闲页面,在men_init中我们提到过。

p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;

将得到的内存当作task_struct 结构体赋值

p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;              // 新进程号。也由find_empty_process()得到。
	p->father = current->pid;       // 设置父进程
	p->counter = p->priority;       // 运行时间片值
	p->signal = 0;                  // 信号位图置0
	p->alarm = 0;                   // 报警定时值(滴答数)
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;        // 用户态时间和和心态运行时间
	p->cutime = p->cstime = 0;      // 子进程用户态和和心态运行时间
	p->start_time = jiffies;        // 进程开始运行时间(当前时间滴答数)
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;     // 任务内核态栈指针。
	p->tss.ss0 = 0x10;                      // 内核态栈的段选择符(与内核数据段相同)
	p->tss.eip = eip;                       // 指令代码指针
	p->tss.eflags = eflags;                 // 标志寄存器
	p->tss.eax = 0;                         // 这是当fork()返回时新进程会返回0的原因所在
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;                // 段寄存器仅16位有效
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);                  // 任务局部表描述符的选择符(LDT描述符在GDT中)
	p->tss.trace_bitmap = 0x80000000;       // 高16位有效

值得注意的是,在tss赋值的时候,esp0=页面顶端,ss0=内核ss段选择子。

p->tss.esp0 = PAGE_SIZE + (long) p;     // 任务内核态栈指针。
p->tss.ss0 = 0x10;                      // 内核态栈的段选择符(与内核数据段相同)

这正是所说的每个进程都各自的内核态栈

此作者没有提供个人介绍。
最后更新于 2025-04-17