Linux内核阅读2·1号进程(1)

博客主要为《Linux内核设计的艺术》(以下简称《设计艺术》)和《Linux内核完全注释》(以下简称《完全注释》),以及非常好的Linux内核视频 - Linux内核精讲内容的搬运和阅读笔记,以及相关博客链接的整理。代码来源于《完全注释》配套代码。
写着玩儿的,如有错误,欢迎指正。

中断

在这里插入图片描述

中断的一种分类,硬件中断又可分为NMI(INT 2),INT32–INT47为硬件中断(见《完全注释》P159)。

kernel里与中断相关的文件:
在这里插入图片描述

.s负责中断前的处理过程和中断后的回复过程,并调用.c文件中的函数。.c文件则为中断处理函数。

asm.s负责硬件中断,system_call.s负责软中断。

asm.s

感觉没啥好说的,简单执行了压栈等操作

no_error_code: ;// 这里是无出错号处理的入口处,见下面第55 行等。
	xchg [esp],eax ;// _do_divide_error 的地址 -> eax,eax 被交换入栈。
	push ebx
	push ecx
	push edx
	push edi
	push esi
	push ebp
	push ds ;// !!16 位的段寄存器入栈后也要占用4 个字节。
	push es
	push fs
	push 0 ;// "error code" ;// 将出错码入栈。
	lea edx,[esp+44] ;// 取原调用返回地址处堆栈指针位置,并压入堆栈。
	push edx
	mov edx,10h ;// 内核代码数据段选择符。
	mov ds,dx
	mov es,dx
	mov fs,dx
	call eax ;// 调用C 函数do_divide_error()。
	add esp,8 ;// 让堆栈指针重新指向寄存器fs 入栈处。
	pop fs
	pop es
	pop ds
	pop ebp
	pop esi
	pop edi
	pop edx
	pop ecx
	pop ebx
	pop eax ;// 弹出原来eax 中的内容。
	iretd

分为有错误码和无错误码,上面的代码是无错误码,有错误码差不多,不过是压栈的内容不一样:
在这里插入图片描述

上图我觉得有点误导,堆栈中存放的就是原eax的值,不过xchg [esp],eax后,eax存放着函数的入口。

当然,8086出现后还会压入r0-r15。

异常处理函数主要在trap.c中,不过貌似主要只是执行printk(栈中寄存器信息)。

system_call.s

在include\linux\sys.h文件中,有一个sys_call_table(系统调用表),包含了全部的系统调用类型。

_system_call函数长这样:

align 4
reschedule:
	push ret_from_sys_call ;// 将ret_from_sys_call 的地址入栈(101 行)。
	jmp _schedule
; int 0x80 --linux 系统调用入口点(调用中断int 0x80,eax 中是调用号)。
align 4
_system_call:
	cmp eax,nr_system_calls-1 ;// 调用号如果超出范围的话就在eax 中置-1 并退出。
	ja bad_sys_call
	push ds ;// 保存原段寄存器值。
	push es
	push fs
	push edx ;// ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。
	push ecx ;// push %ebx,%ecx,%edx as parameters
	push ebx ;// to the system call
	mov edx,10h ;// set up ds,es to kernel space
	mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
	mov es,dx
	mov edx,17h ;// fs points to local data space
	mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个
;// 系统调用C 处理函数的地址数组表。
	call [_sys_call_table+eax*4]
	push eax ;// 把系统调用号入栈。
	mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下面97-100 行查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执行调度程序。
	cmp dword ptr [state+eax],0 ;// state
	jne reschedule
	cmp dword ptr [counter+eax],0 ;// counter
	je reschedule

它的调用过程如下图:
在这里插入图片描述

asm.s中其他子函数都是调用no_error_code的,而system_call.s中是system_call调用其他子函数。《设计艺术》强调:
在这里插入图片描述

看一下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)

果然是堆栈中的数据(众所周知,参数表是通过栈来传递的),值得注意的是,eip~ss寄存器也在栈中。

ret_from_sys_call 部分暂时跳过,貌似与信号量的设定有关,以后来填坑。

首先是寻找空白页存放任务结构体:

p = (struct task_struct *) get_free_page ();	// 为新任务数据结构分配内存。

get_free_page ()的定义在mm/memory.c里,说白了就是弄了个mem_map数组,记录每个页有没有被占用,从后往前遍历一遍,有空闲页就返回起始地址(这里的地址转化必须有)。

下面就是寄存器的复制,不过有意思的是,eip~ss寄存器是在INT 80的时候压进栈的,即EIP指向的是/include/unistd.h文件里fork()的下一句(if_res>0),而eax则决定这个分支的走向。

p_i387 = &p->tss.i387;后面是协处理器部分。

if (copy_mem (nr, p))
{				// 返回不为0 表示出错。
	task[nr] = NULL;
	free_page ((long) p);
	return -EAGAIN;
}

这里开始分配页面。

首先需要明确线性地址的分配:
在这里插入图片描述

除了0号(640KB)进程以外,每个进程空间为64MB,基地址为进程号nr*64M。我认为上面这张图里段限长640KBd的位置貌似应该在进程0处。

// 设置新任务的代码和数据段基址、限长并复制页表。
// nr 为新任务号;p 是新任务数据结构的指针。
int copy_mem (int nr, struct task_struct *p)
{
	unsigned long old_data_base, new_data_base, data_limit;
	unsigned long old_code_base, new_code_base, code_limit;

	code_limit = get_limit (0x0f);	// 取局部描述符表中代码段描述符项中段限长。
	data_limit = get_limit (0x17);	// 取局部描述符表中数据段描述符项中段限长。
	old_code_base = get_base (current->ldt[1]);	// 取原代码段基址。
	old_data_base = get_base (current->ldt[2]);	// 取原数据段基址。
	if (old_data_base != old_code_base)	// 0.11 版不支持代码和数据段分立的情况。
		panic ("We don't support separate I&D");
	if (data_limit < code_limit)	// 如果数据段长度 < 代码段长度也不对。
		panic ("Bad data_limit");
	new_data_base = new_code_base = nr * 0x4000000;	// 新基址=任务号*64Mb(任务大小)。
	p->start_code = new_code_base;
	set_base (p->ldt[1], new_code_base);	// 设置代码段描述符中基址域。
	set_base (p->ldt[2], new_data_base);	// 设置数据段描述符中基址域。
	if (copy_page_tables (old_data_base, new_data_base, data_limit))
    {				// 复制代码和数据段。
		free_page_tables (new_data_base, data_limit);	// 如果出错则释放申请的内存。
		return -ENOMEM;
    }
	return 0;
}

首先get_limit(/include/linux/sched.h)的定义就有一定问题,据参考博客1所说,应该是lsll指令来获取的,但lsll指令的具体原理我并没有查到,只能认为是从当前进程的ldt中的段描述符中提取限长。

get_base 就是取基址,回想一下进程0的初始化,基址为0(INIT_TASK)。

copy_page_tables (old_data_base, new_data_base, data_limit)的具体实现在/mm/memory.c中,说白了就是两层for循环模拟了一下。

首先讨论一个基础问题,两层页表的意义?比如目前只有一个页,那么两层页表只要分配21024个页表项。(同理,在xv6里只要3512个)

另外,翻译地址本来是MMU的任务,为什么要有memory.c?我觉得可以参考xv6的回答:
在这里插入图片描述

另外,页表项中存放的是物理地址(get_base用汇编语言写的)。1号进程目前只是复制了0号进程的页表(0号进程的页表初始化在main.c里的mem_init函数中,但是《设计艺术》要到第6章才细讲)。目前只知道不同进程的页表应该是不一样的,这样可以实现不同进程的虚地址映射到不同的物理地址。

剩下的都比较直观:

// 如果父进程中有文件是打开的,则将对应文件的打开次数增1。
	for (i = 0; i < NR_OPEN; i++)
		if (f = p->filp[i])
			f->f_count++;
// 将当前进程(父进程)的pwd, root 和executable 引用次数均增1。
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
// 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。
// 在任务切换时,任务寄存器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;		// 返回新进程号(与任务号是不同的)。
}

现在,返回了_sys_fork之后,丢弃压栈内容,返回_system_call函数。

后面部分就难以前进了,先做做MIT6.S081,再继续往后读。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值