프로세스 생성
프로세스 확인
# 모든 프로세스 확인
ps aux
해당 명령어를 실행하면 PID, %CPU, %MEM 등 프로세스에 관한 정보를 얻을 수 있다.
단순하게 프로세스의 개수를 알고 싶다면?
ps aux --no-header | wc -l
프로세스 생성
새로운 프로세스를 생성하는 목적은 다음 두 가지 경우이다.
- 동일한 프로그램 처리를 여러 프로세스에 나눠서 처리(웹서버에서 다수의 요청 받기)
- 다른 프로그램 생성(bash에서 각종 프로그램 새로 생성)
첫 번째 경우는 저번 학기에 배운 운영체제 과제에서 수행하였다. 멀티 프로세스를 구현해서 요청에 대해 일대일 대응을 시키는 과제였다.
프로세스 생성을 실제로 실행하는 방법은 fork(), execve() 함수를 사용하는 것이다.
해당 함수는 내부적으로 각각 clone(), execve() 시스템콜을 호출한다. (저번 글에서의 이해가 필요하다. 함수 → 시스템 콜 → 커널)
첫 번째 경우는 fork()만, 두 번째 경우는 fork(), execve()를 모두 사용한다.
fork() 함수 : 자식 만들기
fork() 함수는 간단하게 복사본을 만든다고 생각하면 된다. 원본이 부모(parent process), 복사본이 자식(child process)라고 부른다.
- 실행 순서
- 부모 프로세스가 fork() 함수 호출
- 자식의 메모리 영역 확보한 후 부모 프로세스 메모리 복사
- 부모와 자식 둘 다 fork() 함수에서 복귀(실행 끝 의미: 반환값 생성)
- 부모와 자식은 반환값이 서로 달라서 분기 처리가 가능
운영체제 과목에서 fork에 관하여 학습을 해서 다 알고있는 내용인 줄 알았는데 실행 순서(fork() 함수 복귀 …)에 관해서 이번 기회에 더 자세하게 배웠다. 다시 생각해보면 부모와 자식이 fork() 함수에서 복귀한다는 의미는 당연한 것이다. 역할이 끝났기 때문!
또한 프로세스 메모리를 복사하는 작업은 추후에 배울 내용이지만 카피 온 라이트 기능 덕분에 매우 적은 비용이라는 것이다. 그래서 결과적으로 멀티 프로세스를 구현해도 오버헤드가 적지 않다는 것!
fork() 실습 진행
# fork.py
import os, sys
ret = os.fork()
if ret == 0:
print("Child process pid={}, Parent process pid={}".format(os.getpid(), os.getppid())
exit()
elif ret > 0:
print("Parent process pid={}, Child process pid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
해당 스크립트를 생성하고 python3 ./fork.py를 수행하면 다음과 같은 결과를 얻는다.
정리하자면 fork() 함수에서 복귀한 후 반환값에 조건이 달라진다.
- 부모 프로세스 : 자식 프로세스의 ID 반환(ret > 0 조건)
- 자식 프로세스 : 0을 반환(ret == 0 조건)
execve() 함수 : 다른 프로그램을 기동
fork() 함수로 프로세스 복사본을 만들었으면 자식 프로세스에서 execve() 함수를 호출하여 새로운 프로그램으로 바꾼다.
- 실행 순서
- execve() 함수 호출
- execve() 함수 인수로 지정한 실행 파일에서 프로그램을 읽어서 메모리에 배치하는데 필요한 정보 가져옴
- 현재 프로세스의 메모리를 새로운 프로세스 데이터로 덮어씀
- 프로세스를 새로운 프로세스의 최초에 실행할 명령(entry point)부터 실행 시작
(현재 프로세스 메모리 → execve() 실행 → 실행 파일의 새로운 프로세스 메모리 → 시작)
fork() 함수를 호출한 후에 자식 프로세스는 execve() 함수에 의해서 인수로 지정한 프로세스로 변경된다는 의미! 이 구조를 fork-exec 구조라 한다.
execve() 실습 진행
# fork-and-exec.py
import os, sys
ret = os.fork()
if ret == 0:
print("Child process pid={}, Parent process pid={}".format(os.getpid(), os.getppid())
os.execve("/bin/echo", ["echo", "pid={} hi"].format(os.getpid())], {})
exit()
elif ret > 0:
print("Parent process pid={}, Child process pid={}".format(os.getpid(), ret))
exit()
sys.exit(1)
해당 스크립트를 생성하고 python3 ./fork-and-exec.py를 수행하면 다음과 같은 결과를 얻는다.
execve() 명령어로 인해서 hi가 나온 것을 알 수 있다. 결국 자식 프로세스 메모리에 echo 메모리가 바뀐 것이다.
execve() 함수가 동작하려면 실행 파일은 프로그램 코드와 데이터 이외에도 추가적인 데이터가 필요하다.
- 코드 영역의 파일 오프셋, 크기 및 메모리 맵 시작 주소
- 데이터 영역의 파일 오프셋, 크기 및 메모리 맵 시작 주소
- 최초로 실행할 명령의 메모리 주소(엔트리 포인트)
이 부분은 저번 학기에 배운 운영체제 과목에서 배운 내용으로 이해를 했다.
- 코드 영역:
- 파일 오프셋 : 실행할 파일의 코드(기계어) 파일 내에서 시작하는 위치로 운영체제가 해당 정보를 사용해서 실행 파일에서 코드 영역을 추출
- 크기 : 메모리에 얼마나 많은 공간을 할당해야하는 지 결정
- 메모리 맵 시작 주소 : 코드 영역이 메모리에서 위치할 주소
- 데이터 영역:
- 파일 오프셋 : 데이터 영역(전역 변수, 초기화된 데이터 ..) 파일 내에서 시작하는 위치로 운영체제가 해당 정보를 사용해서 데이터 영역을 파일에서 추출
- 크기 : 메모리에 얼마나 많은 공간을 할당해야하는 지 결정
- 메모리 맵 시작 주소 : 데이터 영역이 메모리에서 위치할 주소
- 엔트리 포인트:
- 프로그램이 처음 실행될 때 최초로 실행될 명령어의 메모리 주소
- 운영체제가 해당 함수로 프로그램을 실행할 때 엔트리 포인트 정보 사용
결국 코드, 데이터 영역에 있는 정보를 추출 및 확인한 후 메모리에 올리는 과정이라고 할 수 있다.
프로세스의 부모 자식 관계
프로세스를 새로 생성하려면 부모 프로세스가 자식 프로세스를 생성해야한다.
그러면 최종적으로 어디까지 뻗어나가는 것일까?(Tree의 끝은 어디지?)
컴퓨터 전원 ON하면 다음과 같은 순서로 시스템 초기화 진행
- 컴퓨터 전원 ON
- BIOS or UEFI 같은 펌웨어를 기동하고 하드웨어 초기화
- 펌웨어가 GRUB 같은 Boot Loader 기동
- Boot Loader가 OS 커널 기동
- OS 커널이 init 프로세스(수업시간에는 조상 프로세스로 배움)기동
- init 프로세스가 자식 프로세를 기동하고 연쇄적으로 기동해서 프로세스 트리 구조 생성
과정을 정리하면 다음과 같다.
펌웨어 → Boot Loader → OS 커널 → init 프로세스 → 자식 프로세스 …
systemd(1)이 init 프로세스를 의미한다.
프로세스의 상태
pstree -p 명령어로 확인해봤듯이 시스템에는 수많은 프로세스가 존재한다. 그러면 프로세스들은 계속해서 CPU를 사용할까?
각 프로세스는 실행된 후 이벤트가 발생할 때까지 CPU를 사용하지 않고 sleep 상태로 기다리고 있다.
예를 들면 bash 프로세스 같은 경우 사용자 입력이 있을 때까지 할 일이 없으므로 사용자 입력을 기다린다. 이 경우가 sleep 상태이다.
위에서 STAT 속성을 정리하면 다음과 같다.
- S : sleep 상태
- R : running 상태
- Z : zombie 상태
만약 시스템의 모든 프로세스가 슬립 상태라면 논리 CPU는 idle process라고 하는 ‘아무 일도 하지 않는’ 특수한 프로세스를 동작시킨다.
CPU 특수 명령을 사용해서 논리 CPU를 휴식 상태로 전환하고, 하나 이상의 프로세스가 실행 가능 상태가 될때까지 소비 전력을 억제하면서 대기한다.
노트북이나 스마트폰이 아무것도 실행이 되지 않으면 배터리가 오래가는데 이때 CPU가 사용되지 않으니까 idle process가 동작하는 원리구나!
좀비 프로세스 & 고아 프로세스
- 좀비 프로세스:
- 자식 프로세스가 종료되었지만 부모가 종료 상태를 확인하지 않은 상태
- 고아 프로세스:
- wait() 계열 시스템 콜을 실행하기 전에 부모 프로세스가 종료된 자식 프로세스
- 커널은 고아 프로세스의 부모로 init 프로세스로 설정
시그널
특정 프로세스가 다른 프로세스에 어떤 신호를 보내서 외부에서 실행 순서를 강제로 바꾸는 방법 의미
- SIGINT:
- bash에서 ctrl+c와 동일하여 프로세스 곧바로 종료
- kill {#pid} : 프로세스 종료
시그널 목록은 man 7 signal 명령어를 통해서 확인이 가능하다.
SIGINT가 프로세스를 바로 종료시키지는 않고, 프로세스는 각 시그널에 시그널 핸들러를 미리 등록해두어서 프로세스를 실행하다가 해당하는 시그널을 수신하면 실행 중인 처리를 일단 중단하고 시그널 핸들러에 등록한 처리를 동작시킨 다음에, 원래로 돌아가 이전에 하던 동작을 재개한다.
셸 작업 관리 구현
세션
세션에는 세션 ID 또는 SID라고 부르는 값이 할당되어져있다.
세션 리더라고 하는 프로세스가 존재하고, 보통은 bash 같은 셸이다.(세션 리더 pid == 세션 id)
ps ajx 를 통해 세션 관련 정보를 확인해보면 TTY 속성의 값은 해당 값으로 가상 단말이 할당되어있다는 의미이다.
세션에 할당된 단말이 연결이 끊기면 세션 리더에는 SIGHUB 시그널이 가는데, 이때 자신이 관리하던 작업을 종료시키고 자신도 종료한다.
예를 들면, 구글 코랩에서 정해진 시간 내에 어떠한 동작도 하지 않으면 세션이 끊기는데 구글 측에서는 특정 시간이 지나면 SIGHUB 시그널이 가도록 설정해놓은 것 같다.
실행에 시간이 오래걸리는 프로세스를 실행 중에 bash가 종료되는 것을 원하는 않는 경우 다음과 같이 할 수 있다.
- nohup : SIGHUP을 무시하도록 설정하고 프로세스를 기동
- bash의 disown 내장 명령어 : 실행 중인 작업을 bash 관리 대상에서 제외
프로세스 그룹
여러 프로세스를 하나로 묶어서 한꺼번에 관리하는 것이다. 세션 내부에는 여러 개의 프로세스 그룹이 존재한다.
예를 들면 다음과 같은 명령어가 프로세스 그룹이라고 할 수 있다.
- 로그인 셸은 bash
- bash 명령어 1
- bash 명령어 2
2개의 프로세스 그룹이 형성되었다.
이렇게 프로세스 그룹을 종료하기 위해서는 kill 명령어에 pgid 앞 - 를 사용하여 종료할 수 있다.
세션 내부에 있는 프로세스 그룹은 두 종류이다.
- 포그라운드 프로세스 그룹:
- 셸의 포그라운드 작업에 대응
- 세션당 하나만 존재하고 세션 단말에 직접 접근 가능
- 백그라운드 프로세스 그룹:
- 셸이 백그라운드 작업에 대응
- 백그라운드 프로세스가 단말을 조작하려고하면 실행이 일시 중단되고, fg 내장 명령어 등으로 프로세스가 포그라운드 프로세스 그룹이 될 때까지 이상태 유지(back → fore가 되어야지 단말 접근 가능)
데몬
데몬은 상주하는 프로세스이다. 보통 프로세스는 사용자가 실행하고 작업이 끝나면 종료한다.
데몬은 이와 달리 시스템 시작부터 종료할 때까지 계속해서 존재하며 실행된다.
- 단말의 입출력이 필요 없기 때문에 단말이 할당 x
- 로그인 세션을 종료해도 영향을 받지 않도록 독자적인 세션 가짐
- 데몬을 생성한 프로세스가 데몬 종료 여부를 신경 쓸 필요없이 init이 부모가 됨
운영체제와 리눅스 과목에서 데몬에 대한 이해가 부족했는데 책을 읽어보니 생각보다 간단한 개념이네? 상주하고 있는 프로세스라고 이해하면 3가지 특성 또한 자연스럽게 이해가 된다!
'Study > OS' 카테고리의 다른 글
[그림으로 배우는 리눅스 구조] CH01. 리눅스 개요 (0) | 2024.08.04 |
---|