#2 - 운영체제 2-1장, 프로세스
프로세스란?
- 현재 실행중인 프로그램을 의미한다.
- 다음과 같은 의미로도 사용된다.
- 메모리의 할당 단위
- 디스패치의 단위
- 다음과 같은 의미로도 사용된다.
- 프로그램은 두가지 방법으로 동작한다.
- Batch system : 배치 크기만큼 한번에 처리
- Interactive system : Time-shared 방법으로 처리(응답시간이 짧다)
- 프로세스가 포함하는 것
- Program, Data, Stack, Heap, Meta data
- 프로세스 이미지
- Kernel Mode : 운영체제가 모든 권한을 가지고 실행됨(전체영역)
- User Mode : 사용자의 권한으로 실행 (시스템 영역을 제외한 나머지 영역)
- text(program) -> data -> heap -> stack 영역으로 나누어진 메모리영역을 의미
- 각각의 영역에서 저장되는 데이터 종류
- Code(text) : 프로그램코드 그자체, 데이터 X
- Stack : 지역변수, 매게변수 등 특정함수에만 필요한 데이터
- Heap : 동적메모리, 할당된 변수 등 사용자가 임의로 받아낸 데이터
- Data : 전역변수, 상수, 문자열상수 등 프로그램 전체에 필요한 데이터
- 프로세스가 가지는 상태
- new : 새롭게 생겨난 프로세스
- running : 실제로 실행되고 있는 상태
- waiting : 다음 이벤트가 발생할 때 까지 대기하는 프로세스
- ready : 프로세스가 되기 위해 할당을 기다리는 상태
- terminated : 실행이 종료된 상태의 프로세스
- 프로세스가 동작하는 구체적인 형태
- 사진
- 프로그램 -> 메모리에 올라온 프로세스(New) -> CPU에 올릴 준비가 된 프로세스(Ready) -> CPU에서 동작중(Running)-> 종료(Terminated)
- Running에서 Waiting으로 넘어갈때 : I/O나 정지이벤트가 발생함 (인터럽트를 발생시킴)
- Waiting에서 Ready로 넘어갈때 : I/O가 끝나서(인터럽트를 처리해서) Ready로 넘어감
- 프로세스의 문맥교환 (Context Switching)
- 굉장히 많은 량의 오버해드가 발생함
- 뭄맥교환이 자주 발생할수록 CPU활용도가 낮아짐
- 하나의 CPU에서 문맥교환 과정
- PCB1에 현재 상태 저장
- PCB2의 상태 로드
- 동작
- PCB2에 현재 상태저장
- PCB1의 상태 다시 로드
PCB란 무엇인가?
- Process Control Block을 의미하며 Meta Data이다.
- 결국 의미하는 것의 핵심은 데이터를 설명하는 데이터라는 것이다.
- ex) 엑셀 파일의 파일명 등
- 다음과 같은 것들이 포함된다.
- 프로세스 상태
- 프로세스 카운터 (실행위치)
- CPU Register
- CPU 스케줄링 정보
- 메모리 관리 정보
- 기본 저장 정보
- I/O 상태 정보
- PCB는 어디에 존재하는가? : 메모리의 커널 영역 어딘가
- 실제로 PCB안에 존재하는 것
- PID : 프로세스 ID : 크기가 아주 큼
- Priority : 우선순위
- Waiting Event : 대기중인 이벤트들
- State : 현재 프로세스의 상태
- Location of image in disk and memory : 실제 주소의 포인터
- 디렉토리, 파일 포인터
- Parent, Child 프로세스 ### 프로세스가 가지는 데이터
- 나중에 교환되며, 메모리에 남아있어야 하는 데이터 (Struct Proc)
- PID, Priority, Waiting Event, State, 실제 주소 포인터 등
- 이들은 항상 필요하므로 프로세스가 나가도 메모리에 존재한다.
- 가지는 데이터의 크기가 작다.
- 프로세스당 하나씩만 있다.
- 실행할 때 필수적인 데이터(Struct User)
- Open files, directory, tty, Parent&child 프로세스 등
- 이들은 프로그램이 동작할 때 사용된다.
- 가지는 데이터의 크기가 크다.
- 항상 가상주소에 의해 접근된다. ### 프로세스 스케줄링
- System에 올라온 모든 Process들은 Job Queue에 존재한다.
- System에 올라왔지만 실행중이 아닌 프로세스들은 Ready Queue에 존재한다.
- System에서 프로세스의 I/O를 기다리는 프로세스틀은 Device Queue에 존재한다.
- 이 밖에도 다양한 Queue가 존재한다.
### Shedulers (스케줄러)
- Long-term Scheduler : Ready상태에 어떤 프로세스를 넣을지 결정하는 스케줄러
- Job Scheduler 라고도 불린다.
- Ready Queue에 프로세스를 넣어주기 때문에 Degree변하게 된다.
- 상대적으로 느리게 Invoke 된다.
- 실질적인 Degree를 관리하는데 주로 Degree를 낮추도록 한다.
- Short-term Scheduler : Ready상태에서 Running 상태로 할당시켜주는 스케줄러
- CPU Scheduler 라고도 불린다.
- Ready 상태에 있는 프로세스들중 특정한 정책에 따라 할당한다.
- 상대적으로 빠르게 Invoke 된다.
- Medium-term Scheduler : Swap 영역으로 프로세스를 옮겨주는 스케줄러이다.
- Thrasing 을 방지하기 위해 개발되었다.
- Degree를 감소시키도록 Swap하여 Thrashing을 방지하는 역할을 수행한다.
- Thrashing이 무엇인가?
- 페이지 단위로 실행되는 프로세스가 어떠한 사고로 인하여 페이지 로드에 실패하면 발생한다.
- 페이지가 부족하면 실행이 불가능하고 CPU는 서로 책임을 전가하기 시작한다.
- 책임 전가가 진행되면 CPU의 사용도가 급감하며 활용도가 낮아지게 된다.
- 이런 상황은 Degree가 증가하면 발생하기 때문에 Degree를 감소시키면 해결된다.
- Process를 나누는 큰 기준 2가지
- I/O-bound process - I/O에 더 오랜시간이 소모되는 프로세스
- CPU-bound process - 연산하는데 더 오랜 시간이 소모되는 프로세스
### 문맥교환 (Context Switch)
- 실행중인 프로세스를 기다리는 상태인 프로세스와 교환하는 작업을 의미한다.
- 문맥이 교환되면 다음 3가지가 교환된다.
- System Context : 메모리 테이블이나 실제 저장공간과 같은 커널역영에 존재하는 데이터가 교환
- Hardware Context : 프로그램 카운터와 같은 레지스터 정보를 교환
- Memory Context : 메모리 공간이나 디스크 이미지가 교환
- 스위칭은 하는 동안 아무작업도 할 수 없기 때문에 문맥교환은 큰 오버헤드를 가진다.
## 리눅스에서 프로세스 생성하기
- 모든 프로세스는 fork()와 exec()순서로 생성된다.
- fork() : 자기 자신의 PID를 제외한 나머지를 모두 복사하여 새로 생성한다.
- fork()를 사용하여 프로세스 수를 늘린다.
- fork()에서는 메모리의 공유가 발생하지 않는다.
- 메모리나 커널 데이터 스트럭쳐를 할당받는 것이 주 목적이다.
- exec() : 디스크에서 원본 파일을 가져와 현재 파일에 오버라이딩 한다
- exec()를 사용하여 실제 파일를 받아오며 처음 시작 위치로 이동한다.
- 파일을 가져오고, 초기화한 후에 Ready 큐에 넣는 것이 주 목적이다.
main()
{
int pid;
pid = fork();
if(pid == 0)
printf("\n Hello, I am child!\n")
else
printf("\n Hello, I am parent!\n")
}
- 예를 들어 위의 프로그램을 실행하면 아래와 같이 출력 될 것이다.
Hello, I am child!
Hello, I am parent!
- 이는 자식 PID가 0이고, 부모 PID는 0이 외의 숫자가 되기 때문이다.
- 출력은 부모, 자식 중 누가 먼저 출력되는지는 스케줄러의 정책에 따라 다르다.
int main() {
pid_t pid;
char *args[] = {"ls", "-l", NULL};
pid = fork(); //여기서 포크시작
if (pid < 0) {
printf("Fork failed\n");
return 1;
}
if (pid == 0) {
// 자식 프로세스에서 ls 실행
execvp("ls", args);
printf("Error: execvp failed\n");
return 1;
} else {
// 부모 프로세스에서 자식 프로세스의 종료를 대기
int status;
wait(&status);
printf("Child process exited with status %d\n", WEXITSTATUS(status));
}
return 0;
}
- 위는 exec가 포함된 포크 실행문이다.
- 위의 코드를 실행하면 다음과 같은 일이 발생한다.
- fork()를 만나서 두개의 프로세스로 분리됨
- 부모 프로세스는 자식 프로세스가 종료될때 까지 wait()상태에 들어간다.
- 자식 프로세스는 ls 문을 실행하고 종료된다.
- 부모 프로세스에서 자식이 종료되었음을 알리는 출력문을 출력한다.
- 핵심
- 포크는프로세스의 복사만 가능하다.
- exec는 덮어쓰기와 유사하기 때문에 프로세스를 생성한다.
- 포크로 생성된 프로세스는 포크에서 사라지지 않는다.
- exec로 실행된 프로세스는 exec로 끝나야한다.
프로세스 종료
- 프로세스는 exit으로 종료할 수 있다.
- 반드시 exit이 아니더라도 abort로하여 종료시킬 수도 있다.
- 종료되는 케이스는 크게 3가지가 있다.
- exit() : 프로세스를 정상 종료시킴
- abort() : 주로 kill()을 사용하며, 부모 프로세스가 자식 프로세스를 중지시킨다.
- 자식이 할당된 자원을 모두 사용하였을 때 주로 발생한다.
- 또는 작업에 더 이상 필요가 없어졌다고 판단했을 때 사용한다.
- 어떤 문제로 인하여 부모가 종료됨
- 이 경우 자식 프로세스는 좀비가 되거나 고아가 된다.
- 좀비가 되면 마지막 리턴값이 무한히 반복된다.
- 고아가 되면 자원이 반환되지 않고 그대로 대기한다. ```C int main() { pid_t pid = fork();
if (pid == 0) { printf(“I’m child process (pid=%d)\n”, getpid()); sleep(10); // 자식 프로세스를 10초 동안 대기시킴 printf(“Child process has finished\n”); } else if (pid > 0) { printf(“I’m parent process (pid=%d)\n”, getpid()); sleep(5); // 부모 프로세스를 5초 동안 대기시킴 printf(“Parent process has finished\n”); kill(pid, SIGTERM); // 자식 프로세스를 종료시킴 } else { perror(“fork failed”); exit(1); }
return 0; } ```
- 위 코드는 abort의 간단한 예로, 대기중이던 자식 프로세스를 종료시켜버린다.
- abort는 부모에서 이루어진다는 것을 알 수 있다.
- 출력은 다음과 같다.
I'm parent process (pid=1234) I'm child process (pid=0) Parent process has finished
Cooperating Process
- 독립된 프로세스는 서로 영향을 줄 수 없다.
- 하지만 코오퍼레이팅 프로세스는 서로 영향을 주며 함께 동작한다.
- 다음과 같은 특징들이 있다.
- 정보공유 : 글로벌 변수나 메모리 포인터의 참조가 가능하다.
- 연산 속도 증가 : 병렬처리가 가능하여 속도가 증가한다.
- 모듈성 : 어디든 이식이 가능해진다.
- 편의성 : 사용자가 사용에 유리해진다
생산자 - 소비자 문제
- 두 프로세서 사이에서는 데이터를 생성한 프로세스와 사용한 프로세스가 있다.
- 이 두 프로세스 사이의 데이터 전송 속도 차이로 인해 발생하는 문제가 있다.
- 보통 버퍼의 크기가 문제가 된다. 특히 고정된 버퍼 사이즈가 그렇다.
- 따라서 버퍼는 오버플로우 되지 않기 위해서 환형 큐로 동작하며 포인터와 함께 데이터가 움직인다.
- 위 문제를 해결하기 위해 counter 변수를 만들고 데이터가 생성되면 1씩 증가, 소비되면 1씩 감소시킨다.
- 이 counter 변수가 하는 일을 동기화 작업이라고 한다.
- Producer Process
while(True) { while(count == BUFFER_SIZE) continue; // int은 버퍼의 주소이다. buffer[in] = nextProduced; // 다음 버퍼 주소로 이동 in = (in + 1) % BUFFER_SIZE; // count 증가로 동기화 count++; }
- Consumer Process
while(True) { while(count == 0) continue; // out은 버퍼의 주소 nextCounsumed = buffer[out]; // 다음 버퍼의 주소로 이동 out = (out + 1) % BUFFER_SIZE; // count 감소로 동기화 count--; }
- 타임 쉐어링 환경에서 동작하면 실행코드가 꼬이게 된다.
- 그렇게 되면 공유변수는 사용하기 어려워진다.
- 따라서 lock을 가지고 동기화 하는 것이 필요하다.(Critical Section)
Interprocess Communication (IPC)
- 프로세스 간의 데이터 교환이 필요한 상황이 있다.
- 이런땐 Message를 보내서 정보를 전달한다.
- 두 프로세스 사이에 링크가 생기고 send()와 receive()로 동작한다.
- 이걸 IPC라고 부르는데 직접 통신과 간접 통신이 있다.
- 직접 통신 (Direct Communication)
- send(P, message) : P에게 메세지를 바로 전송한다.
- receive(Q, message) : Q에게 메세지를 바로 받는다.
- 링크는 대체로 물리적으로 연결된다. 따라서 대규모 프로세스에서 비효율적이다.
- 프로세스 사이에서는 단 하나의 간선(링크)만 존재할 수 있다.
- 링크가 물리적으로 연결되어 있으므로, 바로 직접적으로 전송하는 것을 의미한다.
- 간접 통신 (InDirect Coummunication)
- send(A, message) : A 메일박스에 메세지를 전송한다.
- receive(A, message) : A 메일박스의 메세지를 받아온다.
- 링크는 물리적으로 형성되지 않으며 메모리나 Queue의 형태로 생성된다.
- 자원이 공유되고, 여러 프로세스가 접근 가능하다.
- 대규모 프로세스들에서 효율적으로 동작할 수 있다.
Synchronization
- Blocking 방식과 Non-Blocking 방식으로 구분된다.
- Blocking
- 동기적이며 메세지 교환 과정에 집중한다.
- 메세지를 전송하는 쪽에서는 메세지가 전송될 때 까지 기다린다.
- 메세지를 받는 쪽에서는 메세지가 사용가능할 때 까지 기다린다.
- Non-blocking
- 비동기적이며 메세지 교환 과정중 다른 작업이 가능하다.
- 메세지를 전송하는 쪽에서는 메세지를 전송하고 다른 일을 계속한다.
- 메세지를 받는 쪽에서는 메세지가 NULL일 때 다른 작업을 계속한다.
😺 오타나 논리적 오류 지적은 언제든지 환영합니다!😺
항상 읽어주셔서 감사합니다~ 🙏
Leave a comment