Where apps end and the system begins

Exploring the system calls and control flow that underpin high-level languages.

Editor’s note: Sometimes we can forget how important it is to truly understand how a system works, and how the nuts and bolts affect our everyday activities. Marty Kalin asks us to dive a little deeper and strengthen our computational thinking abilities.

It’s clear that applications need system resources to execute: a processor, memory, and usually I/O devices such as the keyboard and screen. It’s less clear how applications gain access to these shared resources, which are under operating system (OS) control. The OS, like any good manager, is efficient and unobtrusive as it handles resource requests from applications. Let’s take a look at how applications interact with the OS, in both routine and dramatic fashion.

Consider what happens when a print statement executes. Here’s a Ruby example:

puts 'Hello, world!'

The Ruby puts statement wraps a call to a high-level I/O function in the standard C library (in this case, printf), which acts as the interface between resource-requesting applications and resource-granting OS routines. In this example, the screen is the requested resource. The standard library interacts seamlessly with the OS, which also is written in C with some assembly language. The library function printf is high-level because, as the f in the name indicates, the function can format the bytes to be written as integers, floating-point values, and character strings such as Hello, world!. In systems-speak, the Ruby application and the C library function execute in user space, which does not bestow the rights and privileges needed to control system resources such as the screen.

The printf call only starts the request process, which progresses to a low-level, byte-oriented library function named write:

write(1, "Hello, world!\n", 14);

The first argument to write is a file descriptor, an integer value that identifies a file: the 1 in this example identifies the standard output, which defaults to the screen. In a modern OS, any I/O device counts as a file. The second argument is a string, in C an array of 1-byte char elements. An 8-bit char accommodates 7-bit ASCII character codes, which are in play here. The last argument, 14, is the number of bytes to be written. The write call, in turn, invokes an OS routine that executes in kernel space, which bestows the very rights and privileges needed to control system resources such as the screen. If the write call succeeds, it returns 14 in this example — the number of bytes written to the screen. Should the request fail, write would return -1, a distinctive value often used to signal an error: -1 in binary is all 1s.

Figure 1. Routine system calls

Figure 1. Routine system calls

In summary, a routine print statement in a high-level language such as Ruby goes through a standard C library function such as printf, which leads to a system-level function such as write, which in turn invokes an OS routine charged with managing a shared resource such as the screen (see Figure 1). The Ruby call to puts is, in effect, an OS request that terminates in a system call, which either grants or denies the request.

Explicit System Calls

The special library function syscall allows C code to fine-tune a system call in a way that an ordinary library function either disallows outright or makes cumbersome. The aptly named syscall function takes a variable number of arguments, from 0 to 5 depending on the particular call.

Example 1.1 An explicit system call

#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>
#include <stdio.h>

void main() {
  /* 755 means: owner has read/write/execute permissions
            others have read/execute permissions */
  int perms = 0755; 
  int status = syscall(SYS_chmod, "/usr/local/website", perms);
  if (-1 == status) printf("chmod failed: errno = %i\n", errno);
  else printf("chmod succeeded\n");
}

The callExplicit program (see Example 1.1) shows the syntax for syscall. The call targets the OS routine chmod, which has a wrapper with the same name in the standard library. The example is contrived in that the wrapper function chmod could be invoked directly. In this example, the arguments to the variadic syscall are:

  • SYS_chmod: This is a symbolic constant (in C, a macro) for an integer value that identifies the system function to be called, in this case chmod (change mode). The chmod function can change the access permissions for files, including directories.

  • /usr/local/website: This is a directory, a file that can contain other files; the directory’s access permissions are to be changed through syscall. The name website is meant to suggest that the directory holds executable scripts of the kind often used in websites to deliver dynamic content. The directory name is arbitrary, but the directory must exist to avoid an error.

  • perms: This is an integer whose value, 0755, represents read/write/execute permissions for the owner of the directory and read/execute permissions for others. The value is in octal, as signaled by the leading 0.

The syscall function returns an integer value as a status code. By convention, a returned value of 0 represents a successful call, and a returned negative value signals an error. The syscall function always returns -1 to signal an error; the particular error code then is stored in the errno variable. There are symbolic constants for such error codes. For example, an errno value of EPERM, which is 1, means no permission for the operation; an errno value of ENOENT, which is 2, means no such entity.

Application/system interaction can be more dramatic than the examples shown so far. A user might terminate an application by entering Control-C from the keyboard or by executing a kill action from the command-line or another program. An executing program that generates an out-of-bounds address (e.g., by falling off the end of an array) generates a general-protection fault, which usually causes the OS to terminate the program at once. Let’s take a look at system calls that change a program’s normal flow of control.

System Calls and Exceptional Flow of Control

High-level languages such as C++, Java, and C# have a programming construct built around the concept of an exception, which abruptly changes the normal flow of program control. In these languages, an executing statement throws an exception either explicitly (by using, for example, a throw statement) or implicitly (by trying, for example, to open a non-existent file for reading).

Figure 2.1 Exceptional flow of control

try {
  s1  ;; throws an exception   //line 1
  s2
  s3
} 
catch(Exception e) {print(e)}  //line 2

The try code segment (see Figure 2.1) is a pseudo-code depiction of the exception-handling construct. Any statement in the try block might throw an exception, which alters the usual flow of control. In this example, the normal flow of control is from statement s1 to s2 to s3. Suppose that statement s1 tries to open a non-existent disk file, which generates an I/O exception (line 1). The exception halts the normal flow of control: statement s2 does not execute because control switches instead to the print statement in the body of the catch block, which is at the end of the try block (line 2). In this example, the exception can be described as synchronous because the change in the flow of control results from — is synchronized with — a particular executing statement in the program, in this case s1.

Not every abrupt change in the control flow is synchronous. For example, if program P is started at the command line, then entering control-C from the keyboard usually terminates P‘s execution: P aborts at once, which is another example of an abrupt change in control flow. An abort might be described as asynchronous to underscore that something external to the program — and, therefore, something
unpredictable — alters control flow.

The terms used to describe abrupt change in the flow of control do not have fixed, standard meanings. Nonetheless, these traditional terms are worth clarifying:

  • Interrupt

    An interrupt results from a signal that an I/O device generates. Recall the example of entering control-C from the keyboard, which is an I/O device. This generates a signal to abort an executing program. An OS interrupt handler, a system routine, responds by terminating the program. An interrupt in this sense is asynchronous: the interrupt results from an event external to the executing program.

  • Trap

    A typical system call starts with a call to an ordinary library function such as printf, which eventually results in the execution of OS code. The situation can be expressed in a different way, using the language of traps. The call to printf results in an event called a trap, which an OS trap handler processes by mapping the library call to the appropriate OS code. A trap in this sense is synchronous: the trap results from the execution of a specific instruction in the application code.

    The next two types can seen either as trap subtypes or as types in their own right. In either case, the distinction can be introduced with an similar one in Java. The Java Throwable type has two subtypes. An Exception is generally recoverable; hence, an application should try to catch an exception, such as a FileNotFoundException, and handle the exception some some appropriate way, for example, by reverting to a default file. By contrast, an Error is serious enough that an application typically should not try to catch and recover. For example, given an OutOfMemoryError, there is no way for an application on its own to recover from this condition; hence, there is no point in trying to catch and handle such an error. The difference between a fault and an abort resembles the one between an Exception and an Error, although the correspondence is not exact.

    • Fault

      A fault is condition from which an application typically can recover, but recovery is not a certainty. Suppose, for example, that an executing application references data or instructions that are not currently in memory. This results in a page fault, where a page is a fixed-length block of bytes moved between main memory and disk. (On an Intel box, the standard page is about 4K bytes.) The fault is recoverable in that the OS loads the required page into memory, thereby allowing the application to resume execution.

      In some cases, however, faults are not recoverable. For example, an OS generates a general protection fault if an executing application tries to reference a memory location outside of the application’s address space. An application cannot recover from this fault.

    • Abort

      An abort is a serious condition from which an application cannot recover. The general protection fault noted above might be renamed a general protection abort because the offending application is terminated when it generates an abort.

Aborts in particular underscore that not every request from an application to the OS is honored. If an application requests access to an out-of-bounds memory location, the OS normally responds with an emphatic no in the form of an abort. The possibility of failure is baked into every system call, although system calls may become so routine that they are expected to succeed.

It’s time to summarize. In routine application/system interaction, an application calls a standard library function using the language’s ordinary syntax; the Ruby call to puts is a case in point. The application and the library function execute in user space, that is, with no OS rights and privileges. The library call results in a system call, which invokes OS code that runs in kernel space. Once the OS call completes successfully, the application continues as usual. The application/system interaction turns dramatic when such interaction alters the application’s normal control flow, with an application abort as the most dramatic outcome.

tags: , , , , , ,