Linux0.11内核--fork进程分析

时间:2022-09-24 12:23:22

【版权所有,转载请注明出处。出处:http://www.cnblogs.com/joey-hua/p/5597818.html 】

据说安卓应用里通过fork子进程的方式可以防止应用被杀,大概原理就是子进程被杀会向父进程发送信号什么的,就不深究了。

首先fork()函数它是一个系统调用,在sys.h中:

extern int sys_fork ();		// 创建进程。 (kernel/system_call.s, 208)

// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, ...}

前面有文章对系统调用做过详细分析,main.c中的:

static inline _syscall0 (int, fork)

将__NR_fork也就是2和0x80中断绑定了,刚好对应的是上面数组的sys_fork函数,在system_call.s中:

#### sys_fork()调用,用于创建子进程,是system_call 功能2。原形在include/linux/sys.h 中。
# 首先调用C 函数find_empty_process(),取得一个进程号pid。若返回负数则说明目前任务数组
# 已满。然后调用copy_process()复制进程。
.align 2
_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来寻找任务数组中还未使用的编号,在fork.c中:

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

这个函数比较好理解,接下来看find_empty_process的返回值是保存在eax中,如果为负数则直接跳出sys_fork,否则push一堆指令,作为copy_process的参数,也在fork.c中:

/*
* OK,下面是主要的fork 子程序。它复制系统进程信息(task[n])并且设置必要的寄存器。
* 它还整个地复制数据段。
*/
// 复制进程。
// 其中参数nr 是调用find_empty_process()分配的任务数组项号。none 是system_call.s 中调用
// sys_call_table 时压入堆栈的返回地址。
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 分配了1 页新内存,所以此时esp0 正好指向该页顶端。ss0:esp0 用于作为程序
// 在内核态执行时的堆栈。
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; // 这是当fork()返回时,新进程会返回0 的原因所在。
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp; // 新进程完全复制了父进程的堆栈内容。因此要求task0
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 用于清除控制寄存器CR0 中的任务
// 已交换(TS)标志。每当发生任务切换,CPU 都会设置该标志。该标志用于管理数学协处理器:如果
// 该标志置位,那么每个ESC 指令都会被捕获。如果协处理器存在标志也同时置位的话那么就会捕获
// WAIT 指令。因此,如果任务切换发生在一个ESC 指令开始执行之后,则协处理器中的内容就可能需
// 要在执行新的ESC 指令之前保存起来。错误处理句柄会保存协处理器的内容并复位TS 标志。
// 指令fnsave 用于把协处理器的所有状态保存到目的操作数指定的内存区域中(tss.i387)。
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; // 返回新进程号(与任务号是不同的)。
}

这里有问题需要注意一下,为什么copy_process有那么多参数,而sys_fork才push了5个寄存器,这是因为根据系统调用机制,调用sys_fork之前是先调用的system_call函数,已经往栈压入了一堆寄存器,这就对应上了。

首先为新任务数据结构分配内存(注意这里是数据结构不是任务本身),get_free_page放在后面的内存管理文章分析,fork函数和内存管理memory.c是息息相关的。这里只要知道这个函数是获取到主内存区的一页空闲页面并返回这个页面的地址。

接下来的比较好理解,复制当前进程的进程描述符到新任务中,并对各个属性重新赋值。这里值得注意的是p->father = current->pid表示新任务的父进程就是当前进程。

接下来设置esp0指向刚新分配的页内存的顶端,ss0为内核数据段选择子,因为内核数据段描述符中的基址为0,所以ss0:esp0用作程序在内核态执行时的堆栈。

接下来p->tss.ldt = _LDT (nr);设置ldt的索引号,也就是LDT在GDT中的选择子。

下面是最关键的函数copy_mem:

// 设置新任务的代码和数据段基址、限长并复制页表。
// 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");
// 创建中新进程在线性地址空间中的基地址等于64MB * 其任务号。
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;
}

首先取局部描述符表(LDT自身的描述符表)中代码和数据段描述符中的限长,在sched.h中:

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

因为在进程描述符结构中有个

struct desc_struct ldt[3];// struct desc_struct ldt[3] 本任务的局部表描述符。0-空,1-代码段cs,2-数据和堆栈段ds&ss。

lsll是加载段界限的指令,即把segment段描述符中的段界限字段装入某个寄存器(这个寄存器与__limit结合),函数返回__limit加1,即段长。

这表示的是LDT描述符表自身,第一个描述符为空,第二个描述符也就是8-15字节是代码段,又因为描述符的0-15位是段限长,是从当前描述符的0位开始,所以取的是0x0f(第二个描述符的0位),然后第三个描述符也就是16-23字节是数据段,所以取0x17.

接下来是取当前进程的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;
} )
// 取局部描述符表中ldt 所指段描述符中的基地址。
#define get_base(ldt) _get_base( ((char *)&(ldt)) )

current->ldt[1]为当前进程的ldt的代码段描述符项的内容,所以这里就不难理解了,就是从描述符项的内容中提取基地址。

接下来设置新进程的线性地址的基地址,linus给每个程序(进程)划分了64MB的虚拟内存空间,所以新基址就是任务号*64MB。

再接着就是往新进程的LDT表中的段描述符设置基地址了,原理类似。

copy_page_tables和free_page_tables放到后面一篇讲解。

最后面是设置新任务的TSS和LDT描述符项,在进程调度的初始化中讲解过。

最后返回新进程号。

至此fork函数分析结束。

Linux0.11内核--fork进程分析的更多相关文章

  1. Linux0&period;11内核--系统调用机制分析

    [版权所有,转载请注明出处.出处:http://www.cnblogs.com/joey-hua/p/5570691.html ] Linux内核从启动到初始化也看了好些个源码文件了,这次看到kern ...

  2. Linux-0&period;11内核源代码分析系列:内存管理get&lowbar;free&lowbar;page&lpar;&rpar;函数分析

    Linux-0.11内存管理模块是源码中比較难以理解的部分,如今把笔者个人的理解发表 先发Linux-0.11内核内存管理get_free_page()函数分析 有时间再写其它函数或者文件的:) /* ...

  3. linux0&period;11内核源码剖析&colon;第一篇 内存管理、memory&period;c【转】

    转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July  ...

  4. Linux0&period;11内核源码——内核态线程&lpar;进程&rpar;切换的实现

    以fork()函数为例,分析内核态进程切换的实现 首先在用户态的某个进程中执行了fork()函数 fork引发中断,切入内核,内核栈绑定用户栈 首先分析五段论中的第一段: 中断入口:先把相关寄存器压栈 ...

  5. linux0&period;11内核源码——进程各状态切换的跟踪

    准备工作 1.进程的状态有五种:新建(N),就绪或等待(J),睡眠或阻塞(W),运行(R),退出(E),其实还有个僵尸进程,这里先忽略 2.编写一个样本程序process.c,里面实现了一个函数 /* ...

  6. linux0&period;11下的中断机制分析

    http://orbt.blog.163.com/     异常就是控制流中的突变,用来响应处理器状态中的某些变化.当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用, ...

  7. Linux0&period;11内核剖析--内核体系结构

    一个完整可用的操作系统主要由 4 部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如下图所示: 用户应用程序是指那些字处理程序. Internet 浏览器程序或用户自行编制的各种应用程序: ...

  8. LINUX0&period;11 内核阅读笔记

    一.源码目录 图1 二.系统总体流程: 系统从boot开始动作,把内核从启动盘装到正确的位置,进行一些基本的初始化,如检测内存,保护模式相关,建立页目录和内存页表,GDT表,IDT表.然后进入main ...

  9. linux0&period;11内核源码——用户级线程及内核级线程

    参考资料:哈工大操作系统mooc 用户级线程 1.每个进程执行时会有一套自己的内存映射表,即我们所谓的资源,当执行多进程时切换要切换这套内存映射表,即所谓的资源切换 2.但是如果在这个进程中创建线程, ...

随机推荐

  1. intellij idea Jdk编译设置

    Idea加载多项目时因为不同JDK,经常出现JDK编译版本的问题,容易出现以下异常. 一.异常信息: Information:Using javac 1.8.0_91 to compile java ...

  2. HTML5的form表单属性

    form:HTML4中,表单内的从属元素必须书写在<form></form>之内,但是在HTML5中,表单的从属元素可以处于页面的任何位置,然后为其添加form属性,属性值为f ...

  3. &period;net Web开发学习日志 —C&sol;S和B&sol;S结构区别

    查看到<C/S和B/S结构区别整理> B/S结构与C/S结构都是有各自的优缺点: 前者无需安装,只要有浏览器即可,随时随地查询相关的业务,业务扩展强,维护强,共享强.在跨浏览器较难,响应速 ...

  4. MMO之禅(三)职业能力

    MMO之禅(三)职业能力 --技术九层阶梯 Zephyr 201304 有了精神,我们还需要实际的行动. 到底需要什么能力?自我分析,窃以为为有九层,无所谓高低,因为每一层都需要不断地砥砺,编程,本身 ...

  5. win7 系统保留分区 BCDedit

    系统保留分区简介编辑 “系统保留”分区示意图 Windows Vista/7出于安全考虑,在新装Windows Vista/7系统过程中,如果利用光盘的分区工具给硬盘分区时,系统默认的将一部分(100 ...

  6. ELK安装与配置

    ELK介绍 日志主要包括系统日志.应用程序日志和安全日志.系统运维和开发人员可以通过日志了解服务器软硬件信息.检查配置过程中的错误及错误发生的原因.经常分析日志可以了解服务器的负荷,性能安全性,从而及 ...

  7. html&amp&semi;css笔记&lpar;2&rpar;

    第9章 用background-image属性为任何元素添加背景图片,用url()标识背景位置,它落在背景颜色之上,所以类似于背景颜色,它位于边框之内 p380 可以指定任一边框(上.下.左.右)的样 ...

  8. Bytom资产发行与部署合约教程

    比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 发行资产 ...

  9. &lpar;转)MySQL建表设置两个默认CURRENT&lowbar;TIMESTAMP的技巧

    业务场景: 例如用户表,我们需要建一个字段是创建时间, 一个字段是更新时间. 解决办法可以是指定插入时间,也可以使用数据库的默认时间. 在mysql中如果设置两个默认CURRENT_TIMESTAMP ...

  10. hdu1527取石子游戏&lpar;威佐夫博弈&rpar;

    取石子游戏 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submi ...