CS

[운영체제] 프로세스 - 메모리 공간/상태(state)/문맥 교환(context-switching)/pcb/프로세스 vs 스레드

nkdev 2024. 12. 20. 16:50

1. 운영체제

운영체제는 여러 프로그램이 동시에 실행될 때, 하드웨어 자원(cpu, memory, disk, i/o devices)을 각 프로그램에게 최대한 효율적으로 분배해주는 역할을 한다. 프로세스가 연산을 하기 위해서는 cpu와 memory를 사용해야 하는데, 자원은 한정적이므로 모든 프로세스가 동시에 사용할 수 없다. 따라서 운영체제가 일정 시간마다 자원을 사용할 프로세스를 지정해준다. -오늘날 cpu는 보통 시분할 방식(time sharing)을 따른다.

운영체제는 사용자가 하드웨어 자원에 접근하기 위한 인터페이스를 제공해주기도 한다. 사용자는 GUI 또는 CLI를 통해서 os에게 명령을 내릴 수 있다.

이미지 출처 : https://medium.com/computing-technology-with-it-fundamentals/operating-system-its-functions-and-characteristics-c0946e4215c6

 

운영체제는 하드웨어의 device driver를 사용해서 프로세스가 하드웨어 장치를 사용할 수 있게 해준다. device driver란 하드웨어 장치가 소프트웨어 프로세스와 상호작용하기 위해 만들어진 하나의 컴퓨터 프로그램이다. 프로세스가 드라이버의 명령어를 호출하면 드라이버가 하드웨어 장치에 명령어를 전달하여 하드웨어를 사용할 수 있다. 

2. 프로세스

프로세스는 disk에 저장되어있던 정적인 프로그램이 메모리에 올라가 실행되고 있는 상태를 의미한다. 프로세스가 cpu를 점유하게 되면 cpu의 pc(process counter)가 code의 어떤 부분을 가리키고 있고, cpu는 이 부분의 instruction을 하나씩 읽어서 레지스터에 저장하고, alu를 통해 연산을 수행한 후 그 결과를 레지스터 또는 메모리 저장한다. 

 

2-1. 프로세스의 메모리 공간

프로세스가 시작되면 그 프로세스만의 독자적인 주소 공간이 형성되고 이는 stack, data, code 세 구역으로 나뉜다.

 

  • data 영역
    • 프로그램의 전역변수, 정적 변수가 저장되는 곳. main함수가 시작되기 전부터 메모리에 올라가는 데이터들. 프로그램의 시작과 함께 할당되고, 프로그램이 끝나면 소멸한다.
  • code 영역
    • 실행될 프로그램의 코드가 저장되는 곳. 프로그램이 시작하고 끝날 때까지 계속 메모리에 남아있다. 컴파일된 기계어가 들어가며, cpu가 코드 영역의 instruction을 하나씩 가져가서 처리한다.
  • stack 영역
    • 함수 호출 시 사용되는 지역 변수, 매개 변수가 저장되는 곳
  • heap 영역
    • 개발자가 malloc, new를 사용해서 동적으로 생성한 데이터가 저장되는 곳. 이 영역의 데이터는 개발자가 스스로 할당하고 반환하며 관리해야 한다. gc에 의해 자동으로 반환되기도 한다.

* 스택과 힙

2-2. 프로세스의 상태(state)

  • new : 프로세스가 메인 메모리로 올라온 상태
  • ready : 프로세스가 메인 메모리로 올라와서 초기화를 다 끝내고 cpu만 얻으면 실행 가능한 상태, 디스크에서 읽어올 필요 없이 이미 메모리에 올라와있어서 cpu를 바로 사용 가능
  • running : cpu를 얻은 상태, cpu를 사용하여 instruction을 수행 중인 상태
  • blocked(waiting, sleep) : cpu 사용을 허락해도 당장 instruction을 수행할 수 없는 상태, 디스크에서 file을 읽어오거나 사용자의 입력을 기다리는 경우처럼 i/o를 기다리는 상태
  • terminated : 수행이 끝나 정리 중인 상태

* 인터럽트

인터럽트는 cpu가 현재 running을 중단하고 다른 작업을 처리하러 가야 함을 알리는 신호이다.

  • 외부 인터럽트 : 하드웨어 컨트롤러에서 i/o처리가 끝났음을 cpu에게 알리기 위해 인터럽트 신호를 발생시킴
  • 내부 인터럽트 : cpu가 현재 실행 중인 instruction이 i/o처리가 필요할 때, cpu가 처리할 수 없는 연산이 발생하면 자기 자신에게 알림

출처 : kocw 운영체제, 반효경

 

cpu의 ready queue의 프로세스가 하나 실행(running)되던 중 disk i/o작업이 필요하게 되면 프로세스 상태가 running에서  blocked로 바뀌고 해당 프로세스가 disk i/o queue로 넘어간다. i/o가 끝나면 disk controller가 cpu에게 interrupt를 보내서 i/o 작업이 끝났음을 알리고, interrupt를 받은 cpu는 현재 실행하던 프로세스를 중단하고 cpu 제어권을 os에게 넘긴다. os kernel은 작업이 끝난 프로세스의 메모리 영역의 데이터를 cpu에 넘겨주거나 프로세스 상태를 다시 ready 상태로 바꾸어 cpu의 ready queue에 넣는다.

 

cpu 외에도 disk, 프린트 장치, 소프트웨어 등 한 개의 자원에 동시에 여러 프로세스가 접근하려고 하면 os는 이러한 방법을 통해 스케줄링을 한다. time sharing 시스템이라면 running 중 i/o를 만나지 않더라도 timeout이 되면 running에서 ready로 넘어간다.

 

2-3. 문맥 교환(context switch)

cpu가 다른 프로세스에게 넘어가는 과정을 문맥 교환(context switch)라고 한다. 문맥 교환이 발생할 때 os는 기존에 실행되던 프로세스의 상태를 그 프로세스의 pcb에 저장하고, 새롭게 실행되는 프로세스의 상태를 pcb에서 읽어온다.

* 프로세스의 문맥(context)

프로세스 실행 흐름에서 어떤 순간의 상태를 '프로세스의 문맥(context)'이라고 한다. 프로세스의 문맥(context)을 백업해두어야 multiprocessing할 때 이전에 실행하던 작업 상태를 불러와서 이어서 실행할 수 있다.

  •  하드웨어 문맥 : cpu 수행 상태를 나타냄
    • pc가 어떤 instructor을 가리키고 있는지
    • register에 어떤 값이 저장되어서 처리되고 있었는지
  • 프로세스 주소 공간 : stack, data, code에 어떤 데이터가 저장되어 있는지
  • 커널상 문맥 : 운영체제 커널이 프로세스마다 생성하는 pcb(process control block)의 상태가 어떤지
    • kernel stack -> 원래 프로세스 code를 실행할 때 본인의 프로세스 stack에 호출된 함수를 저장하는데, 프로세스가 처리하지 않고 운영체제에게 대신 처리해달라고 system call을 하면 pc가 프로세스의 code가 아니라 커널 주소 공간의 code를 가리키게 되고, 커널의 code를 수행하면 커널 stack에 호출된 함수가 저장된다. kernal stack은 프로세스별로 따로 생성되는 공간이며, 모든 프로세스가 공유하는 데이터에 접근할 때 사용되는 공간이다. 따라서 프로세스의 현재 상태를 정의할 때는 프로세스 스택 뿐만 아니라 커널 스택 정보도 함께 필요하다. 

* pcb

 pcb(process control block)는 os가 각 프로세스를 관리하기 위해 프로세스 마다 유지하는 정보이다. os는 각 프로세스가 어떤 상태인지 파악하고 있어야 자원을 할당할지 block 상태로 만들지 등을 결정할 수 있다. os는 실행되던 프로세스가 중단되면 pcb를 저장해서 kernel data에 저장해둔다. 그리고 다시 해당 프로세스가 실행될 때 pcb를 꺼내서 프로세스 문맥과 똑같은 데이터를 하드웨어에 구성한다. 

  • os가 프로세스를 관리할 때 사용되는 정보
    • process state : running/ready/blocked/new/terminated 등
    • process number (process id) : 프로세스 식별자
    • scheduling information, priority : 리소스 큐에 있는 프로세스 중 우선순위를 정하기 위한 정보
  • cpu 수행 시 사용되는 하드웨어 정보
    • program counter
    • registers
  • 메모리 관련 정보
    • code, data, stack
  • 파일 관련 정보
    • 읽어온 파일이 뭔지 등

system call이나 interrupt가 발생했다고 반드시 context switch가 일어나는 것은 아니다. system call이나 interrupt가 발생하면 바로 문맥 교환이 일어날 수도 있지만, cpu 제어권이 os에게 잠시 넘어갔다가 다시 돌아와서 기존에 수행하던 프로세스를 계속해서 수행할 수도 있기 때문이다. cpu 제어권에 os에게 잠시 넘어갈 때도 cpu 수행 정보 등 context의 일부를 pcb에 save해야 하지만 문맥 교환이라고 보기엔 부족하다.

3. 스레드(thread)

프로세스 안에 여러 개의 cpu 수행 단위가 있는 경우 이를 thread라고 한다. thread는 lightweight process라고도 하고, cpu를 사용하는 가장 기본적인 단위가 된다.

 

3-1. 스레드의 저장공간

스레드 마다 별도로 가지는 부분 (cpu 연산과 관련된 부분)

  • program counter
  • register set
  • stack space : 스레드별로 호출하는 메서드가 다르므로 

스레드 간에 공유하는 부분 (=task)

  • data section
  • code section
  • os resources

 

3-2. 스레드를 이용하는 것의 장점

responsiveness (빠른 응답성)

  • 프로세스 안에서 멀티 스레드가 비동기식 i/o로 동작하면 사용자에게 더 빠른 응답을 제공할 수 있다. 하나의 스레드가 blocked(waiting) 상태일 때 동일한 task 내의 다른 스레드가 실행(running)되기 때문이다. 예를 들어 웹 사이트를 사용할 때 화면이 렌더링되는 동안 그 시간을 낭비하지 않고 프로세스의 다른 스레드가 실행되어 파일을 가져올 수 있다. 

resource sharing (자원 공유)

  • 스레드 간에는 code, data, os resources 자원 공유가 가능하므로 저장공간을 아낄 수 있다.

economy (경제성)

  • 같은 일을 한다면, 새로운 프로세스를 하나 더 만드는 것보다 새로운 스레드를 하나 더 만드는 것이 오버헤드가 덜 든다. 새로운 프로세스로 취급하면 별도의 프로세스 주소 공간이 하나 더 만들어지기 때문이다. 또한 cpu에서 context switching을 하는 것보다 스레드를 switching하는 것이 오버헤드가 덜 든다.

utilization of mp architectures

  • 멀티 프로세서 환경에서는 여러 스레드가 동시에 여러 cpu 위에서 병렬적으로 수행될 수 있으므로 더 효율적인 수행이 가능하다.

* 동기식 입출력, 비동기식 입출력

프로세스가 i/o가 필요하면 os에게 i/o를 요청한다. 이때 i/o가 끝날 때까지 기다렸다가 instruction을 실행하거나 또는 i/o가 끝날 때까지 다른 프로세스를 실행시키면 동기식 입출력이고, i/o가 끝날 때까지 기다리지 않고 바로 instruction을 실행하면 비동기식 입출력이다. i/o가 실행되는 동안 해당 프로세스를 실행하지 못하기 때문에 좀 더 효율적인 리소스 사용을 위해서 i/o가 실행되는 동안 다른 프로세스를 실행시키기 위해 비동기식 입출력을 사용하는 것이다.

 

3-3. 스레드 동기화

자바에서는 synchronized 키워드로 스레드를 동기화할 수 있다. 

  • 임계 영역(critical section) : 둘 이상의 스레드가 동시에 접근하면 안 되는 코드 영역
  • 락(lock) : 임계 영역을 가진 객체에 접근할 수 있는 권한

어떤 자원에 임계 영역을 설정하여 동시 접근을 못 하게 만든다.

스레드가 임계 영역에 접근하면, lock을 얻는다. 

lock을 반납하기 전에는 다른 스레드가 임계 영역에 접근하지 못한다.

public class ThreadSyncEx {
    public static void main(String[] args) {
        Runnable thread = new CreateThread();

		Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);

        thread1.setName("스레드1");
        thread2.setName("스레드2");

        thread1.start();
        thread2.start();
    }
}

class Money {
    // 현재 가지고 있는 금액
    private int myMoney = 10000;

    public int getMyMoney() {
        return myMoney;
    }

    public boolean withdraw(int money) {
        // 가지고 있는 금액이 출금할 금액보다 크거나 같을 때만 출금
        if (myMoney >= money) {
            // 스레드 동시 접근을 위한 코드
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                System.out.println(e);
            }

            // 출금
            myMoney -= money;

            return true;
        }
        return false;
    }
}

class CreateThread implements Runnable {
    Money myMoney = new Money();

    public void run() {
        while (myMoney.getMyMoney() > 0) {
            // 1000 ~ 5000원씩 출금
            int money = (int)(Math.random() * 5 + 1) * 1000;

            // 출금 실행 코드. 실패시 true 반환
            boolean denied = !myMoney.withdraw(money);

            // 출금 과정 출력
            if (denied) {
                System.out.println("출금 거부");
            } else {
                System.out.printf("스레드: %s 출금: %d원  남은금액: %d원\\n",
                    Thread.currentThread().getName(), money, myMoney.getMyMoney());
            }
        }
    }
}
출처: https://ittrue.tistory.com/173 [IT is True:티스토리]
class Money {
    private int myMoney = 10000;

    public int getMyMoney() {
        return myMoney;
    }

    // 메서드 전체를 임계영역으로 설정
    public synchronized boolean withdraw(int money) {
        if (myMoney >= money) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                System.out.println(e);
            }
            myMoney -= money;
            return true;
        }
        return false;
    }
}

class Money {
    private int myMoney = 10000;

    public int getMyMoney() {
        return myMoney;
    }

    public boolean withdraw(int money) {
    	//특정 영역을 임계 영역으로 설정
        synchronized (this) {
            if (myMoney >= money) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    System.out.println(e);
                }
                myMoney -= money;
                return true;
            }
            return false;
        }
    }
}
출처: https://ittrue.tistory.com/173 [IT is True:티스토리]