프로그램이 가능한 쉘

신준우·2023년 6월 5일
0

시스템 프로그래밍

목록 보기
9/12

preview

  • UNIX/Linux 쉘은 프로그래밍 언어이다

  • 쉘 스크립트란 무엇인가? 쉘은 스크립트를 어떻게 프로세스 하는가?

  • 쉘 컨트롤 구조는 어떻게 작동하는가?

  • 쉘 변수: 왜 사용하고 어떻게 사용하는가?

  • environment란 무엇인가? 어떻게 작동하는가?

  • 시스템 콜과 함수
    - exit
    - getenv

  • 명령어들
    - env

쉘 프로그래밍

리눅스 쉘

  • 프로그램, 또는 스크립트를 실행한다
  • 그 자체로 이미 프로그래밍 언어이다
  • 프로그래밍 언어의 통역사로 작동한다
    - 키보드에서 입력되는 명령어들을 통역해준다
    - 쉘 스크립트에 저장된 명령어들과 시퀀스들을 통역해준다
  • 쉘 스크립트: "커맨드의 집합"
    - 커맨드의 집합을 가진 파일이다
    - "스크립트를 실행시킨다"라는 말은 결국 커맨드의 집합에서 뽑아서 실행시킨다는 의미
  • 여러 커맨드를 단 하나의 요청으로 실행시킬 수 있다

Concepts

  • 변수들 : BOOK, NAME
    - 대문자일 필요는 없다
    - $: 변수에 저장된 값을 가져오는데 사용된다
  • User input
    - read: 표준 입력에서 문자열을 읽을 수 있도록 쉘에게 지시하는 명령어이다
    • 스크립트를 상호적으로 만들어주며, (scanf와 비슷한 역할을 한다) 파일이나 파이프로부터 값을 가져올 수 있다
  • Control
    - 프로그램의 flow를 관리한다, if.. then... else.. fi, 또는 while, case, for과같은 함수들을 관리
  • Environment
    - 환경변수는 사용자가 개인화된 설정을 기록하여 변수 프로그램에 영향을 주는 것을 허용한다
    • HOME: 사용자의 홈 디렉토리 경로를 포함
    • PATH: 사용자가 등록한 경로로, 사용자 정의 및 시스템 프로그램을 편리하게 실행하기 위해 사용

smsh1

자, 이제 쉘의 기능중 프로그래밍이 가능한 쉘을 구현한 프로그램을 살펴보자

기본적인 기능들과 구조를 살펴볼까?

command를 입력받고, fork를 통해 프로세스 분리, execvp로 실행, 그리고 다시 get command로 루프하게 된다! 이 점에 유의하며 한번 프로그램을 보자

이번 예제 프로그램은 코드가 4개다!

헤더파일, 그리고 프로그램 3개이다

하나씩 천천히 살펴보자

smsh.h

#define YES 1		//YES라는 변수가 1로 매크로 정의
#define NO 0		//NO라는 변수가 0으로 매크로 정의

char	*next_cmd();		//next_cmd라는 함수 정의

char	**splitline(char*);		//splitline이라는 함수 정의, char형 포인터를 매개변수로 받는다

void	freelist(char**);		//freelist라는 함수 정의, char형 더블포인터를 매개변수로 받는다

void	*emalloc(size_t);		//emalloc이라는 함수 정의, size_t라는 변수를 매개변수로 받음

void	*erealloc(void*, size_t);	//erealloc이라는 함수 정의

int		execute(char**);		//int형 변수를 반환하는 execute라는 함수 정의, char형 더블포인터 매개변수

void	fatal(char*, char*, int);		//fatal이라는 함수, char*두개와 int형 변수 하나를 매개변수로 받는다

int		process(char**);	//process라는 함수 정의, char**형 매개변수 받음

헤더파일을 만들어보는건 처음이다. 함수가 프로토타입으로만 정의되어도 괜찮은걸까?
알아본 결과 큰 상관은없다고한다! 오히려 일반적인 사용법이라고 한다. 아마 뒤의 코드에서 함수들이 정의되어서 사용될것으로 예상된다

smsh1.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include "smsh.h"		//이건 위에서 사용자가 직접 정의한 헤더파일이다!

#define DFL_PROMPT		"> "		//DFL_PROMPT라는 변수를 >로 정의!

int main(){		//메인함수 부분이다 살펴보자
	char *cmdline, *prompt, **arglist;		//char형 변수들을 선언한
    int result;		//int형 변수를 선언
    void setup();		// setup이란 함수의 프로토타입 선언
    
    prompt = DFL_PROMPT;		//prompt라는 char형 포인터 변수에 ">"라는 문자를 넣는다
    setup();		//setup함수가 사용됐네? 이건 밑에서 정의되지 않을까
    while((cmdline = next_cmd(prompt, stdin)) != NULL){	
    //while루프를 사용하여 사용자로부터 명령어를 입력받음
    //next_cmd함수를 사용해서 cmdline에 무슨 변수를 할당해준다
    //만약 입력되는게 없으면, 즉 NULL값인경우 종료된다
    		if((arglist = splitline(cmdline)) != NULL){
            // arglist변수에 splitline을 사용해서 어떤 값을 입력받는모양
            // 만약 NULL이 전달되면, 즉 입력되는게 없다면 실행
            		result = execute(arglist);		//execute함수를 사용해서 result변수에 값 할당
                    freelist(arglist);		//freelist 함수를 사용해서 arglist에 무언가 동작...
            }
            free(cmdline);		//cmdline이라는 동적배열을 free시킴
    }
    return 0;		//프로그램 종료
}

void setup(){		//setup함수가 정의됐다! 살펴보자
		signal(SIGINT, SIG_IGN);		//SIGINT는 인터럽트 신호, Ctrl+C를 입력하면 발생
                                        //SIG_IGN을 사용해서 시그널을 무시하도록 설정
        signal(SIGQUIT, SIG_IGN);		//SIGQUIT은 종료신호, Ctrl+\를 입력하면 발생
                                        //SIG_IGN을 사용해서 시그널을 무시하도록 설정
}

void fatal(char *s1, char *s2, int n){		//fatal함수 정의
		fprintf(stderr, "Error: %s, %s\n", s1, s2);
        // fprintf함수를 사용하여 에러 메세지를 출력, stderr은 에러 출력 스트림, s1값과 s2값을 출력
        exit(n);		//exit함수를 호출하여 프로그램 종료, n은 종료상태 코드, 프로그램의 종료상태를 나타냄
}

가장 처음으로 나온 예시 코드이다
기능들을 천천히 살펴보자, 일단 setup함수와 fatal함수가 정의되어있다.
main함수가 이 코드에 있는걸로 봐서, 아무래도 이게 주요 실행 코드인 모양이다

헤더파일에서 프로토타입으로 선언된 함수들을 직접 정의했다.
setup 함수는종료시그널을 무시하도록 설정되어있다
fatal함수는 에러 메세지를 출력하는 함수이다

일단 이 정도만 알아두자, 헤더파일에 프로토타입으로 정의된 다른 함수는 다른 코드에서 정의 될것이다!

execute.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

int execute(char* argv[]){		//execute 함수의 정의, 함수는 문자열 포인터 배열을 매개변수로 받는다
		int pid;		//PID를 저장하기 위한 변수
        int child_info = -1;		//자식 프로세스의 종료 정보를 저장하기 위한 변수, 초기값을 -1로 설정
        
        if(argv[0] == NULL){		//argv[0]이 NULL인 경우, 즉 실행할 명령어가 없는 경우
        		return 0;		//0을 반환 후 종료
        }
        
        if((pid=fork()) == -1){		//fork 호출이 실패한경우, 
        		perror("fork");		//에러 메세지 출력
        }
        else if(pid == 0){		//pid값이 0인경우, 즉 자식 프로세스인 경우
        		signal(SIGINT, SIG_DFL);		//시그널 핸들러를 기본값으로 설정
                signal(SIGQUIT, SIG_DFL);		//시그널 핸들러를 기본값으로 설정
                execvp(argv[0], argv);		//excvp를 통해 입력받은 명령어를 실행
                perror("cannot execute command");		// 실행에 실패한 경우 에러 메세지를 출력
                exit(1);		//exit을 통해 종료
        }
        else{		//부모 프로세스인 경우
        		if(wait (&child_info) == -1)		//자식 프로세스의 종료를 기다리기 위해 wait함수 호출
                									//만약 오류난다면
                		perror("wait");		//오류 메세지 출력
        }
        return child_info;		//자식 프로세스의 종료 정보 반환
 }

자, 이건 뭘까
이건 사용자가 입력한 프로그램을 실행시키는 부분이다. 이전에 배웠던 fork, wait, execvp함수를 사용하여 프로세스를 생성하고 실행시키게 해주는 부분!
마지막 코드는 좀 길다
인내를 갖고 반드시 주석을 하나씩 잘 읽어보자!

splitline.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "smsh.h"

char*	next_cmd(char *prompt, FILE* fp){		//next_cmd 함수의 정의
        char*	buf;		// 입력된 문자열을 저장하기 위한 버퍼 변수
        int bufspace = 0;		//버퍼 공간의 크기를 저장하는 변수, 결국 입력이 될것이니 0으로 초기화
        int pos		 = 0;		//현재 버퍼에서 문자열을 저장할 위치를 나타내는 변수
        int c;		//입력된 문자를 저장하는 변수
        
        printf("%s", prompt);		//프롬프트 문자열을 출력한다
        while((c = getc(fp)) != EOF){		//파일로부터 문자를 읽어오는 동안 반복!
        		if(pos+1 >= bufspace){		//만약 버퍼의 공간이 부족한경우!
                		if(bufspace == 0){		//처음 할당하는 경우일땐?
                        		buf = emalloc(BUFSIZ);		//emalloc함수를 통해 초기 공간을 할당
                        }
                        else		//아닌경우?
                        		buf = erealloc(buf, bufspace+BUFSIZ);
                                // erealloc 함수를 사용해서 기존 공간에 추가로 할당한다
                        bufspace += BUFSIZ;		//버퍼 공간의 크기를 BUFSIZE만큼 증가시킨다
                }
                
                if(c=='\n') break;		//개행 문자를 만날경우! 즉 엔터키를 입력받은 경우 반복문 종료
                buf[pos++] = c;		//현재 문자를 버퍼에 저장, 버퍼의 위치를 증가시킨다
        }
        if(c == EOF && pos == 0)		//입력이 끝났는데 아무 문자도 입력되지 않은경우?
        		return NULL;		//NULL을 반환해서 종료한다
        buf[pos] = '\0';		//버퍼의 끝에 널문자를 추가하여 종료됨을 표시한다
        return buf;		//입력된 문자열이 저장된 버퍼를 반환한다
}

#define is_delim(x) ((x) == ' ' || (x) == '\t')		//일단 이건, 매크로 함수이다.
													//주어진 문자가 공백인지 탭인지 확인한다
char** splitline(char *line){		//splitline 함수의 정의, 함수는 문자열 포인터를 매개변수로 받는다
									//문자열을 분리해서 문자열 배열을 반환한다
		char	*newstr();		//새로운 문자열을 할당하여 반환해주는 함수
        char	**args;		//문자열 배열의 쵀대 크기를 저장하는 변수
        int spots = 0;		//문자열 배열의 최대 크기를 저장하는 변수
        int bufspace = 0;		//버퍼 공간 크기를 저장하는 변수
        int argnum = 0;		//문자열 배열에 저장된 문자열의 개수를 나타내는 변수
        char	*cp = line;		//문자열을 탐색하기 위한 포인터 변수
        char	*start;
        int len;		//문자열의 길이를 저장하는 변수
        
        if(line == NULL)		//line값이 NULL인 경우
        		return NULL;		// NULL을 반환하여 종료
        args = emalloc(BUFSIZ);		//문자열의 배열을 위한 메모리를 할당
        bufspace = BUFSIZ;		//버퍼 공간의 크기를 설정
        spots = BUFSIZ / sizeof(char*);		//문자열 배열의 최대 크기를 설정
        while(*cp != '\0'){		//문자열의 끝까지 반복
        		while(is_delim(*cp))		//공백 문자를 건너뛰고 다음 문자로 이동
                		cp++;
                if(*cp == '\0')		//만약, NULL문자, 즉 문자열의 끝에 도착할 경우
                		break;		//반복문 종료
                if(argnum+1 >= spots){		//문자열의 배열 공간이 부족한 경우?
                		args = erealloc(args, bufspace+BUFSIZ);		//기존 공간에 추가로 메모리 할당
                        bufspace += BUFSIZ;		//버퍼 공간의 크기를 BUFSIZ만큼 증가
                        spots += (BUFSIZ/sizeof(char*));	//문자열 배열의 최대 크기를 업데이트
                }
                start = cp;		//문자열 배열의 최대 크기를 업데이트 한다
                len = 1;		//문자열의 길이를 1로 초기화 한다
                while(*++cp != '\0' && !(is_delim(*cp)))
                // 공백 문자를 만나거나 문자열의 끝에 도달하면 반복 종료
                		len++;		//문자열의 길이를 증가
                args[argnum++] = newstr(start, len);		//newstr함수가 나왔다 나중에 알아보자
        }
        args[argnum] = NULL;		//문자열의 끝을 나타내는 NULL포인터 설정
        return args;		//분리된 문자열이 저장된 문자열 배열을 반환
}

char* newstr(char *s, int l){		//문자열과 문자열의 길이를 매개변수로 받는 newstr함수
		char		*rv = emalloc(l+1);		//길이ㅣ+1의 메모리를 할당,새로운 문자를 새로운 문자열 rv에 복사
        
        rv[l] = '\0';		//새로운 문자열의 끝에 NULL문자 추가
        strncpy(rv, s, l);		//문자열을 copy, 즉 기존 문자열 s에서 길이 l까지의 문자를 rv에 복사
        return rv;		//새로운 문자열 포인터 rv반환
}

void freelist(char **list){
	char **cp = list;
	while(*cp){
		free(*cp++);
	}
	free(list);
}

void* emalloc(size_t n){		//emalloc함수의 정의. 함수는 크기 n의 메모리 할당
		void *rv = (void*)malloc(n);		//rv라는 배열에 n크기의 배열을 생성
        if(rv == NULL){		//rv라는 배열에 어떤 값도 저장이 안 되어있을경우
        		fatal("out of memory", "", 1);		//오류 메세지 출력
        }
        return rv;		//rv라는 배열 반환
}

void* erealloc(void *p, size_t n){		//p와 n을 매개변수로 가지는 erealloc
		void*	rv = realloc(p,n);		//realloc을 통해 이전 메모리 블럭의 크기를 n으로 조정
        if(rv == NULL){				//rv가 NULL인 경우, 즉 메모리 재할당에 실패한 경우
        		fatal("realloc() failed", "", 1);		//메모리 오류 메세지 출력
        }
        return rv;		//포인터 rv를 반환
}

어우 길다!!
이거 뭐냐 이건 나머지 함수들을 정의해주고있다.. 각자 메모리 크기들을 정의해주는 함수이다..
이건 메인함수를 다시 읽어보면서 주석들을 함께 읽어보면, 이해가 쉬울것이다!

실행화면

저렇게 gcc를 통해 세가지 코드를 한번에 실행하고, smsh1을 실행시키면
이렇게! 명령어가 실행되어 결과를 출력하는걸 알 수 있다

참고사항

smsh1은 좀 더 개선할 점들이 많다!

  • 여러가지 명령어를 한줄에 적을수 있어야 한다
    - 오리지널 쉘은 세미콜론(;)을 통해서 여러개의 명령어를 실행할 수 있다
    - 유저들이 한 줄에 여러개의 명령어를 실행 할 수 있도록 개선해보자
  • 백그라운드 처리
    - 오리지널 쉘은 사용자가 프로세스를 백그라운드에서 실행할 수 있다
    - 사용자가 명령어를 '&'로 끝내면, 프로세스를 백그라운드에서 실행할 수 있다
    - 백그라운드 실행이란? 다른 명령어를 사용하면서 해당 프로세스는 계속해서 실행되는 상태임을 의미
    - 다른 작업을 수행하면서 백그라운드에서 실행중인 프로세스를 동시에 관리 가능
  • exit 명령어
    - exit명령어를 통해 쉘을 종료시킬수 있는 기능을 추가해야한다!

이 정도만 기억하면서 다음 진도를 나가보자!

profile
보안

0개의 댓글