Lecture 6: Signals, scheduling, and threads

Scribe notes for October 22, 2014

Yu Wang and Briley Cantwell


Topic 1: File Descriptor Errors

What can go wrong with file descriptors?

  close(-1);

Can't close a file that doesn't exist.


  close(39);

Invalid file descriptor: Not a valid value or doesn't refer to an open file. This will give errno = EBADF.


  fd = open(...);
  read(fd, ...);
  // missing "close(fd);" FD LEAK!

Files are opened but never closed, which can cause your program to run out of file descriptors.


  fd = open(...);
  read(fd, ...);
  // Power unplug or similar interrupting event occurs here
  read(fd, ...); // will return -1 (failure)

An unanticipated power unplug or similar interrupting event can cause the following read to fail.


Topic 2: Race Conditions

With race conditions, the behavior of your program depends on timing, making it difficult to predict and debug.

Example:

  (cat a & cat b) > outfile
Figure 1: Diagram depiction of a race condition

If a contains "a\n" and b contains "b\n", the following outputs are possible:

For "small" writes (less than around 2048 bytes), outputs like above are done atomically, so they will not be interleaved. For larger writes, the output can be interleaved.


Next example:

  (cat > a & cat > b) < infile
Figure 2: Diagram depiction of a race condition

What do a and b contain after the above completes?

a could contain all of infile while b is empty, or a could be empty while b contains all of infile, or a and b could each contain parts of infile, such that the size of a and the size of b add up to the size of infile.


Topic 3: Motivation for Signals

Let's say the task is to rotate a logfile. The name of the logfile that a program constantly writes to is log and the name of the logfile that stores yesterday's program output is oldLog. Then, at the end of a particular day, we execute the linux command: mv log oldLog. Assuming that the size of the file log is very large, the problem with executing the linux command is that data will be lost, because while the current process is renaming the file log to the file oldFile, the data that the program is still outputting will not be saved in the file log anymore and will be lost. Therefore, below is a pseudocode of a function that can prevent the problem above using a technique called polling.

void checklog(void){
  if (stat("log", &st) < 0 || st.size == 0){
    close(fd);
    fd = open("log", O_WRON);
  }
}

The function will check to see if the file descriptor that the process refers to is the old log file and if it is, it will close the old log file and open a new file descriptor to the new log file to write. However, this function uses polling, which chews up CPU. Therefore, signals should be used to avoid polling.

Topic 4: Signals

Here is an analogy: signals are to processes as traps are to hardware. Every signal has a unique status. So, when a given signal is sent during a process, the user will know how each process is currently behaving by reading status of the signal. When a signal is activated, it can either terminate the current process, continue on as before, or cause the program to call another function by changing the %eip register. Please look at table 1 for a list of common signals.

Table 1: Common Signals
Signal Name Description of Signal
SIGINT interrupt
SIGHUP hangup
SIGSEGV segmentation violation
SIGBUS bus violation
SIGFPE floating point exception
SIGPIPE writing to a pipe with no readers
SIGKILL process must be killed; can not be ignored
SIGALRM alarm clock
SIGXCPU CPU quota exceeded
SIGXFSZ max file size exceeded

Below is an example code on how to send the signal SIGINT to the current process.

int kill(pid_t pid, int sig); // a system call

In some main function...

  pid_t p = fork();
 
  //Entering parent process
  if (p > 0) {
    sleep(30);
    kill(p, SIGINT);
  }
  waitpid(p, ...);

The kill( ) system call takes two arguments: a process id and a signal. It will send the signal to the process. Therefore, in the code above, the kill( ) system call will send SIGINT to the processor with a pid_t of p (which is itself), after 30 seconds.

Below is an example code on how to use the signal SIGALRM.

typedef void (*sighandler_t) (int);
sighandler_t signal(int signame, sighandler_t);

void bing(int sig) {
  printf("BING %d/n", sig);
  exit(27);
 }

int main(void) {
  signal(SIGALRM, bing);
  alarm(30);
 
  printf("Entering random code!\n"); //For debugging purposes
  Some random code...
  printf("Exiting random code!\n"); //For debugging purposes

  return 0;
 }

In this code, the signal( ) function is a signal handler that will execute the bing function as soon as the SIGALRM signal is received by the current process. Also, alarm( ) is a system call that will send a SIGALRM signal to the current process in X seconds, where X is the value of its argument. Therefore, as the current process continues to run code in the main function, in 30 seconds, the SIGALRM signal will be sent to the current process. As a result, the current process will stop what it is doing, remember its current position in the code, execute the bing( ) function, and for this code, exit the program inside the bing ( ) function.

But, the code above has one problem: there is a race condition! The race condition will occur if SIGALRM comes while the current process is issuing a printf( ) function. This is because in the bing( ) function, there is a printf( ) function too. If the main function is interrupted while it is executing the printf( ) function and the current process executes the printf( ) function in the bing( ) function, this is considered undefined behavior and anything can happen. Another problem is that the bing( ) function has a exit( ) function, which is not good because exit will destroy the I/O of the main process.

This leads to the concepts of asynchronous-safe functions and not safe functions. For any functions that are executed during a call to the signal handler, only use asynchronous-safe functions inside it. The functions printf( ) and exit( ) are considered not safe functions, whereas the functions _exit( ) and putchar( ) are asynchronous-safe functions. Table 2 shows several asynchronous-safe functions and not safe functions.

Table 2: Asynchronous-Safe and Not Safe Functions
Asynchronous-Safe Functions Not Safe Functions
write printf
read fopen
close fclose
_exit exit
putchar malloc
unlink free

Below is an example of a safe signal handler.

void bing(int sig) {
  write(1, "BING\n", 6);
  _exit(27);
 }

Now, here is more description of the signal( ) function that was used in the previous example. The signal( ) function's first argument is the name of a signal. The second argument is called an action. Therefore, after a process calls the signal( ) function, if a signal is sent to the process and it corresponds to the first argument that was in the signal( ) function, then perform action specified in the second argument.

In the previous example where signal( ) was used, the second argument was the address of a handler function, which was the bing( ) function. Instead of specifying the address of a handler function, the second argument can also be declared as: SIG_DFL or SIG_IGN. SIG_DFL specifies the default action for the signal that is supplied as the first argument of the signal( ) function. SIG_IGN specifies that the signal specified should be ignored.

There is a way to block, unblock, and replace a set of signals for a given process. Below shows the function that can do this.

int pthread_sigmask(int how, sigset_t const* restrict_newset, sigset_t* restrict_oldset);

The first argument can be specified as: SIG_BLOCK, SIG_UNBLOCK, or SIG_SETMASK. SIG_BLOCK will block the signals that are specified in the restrict_newset argument. SIG_UNBLOCK will unblock the signals that are specified in the restrict_newset argument. SIG_SETMASK will replace the signals specified in the restrict_oldset argument with the signals specified in the restrict_newset argument.

Below is an example code that shows how to use the pthread_sigmask( ) function.

void gzip()
{
  sigset_t newMask, oldMask;
  sigemptyset(&newMask);
  sigemptyset(&oldMask);
  sigaddset(&newMask, SIGINT);
  signal(SIGINT, cleanup);
  int in = open("foo", ORDONLY);
  int out = open("foo.gz", OWRONLY);
  magic.gzip(in, out);
  close(in);
  close(out);
  pthread_sigmask(SIG_BLOCK, &newMask, &oldMask);
  unlink("foo");
}

void cleanup(int sig)
{
  unlink("foo.gz");
  _exit(97);
}

In the code above, magic.gzip is a function that will create a zip file "out" from the file "in". After successfully producing a zip file from the input file, the input file will be deleted ("unlinked"). However, what if the user presses ctrl-c during the gzip( ) function's unlink("foo")? Without the pthread_sigmask function in the code above, the process would have stopped the current process, enter the cleanup function, and unlink the zip file that was just created, and exit. In addition, pressing ctrl-c during the unlink function will most likely delete the original input file "foo" as well. Therefore, before the process enters the unlink("foo") function, any attempt to send a SIGINT signal (i.e. ctrl-c will send a SIGINT signal) is blocked by the pthread_sigmask function, so that the cleanup function will not be entered during the unlink("foo") function.

Topic 5: Threads

Threads are like processes, but much lighter weight. We can have many threads running in a single process, and each thread within a process shares the same code and the same memory.

There are many advantages to using threads:

  1. Performance boosts through running the same code in parallel
  2. Fast context switching between threads
  3. Fast communication between threads

There are also hazards to watch out for when using threads:

  1. No insulation: creates race conditions when accessing the same memory
  2. Less reliability: more prone to error
  3. More complexity: difficult to debug
Table 3: Thread Functions vs Process Functions
Thread Functions Analogous Process Functions
pthread_create fork
pthread_join waitpid
pthread_exit _exit