The way that programs talk to the operating system is via ``system calls.'' A system call looks like a procedure call (see below), but it's different -- it is a request to the operating system to perform some activity.
System calls are expensive. While a procedure call can usually be performed in a few machine instructions, a system call requires the computer to save its state, let the operating system take control of the CPU, have the operating system perform some function, have the operating system save its state, and then have the operating system give control of the CPU back to you. This concept is important, and will be seen time and time again in this class.
1. int open(char *path, int flags [ , int mode ] );
2. int close(int fd);
3. ssize_t read(int fd, void *buf, size_t count);
4. ssize_t write(int fd, const void *buf, size_t count);
5. off_t lseek(int fd, off_t offset, int whence);
You'll note that they look like regular procedure calls. This is how
you program with them -- like regular procedure calls. However, you
should know that they are different: A system call makes a request to
the operating system. A procedure call just jumps to a procedure
defined elsewhere in your program. That procedure call may itself
make a system call (for example, fopen() calls open()),
but it is a different call.
The reason the operating system controls I/O is for safety -- the computer must ensure that if my program has a bug in it, then it doesn't crash the system, and it doesn't mess up other people's programs that may be running at the same time or later. Thus, whenever you do disk or screen or network I/O, you must go through the operating system and use system calls.
These five system calls are defined fully in their man pages (do 'man -s 2 open', 'man -s 2 close', etc). All those irritating types like ssize_t and off_t are ints and longs. They used to all be ints, but as machines and files have grown, so have they.
All actions that you will perform on files will be done through the operating system. Whenever you want to do file I/O, you specify the file by its file descriptor. Thus, whenever you want to do file I/O on a specific file, you must first open that file to get a file descriptor.
Example: o1.c opens the file in1.txt for reading, and prints the value of the file descriptor. If you haven't copied over the file in1.txt, then it will print -1, since in1.txt does not exist. If in1.txt does exist, then it will print 3, meaning that the open() request has been granted (i.e. a non-negative integer was returned).
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
main()
{
int fd;
fd = open("in1.txt", O_RDONLY);
printf("%d\n", fd);
}
|
Note the value of 'flags' -- the man page for open() (or chapter 3 of the book) will give you a description of the flags and how they work. They are described in fcntl.h, which can be found in the directory /usr/include. (Note that fcntl.h merely includes /usr/include/sys/fcntl.h, so you'll have to look at that file to see what O_RDONLY and all really mean).
Example: o2.c tries to open the file "out1.txt" for writing. That fails because out1.txt does not exist already. In order to open a new file for writing, you should open it with (O_WRONLY | O_CREAT | O_TRUNC) as the flags argument. See o3.c for an example of that:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
main()
{
int fd;
fd = open("out2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("o3");
exit(1);
}
}
|
When we run o3, it creates the file out2.txt, which is of zero length when the program terminates. o2.c and o3.c use perror() to flag errors.
UNIX> o2 out1.txt: No such file or directory UNIX> o3 UNIX> ls -l out* -rw-r--r-- 1 plank loci 0 2011-01-27 08:40 out2.txt UNIX>Finally, the 'mode' argument should only be used if you are creating a new file. It specifies the protection mode of the new file. 0644 is the most typical value -- it says "I can read and write it; everyone else can only read it."
You can open the same file more than once. You will get a different fd each time. If you open the same file for writing more than once at a time, you may get bizarre results.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
main()
{
int fd1, fd2;
fd1 = open("in1.txt", O_RDONLY);
if (fd1 < 0) { perror("c1"); exit(1); }
fd2 = open("in1.txt", O_RDONLY);
if (fd2 < 0) { perror("c1"); exit(1); }
printf("Opened the file in1.txt twice: Fd's are %d and %d.\n", fd1, fd2);
if (close(fd1) < 0) { perror("c1"); exit(1); }
if (close(fd2) < 0) { perror("c1"); exit(1); }
printf("Closed both fd's.\n");
fd2 = open("in1.txt", O_RDONLY);
if (fd2 < 0) { perror("c1"); exit(1); }
printf("Reopened in1.txt into fd2: %d.\n", fd2);
if (close(fd2) < 0) { perror("c1"); exit(1); }
printf("Closed fd2. Now, calling close(fd2) again.\n");
printf("This should cause an error.\n\n");
if (close(fd2) < 0) { perror("c1"); exit(1); }
}
|
UNIX> c1 Opened the file in1.txt twice: Fd's are 3 and 4. Closed both fd's. Reopened in1.txt into fd2: 3. Closed fd2. Now, calling close(fd2) again. This should cause an error. c1: Bad file descriptor UNIX>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
main()
{
char *c;
int fd, sz;
c = (char *) calloc(100, sizeof(char));
fd = open("in1.txt", O_RDONLY);
if (fd < 0) { perror("r1"); exit(1); }
sz = read(fd, c, 10);
printf("called read(%d, c, 10). returned that %d bytes were read.\n",
fd, sz);
c[sz] = '\0';
printf("Those bytes are as follows: %s\n", c);
sz = read(fd, c, 99);
printf("called read(%d, c, 99). returned that %d bytes were read.\n",
fd, sz);
c[sz] = '\0';
printf("Those bytes are as follows: %s\n", c);
close(fd);
}
|
When executed, you get the following:
UNIX> r1 called read(3, c, 10). returned that 10 bytes were read. Those bytes are as follows: Jim Plank called read(3, c, 99). returned that 12 bytes were read. Those bytes are as follows: Claxton 221 UNIX>There are a few things to note about this program. First, buf should point to valid memory. In r1.c, this is achieved by calloc()-ing space for c (read the man page on calloc() if you have not seen it before. Alternatively, I could have declared c to be a static array with 100 characters:
char c[100];Second, I null terminate c after the read() calls to ensure that printf() will understand it.
Third, when read() returns 0, then the end of file has been reached. When you are reading from a file, if read() returns fewer bytes than you requested, then you have reached the end of the file as well. This is what happens in the second read() in r1.c.
Finally, note that the 10th character in the first read() call and the 12th character in the second are both newline characters. That is why you get two newlines in the printf() statement. One is in c, and the other is in the printf() statement.
w1.c writes the string "cs360\n" to the file out3.txt.
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
main()
{
int fd, sz;
fd = open("out3.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("r1"); exit(1); }
sz = write(fd, "cs360\n", strlen("cs360\n"));
printf("called write(%d, \"cs360\\n\", %ld). it returned %d\n",
fd, strlen("cs360\n"), sz);
close(fd);
}
|
UNIX> w1 called write(3, "cs360\n", 6). it returned 6 UNIX> cat out3 cs360 UNIX>
Armed with that information, we can write a very simple cat program (one that copies standard input to standard output) with one line: (this is in simpcat.c):
main()
{
char c;
while (read(0, &c, 1) == 1) write(1, &c, 1);
}
|
UNIX> simpcat < in1.txt Jim Plank Claxton 221 UNIX>