모든 Process는 고유 식별값인 PID를 가지고, 부모 Process의 PID인 PPID가 기록되기도 한다.
fork & exec 시스템 호출을 통해 System booting이 일어난다.
Linux에서 system booting 과정
swapper(Scheduler Process)
pid 0
Kernel 내부에서 만들어진 process
Process Scheduling을 담당한다
init(Initailization Process)
pid 1
/etc/init 혹은 /sbin/init에 존재
/ect/inittab 파일에 기술된 대로 system을 초기화
해당 파일 내에서 /etc/rc* 로 시작하는 script 실행
모든 Process의 조상
page daemon(Memory Control Process)
pid 2
메모리 관리 전용 프로세스
...
getty process
로그인 과정 진행
로그인 prompt를 내고 키보드 입력 감지
/bin/login Process 실행
login process
/etc/passwd 참조하여 사용자의 아이디 및 패스워드 검사
/bin/sh Process 실행
shell process
시작 파일을 실행한 후에 prompt를 내고 명령어 입력 대기
터미널 창에서 깜빡깜빡 거리는 단계가 이 시점
📌 fork & exec
1️⃣ fork
자기 복제 : pid와 lock 같은 일부를 제외하고, 모든 정보(fd table)를 복제한 Child Process를 생성한다
Parent와 같은 fd를 공유하므로, 같은 파일 오프셋을 공유하며, 출력 시 서로 뒤섞이게 된다.
fork()의 반환값, PID, 파일 잠금(lock) 속성 등은 복제되지 않는다.
새로운 Process를 생성하는 유일한 방법
병행적으로 실행하며, 순서는 CPU Scheduler가 결정한다.
✨ 예제 코드
#include <stdlib.h>
#include <stdio.h>
/* 부모 프로세스가 자식 프로세스를 생성하고 서로 다른 메시지를 프린트 */
int main()
{
int pid;
pid = fork();
if (pid ==0) { // 자식 프로세스
printf("[Child] : Hello, world pid=%d\n“, getpid());
}
else { // 부모 프로세스
printf("[Parent] : Hello, world pid=%d\n", getpid());
}
}
[Parent] : Hello, world pid=15065
[Child] : Hello, world pid=15066
Process 내에서 새로운 프로그램을 실행시키는 유일한 방법 (보통 fork → exec)
exec은 더 이상 리턴할 곳이 없으므로, 성공한 exec()은 절대 리턴하지 않는다. (실패 시 -1)
✨ 예시 코드
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/* 자식 프로세스를 생성하여 echo 명령어를 실행한다. */
int main( ) {
int pid, child, status;
printf("부모 프로세스 시작\n");
pid = fork();
if (pid == 0) {
execl("/bin/echo", "echo", "hello", NULL);
fprintf(stderr,"첫 번째 실패");
exit(1);
} else {
child = wait(&status);
printf("자식 프로세스 %d 끝\n", child);
printf("부모 프로세스 끝\n");
}
}
부모 프로세스 시작
hello
자식 프로세스 15066 끝
부모 프로세스 끝
3. Thread
📌 Process VS. Thread
실행의 단위, Process를 구성하는 실행 흐름 단위
Stack 영역을 제외한 memory 공간을 공유한다
Thread는 실행에 필요한 최소한의 정보(Thread ID, Promgram counter를 포함한 register 값, Stack)를 가진다
그 외는 모두 Process 자원을 공유한다.
Process 보다 Context Switch가 간단하며, 병렬적으로 일을 수행한다.
✨ 예시 코드
#include <pthread.h>
//return 0 when success, return other values when fail
int pthread_create (pthead_t * thread,
pthread_attr-t * attr,
void * (* start_routine) (void *),
void * arg);
state = pthread_create(&t_id, NULL, thread_function, NULL);
thread : thread's id가 저장될 변수 포인터 (일꾼 식별)
attr: 생성될 thread 속성 (일반적으로 NULL)
start_routine : return 값과 매개변수가 void *인 함수 포인터
arg : 매개변수 값이 존재하면 인자 제공
✨ join
#include <pthread.h>
//return 0 when success, return other values when fail
int pthread_join(pthead_t * th, void **thread_return);
thread를 생성하고 join을 해주지 않으면 Process가 종료될 때 thread 작업 완료 여부와 상관없이 함께 종료된다.
join을 하면 Process가 Thread의 작업이 끝날 때까지 종료하지 않고 대기한다.
📌 Multi Process VS. Multi Thread
Multi-Process
모든 Process는 독립적이어야 한다
정보 교환을 위해서는 반드시 Kernel을 통해야 한다
Process 상태 변화를 위한 Context switch overhead 발생
Multi-Thread
하나의 Process 내에서 자원을 공유하므로 Context swithch overhead 감소
중복된 데이터가 memory에 쌓이지 않으므로 가볍다
다수의 Thread가 공유 자원에 동시에 접근할 경우 원치 않은 결과가 발생할 수 있다. (실행 순서 절대 예측 불가능)
임계 영역에 대한 내용은 뒤에 동기화에서 다시 다루는 거 같으므로 추가적으로 정리하지는 않았다.
💡 copy on write 기법을 사용하면 fork를 한 직후 같은 Process를 통째로 memory에 중복 저장하지 않으면서 동시에 Process끼리 자원을 공유하지 않는 방법이 있다.
Copy on wirte...Docker 할 때 잠깐 나왔던 내용인데, 14장에서 설명을 해준다고 한다.
그 때, 내용 더 조사해서 제대로 정리해놔야겠다.
✒️ IPC(Inter-Process Communication)
Kernel mode에서 IPC라는 내부 Process 간 통신을 제공한다. (같은 PC 내의 Process와 Thread 간에 데이터를 주고 받는 것도 통신으로 간주한다.)
1. Message Passing • Kernel이 제공하는 API를 이용해서 통신한다 • 송신 process는 enqueue, 수신 process는 dequeue를 하여 진행한다 • Message Queue는 Kernel 단에서 관리한다. • 파이프, 소켓(TCP/IP) 등을 통해서도 가능하다
2. Shared Memory • 서로 공유할 수 있는 Memory 영역을 두고 사용한다 • 데이터 자체를 공유하도록 지원한다 • Shared Memory는 Kernel 단에서 관리한다.