강민철님의 "혼자 공부하는 컴퓨터 구조+운영체제"을 기반으로 학습한 게시물입니다.
📕 목차
1. Overall
2. Process State & Hierarchy Structure
3. Thread
1. Overall
📌 Process
- 보조 기억 장치에 저장된 프로그램을 메모리에 적재하고 실행시킨 프로그램
- 종류
- Foreground Process
- 사용자가 보는 앞에서 실행되는 process
- 일반적인 명령어 실행은 모두 전면 처리 과정
- Background Process
- 수행이 오래 걸리는 작업 등의 사용자가 보지 못하는 후면에서 실행되는 process
- 사용자와 일정 상호작용하지 않고 정해진 일만 수행하는 Background Process를 Unix 체계에선 데몬(daemon), Window 체제에서는 서비스(Service)라고 한다.
- Foreground Process
✒️ Service & Daemon
요새 여기저기서 daemon이라는 용어를 접하고 있던 터라 조금 더 조사해보았다.
OS의 차이일 뿐 둘은 99% 동일하다고 봐도 무방하다(고 한다).
그나마 차이라고 한다면 추가적인 daemon을 생성하는 방식이 있다.
• Window는 sc.exe와 같은 프로그램으로 Window API 함수를 이용해 등록해야 한다.
• Linux는 실행 권한을 준 상태로, init process가 부팅될 때 실행하는 '/lib/systemd' 디렉토리에 넣어두면 된다.
재밌는 점은 Network를 처리하는 daemon, Hardware 동작을 처리하는 daemon 외에도
Web server, Name Server, DB Server 와 같은 Server Process도 Daemon이라 지칭한다.
이유는 Deamon은 "부팅 때 자동으로 켜져서 Background에서 계속 실행되는" Process이므로,
Server Process 또한 이 범주에 속한다고 볼 수 있기 때문이다.
systemd에 대한 보다 자세한 내용은 다른 포스팅에서 다루었었다.
📌 PCB(Process Control Block)
- 특정한 Process를 관리할 필요가 있는 정보를 포함하는 Kernel의 자료구조다. (kernel mode에 생성)
- OS가 Process Scheduling을 위해 가지고 있는 Process의 정보를 담은 Database
- 각 Process가 생성될 때마다 고유한 PCB가 생성된다. Process가 종료되면 PCB도 제거된다.
CPU 자원이 한정되어 있으므로, Process는 정해진 시간만큼 이용하다가 Clock에 의해 주기적으로 발생하는 Timer Interrupt(Hardware Interrupt의 일종)가 발생하면 차례를 양보해야 한다.
이때 CPU가 처리하던 작업의 내용들을 PCB에 저장시켜두었다가, 자신의 차례가 오면 이전의 작업을 계속해서 진행할 수 있게 한다.
- PID(Process ID)
- Process 식별을 위한 고유 번호
- Register value
- 자신의 차례가 돌아왔을 때를 대비해 Register 중간값 보관
- 해당 Process가 실행하며 사용했던 Program counter를 비롯한 register 값을 저장한다.
- Process Status
- CREATE, READY, RUNNING, WAITING, TERMINATED
- 해당 Process가 I/O Device 사용을 원하는지, CPU 사용을 원하는지 등의 정보를 저장한다.
- CPU Scheduling 정보
- Process가 언제, 어떤 순서로 CPU를 할당받을지에 대한 정보
- 우선 순위, 최종 실행 시각, CPU 점유시간 등
- Memory 관리 정보
- Process의 주소 공간
- base register, limit register 값과 같은 정보가 담긴다.
- Process 주소를 알기 위해 페이지 테이블 정보도 담긴다.
- 사용한 파일과 입출력 장치 목록
- Process가 실행 과정에서 특정 I/O Device나 file을 사용하면 해당 내용이 명시된다.
- 어떤 I/O Device가 해당 Process에 할당 되었는지, 어떤 file들을 열었는지가 기록된다.
📌 Context Switch
- Context : 중간 정보, 즉 하나의 Process 수행을 재개하기 위해 기억해야 할 정보들
- Context Switch가 너무 자주 일어나면 overhead가 발생하여 성능이 저하된다.
📌 Process Memory
Kernel mode에 저장되는 PCB 외에, User mode에 저장되는 Process 정보가 있다.
- 코드 세그먼트(Code Segment)
- 기계어 명령어, 2진수로 바뀐 (우리가 쓴) 코드 등이 저장
- 프로그램 코드 및 리터럴 상수가 저장
- CPU가 실행할 명령어가 담겨 있기 때문에 write가 금지되어 있는 read-only 공간
- 데이터 세그먼트(Data Segment)
- Program이 실행되는 동안 계속 유지할 데이터
- 전역 변수, 정적 변수, 심볼릭 상수, 문자열 상수
- 프로그램 전체에서 접근할 수 있다.
- 코드&데이터 세그먼트는 정적 할당 영역에 속한다.
- 힙(Heap)
- 프로그래머가 직접 할당 및 해제를 관리
- 메모리 공간을 반환하지 않으면, 자동으로 해제되지 않아 메모리 누수(memory leak)를 유발
- 요구 순서에 일정한 규칙이 없다.
- 스택(Stack)
- 데이터를 일시적으로 저장
- 시스템이 할당, 해제를 관리
- 엄격한 LIFO 구조
- 실행 시간 Stack으로 함수 호출될 때마다 생성
- 활성 레코드 혹은 스택 프레임이라 부른다.
- 지역 변수, 매개 변수, 반환 주소, 반환 값 등
- 힙&스택 영역은 동적 할당 영역에 속한다.
2. Process State & Hierarchy Structure
📌 Process State
설명 | |
CREATE | • Process를 생성 중인 상태 • 이제 막 Memory에 적재되어 PCB를 할당받은 상태 |
READY | • CPU를 할당받을 때까지 대기하는 상태 • Dispatch : Process가 READY → RUNNING으로 전환되는 것 |
RUNNING | • CPU를 할당받아 실행 중인 상태. 할당된 일정 시간 동안 사용 가능 • Timing Interrupt가 발생하면 다시 READY 상태로 돌아간다. • I/O Device 사용하여 작업이 끝날 때까지 기다려야 하면 WAITING 상태로 간다 |
WAITING | • I/O Device의 작업을 기다리는 상태 • I/O Device의 작업이 끝나면 다시 READY 상태로 돌아간다. • 정확히 따지면 특정 이벤트가 일어나길 기다리는 Process는 WAITING 단계가 된다. (대부분 I/O) |
TERMINATED | • Process가 종료된 상태 • Kernel의 PCB와 User mode에 Process가 사용한 memory를 정리한다. |
🟡 Process State Transition Diagram
📌 Process Hierarchy Structure
- 모든 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를 내고 명령어 입력 대기
- 터미널 창에서 깜빡깜빡 거리는 단계가 이 시점
- swapper(Scheduler Process)
📌 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
- fork()의 리턴값은 Child에게 0, Parent에게는 Child Process의 pid, 실패하면 -1을 반환한다.
2️⃣ exec
- 자기 대치 : Child Process를 새로운 프로그램으로 대치
- 새 프로그램의 main, 즉 처음부터 실행을 시작한다
- 새로 실행할 프로그램 정보를 exec()의 인자로 전달한다
- 기존 memory 공간을 새로운 프로그램 데이터로 덮어씌운다
- 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 단에서 관리한다.