Operating System 10 | File I/O Experiment

Series: Operating System

Operating System 10 | File I/O Experiment

  1. Write Files

(1) Open A File by fopen

We can also open this file in an advanced way by the file pointer. C language uses the file pointer for file operations. We can define a file pointer fp by,

FILE *fp;

Then, to open a given file, we should use the fopen function. Note that we should specify the file mode at the moment we open this file. The file can be load as only read, only write, only append, or combinations between these modes. We can refer to the following common modes,

+-----------+---------------------------+
| Mode | Meaning |
+-----------+---------------------------+
| r | Only read |
+-----------+---------------------------+
| w | Only write |
+-----------+---------------------------+
| a | Only append |
+-----------+---------------------------+
| r+ | Read + write |
+-----------+---------------------------+
| w+ | Write + read |
+-----------+---------------------------+
| a+ | Append + read |
+-----------+---------------------------+

Note that we need to add b in the mode for the I/O of binary files.

For example, if we want to open and write to a file named test.txt (create one if doesn’t exist), we can use,

fp = fopen("test.txt", "w+");

(2) Open A File by open

In the previous section, we have discussed how we can open a file by the file pointer. We can also use modes signs like w , r , a , etc. However, this is a more advanced way to open a file. Let’s see how we can open a file by the file descriptor, which is a fundamental way of file I/O. Commonly, it is good to use fopen instead of open. You can find some reason from here.

The synopsis of the open system call is,

#include <sys/types.h> 
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int access, int mode);

where,

  • path is the path of the file we would like to open
  • access is the access mode for this file. Usually, we have three macros to specify for this argument, O_RDONLY (read-only access mode), O_WRONLY (write-only access mode), or O_RDWR (read-write access mode). We can also use or operator | to specify more features. For example, O_CREATE means to create a file if the file is doesn’t exist. What’s more, O_TRUNC means that we will clear the file before we write to it. So here are some corresponding rules,
r == O_RDONLY
r+ == O_RDWR | O_TRUNC
w == O_WRONLY | O_CREATE | O_TRUNC
w+ == O_RDWR | O_CREATE | O_TRUNC
a == O_WRONLY | O_CREATE
a+ == O_RDWR | O_CREATE
  • The mode argument can only be used when access=O_CREAT and it is used to specify the future accesses of the newly created file. The commonest macros for this argument are S_IRUSR (means user has read permission), S_IWUSR (means user has write permission), and S_IXUSR (means user has execute permission). We can also use or operator | to specify more than one value.

For example, if we want to open and write to a file named test.txt by open (create one if doesn’t exist), we can use,

open("test.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);

This function returns a file descriptor (datatype int) for us to use. This file descriptor can be useful for the following write and read operations.

int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);

(3) Write/Read the File with File Pointer

If we want to write to the file, this can be easy for a file pointer. However, it would be quite hard for a file descriptor. Suppose we have a file pointer and a file descriptor for the same file now,

FILE *fp;
int fd;

If we want to write the string Hello World!\n to the file, we can use the function fputs,

fputs("Hello World!\n", fp);

Or we can use the function fprintf,

fprintf(fp, "Hello World!\n");

The fputc function can be used to write a single character to the file (i.e. 65 means A by ASCII),

fputc(65, fp);

Now, let’s see an example code,

The content of the output file test.txt should be,

Hello World!
Hello World!
A

To read from this created file test.txt , we can use fgetc to get a character or the fgets function for a string. In order to use read a string, we should also create a buffer to store the string we have to read (we use a buffer that can store up to 255 characters).

printf("%c\n", fgetc(fp));
char buff[255];
fgets(buff, 255, fp);
printf("%s", buff);

Now, let’s see an example code,

The output should be,

H
ello World!
Hello World!
A

Remember that we have to free the pointer fp by fclose function before we end the program.

(4) Write/Read the File with File Descriptor

To write a file with the file descriptor, we can use the function write. For example, if we want to write Hello World! to the file,

write(fd, "Hello World!", strlen("Hello World!"));

Note that the third argument must be exactly the string length we would like to write. Usually, we can use a buffer to store the message, so it would be better if we use,

#define BUF_SIZE 20
char buffer[BUF_SIZE];
char *message = "Hello World!";
strcpy(buffer, message);
write(fd, buffer, strlen(buffer));

So in general, we can write to the file with the following code.

The content of the output file test.txt should be,

Hello World!

Now that we have the test.txt , if we want to read from this file, we can use the function read. In order to store the read information, we have to create a buffer buffer.

#define BUF_SIZE 20
char buffer[BUF_SIZE];
read(hfile, buffer, BUF_SIZE);

To get the information we have read, we can directly print the value in the buffer,

printf("%s\n", buffer);

The overall code should be,

The output should be,

Hello World!

(5) Buffer Looping Write

Suppose we have a buffer that is smaller than the message we want to send, which can be common to us, what could we do if we want to write this message? Let’s say we have a buffer that can store only 2 chars but we would like to send the message Hello World! , a direct idea is that we can cut this string into pieces and then write 2 characters at a time. We can continue this until all the characters are successfully written to the file.

To implement this idea, for each loop, we copy the string starting from the character pointer message with a length of BUF_SIZE to the buffer by memcpy ,

memcpy(buffer, message, BUF_SIZE);

After this procedure, the buffer is now the 2-character message we would like to write properly. Thus, we can then use write to write the data in the buffer to the file. We should -1 to the strlen(buffer) because we don’t want to add a \0 character for each write.

write(fd, buffer, strlen(buffer)-1);

Note that the message pointer message has to move rightward by BUF_SIZE*sizeof(char) because we have already written these 2 characters. Also, the length of this string should be recalculated by the strlen function,

message += BUF_SIZE;
message_len = strlen(message);

So finally, we have the following example code to implement this idea,

The content of the output file test.txt should be,

Hello World!

(6) Buffer Looping Read

The same problem happens when we want to read from a file. Suppose we have a file that contains a string much longer than the read buffer we have, what can we do? Well, this is much simple than the writing case because the position we read is accumulated and we can use the return value of the read function to show where to stop. The example code is,

The output of this program should be,

Hello World!

2. send And recv

(1) read And write

Now, we are quite familiar with the read and write function and we have known that they can work for all the file descriptors. However, for the socket, use read and write to send our message is not safe. When we use read or write for sending and receiving messages, some messages can be lost and we don’t know this happens. So for socket programming, the more frequently used functions for sending messages are send (corresponding to write) and recv (corresponding to read).

(2) send Function

When we use the write function, we should use the file descriptor, the buffer pointer, and the length of the buffer as arguments,

write(fd, buffer, length);

It’s good to know that the send function actually has a similar structure. When we call send, we should have,

send(fd, buffer, length, 0);

Note that we should specify the flag variable to 0 in our cases. So the interesting thing is that the send function returns a value of the real bytes we send. So,

bytes_sent = send(fd, buffer, length, 0);

Because of the networking issues, we can always expect that,

bytes_sent <= length

Thus, we can maintain the real bytes we send for each loop until we successfully send the whole message.

(3) recv Function

Similarly, the recv function can be called by,

recv_bytes = recv(fd, buffer, BUF_SIZE, 0);

We should loop the recv function until the returned value recv_bytes = 0 (this means that the server has no more data to send).

(4) File Transformation

Let’s finally discuss how we can implement a file transformation instance. Suppose the server has a file and each client connected to this server will receive a copy of this file. In this example, we have to do the following steps,

  • Step 1. Server reads file. Because this file is stored in the server, the server should first use the function read to read from this file.
  • Step 2. Server sends file. Because we have to maintain the file sending process, we have to use send to send the data to the server.
  • Step 3. Client receives file. Because we have to maintain the file receiving process, we have to use recv to send the data to the server.
  • Step 4. Client writes file. In the end, we have to write the data we have received in the buffer to a local file by the write function.