UNIX/Linux 쉘은 프로그래밍 언어이다
쉘 스크립트란 무엇인가? 쉘은 스크립트를 어떻게 프로세스 하는가?
쉘 컨트롤 구조는 어떻게 작동하는가?
쉘 변수: 왜 사용하고 어떻게 사용하는가?
environment란 무엇인가? 어떻게 작동하는가?
시스템 콜과 함수
- exit
- getenv
명령어들
- env
자, 이제 쉘의 기능중 프로그래밍이 가능한 쉘을 구현한 프로그램을 살펴보자
기본적인 기능들과 구조를 살펴볼까?
command를 입력받고, fork를 통해 프로세스 분리, execvp로 실행, 그리고 다시 get command로 루프하게 된다! 이 점에 유의하며 한번 프로그램을 보자
이번 예제 프로그램은 코드가 4개다!
헤더파일, 그리고 프로그램 3개이다
하나씩 천천히 살펴보자
#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**형 매개변수 받음
헤더파일을 만들어보는건 처음이다. 함수가 프로토타입으로만 정의되어도 괜찮은걸까?
알아본 결과 큰 상관은없다고한다! 오히려 일반적인 사용법이라고 한다. 아마 뒤의 코드에서 함수들이 정의되어서 사용될것으로 예상된다
#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함수는 에러 메세지를 출력하는 함수이다일단 이 정도만 알아두자, 헤더파일에 프로토타입으로 정의된 다른 함수는 다른 코드에서 정의 될것이다!
#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함수를 사용하여 프로세스를 생성하고 실행시키게 해주는 부분!
마지막 코드는 좀 길다
인내를 갖고 반드시 주석을 하나씩 잘 읽어보자!
#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은 좀 더 개선할 점들이 많다!
이 정도만 기억하면서 다음 진도를 나가보자!