Back
Featured image of post RTFSC-Linux0.11 Part3: A new process

RTFSC-Linux0.11 Part3: A new process

Read the F**king Source Code

跟着大佬的文章读一下 Linux 0.11 的源码

https://github.com/sunym1993/flash-linux0.11-talk

https://github.com/beride/linux0.11-1

最近太摆了,拷打我自己

Overview

第二部分中了解了每个 init 函数,继续往后看 main

void main(void) {
    // init functions
    
    move_to_user_mode();
    if (!fork()) {
        init();
    }

    for(;;) pause();
}

后续只执行了四个函数

move_to_user_mode() 顾名思义就是将系统切换至用户态

fork() 函数是创建一个子进程,这个子进程将会执行 init() 函数,父进程随后将会进入一个死循环,不断重复执行 pause() 函数

切换到用户态

函数在 include/asm/system.h 文件中定义

#define move_to_user_mode()                                                                     \
_asm {                                                                                          \
    _asm mov eax, esp                                                                           \
    _asm push 0x00000017    /* 将堆栈段选择符(SS)入栈                                     */   \
    _asm push eax           /* 将保存的 esp 入栈                                           */   \
    _asm pushfd             /* 将 eflags 内容入栈                                          */   \
    _asm push 0x0000000f    /* 将内核代码段选择符(CS)入栈                                */   \
    _asm push offset l1     /* 将下面 LABEL l1 的偏移地址入栈                              */   \
    _asm iretd              /* 执行中断返回指令,根据上面这个 push,会跳转到下面的 LABEL l1 */  \
_asm l1: mov eax, 0x17      /* 开始执行任务 0                                              */   \
    _asm mov ds, ax         /* 初始化段寄存器指向本局部表的数据                             */  \
    _asm mov es, ax                                                                             \
    _asm mov fs, ax                                                                             \
    _asm mov gs, ax                                                                             \
}

在系统正常运行的时候,应当以用户态运行,通过中断进入内核态执行内核代码,再通过中断返回退出内核态,整体情况如下图

最常用的就是系统调用,通过 int 0x80 指令触发系统调用中断,再当前根据 ax 寄存器的值判断执行具体哪个中断,执行后进行中断返回

而这个中断返回指令,也就是上面这段代码中的 iretd 指令,而中断返回指令与普通返回指令的区别在于,不仅会 pop ip,还会从栈中恢复一些其他的寄存器,具体如下

low addr
+------------------+ <-  ss:esp
|    ERROR_CODE    |
+------------------+
|        EIP       |
+------------------+
|        CS        |
+------------------+
|      EFLAGS      |
+------------------+
|        ESP       |
+------------------+
|        SS        |
+------------------+
high addr

ERROR_CODE 只有有错误时才会压入、ESPSS 只有特权级发生变化时才会压入

所以在上面源码执行 iretd 指令的时候,会为 eip 及其他四个寄存器赋值,而将会赋的值就由上面的 5 个 push 指令决定

简单总结一下,执行完这部分指令后,进入了用户态,并且 cs=0b1111, ss=0b10111, ds=es=fs=gs=0b10111

最后两个 bit 就是特权级,11 表示用户态;倒数第三位中 0 表示用 GDT, 1 表示用 LDT;其余则表示具体哪个描述符。所以 cs 就是 ldt 的第一条(代码段描述符),其余描述符为 ldt 的第二条(数据段描述符)

至于什么是特权级:“数据访问只能高特权级访问低特权级,代码跳转只能同特权级跳转,要想实现特权级转换,可以通过中断和中断返回来实现”

Several things you need to know about fork

接下来几个小节都在聊 fork,而这个地方的设计则是充分体现了操作系统是如何进行进程调度的

Design

一个现代操作系统势必是要同时执行多个任务的,这就需要一个“第三方”的程序来专门进行调度(毕竟你不能要求每个程序自觉暂停),一个通用且简易的方案就是时间片流转算法,这一点可以用定时器的时钟中断实现

思路明确了,那么肯定需要为每个进程设计一个数据结构,方便进行切换

struct task_struct {
    // Some attrs
};

一、保存上下文

进程的切换和恢复,需要做的其实就是保存此前的状态,然后再从这个状态开始继续执行,所以这个结构体中必然要存储所有寄存器的信息,这件事情专门定义了一个新的结构体 tss_struct

struct task_struct {
    struct tss_struct tss;
    // Some other attrs
}

struct tss_struct {
    long    back_link;      /* 16 high bits zero */
    long    esp0;
    long    ss0;            /* 16 high bits zero */
    long    esp1;
    long    ss1;            /* 16 high bits zero */
    long    esp2;
    long    ss2;            /* 16 high bits zero */
    long    cr3;
    long    eip;
    long    eflags;
    long    eax,ecx,edx,ebx;
    long    esp;
    long    ebp;
    long    esi;
    long    edi;
    long    es;             /* 16 high bits zero */
    long    cs;             /* 16 high bits zero */
    long    ss;             /* 16 high bits zero */
    long    ds;             /* 16 high bits zero */
    long    fs;             /* 16 high bits zero */
    long    gs;             /* 16 high bits zero */
    long    ldt;            /* 16 high bits zero */
    long    trace_bitmap;   /* bits: trace 0, bitmap 16-31 */
    struct i387_struct i387;
};

二、运行时间

既然根据运行时间进行切换,肯定要记录一些相关的内容,这里采用的方法是存储一个“剩余时间片”的参数 counter,每次中断后就来一个自减,最后根据 if ((--current->counter) > 0) 的结果判断是否进行进程切换

struct task_struct {
    long counter;
    struct tss_struct tss;
    // Some other attrs
}

三、优先级

虽然确定了用 counter 的自减来进行进程的切换,但 counter 的初始值也会对进程调度产生影响,这里用一个 priority 来作为初始值

struct task_struct {
    long counter;
    long priority;
    struct tss_struct tss;
    // Some other attrs
};

四、进程状态

考虑到进程状态较多,包括运行、停止等,所以进程状态通常用一个状态机来完成调度,在结构体里使用 state 来保存当前状态,并在宏定义中为每个状态定义一个常量

#define TASK_RUNNING            0
#define TASK_INTERRUPTIBLE      1
#define TASK_UNINTERRUPTIBLE    2
#define TASK_ZOMBIE             3
#define TASK_STOPPED            4

struct task_struct {
    long state;
    long counter;
    long priority;
    struct tss_struct tss;
    // Some other attrs
}

结构体的源码在 include/linux/sched.h 中,剩下的参数直接用注释解释了

struct task_struct {
/* these are hardcoded - don't touch */
    long state; /* -1 unrunnable, 0 runnable, >0 stopped */
    long counter;
    long priority;
    long signal;        // bitmap 信号(每个 bit 代表一个信号),信号值=位偏移值+1
    struct sigaction sigaction[32]; // 信号执行属性结构,对应信号将要执行的操作和标志信息
    long blocked;   /* bitmap of masked signals */ // 信号屏蔽码
/* various fields */
    int exit_code;  // 任务执行停止的退出码,用于返回给父进程
    unsigned long start_code,end_code,end_data,brk,start_stack; // 分别为:代码段地址、代码长度、代码长度+数据长度、总长度、堆栈段地址
    long pid,father,pgrp,session,leader; // 分别为:进程标志号、父进程号、父进程组号、会话号、会话首领
    unsigned short uid,euid,suid;   // 用户id、有效用户id、保存的用户id
    unsigned short gid,egid,sgid;   // 组id、有效组id、保存的组id
    long alarm; // 报警计时器
    long utime,stime,cutime,cstime,start_time;  // 分别为:用户态运行时间,系统态运行时间,子进程用户态运行时间,子进程系统态运行时间,进程开始运行时间
    unsigned short used_math;   // 标志:是否使用了协处理器
/* file system info */
    int tty;        /* -1 if no tty, so it must be signed */
    unsigned short umask;   // 文件创建属性屏蔽位
    struct m_inode * pwd;   // 当前工作目录
    struct m_inode * root;  // 根目录
    struct m_inode * executable;    // 执行文件
    unsigned long close_on_exec;    // 执行时关闭文件句柄位图标志
    struct file * filp[NR_OPEN];    // 进程使用的文件表结构
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
    struct desc_struct ldt[3];  // 本人无的局部表描述符
/* tss for this task */
    struct tss_struct tss;
};

When running out of time

根据之前的初始化,计时器每 10ms 会向操作系统发送一次中断信号,中断向量号是 0x20,每次时钟中断,都会执行 kernel/system_call.s 中定义的中断处理函数

_timer_interrupt:
    push ds
    push es
    push fs
    push edx
    push ecx
    push ebx
    push eax
    mov eax, 10h
    mov ds, ax
    mov es, ax
    mov eax, 17h
    mov fs, ax
    inc dword ptr _jiffies
    mov al, 20h
    out 20h, al
    mov eax, dword ptr [R_CS+esp]
    and eax, 3
    push eax
    call _do_timer
    add esp, 4
    jmp ret_from_sys_call

开始的一些 push 主要是保存此前执行的程序的信息,随后通过赋值将段寄存器都修改到内核的段上

除去这些调度用的代码,核心功能主要是如下两句

    inc dword ptr _jiffies
    call _do_timer

jiffies 变量用于存储系统滴答数,进行 +1 的操作之后调用了 do_timer 函数,该函数位于 kernel/sched.c

long volatile jiffies; // 开机后的系统滴答数,volatile 要求 gcc 不对该变量进行优化

void do_timer(long cpl) {
    extern int beepcount;        // 扬声器发声时间滴答数(kernel/chr_drv/console.c,697)
    extern void sysbeepstop (void);    // 关闭扬声器(kernel/chr_drv/console.c,691)

  // 如果发声计数次数到,则关闭发声。(向0x61 口发送命令,复位位0 和1。位0 控制8253
  // 计数器2 的工作,位1 控制扬声器)。
    if (beepcount)
        if (!--beepcount)
            sysbeepstop ();

  // 如果当前特权级(cpl)为0(最高,表示是内核程序在工作),则将超级用户运行时间stime 递增;
  // 如果cpl > 0,则表示是一般用户程序在工作,增加utime。
    if (cpl)
        current->utime++;
    else
        current->stime++;

// 如果有用户的定时器存在,则将链表第1 个定时器的值减1。如果已等于0,则调用相应的处理
// 程序,并将该处理程序指针置为空。然后去掉该项定时器。
    if (next_timer)
    {                // next_timer 是定时器链表的头指针(见270 行)。
        next_timer->jiffies--;
        while (next_timer && next_timer->jiffies <= 0)
        {
            void (*fn) ();    // 这里插入了一个函数指针定义!!!??

            fn = next_timer->fn;
            next_timer->fn = NULL;
            next_timer = next_timer->next;
            (fn) ();        // 调用处理函数。
        }
    }
// 如果当前软盘控制器FDC 的数字输出寄存器中马达启动位有置位的,则执行软盘定时程序(245 行)。
    if (current_DOR & 0xf0)
        do_floppy_timer ();
    if ((--current->counter) > 0)
        return;            // 如果进程运行时间还没完,则退出。
    current->counter = 0;
    if (!cpl)
        return;            // 对于超级用户程序,不依赖counter 值进行调度。
    schedule ();
}

current->counter==0 时,将会进入 schedule() 函数,进行进程调度,源码在 kernel/sched.c

void schedule (void) {
    int i, next, c;
    struct task_struct **p;    // 任务结构指针的指针。

/* 检测alarm(进程的报警定时值),唤醒任何已得到信号的可中断任务 */

// 从任务数组中最后一个任务开始检测alarm。
    for (p = &LAST_TASK; p > &FIRST_TASK; --p)
        if (*p) {
// 如果任务的alarm 时间已经过期(alarm<jiffies),则在信号位图中置SIGALRM 信号,然后清alarm。
// jiffies 是系统从开机开始算起的滴答数(10ms/滴答)。定义在sched.h 第139 行。
            if ((*p)->alarm && (*p)->alarm < jiffies) {
                (*p)->signal |= (1 << (SIGALRM - 1));
                (*p)->alarm = 0;
            }
// 如果信号位图中除被阻塞的信号外还有其它信号,并且任务处于可中断状态,则置任务为就绪状态。
// 其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILL 和SIGSTOP 不能被阻塞。
            if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
                    (*p)->state == TASK_INTERRUPTIBLE)
                (*p)->state = TASK_RUNNING;    //置为就绪(可执行)状态。
        }

  /* 这里是调度程序的主要部分 */

    while (1) {
        c = -1;
        next = 0;
        i = NR_TASKS;
        p = &task[NR_TASKS];
// 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每个就绪
// 状态任务的counter(任务运行时间的递减滴答计数)值,哪一个值大,运行时间还不长,next 就
// 指向哪个的任务号。
        while (--i) {
            if (!*--p)
                continue;
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                c = (*p)->counter, next = i;
        }
      // 如果比较得出有counter 值大于0 的结果,则退出124 行开始的循环,执行任务切换(141 行)。
        if (c)
            break;
      // 否则就根据每个任务的优先权值,更新每一个任务的counter 值,然后回到125 行重新比较。
      // counter 值的计算方式为counter = counter /2 + priority。[右边counter=0??]
        for (p = &LAST_TASK; p > &FIRST_TASK; --p)
            if (*p)
                (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
    }
    switch_to (next);        // 切换到任务号为next 的任务,并运行之。
}

核心部分就是下半部分的 while 循环

首先遍历 task 数组中的所有任务,找到其中剩余时间片最大的 RUNNABLE 进程

while (--i) {
    if (!*--p)
        continue;
    if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
        c = (*p)->counter, next = i;
}

如果剩余时间片均为 0,则将剩余时间片全部修改为 priority 中的值

if (c)
    break;
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
    if (*p)
        (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;

上面两段代码将会循环执行,直到找到一个 next

随后将会 switch_to,该函数由汇编实现,位于 include/linux/sched.h

#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__( "cmpl %%ecx,_current\n\t" \    
  "je 1f\n\t" \            
  "movw %%dx,%1\n\t" \        
  "xchgl %%ecx,_current\n\t" \    
  "ljmp %0\n\t" \        
// 在任务切换回来后才会继续执行下面的语句。
  "cmpl %%ecx,_last_task_used_math\n\t" \    
  "jne 1f\n\t" \        
  "clts\n" \            
  "1:"::"m" (*&__tmp.a), "m" (*&__tmp.b),
  "d" (_TSS (n)), "c" ((long) task[n]));
}

有个翻译后的版本:

extern _inline void switch_to(int n) 
{
    unsigned short __tmp;
    __tmp = (unsigned short)_TSS(n);

    _asm {
        mov ebx, offset task
        mov eax, n
        mov ecx, [ebx+eax*4]
        cmp ecx, current/* 任务n 是当前任务吗?(current ==task[n]?) */ 
        je l1 /* 是,则什么都不做,退出。*/ 
        xchg ecx,current/* current = task[n]; */
        /*执行长跳转,造成任务切换 (头大了很长时间,多多包涵)*/
        mov ax, __tmp
        mov word ptr ds:[lcs],ax
        _emit 0xea
        _emit 0        // ip
        _emit 0 
        _emit 0 
        _emit 0
lcs:    _emit 0        // cs
        _emit 0
// 在任务切换回来后才会继续执行下面的语句。
        cmp last_task_used_math,ecx /* 新任务上次使用过协处理器吗?*/
        jne l1
        clts/* 新任务上次使用过协处理器,则清cr0 的TS 标志。*/
    }
l1: ;
}

最核心的就是 ljmp 到了一个新的 tss 段处,这里利用了 CPU 的硬件操作,执行这条指令时会自动将当前各个寄存器的值保存在当前进程的 tss 中,并将新进程的 tss 信息加载到各个寄存器,这样就完美做到了保存上下文和恢复上下文

definition of a syscall

接下来就该看 fork() 的源码了

这个源码位于 init/main.cinclude/unistd.h

static _inline _syscall0(int,fork)

_syscall0 的定义如下

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

根据这个宏定义,其实 fork() 就是通过 int 0x80 调用了一个 sys_fork 系统调用

_sys_fork 的定义位于 kernel/system_call.s

_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

fork a new process

这里开始看 fork 的具体实现了,根据两个函数名可以猜测出作用,先是寻找空的进程槽位,随后复制当前进程

find empty process

_find_empty_process 位于 kernel/fork.c

// 为新进程取得不重复的进程号 last_pid,并返回在任务数组中的任务号(数组 index)。
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;
}

task 数组中存放的是 task_struct 结构体的指针

这段程序首先判断 last_pid+1 是否小于 0,如果小于 0 说明爆 int 了,此时将其重新赋值为 1,确保了 pid 始终为正数

随后的 for 循环中,检查所有的 task,如果当前的 last_pid 与某一 taskpid 相同,说明被占用了,就跳转回 repeat 标签,生成下一个 last_pid 并判断能否使用,直到找到一个可用的 last_pid

找到可用的 last_pid 之后,就进入到了下一个循环,这里是寻找一个空闲的 task 项,找到后返回索引值

至此也就完成了函数名对应的任务:find empty process

copy process

fork 接下来还要执行一个 copy_process() 函数

这个函数就需要根据返回的索引将进程放到 task[i] 中了

/*
* OK,下面是主要的fork 子程序。它复制系统进程信息(task[n])并且设置必要的寄存器。
* 它还整个地复制数据段。
*/
// 复制进程。
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;
    struct i387_struct *p_i387;

    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;
// 如果当前任务使用了协处理器,就保存其上下文。
    p_i387 = &p->tss.i387;
    if (last_task_used_math == current)
    _asm{
        mov ebx, p_i387
        clts
        fnsave [p_i387]
    }
//    __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;        // 返回新进程号(与任务号是不同的)。
}

第一部分调用了 get_free_page() 函数是申请一个空闲页面,初始化的时候维护了一个数组 mem_map,这个函数就是从数组中找到一个值为 0 的项(表示该项对应的页处于空闲状态),接下来将该项赋为 1 并返回该项对应的页的内存起始地址

随后,在进程管理结构 task[nr] 中放入新的 task_struct 结构体指针,并将指针指向当前进程,也就是将当前进程的所有内容复制了出来

此后很长一大串内容都是在为当前进程的一部分信息进行特殊的赋值,比如将状态设置为 TASK_UNINTERRUPTIBLEpid 设置为之前获取的 last_pid,等等

接下来进行进程页表和段表的复制,主要调用了 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");
    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 表项的赋值页表的拷贝

LDT 表项的赋值

在复制当前进程到新进程到时候,还需要对新进程的 LDT 表项进行赋值,这样在执行这个进程的时候才能正常进行寻址

首先读取代码段和数据段的段限长,随后设置段基地址为 进程号 * 0x4000000 也就是每个进程分别在逻辑地址中占用 64 MB 的空间

new_code_base = nr * 0x4000000;
new_data_base = nr * 0x4000000;

接下来将上述设置的内容写入 LDT 表中

set_base(p->ldt[1], new_code_base);
set_base(p->ldt[2], new_data_base);

页表拷贝

由于内存采用的是段页式结构,在设置完段之后,就需要设置页表了

// old = 0, new = 64M, limit = 640K
copy_page_table(old_data_base, new_data_base, data_limit);

这个函数位于 mm/memory.c,具体实现如下:

/*
 * 好了,下面是内存管理mm 中最为复杂的程序之一。它通过只复制内存页面
 * 来拷贝一定范围内线性地址中的内容。希望代码中没有错误,因为我不想
 * 再调试这块代码了 :-)
 *
 * 注意!我们并不是仅复制任何内存块- 内存块的地址需要是4Mb 的倍数(正好
 * 一个页目录项对应的内存大小),因为这样处理可使函数很简单。不管怎样,
 * 它仅被fork()使用(fork.c)
 *
 * 注意!!当from==0 时,是在为第一次fork()调用复制内核空间。此时我们
 * 不想复制整个页目录项对应的内存,因为这样做会导致内存严重的浪费- 我们
 * 只复制头160 个页面- 对应640kB。即使是复制这些页面也已经超出我们的需求,
 * 但这不会占用更多的内存- 在低1Mb 内存范围内我们不执行写时复制操作,所以
 * 这些页面可以与内核共享。因此这是nr=xxxx 的特殊情况(nr 在程序中指页面数)。
 */
//// 复制指定线性地址和长度(页表个数)内存对应的页目录项和页表,从而被复制的页目录和
//// 页表对应的原物理内存区被共享使用。
// 复制指定地址和长度的内存对应的页目录项和页表项。需申请页面来存放新页表,原内存区被共享;
// 此后两个进程将共享内存区,直到有一个进程执行写操作时,才分配新的内存页(写时复制机制)。
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
	unsigned long * from_page_table;
	unsigned long * to_page_table;
	unsigned long this_page;
	unsigned long * from_dir, * to_dir;
	unsigned long nr;

	// 源地址和目的地址都需要是在4Mb 的内存边界地址上。否则出错,死机。
	if ((from&0x3fffff) || (to&0x3fffff))
		panic("copy_page_tables called with wrong alignment");
	// 取得源地址和目的地址的目录项(from_dir 和to_dir)。参见对115 句的注释。
	from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
	to_dir = (unsigned long *) ((to>>20) & 0xffc);
	// 计算要复制的内存块占用的页表数(也即目录项数)。
	size = ((unsigned) (size+0x3fffff)) >> 22;
	// 下面开始对每个占用的页表依次进行复制操作。
	for( ; size-->0 ; from_dir++,to_dir++) {
		if (1 & *to_dir)// 如果目的目录项指定的页表已经存在(P=1),则出错,死机。
			panic("copy_page_tables: already exist");
		if (!(1 & *from_dir))// 如果此源目录项未被使用,则不用复制对应页表,跳过。
			continue;
		// 取当前源目录项中页表的地址 -> from_page_table。
		from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
// 为目的页表取一页空闲内存,如果返回是0 则说明没有申请到空闲内存页面。返回值=-1,退出。
		if (!(to_page_table = (unsigned long *) get_free_page()))
			return -1;	/* Out of memory, see freeing */
		// 设置目的目录项信息。7 是标志信息,表示(Usr, R/W, Present)。
		*to_dir = ((unsigned long) to_page_table) | 7;
		// 针对当前处理的页表,设置需复制的页面数。如果是在内核空间,则仅需复制头160 页,
		// 否则需要复制1 个页表中的所有1024 页面。
		nr = (from==0)?0xA0:1024;
		// 对于当前页表,开始复制指定数目nr 个内存页面。
		for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
			this_page = *from_page_table;// 取源页表项内容。
			if (!(1 & this_page))// 如果当前源页面没有使用,则不用复制。
				continue;
// 复位页表项中R/W 标志(置0)。(如果U/S 位是0,则R/W 就没有作用。如果U/S 是1,而R/W 是0,
// 那么运行在用户层的代码就只能读页面。如果U/S 和R/W 都置位,则就有写的权限。)
			this_page &= ~2;
			*to_page_table = this_page;// 将该页表项复制到目的页表中。
// 如果该页表项所指页面的地址在1M 以上,则需要设置内存页面映射数组mem_map[],于是计算
// 页面号,并以它为索引在页面映射数组相应项中增加引用次数。
			if (this_page > LOW_MEM) {
// 下面这句的含义是令源页表项所指内存页也为只读。因为现在开始有两个进程共用内存区了。
// 若其中一个内存需要进行写操作,则可以通过页异常的写保护处理,为执行写操作的进程分配
// 一页新的空闲页面,也即进行写时复制的操作。
				*from_page_table = this_page;// 令源页表项也只读。
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}
	invalidate();// 刷新页变换高速缓冲。
	return 0;
}

根据 fork 的特性,这个函数执行之后的预期是两个进程的页表都指向同一个物理地址空间

具体的实现比较复杂,主要看注释了

Built with Hugo
Theme Stack designed by Jimmy