https://devdocs.io/bash/what-is-a-shell_003f
bash-like program, minishell을 올해 1-2월 새로 끝낸 적이 있는데, 쉘에 대해 정리하지 않았던 것이 늘 마음에 걸렸어서 작성하게 된 글이다.
shell이란?
command를 실행하는 macro processor 다. 라고 정의되어 있다.
macro processor 라는 용어는 '텍스트 text'와 '기호 symbol' 를 확장하여 더 큰 표현을 생성하는 기능을 의미한다.
Unix shell은 command interpreter 이면서 동시에 programming language 이기도 하다.
shell의 특징
command interpreter 로써, shell 은 GNU utilities 와 함께 유저 인터페이스를 제공한다.
programming language 로써, 이러한 기능이 함께 결힙되어 사용되는데 command를 담고 있는 file이 만들어지고, 그 자체로 명령어가 될 수 다. 이러한 명령어들은 /bin 폴더에 존재하는 system command와 같은 status를 갖게 되는데, 사용자들이 특정 작업을 자동화하여 custom 된 환경을 가질 수도 있게 만든다.
쉘은 interactive, non-interactive 할 수 있는데, interactive mode에서는 keyboard에서 타이핑되는 것을 받고, non-interactive mode에서는 file에서 읽어들여서 명령어를 실행한다.
shell은 GNU 명령어들을 동기적으로, 혹은 비동기적으로 실행할 수 있도록 제공한다. 동기적인 명령어를 실행할 때는 이 명령어가 종료될 때까지 기다리고 input을 받게 되며, 비동기적인 명령어 실행을 할 땐 바로 input을 받고 추가적으로 명령을 실행한다.
'redirection' 구성을 사용하면, 명령의 입럭 및 출력을 세밀하게 제어할 수 있다. 또한 쉘은 명령어의 환경의 내용을 제어할 수 있기도 하다.
(redirection 부분 보러가기)
shell은 built-in function 몇 개를 가지고 있기도 하다.
cd, break, continue, exec 과 같은 함수들은 shell 밖에서 구현될 수가 없는데, 그 이유는 이 함수들이 shell 자체를 바꾸기 때문이다.
history, getopts, kill, pwd 와 같은 빌트인 함수들은 별개 utility에서 구현될수도 있기도 하다.
shell의 동작
shell operation은 다음과 같다.
1. file로부터 input을 받는다. quoting rule에 따라서 입력 받는다.
2. input을 단어와 operator로 분류한다. 분류된 token은 'metacharacters' 구분된다.
3. token을 simple, compound command로 구분한다. (echo a b c 같은 simple command, pipeline, loop 등 이 있는 compound command)
4. 확장된 command를 filename으로 만든다. (ㄹilename expansion: *,?,[ 같은 문자들을 pattern으로 인식하고 replace)
5. 필요한 redirection을 수행하고 argument list 에서 삭제한다.
6. command를 실행한다.
7. 선택적으로 command가 끝내기를 기다린 후 exit status를 회수한다.
minishell을 구현할 때 파싱 + 나머지 구현이라고 해도 될 정도로 파싱을 많이했는데,
quoting rule 에 따라 "", '' 같은 기호를 잘 처리하는 것도 꽤나 중요했다.
그리고 linux는 실제 명령어를 parse tree로 가지고서 tokenize 한다는 특징이 있는데, 나는 구현 시에는 linked list로 구현했었다.
init process
또 하나 minishell과 관련해서 알아두면 좋은 것이 '프로세스의 실행'과 'init process'이다.
컴퓨터를 켜면, 운영체제가 동작하기 위해 시스템은 bootlader를 찾는다. 그리고 kernel을 시작한다.
하지만 kernel이 혼자 모든 프로세스를 시작하지는 않고, 프로세스는 생기는 방식 자체가 하나의 프로세스를 fork 하는 방식으로 생기기 때문에 초기의 프로세스 하나가 존재해야 한다.
이렇게 태초의 프로세스 역할을 하는 것이 바로 init process이다.
init process는 pid = 1인 프로세스로 /etc/inittab script 가 init process로 사용된다. system이 시작되거나 끝날 때 필요한 process entry 를 가지고 있다. (file system mount 관련한 부분이나 데몬 프로세스 관련) 따라서 시스템이 종료되기 전까지 init process는 계속 작동해야하면 시스템이 시작할 수 없는 경우 이를 'Kernal Panic'이라고 부른다.
더 상세히 설명하면, /etc/rc 또는 /etc/inittab 라고 불리는 파일 위치에서 (OS마다 다름) 커널이 init process를 호출하는데,
init process가 실행되면 path를 설정하고, file system을 체크하고, serial ports, clock 등 많은 것들을 세팅한다.
그리고 마지막으로는 운영체제가 잘 작동하기 위해 여러 백그라운드 프로세스들을 데몬으로 실행하는데 보통 이러한 프로세스들은 /etc/init.d 디렉토리 내에 스크립트로 존재한다. (여담이지만 .d는 데몬이라는 의미는 아니고 디렉토리라는 의미라고 한다.)
init process는 daemon 프로세스를 자신을 fork 하면서 시작한다.
<process forking>
전통적으로 Unix에서는 process를 생성하기 위해서는 이미 존재하는 process의 복사본을 만드는 수 밖에 없다. 이러한 것은 이미 존재하는 프로세스가 child process를 만들고 exec system call을 통해 다른 프로그램을 시작하는 방식으로 작동한다.
unix standard library 가 제공하는 C 방식의 fork 함수가 곧장 연관되어있다.
child process는 parent process와 거의 동일한데, PID, parent PID 가 다르다는 점과 동시에 memory, async I/O 는 공유하지 않는다는 점을 제외하고는 동일하다고 볼 수 있다.
현재는 fork방식 외에도 프로세스를 만들 수 있다고는 하지만, 대다수의 프로세스들은 여전히 이 방식으로 만들어지고 있다.
<daemons>
daemon이라는 용어는 MIT 의 project MAC 에서 유래했는데 Maxwell's demon :분자들이 이리저리 움직이는 과정을 background에서 분자들을 정렬하는 '악마'로 표현한 사고실험에서 유래했다고 한다.
선하지도 악하지도 않은 초자연적인 존재라는 의미의 그리스어 daemon에서 정확한 스펠링을 가져왔으며, 실제로 우리가 요즘 알고 있는 '악마'라는 개념에서 실제로 유래한 것이라고 볼 수도 있다.
daemon process는 int processes를 부모 프로세스로 갖고 있으며 터미널은 제어하는 것과는 별개로 동작하는 프로세스이다.
보통 네트워크 요청, 하드웨어 동작, 모니터링 혹은 wait과 관련한 작업에 관련한다.
또한 daemon process는 terminal에서 요청한 background process와 다르다.
background process는 terminal session에 연결되기 때문에 터미널을 이용해서 바로 종료할 수 있는 반면 daemon은 init process의 자식 프로세스이기 때문에 종료하기가 어렵다.
신기하게도 daemon process 가 만들어지는 과정은 두가지인데,
init process가 직접 fork 하는 방식은 위에서 이야기 했고 나머지 한 방법은 다른 프로세스가 fork를 하는 것이다. 이 경우 해당 프로세스가 kill 되어서 그 daemon process를 고아 프로세스를 만드는데, 운영체제에서 고아 프로세스는 init process가 입양하므로
두가지 방법 모두 init process가 daemon process의 부모 프로세스가 되는 것이 성공적으로 이루어진다.
minishell(bash like program) 이나 pipex(pipeline, redirection) 같은 과제를 통해서 특정 함수를 실행하려고 할 때마다 fork를 뜨고 exec 시스템 콜을 이용해서 실행했는데, 그러한 과정에는 이런 배경지식이 깔려있었다.
참고
https://thecodeboss.dev/2016/11/how-daemons-the-init-process-and-process-forking-work/