Read, write, open, and close.
We’re one step closer to actually writing code in C. I know, I know, it took forever to reach this point since the inception of our journey to Linux programming. In the next section, we’ll finally start coding.
Since we’ve already discussed file descriptors, we’ll take a look at some sample codes.
read()
To read byte string from a streamed file, we’ll use the system-call, called read().
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t bufsize);
Keep in mind, to use the read system-call, we need to include the header file: unistd.h.
And the type of the returned value, ssize_t, and an argument type, size_t, are both basically int-type or long-type. The sole reason we use ssize_t instead of int/long is to enable us to use exactly the same code that is assured to work regardless of different OSes, CPUs, and kernel versions. This is not a joke, but we could be forced to use different types in other versions of the kernel. Instead of using int/long types, we should use the prototype types.
read() is the system-call that specifies fd from the file descriptor. It reads the maximum bufsize and stores it in buf. Once read() completes reading all data, it returns the byte count 0 for success and -1 for failure. And read() often only reads way fewer numbers than the bufsize, so you must check the returned value every time.
In C language, if you want your program to store human-readable characters, you have to end it with ‘\0’. Some APIs also expect characters to end with ‘\0’ and others are not. read(), for example, doesn’t expect its reading byte-string to end with ‘\0’, while printf() does. So, you can’t simply transfer the date scanned by read() to print().
write()
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t bufsize);
write() will stream the amount of data whose size is equivalent to bufsize. Just like the read(), the returned value’s type is ssize_t, which is an integer, so it returns the byte count when in success or -1 when in failure. Just like read(), you must check the returned value every time.
The definition of the stream
In a previous section, I wrote that we’d define the stream later on. Now’s the time. Our definition of the stream is the one available in the file descriptor as well as controllable from read() and write() APIs. For example, when you open the program with open() API, the procedure will enable you to read/write the file – this particular case pretty much falls down to the category of the stream.
Open()
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt.h>
int open(const char *path, int flags);
int open(const char *path, int flags, mode_t mode);
open() streams to the file that is specified in the first argument path and returns the file descriptor corresponding to the stream. This process is called open.
The second argument, flags, specifies the type of streaming method you can select. And here is the table for that:
Flag | Description |
O_RDONLY | read only |
O_WRONLY | write only |
O_RDWR | both read/write |
O_CREAT | If the file doesn’t exist, create one |
O_EXCL | when being executed with O_CREAT, if the file already exists, it returns an error |
O_TRUNC | when being executed with O_CREAT, if the file already exists, set the file’s byte length to zero |
O_APPEND | append write() from the end of a file |
O_EXCL is useful when multiple processes are trying to open the same file. In this situation, a single process is allowed to create one while others fail. open() has other flags, but they are barely used, so we’ll skip them this time.
Finally, the third argument, mode, is only enabled when O_CREAT is selected for the second. The argument receives the file’s permission. The permission number, however, is not directly used, but rather a value, called umask, has something to do with it. For more info about umask, we’ll take a look at it later.
Close()
close() is another system call that is responsible for streams no longer needed.
#include <unistd.h>
int close(int fd);
close() ends stream whose number corresponds to file descriptor’s fd. If it’s successfully closed, it returned 0, if not -1.
Here’s a sample code of the closing process.
if (close(fd) < 0) {
/* do some thing */
}
Most of the streams whose job is completed by their processes will eventually be deleted by the kernel, so it doesn’t cause any serious problems for the system even if the streams are forgotten to be deleted.
But still, there’s always a limit to the number of streams allowed to run on the system simultaneously, so it’d be better to delete streams that are already done.