8.5 Signals – In-depth Understanding of Computer Systems (CSAPP) (gitbook.io)

[VB]ECF: Signal and Non-local Jump – Zhihu (Zhihu.com)

CSAPP Reading Notes – Signals – Notes from Chapters 8.5-8.8 -P526-P550 – Fiery Wizards – Blogland (CNblogs.com)

Modern systems respond to these conditions by mutating the control flow. In general, we refer to these mutations as Exceptional Control flows (ECFs). Exception control flows occur at all levels of the computer system. For example, at the hardware level, events detected by the hardware can trigger a sudden transfer of control to an exception handler. At the operating system level, the kernel transfers control from one user process to another through a context switch. At the application layer, one process can send signals to another process, and the receiver will suddenly transfer control to one of its signal handlers. A program can react to an error by circumventing the usual stack rules and performing a non-local jump anywhere in another function.

As a programmer, understanding ECF is important for a number of reasons:

  • ** Understanding ECF will help you understand important system concepts. **ECF is the basic mechanism used by operating systems to implement I/O, processes, and virtual memory. Before you can truly understand these important concepts, you must understand ECF.
  • ** Understanding ECF will help you understand how applications interact with the operating system. ** Applications request services from the operating system using a form of ECF called trap or system call. For example, writing data to disk, reading data from the network, creating a new process, and terminating the current process are all done by the application calling the system call. Understanding the basic system call mechanism will help you understand how these services are provided to your application.
  • ** Understanding ECF will help you write interesting new applications. ** The operating system provides powerful ECF mechanisms for applications to create new processes, wait for processes to terminate, notify other processes of abnormal events in the system, and detect and respond to those events. If you understand these ECF mechanisms, you can use them to write interesting programs such as Unix shells and Web servers.
  • ** Understanding ECF will help you understand concurrency. **ECF is the basic mechanism for implementing concurrency in computer systems. Examples of concurrency at run are exception handlers that interrupt application execution, processes and threads that overlap execution in time, and signal handlers that interrupt application execution. Understanding ECF is the first step to understanding concurrency. We’ll look at concurrency in more detail in Chapter 12.
  • ** Understanding ECF will help you understand how software exceptions work. ** languages like C++ and Java provide mechanisms for software exceptions through try, catch, and throw statements. Software exceptions allow a program to make non-local jumps (that is, jumps that violate the usual call/return stack rules) in response to an error condition. Non-local jumps are an application-layer ECF provided in C through setjMP and longjMP functions. Understanding these low-level functions will help you understand how high-level software exceptions are implemented.

Learning about systems, so far you have seen how applications interact with hardware. The importance of this chapter is that you will begin to learn how applications interact with the operating system. Interestingly, these interactions are all around ECF. We will describe the various forms of ECF that exist at all levels of a computer system. Start with exceptions, which are located at the interface between hardware and operating system. We’ll also talk about system calls, which are exceptions that provide an application with an entry point to the operating system. We then elevate the level of abstraction to describe processes and signals at the interface between the application and the operating system. Finally, non-local jumps, an application-layer form of ECF, are discussed.

8.1 abnormal

Exceptions here refer to handing control to the kernel, which is part of the operating system’s resident memory, in response to events (such as changes in processor state), such as system-level events such as dividing by zero, mathematical overflows, page errors, I/O request completion, or the user pressing CTRL + C, etc.

The specific process can be shown in the following figure:

The system determines the jump position through the Exception Table. Each event has a unique Exception number, and the corresponding Exception handling code will be called when the corresponding Exception occurs

8.1.1. Handling of exceptions

Exceptions can be difficult to understand because handling exceptions requires close cooperation between hardware and software. It’s easy to get confused about which parts perform which tasks. Let’s look at the division of hardware and software in more detail.

Exception number: Each type of exception has a non-negative integer exception number assigned partly by the processor designer and partly by the operating system kernel designer. Examples of the former include division by 0, missing pages, memory access violations, breakpoints, and arithmetic overflow. Examples of the latter include system calls and signals from external I/O devices

Exception table: The exception table is similar to the global offset table. It is a jump table that stores the address of the corresponding program and is assigned when the system starts

At system startup (when the computer is rebooted or powered up), the operating system allocates and initializes a jump table called the exception table, such that table k contains the address of the handler for exception K. Figure 8-2 shows the format of the exception table.

At run time (when the system is executing a program), the processor detects that an event has occurred and identifies the corresponding exception number K. The handler then raises the exception by performing an indirect procedure call to the appropriate handler through table k of the exception table.

Figure 8-3 shows how the processor uses the exception table to form the address of the appropriate exception handler. The exception number is the index to the exception table, whose starting address is placed in a special CPU register called exception Table Base Register.

  • During a procedure call, the processor pushes the return address onto the stack before jumping to the handler. However, depending on the type of exception, the return address is either the current instruction (the instruction that was executing when the event occurred) or the next instruction (the instruction that will be executed after the current instruction if the event does not occur).

  • The processor also pushes some extra processor state onto the stack, which is needed to restart execution of the interrupted program when the handler returns. For example, x86-64 systems push the EFLAGS register and other contents containing the current condition code onto the stack.

  • If control is transferred from the user program to the kernel, all of these items are pushed into the kernel stack, not the user stack.

  • Exception handlers run in kernel mode (see Section 8.2.4), which means they have full access to all system resources.

Once the hardware raises the exception, the exception handler does the rest in the software. In the handler after the event, it by performing a special “from the interrupt return” instruction, optionally returned to the interruption of program, the instruction will be the appropriate state control and data back to the processor register, if abort is a user program, will get back to user mode (see section 8.2.4), Control is then returned to the interrupted program.

8.1.2 Types of Exceptions

Exceptions fall into four categories: interrupts, traps, faults, and terminations. The table in Figure 8-4 summarizes the attributes of these categories.

Interrupts occur asynchronously and are the result of signals from I/O devices outside the processor. Hardware interrupts are asynchronous in the sense that they are not caused by any specific instruction. Exception handlers for hardware interrupts are often referred to as interrupt handlers.

Figure 8-5 summarizes the handling of an interrupt. I/O devices, such as network adapters, disk controllers, and timer chips, trigger interrupts by signaling a pin on the processor chip and placing an exception number on the system bus that identifies the device causing the interrupt.

1. The interruption

Interrupts occur asynchronously, also known as Asynchronous exceptions, and are the result of signals from I/O devices outside the processor. Is caused by something happening outside of the processor, and this “interrupt” occurs completely asynchronously to the executing program because it is not known when it will occur. Exception handlers for hardware interrupts are often referred to as interrupt handlers.

Figure 8-5 summarizes the handling of an interrupt. I/O devices, such as network adapters, disk controllers, and timer chips, trigger interrupts by signaling a pin on the processor chip and placing an exception number on the system bus that identifies the device causing the interrupt

After the current instruction completes execution, the processor notices that the voltage of the interrupt pin has increased, reads the exception number from the system bus, and invokes the appropriate interrupt handler. When the handler returns, it returns control to the next instruction (that is, the instruction that would have followed the current instruction in the control flow if no interrupt had occurred). The result is that the program continues as if no interruption had occurred.

There are two common types of interrupts: timer interrupts and I/O interrupts. Timer interrupts are triggered by the timer chip every few milliseconds, and the kernel uses the timer terminal to take back control from the user program. There are various types of I/O interrupts. For example, ctrL-C is entered on the keyboard and a packet is received on the network.

2. Trap and system call

A trap is a deliberate exception, the result of executing an instruction. Like interrupt handlers, trap handlers return control to the next instruction. The most important use of traps is to provide a process-like interface, called a system call, between user programs and the kernel.

User programs often need to request services from the kernel, such as reading a file (read), creating a new process (fork), loading a new program (execve), or terminating the current process (exit). To allow controlled access to these kernel services, the processor provides a special “syscall n” instruction that can be executed when a user program wants to request service N. Executing the syscall instruction results in a trap to the exception handler, which parses the parameters and invokes the appropriate kernel program. Figure 8-6 summarizes the processing of a system call.

From a programmer’s point of view, a system call is the same as a normal function call. However, their implementations are very different. Normal functions run in user mode, which limits the types of instructions functions can execute, and they can only access the same stack as the calling function. System calls run in kernel mode, which allows system calls to execute privileged instructions and access stacks defined in the kernel. Section 8.2.4 discusses user mode and kernel mode in more detail.

3. The fault

A fault is caused by an error condition that may be rectified by a fault handler. When a fault occurs, the processor transfers control to the fault handler. ** If the handler is able to correct the error condition, it returns control to the instruction that caused the failure and reexecutes it. Otherwise, the handler returns to the ABORT routine in the kernel, which terminates the application that caused the failure. ** Figure 8-7 summarizes the handling of a fault.

A classic example of a failure is the page-missing exception, which occurs when an instruction references a virtual address and the physical page corresponding to that address is not in memory and therefore must be fetched from disk. As we’ll see in Chapter 9, a page is a contiguous block of virtual memory (typically 4KB). The missing page handler loads the appropriate page from disk and then returns control to the instruction that caused the failure. When the instruction is executed again, the corresponding physical page already resides in memory, and the instruction completes without failure.

Here’s another example of referencing an invalid address:

int a[1000];
main()
{
    a[5000] = 13;
}
Copy the code

Specifically, SIGSEGV signals are sent to the user process and the user process exits with a segmentation fault mark.

As you can see from the above, the implementation of exceptions depends on switching between user code and kernel code, which is a very low-level mechanism.

4. Termination

Terminations are the result of unrecoverable fatal errors, usually hardware errors such as parity errors that occur when DRAM or SRAM bits are corrupted. The termination handler never returns control to the application. As shown in Figure 8-8, the handler returns control to an ABORT routine, which terminates the application.

8.1.3 Exceptions In Linux/x86-64

To make the description more concrete, let’s look at some exceptions defined for x86-64 systems. There are up to 256 different types of exceptions [50]. Numbers from 0 to 31 correspond to exceptions defined by the Intel architect, so they are the same for any x86-64 system. Numbers 32 to 255 correspond to interrupts and traps defined by the operating system. Figure 8-9 shows some examples.

1.Linux/x86-64 fails or terminates

** Wrong division. ** A division error (exception 0) occurs when an application tries to divide by zero, or when the result of a division instruction is too large for the target operand. Unix does not attempt to recover from a division error, opting instead to terminate the program. The Linuxshell usually reports a division error as a “Floating exception.”

** General protection fault. ** Unknown common protection failures (exception 13) can occur for many reasons, usually because a program references an undefined area of virtual memory, or because the program tries to write a read-only text segment. Linux does not attempt to recover from such failures. Linux shells typically report this general protection failure as a “segment fault.”

Missing pages (exception 14) is an example of an exception that reexecutes the instruction that caused the failure. The handler maps a page of virtual memory on the appropriate disk to a page of physical memory, and then re-executes the failed instruction. We’ll see in detail how the page misses work in Chapter 9.

** Machine inspection. ** Machine check (Exception 18) occurs when a fatal hardware error is detected in the execution of the instruction that caused the failure. The machine check handler never returns control to the application.

2.Linux/86-64 system call

Linux provides hundreds of system calls that applications can use when they want to request kernel services, including reading files, writing files, or creating a new process. Figure 8-10 shows some common Linux system calls. Each system call has a unique integer number that corresponds to an offset to the jump table in the kernel. (Note: This jump table is not the same as the exception table, where the numbers assigned to the operating system are 32-255.)

C programs can call any system call directly with the syscall function. In practice, however, there is little need for this. For most system calls, the standard C library provides a convenient set of wrapper functions. These wrapper functions package the parameters together, trap them into the kernel with the appropriate system call instructions, and then pass the return state of the system call back to the caller. Throughout this book, we refer to both system calls and their associated wrapper functions as system-level functions, and the terms are used interchangeably.

On x86-64 systems, system calls are provided through a trap instruction called syscall. It will be interesting to see how programs can use this instruction to call Linux system calls directly. All arguments to Linux system calls are passed through the generic register rather than the stack. By convention, register % rax contains the system call number, and registers %rdi, %rsi, % RDX, %r10, %r8, and % r9 contain up to six parameters. The first parameter is in % rdi, the second parameter is in % rsi, and so on. When returned from the system call, both the registers % RCX and % r11 are corrupted, and % rax contains the return value. A negative return value between -4095 and -1 indicates that an error occurred, corresponding to a negative errno.

Marginalia – Notes about terms

The terminology for the various exception types varies from system to system. The processor ISA specification typically distinguishes between asynchronous “interrupts” and synchronous “exceptions,” but does not provide a generic term to describe these very similar concepts. To avoid constant references to “exceptions and interrupts” and “exceptions or interrupts,” we use the word “exception” as a generic term, and distinguish between asynchronous exceptions (interrupts) and synchronous exceptions (traps, failures, and terminations) only when necessary. As we mentioned, the basic concepts are the same for every system, but you should be aware that some manufacturers’ manuals use the term “exception” to refer only to changes in control flow caused by synchronous events.

8.2 process

Key abstractions that processes give to applications:

  • A separate logical control flow, which provides the illusion that our program uses the processor exclusively
  • A private address space, which provides the illusion that our program exclusively uses the memory system

Context switching (process scheduling) mechanism works:

  • Saves the context of the current process
  • Restores the context in which a previously preempted process was saved
  • Pass control to the newly restored process

8.3 System Call Error Handling

When Unix system-level functions encounter an error, they typically return -1 and set the global integer variable errno to indicate what went wrong. Programmers should always check for errors, but unfortunately, many people ignore error checking because it makes code bloated and hard to read. For example, here’s how we check for errors when we call Unix fork:

if ((pid = fork()) < 0) {
    fprintf(stderr."fork error: %s\n", strerror(errno));
    exit(0);
}
Copy the code

The strError function returns a text string describing the error associated with an errno value. We can simplify this code to some extent by defining the following error reporting function:

void unix_error(char *msg) /* Unix-style error */
{
    fprintf(stderr."%s: %s\n", msg, strerror(errno));
    exit(0);
}
Copy the code

Given this function, our call to fork is reduced from 4 to 2 lines:

if ((pid = fork()) < 0)
    unix_error("fork error");
Copy the code

Here is the error-handling wrapper for fork:

pid_t Fork(void)
{
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}
Copy the code

Given this wrapper function, our call to fork is reduced to 1 line:

pid = Fork();
Copy the code

We will use error-handling wrappers throughout the rest of this book. They keep code examples simple without giving you the illusion of error by allowing you to ignore error checking. Note that when we talk about system-level functions in this book, we always refer to them by their basic names in lowercase letters, not by their uppercase wrapper function names.

8.4 Process Control

Unix provides a large number of system calls to manipulate processes from C programs. This section describes these important functions and illustrates how to use them.

8.4.1 Obtaining the Process ID

Each process has a unique positive (non-zero) process ID (PID). Getpid returns the PID of the calling process. The getppId function returns the PID of its parent (the process that created the calling process)

#include <sys/type.h>  
#include <unistd.h>  
pid_t getpid(a);   // Return the PID of the process. On Linux, pid_t is defined as int
pid_t getppid(a);  // Return the parent process PID
Copy the code

The getpid and getppID functions return an integer value of type pid_t, which on Linux is defined as int in types.h.

8.4.2 Creating and Stopping processes

From a programmer’s perspective, we can think of a process as always in one of three states:

  • * *. ** processes are either executing on the CPU or waiting to be executed and eventually scheduled by the kernel.
  • * * pause. The execution of a ** process is suspended and will not be scheduled. When a SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU signal is received, the process pauses and remains stopped until it receives a SIGCONT signal, at which point it starts running again. (Signaling is a form of software interruption, described in more detail in Section 8.5.)
  • * * is terminated. The revolution has stopped forever. Processes terminate for three reasons:
    • 1) Receive a signal whose default behavior is to terminate the process;
    • 2) return from the main program;
    • 3) Call exit.
#include <stdlib.h>

void exit(int status);

// This function does not return.
Copy the code

The exit function terminates the process with status (another way to set the exit status is to return an integer value from the main program).

The parent creates a new running child process by calling the fork function.

#include <sys/types.h>#include <unistd.h>pid_t fork(void);// Return: the child returns 0, the parent returns the PID of the child, or -1 if there is an error.
Copy the code

The newly created child is almost but not exactly the same as the parent. The child gets an identical (but separate) copy of the parent’s user-level virtual address space, including code and data segments, heaps, shared libraries, and user stacks. The child also gets the same copy of any open file descriptors as the parent, which means that when the parent calls fork, the child can read and write any open files in the parent. The biggest difference between the parent process and the newly created child process is that they have different Pids.

The fork function is interesting (and often confusing) because it is called only once and returns twice: once in the calling process (parent) and once in the newly created child process. In the parent process, fork returns the PID of the child process. In the child process, fork returns 0. Because the PID of the child process is always non-zero, the return value provides a clear way to tell whether the program is executing in the parent or child process.

8.4.3 Reclaiming a subprocess

When a process terminates for some reason, the kernel does not immediately purge it from the system. Instead, a process is kept in a terminated state, until reaped by its parent.

When the parent reclaims the terminated child, the kernel passes the exit status of the child to the parent and then abandons the terminated process, at which point the process no longer exists.

A terminated process that has not yet been reclaimed is called zombie — the zombie process has been terminated, and the kernel retains some of its state until the parent process retrieves it.

If a parent process terminates, the kernel arranges init to become the adoptive parent of its orphan process. The init process, which has a PID of 1, is created by the kernel at system startup. It does not terminate and is the ancestor of all processes. If the parent process terminates without reclaiming its dead children, the kernel arranges the init process to reclaim them, so there is generally no need to explicitly reclaim them. However, long-running programs, such as shells or servers, should always recycle their dead child processes. Even when zombie child processes are not running, they still consume system memory resources.

One important point here is that the child terminates and the parent needs to wait to retrieve the child before it can be removed from the zombie process. If the parent terminates, init automatically reclaims its dead child. (Of course, if the child process is still executing, it will not be recycled.)

As you can see, the child terminates, and the parent loop while (1) needs to wait to retrieve the child because the parent does not exit. Otherwise, the child remains dead

As you can see, we have successfully reclaimed the child process by killing the parent process, because init does the reclamation automatically instead.

waitpid

Here, the parent exits and the child continues.

This example shows that a child process must enter a zombie state before it can be recycled.

A process can wait for its children to terminate or stop by calling the waitPID function.

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *statusp, int options);

// Returns the PID of the child process if successful, 0 if WNOHANG, and -1 if other errors.
Copy the code

The WaitPID function is a bit complicated. By default (when options=0), WaitPID suspends execution of the calling process until a child of its wait set terminates. If a process in the wait set terminates at the moment it is called, waitPID returns immediately. In both cases, waitPID returns the PID of the terminated child process that causes WaitPID to return. At this point, the terminated child process has been reclaimed, and the kernel removes all traces of it from the system.

1. Determine the members of the waiting set

The membership of the wait set is determined by the parameter PID:

  • If Pid>0, then the wait set is a single child process with a process ID equal to Pid.
  • If Pid=-1, then the wait set consists of all the children of the parent process without any restrictions, and waitPID and wait are the same
  • If PID =0, wait for any child processes in the same process group. If the child has already joined another process group, waitPID does not do anything about it.
  • If pid<-1, wait for any child processes in a specified process group whose ID is equal to the absolute value of the PID. 天安门事件

The waitPID function also supports other types of wait sets, including Unix process groups, which we won’t discuss.

2. Modify the default behavior

You can modify the default behavior by setting options to various combinations of constants WNOHANG, WUNTRACED, and WCONTINUED

**WNOHANG: ** If the child process specified by pid does not end, the waitpid() function returns 0 immediately, instead of blocking and waiting on this function; If it is finished, the process number of the child process is returned. This option is useful if you want to do something useful while the child process terminates. (Equivalent to polling)

**WUNTRACED: ** Suspends the execution of an invocation process until one of the processes in the waiting set becomes terminated or is stopped. The PID returned is the ONE that caused the aborted or stopped child process to return. The default behavior is to suspend the calling process until a child process terminates.

**WCONTINUED: ** Execution of a process called by hanging up until a running process in the wait set terminates or a stopped process in the wait set receives a SIGCONT signal to restart execution

Can also take these options through the configuration or operation, such as WNOHANG | WUNTRACED: return immediately, without waiting for the child process in collections are stopped or terminated, it returns 0, if there is a stop or terminated, it returns the PID of the child process

0: blocks wait

If waitpid is called with the WNOHANG(wait no hung) argument, it will return immediately even if no child process exits, rather than waiting forever like wait. (Non-blocking)

WUNTRACED parameters, due to involve some trace debugging knowledge, and rarely used, here is not much ink, interested readers can check related materials.

3. Check the exit status of the reclaimed subprocess

If the statusp parameter is non-empty, waitPID puts the status information about the child process that led to the return in status, which is the value that Statusp points to. The wait.h header defines several macros that interpret the status argument:

  • WIFEXITED(status) : returns true if the process terminates normally through a call to exit or a return.

  • **WEXITSTATUS(status) : ** Returns the exit status of a normally terminated child process. This state is defined only if WIFEXITED() returns true.

  • Wait signaled (status) if the child ends because of an unsignaled signal, So return true.

  • **WTERMSIG(status) : ** Returns the number of the signal that caused the child process to terminate. Wait signaled () are released from the state only when they become true.

  • **WIFSTOPPED(status) : ** Returns true if the child process that caused the return is currently stopped.

  • **WSTOPSIG(status) : ** Returns the number of the signal that caused the child process to stop. This state is defined only if WIFSTOPPED() returns true.

  • **WIFCONTINUED(Status) : ** Returns true if the child process is restarted by SIGCONT.

Error conditions

If the calling process has no children, waitPID returns -1 and sets errno to ECHILD. If the waitPID function is interrupted by a signal, it returns -1 and sets errno to EINTR.

wait

The wait function is a simpler version of the waitpid function:

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *statusp);
// Returns the PID of the child process on success, or -1 on error.
Copy the code

Calling wait(&status) is equivalent to calling WaitPID (-1, &status, 0), and the wait set is all the child processes

example

In this example, the child process exits directly, and the parent process uses WAIT to retrieve the child process, and then prints CT, so the state diagram is as shown in the figure above.

N=5, our parent calls fork 5 times and retrieves 5 times. We use WIFEXITED to make the child process end normally True and False otherwise. If it ends normally, we use WEXITSTATUS to get the return code.

This is different from the wait version above, in that it specifies the number of the child processes to recycle, so it must be in order

8.4.4 Hibernating processes

The sleep function suspends a process for a specified period of time.

#include <unistd.h>unsigned int sleep(unsigned int secs);// Return: number of seconds to sleep.
Copy the code

Sleep returns 0 if the requested amount of time is up, otherwise it returns the number of seconds left to sleep. The latter situation is possible if the sleep function is returned prematurely because it was interrupted by a signal. We’ll discuss signals in detail in Section 8.5.

Another function we’ll find useful is pause, which puts the calling function to sleep until the process receives a signal.

#include <unistd.h>
int pause(void);

// Always return -1.
Copy the code

8.4.5 Loading and Running the Program

The execve function loads and runs a new program in the context of the current process.

#include <unistd.h>
int execve(const char *filename, const char *argv[],
           const char *envp[]);

// No return on success, -1 on error.
Copy the code

Filename is the name of an executable object file, argv [0] is the name of an executable object file, argv [0] is the name of an executable object file, argv [0] is the name of an executable object file, argv [0] is the name of an executable object file, argv [0] is the name of an executable object file, argv [0] is the name of an executable object file, and envp is the name of an environment variable

The execve function loads and runs an executable object file named filename with the argument list argv and the environment variable list envp. Only when an error occurs, such as missing filename, does execve return to the calling program. So, unlike fork, which returns twice, execve calls once and never returns.

The list of environment variables is represented by a similar data structure, as shown in Figure 8-21. The ENVP variable points to a null-terminated array of Pointers, where each pointer points to an environment variable string, and each string is a name-value pair of the form “name=value”.

After execve loads filename, it invokes the startup code described in Section 7.9. Start the code setup stack and pass control to the new program’s main function, which has a prototype of the form:

int main(int argc, char **argv, char **envp);
Copy the code

Or equivalent

int main(int argc, char *argv[], char *envp[]);
Copy the code

Figure 8-22 shows the organizational structure of the user stack when main starts running.

Let’s take a look from the bottom of the stack (high address) to the top of the stack (low address). First, the parameters and environment strings. Immediately following the stack is a null-terminated array of Pointers, where each pointer points to an environment variable string in the stack. The global environ variable points to the first of these Pointers, ENvp [0] O, followed by a null-terminated array of argv[], where each element points to a parameter string in the stack. At the top of the stack is the stack frame for the system startup function libc_start_main (see Section 7.9).

The main function takes three arguments:

  1. Argc, which gives the number of non-null Pointers to the argv[] array;

  2. Argv, pointing to the first entry in the argv[] array;

  3. Envp, pointing to the first entry in the envp[] array.

Linux provides several functions to manipulate environment arrays:

#include <stdlib.h>
char *getenv(const char *name);

// Returns a pointer to name if it exists, or NULL if it does not match.
Copy the code

The getenv function searches the environment array for the string “name=value”. If found, it returns a pointer to value, otherwise it returns NULL.

#include <stdlib.h>

int setenv(const char *name, const char *newvalue, int overwrite);
// Returns 0 on success, -1 on error.

void unsetenv(const char *name);
// Return: none.
Copy the code

If the environment array contains a string of the form “name=oldva1ue”, unsetenv deletes it and Setenv replaces oldValue with newValue, but only if overwirte is non-zero. If name does not exist, setenv adds “name= newValue” to the array.

#include <csapp.c>

int main(int argc, char *argv[], char *envp[])
{
    int i;

    printf("Command-line arguments:\n");
    for (i = 0; argv[i] ! =NULL; i++)
        printf(" argv[%2d]: %s\n", i, argv[i]);

    printf("\n");
    printf("Environment variables:\n");
    for (i = 0; envp[i] ! =NULL; i++)
        printf(" envp[%2d]: %s\n", i, envp[i]);

    exit(0);
}
Copy the code

8.4.6 Run the program using fork and execve

Programs such as Unix shells and Web servers make extensive use of the fork and execve functions. The shell is an interactive application-level program that runs other programs on behalf of the user. The shell performs a series of read/evaluate steps and then terminates. The read step reads a command line from the user. The evaluation step parses the command line and runs the program on behalf of the user

The following code shows the main routine of a simple shell. The shell prints a command line prompt, waits for the user to type the command line on stdin, and then evaluates the command line.

#include <csapp.c>
#define MAXARGS 128

/* Function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);

int main(a)
{
    char cmdline[MAXLINE]; /* Command line */

    while (1) {
        /* Read */
        printf(">");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))	//feof returns 1 if there is no content in stdin
            exit(0);

        /* Evaluate */eval(cmdline); }}Copy the code

The code to evaluate the command line is shown below. Its first job is to call the parseline function, which parses the whitespace delimited command-line arguments and constructs the argv vector that will eventually be passed to execve. The first argument is assumed to be either a built-in shell command name, which will be explained in a moment, or an executable object file that will be loaded and run in the context of a new child process.

/* eval - Evaluate a command line */
void eval(char *cmdline)
{
    char *argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg? * /
    pid_t pid;           /* Process id */

    strcpy(buf, cmdline);	Comline [] copy to buf[]
    bg = parseline(buf, argv);	
    if (argv[0] = =NULL)
        return;   /* Ignore empty lines */

    if(! builtin_command(argv)) {// If it is not a built-in directive, it is executed as an executable
        if ((pid = Fork()) == 0) {   /* Child runs user job */
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0); }}// If it is a built-in instruction, execute the following code
        /* Parent waits for foreground job to terminate */
        if(! bg) {// If it is not running in the background
            int status;
            if (waitpid(pid, &status, 0) < 0)	// Block, wait for the child process to terminate and reclaim the child process
                unix_error("waitfg: waitpid error");
            }
        else	// If the process is running in the background, print a pid + cmdline, let it continue to execute
            printf("%d %s", pid, cmdline);
    }
    return;
}

/* If first arg is a builtin command, run it and return true */
int builtin_command(char **argv)
{
    if (!strcmp(argv[0]."quit")) /* quit command */
        exit(0);
    if (!strcmp(argv[0]."&"))    /* Ignore singleton & */
        return 1;
    return 0;                     /* Not a builtin command */
}
Copy the code

In our eval, we’ve annotated it in detail, and the general process is to call bg = parseline(buf, argv); To split buf into argV arrays with Spaces and return whether it is background execution. Then there are the two branches. If argv is a built-in command, execute it in builtin_command. If it is not a built-in command, call a fork with a child for execve and a parent waitpid (if not background).

Finally, let’s see what parseline looks like:

/* parseline - Parse the command line and build the argv array */
int parseline(char *buf, char **argv)
{
    char *delim;         /* Points to first space delimiter */
    int argc;            /* Number of args */
    int bg;              /* Background job? * /

    buf[strlen(buf)- 1] = ' ';  /* Replace trailing '\n' with space */
    while (*buf && (*buf == ' ')) /* Ignore leading spaces */
        buf++;

    /* Build the argv list */
    argc = 0;
    while ((delim = strchr(buf, ' '))) {
        argv[argc++] = buf;
        *delim = '\ 0';
        buf = delim + 1;
        while (*buf && (*buf == ' ')) /* Ignore spaces */
            buf++;
    }
    argv[argc] = NULL;

    if (argc == 0) /* Ignore blank line */
        return 1;

    /* Should the job run in the background? * /
    if ((bg = (*argv[argc- 1] = ='&')) != 0)
        argv[--argc] = NULL;

    return bg;
}
Copy the code

If the last argument is an ampersand character, parseline returns 1, indicating that the program should be executed in the background (the shell won’t wait for it to finish). Otherwise, it returns 0, indicating that the program should be executed in the foreground (the shell waits for it to complete).

After parsing the command line, the eval function calls the builtin_command function, which checks whether the first command-line argument is a built-in shell command. If so, it immediately interprets the command and returns a value of 1. Otherwise 0 is returned. A simple shell has only one built-in command, the quit command, which terminates the shell. The actual shell used has a large number of commands, such as PWD, Jobs, and fg.

If builtin_command returns 0, the shell creates a child process and executes the requested program in the child process. If the user asks to run the program in the background, the shell returns to the top of the loop and waits for the next command line. Otherwise, the shell waits for the job to terminate using the waitPID function. When the job terminates, the shell starts the next iteration.

Note that this simple shell is flawed because it does not recycle its backend child processes. Fixing this defect requires the use of signals, which we’ll cover in the next section.

The final code looks like this:

#include <csapp.c>
#define MAXARGS 128

/* Function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);

int main(a)
{
    char cmdline[MAXLINE]; /* Command line */

    while (1) {
        /* Read */
        printf(">");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))	//feof returns 1 if there is no content in stdin
            exit(0);

        /* Evaluate */eval(cmdline); }}/* eval - Evaluate a command line */
void eval(char *cmdline)
{
    char *argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg? * /
    pid_t pid;           /* Process id */

    strcpy(buf, cmdline);	Comline [] copy to buf[]
    bg = parseline(buf, argv);	
    if (argv[0] = =NULL)
        return;   /* Ignore empty lines */

    if(! builtin_command(argv)) {// If it is not a built-in directive, it is executed as an executable
        if ((pid = Fork()) == 0) {   /* Child runs user job */
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0); }}// If it is a built-in instruction, execute the following code
        /* Parent waits for foreground job to terminate */
        if(! bg) {// If it is not running in the background
            int status;
            if (waitpid(pid, &status, 0) < 0)	// Block, wait for the child process to terminate and reclaim the child process
                unix_error("waitfg: waitpid error");
            }
        else	// If the process is running in the background, print a pid + cmdline, let it continue to execute
            printf("%d %s", pid, cmdline);
    }
    return;
}

/* If first arg is a builtin command, run it and return true */
int builtin_command(char **argv)
{
    if (!strcmp(argv[0]."quit")) /* quit command */
        exit(0);
    if (!strcmp(argv[0]."&"))    /* Ignore singleton & */
        return 1;
    return 0;                     /* Not a builtin command */
}

/* parseline - Parse the command line and build the argv array */
int parseline(char *buf, char **argv)
{
    char *delim;         /* Points to first space delimiter */
    int argc;            /* Number of args */
    int bg;              /* Background job? * /

    buf[strlen(buf)- 1] = ' ';  /* Replace trailing '\n' with space */
    while (*buf && (*buf == ' ')) /* Ignore leading spaces */
        buf++;

    /* Build the argv list */
    argc = 0;
    while ((delim = strchr(buf, ' '))) {
        argv[argc++] = buf;
        *delim = '\ 0';
        buf = delim + 1;
        while (*buf && (*buf == ' ')) /* Ignore spaces */
            buf++;
    }
    argv[argc] = NULL;

    if (argc == 0) /* Ignore blank line */
        return 1;

    /* Should the job run in the background? * /
    if ((bg = (*argv[argc- 1] = ='&')) != 0)
        argv[--argc] = NULL;

    return bg;
}
Copy the code