Scribe notes brought to you by Alina Chen & Matthew DeCoste
Table of Contents
a | b
File: an array or stream of bytes
Files are represented by file descriptors.
You can read from it (e.g., read(fd,buf,sizeofbuf)
)
Apparently read
isn't used enough, but let's move on.
Pros: | Cons: |
As a result, there are 2 kinds of files:
Stream-oriented (e.g., I/O, keyboard) | Random access: (e.g, disks) |
lseek doesn't work here |
The read
function works on either type of file.
Example code | What happens |
close(1);
|
i strerror(errno); stores details |
Example code | What happens |
int fd = open("foo", O_RDONLY);
|
read is actually reading from bar |
Example code | What happens |
1 for (i=0; i<N; i++)
|
Example code | What happens |
int fd = open("/dev/usb/flash01", O_RDONLY);
physically remove flash drive at this point in execution read(fd, buf, sizeofbuf);
|
read returns -1 with special errno (about lack of flash drive) |
Example code | What happens |
int fd = open("/tmp/foo", O_RDONLY);
|
NOTE: multithreading will mess this all up so we're assuming this is a single-threaded process.
Let's try using a system call...ideally it would work like this:
int fd = mktempfile(); //file won't be visible to file system/directory |
...Unfortunately, this system call doesn't actually exist.
We can try using a library function instead, but it won't be safe.
Here's a first version of the code:
1 int mktmpfile(void)
|
/tmp/foo
file and the fact /tmp/foo
does appear in the directory for a brief moment. This can cause a race condition with multiple function calls since the file would already be in existence during one of the calls.
Knowing these issues, here's a safer version of the code:
1 int mktempfile(void)
|
The race condition still exists between the first condition checking and the rest of the code, but it is narrower than before. The code also attaches the pid to the name of the file, which means multiple processes can call this function. Also, processes can create more than 1 temporary file even though they have the same pid because the name is unlinked after each file is created.
However, there is a problem, from the point of operations. Since the files are "invisible," we cannot see where the disk space is allocated to after unlinking. This means you don't know how much disk space you have left.
How is this relevant? The scenario given in class was something like this: You're writing a research paper while this function is being called multiple times in the background. When you finally click the Save button, a popup appears stating "No more disk space," although your directory hasn't changed from when you first started this paper. This occurs because the invisible files exist in your directory, and thus are taking up space, even if you can't see them.
Well, we can think of a solution to that. We remove the unlink code, so now all temporary files are visible.
Proceed to remove lines 12-13:
from the previous code.
if (0 <= fd)
unlink("foo");
Well now, what wasn't a problem before is now a problem! A process now cannot have multiple files, because they will have the same name.
Let's try fixing this...how about a random name generator?
1 int mktempfile(char *name, size_t size)
|
Welp. Now we have the same race condition as our first attempt...again. This time they appear during the while loop condition and open
function call (line 6-7).
Our current set of primitives can't solve this. There's this new flag called O_EXCL
. Basically, if the file exists, the call will fail.
We'll also replace the while loop with a different one so that we can avoid this failed call scenario.
1 int mktempfile(char *name, size_t size)
|
As you can see, it's difficult to find a robust solution, due to the waterbed effect - when you fix 1 problem, another appears in a different area.
There might be another way to solve this, but it's tricky and not generally used for this type of problem. In any case, this system is called locks. Here's a brief section about it.
Locking primitives exist in UNIX/POSIX, but are advisory and rarely used. Shells don't use these. Databases do. If you control almost everything, you can use this.
Use fcntl(fd, cmd, p)
, where cmd
is replaced by:
FSETLK
: get a lock, fails if lockedF_SETLKW
: set a lock, wait for unlockingF_GETLK
: checks if it is lockedp
is for:
struct float *
short, l type (regions)
- l_len
- l_short
F_SETLEASE(arg)
to set it) is notified when a file tries to open or change the file size (truncate). The arg
can be one of the following:
F_RDLOCK
: calling process is notified when the file is opened for writing or is truncated; can only be placed on a read-only fdF_WRLOCK
: calling process is notified when the file is opened for writing or is truncated; can only be placed on a write-only fdF_UNLOCK
: remove lease from file"It-sa me! Mario!"
Pipes are bounded buffers. If they were unbounded, they could potentially take up the whole disk space.
Pros: | Cons: |
lseek |
Here's what they might look like in a diagram:
Another thing to note: redirection (< >) precedence is higher than pipe precedence.
Scenario: | What happens: |
b reads an empty pipe | |
a tries to write to full pipe | |
a writes, but b closed its end of the pipe |
|
b reads, but a closed its end of the pipe |
|
a is done generating output, but forgets to close its end of the pipe | read hangs forever |
There are 3 configurations we can consider a pipe a|b is implemented in the shell. They are described by the tree diagrams below. Each node is the parent of the one below it.
Pipes are implemented something like this, which rules out the first possibility.
1 int fd[2];
|
Also, we want the exit status of the pipe to be set to the exit status of b. Doing this with the 2nd possible configuration would be too much work, so we use the 3rd configuration. The rewritten code looks like this:
1 int fd[2];
|
Use dup2(i,j)
to clone. Remember to close your pipes after you're done using them.
For example, if a exits and b reads(0,...), then the program can hang if b forgets to close fd[1].
Signals are ways to force code to respond to run-time situations.
Here's an example: you're up late one night, writing some last-second code, when the power goes out. Luckily, your desktop is attached to a UPS, so it hasn't completely lost power yet. Unfortunately, your electron-guzzler of a PC will drain the UPS in a matter of minutes. Ideally, this is where your computer comes in to save the day. The kernel needs to take a snapshot of RAM and store it on disk (for recovery after power is restored). When the power does eventually return, you would hope the kernel would restore everything to its rightful place, and that your various programs (and precious code) will return in exactly the same state as before.
So, will it? Well, not exactly. Obviously, network conditions and connections will not be the same upon revival, which could mess up some programs (cough EA games cough). There are also security implications if too much sensitive information is restored onto the screen in a new, insecure context. However, it's not the kernel's responsibility to address each and every possible consequence of this necessary action. At some point, user processes must be informed of the issue and allowed to take appropriate action.
Essentially the question is how to place new code into the already-existing user code to make it handle a new situation. There are two ways to do this:
Pulling Solution (continuously reading from status file) | Pushing Solution (receiving, handling signals) |
signal_handler(int sig) |
|
Signals are essentially like the kernel inserted the pulling (polling) check for you, but only when the value is actually of use.