# Homework Report - Simple Shell ## (1) 開發環境 在ubuntu上使用CodeBlocks,並用C語言來撰寫。 ## (2) 程式邏輯 ### 1. (1) 印出父子程序的return值: ```clike= pid_t pid = fork(); if (pid == 0){ printf("Child return : %d\n", pid); exit(0); } else{ printf("Parent return : %d\n", pid); wait(NULL); } ``` 利用fork()創造出子程序後,在各自印出來。子程序會return 0,而父程序會印出大於零的數字。 (2) 控制父子程序的執行順序: ```clike= int num; pid_t pid = fork(); if (pid == 0) { printf("Child run first.\n"); printf("Please input a number: \n"); } else { wait(NULL); printf("------------------------"); printf("\nChild completed and Input in parent process :"); scanf(" %d", &num); printf("Output in parent process : %d\n", num); } ``` 在父程序中使用wait()來確保子程序完成後再執行父程序的指令。我用了很簡單的應用:在子程序中印出輸入提示,但最後輸入是在父程序來完成,並且印出輸入。 ### 2. (1) 寫出基本的shell,且能執行各種linux指令及參數。 - 控制整個shell的流程 ```clike= main : while (1){ type_prompt(); read_command(input); if (input[0] == '\n') continue; parse = parse_input(input); char *cmd = strtok(parse, " "); int i = 0; int check = 1; if(vfork() != 0){ wait(NULL); if (check == 0) break; } //if the command which is not exit, should say something. else{ int redirection_check = 0 , pipe_check = 0; while(cmd){ if (*cmd == '>'){ cmd = strtok(NULL, " "); red_to_file(cmd); if (pipe_check == 1) run_pipe(pipe_cmd); command[i] = NULL; if (fork() == 0){ execvp(command[0], command); exit(0); } else{ wait(NULL); } redirection_check = 1; } else if (*cmd == '<'){ cmd = strtok(NULL, " "); red_from_file(cmd); if (pipe_check == 1) run_pipe(pipe_cmd); command[i] = NULL; if (fork() == 0){ execvp(command[0], command); exit(0); } else{ wait(NULL); } redirection_check = 1; } else if (*cmd == '|'){ command[i] = NULL; run_pipe(command); pipe_check = 1; int j = 0; while (command[j]){ pipe_cmd[j] = command[j]; j++; } pipe_cmd[j] = NULL; i = 0; } else{ command[i] = cmd; i++; } cmd = strtok(NULL, " "); } if (redirection_check == 1) exit(0); command[i] = NULL; built_in_cmd(command); execvp(command[0], command); } if (strcmp(command[0], "exit") == 0){ check = 0; exit(0); break; } } ``` 這是我主要控制整個shell的流程,首先type_prompt()是印出每次的輸入提示,之後在read_command()中輸入input指令,輸入完得到完整字串,透過parse_line()來處理子傳為分隔的token以便判斷linux指令及參數。分隔完的token會存到parse字元陣列中並以token與token用空格隔開的方式儲存,所以之後在內部的while迴圈中都是用空格(" ")來做一次token的更新判斷,每執行完一次while就用strtok()函數來找到下一個token,值到為NULL。其中因為要有redirection和pipe的功能所以要判斷他們的執行字符,並呼叫各自的執行函數。其中pipe_check和redirection_check變數是用來處理連續重導入時所用,因為多個導入需要由一開始的指令去個別操作要倒入的對象,所以需要加入一個check變數來確保每個獨立的導入對象都可以被執行,且最後要避免離開while迴圈時多執行一遍。 - 讀取input ```clike= void read_command(char input[]){ int count = 0; while (1){ int c = fgetc(stdin); //fgetc returns the character read as an int //stdin is standard input. input[count++] = (char) c; if (c == '\n') break; } input[count] = '\0'; } ``` 利用fegtc來一次接收一個字元,並搭配while來接收完整字串,讀到'\n'及輸入中止。 - 處理input字串成為一個個token ```clike= char *parse_input(char input[]){ int k = 0; char *parse_arr = malloc(500*sizeof(char)); for (int i = 0; i < strlen(input); i++){ if (input[i] != '>' && input[i] != '<' && input[i] != '|'){ parse_arr[k++] = input[i]; } else{ parse_arr[k++] = ' '; parse_arr[k++] = input[i]; parse_arr[k++] = ' '; } } parse_arr[k-1] = '\0'; return parse_arr; } ``` 逐一將token放入parse_arr中,並以空格隔開每個token,關鍵字元的前後也需要是空格才可判斷。最後將字串末端設為NULL。 - 執行pipe ```clike= void run_pipe(char *command[]){ int fd[2]; pid_t p1; if (pipe(fd) < 0){ printf("Pipe Error\n"); return; } p1 = fork(); if (p1 < 0){ printf("Fork Error\n"); return; } if (p1 == 0){ close(fd[0]); dup2(fd[1], 1); // 1 represent STDOUT_FILENO which is an integer file descriper. close(fd[1]); if (execvp(command[0], command) < 0){ //execuse command and the stdout send to pipe fd[1] printf("command1 run error....\n"); exit(0); } } else{ close(fd[1]); dup2(fd[0], 0); // 0 represent STDIN_FILENO // receive fd[1] and take it as input to parent process close(fd[0]); } wait(NULL); } ``` 串建pipe,fd[1]為寫入端,fd[0]為讀端,第19行將指令執行後寫入fd[1]管道,寫完close後作為fd[0]的input到父程序接收。 - 執行Redireciton ```clike= void red_from_file(char *file_name){ int in = open(file_name, O_RDONLY); dup2(in, 0); //0 is stdin value close(in); } void red_to_file(char *file_name){ int out = open(file_name, O_WRONLY | O_TRUNC | O_CREAT, 0600); dup2(out, 1); //1 stdout value close(out); } ``` 做Redirection,遇到'<'字元,呼叫red_from_file function,打開其後所接的file後將其開啟並用dup2()讀入(複製到)standard input。反之則寫到檔案中。 - 執行基本內部指令 ```clike= if (strcmp(command[0], "cd") == 0){ chdir(command[1]); } else if (strcmp(command[0], "help") == 0){ printf("GNU bash, version 5.0.17(1)-release (x86_64-pc-linux-gnu)\n"); printf("These shell commands are defined internally. Type `help' to see this list.\n"); printf("cd [-L|[-P [-e]] [-@]] [dir].\n"); printf("pwd [-LP].\n"); printf("echo [-neE] [arg ...] .\n\n"); printf("exit [n].\n\n"); } ``` 我只寫最基本的cd和help功能,而exit功能我是寫在mian裡面,其中cd的功能只要透過chdir即可,而help我就印出一些簡單的指令提示。 ## (3) 函式功能說明 1. fork() : 能夠從父程序中複製出完全一樣的程序,此程序稱為子程序。父程序與子程序有各自獨立的memory space,但變數中指向的外部檔案人為同一份,此外,子程序並不會繼承父程序原有的lock、timer、及signal處理。子程序終止後會回傳SIGCHLD的信號給父程序,父程序可以透過此訊號得知子程序的中止狀態。而若父程序在子程序完成前中止則子程序會成為orphan process;若子程序完成後父程序卻沒收到SIGCHLD則子程序會成為zombie process。 2. vfork() : 與fork()最大差別在於能夠共享資料片段,且保證子程序先執行,在呼叫 exec 或exit 之前與父程序資料是共享的,在它呼叫exec或exit之後父程序才可能被排程執行。 3. wait() : 會暫時停止目前程序的執行, 直到有訊號來到或子程序結束。如果在呼叫wait()時子程序已經結束,則wait()會立即返回子程序結束狀態值。子程序的結束狀態值會由引數status返回,如果不在意結束狀態值,則引數status 可以設成NULL。 4. strtok(): 第一個參數放待切割字串,第二則放切割字符。切割完第一次後要繼續切則把第一個參數改為NULL,因為每次切完會將切割字符改為'\0'。 5. fgetc() : 用於單個字符寫入,返回值為讀取的字符的unsigned char並轉為int 或 EOF文件結束。 6. excvp() : 用來執行執行檔,第一個參數是即將被執行的執行檔名字,要的是const char*,而第二個是傳給file作為其引數,且其結尾必須是NULL。有error的話回傳-1,沒有error則不會回傳任何值。 7. pipe() : 用來做linux IPC,回傳值為0則建立成功,-1則建立失敗。將一端的標準輸出導入另一端作為標準輸入。其參數為接收整數值陣列,並建立出一組雙向管道,假如此陣列為pfd[2],那pfd[0] (讀的端口)進入的東西會從pfd[1] (寫的端口)出來,反之亦然。pipe的write end傳完畢後,會傳送EOF給read end。若pipe的read end接收到EOF,就會關閉此read end。 8. dup2() : int dup2(int oldfd, int newfd),通過oldfd複製出一個新的文件描述符newfd,如果成功,newfd 和函數返回值是同一個返回值,最終 oldfd 和新的文件描述符 newfd 都指向同一個文件。其中newfd可以指定一個合法數字(0123..),如果指定的數字已被占用,函數會自動關閉 close() 斷開這個數字和某個文件的關聯,再來使用這個合法數字。若成功執行返回newfd,反之為-1。與舊的文件都是指向同一個file結構體。 (STDIN_FILENO,Standard input value, stdin. Its value is 0.) (STDOUT_FILENO,Standard output value, stdout. Its value is 1.) ###### tags: `OS Homework & Learning`