cpp
// Create a new process, copy the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
// increment reference counts on open file descriptors.
// 这一块是做什么的?
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
release(&np->lock);
acquire(&wait_lock);
np->parent = p;
release(&wait_lock);
acquire(&np->lock);
np->state = RUNNABLE;
release(&np->lock);
return pid;
}
功能解释
-
文件描述符的复制:
for(i = 0; i < NOFILE; i++)逐个遍历父进程的文件描述符表(p->ofile),NOFILE是文件描述符的最大数量。if(p->ofile[i])检查当前文件描述符(p->ofile[i])是否被父进程打开,只有打开的文件才需要复制。np->ofile[i] = filedup(p->ofile[i]);将父进程的每个有效文件描述符(p->ofile[i])复制到子进程(np->ofile[i])中。filedup函数的作用是增加文件引用计数,以便文件资源能被父子进程共享而不冲突。
-
工作目录的复制:
np->cwd = idup(p->cwd);将父进程的当前工作目录(p->cwd)复制给子进程,并增加目录的引用计数。idup函数的作用是增加目录的引用计数,以便父子进程可以共享同一个工作目录,同时避免一个进程修改目录状态影响到另一个进程。
通俗解释
当一个进程调用 fork() 时,会创建一个子进程,该子进程需要与父进程共享文件资源(例如打开的文件、工作目录等)。为了实现这一点,fork() 需要将父进程中所有打开的文件描述符都复制到子进程中。
- 每个文件描述符都对应一个文件对象,当我们复制文件描述符时,其实并不是创建一个新的文件对象,而是增加文件对象的引用计数,让父子进程可以同时使用它。
- 工作目录也是类似的,父子进程共享相同的目录对象,通过引用计数来管理资源的共享。
cpp
// Create a new process, copy the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
// copy saved user registers.
*(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);
np->cwd = idup(p->cwd);
safestrcpy(np->name, p->name, sizeof(p->name));
pid = np->pid;
release(&np->lock);
acquire(&wait_lock);
np->parent = p;
release(&wait_lock);
acquire(&np->lock);
np->state = RUNNABLE;
//TODO
for(int i=0; i < VMASIZE; i++)
{
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));
if(p->vma[i].f) filedup(p->vma[i].f);
}
//TODO end
release(&np->lock);
return pid;
}
解释新增代码
在原始的 fork() 实现中,只考虑了复制内存页和文件描述符,但当进程增加了 VMA 功能(比如 mmap 支持),就需要在 fork() 时把父进程的 VMA 结构一并复制到子进程。这里的 TODO 部分完成了这个目的。
cpp
for(int i=0; i < VMASIZE; i++)
{
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));
if(p->vma[i].f) filedup(p->vma[i].f);
}
新增代码的详细解释
-
遍历和复制 VMA:
for(int i=0; i < VMASIZE; i++):遍历 VMA 数组,VMASIZE是进程支持的最大 VMA 数量。memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));:将父进程p的第i个 VMA 结构复制到子进程np的相应位置。- 这样做是为了让子进程拥有与父进程相同的 VMA 设置,使得
mmap相关的映射关系可以在子进程中继续有效。
-
增加 VMA 文件的引用计数:
if(p->vma[i].f) filedup(p->vma[i].f);:如果父进程的 VMA 关联了一个文件(p->vma[i].f非空),则通过filedup函数增加文件的引用计数。- 这样做可以确保子进程的 VMA 正确引用到同一个文件,并且当父子进程使用映射时,共享同一个文件对象,防止因为文件关闭导致子进程的映射失效。
1. -> 操作符
- 含义 :
->是结构体指针访问操作符,用于访问指针指向的结构体中的成员。 - 使用方式 :
指针变量->成员名 - 例子 :在
np->vma[i]中,np是一个指向proc结构体的指针,vma是proc结构体中的一个成员。np->vma[i]表示访问np指向的proc结构体的vma数组的第i个元素。
2. . 操作符
- 含义 :
.是结构体成员访问操作符,用于访问结构体变量中的成员。 - 使用方式 :
结构体变量.成员名 - 例子 :
p->vma[i].f中,p->vma[i]是一个vma结构体,.f用于访问vma结构体中的f成员(假设f是vma结构体中的一个文件指针)。
3. & 操作符
- 含义 :
&是取地址符号,用于获取变量的地址。 - 使用方式 :
&变量名 - 例子 :在
&p->vma[i]中,p->vma[i]是一个vma结构体,&p->vma[i]表示获取该结构体的地址。
4. memmove 函数
- 功能 :
memmove用于内存拷贝,将源地址的内容复制到目标地址,可以安全地处理重叠区域。 - 原型 :
void *memmove(void *dest, const void *src, size_t n);dest:目标地址,即将数据复制到的地址。src:源地址,即从哪里复制数据。n:要复制的字节数。
- 例子 :
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));表示将父进程的第i个vma结构体数据复制到子进程对应的vma结构体中。
5. filedup 函数
- 功能 :
filedup用于增加文件的引用计数,让多个进程能够共享同一个文件对象,而不需要各自打开新的文件。 - 使用 :
filedup(p->vma[i].f)中,如果p->vma[i].f是一个指向文件的指针,filedup会让f文件的引用计数加 1,确保文件在父子进程中可以同时使用。
6. [i]:数组索引
- 含义 :
[i]是数组索引,用于访问数组中第i个元素。 - 例子 :
p->vma[i]中,p->vma是一个数组,[i]表示数组中的第i个元素。
cpp
for(int i=0; i < VMASIZE; i++)
{
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));
if(p->vma[i].f) filedup(p->vma[i].f);
}
-
for(int i=0; i < VMASIZE; i++):这是一个循环,从i=0到i=VMASIZE-1,每次循环内会执行复制 VMA 的操作。 -
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));:&p->vma[i]表示获取父进程p的vma[i]的地址。&np->vma[i]表示获取子进程np的vma[i]的地址。sizeof(p->vma[i])表示要复制的数据大小,即每个vma结构体的大小。memmove会将p->vma[i]的数据复制到np->vma[i]中。
-
if(p->vma[i].f) filedup(p->vma[i].f);:p->vma[i].f是父进程vma[i]结构体中的文件指针f。if(p->vma[i].f)检查f是否为非空,如果f不为空,说明vma[i]关联了一个文件。filedup(p->vma[i].f);增加该文件的引用计数,确保子进程也可以安全使用这个文件。
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));这一行为啥不是比如说直接用等号或者copy啥的
这是因为 memmove 更适合在 C 语言中处理结构体的内存复制。直接使用等号 = 或某些 copy 函数(如 strcpy)可能无法正确完成结构体的深层次复制。以下是一些原因:
1. memmove 适合复制结构体数据块
memmove 是一种低层次的内存复制函数,可以直接在内存级别将一个区域的数据复制到另一个区域。它能够高效地复制任意类型的数据结构,包括数组、结构体、或者其他复杂类型。相比于简单的赋值操作 =,memmove 可以确保:
- 深层次复制 :即使结构体中包含指针,
memmove也会将指针的内容(而不是地址)复制到目标区域。 - 大小灵活 :通过
sizeof可以精确控制复制的字节数。
2. 为什么不能直接使用 =?
= 在 C 语言中只能对基础类型 或指针进行赋值,不能直接用于结构体数组或复杂类型的深层次复制。直接使用 = 可能会导致不完全的复制或错误的指针引用。下面是一些具体原因:
- 结构体复制 :在
C中,如果两个结构体指针a和b,a = b只会复制指针本身,即它们将指向同一个地址,而不是复制指向的数据本身。这会导致父子进程的vma数组引用同一个内存区域,这并不是我们期望的行为。 - 指针成员问题 :如果结构体中有指针成员(如文件指针
f),直接赋值会导致父子结构体的指针指向同一块内存,而不是复制数据。
3. memmove 和 memcpy 的选择
memmove 与 memcpy 的功能类似,但 memmove 能更安全地处理重叠内存区域 ,保证复制过程中的数据不会被破坏。因此,在一些情况下,memmove 是更安全的选择(尽管在这里没有明显的重叠问题)。
cpp
memmove(&np->vma[i], &p->vma[i], sizeof(p->vma[i]));