Lecture 4: System calls
Note: Reading these lecture notes is not a substitute for watching the lecture. I frequently go off script, and you are responsible for understanding everything I talk about in lecture unless I specify otherwise.
Layers of Abstraction in Filesystems
The Unix V6 filesystem comes from the 1970s, yet, as you can see, there is already a large amount of complexity. One common paradigm for dealing with complexity is layering. Here’s a breakdown of layering involved in this filesystem:
- The hardware layer involves the actual mechanical and electrical details of making a disk work. There is so much complicated circuitry and physics involved here, and, in fact, the hardware layer is broken into many layers by the people who work on it.
- The block layer is among the lowest software layers. It manages the details of communication with the disk, enabling us to read or write sectors. This layer sits underneath almost every filesystem operation, and above this layer, we don’t want to have to think about the nitty-gritty of talking to the hardware.
- The inode layer supplies higher layers with metadata about files. When we need to know which block number is storing a particular portion of a file, the inode layer tells us that. Above this layer, we don’t want to think about the mechanics of retrieving or updating metadata (which isn’t simple, considering inodes are smaller than sectors).
- The file layer supplies higher layers with actual file contents. We request some number of bytes from a file at a particular offset, and the file layer populates a buffer with that information.
- The filename layer allows us to find a file within a directory. Given a filename and a directory presumably containing that file, this layer figures out what inode stores the information for that file.
- Lastly, the pathname layer implements full absolute path lookups, starting
from the root directory. If you hand
/usr/class/cs110/hello.txt
to the pathname layer, it will utilize the filename layer beneath it, looping through the components of that full path, until it findshello.txt
.
On top of these 6 layers sit many application layers that use the filesystem without having to think about how it works.
Not only does layering provide us with a means of breaking down complexity, but it also has some nice properties if we ever want to modify the system to do something new. Let’s say we want to create a networked filesystem. Instead of having to write it from scratch, we can keep everything except for the hardware and block layers, replacing those with some layers that deal with network communication.
Unix employs this principle everywhere. As you will see later in the course, many resources are made to look like files (even though they aren’t files) so that we can control them using the file abstractions we’ve developed. Your computer interacts with your terminal window, printer, Bluetooth radio, and even (to a certain extent) CPU as if they were files, even though that is certainly not the case. As we will see towards the end of the class, the layering principle is equally pervasive in the land of networking.
System Calls
There is a reason you have probably never written code that manages raw sectors
on disk: you can’t. It would be dangerous if you could; you might
unintentionally corrupt some critical sectors, rendering your entire filesystem
unusable. Worse, malicious code could access or alter data that it isn’t
supposed to have access to. For example, unprivileged code isn’t allowed to
read or modify the /etc/passwd
or /etc/shadow
files (which store
information about passwords on your system), but if a program were allowed to
access the raw filesystem, it could circumvent those permission checks.
We need the operating system to perform these privileged operations on our behalf, and to mediate access to the filesystem so that it can block malicious behavior. We interact with the operating system, asking it to do privileged operations on our behalf, through functions known as system calls (syscalls for short).
Filesystem-related syscalls
int open (const char *filename, int flags, ...)
- This tells the OS, “hey, I would like to work with this file.”
flags
tellsopen
how we’d like to interact with the file. (Are we reading or writing? If writing, what do we do if the file already exists? Etc)- If we are writing to a file that doesn’t already exist and we wish to create it, a third argument can be used to specify the new file’s permissions.
- This function returns a number, which is a file descriptor. This fd can be passed to other filesystem-related syscalls to work with the file we’ve just opened.
ssize_t read(int fd, char* buffer, size_t count)
- Given a file descriptor (returned by
open
), attempt to readcount
bytes from the file intobuffer
- Returns the number of bytes actually read from the file.
- Given a file descriptor (returned by
ssize_t write(int descriptor, char buf[], size_t count)
- Given a file descriptor, attempt to write
count
bytes frombuf
to the file - Returns the number of bytes successfully written to the file.
- Given a file descriptor, attempt to write
int close(int descriptor)
- Tell the operating system we’re done using a particular file. Frees resources that the operating system was using to keep track of the file
Many IO-related stdlib functions are layered on top of syscalls
printf
might seem like some magical core function to you, but it’s actually
built on top of syscalls in ways that you can easily understand now. I can
write a “hello world” program without using printf
:
int main(int argc, char *argv[]) {
char* output = "Hello world\n";
write(STDOUT_FILENO, output, 12);
return 0;
}
(Note: 12 is the length of the output
string.)