Using fork and exec Together
In most programs, the fork and exec calls are used in conjunction with one another (in some operating systems, the fork and exec calls are packaged as a single spawn system call). The parent process generates a child process, which it then overlays by a call to exec , as in Program 3.6.
Program 3.6 Using fork with execlp .
File : p3.6.cxx /* Overlaying a child process via an exec */ #include + #include #include #include using namespace std; int 10 main( ){ char *mesg[ ] = {"Fie", "Foh", "Fum"}; int display_msg(char *); for (int i=0; i < 3; ++i) display_msg(mesg[i]); + return 0; } int display_msg(char *m){ ostringstream oss(ostringstream::out); 20 switch (fork( )) { case 0: sleep(1); execlp("/bin/echo", "echo", m, (char *) NULL); oss << m << " exec failure"; // build error msg string + perror(oss.str().c_str()); return 1; case -1: perror("Fork failure"); return 2; 30 default: return 0; } }
Program 3.6 displays three messages (based on the contents of the array mesg ). This action is accomplished by calling the display_msg function three times. Once in the display_msg function, the program fork s a child process and then overlays the child process code with the program code for the echo command. The output of the program is shown in Figure 3.6.
Figure 3.6 Output of Program 3.6.
linux$ p3.6 Foh Fie Fum
Due to scheduling, the order of the messages may change when run multiple times.
It is interesting to observe what happens if the execlp call in display_msg fails (line 23). If we purposely sabotage the execlp system call by changing it to
execlp("/bin/no_echo", "echo", m , (char *) NULL );
and assuming there is not an executable file called no_echo to be found in /bin , the output [3] of the program becomes that shown in Figure 3.7.
[3] The program uses a common programming trick to create a message string on-the-fly to pass to the perror routine.
Figure 3.7 Output of Program 3.6 when execlp fails.
linux$ p3.6 Foh exec failure: No such file or directory Fie exec failure: No such file or directory Fum exec failure: No such file or directory Fum exec failure: No such file or directory Foh exec failure: No such file or directory Fum exec failure: No such file or directory Fum exec failure: No such file or directory
Surprisingly, when the execlp call fails, we end up with a total of eight processesthe initial process and its seven children. Most likely this was not the intent of the original programmer. One way to correct this is within the display_msg function: In the case 0: branch of the switch statement, replace the return statement in line 26 with a call to exit .
Combining what we have learned so far, we can produce, in relatively few lines of code, a shell program that restricts the user to a few basic commands (in this example, ls , ps , and df ). The code for our shell program [4] is shown in Program 3.7.
[4] For reasons that become obvious when the program is run, this is nicknamed the huh shell.
This program could be considered a very stripped-down version of a restricted [5] shell. The main thrust of the program is pedagogical , and improvements and expansions (of which there can be many) will be addressed in ensuing sections of the text and in a number of exercises.
[5] Many UNIX environments come with a predefined restricted shell (which is different from the remote shell /bin/rsh ). A restricted shell is sometimes specified as a login shell for users (such as ftp ) that require a more controlled environment. Linux does not come with a specific restricted shell for users, but some of the standard shells (such as bash and ksh ) can be passed a command-line option ( r ) that will run the shell in restricted mode. Linux does come with a restricted shell for sendmail ( smrsh ).
Program 3.7 The huh shell.
File : p3.7.cxx /* A _very_ limited shell program */ #include + #include #include #include #include using namespace std; 10 const int MAX =256; const int CMD_MAX=10; char *valid_cmds = " ls ps df "; int + main( ){ char line_input[MAX], the_cmd[CMD_MAX]; char *new_args[CMD_MAX], *cp; int i; while (1) { 20 cout << "cmd> "; if (cin.getline(line_input, MAX, ' ') != NULL) { cp = line_input; i = 0; if ((new_args[i] = strtok(cp, " ")) != NULL) { + strcpy(the_cmd, new_args[i]); strcat(the_cmd, " "); if ((strstr(valid_cmds, the_cmd)valid_cmds) % 4 == 1) { do { cp = NULL; 30 new_args[++i] = strtok(cp, " "); } while (i < CMD_MAX && new_args[i] != NULL); new_args[i] = NULL; switch (fork( )) { case 0: + execvp(new_args[0], new_args); perror("exec failure"); exit(1); case -1: perror("fork failure"); 40 exit(2); default: // In the parent we should be waiting for // the child to finish ; + } } else cout << "huh?" << endl; } } 50 } }
The commands the user is permitted to issue when running our shell are found in the global character string called valid_cmds . In the valid_cmds string, each two-letter command is preceded and followed by a space. By delimiting the commands in this manner, a predefined C string searching function strstr can be used to determine if a user has entered a valid command. While this technique is simplistic, it is effective when a limited number of commands need to be checked. The program then issues a shell-like prompt, cmd> , and uses the C++ input function getline to store user input in a character array buffer called line_input . The getline function will read a line of input, including intervening whitespace that is terminated by a newline. If the getline function fails (such as when the user just presses return), the program loops back around and reprompts the user for additional input. Upon entry of input, the program uses the C string function strtok to obtain the first valid token from the line_input array. The strtok function, which will divide a referenced character string into tokens, requires a pointer to the array it is to parse and a list of delimiting characters that delimit tokens (in this case only a blank " " has been indicated). The strtok function is a wonderful example of the idiosyncratic nature of some functions in C/C++. When strtok is called successive times and passed a reference to NULL, it will continue to parse the initial input line starting each time where it left off previously. The strcat function is used to add a trailing blank to this first token (assumed to the command), and the resulting sequence is stored in a character array called the_cmd .
The next line of the program checks for the presence of the command in the valid_cmds string at a modulus -4-based offset (see Figure 3.8).
Figure 3.8. Character offsets in the valid_cmds string.
If the command is found, a do-while loop is used to obtain the remaining tokens (up to the limit CMD_MAX ). These tokens are stored in successive elements of the previously declared new_args array. Upon exiting the loop, we assure that the last element of the new_args array contains the requisite NULL value. A switch statement, in concert with fork and execvp system calls, is used to execute the command.
EXERCISE
In Program 3.7, why was new_args[0] , rather than the reference the_cmd , passed to the execvp system call? |