What are file descriptors in Unix? A glimpse of how Unix works.
4 min read
Hello guys today we're going to be going a bit deep into what are file descriptors in the operating system and by understanding them we're going to talk a bit about how the terminal itself works. Let's work!
What are file descriptors?
File descriptors are nothing but an index i.e 0, 1, 2. When initializing a process it is created for you. Every process has its own file descriptor, now that we got that out of the way what do these indexes point to? These indexes point to an entry in a table called System Open File Table, which is global for all processes and not specific to a single one. What this table does is it tracks every open file by each process and it has information such as the current offset of the file, the open mode of the file (Read only, Read Write, Write only). If the same process opens the same file twice it will have 2 entries in the open file table and so on. Now what does the entries in the open file table point to afterwards? They point to another data structure called the V Node Table. This table has an entry for each file in the file system and contains information related to each file such as the UID of the owner, GroupId of its belonging group, the file size and the actual number of blocks the file uses, .. etc.
Connecting the dots
A terminal itself is a process with a PID, etc. When you run any program in the terminal what happens under the hood is that the terminal calls the fork() system call followed by an exec() call which then makes the program executing a child of the terminal process.
Before continuing, What does calling fork followed by exec even do? When you fork a process you literally duplicate it into a child process. The entire process gets duplicated and gets issued another PID. Then when calling exec after that what we do is replace the existing process with a new one, keeping the same PID. That way gives us more flexibility in configuring what we want to keep/change from the things that exec inherits. An example of things exec inherits are file descriptors and Process group and session IDs.
Also when we close a terminal running a child process. For example a web server. A signal is passed to the whole process group which has all the child processes too. That's the only reason the child gets killed on killing the parent. I just put this here so I don't forget it.
Every process created gets 3 default file descriptors on creation. stdin, stdout and sterr which are 0, 1, 2 respectively. These are input/output steams and the file descriptor always maps to files in the end, these input streams map to different files depending on what they are reading/writing from. There are device files for example, What even are device files? Since in Unix everything is considered a file, device files are any files associated with device drivers. What does this mean? it means that any device driver for hardware such as the keyboard, network driver, .. etc they all have device files associated to them which we can read() and write() to. The OS detects when we're reading and writing to a device file and proceeds in handling this special case. There exists other files that aren't device files such as /dev/null which is basically a black hole. all reads return end of file and all writes are discarded. These files all have special cases which make them act as files for abstraction.
There are 2 main types of device files. Character device files and Block device files, character device files are concerned with IO that return separate bytes as a stream of data e.g Keyboard. Whereas block files handle IO that reads from blocks of data, e.g hard drive. In the terminal the stdin is associated with our keyboard by default, hence it's a stream to a character device file.
So as long as you have a file descriptor, it probably supports reading, writing, etc. Other than stdin, stdout and stderr you have also socket descriptors and a lot more.
When we open a terminal it's initially waiting on the read() command ( waiting for us to type anything ) That's where stdin which is mapped to our keyboard as explained above comes in. When we type in the terminal for example lets say
This list command will fork and exec the ls program, now this new ls process inherits the file descriptors and hence has its stdout to the terminal window already, it executes the command and finishes then the parent process gets back it's control allowing for more commands to be typed in.
To be able to understand this previous sentence all the above was necessary just to get a simple understanding of how it all works under the hood.
This article took me some time to understand and hopefully I gave you some insight on how it all works under the hood! Hope you enjoyed it.