# 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`