Video failed to load

File descriptor

1203 words
6 minutes
File descriptor
  1. 1File descriptorThis article

How does the system keep track of the connection between the string filename argv[i] passed to open(), and the resulting integer file descriptor fd? What does the integer file descriptor number refer to?

Câu hỏi này bắt đầu xoay quanh vào implementation ở user/cat.c. Nếu nhìn vào code xử lý của cat

if((fd = open(argv[i], 0)) < 0){ ... }
cat(fd);
close(fd);

Từ các đoạn logic xử lý chính của cat thì có 1 câu hỏi? nếu mình truyền vào 1 string “README” thì làm sao kernel xử lý trả về là 1 interger fd, và mọi câu lệnh xử lý sau đó là số nguyên interger fd chứ không phải là chuỗi README. Nếu vậy thì câu hỏi thật sự cần quan tâm ở đầy là kernel làm sao có thể xử lý được mọi file mà chỉ cần thông qua 1 interger fd là đủ.

Ví dụ, sau khi thực hiện lệnh cat README trên user, thì systemcall open được gọi ra để xử lý. Đầu tiên nó lấy được 2 giá trị thông số là mode permission và chuỗi string README thông qua

argint(1, &omode);
n = argstr(0, path, MAXPATH)

Phần logic detail làm sao có thể từ user -> kernel sẽ được detail ở blog systemcall. Tư đó mình có dược permission của file và chuỗi README từ kernel.

Có được chuỗi “README” rồi, kernel dùng nó để tìm file thật sự nằm ở đâu:

if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);

Ở phần này chỉ cần biết namei nhận vào một chuỗi tên và trả về ip, một con trỏ tới struct inode đại diện cho file đó trong kernel. Cách nó tìm ra inode sẽ có bài riêng. Điều quan trọng là đây là lần duy nhất chuỗi “README” được dùng. Từ đây trở đi, kernel chỉ làm việc với con trỏ ip.

Câu hỏi được chuyển đổi từ README sang fd thành: con trỏ ip sang fd?

Kernel không đưa thẳng ip cho process. Nó bọc ip trong một cấu trúc khác, struct file (kernel/file.h):

struct file {
enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
int ref; // reference count
char readable;
char writable;
struct pipe *pipe; // FD_PIPE
struct inode *ip; // FD_INODE and FD_DEVICE
uint off; // FD_INODE
short major; // FD_DEVICE
};

Inode mô tả file. struct file mô tả một lần mở file: đang đọc tới byte thứ mấy (off), được đọc hay ghi (readable, writable). Nếu hai process cùng mở README, cả hai dùng chung một inode nhưng mỗi bên có struct file riêng, nên offset của bên này không ảnh hưởng bên kia.

Để ý trường type: một struct file không nhất thiết là file trên đĩa. Nó có thể là pipe (FD_PIPE) hoặc thiết bị như console (FD_DEVICE). Đây chính là lý do một fd có thể đại diện cho “mọi thứ”.

Tất cả struct file của toàn hệ thống nằm trong một mảng global ftable (kernel/file.c):

struct {
struct spinlock lock;
struct file file[NFILE]; // NFILE = 100
} ftable;

sys_open gọi filealloc() để lấy một ô trống (ref == 0) trong mảng này:

struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0;
}

Mỗi process có một bảng file đang mở của riêng nó, trong struct proc (kernel/proc.h):

struct file *ofile[NOFILE]; // Open files, NOFILE = 16

fdalloc tìm ô trống đầu tiên trong mảng này, đặt con trỏ f vào và trả về chỉ số của ô đó:

static int
fdalloc(struct file *f)
{
int fd;
struct proc *p = myproc();
for(fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd] == 0){
p->ofile[fd] = f;
return fd;
}
}
return -1;
}

Vậy là có câu trả lời: file descriptor chỉ là một chỉ số trong mảng p->ofile[] của process. Không có gì bí ẩn cả.

Với cat, ô 0, 1, 2 đã được dùng cho stdin, stdout, stderr (console), nên README thường nhận fd = 3.

Phần còn lại của sys_open điền thông tin vào struct file vừa cấp:

f->type = FD_INODE;
f->off = 0;
f->ip = ip;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
iunlock(ip);
end_op();
return fd;

User chỉ cầm số 3. Mọi thông tin thật (file nào, đọc tới đâu, có quyền gì) đều nằm trong kernel, và số 3 là chìa khóa để tìm lại chúng.

Hàm cat() trong user/cat.c gọi read(fd, buf, sizeof(buf)). Trong kernel, sys_read không cần biết tên file. Nó chỉ cần đổi fd ngược lại thành struct file qua argfd:

static int
argfd(int n, int *pfd, struct file **pf)
{
int fd;
struct file *f;
argint(n, &fd);
if(fd < 0 || fd >= NOFILE || (f=myproc()->ofile[fd]) == 0)
return -1;
if(pfd)
*pfd = fd;
if(pf)
*pf = f;
return 0;
}

Chỉ là myproc()->ofile[fd], kèm kiểm tra fd có hợp lệ và có đang được mở hay không. Sau đó:

uint64
sys_read(void)
{
struct file *f;
int n;
uint64 p;
argaddr(1, &p);
argint(2, &n);
if(argfd(0, 0, &f) < 0)
return -1;
return fileread(f, p, n);
}

fileread nhìn vào f->type để quyết định đọc từ đâu:

int
fileread(struct file *f, uint64 addr, int n)
{
int r = 0;
if(f->readable == 0)
return -1;
if(f->type == FD_PIPE){
r = piperead(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
...
r = devsw[f->major].read(1, addr, n);
} else if(f->type == FD_INODE){
ilock(f->ip);
if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
f->off += r;
iunlock(f->ip);
} else {
panic("fileread");
}
return r;
}

Đây là lời giải cho câu hỏi “làm sao chỉ với một số nguyên mà xử lý được mọi thứ”:

  • read(3, ...) với README đi vào nhánh FD_INODE, đọc từ đĩa rồi tăng f->off. Lần read sau sẽ đọc tiếp từ chỗ cũ, dù user không hề truyền offset.
  • read(0, ...) từ console đi vào nhánh FD_DEVICE.
  • read từ đầu đọc của pipe đi vào nhánh FD_PIPE.

Code của cat giống hệt nhau trong cả ba trường hợp. Sự khác biệt được giấu trong struct file. Nhờ vậy, cat README, cat < README hay echo hi | cat đều chạy được mà cat không cần biết mình đang đọc từ đâu.

Cuối cùng cat gọi close(fd), tức là tháo liên kết ra:

uint64
sys_close(void)
{
int fd;
struct file *f;
if(argfd(0, &fd, &f) < 0)
return -1;
myproc()->ofile[fd] = 0;
fileclose(f);
return 0;
}
  • ofile[fd] = 0 giải phóng ô trong bảng của process. Lần open sau có thể dùng lại số 3.
  • fileclose(f) giảm f->ref. Khi ref về 0, ô trong ftable được giải phóng và iput(ff.ip) trả lại tham chiếu tới inode.
  • ref cần thiết vì một struct file có thể được nhiều fd trỏ tới cùng lúc, ví dụ sau dup(), hoặc khi fork() copy ofile[] sang process con qua filedup. Chỉ khi không còn ai dùng thì struct file mới thật sự bị giải phóng.

Kernel không “nhớ” liên kết giữa chuỗi “README” và số 3. Chuỗi tên chỉ dùng một lần trong open để tìm ra inode. Sau đó kernel tạo ra một chuỗi con trỏ:

fd (int) → p->ofile[fd] → struct file { type, off, readable, writable, ip / pipe } → inode / pipe / device

Và trả cho user chỉ số của mắt xích đầu tiên. Mọi syscall sau đó (read, write, close, fstat, dup) đều bắt đầu bằng cùng một bước: myproc()->ofile[fd].

Comments

Profile Image of the Author
Art
When the world stopped, I started compiling.
Announcement
Welcome to my blog!
Latest Moments

No table of contents on this page

Table of Contents

No table of contents on this page