Technical Women

035
Figure 1. Hessa Sulta Al Jaber

Today

  • Deadlock.

  • exec()

  • wait()/exit()

ASST1 Checkpoint

At this point:
  • If you are not done, you have three hours left!

  • Keep in mind: you need working locks and CVs for future assignments.

    • (The rest of the assignment is for points and won’t hurt you as much in the future.)

ASST2 Checkpoint

  • Get started!

  • We’ll distribute new test161 targets this weekend.

  • Your first task is to complete sys_write.

Locking Multiple Resources

  • Locks protect access to shared resources.

  • Threads may need multiple shared resources to perform some operation.

Locking Multiple Resources

Consider two threads A and B that both need simultaneous access to resources 1 and 2:

  1. Thread A runs, grabs the lock for Resource 1.

  2. → CONTEXT SWITCH ←

  3. Thread B runs, grabs the lock for Resource 2.

  4. → CONTEXT SWITCH ←

  5. Thread A runs, tries to acquire the lock for Resource 2.

  6. → THREAD A SLEEPS ←

  7. Thread B runs, tries to acquire the lock for Resource 1.

  8. → THREAD B SLEEPS ←

Now what?

Deadlock

Deadlock occurs when a thread or set of threads are waiting for each other to finish and thus nobody ever does.

Self Deadlock

A single thread can deadlock. How?
  • Thread A acquires Resource 1. Thread A tries to reacquire Resource 1.

This seems inane. Why would this happen?
  • foo() needs Resource 1. bar() needs Resource 1. While locking Resource 1 foo() calls bar().

Can we solve this problem?
  • Yes! Recursive locks. Allow a thread to reacquire a lock that it already holds, as long as calls to acquire are matched by calls to release.

  • This kind of problem is not uncommon. You may want to implement recursive locks for OS/161.

  • (But don’t make the locks we gave you suddenly recursive…​)

Conditions for Deadlock

A deadlock cannot occur unless all of the following conditions are met:
  • Protected access to shared resources, which implies waiting.

  • No resource preemption, meaning that the system cannot forcibly take a resource from a thread holding it.

  • Multiple independent requests, meaning a thread can hold some resources while requesting others.

  • Circular dependency graph, meaning that Thread A is waiting for Thread B which is waiting for Thread C which is waiting for Thread D which is waiting for Thread A.

Dining Philosophers

  • "Classic" synchronization problem which I feel obligated to discuss.

  • Illustrated below.

philosophers

philosophers 1

philosophers 2

philosophers 3

philosophers 4

philosophers 5

philosophers 6

Feeding Philosophers

Breaking deadlock conditions usually requires eliminating one of the requirements for deadlock.
  • Don’t wait: don’t sleep if you can’t grab the second chopstick and put down the first.

  • Break cycles: usually by acquiring resources in a well-defined order. Number chopsticks 0–4, always grab the higher-numbered chopstick first.

  • Break out: detect the deadlock cycle and forcibly take away a resource from a thread to break it. (Requires a new mechanism.)

  • Don’t make multiple independent requests: grab both chopsticks at once. (Requires a new mechanism.)

Deadlock v. Starvation

Starvation is an equally-problematic condition in which one or more threads do not make progress.
  • Starvation differs from deadlock in that some threads make progress and it is, in fact, those threads that are preventing the "starving" threads from proceeding.

Deadlock v. Race Conditions

What is better: a deadlock (perhaps from overly careful synchronization) or a race condition (perhaps from a lack of correct synchronization)?

I’ll take the deadlock. It’s much easier to detect!

Using the Right Tool

  • Most problems can be solved with a variety of synchronization primitives.

  • However, there is usually one primitive that is more appropriate than the others.

  • You will have a chance to practice picking synchronization primitives for ASST1, and throughout the class.

Approaching Synchronization Problems

  1. Identify the constraints.

  2. Identify shared state.

  3. Choose a primitive.

  4. Pair waking and sleeping.

  5. Look out for multiple resource allocations: can lead to deadlock.

  6. Walk through simple examples and corner cases before beginning to code.

Questions about Synchronization?

Now back to the process-related system calls!

Review: After fork()

returnCode = fork();
if (returnCode == 0) {
  # I am the child.
} else {
  # I am the parent.
}
  • The child thread returns executing at the exact same point that its parent called fork().

    • With one exception: fork() returns twice, the PID to the parent and 0 to the child.

  • All contents of memory in the parent and child are identical.

  • Both child and parent have the same files open at the same position.

    • But, since they are sharing file handles changes to the file offset made by the parent/child will be reflected in the child/parent!

Review: Issues with fork()

Copying all that state is expensive!
  • Especially when the next thing that a process frequently does is start load a new binary which destroys most of the state fork() has carefully copied!

Several solutions to this problem:
  • Optimize existing semantics: through copy-on-write, a clever memory-management optimization we will discuss in several weeks.

  • Change the semantics: vfork(), which will fail if the child does anything other than immediately load a new executable.

    • Does not copy the address space!

Review: The Tree of Life

  • fork() establishes a parent-child relationship between two process at the point when each one is created.

  • The pstree utility allows you to visualize these relationships.

pstree

Let’s Go On…​

  • Questions about fork()?

$ wait %1 # Process lifecycle

  • Change: exec()

  • Death: exit()

  • The Afterlife: wait()

Groundhog Day

Is fork() enough?

initfork 1
initfork 2
initfork 3
initfork 4

Change: exec()

  • The exec() family of system calls replaces the calling process with a new process loaded from a file.

  • The executable file must contain a complete blueprint indicating how the address space should look when exec() completes.

    • What should the contents of memory be?

    • Where should the first thread start executing?

  • Linux and other UNIX-like systems use ELF (Executable and Linkable Format) as the standard describing the information in the executable file is structured.

$ readelf # display ELF information

readelf

$ /lib/ld-linux.so.2

ldlinux

exec() Argument Passing

  • The process calling exec() passes arguments to the process that will replace it through the kernel.

    • The kernel retrieves the arguments from the process after the call to exec() is made.

    • It then pushes them in to the memory of the process where the replacement process can find them when it starts executing.

    • This is where main gets argc and argv!

  • exec() also has an interesting return, almost the dual of fork(): exec() never returns!

$ exec()

exec 1
exec 2
exec 3
exec 4
exec 5
exec 6
exec 7

exec() File Handle Semantics

  • By convention exec does not modify the file table of the calling process! Why not?

  • Remember pipes?

    • Don’t undo all the hard work that fork() put in to duplicating the file table!

pipes example 3

Next Time

  • Carl!

  • At this point we will head from outside looking in—or at the top of the world looking down—and get deep inside the trenches to examine the thread abstraction.

  • Tentatively:

    • One week on interrupts, kernel privileges, context switching and threading.

    • One week on scheduling.