쉘 구현하기

--·2023년 1월 3일
0

쉘 구현을 해보았습니다.

redirection, background processing 포함

코드

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdbool.h>
#include <errno.h>

extern char **environ;// 환경변수

void help() { // 명령어를 help해주는 함수

   printf("exit\t: exit this shell\n");
   printf("help\t: show this help\n");
   printf("&\t: background processing\n");
   printf("cat\t: show this file\n");
	printf("rm\t: remove this file\n");
	printf("date\t: show present time\n");
	printf("ps\t: show process\n");
	
}

int parsing(char* buf, char* delims, char* tokens[]) {
	char* token; // strtok로 받은 값을 저장할 변수
	int cnt = 0; // 토큰의 개수를 세어줌

   token = strtok(buf, delims); // 토큰을 받습니다.

   while(token != NULL) { // 토큰이 마지막일 때 까지
      tokens[cnt] = token; // tokens 배열에 토큰별로 넣어줍니다.
      cnt++;   // 다음 index로 이동
      token = strtok(NULL, delims); // 다음 토큰을 받습니다.
   }
   tokens[cnt] = NULL; // 맨 마지막에 NULL값을 넣어줍니다.
   return cnt; // 토큰의 개수 리턴
}

bool exitFucn(){ // myShell 프로그램을 종료해주는 함수
	return false; // false를 리턴하면 프로그램종료
}

bool play(char* input) {
	 int  idx;
   int flag = 0;
   int i;
   int fd;
   int re = 0;
   int bg = 0;
   int re_check = 0;
   int bg_check = 0;
   int token_count;
   pid_t child;
   char delims[] = " \r\t\n";
   char* tokens[128];
   
   token_count = parsing(input, delims, tokens); // 파싱을 수행하여 파싱을하고 토큰의 개수를 리턴 받습니다.

   if(token_count == 0) // 아무것도 없으면
      return true; // return true
   if(strcmp(tokens[0], "exit") == 0) // 입력 값이 exitFucn()를 호출 false 리턴하며 프로그램 종료
   {return exitFucn();} // false 리턴합니다.
	if(strcmp(tokens[0], "help") == 0){ // help를 입력하면
      help(); // help 함수 호출
      return true;}
	if(strcmp(tokens[0], "clear") == 0){system("clear");} // clear해줍니다.
	

   for(i = 0; i < token_count; i++) { // 토큰의 개수만큼 반복합니다.
      if(strcmp(tokens[i],">") == 0) { // 토큰에 > 가 있으면
         re = i; // 인덱스를 저장해줍니다.
         re_check = 1; // rediretion이라고 확인합니다.
		 flag=O_WRONLY | O_CREAT | O_TRUNC; // O_TRUNC로 파일을 덮어씁니다.
         break; // for문 종료
      }
	   if(strcmp(tokens[i],">>") == 0) { // 토큰에 >> 가 있으면
         re = i; // 인덱스를 저장해줍니다.
         re_check = 1; // rediretion이라고 확인합니다.
		 flag = O_WRONLY | O_CREAT | O_APPEND; // O_APPEND로 파일 뒤에 추가해줍니다.
         break; // for문 종료
      }
   
      if(strcmp(tokens[i],"&") == 0) { // 토큰에 &가 있으면
         bg = i; // 인덱스를 저장해줍니다.
         bg_check = 1; // background processing이라고 확인합니다.
         break; //for문 종료
      }

   }   

   child = fork(); // 자식프로세스 생성
   if(child < 0) { //fork error 발생
		printf("fork error\n");
	   	return false; // 에러가 발생하였으므로 프로그램을 종료합니다.
   }
   else if(child == 0) { // 자식 프로세스 정상적으로 실행
     	if(bg_check==1) { // 백그라운드 프로세싱이면
         tokens[bg] = '\0';
        }
   
      if(re_check == 1) { // 리다이렉션이면
         fd = open(tokens[re + 1], flag, 0664); // 리다이렉션이면 O_APPEND로 아니면 O_TRUNC로 파일을 엽니다.
		if(fd < 0){ // 파일 열기 오류
			printf("Can't open file"); // 파일을 읽을수 없으면
			exit(-1); // 프로그램 종료
		}
         close(STDOUT_FILENO); // 모니터에 있는 값을 닫음
         dup2(fd, STDOUT_FILENO); // Standard output을 위에서 create한 fd로 가도록 설정
		  // dup2 실행 후, 자식의 모든 출력은 fd로 저장됨
         tokens[re] = '\0';
      	}
	   execvp(tokens[0],tokens); // 나머지 명령어를 수행합니다.
	   printf("execvp error"); // 이 메시지가 보이면 실행 문제가 있는것입니다.
	   return false;
	   
}
   else if(bg_check == false) { // 백그라운드 프로세싱이 아니면
      wait(NULL); // 자식 프로세스가 끝날때까지 기다립니다.
   } // 백그라운드 프로세싱이면 부모는 자식을 기다리지 않고 독립적으로 수행합니다.
   
   return true;
}   

int main() {
   char input[100]; // 명령어를 저장할 공간
   while(1) { // 사용자가 exit를 입력할 때까지 무한 반복합니다.
      printf("%s $ ",get_current_dir_name()); // 명령어 입력 전에 출력합니다.
      fgets(input, sizeof(input) - 1, stdin); // 사용자에게 입력을 받습니다.
      if(play(input) == false) // pasing함수가 false를 리턴하면 break; 하므로 프로그램 종료
         break;
   }
   
   return 0;
}

쉘 실행결과

  1. ls : ls를 이용하여 myShell.c 파일을 확인할 수 있습니다.
  2. gcc -o myShell myShell.c : gcc를 이용하여 c파일을 실행가능한 파일로 만듭니다.
  3. ./myShell: myShell에 들어갑니다.
  4. gcc -o Hello Hello.c : gcc를 사용하여 myShell안에서 Hello.c를 컴파일합니다.
  5. ls : ls로 Hello 실행가능한 파일이 만들어진 것을 확인할 수 있습니다.
  6. .Hello : ./Hello로 파일을 실행하니 Hello World가 출력되는 것을 알 수 있습니다.
  7. help : help 명령어를 이용하여 사용가능한 명령어를 확인합니다.
  8. date : date 명령어를 사용하면 시간을 출력해줍니다.
  9. cat : cat을 사용하면 파일의 내용을 출력해줍니다.

redirection 실행하기


1. cat : alphabet.txt 파일의 내용을 출력하면 abcdefgh인 것을 알 수 있습니다.
2. date : 현재 날짜, 년도, 시간을 출력해줍니다.
3. >> : redirection을 이용하여 date의 출력 내용이 alphabet.txt 내용 뒤에 추가 시킵니다.
4. cat을 이용하여 확인해보면 abcdefgh 뒤에 date의 출력 결과물이 추가되었습니다.
5. > :을 이용하여 date의 출력물을 date.txt에 덮어씌웁니다. 6. cat을 이용하여 date.txt를 확인해보면 date의 출력물이 들어가 있습니다.

redirection 구현방법

  1. > 일때 flag=O_WRONLY | O_CREAT | O_TRUNC
    >> 일 때 flag = O_WRONLY | O_CREAT | O_APPEND
    값을 flag에 저장해준 후 나중에 파일을 열 때 flag를 이용하여 파일을 open하였습니다.
  2. close(STDOUT_FILENO); // 모니터에 있는 값을 닫음
  3. dup2(fd, STDOUT_FILENO); // Standard output을 위에서 create한 fd로 가도록 설정합니다.
  4. 이후에는 자식의 모든 출력은 fd로 저장되어 나타나게 됩니다.

Background Processing

프로세스 종류

  • 프로세스 종류는 foreground 프로세스, background 프로세스 두가지 종류가 있다. foreground 프로세스는 사용자가 입력한 명령이 실행 되어 결과가 출력될 때까지 기다리는 방식이다 반면에 background 프로세스는 명령을 실행하면 명령의 처리가 끝나는 것과 관계 없이 곧바로 프롬프트가 출력되어 사용자가 다른 작업을 할 수 있는 방식이다. 그래서 사용자가 어떠한 큰 용량을 다운로드할 때, 다운 로드가 끝날 때 까지 기다려야 하는 foreground 프로세스 방식을 사용하는 것 보단 다운로드 중 다른 작업을 할 수 있는 background 프로세스 방식을 사용하는 것이 더 좋은 방법이다.
  1. ps를 이용하여 현재 실행중인 프로그램을 확인합니다.
  2. ./myShell을 이용하여 myShell프로그램을 실행시킨 후 ps로 확인해보면 myShell 프로세스가 추가되었음을 확인할 수 있습니다.
  3. 이후./myShell을 한번더 하면 myShell 프로세스가 PID가 다른 두 개가 생깁니다. 이것은 자식이 자식을 낳은 것이라고 해석할 수 있습니다.
  4. ./myShell & : background로 ./myShell을 실행해라입니다. background로 실행하면 부모는 자식의 실행이 끝날때까지 기다리는 것이 아니라 독립적으로 자기 할 일만 합니다. 위의 사진을 보면 ./myShell &을 실행하자마자 부모는 바로 /workspace/sys32190192 를 출력합니다. background 프로세싱이 아니면 자식이 수행할 때까지 기다린 후에 /workspace/sys32190192 를 출력해야 하지만 background 프로세싱은 자식과 독립적이므로 자식의 수행을 기다리지 않고 자기 할 일 /workspace/sys32190192를 출력해 버리고 다음 입력을 기다립니다.
  5. ps로 확인해보면 마지막 자식이 실행시킨 ./myShell &도 background로 실행이 되어서 프로세스에 추가되었습니다.
  6. myShell을 3번 실행시켰으므로 자식이 3번 생겨서 나갈 때도 exit를 3번 입력하면 원래 커멘드 창에 돌아온 것을 확인할 수 있습니다.

    Background Processing 구현 방법

    foreground 방식은 fork()로 자식을 생성 후 부모는 자식이 끝날 때까지 wait() 함수를 이용 하여 대기하지만
    background 방식은 wait() 함수를 삭제하여 fork()로 자식을 생성 후 자식이 무슨 작업을 하던 상관 없이 부모의 작업을 수행할 수 있게 합니다.

0개의 댓글