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
    评论
读核感悟-Linux内核启动-内核的生成...........................................2 读核感悟-Linux内核启动-从hello world说起...................................3 读核感悟-Linux内核启动-BIOS.................................................5 读核感悟-Linux内核启动-setup辅助程序........................................6 读核感悟-Linux内核启动-内核解压缩...........................................8 读核感悟-Linux内核启动-开启页面映射.........................................9 读核感悟-Linux内核启动-链接脚本............................................11 读核感悟-伪装现场-系统调用参数.............................................13 读核感悟-伪装现场-fork()系统调用...........................................15 读核感悟-伪装现场-内核线程:...............................................17 读核感悟-伪装现场-信号通信.................................................19 读核感悟-kbuild系统-内核模块的编译.........................................22 读核感悟-kbuild系统-编译到内核和编译成模块的区别...........................24 读核感悟-kbuild系统-make bzImage的过程.....................................26 读核感悟-kbuild系统-make menuconfig........................................31 读核感悟-文件系统-用C来实现面向对象........................................32 读核感悟-设计模式-用C来实现虚函数表和多态..................................32 读核感悟-设计模式-用C来实现继承和模板......................................33 读核感悟-设计模式-文件系统和设备的继承和接口...............................34 读核感悟-设计模式-文件系统与抽象工厂.......................................36 读核感悟-阅读源代码技巧-查找定义...........................................37 读核感悟-阅读源代码技巧-变量命名规则.......................................42 读核感悟-内存管理-内核中的页表映射总结.....................................43 读核感悟-健壮的代码-exception table-内核中的刑事档案.......................44 读核感悟-定时器-巧妙的定时器算法...........................................45 读核感悟-内存管理-page fault处理流程.......................................45 读核感悟-文件读写-select实现原理...........................................47 读核感悟-文件读写-poll的实现原理...........................................49 1 功能介绍:.............................................................49 2 关键的结构体:.........................................................49 3 poll的实现.............................................................49 4 性能分析:.............................................................50 读核感悟-文件读写-epoll的实现原理..........................................50 1 功能介绍...............................................................50 2 关键结构体:...........................................................51 3 epoll_create的实现.....................................................53 4 epoll_ctl的实现........................................................53 5 epoll_wait的实现.......................................................54 6 性能分析...............................................................54 读核感悟-同步问题-同步问题概述.............................................55 1 同步问题的产生背景.....................................................55 2 内核态与用户态的区别...................................................55 读核感悟-同步问题-内核态自旋锁的实现.......................................56 1自旋锁的总述............................................................56 2非抢占式的自旋锁........................................................56 3 锁的释放...............................................................57 4 与用户态的自旋锁的比较.................................................57 5 总结...................................................................58 读核感悟-内存管理-free命令详解.............................................58 读核感悟-文件读写-2.6.9内核中的AIO.........................................59 1 AIO概述................................................................59 2 内核态AIO的使用.......................................................61 读核感悟-文件读写-内核态AIO相关结构体......................................61 1 内核态AIO操作相关信息.................................................61 2 AIO上下文:............................................................63 3 AIO ring...............................................................63 4 异步I/O事件的返回信息.................................................64 读核感悟-文件读写-内核态AIO创建和提交操作..................................65 1 AIO上下文的创建-io_setup().............................................65 2 AIO请求的提交:io_submit实现机制......................................66 读核感悟-文件操作-AIO操作的执行............................................66 1.在提交时执行AIO........................................................66 2.在工作队列中执行AIO....................................................66 3.负责AIO执行的核心函数aio_run_iocb.....................................67 4 AIO操作的完成..........................................................67 读核感悟-文件读写-内核态是否支持非direct I/O方式的AIO.....................67
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值