您的位置:首页 > 运维架构 > Linux

Linux 内核--fork()函数创建进程

2012-04-21 19:33 796 查看

Linux 内核--fork()函数创建进程

分类:
Linux内核游记 2011-06-05 22:24
464人阅读 评论(0)
收藏
举报

本文分析基于Linux 0.11内核,转载请表明出处http://blog.csdn.net/yming0221/archive/2011/06/05/6527337.aspx

Linux在move_to_user_mode()之后,进程0通过fork()产生子进程实际就是进程1(init进程)。

其中fork()是通过内嵌汇编的形式给出

[cpp]
view plaincopyprint?

#define _syscall0(type,name) /
type name(void) /
{ /
long __res; /
__asm__ volatile ( "int $0x80" / // 调用系统中断0x80。
:"=a" (__res) / // 返回值??eax(__res)。
:"" (__NR_##name)); / // 输入为系统中断调用号__NR_name。
if (__res >= 0) / // 如果返回值>=0,则直接返回该值。
return (type) __res; errno = -__res; / // 否则置出错号,并返回-1。
return -1;}

这样使用int 0x80中断,调用sys_fork系统调用来创建进程。详细过程如下:

系统在sched.c中sched_init()函数最后设置系统调用中断门

set_system_gate (0x80, &system_call);

设置系统调用的中断号。

通过int 0x80调用sys_fork()

其使用汇编实现

系统将堆栈的内容入栈,然后执行call _sys_call_table(,%eax,4)

调用地址 = _sys_call_table + %eax * 4

然后真正调用sys_fork()

[cpp]
view plaincopyprint?

_sys_fork:
call _find_empty_process # 调用find_empty_process()(kernel/fork.c,135)。
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

然后调用find_empty_process()

[cpp]
view plaincopyprint?

int find_empty_process (void)
{
int i;
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;
}

该函数设置last_pid为最后可用不重复的pid号,然后返回task[]数组中空闲的项的index,存放在EAX中。

再将相应的寄存器 入栈,作为C函数的参数,调用copy_process()

[cpp]
view plaincopyprint?

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; // 将新任务结构指针放入任务数组中。
// 其中nr 为任务号,由前面find_empty_process()返回。
*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))
{ // 返回不为0 表示出错。
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++;
// 在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; // 返回新进程号(与任务号是不同的)。
}

这段代码的执行内容是:首先为进程分配内存,然后将新任务的指针放入上步查到的空闲task[]数组项中,然后复制父进程的内容后修改当前

进程的一部分属性和tss(任务状态段),最后设置新进程的代码段和数据段,限长,在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;

这样,新进程就创建完毕了。

copy_mem(int nr, struct task_struct *p)函数是为进程设置段基址,限长,并复制页表。下面是其代码

[cpp]
view plaincopyprint?

// 设置新任务的代码和数据段基址、限长并复制页表。
// 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()函数是利用内嵌汇编取特定段描述符中段限长,其中用到指令lsll

[cpp]
view plaincopyprint?

// 取段选择符segment 的段长值。
// %0 - 存放段长值(字节数);%1 - 段选择符segment。
#define get_limit(segment) ({ /
unsigned long __limit; /
__asm__( "lsll %1,%0/n/tincl %0": "=r" (__limit): "r" (segment)); /
__limit;})

将指定描述符段的段限长返回,其中由于段限长是从0开始,所以在lsll之后需要增一。

至于ldt数据段描述符为什么是0x17,而ldt中代码段描述符是0x0f原因是段选择子的格式,一共16位,高13位表示描述符在描述符表的索引

[2]位表示这项是GDT还是LDT,0表示LDT;[1][0]表示RPL权限位。所以,0x17=0B0000 0000 0001 0111,其中10表示第二

项,0x0f=0B0000 0000 0000 1111,表示位于描述符表中的第一项。基地址在LDTR寄存器中。

get_base(addr)取描述符的中指向段的及地址,其宏定义如下:

[cpp]
view plaincopyprint?

// 取局部描述符表中ldt 所指段描述符中的基地址。
#define get_base(ldt) _get_base( ((char *)&(ldt)) )
// 从地址addr 处描述符中取段基地址。功能与_set_base()正好相反。
// edx - 存放基地址(__base);%1 - 地址addr 偏移2;%2 - 地址addr 偏移4;%3 - addr 偏移7。
#define _get_base(addr) ({/
unsigned long __base; /
__asm__( "movb %3,%%dh/n/t" / // 取[addr+7]处基址高16 位的高8 位(位31-24)??dh。
"movb %2,%%dl/n/t" / // 取[addr+4]处基址高16 位的低8 位(位23-16)??dl。
"shll $16,%%edx/n/t" / // 基地址高16 位移到edx 中高16 位处。
"movw %1,%%dx" / // 取[addr+2]处基址低16 位(位15-0)??dx。
:"=d" (__base) / // 从而edx 中含有32 位的段基地址。
:"m" (*((addr) + 2)), "m" (*((addr) + 4)), "m" (*((addr) + 7)));
__base;
}
)

下图表示描述符格式:



copy_page_tables()函数复制页表,据说是内存管理中最复杂函数之一,以后研究,待续........
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: