第六部分是实现基本的文件数据结构、标准输入输出文件和管道文件。相对来讲比较简单,不过因为每个进程的文件描述符表需要用一个既能动态扩增又能直接索引的数据结构,所以我顺便自己实现了一个简单的vector,并利用相同的思想实现了一个队列,使用非常简单,所以又把queue.h换掉了。
首先是vector和queue,我采用了倍增扩容的方法,即容量满了以后扩大一倍。为了保证空间足够,需要将其包含的数据类型的大小作为属性存入结构体,开辟空间和复制操作的时候都需要乘以该属性,这里以vector和queue的push函数为例:
void vector_push(struct vector *v, void *d) {
if (v->size == v->capacity) {
v->capacity <<= 1;
char *t = bd_malloc(v->capacity * v->dsize);
memcpy(t, v->buffer, v->size * v->dsize);
bd_free(v->buffer); v->buffer = t;
}
memcpy(v->buffer + (v->size++) * v->dsize, d, v->dsize);
}
void queue_push(struct queue *q, void *d) {
if (q->size == q->capacity) {
q->capacity <<= 1;
char *t = bd_malloc(q->capacity * q->dsize);
memcpy(t, q->buffer, q->tail * q->dsize);
memcpy(t + (q->capacity - (q->size - q->front)) * q->dsize,
q->buffer + q->front * q->dsize,
(q->size - q->front) * q->dsize);
q->front = q->capacity - (q->size - q->front);
bd_free(q->buffer); q->buffer = t;
}
memcpy(q->buffer + q->tail * q->dsize, d, q->dsize);
q->tail = (q->tail + 1) % q->capacity; q->size++;
}
vector的操作很直观,queue要麻烦一些,为了节省空间,queue用的是循环队列,当队列满时,头索引和尾索引必然相等,因为尾索引是向右增加,头索引是像左减少的,所以扩容的时候需要将原缓冲区最左侧到尾索引的数据复制到新缓冲区的最左侧,将原缓冲区头索引到最右侧的数据复制到新缓冲区的最右侧,这时候就得计算好复制到新缓冲区的起始位置。
然后是文件描述符的设计,rCore很好地利用了rust语言的面向对象特性,让File对象“继承”(我不确定rust语言里这个行为叫什么,大概类似于java里继承抽象类)管道、磁盘文件、设备等对象,这样File对象就只要实现一些最基本的方法和属性,比如定义文件写和文件读函数的指针,具体的属性和方法,如管道释放方法、管道读写权限、管道对应缓冲区、磁盘文件索引之类就通过“继承”赋予给File对象;xv6就比较朴素,在文件描述符里对应了多种多样的属性,但是没有定义函数指针,而是由统一的读写函数通过文件描述符里的文件类型属性判断。
我则综合了两者,给文件描述符定义了包括读、写、释放、复制多个函数的指针,这样在读、写、释放的系统调用中就不用判断文件类型,而是调用对应的函数指针即可,同时也在文件描述符中额外定义了一个void类型的指针,用来指向这个文件“绑定”的数据,比如是管道文件,这个指针就指向一个管道。在调用文件处理函数指针的时候把文件指针传进去,由不同的函数来对这个“绑定”指针进行不同的解释,算是一种伪泛型吧。
这里还有一点要注意的,就是文件的引用计数问题。这个问题对于rCore和xv6的意义不同。对于rCore来说,这个计数不是针对于单个的文件描述符,而是针对于文件描述符所产生的跨进程资源。比如管道文件,虽然每个进程各自持有,释放也各自释放就行,但是管道包含一个用来传输数据的缓冲队列,这个缓冲队列就是跨进程资源。这就需要引用计数,对于每个缓冲队列记录自己有多少个“管口”,当所有“管口”被关闭时,即没有管道引用它时,就要释放这个缓冲队列。这里rust又拿下一城:拥有内置了引用计数功能的智能指针,不需要显式地处理引用计数问题。再看看xv6,xv6是父子进程共享一个文件描述符,所以引用计数是文件描述符的一个属性,每次进程fork都会增加一次引用,释放文件会减少一次引用,当引用为0时将文件描述符及其对应的资源释放。
我在文件描述符和进程的关系上遵循的是rCore,即每个进程持有一个独立的文件描述符,而把引用计数属性放在需要被释放的结构体中,比如这里就给管道添加了引用计数属性,同时定义了管道释放和复制的方法对其指向的缓冲区引用计数进行修改,这一步就比较像xv6,然后把这两个函数填入文件描述符的函数指针中,由fork函数和文件关闭函数调用。
这里给出管道相关的创建、读写、复制、释放函数:
usize pipe_read(File *self, char *buffer, usize len) {
Pipe *p = (Pipe *)self->bind;
for (int i = 0; i < len; i++) {
while (queue_empty(&p->q)) {
if (p->wrefcnt == 0) {
buffer[i] = '\0'; return i;
}
suspend_current_and_run_next();
}
buffer[i] = *(char *)queue_front(&p->q); queue_pop(&p->q);
}
return len;
}
usize pipe_write(File *self, char *buffer, usize len) {
Pipe *p = (Pipe *)self->bind;
for (usize i = 0; i < len; i++) queue_push(&p->q, buffer + i);
return len;
}
void pipe_copy(File *self) {
Pipe *p = (Pipe *)self->bind;
if (self->read == illegal_rw) p->wrefcnt++; p->refcnt++;
}
void pipe_close(File *self) {
Pipe *p = (Pipe *)self->bind;
if (self->read == illegal_rw) p->wrefcnt--;
self->occupied = 0; p->refcnt--;
if (p->refcnt == 0) {
queue_free(&p->q); bd_free(p);
}
}
void make_pipe(usize *p) {
Pipe* t = bd_malloc(sizeof(Pipe));
queue_new(&t->q, 1); t->refcnt = 2; t->wrefcnt = 1;
File *f = alloc_fd(p); f->bind = (void *)t;
f->read = pipe_read; f->write = illegal_rw;
f->copy = pipe_copy; f->close = pipe_close;
f = alloc_fd(p + 1); f->bind = (void *)t;
f->read = illegal_rw; f->write = pipe_write;
f->copy = pipe_copy; f->close = pipe_close;
}
我这里Pipe结构体和rCore不太一样,rCore里Pipe对象的意思是“管口”,有额外的PipeRingBuffer对象表示管道本身。我这里Pipe就是管道本身,文件描述符中bind属性指向它,所以文件描述符本身就充当“管口”了。管道中还有一个wrefcnt属性表示写引用计数,在读函数中如果判断写引用计数为0,说明管道中不会再有新数据了,则直接返回已经读取的全部数据。
在复制和释放管道文件的时候,判断释放应该修改写引用计数的方法是判断该文件的读函数是否指向illegal_rw
,这个函数是一个直接返回错误的函数,用来填充该文件不支持的操作对应函数指针,如果管道文件的读函数指向illegal_rw
,说明这个管道文件不支持读取,是写管口,此时就应该修改写引用计数。