Foreground activities
Background activities
Variables and threads
Re-entrancy
Threads
See also
Whenever you type any command, the shell’s normal behavior is to start up that child activity and then go to sleep waiting for it to complete. This is a foreground activity. If you start something and then decide you want to stop it, type Ctrl-C, which wakes up the shell and causes it to stop the foreground activities.
If the application is going to run in its own separate window, that’s treated specially. For example, typing:
causes notepad, which is a graphical application, to appear in a new window. In that case, unless the command is in script or part of a more complex structure, e.g., a pair of statements joined by ;, && or ||, there’s no reason for the shell to wait for the child’s completion. After all, if you want to wait until it’s done you can do that yourself!
The way the shell figures out what type of application you’re starting is by actually examining the executable file itself. It can tell the difference between console and graphical applications. When you start an application that needs a new window, the shell comes right back for the next command.
Also, if the child is a graphical application and stdout or stderr is tied to the console window where the C shell’s running, the C shell recognizes that that handle won’t be inheritable by the child and instead will create a pipe. It’ll give the write end to the child and create a background thread in the C shell to read anything coming back over the pipe from the child and copy it to the console window. This means that with the C shell, you can still use ordinary printf’s in a Windows graphical application and not lose any output.
If you want to run something but don’t want to wait for it complete, just type an ampersand at the end of the statement:
This creates a child process to run word count in the background, with its output directed to a file. The 1 message means that a new background job (job 1) has been spawned to process the command while you continue to work. The job starts as a new thread. If, as in this case, a separate process is needed, that thread will create it with a CreateProcess call to the Windows kernel then exit. Each new thread, process or screen group spawned by a background job will inherit its parent’s job number. Every time a new background job is created, the job number is incremented.
The use of i/o redirection in combination with a background activity is not accidental. If it’s not redirected, it goes to your terminal, intermixing with the output of any foreground activities. Occasionally, that might be exactly what you want. For example, here’s a timer to wake you up in 5 minutes (300 seconds):
The ampersand works consistently for things that need a new window:
A new job starts up and announces itself, then realizes that the notepad has to be run in a separate window. Once it’s started the child, the thread exits, its children are adopted by its parent thread and the child is left running as job 3.
Background activities are, in a sense, detached: typing Ctrl-C doesn’t interrupt them (unless they explicitly ask to be notified.) You can start a large number of background activities and check on their status using the ps (process status) command. Here’s what you’d see:
Several threads are always running in the background. Each spends most of the time asleep, waking up to do some housekeeping only when an interrupt or the signal that a child activity has completed is received. One thread is dedicated to reading characters from the keyboard on request from other threads. Another thread is the foreground command processor. The console_io thread was spawned when the C shell realized that notepad is a graphical application and that a pipe would be needed to capture any stdio output from the child. Other entries in the ps report come and go as you type commands.
If you want to explicitly terminate a background activity, use the kill command. But do keep in mind that under Windows, there are two ways to kill a process: If it’s a console (text window) application, it can be done by sending it a Ctrl-C signal; that’s what kill does by default. But if it’s a graphical application, it can only be done using the TerminateProcess call, a very brute force way of killing something; any DLL’s that were being used by that process will not get any notification that the process has died and, thus, will not know to do any cleanup they might normally do.
User-defined variables are shared between all active threads unless they’re declared as local. If one changes a variable’s value, the other threads see that change immediately. Because the individual threads run asynchronously, this can cause some surprising results. In this example, the foreground thread spawns new background threads and increments the variable i faster than the children can execute. By the time any the children actually start, the loop has finished and every thread sees i as having the value 5.
One solution is to use the eval statement. eval parses the text it’s passed at run-time, after any variable substitutions have been done. Because the ampersand is inside the quotes, its special meaning isn’t detected until run-time.
A better solution is to make i a local variable, since locals are snapshotted and copied when the child is spawned:
Threads also introduce the possibility of re-entrancy. In the next example, we define a procedure for summing all the integers 1 to n. Notice that it works fine if it’s run by itself, but gives the wrong answers if two threads try to run it simultaneously:
Here also, the solution is simply to include a statement defining i and s as local inside the procedure.
In building Hamilton C shell, a conscious and fundamental decision was made to use threads in many situations where earlier shells might have created separate processes. The result is a dramatically more responsive tool albeit one with some subtle semantic differences from the original.
The UNIX C shell language definition called for individual stages of a pipeline, command substitutions and scripts each to be run in a separate process cloned by forking the main process. Using forking, the child inherited all of its parent’s state (current directory, open file handles, environmental and set variables, etc.) but any changes it made only affected itself. On a UNIX system with paging hardware and the fork mechanism built into the kernel, it’s pretty fast.
But the Windows Win32 API’s do not have fork , and trying to recreate precisely this language semantic under Windows would have been foolishly expensive, potentially adding several seconds to the startup time each time you invoked a shell script. On the other hand, these systems do offer threads. A process can have lots of threads and each one can run along at its own pace. When a thread calls the kernel to do something that takes a long time (e.g., a disk read), it goes to sleep and doesn’t wake up until the data’s ready. When one thread goes to sleep, the kernel looks around for another that’s ready to run. By using threads, it’s possible to ensure that if one thing’s got to wait, that won’t hold up everything else.
Threads turn out to be even faster than a fork (regardless of the hardware), because the amount of state information associated with a thread is so little compared to that of a process. As viewed by the kernel, a thread “owns” only a register set, a stack and an instruction pointer. Everything else, memory, current directories, etc., is shared among all the threads in a process. This means creating a thread is very fast, as is switching between threads.
On the other hand, using threads to best advantage imposed some significant design challenges in Hamilton C shell. Certainly, for example, few would consider it acceptable if a script running in the background could Boom change your foreground current disk! The problem was to create a way for threads to cooperatively share the process resources but without giving away all the performance advantage we’d started with by using threads. Also, some of the elegance of threads is the idea you can keep creating new ones. Each is just like the next: any given thread can run just as complex a program as the next and each can spawn new threads. It would be a shame to lose that recursive characteristic by clumsiness in the language design.
Starting with a clean sheet of paper, our solution was a highly multi-threaded architecture. It expects you to start lots of threads: stages in a pipe, background activities, etc. To our knowledge, no other command processor on any system employs this technology. Certainly, all the code in Hamilton C shell is re-entrant: there is a minimum of global, statically-allocated data; the few variables that are global tend to be pointers to the roots of various dynamically-allocated information trees for managing variables, threads, processes, file handles and other resources. When the shell creates a new thread, it creates the appropriate records and links them in. Some characteristics given the new thread are inherited from its parent and some always get set to specific defaults.
Shared variables and other resources are semaphored: before using a resource, a thread requests it; if several resources are needed simultaneously, they’re always requested in the same order to avoid deadlocks. Critical resources are held only for short periods. There’s no polling anywhere. “Handle” mechanisms are used so that, e.g., a thread can decide if its current disk and directories are set up by simply comparing an integer. Path hashing structures are shared with a “copy on write” mechanism in case they change directories and need slightly different hash structures. Any thread can do what any other can: compile or execute an arbitrarily complex C shell program or even spawn or pipe child threads.
Given the enormous advantage offered by threads and the unique technology we’ve developed to exploit them, we expect Hamilton C shell should easily outperform any UNIX shell on comparable hardware.
Statement relationships
Order of evaluation
Compatibility
Tutorial: Interrupts