Process
Program vs Process
- Process : 실행중인 프로그램
- Program : 실행 가능한 파일
- Process는 메모리에 올라가 있는 상태의 프로그램을 의미한다.
C언어 Program to Process
- C언어로 구성된 프로그램은 전처리 - 컴파일 - 링킹 - 로딩의 과정을 거친다.
- 전처리 :
#
으로 시작하는 라인들을 알맞은 형태로 치환한다. - 컴파일 : C언어(high-level language)를 어셈블리어(기계어) 로 변환한다.
- 링킹 : 외부의 ELF(Executable and Linkable Format) 파일들을 호출할 수 있도록 연결한다.
- 로딩 : 최종 생성된 파일을 실행시켜 메모리에 올려 프로세스로 만든다.
- 리눅스에서는
execv()
함수에 의해 프로세스화 된다.
- 리눅스에서는
- 전처리 :
프로세스
fork
fork()
함수는 프로세스를 복사하는 함수이다.unistd.h
헤더에 선언되어 있다.- 복사당한 프로세스를 부모 프로세스, 복사해서 생성된 프로세스를 자식 프로세스라 한다.
- 복사된 자식 프로세스도
fork
실행 이후부터 코드가 진행된다.
fork
함수의 반환값은pid_t
타입이다.- 반환값이 -1이라면 실패를 의미한다.
- 결과가 0이라면 현재 프로세스는 자식 프로세스임을 의미한다.
- 0이 아니닌 값이라면 현재 프로세스는 부모 프로세스이다.
- 반환값은 자식프로세스의 process id를 의미하며, 리눅스 명령어
ps -ef
로 pid를 확인 할 수 있다.
- 반환값은 자식프로세스의 process id를 의미하며, 리눅스 명령어
- Race Condition : 일단 fork가 되어 프로세스가 부모 자식으로 나뉘면, 프로세스의 실행은 병렬적으로 이루어지며, 같은 코드라도 어느 것이 먼저 동작할지 알 수 없다.
wait
- fork() 로 자식 프로세스를 생성한 후 자식 프로세스가 exit()를 호출하여 종료될 때, 부모 process는 자식 process의 종료 결과를
wait()
으로 받을수 있다. wait(statloc *status)
: 자식 process에서 호출된 exit() 함수 안에 들어간 인자값을 status(인자는 4byte int지만, 사용하는 부분은 2byte) 에 담아낸다.- status 값은 상위 1byte와 하위 1byte를 구분해서 사용한다.
- 정상적으로 종료가 된경우는 exit() 함수에 의한 종료를 의미하며, status의 상위 1byte에 exit의 인자값을 담아낸다.
- 비정상 종료는 signal에 의한 종료를 의미하며, signal 번호 값을 status의 하위 1byte에 담아낸다.
- 0~7번 bit : 자식 process 정상종료시 종료 status
- 8번 bit : core dump 여부
- 9~15번 bit : 시그널 번호
- status값을 인자로 받아 종료 사유를 회신하는 매크로 함수를 사용하면 쉽게 판단할 수 있다.
- WIFEXITED, WEXITEDSTATUS, WIFSIGNALED …
메모리
- 부모 프로세스를 복사해 자식 프로세스를 생성해도 code 영역은 공유된다.
- code 영역은 read only memory 이기 때문에
- 자식 프로세스는 부모 프로세스의 ram 영역 값도그대로 복사 해 온다.
- 하지만, 자식 프로세스가 새성될 당시 메모리가 바로 복사되는 것이 아니라, 메모리에 값을 작성하는 시점에 복사가 된다.
- 즉, 부모나 자식 프로세스에서 값을 덮어쓰거나 새로 생성하지 않은 변수에 대해서는 같은 메모리를 바라보고 있다고 볼 수 있다.
- 메모리는 reference count를 들고 있어 몇개의 프로세스에서 해당 영역을 참조하는지 체크한다.
프로세스 생명 주기 (Life Cycle)
모든 프로세스는 부모 프로세스가 있고, 가장 최초로 실행된 프로세스를 init 프로세스라 하며, init 프로세스의 pid는 1이다.
생성된 프로세스는
exit()
함수를 호출하면 종료된다. (일반적으로 main 함수의 리턴값이 exit을 호출하도록 되어있다.)exit
은 라이브러리로 버퍼를 flush하고, open된 모든 파일을 close하고, 프로세스가 사용하고 있는 메모리 풀을 반환한다. 그후_exit
을 호출하여 프로세스를 종료시킨다.하지만,
exit
만 호출되었다고 해서 프로세스가 완벽하게 종료되는 것이 아니다. exit을 호출하면 부모 프로세스에서 상태코드(exit의 인자값)를 받아가기를 대기한다. 메모리의 반환 작업은 부모 프로세스의 처리가 끝나야 이루어진다.exit
의 결과값을 처리하는 함수는wait
이다. 부모 프로세스에서wait
을 실행하면 그제서야 자식 프로세스는 메모리를 정리하고 완벽하게 종료된다.- (커널 레벨에서 자식은 부모를, 부모는 자식들의 포인터를 갖고 있어 서로 참조할 수 있도록 연결되어 있다.)
좀비 프로세스
- 부모 프로세스에서 wait를 호출하지 않아 자식 프로세스를 정리해 주지 않으면
좀비 프로세스
가 생성된다. - 좀비 프로세스는 사용하지 않는 메모리 및 리소스들을 차지하고 있어서 다른 프로세스들의 성능을 저하시킨다.
고아 프로세스
- 자식 프로세스보다 부모 프로세스가 먼저 종료되는 경우, 그 자식 프로세스들은
고아 프로세스
가 된다. - 고아 프로세스들은 종료 처리를 해줄 부모 프로세스가 없기 때문에 좀비 프로세스가 될 수 있는데, 이를 막기 위해 커널은 고아 프로세스를 주기적으로 찾아 ‘init’ 프로세스의 자식으로 재설정한다.
signal
wait() 함수를 호출하면 부모 프로세스는 자식 프로세스가 exit를 호출하기를 기다린다.
이렇게 되면, 부모 프로세스는 다른 동작을 수행하지 못하여 concurrent한 동작 수행이 불가능하다.
부모가 자기 할 일을 수행하다 자식이 종료될 떄 종료 처리를 해 주도록 하려면
signal
기능을 사용하면 된다.- 자식 프로세스에서 exit를 호출하면 내부적으로 부모 프로세스에 SIGCHLD(sig child) 시그널을 보내도록 되어 있다.
- 부모 프로세스에서는
signal(SIGCHLD, my_function)
형태로 SIGCHLD 시그널의 처리를 my_function 으로 받아서 처리하도록 하고, myfunction 안에서wait
을 호출하면 된다.
signal(SIGCHLD, my_function); ... fork() ... my_function() { wait(0); }
단, 자식 프로세스가 여러개인 경우, 동시에 종료되는 자식 프로세스들에 대해서는 단순 signal 로 처리가 불가능하다.
signal이 호출되어 my_function이 돌고 있는 도중에 다음 signal이 호출되면, my_function 함수가 또 호출되지 않는다. 해당 signal은 무시되는 것이다.
하지만, signal은 무시되더라도 ‘부모 프로세스가 처리해야할 목록’ 에는 추가되기 때문에, wait()를 반복한다면 동시에 종료된 자식 프로세스들도 처리할 수있다.
또한 wait 대신 waitpid 를 사용하여 timeout을 짧게 가져가는 식으로 부모 프로세스의 concurrency도 보장할 수 있다. (WNOHANG 옵션으로 더이상 처리할 내용이 없으면 기다리지 않도록 할 수 있음)
my_function() { while ( waitpid(-1, 0, WNOHANG) > 0 ); }
하지만, SIGCHLD 시그널은, 자식 프로세스가 종료 되었을 때 뿐 아니라, 정지되었을 때도 호출된다. signal 설정 옵션으로 자식 프로세스가 종료되었을 때 날아오는 SIGCHLD 는 처리하지 않도록 설정해야 완벽하다.
- signal의 상위호환인
sigaction
함수를 사용하면 flag를 설정하여 처리 가능하다.
- signal의 상위호환인
Init Process
- init 프로세스는 고아 프로세스들을 모아서 종료시켜준다.
- init 프로세스는
socketpair()
을 사용하여 3번4번 entry에 socket을 하나씩 연다. 3번 socket은 4번 socket으로 pipeline이 연결되어 있다. - signal handler가 프로세스에서 고아 프로세스를 감지하면 3번 entry의 socket으로 데이터를 write 하면, init 프로세스의 4번 socket에서 데이터가 튀어나온다.
- 4번 socket에서 데이터를 받은 init 프로세스는 받은 데이터를 기반으로 고아 프로세스를 처리한다.
- init 프로세스는
- 이런 식으로 구조를 짜면, 커널에서 직접 프로세스를 처리하지 않고, init process가 프로세스 처리를 하도록 할 수 있다.
| signal handler | ---write()------↴ ↷
|[0] [1] [2] [3] [4]|
| init process ↙ |
| wait() |
| |
| |
| |
exec 함수
system()
이라는 라이브러리 함수로 커널 명령을 실행할 수 있다. 내부적으로 exec 함수들을 사용한다.
- exec뒤에 붙은 글자에 따라 인자로 받는 데이터의 형태나 종류가 달라지며, 여러 속성들을 합해서 사용 가능하다.
- exec함수들은
execve
를 제외하고는 모두 라이브러리이며, 최종적으로 execve를 호출한다.
l
: 리스트 형태의 인자를 받아 명령어 호출시 전달- ex)
execl("ls", "-l")
- ex)
v
: 벡터 형태의 인자를 받아 명령어 호출시 전달- ex)
char* cmd[] = {"/bin/ls", "ls", "-l", null}; // 파일 위치, 프로세스 이름, argument excecv(cmd);
- ex)
e
: 환경변수를 인자를 받아 명령어 호출시 전달- ex)
char* env[] = {"name=justin", "age=20", null}; excece(cmd);
- ex) v와e를 혼합해서 사용 가능
char* cmd[] = {"/bin/ls", "ls", "-l", null}; // 파일 위치, 프로세스 이름, argument char* env[] = {"name=justin", "age=20", null}; excecve(cmd, env);
- c에서 main 함수는
int main(int argc, char** argv, char** envp)
형태이다.- argc: 인자의 갯수
- argv: 인자의 배열
- envp: 환경변수의 배열
- ex)
p
: 환경변수 path를 참조하여 명령어 실행- exec 파일들은 기본적으로 path를 참조하지 않고 실행되어 명령어 파일의 절대경로를 인자로 넣어야 한다.
- p옵션이 붙은 함수를 사용하면 환경변수 path를 사용하여 명령어를 실행할 수 있다.
- ex)
execlp("ls", "ls", "-l", null)
쉘을 이용한 옵션처리
execlp(command, command, null);
형태로 실행하지 않고,execl("/bin/sh", "sh", "-c", command, null);
형태로 실행하면 ‘command’ 명령을 쉘이 실행하게 되어 옵션을 알아서 처리해 준다.
exec로 생성한 프로세스의 속성
- 상속되는 속성
- 파일 디스크립터
- 사용자 ID, 그룹 ID, 프로세스 그룹 ID, 세션 ID, 제어 터미널
- alarm 시그널 남은 설정시간
- 작업 디렉터리, root 디렉터리
- 파일 잠금 여부, 파일 생성 마스크
- 자원 제약, CPU 사용시간
- 상속되지 않는 속성
- signal의 처리는 SIG_IGN 처리되던 시그널 외에는 default로 복구된다.
- 유효 사용자 ID (파일 속성에서 set_user_ID 비트가 설정된 경우)
- 유효 그룹 ID (set_group_id 비트가 설정된 경우)
reference count
- exec를 사용하여 프로세스를 호출하면, 그 아래 line들은 실행이 되지 않는다.
- 프로세스는 코드 영역 메모리를 참조하고, 메모리는 reference count를 두어 몇개의 프로세스가 해당 메모리를 참조하는지 체크한다.
- 이때, exec를 사용하면 기존 프로세스는 code 영역을 내버려두고 exec에서 사용할 새로운 코드영역을 참조하게 된다. (+program counter 이동)
- 그렇게 되면 기존에 남아있던 코드 영역 메모리는 reference count가 0이되어 더이상 사용하지 않는 메모리로 취급되어 free된다.
-> fork와 exec를 함께 사용하면 exec 아래의 코드도 실행할 수 있게 할 수 있다.
...
pid = fork()
if (pid)
exec(...);
wait(0);
something_to_do(); // 부모 process에서 실행 가능
...
프로세스 그룹
- 하나 이상의 프로세스들의 집합을 프로세스 그룹이라 한다.
- 일관된 작업을 하는 프로세스들을 그룹으로 묶어서 관리하며, 이 그룹은 고유한 ‘프로세스 그룹 ID’ 를 갖는다.
- 프로세스 그룹에는 ‘프로세스 그룹 리더’ 가존재한다. 리더는 프로세스 그룹 ID와 동일한 값을 프로세스 ID로 가진 프로세스이다.
- 리더는 그룹 및 그룹내의 프로세스의 생성/종료 권한을 가진다.
- 리더가 종료되거나 그룹을 떠나면 해당 프로세스 그룹 내의 다른 프로세스가 리더 권한을 위임받는다.
- 그룹 내의 마지막 프로세스가 그룹을 떠나면 해당 그룹은 사라진다.
- pipe 명령으로 묶어서 한번에 실행한 명령들은 같은 프로세스 그룹에 묶인다.
그룹 제어 함수 getpgrp, setpgid
- getpgrp
- 호출한 프로세스가 속한 프로세스 그룹 ID를 리턴하는 함수
- setpgrid(PROCESS_ID, PROCESS_GROUP_ID)
- 새로운 프로세스 그룹을 생성하거나, 선택한 프로세스를 특정 그룹에 합류시키는 함수
- PROCESS_ID 와 PROCESS_GROUP_ID를 동일한 수를 넣어주면, 해당 프로세스를 리더로 승격시킨다.
- 자기 자신이나 자식 프로세스의 그룹ID만 변경 가능하며, exec를 수행한 자식 프로세스의 그룹ID는 접근할 수 없다.
세션
- 일반적으로 같은 터미널에서 수행되고 있는 프로세스 그룹들을 모은 집합을 session이라 한다.
- 세션, 프로세스 그룹, 프로세스 간 연관관계는
프로세스 ⊂ 프로세스 그룹 ⊂ 세션
형태이다. - 세션도 unique한 번호를 가지며 이를 세션ID라 한다.
- 세션ID와 동일한 프로세스ID를 가진 프로세스를 세션 리더라 한다.
- 한 세션의 프로세스들은 하나의 foreground 프로세스와 다수의 background 프로세스로 이루어져 있다. foreground 프로세스는 현재 키보드 input을 받을 수 있는 유일한 프로세스이다.
세션 제어 함수 setsid
- 새로운 세션을 생성하여 특정 프로세스를 해당 세션으로 이동하는 함수이다.
- 호출한 프로세스가 프로세스 그룹 리더가 아닌 경우에만 실행 가능하다.
- 프로세스 그룹에 프로세스가 하나밖에 없더라도, fork로 생성된 자식 프로세스는 그룹 리더가 아니기 때문에 setsid를 호출할 수 있다.
- 새로운 세션이 생성되면, 프로세스 그룹도 신규로 생성하고, 그 안에 프로세스를 이동시킨다. 이동된 프로세스는 세션 리더이자 프로세스 리더가 된다.
- setsid 명령은 제어 터미널을 갖지 않기 때문에 기존에 연결되어있던 터미널과의 연결이 끊기게 된다.
제어 터미널
하나의 세션은 하나의 제어 터미널을 가질 수 있다.
- 프로세스에서 가상 터미널은 ‘pts’, 실제 터미널은 ’tty’ 라 표시된다.
세션 리더는 제어 터미널과의 연결을 관할한다.
tcgetpgrp
: foreground 프로세스 그룹ID를 반환한다.tcsetpgrp
: 제어터미널을 갖고 있는 경우, 특정 프로세스 그룹을 전위 프로세스 그룹으로 설정한다.stty -a
명령으로 확인시 작업제어를 위한 시그널이 설정되어 있음을 확인할 수 있다.
Daemon Process
- 제어 터미널 없이 주기적으로 주어진 일을 처리하거나, 특정 이벤트를 대기하기 위해 background에서 돌고있는 프로세스
- 보통 시스템이 부팅될 때 시작되며, shutdown 될 때 종료된다.
- 다른 프로세스가 발생한 시그널에 간섭받지 않아야 한다.
- 데몬 프로세스는 터미널과 연결이 되어있지 않기 때문에 터미널로 입력/출력을 할 수 없다.
- 출력은
printf
대신syslog
를 사용하여 시스템 로그로 출력을 하도록 해야한다.
- 출력은
Daemon 구현 방법
fork()
로 child process를 생성하고 parent process를 종료한다.- child process에서
setsid()
함수로 새로운 세션을 생성한다.
- background로 실행 가능하며, 터미널에 영향을 받지 않게됨
- 열려진 모든 file descriptor를 닫는다.
- 1~64까지 index를 순회하며
close()
로 file descriptor를 모두 닫는다.
- 작업 디렉터리를 root("/")로 바꿔서 다른 파일 시스템의 unmount 동작에 영향을 주지 않도록 한다.
chdir("/");
로 작업 디렉터리를 옮길 수 있다.
- 파일 생성 마스크를 0으로 설정한다.
umask(0)
명령으로 umask값을 없애준다.- umask란, 프로세스에서 생성되는 파일의 파일 접근 권한 설정시 해당 값을 빼도록 설정된 값이다. 즉, 초기 파일 권한 666에서 umask값을 뺀 값이 생성된 파일의 권한이 된다. (디렉터리는 초기값 777)
- SIGCLD 시그널을 무시한다.
signal(SIGCHLD, SIG_IGN)
를 통해 자식이 발생하는 시그널을 무시하도록 설정해야 데몬이 생성한 프로세스들이 좀비 프로세스가 생성되지 않는다.- 직접 wait를 해서 처리 해줘도 된다.