두 번째 쉘 프로그램 작성해보기!

신준우·2023년 6월 4일
0

시스템 프로그래밍

목록 보기
7/12

이전 내용 복습

이전 포스트에서 보았던 parent_code()함수를 다시 살펴보며 복기해보자

void parent_code(int childpid){		//childpid값을 매개변수로 받는 함수
		int wait_rv;		//wait_rv라는 int형 변수를 선언 이름을 보아하니 wait함수와 관련
		int child_status;		//child_status라는 int형 변수 선언, 이름을 보았을때 child process의 상태?
        int high_8, low_7, bit_7;		//high, low, bit라는 변수 선언
        
        wait_rv = wait(&child_status);		//child프로세스의 종료 상태를 wait함수를 사용해서 기다림
        printf("done waiting for %d. Wait returned: %d\n", childpid, wait_rv);
        // child process이 종료됐음을 출력, wait함수의 반환값 출력
        
        
        high_8 = child_status >> 8;		// child status의 변수값을 8비트 이동하여 high_8에 저장
        
        low_7 = child_status & 0x7f;	//child status와 0x7f(01111111)를 비트단위의 AND연산을 하여 저장
        								// child프로세스가 종료될때 반환하는 상태 정보 중 하위 7비트인 
                                        // 시그널 추출
        
        bit_7 = child_status & 0x80;	//child status와 0x80(10000000)를 비트단위의 AND연산을 하여 저장
        								// 7번째 비트인 코어 덤프 여부 추출
        printf("status: exit = %d, sig = %d, core = %d\n", high_8, low_7, bit_7);
        //이제 결과값 출력~!
}

일단 child status, 즉 child프로세스가 어떻게 종료됐는지에 대한 정보가 어떻게 저장되는지 살펴보아야 한다


다음과 같이,

  • exit value는 8bits
  • core dump flag 1bits
  • signal number 7bits

이렇게 세 구역으로 비트를 나누어 갖는다. 그래서 각 출력값이 0x7f라던지, 0x80으로 표현이 된것이다

이제 본격적으로 더 발전된 쉘을 만들어보자!

쉘은 어떻게 프로그램을 실행시키는가?

  • 쉘은 새로운 프로세스를 생성하기 위해 fork()함수를 사용한다
  • 그 후, 새로운 프로세스에서 프로그램을 실행시키기 위해 exec()함수를 사용한다
  • 그 후, wait() 함수를 사용하여 새로운 프로세스가 명령을 실행하는 동안 대기하고, 종료되었을 때의 상태에 대한 알림을 받는다

그림으로 전반적인 과정을 확인해보자!

psh2.c

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

#define MAXARGS 20
#define ARGLEN 100

void execute(char**);
char* makestring();

int main(int argc, char* argv[]){
	char	*arglist[MAXARGS + 1];
    int		numargs;
    char	argbuf[ARGLEN];
    
    numargs = 0;
    while(numargs < MAXARGS){
    		printf("Arg[%d]?", numargs);
            if(fgest(argbuf, ARGLEN, stdin) && *argbuf != '\n'){
            		arglist[numargs++] = makestring(argbuf);
            }
            else{
            		if(numargs > 0){
                    		arglist[numargs] = NULL;
                            execute(arglist);
                            numargs = 0;
                    }
            }
    }
    return 0;
}

void execute(char *arglist[]){
		int pid, exitstatus;
        pid = fork();
        switch(pid){
        		case -1:
                		perror("fork failed.");
                        exit(1);
             	case 0:
                		execvp(arglist[0], arglist);
                        perror("execvp failed.");
                        exit(1);
                default:
                		while(wait(&exitstatus) != pid);
                        printf("child exited with status %d, %d\n", exitstatus>>8, exitstatus&0377);
                    }
}

char* makestring(char *buf){
		char	*cp;
        buf[strlen(buf)-1] = '\0';
        cp = malloc(strlen(buf)+1);
        if(cp == NULL){
        		fprintf(stderr, "no memory\n");
                exit(1);
        }
        strcpy(cp, buf);
        return cp;
}

어우 슬슬 어려워진다

자, 그래도 메인함수부터 천천히 살펴보자

main 함수

int main(int argc, char* argv[]){
	char	*arglist[MAXARGS + 1];		//명령행 인수들을 저장하는 문자열 포인터 배열
    int		numargs;		// 현재까지 입력된 인수의 개수 저장
    char	argbuf[ARGLEN];		// 사용자로부터 입력받은 인수를 임시로 저장하는 문자열 버퍼
    
    numargs = 0;		// numargs 변수 초기화
    while(numargs < MAXARGS){		//numargs값이 최대 인수 개수(MAXARGS)보다 작은동안 반복
    		printf("Arg[%d]?", numargs);		//사용자에게 현재 인수의 위치를 묻고 입력을 요청하는 문장
            if(fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n'){	
            //사용자로부터 인수를 입력받는다, 입력이 성공하고? 입력된 인수가 개행문자, 즉 엔터가 아닌 경우를 체킹
            		arglist[numargs++] = makestring(argbuf);	
                    // 입력된 인수를 문자열로 만들어서 arglist에 배열. numargs값 증가
            }
            else{ // 입력된 인수가 없는경우!
            		if(numargs > 0){		//numargs가 양수 == 입력된 인수가 있을경우
                    		arglist[numargs] = NULL;		//arglist 배열의 마지막 요소를 NULL로 설정
                            execute(arglist);		//execute함수를 사용
                            numargs = 0;		//numargs초기화, 다음 인수 받을준비
                    }
            }
    }
    return 0;		//프로그램 종료
}

사용자로부터 인수를 입력받는다, 그리고 입력된 인수를 실행하는 간단한 인터페이스를 제공한다.
입력된 인수는 arglist 배열에 저장, execute 함수를 통해 실행된다

  • 결국 우리가 구현하고자 하는건 '쉘(shell)'이다!!
  • 기능을 잘 생각해보자, 어떤 기능이 필요할까?
  1. fork를 통해 새로운 프로세스를 만들 수 있어야한다
  2. 프로세스에서 프로그램을 실행시키기 위해 exec함수를 사용
  3. wait() 함수를 사용하여 새로운 프로세스가 명령을 실행하는 동안 대기하고, 종료되었을 때의 상태에 대한 알림을 받는다
  4. 사용자에게 어떤 명령어 및 프로그램을 실행할지 입력받아야한다!

while문은 사용자로부터 명령행 인수를 계속해서 받을 수 있도록 적용된것!

execute함수

void execute(char *arglist[]){		//arglist라는 포인터배열을 매개변수로 사용
		int pid, exitstatus;		//int형 변수인 pid, exitstatus 선언
        pid = fork();		//fork를 통해 현재 실행중인 프로세스 복제, 즉 자식프로세스를 생성
        switch(pid){		//fork의 반환값은 3가지
        					
        		case -1:	//오류난 경우, -1을 반환함
                		perror("fork failed.");		//오류 메세지 출력
                        exit(1);		//프로그램 종료
             	case 0:		//child 프로세스를 복사하는 경우 0을 반환함
                		execvp(arglist[0], arglist);	//그럼 arglist배열에 저장된 명령어 실행
                        perror("execvp failed.");		//execvp가 실패하면 에러메세지 출력
                        exit(1);
                default:	//fork값이 -1과 0이 아니라면, parent 프로세스를 fork하려는 경우이다
                		while(wait(&exitstatus) != pid);		
                        //자식 프로세스의 종료를 대기 wait 함수 사용!
                     	//wait 함수를 통해서 exitstatus 변수에 종료된 프로세스값을 넣는다
                        printf("child exited with status %d, %d\n", exitstatus>>8, exitstatus&0377);
                        //자식 프로세스의 종료 상태를 출력한다 
                    }
}

좀 더 psh2.c에 대한 이해가 될것이다!
1. fork함수를 통해 프로세스를 복사해서 자식프로세스를 생성
2. execvp를 사용해서 명령어를 실행
3. 종료가 되어야 하니 wait함수를 통해서 자식프로세스의 종료를 대기, 그리고 종료 상태를 저장

이렇게 세가지 기능을 하는 함수이다

그럼 메인함수에서 입력된 인수가 없을경우, 입력받았던 인수를 기반으로 명령어 및 프로그램을 실행시키게 하는 역할을 한다고 할 수 있다

그럼 이제 마지막으로 makestring 함수를 확인해보자

makestring() 함수

char* makestring(char *buf){		//문자열 포인터 변수 buf를 매개변수로
		char	*cp;		//문자열을 저장할 포인터 변수
        buf[strlen(buf)-1] = '\0';		//buf문자열의 마지막 문자를 널문자로 대체, 문자열의 끝을 표시
        cp = malloc(strlen(buf)+1);	//buf의 길이에 따라 동적으로 메모리 할당, strlen(buf)+1은 널문자 포함
        if(cp == NULL){		// cp값, 즉 저장할 포인터 변수가 없다면?
        		fprintf(stderr, "no memory\n");		//fprintf를 통해 no memory 출력
                exit(1);		//함수 종료
        }
        strcpy(cp, buf);		//buf의 내용을 cp로 복사, 이를 통해 새로 할당, 입력된 문자열 복사
        return cp;		//복사된 문자열을 가리키는 cp를 반환
}

뭐 사실 이 함수는 별거 없다
user가 입력한 명령어 혹은 프로그램을 문자열로 입력받아 cp라는 문자열 포인터 변수에 저장하는 과정일 뿐이다

결론

이 psh2.c라는 프로그램은 간단한 쉘이다!

우리가 처음에 가졌던 의문에 대해 다시 생각해보자, 한가지 프로세스만 실행하고 종료하는 문제가 있어서

  1. fork를 통해 프로세스의 메모리를 할당해주고,

  2. wait을통해 프로세스가 종료되고 다음 프로세스를 실행시킬수 있게 했다!

profile
보안

0개의 댓글