00_前言

现在只剩下copyin、copyout、copyinstr这三个函数了

01_copyin函数

1.函数的作用

这个函数负责从给定的用户页表pagetable的虚拟地址srcva处拷贝长度为len字节的数据到地址dst处。

为什么内核目的地址dst使用指针来表示,而用户态的地址却使用uint64 srcva来代替?

  • 内核中的指针(如dst)指向的是内核地址空间中的内存,可以直接通过MMU自动转换为物理地址。
  • 而用户态的虚拟地址(如srcva)不能直接使用,因为内核态与用户态的地址空间不同。
  • 为了读取用户态的数据,内核需要通过软件逻辑(如walkaddr)手动完成虚拟地址到物理地址的映射.
2.代码
// Copy from user to kernel.
// Copy len bytes to dst from virtual address srcva in a given page table.
// Return 0 on success, -1 on error.
// 译:从用户空间向内核空间拷贝数据
// 从页表pagetable的虚拟地址srcva中拷贝len长度的数据到dst
// 成功时返回0,出错时返回-1
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
  uint64 n, va0, pa0;

  // 总共要复制len个字符之后再跳出循环
  while(len > 0){
    // 使用PGROUNDDOWN宏来找到srcva对应页的起始虚拟地址
    va0 = PGROUNDDOWN(srcva);

    // 找到va0对应的实际物理地址
    pa0 = walkaddr(pagetable, va0);

    // pa0 == 0表示walkaddr函数出错,返回-1
    if(pa0 == 0)
      return -1;

    // 注意:这里的n是在本次迭代中要复制的字节数
    // 考虑两种非对齐的情形:
    // 1.srcva起始地址如果不是页对齐的,n的大小恰好等于第一个页面要复制的剩余的字节数量
    // 在复制完第一个页面之后,页面会强制对齐,假设剩余len > PGSIZE
    // 那么此时会有n = PGSIZE - (srcva - va0) = PGSIZE,这会复制一整个页面
    n = PGSIZE - (srcva - va0);

    // 这是为了收尾处理,防止复制多余数据
    // 2.此时if(n > len)的判断可以保证不复制多余的数据
    if(n > len)
      n = len;

    // 调用memmove完成本次复制
    memmove(dst, (void *)(pa0 + (srcva - va0)), n);

    // 更新len、dst、srcva
    len -= n;
    dst += n;
    // 注意这里的对srcva的更新,无论一开始srcva是否是页对齐的
    // 经过这里的更新后一定会是页对齐的,因为va是圆整后的页面起始地址,细细品味一下...
    srcva = va0 + PGSIZE;
  }
  return 0;
}

02_copyinstr函数

1.函数的作用

copyinstrcopyin函数作用类似,但是不同的是:copyinstr复制的对象是字符串,所以复制结束的标志是遇到空字符(null-terminated)

2.代码
// Copy a null-terminated string from user to kernel.
// Copy bytes to dst from virtual address srcva in a given page table,
// until a '\0', or max.
// Return 0 on success, -1 on error.
// 译:从用户空间拷贝一个以空字符结尾的字符串到内核空间
// 从页表pagetable的srcva地址处以字节为单位拷贝数据到dst
// 直到遇见空字符或达到最大数量
// 成功时返回0,失败时返回-1
int
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
  uint64 n, va0, pa0;
  // 是否遇到空字符的标志位,为0表示没有遇到
  int got_null = 0;

  // 如果没有遇到空字符或尚未达到最大字符限制
  while(got_null == 0 && max > 0){
    // 找出srcva对应页的起始物理地址
    va0 = PGROUNDDOWN(srcva);
    pa0 = walkaddr(pagetable, va0);

    // 如果查询的物理地址为0,表示出错,返回-1
    if(pa0 == 0)
      return -1;

    // 这里的逻辑和copyin一样,不再赘述
    // n表示的是本次迭代要复制的字节数
    n = PGSIZE - (srcva - va0);
    if(n > max)
      n = max;

    // 得到本次复制源字符串起始的物理地址
    char *p = (char *) (pa0 + (srcva - va0));

    // 完成n个字节的复制
    while(n > 0){
      // 如果已经遇到了空字符
      if(*p == '\0'){
        // 则给dst处也放置一个空字符
        // got_null标志位置为1,表示已经遇到空字符,并跳出循环 
        *dst = '\0';
        got_null = 1;
        break;
      // 如果没有遇到空字符,则拷贝当前字符
      } else {
        *dst = *p;
      }
      // 更新循环变量
      --n;
      --max;
      p++;
      dst++;
    }
    // 强制对齐srcva到下一个页面开始位置
    srcva = va0 + PGSIZE;
  }
  // 如果是因为遇到空字符而结束复制,则返回0表示正常
  // 如果是因为超过了最大允许复制的字节数量,则返回-1表示出错
  if(got_null){
    return 0;
  } else {
    return -1;
  }
}

03_copyout函数

1.函数的作用

copyout函数的主要作用是在内核态代码中,将数据从内核空间复制到用户进程的虚拟地址空间中。它会处理跨页的情况,并确保数据能够正确地写入用户空间的目标地址。

2.代码
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
// 译:从内核空间向用户空间拷贝数据
// 从src拷贝len长度的字节到pagetable页表的dstva位置处
// 成功时返回0,错误时返回-1
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
  uint64 n, va0, pa0;

  // 当字符还没有复制完毕时
  while(len > 0){
    // 找到对应当前虚拟地址dstva的物理地址pa0
    va0 = PGROUNDDOWN(dstva);
    pa0 = walkaddr(pagetable, va0);

    // 如果pa0为0,表示查找过程出错,返回-1
    if(pa0 == 0)
      return -1;

    // 这里的逻辑和copyin一样,处理非对齐的情况和防止复制超出范围
    // n表示的是当前循环可以复制的字节数量
    n = PGSIZE - (dstva - va0);
    if(n > len)
      n = len;
    memmove((void *)(pa0 + (dstva - va0)), src, n);

    // 更新循环变量
    // 强制对齐dstva,和copyin对齐srcva的做法一致,不再赘述
    len -= n;
    src += n;
    dstva = va0 + PGSIZE;
  }
  return 0;
}

踏上取经路,比抵达灵山更重要