본문 바로가기

Learning/└◆Shell Scripts

[ShellScripting] 본 셸(Bourne shell)의 동작

________________________________________________________________________________

 

 

1 부 본쉘

 

1 장 유닉스 쉘

2 장 본쉘의 개요

3 장 본쉘의 동작

4 장 유닉스 명령어

5 장 본셀의 특징

6 장 본셀 프로그래밍

 

________________________________________________________________________________

 

 

 

3 장 본 쉘의 동작

---------------------------

1. 시스템 시동과 로그인 쉘

2. 명령어 종류

3. 프로세스와 쉘

4. 리다이렉션

5. 파이프

---------------------------

 

 

이번장에서는 본쉘의 동작을 이해하기 위해 사용자 로그인시의 로그인 과정, 쉘에서 해석되는 명령어의 종류, 프로세스와 쉘의 동작 방법 및 파이프, 리다이렉션등의 동작등에 알아보자.

 

 

 

 

 

1

시스템 시동과 로그인 쉘

 

 

 

시스템에 전원이 들어오면, 시스템은 POST 과정을 수행하고, 디스크쪽에 위치하는 부팅프로그램을 실행하게 되고, 부팅 프로그램을 커널을 실행하게 되고, 커널은 init 프로세스를 실행하게 되고, init 프로세스가 구동이 되면 시스템을 초기화 하기 위해 /etc/inittab 파일을 읽어 들여 초기화 과정을 시작한다. init 프로세스는 시스템을 초기화 한후에 표준 입력(stdin), 표준 출력(stdout), 표준 에러(stderr) 등 터미널을 시작하기 위해 필요한 제반 프로세스를 시작 한다. 일반적으로 표준 입력은 키보드를 통해 이루어지고, 표준 출력과 표준 에러는 화면에 나타난다. 여기까지 진행되면 로그인 프롬프트가 터미널에 나타난다.

 

로그인 이름과 비밀번호를 입력하면 /bin/login 프로그램이 /etc/passwd 파일의 첫 번째 필드와 비교해서 입력값과 동일한지 확인한다. 입력된 사용자 이름이 /etc/passwd 파일에 있는지 검사하여 확인되면, 다음 단계로 비밀번호가 실제로 정확한지 암호화 프로그램을 이용하여 검사하는 것이다. 비밀번호 확인이 완료 되면 /bin/login 프로그램은 쉘이 사용할 변수들에 대해 초기 환경 설정을 수행한다. HOME, SHELL, USER, LOGNAME 변수들은 /etc/passwd 파일에 기록된 내용으로부터 값이 설정된다. 따라서 사용자는 위의 변수들을 초기화 하지 않아도 자동으로 초기화 되는 것이다. 사용자의 로그인 쉘이 본셀이라면, 쉘 프롬프트가 나타나기 전에 다음과 같은 [그림]과 같은 일련의 작업이 수행된다.

 

[참고] getty & login

getty 대해서

 

# man getty

NAME

getty - set terminal type, modes, speed, and line discipline

 

SYNOPSIS

/usr/lib/saf/ttymon [-h] [-t timeout] line [ speed [ type [linedisc]]]

 

/usr/lib/saf/ttymon -c file

 

DESCRIPTION

getty sets terminal type, modes, speed, and line discipline.

getty is a symbolic link to /usr/lib/saf/ttymon. It is

included for compatibility with previous releases for the

few applications that still call getty directly.

 

 

 

login 대해서

 

# man login

NAME

login - sign on to the system

 

SYNOPSIS

login [-p] [-d device] [-h hostname | [terminal] |

-r hostname] [ name [environ]...]

 

DESCRIPTION

The login command is used at the beginning of each terminal

session to identify oneself to the system. login is invoked

by the system when a connection is first established, after

the previous user has terminated the login shell by issuing

the exit command.

 

# login

login: shuser

Password:

Sun Microsystems Inc. SunOS 5.9 Generic January 2003

$

 

 

 

 

 

 

2

명령어 종류

 

 

 

일반적인 쉘에서 실행할수 있는 명령어(실행할 수 있는 형식)()별명(alias), ()함수(function), ()쉘내부명령어(쉘내장 명령어), ()디스크에 존재하는 실행 프로그램등이 있다. 하지만 본쉘은 별명(Alias)이 존재하지 않는다.

 

함수는 쉘의 메모리에서 정의된다. 내부 명령어는 쉘 안의 내부 수행과정인 반면, 실행 프로그램은 디스크에 위치 한다. 쉘은 경로변수(PATH)를 검사하여 디스크에서 실행할 수 있는 프로그램을 찾고, 해당 명령어가 실행되기 전에 자식 프로세스를 만들어 놓는다. 명령어의 실행 준비가 되면 아래 순서와 같이 명령어 종류를 실행한다.

 

키워드(keyword) - if, case, for, while ...

내부명령어 - cd, set, unset, exit ...

함수(function) - function () { } ...

스크립트(Script)/실행 명령어 - backup.sh, ls, find ...

 

쉘 프롬프트에서 사용자의 명령어가 입력이 되면 쉘에서 정의된 키워드인지 확인 한후 아니라면 내장 명령어인지를 확인한다. 만약 내장 명령어라면 실행을 시키고 아니면 자식 프로세스를 생성(fork)한다. 그럼 부모 쉘은 대기모드로 접어들게 되고 자식 프로세스는 명령어가 컴파일된 실행 프로그램인가를 봐서 만약 맞다면 커널이 새 프로그램을 메모리에 올리고 exec를 사용하여 자식 프로세스로 대체한다. 하지만 컴파일된 프로그램이 아니라면 스크립트 프로그램인가를 보고 맞다면 실행시킨다. 그리고 자식 프로세스가 종료하게 되면 부모 쉘이 대기상태에서 빠져 나오게 된다.

 

 

 

 

 

3

프로세스와 쉘

 

 

 

프로세스 관리와 제어는 커널이 담당한다. 프로세스는 해당 프로그램과 프로그램 데이터와 스택, 스택 포인터, 레지스터, 그리고 프로그램 수행에 필요한 여러 정보들로 구성 된다. 사용자가 로그인 하게 되면 로그인 쉘이 터미널을 제어하게 되고 프롬프트에서 명령어가 입력되기를 기다린다.

 

쉘은 다른 프로세스들을 만들수 있다. 실제로 사용자 명령어를 입력하거나 쉘 스크립트를 실행하면 쉘은 내부 코드나 디스크에서 해당 명령어를 찾아 실행할 책임을 지게 된다. 이 작업은 커널을 호출하는 것으로 이루어지며, '시스템 호출(System Call)'이라고도 부른다. 시스템 호출은 커널에 도움을 요청하는 것으로, 프로세스가 시스템의 하드웨어에 접근할 수 있는 유일한 방법이다. 프로세스의 생성, 실행, 종료를 수행하는 다양한 시스템 호출이 있다.(쉘은 리다이렉션, 파이프, 명령어 치환, 사용자 명령어 실행 등에 커널을 이용하는 다른 방법을 제공한다.)

 

 

 

(1) 프로세스 생성

 

# ls

 

 

 

 

fork 시스템 호출(System Call)

 

시스템에서 프로세스 생성은 fork 시스템 호출로 이루어진다. fork 시스템 호출은 자식을 호출한 프로세스의 복사본을 하나 만들어낸다. 새로 생성된 프로세스를 자식 프로세스(Child Process)라 하며, 호출하는 프로세스를 부모 프로세스(Parent Process)라고 한다. 자식 프로세스는 fork 호출이 이루어진 직후에 시작되고 초기에는 두 프로세스가 CPU를 공유한다. 자식 프로세스를 열린 파일, UID, 실제(Real) UID, umask, 현재 작업 디렉토리, 시그널 등 부모 프로세스의 환경을 복사하여 사용한다.

 

명령어가 입력되면 명령행을 분석하고 첫 단어가 내부 명령어인지 아니면 디스크에 위치하는 실행 프로그램인지 판단한다. 내부 명령어이면 쉘은 바로 처리할 수 있지만, 디스크에 위치하는 실행 프로그램이면 fork 시스템 호출을 통해 자신의 프로세스를 복사하여 자식 프로세스를 생성한다. 이렇게 생성된 자식 프로세스는 해당 명령어를 찾기 위해 경로를 검색하여 작업뿐 아니라 리다이렉션, 파이프, 명령어 치환, 백그라운드 처리 등을 설정한다. 한편 자식 프로세스가 진행되는 동안 부로 프로세스는 일반적으로 대기 상태에 놓인다.(wait 시스템 호출)

 

 

 

wait 시스템 호출(System Call)

 

부모 프로세스인 쉘은 자식 프로세스가 리다리렉션, 파이프, 백그라운드 설정 등 세부작업을 처리하는 동안 대기(wait) 상태에 들어간다. wait 시스템 호출은 자식 프로세스들 중 하나가 종료될 때까지 보로 프로세스의 진행을 정지시킨다. 부모 프로세스가 성공적으로 대기하게 되면 시스템은 작업이 끝난 자식 프로세스의 PID 번호와 종료 상태값을 반환한다.

 

부모 프로세스가 대기하지 않는 상황에서 자식 프로세스가 종료되면 자식 프로세스는 좀비(zombie) 상태에 놓이게 된다. 자식 프로세스는 부모 프로세스가 wait 호출을 하거나 부모 프로세스 자체가 종료될 때까지 좀비 상태로 존재한다. 만약 자식 프로세스보다 부모 프로세스가 먼저 종료되면, init 프로세스는 고아가 된 좀비 프로세스를 양자로 삼는다. wait 시스템 호출은 부모 프로세스를 대기 상태로 만들 뿐 아니라, 부모 프로세스가 올바르게 종료할 수 있도록 필요한 조치를 취한다.

 

 

 

exec 시스템 호출(System Call)

 

터미널에서 입력이 들어오면 쉘은 보통 새로운 쉘 프로세스를 생성(fork)하여 자식 프로세스를 만든다. 한편, 앞에서도 언급했듯이 자식 프로세스는 입력된 명령어의 실행을 책임지게 된다. 자식 프로세스는 exec 시스템 호출을 통해 이 과정을 수행한다. 이 때 사용자가 입력한 명령어는 반드시 실행할 수 있는 프로그램이어야 한다. 쉘은 입력된 프로그램을 찾으려고 경로를 검사한다. 명령어를 찾으면 쉘은 명령어 이름을 인자로 하여 exec 시스템 호출을 요청한다. 그러면 커널은 호출했던 쉘 대신 새 프로그램을 메모리로 불러 온다. 이때 자식 쉘은 실행될 프로그램과 겹쳐진 형태로 존재한다. 이 프로그램이 자식 프로세스가 되고, 실행을 시작한다. 새 프로세스가 자신이 사용할 다른 지역변수들을 갖고 있더라도, 모든 환경변수와 열린 파일들, 시그널, 현재 작업 디렉토리 등은 이 프로세스에 전해 진다. 이 프로세스가 실행을 완료하면 부모 쉘이 대기 상태에서 빠져 나온다.

 

 

 

exit 시스템 호출(System Call)

 

exit 시스템 호출을 사용하면 실행 프로그램을 언제든지 종료할 수 있다. 보통 자식 프로세스가 종료할 때는 sigchild라는 시그널을 전송하고, 부모 프로세스가 종료 상태값을 받아들일 때까지 기다린다. 종료 상태값은 0에서 255사이의 숫자가 된다. 종료 상태값이 0이면 프로그램이 실행을 성공적으로 마친 것이고 0이 아닌 다른 값이면 프로그램의 실행이 어떤 면에서도 실패했다는 것이다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

[그림3]에서 부모쉘이 fork 시스템 호출을 통해 자신의 복사본인 자식 쉘을 생성하고 자식쉘은 새로운 PID 번호를 할당 받는다. 초기에는 부모 프로세스와 CPU를 공유한다. 커널이 ls 프로그램을 메모리로 불러온 다음, 자식 프로세스의 위치에서 실행(exec())한다. ls 명령어는 자식 쉘로부터 열린 파일과 환경을 상속한다. 그리고 ls 명령어가 종료되면 커널은 메모리를 청소하고, 부모 프로세스는 대기 상태에서 빠져 나온다.

 

 

 

(2) 환경 상속

 

사용자가 로그인하면 쉘이 시작된다. 이 때 쉘은 자신을 실행했던 /bin/login 프로그램으로부터 여러 개의 변수, 입출력 스트림, 프로세스 특징 등을 상속한다. 마찬가지로 부모 프로세스가 새로운 자식 프로세스를 생성하면 자신의 환경을 자식 프로세스에게 물려 준다.

 

자식 프로세스를 백그라운드 프로세스 처리, 여러 명령어 그룹 처리, 스크립트 실행 등을 위해 생성될 수 있다. 이 때 자식 프로세스는 자신의 부모 프로세스로부터 환경을 이어 받는다. 부모 프로세스가 물려주는 환경에는 프로세스 권한, 작업 디렉토리, 파일 생성 마스크(umask), 특수 변수, 열린 파일, 시그널 등이 있다.

 

4

리다이렉션

 

 

 

파일 식별자(File Descriptor)는 프로세스가 파일을 열면 할당되는 번호이다. 다시말해서 열린 파일을 식별할 때 사용하는 번호이다. 파일 식별자 중 열린 파일에 할당하지 않는 번호가 있다. 이것은 0(표준입력), 1(표준출력), 2(표준에러) 이다. 이 파일 기술자는 키보드에서 입력을 받거나 터미널로 출력되지 않고 파일에서 입력 받거나, 파일로 출력하는 경우가 있다. 이런 경우 이것을 I/O 리다이렉션(I/O Redirection)이라고 한다.

 

리다이렉션 표준입력(Redirection Stdin), 리다이렉션 표준출력(Redirection Stdout), 리다이렉션 표준출력(Redirection Stderr)는 키보드로 입력받지 않고 파일로 입력 받거나, 파일로 출력하는 경우를 나타낸다.

 

쉘은 다음과 같은 동작을 통해 리다이렉션을 수행한다.

터미널에 지정된 표준 출력 파일 식별자 1을 닫은 후, 파일로 재지정함으로써 출력을 파일로 리다

이렉션 해준다.

표준 입력 방향을 재 지정하는 경우, 터미널로 지정된 표준 입력 파일 식별자 0을 닫고, 그것을

파일로 지정한다. 쉘은

파일 식별자 2를 파일로 지정함으로써 에러를 처리한다.

 

 

 

(1) 표준입력 리다이렉션(Redirection Standard Input)

 

표준입력 리다이렉션은 키보드로 입력을 받지 않고, 파일로 입력을 받는 경우이다.

 

$ mailx -s "Hello Test" root@example.com < mail.txt

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

mailx 명령어를 메일 전송시에 표준입력(키보드)을 통해 메일의 내용을 입력하지 않고 mail.txt 파일의 내용을 통해 입력을 받는 경우이다.

(2). 표준출력 리다이렉션(Redirection Standard Output)

 

표준출력 리다이렉션은 출력 내용을 표준출력(모니터)으로 하지 않고 파일로 출력하는 경우이다.

 

$ grep user01 /etc/passwd > file.txt

 

 

 

 

(3). 표준에러 리다이렉션(Redirection Standard Error)

 

표준에러 리다이렉션은 출력 내용을 표준에러(모니터)로 출력하지 않고 파일로 출력하는 경우이다.

 

$ find / -name filename 2> error.log

 

 

5

파이프

 

 

 

파이프를 사용하면 한 명령어의 출력을 다른 명령어의 입력으로 보낼 수 있다. 쉘은 파일 식별자를 닫았다가 다시 여는 방법으로 파이프를 구현한다. 하지만 파일 식별자를 직접파일에 지정하기보다는 "파이프 시스템 호출(Pipe System Call)"로 생성되는 파이프 식별자에 파일 식별자를 지정한다.

 

부모 쉘이 파이프 파일 식별자를 생성하고 나서 파이프라인에 있는 각 명령어에 대해 자식 쉘을 생성한다. 한 명령어는 파이프로 출력하고 다른 명령어는 파이프로부터 입력을 받는다. 즉 파이프는 두 프로세스가 데이터를 공유할 수 있는 커널 버퍼(kernel buffer)라고 할 수 있다. 임시 파일들을 따로 사용할 필요가 없어지는 것이다.

 

파이프 식별자가 설정되고 나면, 명령어들이 동시에 실행된다(exec). 한 명령어의 출력이 버퍼로 보내지고 나서 버퍼가 가득 차게 되거나 그 명령어가 종료되면 파이프의 오른쪽에 있는 명령어가 버퍼로부터 데이터를 읽는다. 커널은 하나의 프로세스가 버퍼에 데이터를 쓰고 있거나 데이터를 읽어가는 중이면 다른 프로세스가 대기하도록 동기화 해준다.

 

부모 쉘이 파이프 시스템 호출을 하면 두 개의 파이프 식별자가 생성된다. 하나는 파이프로부터 데이터를 읽기 위한 것이고 다른 하나는 파이프에 데이터를 쓰기 위한것이다. 파이프 식별자에 결합되는 파일들은 데이터를 임시로 저장하기 위해 커널이 관리하는 I/O 버퍼들이다.

 

 

 

다음 예제를 가지고 실제 파이프의 동작 과정을 이해하여 보자.

# who | wc

 

부모 쉘이 파이프 시스템 호출을 하면, 두 개의 파일 식별자가 반환된다.

 

하나는 파이프로부터 데이터를 읽기 위한 것이고, 다른 하나는 파이프에 데이터를 쓰기 위한 것이다. 반환된 파일 식별자 번호들은 파일 식별자 데이틀(fd table)에서 사용할 수 있는 번호를 중 가장 먼저 사용할 수 있는 fd 3fd 4가 된다.

 

 

 

 

부모 쉘이 who 명령어와 wc 명령어를 위한 자식 프로세스를 각각 생성한다.

두 프로세스는 모두 부모 프로세스의 열린 파일 식별자의 목록을 상속한다.

 

 

 

첫째 자식 프로세스가 자신의 표준 출력을 닫는다.

그런 다음, 파이프에 데이터를 쓰기 위한 fd 4를 복사한다.(dup 시스템 호출). 이때 dup 시스템 호출은 fd 4의 복사본을 만들어 테이블에 남아 있는 식별자들 중에서 가장 작은 fd 1에 대입하고, 이 복사 과정이 완료되면 fd 4를 닫는다. 자식 프로세스는 fd 3을 닫는다. 이 자식 프로세스는 자신의 표준 출력을 파이프로 보내려고 하기 때문에 fd 3은 사용하지 않는다.

 

 

 

 

둘째 자식 프로세스가 자기 표준 입력을 닫는다.

그런다음 파이프로부터 데이터를 읽어오기 위해 파일 식별자 3을 복사한다.(dup 시스템 호출). 이때 dup 시스템 호출은 fd 3의 복사본을 만들어 테이블에 남아 있는 식별자들 중에서 가장 작은 fd 0에 대입한다. 현재 fd 0이 닫혀 있기 때문에 최소 식별자가 된다. dup 시스템 호출은 fd 3을 닫는다. 그 다음, 자식 프로세스가 fd 4를 닫는다. 이제 표준 입력은 파이프로부터 들어오게 된다.

 

 

 

who 명령어가 첫째 자식 쉘에서 실행된다.

그리고 wc 명령어는 둘째 자식 쉘에서 실행된다. who 명령어의 출력은 파이프로 들어가고 wc 명령어는 이 파이프의 반대편에서 이 내용을 읽는다.