File descriptor

Trong hệ điều hành máy tính Unix và kiểu Unix, file descriptor (tạm dịch: mô tả tập tin), thường được viết tắt là fd hay fildes, là định danh (handler) độc nhất cho tập tin hoặc tài nguyên đầu vào/đầu ra khác, chẳng hạn như pipe hoặc socket mạng.

File descriptor thường có giá trị nguyên không âm, còn giá trị âm thì được dành để biểu thị trạng thái "không có giá trị" hoặc tình trạng bị lỗi.

File descriptor là một phần của POSIX API. Mỗi tiến trình Unix (ngoại trừ daemon) phải có 3 file descriptor POSIX tiêu chuẩn, tương ứng với 3 luồng tiêu chuẩn:[a]

Giá trị số nguyên Tên Hằng biểu trưng <unistd.h>[1] Luồng tập tin <stdio.h>[2]
0 Đầu vào tiêu chuẩn[b] STDIN_FILENO stdin
1 Đầu ra tiêu chuẩn[c] STDOUT_FILENO stdout
2 Đầu lỗi tiêu chuẩn[d] STDERR_FILENO stderr

Tổng quan

Hình vẽ thể hiện các file descriptor cho tiến trình đơn, bảng file và bảng inode. Lưu ý rằng nhiều file descriptor có thể cùng tham chiếu vào một mục bảng file (ví dụ do kết quả của lệnh gọi hệ thống dup[3]:104), và lưu ý rằng nhiều mục bảng file có thể cùng tham chiếu vào một inode (nếu nó được mở nhiều lần; bảng inode đây đã được đơn giản hóa theo hướng mỗi inode được đại diện bằng một tên file, trong thực tế inode có thể có nhiều tên). File descriptor số 3 không tham chiếu đến bất kỳ mục nào trong bảng file, báo hiệu rằng nó đã bị đóng.

Trong bản thực hiện truyền thống của Unix, file descriptor lập chỉ mục vào trong bảng file descriptor dành riêng cho mỗi tiến trình, kernel có vai trò bảo quản bảng này, kế đến lập chỉ mục vào trong một bảng liệt kê các file đang được mở từ tất cả tiến trình, bảng đó gọi là bảng file và dùng chung cho toàn hệ thống. Bảng này ghi lại chế độ mà file (hoặc tài nguyên khác) được mở lên: để đọc, để ghi, để phụ chú,[e] và có thể là chế độ nào đó khác. Nó cũng lập chỉ mục vào một bảng thứ ba được gọi là bảng inode mô tả tập tin thực tế nằm bên dưới.[3] Để thi hành lên đầu vào hoặc đầu ra thì tiến trình sẽ truyền file descriptor vào kernel thông qua lệnh gọi hệ thống[f] và kernel sẽ truy cập file giùm cho quá trình. Tiến trình không có cách nào truy cập trực tiếp vào file hoặc bảng inode.

Trên Linux, tập hợp các file descriptor được mở trong tiến trình thì có thể được truy cập theo đường dẫn /proc/PID/fd/, trong đó PID là định danh tiến trình. File descriptor /proc/PID/fd/0stdin, /proc/PID/fd/1stdout/proc/PID/fd/2stderr. Tiến trình đang chạy còn có thể truy cập các file descriptor của chính mình thông qua các thư mục lối tắt là /proc/self/fd/dev/fd thay vì dùng đường dẫn cụ thể như thế kia.[4]

Trong các hệ thống kiểu Unix, file descriptor có thể tham chiếu đến bất kỳ kiểu file Unix nào có mang tên trong hệ thống file. Không chỉ các file thông thường mà còn bao gồm cả thư mục, thiết bị khối[g] và thiết bị kí tự[h] (còn được gọi là "file đặc biệt"), Unix domain socket[i]named pipe. File descriptor cũng có thể tham chiếu đến những đối tượng khác mà bình thường không tồn tại trong hệ thống file, chẳng hạn như anonymous pipesocket mạng.

Cấu trúc dữ liệu FILE trong thư viện I/O tiêu chuẩn C thường bao gồm file descriptor cấp thấp cho các loại đối tượng như trên trong các hệ thống kiểu Unix. Cấu trúc dữ liệu có tính tổng thể đấy mang lại thêm sự trừu tượng và nó được gọi với cái tên khác là file handle.

Các thao tác trên file descriptor

Dưới đây liệt kê các thao tác (hàm) điển hình lên file descriptor trên các hệ thống kiểu Unix hiện đại. Hầu hết các hàm này đều được khai báo trong header <unistd.h>, nhưng một số thì lại nằm trong header <fcntl.h>.

Tạo ra file descriptor

  • open()
  • creat()[5]
  • socket()
  • accept()
  • socketpair()
  • pipe()
  • epoll_create() (Linux)
  • signalfd() (Linux)
  • eventfd() (Linux)
  • timerfd_create() (Linux)
  • memfd_create() (Linux)
  • userfaultfd() (Linux)
  • fanotify_init() (Linux)
  • inotify_init() (Linux)
  • clone() (khi kèm cờ CLONE_PIDFD, Linux)
  • pidfd_open() (Linux)
  • open_by_handle_at() (Linux)

Phái sinh ra file descriptor

  • dirfd()
  • fileno()

Thao tác trên file descriptor đơn

  • read(), write()
  • readv(), writev()
  • pread(), pwrite()
  • recv(), send()
  • recvfrom(), sendto()
  • recvmsg(), sendmsg() (cũng được dùng để gửi FD sang tiến trình khác thông qua Unix domain socket)
  • recvmmsg(), sendmmsg()
  • lseek(), llseek()
  • fstat()
  • fstatvfs()
  • fchmod()
  • fchown()
  • ftruncate()
  • fsync()
  • fdatasync()
  • fdopendir()
  • fgetxattr(), fsetxattr() (Linux)
  • flistxattr(), fremovexattr() (Linux)
  • statx (Linux)
  • setns (Linux)
  • vmsplice() (Linux)
  • pidfd_send_signal() (Linux)
  • waitid() (khi kèm kiểu ID là P_PIDFD, Linux)
  • fdopen() (hàm stdio: chuyển đổi file descriptor thành FILE*)
  • dprintf() (hàm stdio: in ra file descriptor)

Thao tác trên nhiều file descriptor

  • select(), pselect()
  • select(), pselect()
  • poll(), ppoll()
  • epoll_wait(), epoll_pwait(), epoll_pwait2() (Linux, nhận vào một epoll filedescriptor đơn để chờ nhiều file descriptor khác)
  • epoll_ctl() (dành cho Linux)
  • kqueue() (dành cho hệ thống dựa trên BSD).
  • sendfile()
  • splice(), tee() (cho Linux)
  • copy_file_range() (cho Linux)
  • close_range() (cho Linux)[6]

Thao tác trên bảng file descriptor

Hàm fcntl() được dùng để làm các thao tác khác nhau trên file descriptor, tùy vào 'đối số lệnh' được truyền cho nó. Có các lệnh để truy xuất và thiết đặt những thuộc tính liên đới với file descriptor, bao gồm F_GETFD, F_SETFD, F_GETFLF_SETFL.

  • close()
  • closefrom() (chỉ có ở BSD và Solaris; xóa hết những file descriptor lớn hơn hoặc bằng trị số chỉ định nào đó)
  • dup() (nhân bản file descriptor có sẵn nào đó, đảm bảo tạo ra file descriptor có trị số nhỏ nhất sẵn có)
  • dup2(), dup3() (đóng fd1 nếu cần thiết, và khiến file descriptor fd1 trỏ đến file đang mở của fd2)
  • fcntl (F_DUPFD)

Thao tác sửa đổi trạng thái tiến trình

  • fchdir() (thiết đặt thư mục hiện hành của tiến trình dựa trên file descriptor thư mục nào đó)
  • mmap() (ánh xạ một khoảng của file vào trong không gian địa chỉ của tiến trình)

Khóa file

  • flock()
  • fcntl() (F_GETLK, F_SETLKF_SETLKW)
  • lockf()

Socket

  • connect()
  • bind()
  • listen()
  • accept() (tạo file descriptor mới cho kết nối truyền tới nào đó)
  • getsockname()
  • getpeername()
  • getsockopt()
  • setsockopt()
  • shutdown() (shut down một hoặc cả hai đầu của kết nối song công toàn phần nào đó)

Tạp vụ

  • ioctl() (làm được nhiều thao tác lặt vặt trên một file descriptor đơn, thường hay được liên đới với một thiết bị nào đó)

Các thao tác sẽ có

Một loạt các thao tác mới trên file descriptor đã được bổ sung vào nhiều hệ thống kiểu Unix hiện đại và cũng đã được bổ sung vào khá nhiều thư viện C, nhằm để được chuẩn hóa trong một phiên bản tương lai nào đó của POSIX.[7] Hậu tố at trong tên của hàm biểu thị rằng hàm đấy nhận vào thêm một đối số ở vị trí thứ nhất, đối số đó cung cấp file descriptor ứng với thư mục cơ sở để từ đó phân giải đường dẫn tương đối, còn nếu dùng hàm mà tên không có hậu tố at thì tương đương với việc gọi hàm tương ứng có hậu tố at và truyền file descriptor ứng với thư mục làm việc[j] hiện hành làm tham số thứ nhất. Mục đích của các thao tác mới này là để phòng ngừa một số loại tấn công TOCTOU nhất định.

  • openat()
  • faccessat()
  • fchmodat()
  • fchownat()
  • fstatat()
  • futimesat()
  • linkat()
  • mkdirat()
  • mknodat()
  • readlinkat()
  • renameat()
  • symlinkat()
  • unlinkat()
  • mkfifoat()
  • fdopendir()

File descriptor làm công năng

Theo nhiều cách thì file descriptor của Unix vận hành như một dạng công năng. Công năng như vậy có thể được truyền giữa các tiến trình thông qua Unix domain socket bằng cách sử dụng lệnh gọi hệ thống sendmsg(). Tuy nhiên, lưu ý rằng cái thực sự được truyền đi chính là tham chiếu đến "open file description" có trạng thái khả biến đổi (offset của file, tình trạng của file, và cờ truy cập của file). Điều này gây rắc rối cho việc sử dụng file descriptor làm công năng sao cho an toàn, vì khi các chương trình chia sẻ quyền truy cập vào cùng "open file description" thì chúng có thể can thiệp vào việc sử dụng file của nhau như bằng cách thay đổi offset của file hoặc thay đổi file thành blocking hoặc là non-blocking, chẳng hạn vậy.[8][9] Trong các hệ điều hành được đặc biệt thiết kế để làm hệ thống công năng, rất hiếm khi có bất kỳ trạng thái khả biến đổi nào mà bản thân nó có liên đới với công năng nào đó.

Bảng file descriptor của tiến trình trong Unix là một ví dụ về C-list (danh sách công năng).

Ghi chú thuật ngữ

  1. ^ Standard stream
  2. ^ Standard input
  3. ^ Standard output
  4. ^ Standard error
  5. ^ Appending
  6. ^ System call
  7. ^ Block device
  8. ^ Character block
  9. ^ Unix domain socket
  10. ^ Working directory

Tham khảo

  1. ^ The Open Group. “The Open Group Base Specifications Issue 7, IEEE Std 1003.1-2008, 2016 Edition”. Truy cập ngày 21 tháng 9 năm 2017.
  2. ^ The Open Group. “The Open Group Base Specifications Issue 7, IEEE Std 1003.1-2008, 2016 Edition”. <stdio.h>. Truy cập ngày 21 tháng 9 năm 2017.
  3. ^ a b Bach, Maurice J. (1986). The Design of the UNIX Operating System (ấn bản thứ 8). Prentice-Hall. tr. 92–96. ISBN 9780132017992.
  4. ^ “Devices - What does the output of 'll /Proc/Self/Fd/' (From 'll /Dev/Fd') mean?”.
  5. ^ The Open Group. “The Open Group Base Specifications Issue 7, IEEE Std 1003.1-2008, 2018 Edition – creat”. Truy cập ngày 11 tháng 4 năm 2019.
  6. ^ Stephen Kitt, Michael Kerrisk. “close_range(2) — Linux manual page”. Truy cập ngày 22 tháng 3 năm 2021.
  7. ^ Extended API Set, Part 2. The Open Group. tháng 10 năm 2006. ISBN 1931624674.
  8. ^ Brinkmann, Marcus (4 tháng 2 năm 2009). “Building a bridge: library API's and file descriptors?”. cap-talk. Bản gốc lưu trữ ngày 30 tháng 7 năm 2012. Truy cập ngày 21 tháng 9 năm 2017.
  9. ^ de Boyne Pollard, Jonathan (2007). “Don't set shared file descriptors to non-blocking I/O mode”. Truy cập ngày 21 tháng 9 năm 2017.