Session 2: System calls

About the assignment

System calls (Section 1.4)
Error codes
Process management
Signals
File management
Directory and file system management

About the assignment

Today I want to get you to a point where you can do the assignment over the weekend.

The primary point of the assignment is to get a greater understanding of what the operating system does from the programmer's point of view. So what we'll do is write a program that uses several of the functions that the operating system provides.

System calls

Functions that directly access the operating system are called system calls. Page 23 of the textbook lists the system calls for Minix. Naturally, the list is much shorter than the typical industrial strength program. But it's long enough to let you do 99.5% of what you want to do.

Today we'll just discuss the system calls that apply directly to the assignment. We'll look at the others later when we talk about how Minix handles them.

Error codes

System calls occassionally run into a problem. Perhaps the user specified a nonexistent filename, or there weren't permissions to do whatever was requested, or whatever. Unix system calls handle this by returning some bad value (usually -1) and setting a global integer variable errno to some value that indicates the problem.

So what programs typically have to do after most system calls is check the return value to see if there was an error. If there is, then it can look at errno to see the type of the error.

There's a subroutine perror() (it's not a system call - it's just a regular library function) that prints a meaningful error message to the standard error file. It takes a single string as a parameter, which it prints along with the error message.

So here's how a typical segment might look.

    value = open("filename", O_RDONLY, 0600);
    if(value < 0) { /* ah, there's an error */
        printf("sorry, I couldn't open filename\n");
        perror("open"); /* This will explain why */
        return;
    }

The man page (e.g., man -s 2 open) will explain the different values that errno might receive. For example, if we want to do something special should the file not exist, we might test it.

    value = open("filename", O_RDONLY, 0600);
    if(value < 0) { /* ah, there's an error */
        if(errno == ENOENT) { /* the file doesn't exist */
            printf("you have to create the file first.\n");
        } else {
            printf("sorry, I couldn't open filename\n");
            perror("open"); /* This will explain why */
        }
        return;
    }

Process management

In primitive operating systems (up to MS-DOS), the operating system didn't have to worry about processes. There was the operating system, and usually there's the program that's running, and that's it. End of story.

Modern operating systems juggle several processes at once. Most users think of a process as a program running. But that's not accurate. Many programs work with several processes.

The operating system starts a process by loading it into memory and then calling its main() function. One of the most basic system calls is exit(), which terminates the process that calls it. The exit() system call can't have a meaningful return value, since it never returns. It has a single integer as a parameter - this is the exit code. This exit code might be used by other programs as information about what the process accomplished. Most Unix programs exit with a code of 0 when they accomplished their job successfully, and they exit with a non-0 code when they encountered some error that led it to abort.

In Unix, each process gets its own process ID. Each new process gets its own ID. When the ID gets too large, the IDs cycle back to 1 (the first process ID). For example, on my computer now, the editor in which I'm writing this is process 9491.

How does a program generate a new and separate process? That's what the fork() system call does. When a process P calls fork(), the operating system takes all the data associated with P, makes a brand-new copy of it in memory, and enters a new process Q into the list of current processes. Now both P and Q are on the list of processes, both about to return from the fork() call, and they continue.

The fork() system call returns Q's process ID to P and 0 to Q. This gives the two processes a way of doing different things. Generally, the code for a process looks something like the following.

int child = fork();
if(child == 0) {
   // code specifying how the child process Q is to behave
} else {
   // code specifying how the parent process P is to behave
}

The waitpid() system call gives a process a way to wait for a process to stop. It's called as follows.

pid = waitpid(child, &status, options);
In this case, the operating system will block the calling process until the process with ID child ends. (The options parameter gives ways of modifying the behavior to, for example, not block the calling process. We can just use 0 for options here.) When the process child ends, the operating system changes the int variable status to represent how child passed away (incorporating the exit code, should the calling process want that information), and it unblocks the calling process, returning the process ID of the process that just stopped.

Finally, the execve() system call gives a way of executing another file. The execve() system call takes three arguments.

    i = execve(name, argv, envp);
The name is a string containing the absolute filename specifying the executable file. (By absolute, I mean it needs to be relative to the root directory /; e.g., "/usr/bin/more".) The argv and envp arguments are the arrays of strings that should be passed into the main function of that executable file.

When a process calls execve() executes, the operating system deallocates all memory associated with that process, reads the file off the disk into memory, and calls the main() function for this new executable. The calling process essentially dies, but it is immediately reincarnated as the executable file specified in the parameter.

So normally execve() doesn't return - it forgets the identity of the current process entirely. If it returns, it returns -1 to mean an error has occurred. A code specifying the identity of the error is placed into the global errno variable. For example, the ENOENT constant is placed into errno if the file specified in the first argument doesn't exist.

Signals

A signal is basically a way for one process to try to control another. We won't have to deal with signals in the programming assignment, but you'll need to read about signals for the written assignment.

File management

One of the more important functions of the operating system is to give programs a way to access the disk. Just about all operating systems do this via the concept of a file, with the files organized into a hierarchy of directories. Minix is no exception.

Unix uses files for much more than this, however. For example, Unix considers each terminal to be a file. Whenever something is written to the file, it gets displayed onto the terminal output. Whenever something is read from the file, it gets read from the terminal input.

Each file opened by a process is identified by a file descriptor. Every Unix process has three special file descriptors: 0 is for standard input (conceptually, it's the keyboard that the user's typing at), 1 is for standard output (the monitor that the user's typing at), and 2 is for standard error (also the monitor that the user's typing at, but it has priority over standard output). Other open files are given successively larger numbers.

The open() system calls allows you to open a file.

    fd = open(filename, type, mode);
This opens the file whose path is specified in the filename argument. The way in which the file is opened is specified by the type argument: O_RDONLY to read or O_WRONLY to write; you can do a bitwise OR with O_CREAT if you want the system to create the file if it doesn't exist already. The mode argument gives the permissions for the file (the octal code 0600 means that the user can read and write the file, which is good enough for us). The system call returns the file descriptor of the new file (or a negative number if there is an error, placing the code into errno).

The close() system call takes a single file descriptor as its parameter and closes the file. (The operating system here flushes any buffers it has for the file and frees that entry from its table so that it can recycle the file descriptor later.)

The read() and write() system calls are two of the most important system calls.

	n = write(fd, buffer, num_bytes);
	n = read(fd, buffer, num_bytes);
In the write() system call, the operating system will try to write the first num_bytes pointed to by buffer into the file described by fd. It returns the number of bytes written (typically num_bytes), or -1 if it ran into an error.

The read() call works similarly: It reads up to num_bytes bytes into the memory pointed to by buffer from the file described by fd. It returns the number of bytes read, or -1 if it ran into an error.

When I completed Assignment 1, I didn't use read or write at all. But I did implicitly: The printf() library function uses the write() system call to print characters onto file descriptor 1. And the fgets() library function uses read() to read from a file.

The last file management function that I found useful for completing the assignment was stat(), which gives a way of finding information abouta file, like its size, its type, and its permissions. I used stat() because I wanted to verify that a file was executable, and that information is stored in the permission bits for a file.

Directories

The only directory function that I found useful for the assignment is chdir(), which takes a directory path for an argument and changes the current working directory for the current process to the directory described. Any future file accesses that don't use absolute paths will be relative to this new working directory.