| Operating
Systems [22c:112] |
| Tutorial 2: The first project |
| At page 64 of
your book there is a nice example of a primitive shell that take the
input commands from a text file. Here there is the code explained
line-by-line: #include <stdio.h> #include <unistd.h> In lines 1-2 we're including the two libraries containing the functions used in this little software. You always have to include the stdio.h file in your code, because it contain the standard functions and variable types, and for each system function you want to use, check launching man <function> at the shell prompt what libraries are needed by the function you want to use. #define MAX_ARGS 64 #define MAX_ARG_LEN 16 #define MAX_LINE_LEN 80 #define WHITESPACE " .,\t\n" Here we are defining some magic words that we can use while programming. Each time we will compile the code, the compiler will substitute MAX_ARGS with the number 64, MAX_ARG_LEN with the number 16, and so on. Sometimes is useful to do that because if in the future you want to change the maximum number of arguments supported by your software, you have to change it only once, and not in all the points you used that number in the software. In addition, is easyer for who read the code to understad MAX_ARGS, than an anonymous 16 placed somewhere in an IF statement or similar. struct command_t { char *name; int argc; char *argv[MAX_ARGS]; }; Here we're creating a new structure. A structure is something similar to an integer, or a string, only more complicated, and personalized. Here, we're telling to the computer that the new name for the structure is command_t (so, instead of saying for example int foo, will will write struct command_t foo) and we're explaining its characteristic. This structure has 3 fields: the first one has name name and is a pointer to a string, the second has name argc and is an integer, and the third is an array of pointers to strings and has name argv. int main(int argc, char *argv[]) { This is the first line of the the main function. The main function is the one that will be executed when someone will launch our program from a shell. It has two parameters: the first one argc contains the number of parameters you passed, and the second argv is an array of pointers to strings. These are the "real" argc and argv variables that you shouldn't confuse with the ones in the command_t structure. For example, the variable argv[0] contains a pointer to a string that contains the name of the file, argv[1] contains a pointer to a string that contains the first parameter passed by the command line, argv[2] the second, and so on. int i; int pid, numChildren; int status; Here we're declaring 4 integers named i, pid, numChildren and status. No big deal, that's it. FILE *fid; This is the declaration of fid, a pointer to a file. We will use function as fopen, fputs, fgets, fclose, to manipulate this handle, and consequently the file associated with him. char cmdLine[MAX_LINE_LEN]; Guess what...yes, it is. This is simply an array of character, with length MAX_LINE_LEN and name cmdLine. Small exercise: how much is MAX_LINE_LEN? struct command_t command; Here we are! As explained before here we're initializing a variable with name command, that is a structure of the new type we just declared some lines above. We will refer to its field using command->name, command->argc, and so on. if(argc != 2) { fprintf(stderr, "Usage: launch <launch_set_filename>\n"); exit(0); } Easy to understand, if the variable argc is different from 2, print on the standard output for errors (in our case the screen) that line, and exit interrupting the execution of the program. Otherwise, do nothing and keep executing the program. fid = fopen(argv[1], "r"); The function fopen return a "handle" to the file given as it first parameter, with the right defined by its second parameter. The variable argv[1] contains a pointer to a string containing the first parameter passed by the command line. Here the second parameter is "r", so fopen will return a "handle" to a file in the local path with name equal to the first passed argument and with the read-only right. The returned handle will be saved in the variable fid. numChildren = 0; The variable numChildren is initialized with the value 0. while (fgets(cmdLine, MAX_LINE_LEN, fid) != NULL) { The function fgets read a line (with a maximum of MAX_LINE_LEN character) from the handle passed as it third parameter (fid) and save it in the space pointed by its first parameter (cmdLine). If it return NULL, there are no more lines in the file, so we have to exit from the cycle. parseCommand(cmdLine, &command); This function parse the command from cmdLine. It will extract the name of the program called, its eventual parameters, and will store everything in the command structure. command.argv[command.argc] = NULL; This line terminate the command arguments with a NULL. Since command.argc count the number of arguments given after the command (remember, we're talking about cmdLine's command) now it contain a number that is bigger than the last argument (since in C the arrays start from 0). Executing this line we are setting to NULL the last pointer of the array command.arvg. if((pid = fork()) == 0) { execvp(command.name, command.argv); } The function fork() create a copy of the program in the main memory. Basically it split the software in such a way that two copies of the same program will be running. In the old copy, it will return the PID (Process IDentification number) of the newly created process, while in the new copy will return 0 (the new copy of the program will start exactly from where the fork() has been executed, and all the variables will have initially the same value as in the original copy until just before the fork()). With the guard in the IF we are so checking "where" we are. If the variable pid is 0, it mean that we are in the child, and so we have to execute the instructions contained instead the curling brackets. The function execvp simply execute in the system the file pointed by its first parameter (command.name), with the parameters pointed by its second parameter (command.argv). The function will not return if the command will terminate correctly, so the newly created child will terminate itself without problems. numChildren++; } If we reach this point we are in the father (or the original program, as you prefer), so we increment numChildren, to keep track of how many children we executed. printf("\n\nlaunch: Launched %d commands\n",numChildren); Do I need to explain this? We're printing Launched commands substituting the %d with the number contained in numChildren. for(i = 0; i< numChildren; i++) { wait(&status); } The function wait() make the computer sleeping until one of the children is terminated. The loop will be repeated until the variable i will be lower than numChildren, so basically we're waiting the termination of all the children we lanuched. printf("\n\nlaunch: Terminating successfully\n"); return 0; } When the main program reach that point, it has finished to read the text file, it has executed all the instructions and it has waited for the termination of all the children. So it can exit gracefully. Now I will try to explain what the function parseCommand does. Its purpose is to analyze the line grabbed by the text file, separate the command from its parameters, and put everything in the command_t structure given as parameter. int parseCommand(char *cLine, struct command_t *cmd) { As before, this is the first line of the declaration of the function. It say that the function will return an integer, and that has a pointer to a string (that inside the function will be called cLine) and a pointer to a command_t structure (cmd in the function) as parameters. int argc; char **clPtr; clPtr = &cLine; Here they declare an integer variable (argc), a pointer to a pointer to a string (clPtr), and a pointer to the variable cLine (look between the parameters of the declaration of this function). argc = 0; Guess what. Yes, the internal (because since it has been declared at the beginning of the function, when we will refeer to argc we will be referring to the internal argc) argc is now 0. cmd->argv[argc] = (char *) malloc(MAX_ARG_LEN); The function malloc() return an undefined pointer to an area of memory able to cantain MAX_ARG_LEN of bytes. We are going to save this pointer inside the variable argv[argc] of the structure cmd (that is a command_t structure). But since that variable is a pointer to a string, we have to cast (that mean explain to the compiler what is the "format" of that result) it to a char pointer. That's the reason because there is that strange (char *). while ((cmd->argv[argc] = (char*) strsep(clPtr, WHITESPACE)) != NULL) { You better look at what the function srtsep exactly does yourself (man strsep). Basically here we are cycling in the string pointed by clPtr, looking for a whitespace, that separate the command from its parameters. cmd->argv[++argc] = (char *) malloc(MAX_ARG_LEN); } Once we found that, we allocate more memory to save what returned by that function in one of the places of the array cmd->argv, incrementing the counter at the same time. cmd->argc = argc-1; We substract one from the argc counter. cmd->name = (char *) malloc(sizeof(cmd->argv[0])); We allocate enough space in the memory to contain the first "parameter" in the command line (that is the name of the software to launch) and we save the returned pointer into the cmd->name variable. strcpy(cmd->name, cmd->argv[0]); We copy that string (the command name) inside the allocate memory. Take a look to what exactly does the function strcpy (man strcpy). return 1; } And we return to the function where the parseCommand functions have been called. We're done. :) |