📒 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다.
유닉스는 프로그램을 짜고 실행파일을 만들면 OS 차원에서 부가되는 프로그램의 레이아웃이 있음.
기본적으로 C언어를 사용함.
C언어는 main이라는 함수로 시작함.
int main(int argc, char* argv[])
// char* argv[] == char** argv
✔ argc
커맨드 라인에 명령어를 입력할 때의 인자의 개수.
✔ argv
문자열로 된 인자들의 포인터의 배열.
새로운 프로세스를 생성할 때, 반드시 exec 시스템 콜을 호출해야함.
exec 시스템 콜을 통해 프로세스를 생성하기 전에,
main이 실행되기 전 단계에서 실행되는 특별한 스타트업 코드가 있음.

쉘이 어떤 프로그램을 실행할 때는 main에서 시작하는 것이 아니라 스타트업 코드에서 시작함.
그 스타트업 코드에서 main 함수를 호출함.
이런 스타트업 루틴은 커널에서 알아서 처리해줌.
이 스타트업 루틴에서 중요한 것은
✔ command-line argument

위의 예시의 argc = 4.
argv는 다 문자열임.
./echoarg는 상대 경로명.
이러한 command-line argument 세팅을 스타트업 코드에서 해줌.
💡사용 예시

✔ environment list
사실 argc, argv 외에 envp라는게 하나 더 있음.
프로세스가 실행되는 환경변수.
int main(int argc, char* argv[], char* envp[])
💡대표적인 환경변수
HOME = /home/sar\0
PATH = :/bin:/usr/bin\0 : 명령어를 입력하면 그 명령어를 찾는 순서.
SHELL = /bin/bash\0
USER = sar\0
LOGNAME = sar\0
이 경로명들을 문자열로 해서 envp[]에 넣음.
main 함수에 envp 인자가 없어도 디폴트로 존재함.


C 프로그램이 메모리에 로드될 때, 갖고 있는 것들.
✔ Text(code) segment
프로그램 instruction이 실행되는 부분.
기계어 코드 부분.
✔ Initialized data segment
변수를 선언할 때 변수의 초기값을 세팅해준 것.
전역 변수와 static 변수에 초기값이 있는 경우.
프로그램에 그 값이 미리 저장됨.
int maxCount = 99;
✔ Uninitialized data segment(bss)
초기값이 없는 전역 변수와 static 변수.
int x;
long sum[1000];
✔ Stack
함수 내부의 지역변수들이 저장됨.
함수가 호출되면 메모리가 할당되어 스택의 top에 들어감.
함수가 리턴될 때, 변수들의 할당이 해제됨.
✔ Heap
프로그램을 실행할 때, 동적 할당하는 것들이 들어감.
프로그램을 컴파일해서 디스크에 실행 파일을 저장할 때, Uninitialized data segment, heap, stack은 저장할 필요 없음.
→ 동적으로 프로그램이 메모리에 올라올 때만 할당하는 것들.
디스크에 실행 파일을 저장할 때는 text segment와 initialized data segment만 저장됨.

dec : 총 사이즈를 10진수로.
hex : 총 사이즈를 16진수로.

init.c의 myarray는 초기화됨.
→ initialized data segment에 저장됨.
→ 디스크에 저장됨.
noinit.c의 myarray는 초기화 안됨.
→ bss에 저장됨.
→ 디스크에 저장 안됨.

ls를 통해 조회해보면, init.c는 디스크에 myarray가 저장되어 있기 때문에 사이즈가 큼.

사이즈를 보면 init.c와 noinit.c에 차이가 있음.
init은 data 부분이 크고, noinit은 bss 부분이 큼.
프로그램이 실행되고 있는 순간의 인스턴스.
프로그램은 한번 컴파일하면 고정되어 있음.
변하는 내용이 없음.
프로세스는 생성할 때마다 변수의 내용이 변함.
프로세스는 그 내용이 변함.
지역변수가 할당되었다 해제되었다 함.
프로그램 ≠ 프로세스
프로그램은 컴파일한 실행 파일이고,
프로세스는 프로그램이 메모리에 올라와서 실행되고 있는 인스턴스임.
프로세스에는 아래의 것들이 포함됨.
💡프로그램의 코드
💡변수가 갖고 있는 값
💡하드웨어 레지스터 값
💡프로그램 스택
프로그램에는 id가 없지만 프로세스에는 프로세스 아이디(pid)라는 번호를 붙여줌.
프로세스는 fork라는 시스템 콜을 통해 만들어짐.
프로세스에서 프로세스가 만들어짐.
쉘에서 명령어를 입력하면 단순히 문자열에 불과함.

쉘이 위의 문자열을 해석해서 fork와 exec이라는 시스템 콜을 이용해 cat이라는 프로세스를 만듦.

위의 명령에서 '|'를 '파이프'라고 함.
ls 명령과 wc 명령을 중간에 파이프해서 앞에 있는 프로세스의 출력을 뒤에 있는 프로세스의 입력으로 넣어줌.
✔ 프로세스의 실행 환경
유닉스의 모든 프로세스는 fork와 exec 시스템 콜을 이용해 다른 프로세스를 실행시킬 수 있음.
어떤 프로세스가 다른 프로세스를 생성하면 부모 프로세스와 자식 프로세스라는 계층적 구조가 생성됨.
이 과정이 반복되면서 프로세스간에 계층적인 트리 구조가 생성됨.
파일 시스템의 디렉토리와 비슷함.
맨 꼭데기에, 한 개의 가장 중요한 컨트롤 프로세스가 있음.
'init'이라는 os의 프로세스.
모든 프로세스는 init 프로세스로부터 나옴.
init은 프로세스 계층의 루트.
✔ 프로세스에 관련된 시스템 콜
fork, exec, wait, exit

fork 시스템 콜로 프로세스를 생성하기 전에 프로세스의 pid를 알아내는 시스템 콜.
getpid는 호출한 프로세스의 pid를 리턴함.
getppid는 호출한 프로세스의 부모 프로세스 pid를 리턴함.
✔ 프로세스 아이디(pid)
음이 아닌 정수임.
시스템에서 유니크함.
유니크하지만 재사용됨.
프로세스가 죽어서 pid가 free해지면 다른 프로세스의 pid로 사용할 수 있음.

자식 프로세스를 만들어주는 시스템 콜.
호출한 프로세스와 아주 똑같은 프로세스를 만들어줌.
코드, 실행되었을 때의 변수와 그 값이 부모 프로세스와 자식 프로세스에서 똑같음.
그러나 부모 프로세스와 자식 프로세스는 전혀 별개의 프로세스.
똑같은 another 프로세스가 만들어진 것.


리턴 값이 굉장히 중요함.
리턴 값이 0이면 만들어진 자식 프로세스에서 실행되는 것.
리턴 값이 0이 아닌 값이면 부모 프로세스에서 자식 프로세스의 pid가 리턴된 것.
리턴 값이 -1이면 에러.
부모 프로세스가 fork를 호출하면 자식 프로세스가 생성됨.
자식 프로세스는 부모 프로세스의 완전한 복제본이라고 볼 수 있음.
→ data space, heap, stack 등의 모든 내용이 똑같이 복사됨.
부모 프로세스와 자식 프로세스는 메모리 공간을 공유하지는 않음. 전혀 별개임.
그런데 부모 프로세스에서 fork한 직후의 상태가 자식 프로세스가 생성되었을 때의 상태와 똑같음.
부모 프로세스와 자식 프로세스가 공유하는 부분은 있음.
→ text segment : 프로그램 코드 부분.
프로그램 코드는 공유해도 아무 관계 없게 되어있음.
부모 프로세스가 자식 프로세스를 만드는 순간 프로세스가 하나 더 생김.
어느 것이 먼저 실행되는지는 알 수 없음.
→ os의 스케쥴링 알고리즘이 알아서 해줌.

프로세스 A가 부모 프로세스, B가 자식 프로세스라고 하면,
→ text 부분은 같음.
→ data 부분은 서로 겹치면 안됨.
→ stack과 heap도 겹치면 안됨.

부모 프로세스가 fork를 실행하면 똑같은 코드를 수행하는 프로세스가 두 개가 됨.
부모 프로세스는 fork 수행 이후에 fork의 리턴 값을 pid에 저장하는 것부터 시작함.
자식 프로세스는 fork를 수행한 적이 없음.
→ pid에 값을 세팅하는 것부터 시작함.
부모 프로세스의 pid 값과 자식 프로세스의 pid 값이 다름.
자식 프로세스에서의 pid 값은 0임.
부모 프로세스에서의 pid 값은 0보다 큰 정수로, 자식 프로세스의 pid 값임.


부모 프로세스와 자식 프로세스가 똑같은 코드를 수행함.
그런데 fork 이후에 조건문을 통해 pid 값을 확인해서 자식 프로세스가 실행하는 부분과 부모 프로세스가 실행하는 부분이 명확하게 구분됨.
이를 통해 자식 프로세스는 어떤 작업을 하고, 부모 프로세스는 어떤 작업을 하는지를 나눌 수 있음.
fork를 이용해 자식 프로세스를 생성하면,
자식 프로세스는 부모 프로세스와 똑같은 코드를 실행하는 거의 99% 같은 프로세스가 됨.
그런데 우리가 원하는 것은 자식 프로세스가 부모 프로세스와 똑같은 프로그램을 실행하는 것이 아님.
부모 프로세스와 다른 작업을 하는 자식 프로세스를 만들고 싶으면?
→ fork해서 리턴된 pid 값을 통해 부모 프로세스가 수행하는 부분과 자식 프로세스가 수행하는 부분을 구분해서 작성할 수 있음.
그런데 조금 더 일반적으로, 부모 프로세스가 자식 프로세스를 만들어서 "전혀 다른 프로그램"을 실행하기를 원하면?
→ 이때 사용하는 것이 exec이라는 시스템 콜.
exec 시스템 콜은 자식 프로세스가 부모 프로세스와 다른 새로운 프로그램을 실행하도록 할 때 사용됨.
자식 프로세스가 생성될 당시에는 부모 프로세스와 똑같은 코드를 실행함.
그런데 exec를 호출함으로써 자식 프로세스가 실행하는 코드를 싹 바꿔버림.
자식 프로세스가 자신이 다른 프로그램을 실행하는 프로세스가 되도록 스스로를 변신시킴.
→ 부모 프로그램을 실행하지 않고, 전혀 다른 새로운 프로그램을 실행하도록 자신을 변신시킴.
자식 프로세스는 fork를 통해 생성되고, exec는 자기 자신을 변신시키는 것에 불과하기 때문에 프로세스의 아이디는 바뀌지 않음.
fork로 자식 프로세스가 만들어질 당시의 pid가 그대로 유지됨.
exec 시스템 콜을 호출하면 새로운 프로세스가 생성되는 것이 아니고, 자신이 실행할 프로그램 코드를 새로운 것으로 싹 바꾸는 것.
exec 시스템 콜은 단순히 현재 프로세스가 실행하는 코드, 데이터, 힙, 스택 등을 새로운 프로그램의 것으로 대체해줌.

exec 시스템 콜에는 6가지 버전이 있음.
실제로는 execve가 시스템 콜이고, 나머지 5개는 쓰기 편하게 라이브러리 형태로 변형한 것.

exec 뒤에 'l'이 들어가면 argv에 값들이 나열되어 들어옴
→ arg0, arg1, arg2, ...
exec뒤에 'v'가 들어오면 argv에 들어갈 값들이 배열로 주어짐
'l'과 'v' 뒤에 'e'가 붙으면 argv 뒤에 environment 배열이 들어감.
'p'가 붙으면 pathname 대신 filename을 받음.
→ 함수가 filename 인자를 취할 때, 실행파일을 찾기 위해 PATH 환경 변수를 사용함.

PATH 환경변수가 위와 같으면,
filename에 해당하는 실행파일을 /bin에서 먼저 찾음.
못찾으면 /usr/bin에서 찾음.
못찾으면 /usr/local/bin에서 찾음
못찾으면 . (현재 디렉토리)에서 찾음.
거기도 없으면 에러가 발생함.

위의 runls 프로그램은 excl을 실행하는 순간 ls를 실행하는 프로그램으로 변신함.
프로세스는 변하지 않고 그대로임.
그런데 프로세스가 실행하는 프로그램이 ls로 바뀜.
ls 프로그램의 실행이 다 끝나더라도 runls 프로그램으로 다시 돌아오지 않음.
그냥 ls를 실행하고 끝내는 것.
위의 예시에서 excl 아래에 작성된 코드들은 excl의 호출이 실패한 경우에 대한 코드들.
excl에 성공하면 다시 돌아오지 않음.


execv이기 때문에 argv를 배열로 묶어서 넣음.
runls2를 수행하는 프로세스가 execv가 호출되면 ls 프로그램을 수행하게 변신함.

p가 붙으면 인자로 pathname 대신 filename이 들어감.
myecho에서 argc = 3.
argv[0] = "myecho"
argv[1] = "hello"
argv[2] = "world"

fork의 리턴값인 pid를 통해 부모 프로세스와 자식 프로세스를 구분함.
자식 프로세스에서는 exec를 호출하고, 부모 프로세스에서는 wait를 호출함.
부모 프로세스는 wait 시스템 콜을 통해 자신이 생성한 자식 프로세스가 종료될 때까지 기다림.
자식 프로세스가 수행을 종료하면 wait에서 깨어나서 다음을 실행함.


쉘에서 명령어를 입력하면 쉘이 fork 해서 자식 프로세스를 만듦.
생성된 자식 프로세스는 부모 프로세스인 쉘의 코드를 실행함.
exec를 통해 자식 프로세스가 수행할 프로그램을 입력된 커맨드로 바꿔줌.
그리고 부모 프로세스인 쉘은 자식 프로세스가 끝날 때까지 기다림.
💡예시

✔ sh -c "명령어"

쉘이 fork 해서 자식 프로세스 쉘을 만듦.
자식 프로세스 쉘이 손자 명령어 프로세스를 만듦.
fork 해서 자식 프로세스가 만들어지면 자식 프로세스는 부모 프로세스의 모든 속성과 데이터를 상속받음.
부모 프로세스가 open한 모든 파일들도 자식 프로세스에서 open되어 있음.

자식 프로세스가 생성되면, 부모 프로세스로부터 독립적이기 때문에 table entry가 따로 만들어짐.
그러나 부모와 똑같은 값을 가져 같은 file table을 가리킴.
그리고 file offset 마저도 똑같음.
왜냐하면 file offset은 file table에 있기 때문.
자식 프로세스가 출력하면 부모 프로세스의 file offset이 업데이트 됨.
💡예시

대부분의 속성이 부모 프로세스로부터 자식 프로세스로 상속됨.
그러나 부모 프로세스와 자식 프로세스에서 다른 값들이 존재함.
💡fork의 리턴 값.
💡프로세스의 아이디(pid)
💡부모 프로세스의 id. 자식의 부모가 있고, 부모의 부모가 있음.
💡프로세스의 실행 시간을 측정하는 변수들(tms_utime, tms_stime, tms_cutime, tms_cstime).
→ 부모 프로세스가 자식 프로세스보다 먼저 생성되어서 실행 시간이 더 김.
💡File locks
💡pending alarm은 clear 됨.
fork 이후에 자식 프로세스는 부모 프로세스가 open한 file descriptor를 그대로 상속 받음.
자식 프로세스가 exec를 통해 변신해도 부모 프로세스가 open 했던 file descriptor는 그대로 갖고 있음.
fork와 exec를 통해 새 프로그램으로 변신해도 원래의 부모 프로세스가 open 했던 file descriptor를 그대로 유지함.
이것이 문제가 될 수 있음.
이를 해결하기 위한 것이 'close-on-exec flag'.
자식 프로세스가 exec를 실행하면 부모 프로세스가 open한 file descriptor를 자동으로 close 하도록 close-on-exec flag를 통해 컨트롤 할 수 있음.
close-on-exec flag가 off이면, 자식 프로세스가 exec를 해도 file descriptor가 close 되지 않음.
close-on-exec flag가 on이면, 자식 프로세스가 exec 할 때 부모 프로세스가 open한 file descriptor가 자동으로 close 됨.
close-on-exec flag의 디폴트 값은 off.
💡close-on-exec flag를 on 해주는 프로그램 예시

✔ Normal termination(정상 종료)
💡메인 함수에서 리턴되어서 종료
💡메인이 아니더라도 어디에서든지 exit을 호출해서 종료
💡_exit 또는 _Exit을 호출해서 종료
✔ Abnormal termination(비정상 종료)
💡abort 시스템 콜을 호출해서 종료
💡시그널을 받고 종료

exit은 라이브러리.
_exit이 시스템 콜.
exit에 들어가는 인자인 status를 프로세스의 exit status라고 함.
종료되는 상태를 status 변수에 저장함.
부모 프로세스는 자식 프로세스가 종료할 때까지 wait하면서 기다리고 있음.
wait(&status) exit(status)
자식 프로세스에서 status 값이 부모 프로세스의 status 값으로 전달이 됨.
관습적으로 프로그램이 종료할 때, 정상적으로 잘 수행했다면 return이나 exit에 0을 전달함.
실행했는데 뭔가 문제가 있다면 0이 아닌 값을 전달함.
exit은 라이브러리임.
종료하는것 이외에 추가적으로 clean up 작업을 함.
open 했는데 close 하지 않은 것들을 전부 닫아주고,
메모리를 할당했는데 해제하지 않은 것들을 해제해줌.
이러한 작업을 추가적으로 해주고 _exit을 호출함.

스타트업 코드에 exit이 있기 때문에, 메인에서 return이나 exit 없이 끝나도 exit이 반드시 실행됨.
exit의 실행 과정을 보자.
유저 프로그램이 exit을 호출하면 먼저 클린업을 하고 나서 _exit 시스템 콜을 호출해 프로세스가 죽음.
메인 함수에서 exit을 호출해도 죽고, 메인 함수에서 다른 함수를 호출한 다음에 거기서 exit을 호출해도 죽음.
exit 라이브러리 프로그램은 종료하기 전에
먼저 자기 자신의 클린업 작업이 있고, 등록한 exit handler가 있음.
이러한 클린업 작업을 다 하고 나서 _exit 시스템 콜을 호출함.
그냥 _exit 시스템 콜만 호출하면 클린업 없이 종료함.

exit handler라고 불림.
exit을 실행하면 clean up을 먼저 하고 _exit 시스템 콜을 호출함.
atexit은 exit을 실행할 때, clean up 할 것으로 등록시켜줌.
exit 함수가 호출되었을 때, 등록된 역순으로 함수들이 실행됨.
최대 32개까지 등록할 수 있음.


💡예시 : 쉘의 '$' 프롬프트
쉘은 명령어가 입력되면 그 명령어를 수행하는 자식 프로세스를 만듦.
쉘이 fork해서 자식 프로세스가 생성됨.
자식 프로세스는 exec로 해당 명령어가 실행되는 프로세스로 변신함.
자식 프로세스는 수행을 마치면 exit함.
자식 프로세스가 수행될 동안 부모 프로세스는 wait함.
쉘은 명령어 수행이 끝날 때까지 다음 '$' 프롬프트가 나오지 않음.
커맨드의 수행이 끝나면 다음 프롬프트가 나옴.
이것도 일종의 synchronizing.
정상 종료든 비정상 종료든 자식 프로세스가 종료될 때 부모 프로세스가 깨어남.
커널은 크 프로세스에서 open한 모든 descriptor를 close 함.
그 프로세스가 사용했던 모든 메모리를 해제함.
프로세스 엔트리에는 pid, status, cpu time 등이 저장됨.
이때, status 값은 부모 프로세스에 전달되고 난 다음에 clear 되어야 함.
그래서 부모 프로세스가 자식 프로세스가 종료된 것을 확인할 때까지 프로세스 엔트리의 정보가 남아 있음.
종료된 프로세스의 부모 프로세스는 wait 시스템 콜을 통해 confirm 함.
부모 프로세스가 wait 해줘야 종료하는 자식 프로세스의 프로세스 엔트리가 프로세스 테이블에서 clear 됨.

💡인자로 갖는 int* statloc
자식 프로세스가 exit할 때 넘겨주는 값을 받기 위함.
💡wait의 리턴 값이 pid_t인 이유
자식 프로세스가 여러개일 때, 어떤 자식 프로세스가 종료된 것인지를 알기 위함.
따라서 리턴 값은 종료된 자식 프로세스의 pid 값.
자식 프로세스가 수행될 동안 부모 프로세스는 wait를 통해 잠시 중단됨.
여러 자식 프로세스들 중에 어느 하나라도 종료되면 부모 프로세스는 wait에서 깨어남.
만약 wait가 -1을 리턴한다면 자식 프로세스가 없을 때 호출에 실패한 것.
자식 프로세스가 여러개인 경우 반복문을 이용해 wait를 여러번 하면 됨.
자식 프로세스는 종료되는 순서대로 리턴됨.

부모 프로세스는 반복문을 통해 fork만 3번 함.
3개의 자식 프로세스는 각각 sleep과 exit만 수행함.
부모 프로세스는 wait를 3번 수행함.
각각의 자식 프로세스가 종료될 때마다 wait가 깨어남.

wait에 인자로 NULL이 들어가면 자식 프로세스가 종료된 값을 전달받지 않겠다는 의미.
wait의 리턴 값은 방금 종료된 자식 프로세스의 pid 값임.
위의 예시에서는 3번까지 -1이 아니니까 true.
그런데 4번째 wait를 실행하면 자식 프로세스가 없는데 wait를 실행했으니까 -1이 리턴됨.
💡사용 예시

예를 들어 아래와 같이 종료된 경우,
exit(5);
wait(&status)의 status에는 exit의 값인 5만 들어있는 것이 아니고 어떻게 종료되었는지를 비롯해 여러가지 값들이 더 있음.
그래서 5만 끄집어낼 수 있는 매크로가 준비되어 있음.
WIFEXITED(status)
// wait if exited
// 자식 프로세스가 정상 종료했다면 true, 비정상 종료했다면 false
WEXITSTATUS(status)
// exit의 값만 끄집어내서 리턴해줌.

자식 프로세스가 여러개 있을 때, wait 시스템 콜을 사용하면 아무거나 먼저 종료하는 자식 프로세스가 나타나면 wait에서 깨어남.
어떤 자식 프로세스가 종료해서 깨어난 것인지는 리턴 값으로 알 수 있음.
waitpid 시스템 콜은 기다리는 자식 프로세스를 지정할 수 있음.
pid로 지정한 자식 프로세스가 종료했을 때 깨어나는 것이 waitpid.
인자로 넘기는 pid 값에 따라 각각 경우가 다름.
💡pid == -1
특정 pid를 지정하지 않음.
아무 자식 프로세스나 기다림.
wait하고 똑같음.
💡pid > 0
특정 pid를 지정한 것.
pid 값에 해당하는 자식 프로세스의 종료를 기다림.
💡pid == 0
부모 프로세스가 속한 그룹 아이디와 같은 그룹 아이디를 가진 자식 프로세스 중 아무거나 기다림.
💡pid < 0(pid ≠ -1)
pid 값을 양수로 주면 그 값에 해당하는 자식 프로세스를 찾음.
pid 값을 음수로 지정해줌으로써 그 절대값과 같은 프로세스 그룹 아이디를 가진 자식 프로세스를 기다림.
💡int options
waitpid의 마지막 인자.
WNOHANG : Wait No HANG
기다리지 말라는 옵션.
waitpid 함수는 만약에 pid로 지정된 자식 프로세스가 즉각 available 하지 않으면 기다리지 않음.
→ 곧 바로 리턴함.
available 함?
= 자식 프로세스가 exit해서 status 값을 부모 프로세스에게 넘겨줌.
waitpid를 실행할 당시에 자식 프로세스가 계속 수행중이라면, 원래는 기다려야하는데 기다리지 않고 곧바로 리턴함.
→ 문제가 생김.
자식 프로세스가 실행 도중에 리턴했는지, exit해서 리턴했는지 리턴 값을 통해 구분함.
자식 프로세스가 실행 도중에 리턴되었다면 waitpid의 리턴 값은 0.
자식 프로세스가 exit 되어서 리턴되었다면 waitpid의 리턴 값은 자식 프로세스의 pid.
이 옵션을 왜 쓰는가?
부모 프로세스가 자식 프로세스를 무조건 wait 하면 자식 프로세스가 수행되는 동안 부모 프로세스는 아무것도 못함.
자식 프로세스가 수행되는 도중에도 부모 프로세스가 어떤 작업을 해야한다면 일단 자식 프로세스의 상태를 한 번 체크해 보고 부모 프로세스가 할 일을 하자는 취지.
이 옵션을 줘서 리턴 값이 0임이 확인되면 자식 프로세스가 아직 수행중인 것.
→ 그때 필요한 작업을 함.
그런데 리턴 값이 자식 프로세스의 pid가 나오면 자식 프로세스가 종료된 것.
→ 부모 프로세스는 자식 프로세스가 종료했을 때 필요한 작업을 함.
자식 프로세스가 수행중일때도 부모 프로세스가 기다리지 않고 필요한 작업을 하게 해주는 옵션
💡사용 예시

wait는 무작정 기다림.
자식 프로세스가 종료될 때까지 부모 프로세스를 무조건 block 시킴.
waitpid는 block을 방지하는 옵션이 있음.
부모 프로세스가 기다리지 않고 작업을 수행할 수 있는 여유를 줌.
✔ 정상 종료
WIFEXITED(status)
WEXITSTATUS(status)
✔ 비정상 종료
시그널을 받고 비정상 종료할 수 있음.
status에는 시그널 번호도 들어있음.
어떤 시그널을 받고 종료했는지 알아보는 방법
WIFSIGNALED(status)
WTERMSIG(status)
WCOREDUMP(status) // 덤프 되었는지를 알아보는 매크로
✔ 이외의 경우
stop 시그널을 받아 프로세스가 멈추는 경우가 있음.
프로세스가 stop 시그널을 받으면 잠시 실행을 중단함.
종료된 것은 아님.
어떤 시그널을 받아 중단된 것인지를 알아보는 방법
WIFSTOPPED(status)
WSTOPSIG(status)
멈춰있다가 재개되는 경우가 있음.
WIFCONTINUED(status)
💡사용 예시


프로세스가 exit해서 죽었는데도 프로세스 목록을 보면 나타나는 것.
실행을 종료했음에도 불구하고 프로세스 테이블의 엔트리에 남아있는 것.
부모 프로세스가 자식 프로세스의 종료를 wait 하다가 exit status 값을 전달 받으면 자식 프로세스가 깨끗하게 종료됨.
그런데 자식 프로세스가 종료되었을 때 부모 프로세스가 wait를 하지 않고 있으면 자식 프로세스가 종료하면서 남긴 status 값을 없앨 수 없음.
그러면 자식 프로세스는 좀비 프로세스가 됨.
좀비 프로세스는 부모 프로세스가 wait나 waitpid를 호출할 때까지 기다림.
부모 프로세스가 wait를 호출해주면 그때서야 사라짐.
자식 프로세스가 exit하고 죽으면 SIGCHILD라는 시그널을 부모에게 보내줌.
이때, wait를 하고 있으면 그 시점에 wait가 끝남.
💡사용 예시


자식 프로세스가 죽기 전에 부모 프로세스가 먼저 죽은 경우.
부모 프로세스가 죽으면 자식 프로세스는 고아 프로세스가 됨.
init 프로세스가 주기적으로 고아 프로세스를 찾아서 입양함.
init 프로세스가 주기적으로 wait를 호출해 좀비가 된 프로세스들을 제거해줌.
💡사용 예시


프로세스의 가장 중요한 속성.
프로세스 아이디에도 여러가지가 있음.
💡프로세스 아이디(pid)
💡부모 프로세스 아이디(ppid)
💡프로세스의 real 유저 아이디(uid)
💡프로세스의 effective 유저 아이디(euid)
💡프로세스의 real 그룹 아이디(gid)
💡프로세스의 effective 그룹 아이디(egid)
프로세스 아이디 1번은 모든 프로세스의 조상인 init 프로세스.
필요 목적에 따라 프로세스를 그룹으로 묶을 수 있음.
그리고 묶여진 프로세스들의 그룹에 대해 아이디를 붙임
→ "프로세스 그룹 아이디"
여기서의 그룹 아이디는 순수한 프로세스들에 붙여진 숫자.
프로세스의 real gid, effective gid와는 다름.
모든 프로세스에는 유니크한 pid가 있음.
그리고 모든 프로세스는 자신이 속한 그룹이 있음.
프로세스 그룹은 동일한 작업을 하는데 연관이 되어 있는 하나 이상의 프로세스들.
ex) 파이프로 데이터를 주고 받는 두 개의 프로세스.
각각의 프로세스 그룹은 유니크한 그룹 아이디를 가짐.
프로세스 그룹 아이디는 프로세스 아이디하고 유사함.
→ 양의 정수
→ pid_t 데이터 타입.
프로세스 그룹 안의 여러 프로세스 중 하나가 그룹 리더가 됨.
그룹 리더의 프로세스 아이디가 프로세스 그룹의 아이디가 됨.
✔ 관련 시스템 콜

💡getpgrp
호출한 프로세스의 프로세스 그룹 아이디를 리턴해줌.
💡getpgid
pid로 지정한 프로세스의 그룹 아이디를 리턴해줌.
pid에 0을 넣으면 getpgrp와 같음.
부모 프로세스가 fork 해서 자식 프로세스를 만들 때 프로세스 그룹 아이디가 상속됨.
아무 프로세스나 다른 프로세스의 프로세스 그룹 아이디를 바꿀 수는 없음.
부모 프로세스는 자식 프로세스의 프로세스 그룹 아이디를 바꿀 수 있음.
자식 프로세스도 자신의 프로세스 그룹 아이디를 바꿀 수 있음.
오로지 자기 자신과 자신이 만든 자식 프로세스의 프로세스 그룹 아이디만 변경할 수 있음.
부모 프로세스가 자식 프로세스의 프로세스 그룹 아이디를 바꾼다는 것은 다른 그룹으로 독립시키는 것.
자식 프로세스가 스스로의 프로세스 그룹 아이디를 바꾼다는 것은 스스로 그룹을 독립해서 나가는 것.
부모 프로세스가 강제적으로 자식 프로세스를 독립시킬 수도 있고, 자식 프로세스가 스스로 부모 프로세스로부터 독립할 수도 있음.
그런데, 자식 프로세스가 exec를 통해 변신한 이후에는 부모 프로세스라고 하더라도 자식 프로세스의 프로세스 그룹을 변경할 수 없음.
✔ setpgid

pid로 주어진 프로세스의 프로세스 그룹 아이디를 pgid로 변경하라.
인자로 주어지는 pid와 pgid의 값에 따라 다른 의미를 가짐.

💡pid == pgid
pid가 가리키는 프로세스의 프로세스 그룹 아이디를 그 프로세스의 pid로 변경함.
즉, pid가 가리키는 프로세스가 새로 만들어진 그룹의 리더가 됨.
💡pid == 0, pgid ≠ 0
pid가 0인 프로세스는 원래 없음.
setpgid에서 pid를 0으로 주면, 자기 자신을 의미함.
setpgid를 호출한 프로세스가 자기 자신의 프로세스 그룹 아이디를 주어진 pgid 값으로 변경함.
자식 프로세스가 스스로 독립을 선언하는 것.
💡pgid == 0
프로세스 그룹 아이디 0은 없음
setpgid(0, 0)
자기 자신의 프로세스 그룹 아이디를 자신의 pid 값으로 줘라.
자식 프로세스가 새로운 그룹을 만들어 자기가 스스로 리더가 됨.
세션은 프로세스 그룹보다 더 큰 차원의 그룹.
하나 이상의 프로세스 그룹들이 모여있는 것.
✔ 세션이란?

터미널에서 로그인을 하면 $ 프롬프트가 뜸.
로그인을 처리하는 프로세스가 fork해서 쉘을 만드는 것.
쉘 프로세스에서 명령어를 입력해 자식 프로세스를 생성함.
그 프로세스들의 표준 입력은 키보드, 표준 출력은 터미널의 스크린이 됨.
그 터미널에서 만들어진 모든 명령들의 표준 출력은 다 그 터미널의 스크린.
쉘에서 만들어지는 모든 프로세스는 표준 입력과 표준 출력이 같음.
세션 안에 있는 모든 프로세스는 표준 입력과 표준 출력이 같음.
같은 세션 내에서 foreground 프로세스 그룹은 오직 한 개만 존재함.
background 프로세스 그룹은 여러개가 존재할 수 있음.
세션 안에 있는 프로세스 그룹들은 한 개의 foreground 프로세스 그룹과 한 개 이상의 background 프로세스 그룹으로 나뉘어질 수 있음.
✔ 세션 리더와 세션 아이디
세션에서 최초로 생성된 쉘은 프로세스 그룹의 리더이자 세션의 리더임.
세션 아이디는 세션 리더의 pid임.
쉘은 pid = pgid = sid.
✔ getsid

pid로 지정된 프로세스의 세션 아이디를 리턴함.
만약에 인자로 넘어가는 pid 값이 0이라면 getsid를 호출한 자기 자신을 의미함.
✔ 데몬 프로세스(Daemon process)
controlling terminal을 갖고 있지 않은 프로세스.
프로세스가 스스로 그룹을 만들어 독립해서 나갈 수 있었던 것처럼, 프로세스가 자기 자신을 세션 독립할 수 있음.
독립한 프로세스의 pid = pgid = sid로 모두 같아짐.
세션 안에 있는 모든 프로세스들은 그 세션에 연결된 controlling terminal을 공유함.
그런데 세션 독립을 해버리면 controlling terminal을 사용할 수 없게 됨.
이걸 데몬 프로세스라고 함.
세션 리더인 쉘이 exit해서 로그아웃을 하면, 세션 리더가 세션을 종료하는 것.
세션 리더가 세션을 종료하면 그 세션에서 만들어진 모든 프로세스가 다 죽어버림.
그런데 세션 독립을 해서 나간 프로세스는 이전의 세션과 무관하기 때문에 이전 세션의 리더인 쉘이 exit해서 세션을 종료해도 죽지 않고 시스템에 데몬 프로세스로 남음.
세션에서 controlling terminal 없이 영원히 도는 프로세스.
✔ setsid

세션 독립을 할 때 자기 자신의 세션 아이디를 바꾸는 명령.
setsid를 통해 자기 자신의 세션을 독립시키면
1. 스스로 세션 리더가 되고,
2. 스스로 새로 만들어진 프로세스 그룹의 리더가 되고,
3. 그 프로세스는 controlling terminal을 상실함.

쉘이 처음에 만들어지면, 쉘의 pid = pgid = sid가 됨.
쉘은 세션의 리더, 프로세스 그룹의 리더가 됨.

쉘에서 fork해서 자식 프로세스를 만들면 pid는 다르지만 pgid와 sid는 같음.

프로세스가 setpgid를 통해 프로세스 그룹으로부터 독립하면 새로운 프로세스 그룹이 만들어짐.
setpgid(pid, 0)
부모가 자식 프로세스를 독립시키는 명령

setpgid(0, 0)
자식 프로세스가 스스로 프로세스 그룹에서 독립하는 명령

프로세스가 스스로 세션 독립을 하고 싶어서 setsid를 호출함.
setsid()
그 프로세스가 세션 독립을 하면서 새로운 세션이 만들어짐.
새로 만들어진 세션과 프로세스 그룹 아이디는 독립한 프로세스의 pid로 설정됨.

이때, 기존 세션에 연결된 controlling terminal과 단절됨.
표준 입력도 없고 표준 출력도 없는 데몬 프로세스가 됨.

current root 디렉토리를 변경하는 명령.
💡사용 예시

프로세스에는 우선순위가 있음.
이 우선순위를 nice value로 줌.
0부터 보통 8까지(시스템에 따라 다름)
숫자가 높을수록 우선순위가 낮은 것.
→ 0이 우선순위가 가장 높음.
우선순위가 높은 것에 cpu를 우선적으로 사용함.