linux内核 move_to_user_mode/fork

move to user mode

在这里插入图片描述

#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \  保存堆栈指针 esp 到 eax 寄存器中
	"pushl $0x17\n\t" \        首先将堆栈段选择符(SS)入栈。
	"pushl %%eax\n\t" \       然后将保存的堆栈指针值(esp)入栈。
	"pushfl\n\t" \               将标志寄存器(eflags)内容入栈
	"pushl $0x0f\n\t" \           将内核代码段选择符(cs)入栈
	"pushl $1f\n\t" \               将下面标号 1 的偏移地址(eip)入栈
	"iret\n" \                      执行中断返回指令,则会跳转到下面标号 1"1:\tmovl $0x17,%%eax\n\t" \      此时开始执行任务 0
	"movw %%ax,%%ds\n\t" \          初始化段寄存器指向本局部表的数据段
	"movw %%ax,%%es\n\t" \
	"movw %%ax,%%fs\n\t" \
	"movw %%ax,%%gs" \
	:::"ax")

首先解释指令iret.

iret 指令(interrupt return)中断返回,终端服务程序的最后一条指令。iret指令将推入堆栈的段地址和偏移地址弹出,使程序返回到原来中断发生的地方。它将产生以下三点效应:

1.恢复IP(instruction pointer):(IP)←((SP)+1:(SP)),

2.恢复CS(code segment):(CS)←(SP)←(SP)+2

3.恢复中断前的PSW(program status word),即恢复中断前的标志寄存器的状态。

以上操作按顺序进行。
  当使用IRET指令返回到相同保护级别的任务时,IRET会从堆栈弹出代码段选择子及指令指针分别到CS与IP寄存器,并弹出标志寄存器内容到EFLAGS寄存器。
  当使用IRET指令返回到一个不同的保护级别时,IRET不仅会从堆栈弹出以上内容,还会弹出堆栈段选择子及堆栈指针分别到SS与SP寄存器。
  
  因此,这段程序的大概意思是先将任务0所需要的各个寄存器的值压栈,压栈后执行IRET指令,利用该中断返回指令将各个寄存器设置为我们所理想的值。
  但是,程序中压入了几个常数,0x17,0x0f,1f是什么意思呢。
  分两类,0x17,0x0f是段选择子。段选择子用于保护模式下的寻址。0-1位表示请求的特权级,0表示系统级,3表示用户级。2位用于选择全局描述符表还是局部描述符表。
  3-15位是描述符表项索引。
  1f表示将下面标号1的程序段的偏移地址入栈。

fork 函数
main.c 中定义的fork应用层函数
static inline _syscall0(int,fork)
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \   系统调用进入sys_fork
	: "=a" (__res) \            __res 中断返回值           
	: "0" (__NR_##name)); \     //输入系统中断调用号__NR_fork (=2)
if (__res >= 0) \
	return (type) __res; \    //如果返回值>=0,则直接返回该值
errno = -__res; \           //否则出错号
return -1; \
}

在进入系统调用给的时候cpu会自动保存一些寄存器的值
在这里插入图片描述
接下来,程序跳转进入/kernel/system_call.S文件中system_call系统调用入口函数_system_call处执行。
关于系统调用这块代码请参考:系统调用代码流程
或者 深入分析fork的执行过程(Linux-0.11内核)

.align 2
_sys_fork:
	call _find_empty_process
	testl %eax,%eax
	js 1f
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax
	call _copy_process  # 调用 C 函数 copy_process()(kernel/fork.c,68)
	addl $20,%esp   # 丢弃这里所有压栈内容
1:	ret

在sys_fork中,首先调用find_empty_process函数取得不重复的进程号,其返回值在eax寄存器中;然后把gs,esi,edi,ebp,eax寄存器的值压栈,然后,以前面压入栈内的ss到eax(nr)。这些值为参数调用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;

	p = (struct task_struct *) get_free_page();  // 为新任务数据结构分配内存
	if (!p)
		return -EAGAIN;
	task[nr] = p;   // 将新任务结构指针放入任务数组中。
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	/* 注意!这样做不会复制超级用户的堆栈 */(只复制当前进程内容)
	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->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;   // 堆栈指针(由于是给任务结构 p 分配了 1 页
// 新内存,所以此时 esp0 正好指向该页顶端)
	p->tss.ss0 = 0x10;   // 堆栈段选择符(内核数据段)[??]。
	p->tss.eip = eip;   // 指令代码指针。
	p->tss.eflags = eflags;  // 标志寄存器。
	p->tss.eax = 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);   // 该新任务 nr 的局部描述符表选择符(LDT 的描述符在 GDT 中)
	p->tss.trace_bitmap = 0x80000000; 
	// 如果当前任务使用了协处理器,就保存其上下文。
	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。
	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++;
	// 设置新任务的 TSS 和 LDT 描述符项(在 GDT 中),数据从 task 结构中取
	// 在任务切换时,任务寄存器 tr 由 CPU 自动加载。局部描述符表寄存器 ldtr 已在 task0 时加载。
	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;
}

p->tss.eip = eip; //新的进程b的TSS里头的eip指向 syscall0中的 (if (__res >= 0) return (int) __res;)指令

p->tss.eax = 0; //新的进程b的TSS里头的eax赋值为0,当调度新进程运行时,新进程的syscall0中返回值__res = p->tss.eax = 0(即在新进程中fork返回的进程号为0)

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

容易忽略的地方:

	movl _current,%eax
	cmpl $0,state(%eax)		# state
	jne reschedule
	cmpl $0,counter(%eax)		# counter
	je reschedule

这些代码的意思是:先比较当前current,即子进程的状态是否可以运行,如果当前进程不再就绪状态就去执行调度程序,如果该任务在就绪状态但时间片用完了,也就执行调度程序。所有后续的情况是,我们无法确定进程0或者进程1先执行,但是返回值已经明显确定了。
a. 对于进程0(父进程)而言,接下来会执行ret_from_sys_call后的指令,进行system_call系统调用的退出和信号处理,其中call _do_signal后面的popl %eax 表示把 子进程号 出栈存到eax中,返回到syscall0时传递给__res,表示进程0(父进程)的fork返回的子进程号。
b. 对于进程1(子进程)而言,在schedule函数中调度到子进程运行时,由前面copy_process函数可知,子进程会返回到syscall0中if(__res >= 0) return (int) __res;)指令处,__res为 0,即子进程的fork返回0 ,其过程如下:

eax返回值一直为0.

.align 2
reschedule:
	pushl $ret_from_sys_call
	jmp _schedule

先把_ret_from_sys_call的地址压入栈,再跳转到schedule函数执行,在schedule函数最后会调用switch_to宏:

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \  // 比较当前任务current和要切换到的任务task[n]
	"je 1f\n\t" \					 // 如果要切换到的任务是当前任务,则跳到标号1,即结束,什么也不做,否则继续执行下面的代码
	"movw %%dx,%1\n\t" \			 // 把新任务的TSS选择符_TSS(n) 赋值给 __tmp.b的低16位
	"xchgl %%ecx,_current\n\t" \	 // 交换两个操作数的值,相当于C代码的:current = task[n] ,ecx = 被切换出去的任务(原任务);
	"ljmp %0\n\t" \					 // 长跳转到地址&__tmp.a中包含的48bit逻辑地址处:__tmp.a即为该逻辑地址的offset部分,
									 // __tmp.b的低16bit为seg_selector(高16bit无用)部分, 即切换到选择符_TSS(n)指定的的任务
										
	"cmpl %%ecx,_last_task_used_math\n\t" \   // 返回原进程后开始执行指令的地方。
	"jne 1f\n\t" \
	"clts\n" \
	"1:" \							 // 返回_ret_from_sys_call处
	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
	"d" (_TSS(n)),"c" ((long) task[n])); \
}

switch_to宏的核心是"ljmp %0\n\t"指令,它实现任务的切换。当它切换到子进程时,由于子进程的tss.eip指向syscall0中if(__res >= 0) return (int) __res;)指令处,且tss.eax = 0,所以子进程中fork会返回0值。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值