운영체제 Chapter 3 요약

Abraham Silberschatz Operating system 10th Ed.

By Hank Kim

[Chapter 3]

I. 프로세스

A. 프로세스 vs 프로그램

1. 프로그램: 디스크에 저장되어있는 실행 가능한 파일. 메모리에 올라가있지 않지만 실행시키면 메모리로 로드되어서 실행할 수 있다. ./a.out 와 같이 실행시키면 디스크에 저장된 실행파일이 버스를 타고 메모리 구조에 맞게 메모리에 올라간다(로드된다). 코드 세그먼트의 시작을 프로그램 카운터가 가리키게 되고 실행중인 프로그램이 되는데, 이것을 프로세스라고 한다.

2. 프로세스: 실행된 프로그램을 말하며, 프로그램을 실행하면 로더가 프로그램의 복사판을 만들어서 메모리에 올린다. 바이너리 코드, 즉 cpu연산들의 집합으로 이루어져 있고 그게 코드세그먼트로 복사된다. 프로그램이 실행되어 생성된 하나의 인스턴스라고 할 수 있다. 프로세스는 고유한 pid를 가지고 커널에 의해서 관리된다. 커널은 pid테이블을 가지고 스케줄링을 진행함. 프로세스는 실행과 스케줄링의 기초 단위이다.

B. Process Address Space

1. Virtual Memory 덕분에 모든 프로세스는 0x00000000부터 0xffffffff까지의 공간이 있다고 가정하고 사용할 수 있다. 하드웨어 메모리 주소를 기억하고 관리하기 어렵기 때문에 abstraction을 제공하는것이다. MMU가 컨버팅을 담당한다.

2. 프로세스의 주소 공간은 코드(텍스트)세그먼트, static(데이터)세그먼트, 힙, 스택으로 이루어져 있다. 프로그램 카운터가 코드 세그먼트에서 가리키는 부분을 Instruction register에 복사하고, 명령을실행하는데 필요한 지역변수나 전역변수가 스택, 데이터 세그먼트, 힙에서 가져와서 general purpose register에 저장하고 메모리 공간에다 계산한 값을 덮어쓰고 다음 pc+1증가..
이런 식으로 프로그램이 실행된다.

3. 코드 세그먼트:맨 아래에 코드 세그먼트가 적재되고 만 아래에서부터 프로그램 카운터가 한칸씩 이동하면서 실행한다. cout과 같은 표준 라이브러리 함수를 사용하면 cout코드가 존재하는 바이너리 파일의 주소로 점프했다가 돌아온다. 코드 세그먼트는 수정되면 안되므로 readonly이다.

4. 스택: 함수수행에 필요한 메모리를 스택에 위에서부터 쌓는다. 쌓이는 단위를 스택 프레임이라고 하고, 각 함수에서 사용하는 지역변수, 매개변수가 스택 프레임 내에 저장된다.

5. 힙: 동적으로 할당한 메모리가 위쪽으로 쌓인다. 런타임에 크기가 결정된다. 사용자가 동적으로 할당 및 해제한다.

6. Static 세그먼트는 전역변수, static 변수 등을 저장하는 공간이다.

C. 프로세스 상태값

1. new: 프로세스 초기화 작업을 진행한다. PCB(프로세스 컨트롤 블록)을 생성하고 pid를 부여하고, 메모리 상에 프로세스 주소공간을 만드는 등의 작업을 하고 완료되면 ready큐에 들어간다.

2. ready: 수행되기를 기다리는 프로세스이다. 지금 당장 cpu에 올라가서 수행되도 문제없는 프로세스들이 ready큐에 들어가있고, 현재 cpu에서 동작중인 프로세스가 타이머 인터럽트나 IO요청으로 인한 시스템 콜 등에 의해서 커널이 수행되면 현재 진행중인 프로세스는 내려오고 인터럽트 핸들링을 한 다음 스케줄링을 통해서 다음 실행될 프로세스를 ready큐에서 고른다.

3. running: 스케줄러에 선정된 프로세스가 cpu에 의해서 실행되는 상태. 타이머 인터럽트가 걸리거나 트랩을 걸었을때, 혹은 waiting 프로세스가 하드웨어 인터럽트를 걸었을때 내려와서 waiting으로 빠진다. 트랩이 걸렸을때 처리가 끝나면 데이터를 waiting인 프로세스에 전달하면 그 프로세스는 ready상태가 된다. 결과를 받아와야 다음 명령을 수행 가능한 ready상태가 될 수 있기 때문.

4. waiting: IO이벤트를 기다리는 등의 상황으로 인해서 당장 수행될 수 없는 프로세스. 커널으로부터 결과를 받아서 다음명령을 수행할 수 있는 상태가 되면 ready가 된다.

5. terminated: 프로세스가 종료되고 메모리에서 내려감.

D. PCB

1. PCB: 프로세스 컨트롤 블록. 프로세스가 어디까지 동작했고 파일이 어떻게 연결되어있는지 등 프로세스가 cpu에서 내려갔다가 다시 올라갈 때 필요한 모든 정보를 갖고 있는 구조체. 커널에서 관리되고 있다. 프로세스 상태, 프로그램 카운터, 레지스터 스케줄링 관련 정보, 메모리 정보, IO의 몇번 디스크에 접근하고 있는지 등의 정보를 담고있다. 리눅스 2.4 기준으로도 1456바이트로 크기가 상당히 크다. 모든 프로세스는 PCB를 기반으로 관리되고, 리눅스에서 PCB는 연결리스트로 관리된다.

2. PCB가 포함하는 정보: process state, program counter, register, 오픈된 파일 리스트, 스케줄링 정보, 메모리 정보, 부모, 자식 프로세스 등.

3. PCB는 ready상태일때는 ready큐에 존재하고, IO요청을 기다릴때는 IO Device큐에 연결리스트로 존재한다.

E. Context Switching (CPU switching)

1. 정의: 컨텍스트란 명령어들이 어디까지 수행되었는지에 대한 정보 및 명령을 수행할때 필요한 기타 정보들을 합친 것이다. 하나의 프로세스가 수행되던 컨텍스트가 PCB에 저장되고 다른 프로세스로 스위칭되어서 CPU에서 내려온다. 다시 CPU사용권한을 받으면 PCB에서 컨텍스트를 받아와서 다시 수행한다. 컨텍스트 스위칭은 일반적으로 1초에 100~1000번정도 일어난다.

2. 단점: 오버헤드가 발생한다. PCB에 컨텍스트를 저장하고 가져오는 스위칭 작업, 인터럽트로 인해서 운영체제가 수행되고 스케줄러 동작 등의 과정 전체가 오버헤드이다. 이것을 줄이기 위해서 CPU하드웨어 개선이 많이 일어났다.

3. 진행순서: p0이 실행중이다가 interrupt가 발생한다(타이머 혹은 시스템 콜). 그러면 커널이 cpu에 올라와서 pcb에 현재 상태를 저장하고 스케줄러를 통해서 다음 프로세스를 선정하고 선정된 프로세스 p1의 pcb를 불러와서 p1을 실행시킨다.

F. 스케줄러

1. Short Term Scheduler(CPU 스케줄러): CPU를 할당받을 다음 프로세스 선택. 레디큐에 있는 프로세스들 중에서 어떤 걸 올려줄것이냐.

2. Long Term Scheduler(Job Scheduler): 메모리에 어떤 프로그램을 올릴것인지 선정. 피지컬 메모리가 적고 프로세스가 차지할 메모리가 더 클때 메모리에 누구를 올려둘 것인가.

3. Medium Term Scheduler(swapper): 동적 메모리를 많이 사용하는 등의 경우에 메모리가 부족해져서 메모리에 올라가있는 프로세스들 중 하나를 디스크로 내려야한다. 디스크에서 메모리로 가는것을 swap in, 메모리에서 디스크로 내리는것을 swap out이라고 한다.

II. 프로세스 동작

A. 프로세스 생성

1. fork(): fork를 호출한 프로세스를 parent라고 하고 새로 생성된 프로세스를 child라고 한다. 가장 먼저 parent의 PCB를 복사해서 새로 생성한 PCB에 붙여넣고, parent의 fork를 호출한 프로세스의 시점의 상태를 그대로 복사해서 새로운 address space를 만든다. PCB와 프로세스 둘다 shallow copy된다. 그래서 커널 리소스도 부모와 같은 것을 가리킨다. (부모가 가리키던 파일 디스크립터를 그대로 가리킨다.) 즉, 프로세스에서 열려있는 파일 등 사용하고 있는 IO등도 그대로 복사된다는 뜻이다. 이렇게 파일디스크립터를 공유하게 되는 이유는 프로세스 간 협력을 원할하게 하기 위해서이다. critical한 부분이 아닌 경우 함께 사용할 수 있도록 구현되었다.

만들어진 PCB는 레디큐에 들어간다. child는 자신의 고유한 pid를 가지며, 프로세스를 그대로 옮겼으므로 프로그램 카운터도 복사했기 때문에 자식도 포크를 호출한 상태이다. fork가 끝나면 부모 프로세스는 자식의 pid를 반환받고 자식은 0을 반환받기때문에 parent/child를 구분할 수 있다.

위 코드에서 pid=fork()부분에서 프로세스가 갈라진다. fork()의 리턴으로 0을 받으면 자식에 해당하는 코드를 실행한다. getppid()는 부모 프로세스의 pid를 받아오는것이고 getpid()는 자신의 pid를 받아온다.
요약하면 시스템콜 fork를 호출하면 운영체제가 실행되어서 pcb와 메모리 공간에 프로세스를 복사해 새로 만들고, 리턴값으로 부모는 자식 프로세스의 pid, 자식은 0을 반환한다.

2. 예시1 : 웹서버의 경우. 서버와 클라이언트가 존재하고 서버는 accept()로 클라이언트의 요청을 기다리고 있다. 요청이 들어오면 소켓을 연결시키고 fork로 똑같은 프로세스를 만든다. child는 소켓을 유지한 상태로 요청을 처리한다. parent는 소켓을 닫고 다시 요청을 기다린다. 부모는 클라이언트의 요청을 기다리다가 자식 프로세스 만들고 대기하는 역할만 하고, 클라이언트 하나당 자식 프로세스를 연결해준다. 아파치가 이와 같이 동작한다.

3. 예시 2: scanf를 사용해서 IO가 발생한다. 프로세스가 한개면 코드가 순차적으로 수행되어야하기 때문에 다른 작업을 못한다.그럴때 복사한 프로세스를 만들어서 자식은 다른 작업을 할 수있도록 하면 하나의 프로그램이 다중의 프로세스로 동작하게 된다. fork하면 한 터미널에 자식과 부모 프로세스의 결과가 한번에 나온다. IO스트림, 커널 리소스를 다 복사해오기 때문에 한 터미널에서 표시된다. 출력되는 순서가 달라지는 이유는 스케줄러가 무엇을 먼저 선정했는지의 순서이다.

B. 프로세스 실행

  1. exec(): 일반적으로 fork와 같이 수행된다. prog를 인자로 받아서 명시된 경로에 있는 프로그램을 새로 생성된 프로세스의 memory address space에 올려서 덮어쓰고 실행한다. child는 부모랑 같은 내용의 프로세스였다가 새로운 프로그램을 실행시키는 껍데기 역할을 하게 되는것이다.
    코드세그먼트를 덮어쓰고 프로그램 카운터 등의 레지스터를 지워서 완전히 새로운 프로세스로 만든다.

exec을 호출하면 현재 프로세스를 멈추고 prog에 명시된 경로의 프로그램을 로드하여 하드웨어 컨텍스트와 args를 초기화한다. 그리고 pcb블럭을 레디 큐에 넣는다. exec은 새로운 프로세스를 생성하지는 않기 때문에 fork한 다음 child에서 exec을 수행하는 형태로 사용된다.

  1. 예시: 쉘코드를 입력받아서 다른 프로그램을 exec하는 코드이다. fork이후 child프로세스인 경우 cmd로 받아온 입력을 input parameter로 exec을 수행한다. 그러면 자식은 새로운 프로그램으로 덮어씌워지고 부모는 wait을 통해서 자식 프로세스가 종료될 때까지 기다렸다가 자식이 종료되면 커널은 시그널을 받아서 부모에게 전달하고 종료된다.
  2. pid 1번 프로세스는 init이고, init에서 fork and exec하면서 프로세스간에 트리 구조의 부모자식관계를 가지게 된다. 윈도우에서는 createProcess이라는 시스템콜로 fork와 exec이 한번에 이루어지는데, 윈도우는 프로세스의 관계를 트리구조로 관리하지 않고 그냥 프로세스를 생성한다는 차이점이 있다.

C. 프로세스 종료

  1. 일반적인 프로세스 종료: main함수에서 return -> exit함수호출 -> exit함수의 내부에서 _exit()호출. exit함수는 정상적으로 프로세스 관련된 내용을 수습하는 코드를 수행하고 마지막에 _exit()을 호출해서 프로세스를 종료한다. _exit을 직접 호출하면 캐싱된 데이터가 사라지고 메모리와 캐시의 데이터의 일관성이 깨질 수 있다. 따라서 일반적으로 프로세스를 종료할때는 close작업을 수행해서 동기화 작업을 수행하고 열어둔 파일을 닫는 작업도 수행한다. 그리고 프로세스의 메모리도 운영체제에 반환하고 PCB도 삭제된다.
  2. 비정상적 프로세스 종료: abort() 호출 - 라이브러리 작성할때 사용되는 경우가 있고 일반 프로그램에서는 잘 쓰지 않음. 오류가 발생했을 때 프로그램을 종료시킨다.
  3. 자식 프로세스가 종료되기를 기다리고 종료: wait() - 자식이 먼저 종료되었지만 부모가 아직 wait()를 호출하지 않아서 종료 상태 정보를 수집하지 못한 상태를 좀비(Zombie) 상태라고 하고, 부모가 먼저 종료되어서 부모 없이 남은 자식 프로세스의 상태를 고아(Orphan)상태라고 한다. 이런 상태의 프로세스가 생기면 운영체제의 퍼포먼스에 영향을 줄 수 있다.

D. 프로그램의 시작과 종료

  1. 순서: 프로그램이 exec으로 실행되면 C startup routine이 실행되고, C Startup Routine은 지정된 작업을 수행하고 마지막에 main함수를 호출한다. main함수는 user 함수들을 콜하고 리턴하는 작업을 수행하면서 코드가 진행되다가 메인에서 리턴하면 C Startup Routine으로 돌아와서 exit을 호출한다. exit시스템 콜이 호출되면 exit hanlder를 호출해서 cleanup을 실행하고 마지막에 _exit을 수행해서 프로세스가 종료된다.

E. 멀티프로세스 예시

  1. 크롬 브라우저: 브라우저의 메인 프로세스가 존재하고 탭별로 데이터를 가져오고 렌더링을 해야하므로 탭마다 렌더러 프로세스가 따로 존재한다. 애드블락 등의 플러그인을 수행하는것도 멀티 프로세스로 fork가 일어나서 실행되는 프로세스이다.
  2. 초기 버전 iOS: 표면적으로 멀티프로세싱을 지원하지 않음 foreground로 돌릴수 있는 프로세스는 하나이고 여러개의 background프로세스를 가질 수 있다.

F. IPC

  1. IPC(interprocess communication): 프로세스 간 통신. 두개의 프로세스 a,b가 있고 a가 계산한 것을 b의 결과값으로 처리해야하는 경우 a가 계산한 내용을 b로 넘겨줘야한다. 하지만 메모리 프로텍션 정책이 있기 때문에 다른 프로세스의 메모리에 접근하면 프로세스가 kill당한다.
    그래서 대표적으로 2가지 방법으로 프로세스 간 데이터를 주고받는다. 메세지 패싱 기법/ Shared memory 기법이다.
  2. 메세지 패싱 기법은 운영체제에서 서로 정보를 교환할 수 있는 양방향 버퍼, 메세지 큐를 관리해준다. 메세지 큐에 a프로세스가 데이터를 넣으면 b한테 커널이 시그널을 보내고 b가 데이터를 가져가서 처리하는 운영체제가 직접 관여하는 방식이다. 메세지 큐는 안전하지만 커널의 오버헤드가 늘어나는 단점이 있다.
  3. shared memory 기법은 운영체제가 프로세스 사이에서 공유할 수 있는 메모리를 할당해 주고 둘 중 하나가 메모리를 반환시키는 경우 가져오는 역할만 수행하고 나머지는 관여하지 않는다. shared memory방식은 동기화 문제나 메모리 누수 등 위험성을 동반하지만 오버헤드가 늘어나지 않는다. 또한 shared memory를 어떻게 안전하게 사용할지 개발자가 전부 매뉴얼링해야한다.

  4. 대표적인 shared memory에서 발생할 수 있는 문제가 bounded buffer problem이다. producer-consumer problem이라고도 한다. producer는 데이터를 in이 가리키고 있는 주소에 계속해서 집어넣고 consumer는 out에서 데이터를 빼낸다. 인터럽트로 인해서 다른 프로세스로 공유자원이 넘어가서 발생하는 문제이다. 추후 semaphore를 다룬 후 더 자세히 기술 예정.
  5. Pipe: 부모와 자식 프로세스 사이에서 데이터를 전송하는데 사용되는 기본적인 IPC 방식 중 하나이다. 단방향 통신만 지원하며 일반적으로 부모가 쓰고 자식 프로세스가 읽기를 담당한다. pipe는 메모리 내에 생성되며 파일 시스템에 존재하지 않고 관련된 프로세스들이 종료되면 사라진다.
  6. FIFO: Named Pipe라고 하며 파일 시스템 내에 실제 이름을 가진 pipe이다. 서로 다른 부모-자식 관계를 가진 프로세스들 사이에서 통신할 수 있고 명시적으로 삭제하기 전까지 유지된다.
  7. Socket: Socket또한 IPC이다. 원격에 있는 프로세스와 다른 컴퓨터 프로세스 간의 통신 IPC이다. 파이프도 소켓으로 처리해도 성능에 큰 차이가 나지 않기때문에 소켓을 이용해서 구현하는 사람도 있다.
    Client-Server Communication에 주로 사용되며 RPC를 위해서 사용하기도 한다. RPC(Remote Procedure Call)는 chatgpt와 같은 프로세싱 파워를 많이 필요로하는 서비스를 사용할 때 프로시저 자체를 리모트 서버에서 수행하고 클라이언트에서 해당하는 결과를 받는 방식이다.
Tags: OS
Share: Twitter Facebook LinkedIn