Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🐧 Linux Kernel Comments
    • Chapter 2 - 微型计算机组成结构

      • Chapter 2 - 微型计算机组成结构
    • Chapter 3 - 内核编程语言和环境

      • Chapter 3 - 内核编程语言和环境
    • Chapter 4 - 80X86 保护模式及其编程

      • Chapter 4.1-4.2 - 80X86 系统寄存器和系统指令 & 保护模式内存管理
      • Chapter 4.3 - 分段机制
      • Chapter 4.4 - 分页机制
      • Chapter 4.5 - 保护
      • Chapter 4.6 - 中断和异常处理
      • Chapter 4.7 - 任务管理
      • Chapter 4.8 - 保护模式编程初始化
      • Chapter 4.9 - 一个简单的多任务内核实例
    • Chapter 5 - Linux 内核体系结构

      • Chapter 5.1-5.2 - Linux 内核模式 & 体系结构
      • Chapter 5.3 - Linux 内核对内存的管理和使用
      • Chapter 5.4-5.6 - 中断机制 & 系统调用 & 系统时间和定时
      • Chapter 5.7-5.9 - Linux 进程控制 & 堆栈使用 & 文件系统
    • Chapter 6 - 引导启动程序 (boot)

      • Chapter 6 - 引导启动程序 (boot)
    • Chapter 7 - 初始化程序 (init)

      • Chapter 7 - 初始化程序 (init)
    • Chapter 8 - 内核代码

      • Chapter 8.1 - 内核代码总体功能
      • Chapter 8.2 - asm.s 程序
      • Chapter 8.3 - traps.c 程序
      • Chapter 8.4 - sys_call.s 程序
      • Chapter 8.5 - mktime.c 程序
      • Chapter 8.6 - sched.c 程序
      • Chapter 8.7 - signal.c 程序
      • Chapter 8.8 - exit.c 程序
      • Chapter 8.9 - fork.c 程序
      • Chapter 8.10 - sys.c 程序
    • Chapter 9 - 块设备驱动程序

      • Chapter 9.1 - 块设备驱动程序 总体功能
      • Chapter 9.2 - blk.h 文件
      • Chapter 9.3 - hd.c 程序
      • Chapter 9.4 - ll_rw_blk.c 程序
      • Chapter 9.5 - ramdisk.c 程序
    • Chapter 10 - 字符设备驱动程序

      • Chapter 10.1 - 字符设备驱动程序 总体功能
      • Chapter 10.2 - keyboard.S 程序
      • Chapter 10.3 - console.c 程序
      • Chapter 10.4 - serial.c 程序
      • Chapter 10.5 - rs_io.s 程序
      • Chapter 10.6 - tty_io.c 程序
      • Chapter 10.7 - tty_ioctl.c 程序
    • Chapter 12 - 文件系统

      • Chapter 12.1 - 文件系统 总体功能
      • Chapter 12.2 - buffer.c 程序
      • Chapter 12.3 - bitmap.c 程序
      • Chapter 12.4 - truncate.c 程序
      • Chapter 12.5 - inode.c 程序
      • Chapter 12.6 - super.c 程序
      • Chapter 12.7 - namei.c 程序
      • Chapter 12.9 - block_dev.c 程序
      • Chapter 12.10 - file_dev.c 程序
      • Chapter 12.11 - pipe.c 程序
      • Chapter 12.12 - char_dev.c 程序
      • Chapter 12.13 - read_write.c 程序
      • Chapter 12.14 - open.c 程序
      • Chapter 12.15 - exec.c 程序
      • Chapter 12.16 - stat.c 程序
      • Chapter 12.17 - fcntl.c 程序
      • Chapter 12.18 - ioctl.c 程序
      • Chapter 12.19 - select.c 程序

Chapter 8.9 - fork.c 程序

Created by : Mr Dk.

2019 / 08 / 21 11:34

Ningbo, Zhejiang, China


8.9 fork.c 程序

8.9.1 功能描述

fork 系统调用用于创建子进程。在 Linux 中,所有进程都是进程 0 的子进程,该程序是 sys_fork() 函数的辅助处理函数集:

  • find_empty_process()
  • copy_process()

copy_process() 函数用于复制进程的代码段和数据段,以及环境

  • 为新建进程在主内存区中申请内存
  • 复制当前进程的任务数据结构中的所有内容,作为新进程任务数据结构的模板
  • 对复制的任务数据结构进行修改
    • 利用系统调用发生时,压入堆栈的寄存器信息,重新设置任务结构中的 TSS
    • 使新进程的状态保持父进程即将进入中断过程前的状态
  • 复制当前进程的页目录项和页表项
  • 同步文件使用
  • 在 GDT 中设置 LDT 和 TSS,新任务设置为可运行状态,向当前进程返回新进程号

8.9.2 代码注释

进程空间区域写前验证函数 verify_area()

对于 80386 CPU,在执行特权级 0 代码时,页保护标志不起作用,copy-on-write 失去作用 - 该函数用于解决这个问题。对于 80486 CPU,内核可以通过设置 CR0 中的写保护标志 WP 禁止向用户空间只读页面写数据。检测以页为单位进行操作,因此程序需要首先定位到 addr 所在页面的基地址:

8-13

该地址是进程空间中的逻辑地址,即相对于进程起始地址空间的偏移量,然后加上进程数据段基地址,变换为 CPU 线性空间中的地址。最后调用 write_verify() 对指定大小的内存空间进行写前验证。若页面只读,则执行共享检验和写时复制:

void verify_area(void *addr, int size)
{
    unsigned long start;
    
    start = (unsigned long) addr;
    size += start & 0xfff; // addr 在页内的偏移
    start &= 0xfffff000; // 对齐页边界,进程空间中的逻辑地址
    
    start += get_base(current->ldt[2]); // 加上进程数据段基址,转换为线性地址
    
    while (size > 0) {
        // 循环进行写页面验证
        // 如果页面不可写,则复制页面
        size -= 4096;
        write_verify(start);
        start += 4096;
    }
}

复制内存页表函数 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)
        // Linux 0.12 不支持代码段和数据段分离
        panic("We don't support separate I&D");
    if (data_limit < code_limit)
        panic("Bad data_limit");
    
    // 设置新进程在线性地址空间中的基地址
    new_data_base = new_code_base = nr * TASK_SIZE;
    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;
}

进程拷贝函数 copy_process()

复制进程信息 task[n],并设置必要的寄存器,整个地复制数据段 (也是代码段)。函数参数是汇编依次压入堆栈的寄存器数据。

int copy_process(int nr, long ebp, long edi, long esi, long gs, long none,
                long ebx, long ecx, long edx, long orig_eax,
                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;
    
    // 为新的 PCB 申请一页
    // 并将地址放入任务数组中
    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
    task[nr] = p;
    *p = *current; // 只复制任务结构,不复制内核态堆栈
    
    // 对复制来的任务数据结构进行一些修改
    p->state = TASK_UNINTERRUPTIBLE; // 防止被内核调度执行
    p->pid = last_pid; // 新进程号
    p->counter = p->priority; // 进程时间片
    p->signal = 0; // 信号 bitmap
    p->alarm = 0; // 报警定时值
    p->leader = 0; // 进程领导权不能继承
    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->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;
    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 描述符的选择符 (LDT 描述符在 GDT 中)
    p->tss.trace_bitmap = 0x80000000;
    
    // 如果当前任务使用了协处理器,就保存上下文到新进程的 i387 中
    if (last_task_used_math == current)
        __asm__("clts; fnsave %0; frstor %0"::"m"(p->tss.i387));
    
    // 复制进程页表
    if (copy_mem(nr, p)) {
        // 出错
        task[nr] = NULL;
        free_page((long) p); // 回收 PCB 所在页
        return -EAGAIN;
    }
    
    // 子进程与父进程共享打开的文件
    // 将对应文件的打开次数 +1
    for (i = 0; i < NR_OPEN; i++)
        if (f = p->filp[i])
            f->f_count++;
    
    // 父进程的 pwd、root、executable 的 inode 引用次数 +1
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;
    if (current->library)
        current->library->i_count++;
    
    // 在 GDT 表中设置新任务的 TSS 段和 LDT 段描述符
    set_tss_desc(gdt + (nr<<1) + FIRST_TSS_ENTRY, &(p->tss));
    set_ldt_desc(gdt + (nr<<1) + FIRST_LDT_ENTRY, &(p->ldt));
    
    // 将进程插入进程链表
    p->p_pptr = current; // 设置新进程的父进程为当前进程
    p->p_cptr = 0; // 新进程无子进程
    p->p_ysptr = 0; // 新进程无更年轻的兄弟进程
    p->p_osptr = current->p_cptr; // 设置新进程的老兄弟进程
    if (p->p_osptr) // 设置老兄弟进程的年轻兄弟进程指针
        p->p_osptr->p_ysptr = p;
    current->p_cptr = p; // 设置当前进程的最新子进程
    
    p->state = TASK_RUNNING; // 子进程就绪
    return last_pid;
}

寻找进程槽 find_empty_process()

为新进程取得不重复的进程号,返回在任务数组中的 index。

int find_empty_process(void)
{
    int i;
    
    // 寻找唯一的 pid
repeat:
    if ((++last_pid < 0))
        last_pid = 1; // overflow
    for (i = 0; i < NR_TASKS; i++)
        if (task[i] && ((task[i]->pid == last_pid) || (task[i]->pgrp == last_pid)))
            goto repeat;
    
    // 唯一的 pid 已找到
    // 寻找任务数组空槽
    for (i = 1; i < NR_TASKS; i++)
        if (!task[i])
            return i; // 找到任务数组空槽
    return -EAGAIN;
}
Edit this page on GitHub
Prev
Chapter 8.8 - exit.c 程序
Next
Chapter 8.10 - sys.c 程序