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]));