File descriptor
- 1File descriptorThis article
How does the system keep track of the connection between the string filename
argv[i]passed toopen(), and the resulting integer file descriptorfd? 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 = 16fdalloc 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 intfdalloc(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 intargfd(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 đó:
uint64sys_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:
intfileread(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ánhFD_INODE, đọc từ đĩa rồi tăngf->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ánhFD_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:
uint64sys_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] = 0giải phóng ô trong bảng của process. Lần open sau có thể dùng lại số 3.fileclose(f)giảmf->ref. Khi ref về 0, ô trongftableđượ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 filecó thể được nhiều fd trỏ tới cùng lúc, ví dụ saudup(), hoặc khifork()copyofile[]sang process con quafiledup. Chỉ khi không còn ai dùng thìstruct filemớ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 / deviceVà 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].










