文件描述符(File Descriptor)
In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique identifier (handle) for a file or other input/output resource, such as a pipe or network socket.
File descriptors typically have non-negative integer values, with negative values being reserved to indicate “no value” or error conditions.
File descriptors are a part of the POSIX API. Each Unix process (except perhaps daemons) should have three standard POSIX file descriptors, corresponding to the three standard streams - STDIN (standard input)
, STDOUT (standard output)
and STDERR (standard error)
.
Integer value | Name | <unistd.h> symbolic constant | <stdio.h> file stream |
---|---|---|---|
0 | Standard input | STDIN_FILENO | stdin |
1 | Standard output | STDOUT_FILENO | stdout |
2 | Standard error | STDERR_FILENO | stderr |
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于 UNIX、Linux 这样的操作系统。
基于*nix万物皆文件的哲学,每个物理硬件设备都会在文件系统中体现为一个文件(位于/dev
下)。内核以将硬件设备抽象成文件的方式,进而向用户提供统一的设备调用接口。这样,用户就可以直接与硬件设备交互(而无需关心这个设备底层是如何实现这个具体的操作的)
文件描述符与打开文件之间的关系
内核维护的3个数据结构
In the traditional implementation of Unix, file descriptors index into a per-process file descriptor table maintained by the kernel, that in turn indexes into a system-wide table of files opened by all processes, called the file table. This table records the mode with which the file (or other resource) has been opened: for reading, writing, appending, and possibly other modes. It also indexes into a third table called the inode table that describes the actual underlying files. To perform input or output, the process passes the file descriptor to the kernel through a system call, and the kernel will access the file on behalf of the process. The process does not have direct access to the file or inode tables.
- 进程级文件描述符表(file descriptor table)
- 系统级全局文件表(global file table)
- 文件系统i-node表(i-node table)
每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开,也可以在同一个进程中被多次打开。
进程级文件描述符表(file descriptor table)
操作系统内核为每个进程都维护一张**进程级文件描述符表(file descriptor table)**以记录着所有的文件描述符,该表每一条目都记录了单个文件描述符的相关信息,包括:
- 控制标志(flags),目前内核仅定义了一个,即
close-on-exec
- 打开文件描述体指针
系统级全局文件表(global file table)
同时,内核对所有打开的文件的文件维护有一个系统级全局文件表(global file table)。表中各条目称为打开文件描述体(open file description),存储了与一个打开文件相关的全部信息,包括:
- 文件偏移量(file offset):调用
read()
和write()
更新,调用lseek()
直接修改 - 状态标示(status flags):由
open()
调用设置,例如:只读、只写或非阻塞(nonblocking)等 - inode 值
inode表
文件系统会为存储于其上的所有文件(包括目录)维护一个 inode 表,每个 inode 节点包含以下信息:
- 文件类型(file type),可以是常规文件、目录、套接字或
FIFO
- 访问权限
- 文件锁列表(file locks)
- 文件大小
- 等等
inode 存储在磁盘设备上,内核在内存中维护了一个副本,这里的i-node表为后者。副本除了原有信息,还包括:引用计数(从打开文件描述体)、所在设备号以及一些临时属性,例如文件锁。
Opeartions
Opening or Creating a File in C
To manipulate a file in C, we must first inform the operating system of our intentions with the open
function of the <fcntl.h>
library. This system call allows us to open an existing file, or create the file if it doesn’t already exist. We must at least specify the path towards the file we’d like to open, as well as the way in which we want to access it:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
So there are two versions of this open
system call. Their parameters are:
- pathname: the path towards the file in the form of a string of characters,
- flags: an integer representing the flags indicating the access mode, which we will take a closer look at below,
- mode: an integer representing the permissions to give to the file upon creation. This is an “optional” parameter that will be ignored if we don’t ask to create the file if it doesn’t already exist.
The open
function returns the newly created file descriptor if the file was successfully added to the inode and open file tables. This file descriptor will typically be larger than 2, since fds 0, 1, and 2, are reserved for the standard input, output and error respectively. However, if there is any issue, for example if we ask to open a file that doesn’t exist, or a file we don’t have permissions for, open
will return -1.
Closing a File Descriptor in C
When we are done manipulating a file, we must of course de-reference its file descriptor with the close
function of the <unistd.h>
library. Its prototype could not be simpler:
int close(int fd);
We supply it with a file descriptor and the system de-references it, and, if no other process has that file opened, deletes it from its open file and inode tables. Upon success, the close
function returns 0, but on failure, it returns -1 and sets errno to indicate the error.
However, the close function only closes the file descriptor, it does not delete the file itself! That is unlink
’s prerogative.
Duplicating File Descriptors with dup/dup2
It can sometimes be useful to duplicate a file descriptor in order to save it as a backup or replace another one. This is the case, for example, in the context of a standard input or output redirection towards a file.
The dup
and dup2
system calls from the <unistd.h>
library enable us to duplicate a file descriptor. Here are their prototypes:
int dup(int oldfd);
int dup2(int oldfd, int newfd);
Both take the file descriptor we want to duplicate ( oldfd) as a parameter and return the new file descriptor, or -1 in case of error. The difference between the two is that dup
automatically chooses the smallest unused number for the new file descriptor, whereas with dup2
, we can specify which number we want ( newfd).
We have to keep in mind that dup2
is going to try to close the newfd if it is in use before transforming it into a copy of oldfd. However, if oldfd is not a valid file descriptor, the call will fail and newfd will not be closed. If oldfd and newfd are identical and valid, dup2
will just return the newfd without doing anything further.
Demo
将标准错误(stderr)重定向到文件。以下是一个简单的示例,演示如何将标准错误输出重定向到一个文件,这意味着内容将不会被输出到标准错误默认的输出文件(控制台)了。
What happens if a process forks
When a process forks, all the descriptors are “duplicated” in the child process. If any of the descriptors are marked close-on-exec, then after the parent forks but before the child execs, the descriptors in the child marked as close-on-exec are closed and will no longer be available to the child process. The parent can still continue using the descriptor but the child wouldn’t be able to use it once it has exec*-ed.*
https://copyconstruct.medium.com/the-method-to-epolls-madness-d9d2d6378642
Reference
-
《Linux System Programming Talking Directly to the Kernel and C Library》
-
Lec 21: File System, Kernel Data Structures, and Open Files - https://www.usna.edu/Users/cs/aviv/classes/ic221/s16/lec/21/lec.html
-
https://www.codequoi.com/en/handling-a-file-by-its-descriptor-in-c/
-
https://copyconstruct.medium.com/the-method-to-epolls-madness-d9d2d6378642
-
File descriptor - https://www.computerhope.com/jargon/f/file-descriptor.htm