Linux内核-进程初始化与创建

进程初始化与创建

Linux进程表示

Linux内核对于进程的表示使用的是task_struct结构体,它的定义在<include/sched.h>中,每一个进程都被抽象为一个task_struct结构体

而所有进程是用一个task_struct链表来管理

struct task_struct * task[NR_TASKS] = {&(init_task.task), };    // 最多64个任务

task_struct结构体含有很多成员,将进程和各个内核子系统联系起来

// 任务(进程)数据结构
struct task_struct {
/* these are hardcoded - don't touch */
    long state;    /* -1 unrunnable, 0 runnable, >0 stopped */            // 任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止)
    long counter;    //counter 值的计算方式为 counter = counter /2 + priority,  优先执行counter最大的任务;  任务运行时间计数(递减)(滴答数)(时间片)
    long priority;        // 运行优先数。任务开始运行时 counter = priority,越大运行越长
    long signal;        // 信号。是位图,每个比特位代表一种信号,信号值=位偏移值+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
    unsigned short gid,egid,sgid;                                    // 组标识号,有效组号,保存的组号
    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 */    // 进程使用 tty 的子设备号。-1 表示没有使用
    unsigned short umask;                                        // 文件创建属性屏蔽位
    struct m_inode * pwd;                                        // 当前工作目录i节点
    struct m_inode * root;                                        // 跟目录i节点
    struct m_inode * executable;                                // 执行文件i节点
    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];                                        // 局部描述符段, 0-空,1-代码段 cs,2-数据和堆栈段 ds&ss
/* tss for this task */
    struct tss_struct tss;                                        // 本进程的任务状态段信息结构
};

LDT是进程的局部描述符,主要是存储进程的代码段数据段等;而TSS是进程的状态描述符,里面存储的是CPU需要知道的进程的状态标识

TSS

// 任务状态段(Task State Segment),104字节,保存任务信息,任务(进程/线程)切换用,由TR(任务寄存器)寻址。字段构成
// 1. 寄存器保存区域
// 2. 内核堆栈指针区域   一个任务可能具有四个堆栈,对应四个特权级。四个堆栈需要四个堆栈指针,3级属于用户态,没有后缀
// 3. 地址映射寄存器    用于分页寻址,似乎线程切换不需要
// 4. 链接字段     前一任务的TSS描述符的选择子
// 5. 其他字段     I/O许可位图
struct tss_struct {
    long    back_link;    /* 16 high bits zero  前一执行任务的TSS任务的描述符 */   
    long    esp0;                                    // 内核0级堆栈栈顶指针
    long    ss0;        /* 16 high bits zero 0级堆栈栈段寄存器 */        
    long    esp1;                                    // 1级堆栈栈顶指针
    long    ss1;        /* 16 high bits zero 1级堆栈栈段寄存器 */
    long    esp2;                                    // 2级堆栈栈顶指针
    long    ss2;        /* 16 high bits zero 2级堆栈栈段寄存器 */
    long    cr3;                                    // 控制寄存器3,存储页目录地址
    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 */  //  当任务进行切换时导致 CPU 产生一个调试(debug)异常的 T-比特位(调试跟踪位);I/O 比特位图基地址
    struct i387_struct i387;
};

在单核CPU中想要进行多任务运行,CPU的寄存器只有一个,而每个进程都有自己的寄存器值和堆栈,地址信息,所以将放进TSS这个描述符中

进程ID号

UNIX进程总是会分配一个号码用于在其命名空间中唯一地标识它们。该号码被称作进程ID号,简称PID。用forkclone 产生的每个进程都由内核自动地分配了一个新的唯一的PID值。

进程的初始化

main函数初始化

linux中从设备上电起来后,首先进入main函数,先保存根设备号,内存地址等,然后进行一系列的初始化,如内存初始化,中断初始化,字符/块设备初始化,以及进程调度(进程0)初始化等等。

    mem_init(main_memory_start,memory_end);                    //如果内存为8M,分布形式为  0          2M             4M          6M            16M          
    // 中断表与函数挂接                                         //                        |  内核程序  |   缓冲区       |   虚拟盘  |    主内存    | 
    trap_init();                                                 
    //  初始化request[32] 请求项, 用于缓冲区与块设备之间的沟通协调
    blk_dev_init(); 
    // 初始化字符设备,目前为空函数   
    chr_dev_init();
    // 初始化串行口, 包括串口和控制台(serial.c console.c)
    tty_init();
    // 初始化时间
    time_init();
    // 初始化进程0
    sched_init();
    // 缓冲区初始化
    buffer_init(buffer_memory_end);
    // 硬盘软盘初始化
    hd_init();
    floppy_init();

进程调度初始化

//kernel/sched.c
// 进程调度初始化
void sched_init(void)
{
    int i;
    struct desc_struct * p;

    if (sizeof(struct sigaction) != 16)
        panic("Struct sigaction MUST be 16 bytes");
    set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));           // 设置初始任务, 非常重要:初始化进程0
    set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));   //将后面一项拷贝到前面一项中
    p = gdt+2+FIRST_TSS_ENTRY;
    // 清任务数组和描述符表项(注意 i=1 开始,所以初始任务的描述符还在)。
    for(i=1;i<NR_TASKS;i++) {
        task[i] = NULL;
        p->a=p->b=0;
        p++;
        p->a=p->b=0;
        p++;
    }
/* Clear NT, so that we won't have troubles with that later on */
/* 清除标志寄存器中的位 NT,这样以后就不会有麻烦 */
    __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
    //给 tr 寄存器赋值, 指向任务状态段 tss
    ltr(0);
    //给 ldtr 寄存器赋值, 指向局部描述符表 ldt
    lldt(0);
    // 下面代码用于初始化 8253 定时器
    outb_p(0x36,0x43);      /* binary, mode 3, LSB/MSB, ch 0 */
    outb_p(LATCH & 0xff , 0x40);    /* LSB */
    outb(LATCH >> 8 , 0x40);    /* MSB */             //  这四行代码就开启了这个定时器,之后这个定时器变会持续的、以一定频率的向 CPU 发出中断信号, 中断处理程序为 timer_interrupt
    set_intr_gate(0x20,&timer_interrupt);
    outb(inb_p(0x21)&~0x01,0x21);
    set_system_gate(0x80,&system_call);          // 所有用户态程序想要调用内核提供的方法,都需要基于这个系统调用来进行
}

保存描述符至GDT

首先将初始化进程的tss和ldt两个描述符地址保存到gdt全局描述符当中

这是一个容易混淆的地方。

set_tss_desc(gdt+4, &(init_task.task.tss));
set_ldt_desc(gdt+5, &(init_task.task.ldt));

并不是把整个 TSS 和 LDT 结构复制到 GDT 中。

GDT 的每一个表项只有 8 字节,显然放不下完整的 TSS。

  ┌──────────┬────────┬──────────────────────────┐
  │ GDT 下标 │ 选择子 │           内容           │
  ├──────────┼────────┼──────────────────────────┤
  │        0 │   0x00 │ 空描述符                 │
  ├──────────┼────────┼──────────────────────────┤
  │        1 │   0x08 │ 内核代码段               │
  ├──────────┼────────┼──────────────────────────┤
  │        2 │   0x10 │ 内核数据段               │
  ├──────────┼────────┼──────────────────────────┤
  │        3 │   0x18 │ 临时/保留项              │
  ├──────────┼────────┼──────────────────────────┤
  │        4 │   0x20 │ 第一个可供任务使用的位置 │
  ├──────────┼────────┼──────────────────────────┤
  │        5 │   0x28 │ 第二个可供任务使用的位置 │
  └──────────┴────────┴──────────────────────────┘

GDT 中存放的是描述符,描述符记录:

  • TSS/LDT 的内存基地址
  • 段界限
  • 描述符类型
  • 是否存在
  • 权限等级 关系是: GDT[4]:TSS 描述符 │
    │ base 地址
    init_task.task.tss:实际的 TSS 数据 GDT[5]:LDT 描述符 │
    │ base 地址
    init_task.task.ldt:实际的 LDT 表

清空任务链表

接下来初始化进程会去清空任务链表,除了自身其余的描述符全部清零

    p = gdt+2+FIRST_TSS_ENTRY;
    // 清任务数组和描述符表项(注意 i=1 开始,所以初始任务的描述符还在)。
    for(i=1;i<NR_TASKS;i++) {
        task[i] = NULL;
        p->a=p->b=0;
        p++;
        p->a=p->b=0;
        p++;
    }

这里p指向的是gdt[6]的位置,对应进程1的TSS描述符

Linux 0.11 给 GDT 规定了一套固定布局:

GDT[0]:空描述符
GDT[1]:内核代码段描述符
GDT[2]:内核数据段描述符
GDT[3]:保留项
GDT[4]:进程 0 的 TSS 描述符
GDT[5]:进程 0 的 LDT 描述符
GDT[6]:进程 1 的 TSS 描述符
GDT[7]:进程 1 的 LDT 描述符

设置定时器中断和系统调用

在函数的最后会去设置定时器中断,然后使用set_system_gate设置系统调用,所有进程的创建都是使用系统调用完成的

    set_intr_gate(0x20,&timer_interrupt);
    outb(inb_p(0x21)&~0x01,0x21);
    set_system_gate(0x80,&system_call);          // 所有用户态程序想要调用内核提供的方法,都需要基于这个系统调用来进行

这里使用的是所有用户程序都可以调用内核提供的方式

转换特权

在主函数完成了上面一系列初始化之后,会去切换到用户态。之前的一系列初始化都使用的是内核态完成,内核态的目的是不可抢占。而就如同上面所说,进程的创建都是依赖系统调用,所以必须切换到用户态才可以执行系统调用

    // 进程均为3特权等级,此处的进程0也需要翻转到3用户态模式
    move_to_user_mode();

创建子进程(进程1)

进程的创建全部都是由父进程fork而来的

main函数的最后会调用fork

    if (!fork()) {      /* we count on this going ok */       // 进程1
        init();                                   //child process
    }

init进程

在init()函数开始会去调用setup系统调用,是进程 1 发起的、只能调用一次的系统调用。它把启动阶段取得的 BIOS硬盘参数交给内核,并触发磁盘分区识别、RAM disk,加载和根文件系统挂载,从而完成从“内核已经启动”到“可以访问/bin/sh、/etc/rc 等文件”的关键过渡。

打开标准输入输出错误

在接下来init进程会去打开标准输入,标准输出和标准错误

(void) open("/dev/tty0",O_RDWR,0);   // 返回的句柄号 0 -- stdin 标准输入设备
(void) dup(0);                    // 复制句柄,产生句柄 1 号 -- stdout 标准输出设备
(void) dup(0);                   // 复制句柄,产生句柄 2 号 -- stderr 标准出错输出设备

创建子进程(进程2)

在打开了上面三个之后,会去调用fork创建一个子进程,在子进程里面打开/etc/rc系统配置文件,然后加载shell程序

if (!(pid=fork())) {                                      // 进程1创建进程2!!!!
    close(0);
    if (open("/etc/rc",O_RDONLY,0))         // 以只读形式打开rc文件(配置文件),占据0号文件描述符
        _exit(1);
    execve("/bin/sh",argv_rc,envp_rc); // 加载shell程序. 此shell程序读取rc文件并启动update进程,用于同步缓冲区的数据到外设
    _exit(2); //这里是防止execve失败所以做的兜底措施    // 每隔一段时间,update程序会被唤醒
}

这个shell程序会去执行rc文件中的update程序

这里的_exit()函数与exit()函数不同,他没有exit()函数做的用户态清除,而是直接进入内核态调用系统调用,在内核态中销毁进程

创建交互式shell

前面执行的execve("/bin/sh",argv_rc,envp_rc);是启用的批处理模式shell,Shell 会打开这个文件,从头到尾逐行读取并执行。当它读完文件的最后一行,遇到 EOF(文件结束符) 时,执行任务完成,Shell 进程就会调用 exit 正常退出

所以后面需要启用一个交互式shell

// shell加载普通文件 如/etc/rc 后会退出,所以下面需要重新启动一个shell
    // shell加载字符设备文件不会退出, 如/dev/tty0
    if (pid>0)                                      //parent process
        while (pid != wait(&i))           //父进程等待子进程的结束。&i 是存放返回状态信息的位置。
            /* nothing */;
    while (1) {                         //创建的子进程的执行已停止或终止,再创建一个子进程
        if ((pid=fork())<0) {
            printf("Fork failed in init\r\n");
            continue;
        }
        if (!pid) {
            close(0);close(1);close(2);
            setsid();
            (void) open("/dev/tty0",O_RDWR,0);     // 标准输入设备 stdin
            (void) dup(0);                         // 标准输出设备 stdout
            (void) dup(0);                         // 标准错误输出设备 stderr
            _exit(execve("/bin/sh",argv,envp));
        }
        while (1)
            if (pid == wait(&i))
                break;
        printf("\n\rchild %d died with code %04x\n\r",pid,i);
        sync();
    }

首先是init进程确保前面的shell进程正常退出,进入一个while(1)的循环,创建一个新的shell子进程,在这里可以看到和上面的参数是有区别的

static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL };

static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };

创建shell的参数区别

  1. 最关键的区别:argv[0]-(短横线)

这是最容易被忽视但最重要的区别。

  • argv_rcargv[0] = "/bin/sh"没有前导 -)。
  • argvargv[0] = "-/bin/sh"前导 -)。

Shell(/bin/sh)在启动时会检查 argv[0] 的第一个字符

  • 如果 argv[0]- 开头(例如 -/bin/sh-sh),Shell 会认为自己是一个 “登录 Shell”(Login Shell)
  • 如果 argv[0] 不以 - 开头,Shell 会认为自己是一个 “普通 Shell”(Non-login Shell)

这会导致截然不同的行为

  • 普通 Shell(argv_rc:启动速度极快,它不会去读取 /etc/profile~/.profile 等系统配置文件,只会执行从标准输入(这里是 /etc/rc 文件)读到的命令。执行完就退出。
  • 登录 Shell(argv:会先执行一系列系统级别的初始化脚本(加载环境变量、设置终端参数等),然后才进入交互模式,等待用户输入。这也是为什么正常开机后控制台会有登录提示符和自定义环境的原因。
  1. HOME 环境变量的差异
  • envp_rc"HOME=/"
  • envp"HOME=/usr/root"

这直接改变了 Shell 及其子进程的当前工作目录默认值历史记录文件位置

  • 第一阶段(初始化)HOME=/。执行 /etc/rc 时,所有相对路径都基于根目录,cd ~ 会回到 /。这防止了因为找不到用户目录而报错,处于“急救/系统初始化”状态。
  • 第二阶段(用户交互)HOME=/usr/root。当交互式 Shell 启动后,cd ~ 会把你带到 /usr/root(即 root 用户的主目录),history 文件也会保存在这里。这符合用户正常使用的逻辑。

死循环的意义

这里init陷入死循环是因为在 Linux 中,进程 1 (init) 是所有用户态进程的祖先。内核在启动完 init 后,如果发现进程 1 意外退出了,内核会直接触发 Kernel Panic(内核恐慌),系统立即崩溃(挂起或重启)。

所以,init 函数的最后有一行 _exit(0);,但这行代码永远不会被执行。外围的 while (1) 死循环保证了 init 永远驻留在内存中,不会“跑飞”到最后的退出逻辑。

当子进程execve失败或者shell退出时

  • 正常情况:用户在 Shell 里输入 exit 或按 Ctrl+D,Shell 进程正常退出。wait 返回,init 打印死亡信息,然后循环回去,fork 一个新的 Shell 给你。(这意味着即使用户退出了控制台,init 也会立刻给你开一个新的登录 Shell)
  • 异常情况(挂了):如果 Shell 因为段错误(Segmentation Fault)或其他 bug 崩溃了,内核会发送信号终止该子进程。父进程 initwait 会立即捕捉到子进程的死亡事件,打印信息,然后重新 fork 一个新的 Shell 进程。这就实现了“崩溃自动重启”的看门狗功能。

重新关闭打开标准输入输出错误

这个细节非常关键,也是为了防止 Shell 挂掉后“借尸还魂”:

  • 如果子进程的 Shell 挂了,它的文件描述符(0,1,2)会被内核自动关闭。
  • init 重新 fork 出新进程后,新子进程继承的是 init 进程的文件描述符。而 init 进程当初打开过 /dev/tty0,所以子进程一开始可能继承了无效或错误的描述符。
  • 因此,子进程在 execve 之前,强制 close(0);close(1);close(2); 清空一切,然后重新 open("/dev/tty0")。由于 open 总是返回当前最小可用描述符(0),再 dup 两次得到 1 和 2,完美地给新的 Shell 重新挂载了干净的控制台标准输入/输出/错误

0号进程永不结束

在main函数的最后,可以看到

/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
    for(;;) pause();

这个进程是不会退出的这里的英文翻译是:

注意! 对于任何其他任务(进程)来说,执行 pause() 意味着我们必须收到一个信号(signal)才能被唤醒;

任务 0 是唯一的例外(具体原因见 schedule() 函数)。

因为任务 0 会在每一个空闲时刻(即没有其他任务可以运行的时候)被激活。

对于任务 0 来说,pause() 仅仅意味着:“我们去检查一下是否有其他任务可以运行,如果没有,我们就回到这里(继续循环)。”

在 Linux 0.1 中,任务 0(Task 0) 是所有进程的祖先进程(即 init 的父进程,在 main.c 中静态初始化)。当系统没有任何任务需要处理时,CPU 就会执行任务 0。

注释的关键在于解释了 pause() 系统调用的“双重标准”

  1. 对于普通进程(任务 > 0)
    调用 pause() 意味着进程主动让出 CPU,并将自己设置为“可中断睡眠”状态。除非收到外部信号(如键盘中断、kill 命令),否则它永远沉睡,不会被调度执行。
  2. 对于任务 0(空闲进程)
    如果任务 0 调用了 pause(),它并不会真的“睡死”过去。因为内核调度函数 schedule() 有一个兜底逻辑:当 schedule() 遍历所有任务,发现除了任务 0 之外没有其他任务处于可运行状态(TASK_RUNNING)时,schedule() 会强行选中任务 0 来运行

进程创建

进程的创建就是对0号进程或当前进程的复制

进程的创建是系统调用,通过fork函数去触发系统调用

.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
    addl $20,%esp
1:    ret

寻找空task_struct链表成员

首先是调用的find_empty_process函数,去寻找空闲的task_struct链表成员

// 为新进程取得不重复的进程号 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++)
        if (!task[i])
            return i;
    return -EAGAIN;
}

这里找到后就返回数组下标

复制系统信息

接下来就是调用copy_process函数复制系统信息

/* 
 * OK,下面是主要的 fork 子程序。它复制系统进程信息(task[n])并且设置必要的寄存器。 
 * 它还整个地复制数据段。 
 */ 
 // 复制进程。  复制task_struct(包含tss和ldt等),复制寄存器,复制内存;再进行一些个性化设置(如各种id,状态等)
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; 

这里的get_free_page()在现代linux中就是malloc的效果,动态分配内存

然后把这块内存指针放进task表中,这里的nr就是新进程的任务号

拷贝父进程的结构体

*p = *current;    /* NOTE! this doesn't copy the supervisor stack */     //完全复制之前的进程的 task_struct   

这里current指向当前被创建的进程的父进程,把父进程的task_struct结构体拷贝到当前进程

保存自定义参数

子进程不可能跟父进程完全一样,会有自己的不同的参数

    p->state = TASK_UNINTERRUPTIBLE;                                       //很重要,此时进程还未复制完成,不能被进程调度打断
    p->pid = last_pid;
    p->father = current->pid;
    p->counter = p->priority;
    p->signal = 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;
    p->tss.back_link = 0;
    p->tss.esp0 = PAGE_SIZE + (long) p;
    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;
    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);
    p->tss.trace_bitmap = 0x80000000;

协处理器设置

协处理器就是专门算浮点数的硬件加速器

    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));

拷贝内存

拷贝内存调用的copy_mem函数

if (copy_mem(nr,p)) {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
 // 设置新任务的代码和数据段基址、限长并复制页表。 
 // 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);        // 0x0f = 00001111: 代码段,LDT,3特权级
    data_limit=get_limit(0x17);        // 0x17 = 00010111: 数据段,LDT,3特权级
    old_code_base = get_base(current->ldt[1]);
    old_data_base = get_base(current->ldt[2]);
    if (old_data_base != old_code_base)
        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;
}

在这里面去获取到父进程的代码段和数据段的基地址和大小,然后给新的进程一个新的基地址和大小,并且更新task_struct中对应内容,然后将原来的数据段代码段拷贝到新的数据段和代码段

增加对应索引计数

    for (i=0; i<NR_OPEN;i++)
        if (f=p->filp[i])
            f->f_count++;
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;

这里的flip是struct file *指针

struct file * filp[NR_OPEN];    // 进程使用的文件表结构,用于保存文件句柄

这里表示如果父进程有打开了文件,那么子进程继承父进程也会打开这个文件,并且这个文件的计数f_count会加1

同理剩下的pwd,root,executable的计数都会加1

    struct m_inode * pwd;                       // 当前工作目录i节点
    struct m_inode * root;                      // 跟目录i节点
    struct m_inode * executable;                // 执行文件i节点

更新gdt

在最后会更新gdt,并且把进程状态设置为运行状态,返回进程pid

    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;
上一篇
下一篇