[minishell] 셸 구현하기

JH Bang·2022년 11월 16일
0

42 Seoul

목록 보기
7/9
post-thumbnail

미니셸은 bash를 참고해 간단한 셸을 구현해보는 과제이다. 팀원과 함께 짝코딩 방식으로 진행했다. 파싱처리를 한 뒤 파이프와 함께 && || 연산자, 빌트인 명령어, 히어독을 포함한 리다이렉션 등을 구현했다.

주요 개념

커널

CPU, RAM, 디바이스 등 컴퓨터 자원을 할당하고 관리하는 핵심 소프트웨어로 사용자는 셸을 통해 커널에 접근한다. (커널 모드/사용자 모드)
커널은 모든 프로세스를 제어하며, 모든 상황을 알고 있음. 반면 프로세스는 다른 프로세스와 격리돼 있고, 커널에 의한 시그널의 전달과 IPC에 따라 다시 커널에 요청하여 처리하게 된다.

커널의 주요 기능

💡프로세스 관리

  • 어떤 프로세스가 CPU의 사용을 허가받았는지 파악한다.
  • 프로세스마다 PCB(process control block)를 할당하여 프로세스 관련 정보를 관리한다. 이는 CPU 자원을 대기할 동안 레지스터 값이나 프로그램 카운터 등 context를 기억하기 위한 것이다.
  • 커널은 각 프로세스의 메모리를 분리시키고, 실행되는 프로세스의 일부만 메모리에 올려 자원을 효율적으로 사용하게 만든다. 또 끝난 프로세스에서는 자원을 회수한다.

    참고로 리눅스는 preemptive multitasking OS이다. 선점이란 프로세스의 CPU 사용권이 프로세스가 아닌 커널에 의해 결정된다는 뜻이고, 멀티태스킹은 메모리에 여러 프로세스가 실행될 수 있다는 뜻이다.

💡메모리 관리

  • 커널은 모든 메모리 사용을 파악한다. 모든 메모리란 특정 프로세스에 할당된 메모리, 프로세스 간 공유할 수 있는 메모리, 할당되지 않은 메모리 등을 포함한다.

💡디바이스 접근

  • 키보드, 마우스, 모니터, 스피커, 외장메모리 등을 파일로 추상화하여 프로세스에 대한 접근을 가능하게 만든다.

💡네트워크 통신

  • 네트워크 전송 단위인 패킷을 목적지로 라우팅해 전송하거나 받아들인다.

💡시스템 콜

  • 시스템 콜이란 사용자 프로세스 단독으로는 할 수 없는 특정한 작업을 수행하는 기능이다.
  • 셸에서 ls 입력 시 셸은 시스템콜 함수인 fork()를 호출하여 자식 프로세스를 만들고, 다시 시스템콜 함수인 execv()를 호출하면, execv() 시스템 콜은 명령어 바이너리 파일을 찾아서 ls 프로그램을 실행하게 된다.
    참고로 모든 시스템콜은 atomic하게 실행된다. atomic하다는 것은 시스템콜의 모든 단계가 다른 프로세스나 스레드에 의해 중단되지 않는 것이다.

부팅 시 커널이 하는 일
시스템을 부팅하면 커널은 /sbin/init 프로그램을 실행한다.
init은 모든 부모 프로세스가 되는데, init의 pid = 1이며, sudo로도 종료가 되지 않는다. init은 시스템이 종료될 때만 종료되고, 여러 프로세스를 fork()로 만들고 감시하는 역할을 하는 프로세스다. 각 프로세스는 sterlimit() 시스템콜을 통해 자원의 상한선을 지정받게 된다.
참고로 pid = 0인 프로세스는 수퍼유저 프로세스로 모든 실행 권한을 갖는다.

리눅스 커널은 pid를 32767(0x7fff)(PID_MAX=32768,0x8000)까지 할당한다. 새 프로세스가 생기면 커널은 pid를 300부터 다시 할당하는데, 이는 0-299 범위의 pid들은 시스템 프로세스와 데몬들이 차지하고 있는 경우가 대다수이므로 사용 가능한 pid를 찾기 위한 시간을 절약하기 위한 것이다. 64비트 플랫폼에서는 PID_MAX가 2^22개까지 설정 가능하다.

셸은 사용자의 명령을 읽고 해당하는 프로그램을 커널을 통해 실행하는 프로그램이다.

셸은 기본적으로 3가지 동작을 한다.

  • 환경변수 읽기/초기화
  • 명령어 파싱
  • 프로세스 종료

환경변수 읽기 단계에서 셸은 설정 파일을 읽고 실행한다.
파싱 단계에서는 STDIN으로부터 읽어들인 커맨드 라인을 해석하고 STDOUT으로 실행한다.
프로세스 종료 단계에서는 커맨드 프로그램을 실행한 후 메모리 자원을 반환한다.

IPC 방법

IPC는 프로세스간 통신을 말하며 유닉스는 다양한 계열로 발전해 왔기 때문에 다양한 IPC 표준이 존재한다.

  • 시그널 = 이벤트 발생
  • 파이프 = 프로세스 간 데이터 전송
  • 소켓 = 같은 컴퓨터 또는 네트워크로 연결된 다른 컴퓨터에 있는 프로세스로 데이터를 보낼 때 사용
  • 파일 잠금 = 다른 프로세스가 파일을 읽거나 갱신할 수 없도록 잠금
  • 메시지 큐 = 프로세스 간 데이터 패킷 교환
  • 세마포어 = 프로세스의 동작 동기화
  • 공유 메모리 = 둘 이상의 프로세스가 메모리의 일부를 공유

세션

프로세스의 묶음을 세션이라고 하는데, 세션 내 프로세스는 같은 세션 ID 를 가진다.
만약 한 셸에서 프로세스를 생성할 경우 셸은 세션 리더가 된다. 세션 리더 프로세스가 처음으로 터미널 디바이스를 열면 제어 터미널이 할당되고, 제어 터미널을 통해 세션을 관리한다.

fd table

커널은 각 프로세스별로 open fd table을 관리한다. 또한 커널은 전체 시스템에 대한 open fd table을 관리하며, 파일 시스템 i-node talbe을 같이 관리한다.

파일 시스템

/ : 루트 디렉터리

/bin : ls와 같은 기본적인 유닉스 명령어 이진파일 디렉터리

/dev : 장치 파일 디렉터리

/etc : 핵심 시스템 설정 디렉터리, 사용자 비밀번호, 부트, 네트워킹 등 설정파일. /etc/sudoers에서 sudo 권한을 설정 가능

/home : 일반 사용자들의 개인 디렉터리가 모인 디렉터리

/lib : 공유 라이브러리 파일 디렉터리, 커널이 시스템을 운영하기 위해 필요한 모듈들이 /lib/modules에 위치
**/usr/lib와의 차이점 : /lib는 공유 라이브러리들만 포함, /usr/lib는 정적 및 동적 라이브러리, 그리고 보조 파일 등을 포함

/proc : 시스템 통계 정보 디렉터리

/sys : sysfs를 적재하기 위한 디렉터리

/sbin : 관리자용 명령어를 보관하는 시스템 실행(관리) 파일 디렉터리로 루트로 실행해야 함

/tmp : 임시 파일들을 저장하는 디렉터리로 많은 프로그램들이 작업 공간으로 사용함. 커널은 주기적으로 /tmp를 정리

/usr : 루트 디렉터리 안의 하위 디렉터리 구조와 유사하며, 대부분의 리눅스 시스템과 대규모 디렉터리 계층 구조가 위치함

/usr/include : C컴파일러가 사용하는 헤더파일 보관

/usr/info : GNU 정보 매뉴얼 보관

/usr/local : user의 소프트웨어를 설치하는 디렉터리

/usr/man : man 페이지 보관

/usr/share : 과거 메모리가 부족했던 시절, 다른 종류의 유닉스와 공동 작업하는 파일을 보관

/var : 변수 서브디렉터리로 프로그램이 런타임 정보를 기록. 시스템 로깅, 사용자 트래킹, 캐시 등이 있음

/boot : 커널 부트로더 파일들이 위치한 디렉터리
** 커널은 /boot/vmlinuz 또는 /vmlinuz에 위치함. 부트로더가 이 파일을 메모리로 로딩하고 시스템이 부팅하면서 가동

/media : 제거 가능한 미디어의 기본 마운트 지점

/opt : 추가적인 제3자 소프트웨어 파일 디렉터리

시그널

시그널은 프로세스에 이벤트가 발생했다는 것을 알린다. 하나의 프로세스는 다른 프로세스로 시그널을 전송할 수 있다.

이벤트 종류로는

  • 하드웨어 예외(접근 불가 메모리 참조, 잘못된 명령어 실행)
  • 사용자가 터미널로 시그널을 입력한 경우(ctrl + z, ctrl + c)
  • 소프트웨어 이벤트(fd값 생성, 윈도우 크기 조절, 타이버 만료, 자식 프로세스 종료 등)

프로세스는 시그널을 받으면 다음 동작 중 하나를 한다.

  • 시그널 무시
  • 프로세스 종료
  • 프로세스 중지

구현

허용함수

참고, 클러스터 homebrew 설치
클러스터에서는 권한 문제로 homebrew를 설치하려면 다음 명령어를 zsh에 입력하도록 한다.

curl -fsSL https://rawgit.com/kube/42homebrew/master/install.sh | zsh

readline

brew install readline으로 설치
STDIN으로부터 문자열을 입력받는 함수. 개행으로 버퍼에 담긴 문자열을 비운 뒤 코드를 진행시킨다. readline 헤더를 사용하기 위해선 stdio.h가 먼저 선언돼야 한다.

#include <stdio.h>
#include <readline/readline.h>
#include <readline/history.h>

char *readline (const char *prompt);

readline의 return값은 동적할당 돼 있으므로 사용 후 free를 해줘야 한다.
brew info readline으로 컴파일 방법을 확인할 수 있다.

gcc minishell.c -lreadline ${LDFLAGS} ${CPPFLAGS}
int main(void)
{
    char *str;
    while(1)
    {
       /* readline함수는 인자($ )를 터미널에 출력하고 라인을 입력 받음*/
        str = readline("$ ");
        if (str)
            printf("%s\n", str);
        else
            break ;
	/* add_history는 커맨드라인의 이전 입력값을 방향키를 통해 확인시킴 */
        add_history(str);
	/* readline은 힙 메모리를 사용하므로 free 시켜줘야 함 */
        free(str);
    }
    return(0);
}

add_history(str) 을 하면 히스토리 구조체에 str 을 추가해준다

typedef struct _hist_entry
{
  char *line;
  char *data;
}              HIST_ENTRY;

typedef struct _hist_state {
  HIST_ENTRY  **entries;         /* Pointer to the entries themselves. */
  int         offset;            /* The location pointer within this array. */
  int         length;            /* Number of elements within this array. */
  int         size;              /* Number of slots allocated to this array. */
  int         flags;
}               HISTORY_STATE;

tcsetattr/tcgetattr

터미널 세팅값을 설정하고 받아오는 함수다.
https://man7.org/linux/man-pages/man3/tcflush.3.html

The termios structure
Many of the functions described here have a termios_p argument
that is a pointer to a termios structure. This structure
contains at least the following members:

       tcflag_t c_iflag;      /* input modes */
       tcflag_t c_oflag;      /* output modes */
       tcflag_t c_cflag;      /* control modes */
       tcflag_t c_lflag;      /* local modes */
       cc_t     c_cc[NCCS];   /* special characters */

opendir/readdir/closedir

opendir은 현재 디렉터리를 열때 사용하는 시스템콜이며 readdir은 호출할 때마다 opendir에서 열린 파일 목록들을 하나씩 순회하게 된다. ft_printf 구현할때 가변인자를 컨트롤 하기 위한 va_arg와 비슷하게 동작한다고 보면 된다.

Bash 분석

GNU 홈페이지 참고https://www.gnu.org/software/bash/manual/html_node/Definitions.html

  • POSIX
    A family of open system standards based on Unix. Bash is primarily concerned with the Shell and Utilities portion of the POSIX 1003.1 standard.

  • blank
    A space or tab character.

  • builtin
    A command that is implemented internally by the shell itself, rather than by an executable program somewhere in the file system.

  • control operator
    A token that performs a control function. It is a newline or one of the following: ‘||’, ‘&&’, ‘&’, ‘;’, ‘;;’, ‘;&’, ‘;;&’, ‘|’, ‘|&’, ‘(’, or ‘)’.

  • exit status
    The value returned by a command to its caller. The value is restricted to eight bits, so the maximum value is 255.

  • field
    A unit of text that is the result of one of the shell expansions. After expansion, when executing a command, the resulting fields are used as the command name and arguments.

  • filename
    A string of characters used to identify a file.

  • job
    A set of processes comprising a pipeline, and any processes descended from it, that are all in the same process group.

  • job control
    A mechanism by which users can selectively stop (suspend) and restart (resume) execution of processes.

  • metacharacter
    A character that, when unquoted, separates words. A metacharacter is a space, tab, newline, or one of the following characters: ‘|’, ‘&’, ‘;’, ‘(’, ‘)’, ‘<’, or ‘>’.

  • name
    A word consisting solely of letters, numbers, and underscores, and beginning with a letter or underscore. Names are used as shell variable and function names. Also referred to as an identifier.

  • operator
    A control operator or a redirection operator. See Redirections, for a list of redirection operators. Operators contain at least one unquoted metacharacter.

  • process group
    A collection of related processes each having the same process group ID.

  • process group ID
    A unique identifier that represents a process group during its lifetime.

  • reserved word
    A word that has a special meaning to the shell. Most reserved words introduce shell flow control constructs, such as for and while.

  • return status
    A synonym for exit status.

  • signal
    A mechanism by which a process may be notified by the kernel of an event occurring in the system.

  • special builtin
    A shell builtin command that has been classified as special by the POSIX standard.

  • token
    A sequence of characters considered a single unit by the shell. It is either a word or an operator.

  • word
    A sequence of characters treated as a unit by the shell. Words may not include unquoted metacharacters.

                     "&&"
                 ___/    \______
               /                \
          __CMD__               __"||"___
        /        \            /          \
	CMD_LIST   REDIR_LIST   CMD        __"|"__
                                      /        \
                                    CMD        CMD

셸 구현 순서

  1. readline으로 입력받은(만약 백슬래시()가 있으면 하나의 라인으로 합쳐준다.)string에 semicolon(;)이 있는지 찾고 세미콜론에 맞춰 line들로 분리한다.
  2. 각 line을 token list로 만들어 준다. quotes나 parenthesis가 돼 있으면 하나의 token으로 본다.
  3. 토큰 노드는 트리의 틀을 만들 CMD, PIPE(|), AND/OR(&& ||) 타입을 갖는다.
  4. CMD인 트리노드를 CMD_LIST와 REDIR_LIST의 두개의 자식노드로 분기한다.
  5. 명령어 노드를 만나면 실행해주는 트리실행 함수를 만들되, 리다이렉션을 먼저 하도록 함
  6. 단 here_doc의 경우 모든 명령어 노드에서 그 입력값을 다 받아온 뒤에야 실행하므로, 먼저 각 명령어 노드별로 here_doc 파일을 각각 만들어 준뒤 입력값을 받아 놓고, 추후 실행 과정에서 리다이렉션을 설정해주도록 함
  7. 괄호 노드는 subshell이라 하여 재귀적으로 실행하도록 함

참고자료
https://man7.org

profile
의지와 행동

0개의 댓글