프로세스는 컴퓨터에서 실행되고 있는 프로그램을 말하며 CPU 스케줄링의 대상이 되는 작업(task)이라는 용어와 거의 같은 의미로 사용된다. 스레드는 프로세스 내 작업의 흐름을 지칭한다.
프로그램이 메모리에 올라가면 프로세스가 되는 인스턴스화가 일어나고, 이후 운영체제의 cpu 스케줄러에 따라 CPU가 프로세스를 실행한다.
프로세스는 프로그램으로부터 인스턴스화 된 것을 말한다. 프로그램은 컴파일러가 컴파일 과정을 거쳐 컴퓨터가 이해할 수 있는 기계어로 번역되어 실행할 수 있는 파일이 된 것이다.
소스코드가 실행 가능한 프로그램 파일이 되기 까지는 아래와 같은 과정을 거친다.
소스코드 -> 전처리 -> 컴파일러 -> 어셈블리어 -> 어셈블러 -> 목적 코드 -> 링커 + 라이브러리 -> 실행가능 파일
프로세스는 여러가지 상태 값을 갖는다.
프로세스가 생성된 상태를 의미하며 한다. 이때 프로세스의 메타데이터인 PCB가 할당된다.
fork()는 부모 프로세스의 주소 공간을 그대로 복사하며, 새로운 자식 프로세스를 생성하는 함수이다.
부모 프로세스의 주소 공간만 복사한다. 그 외의 다른 것들을 상속하지 않는다.
exec()은 새롭게 프로세스를 생성하는 함수이다.
Ready State 혹은 Blocked State 에서, 메모리를 잃게 되면 Suspended State가 되고,
메모리를 할당 받으면 다시 Ready State 혹은 Blocked State 로 돌아간다.
이 때 Suspend 되기 전의 작업 상태를 되찾기 위해, Suspend 될 때 Memory Image를 Swap device에 보관하고 Resume될 때 Image를 복구하여, 다시 작업을 진행하게 된다.
운영체제가 프로세스에게 할당하는 메모리의 구조는 다음과 같다.
이 중 스택과 힙은 동적 영역에 해당하고, 데이터 영역과 코드 영역은 정적 영역에 해당한다.
스택에는 지역변수, 매개변수, 함수가 저장되서 컴파일 시에 크기가 결정되며 런타임에 크기가 동적으로 변할 수 있다. 런타임에 함수가 함수를 재귀적으로 호출하면서 동적으로 크기가 늘어날 수 있는데, 이 때 힙과 스택의 메모리 영역이 겹치면 안되기 때문에 힙과 스택 사이의 공간을 비워놓는다.
사용자의 프로그램이 런타임에 메모리를 동적 할당할 때 사용되며, 런타임 시에 크기가 결정된다.
데이터 영역에는 전역 변수, 정적 변수가 저장되고, 내부적으로 BSS 영역과 Data 영역으로 다시 나눌 수 있다. BSS 영역에는 초기화 되지 않은 전역 변수 또는 정적 변수가 저장되고, Data 영역에는 실제로 값이 할당되어 프로그램에서 사용하는 데이터를 담은 전역 변수와 정적 변수가 저장된다.
코드 영역에는 사용자가 작성한 프로그램의 소스코드가 CPU에서 수행할 수 있는 기계어 명령 형태로 번역되어 저장되어 있다. 이 영역은 수정이 불가능한 Read-Only로 되어있다.
엄밀히 말하면, 프로세스에 할당되는 가상 메모리의 구조는 아래와 같다.
위의 설명에서는 그 중에서 커널 영역을 제외한 사용자 영역만을 설명한 것이다.
! 추가적으로 메모리 주소 최하단 일부(0x000000 ~ 0x400000)는 운영체제에서 사용하기 위한 영역으로 예약되어있는 것으로 알려져 있다.
https://stackoverflow.com/questions/65338442/whats-under-0x400000-in-virtual-memory
가상 메모리의 커널 영역은 여러 프로세스가 공유하는 부분과 각 프로세스가 독립적으로 사용하는 부분으로 나뉜다.
커널 코드(Code) 영역: 모든 프로세스가 동일한 운영 체제 커널 코드를 공유한다. 이 코드 영역에는 시스템콜 및 핵심 운영 체제 기능을 적재하고 있다. 따라서 각 프로세스가 동일한 운영 체제의 기능을 호출할 때 이 부분은 공유된다.
커널 데이터(Data) 영역: 시스템 전반에 걸쳐 필요한 정보 및 상태를 저장하는 데이터가 공유 데이터로서 데이터 영역에 저장되어있다. ex) 디바이스 드라이버의 정보, 시스템 시계 등
커널 모듈(Module) 영역 : 커널이 동적으로 로드되어 언로드되는 모듈의 코드와 데이터가 저장되는 부분이다. 이는 일부 운영 체제에서만 해당되며, 필요에 따라 동적으로 추가되거나 제거될 수 있습니다.
커널 스택(Stack) 영역: 각 프로세스는 독립된 커널 스택을 가지고 있다. 스택은 각 프로세스의 함수 호출 및 반환을 관리하기 위해 사용되므로 각각의 프로세스가 독립적인 스택을 가져야한다.
기타 데이터 구조 및 테이블: 프로세스 간에는 페이지 테이블, PCB(프로세스 제어 블록) 같은 데이터 구조와 테이블이 독립적으로 할당되어 각 프로세스의 상태와 관련된 정보를 저장한다.
이와 같은 구조로 인해 커널 영역은 공유되는 부분과 독립적인 부분으로 나뉘어져 있어 여러 프로세스가 동시에 실행될 때 각각의 프로세스는 독립성을 유지하면서도 운영 체제의 기능에 접근할 수 있다.
PCB(Process Control Block)은 운영체제에서 '프로세스에 대한 메타데이터'를 저장한 데이터를 말한다. 프로세스가 생성되면 운영체제는 해당 프로세스의 PCB를 생성한다.
프로세스에 대한 메타데이터는 다음과 같은 것들이 있다.
위의 메타데이터는 프로세스가 생성되면 PCB에 저장되는 것이다.
CPU에서는 프로세스의 상태에 따라 교체작업이 이루어진다. (ex. interrupt가 발생해서 할당받은 프로세스가 waiting 상태가 되고 다른 프로세스를 running으로 바꿔 올리는 상황)
이때, 앞으로 다시 수행할 대기 중인 프로세스에 관한 저장 값을 PCB에 저장해두는 것이다.
위에서 예시로 든 것 처럼, 멀티태스킹 환경에서 인터럽트가 발생하거나, 실행 중인 CPU 사용 허가시간(time slice)을 모두 소모하거나, 입출력을 위해 대기해야 하는 경우에 실행중인 프로세스를 다른 프로세스로 교체해야한다.
프로세스를 변경할 때 이전의 프로세스의 상태(문맥)을 보관하고 새로운 프로세스의 상태를 적재하는 작업을 Context Switching이라고 한다.
Context Switching이 수행될 때, PCB를 변경하는 프로세스의 것으로 교환하고, 변경된 PCB에 저장된 메타데이터를 기반으로 CPU의 레지스터 정보가 변경된다.
컨텍스트 스위칭은 여러 프로세스를 돌아가면서 실행할 수 있는 멀티태스킹 환경을 가능하게 하지만, 컨텍스트 스위칭이 일어날 때 아래와 같은 Overhead가 생기게 된다.
하지만 특정 프로세스가 CPU를 사용하지 않는 작업을 수행할 때, CPU를 그냥 아무것도 하지 않는 상태로 두는 것보다 다른 프로세스가 사용할 수 있게하는 것이 효율적이다. 너무 잦은 Context Switching은 문제가 되지만, 적절한 Context Switching은 CPU의 작업 효율을 상승시킨다.
참고. 컨텍스트 스위칭은 한 프로세스 내의 서로다른 스레드 사이에서도 일어난다. 스레드는 스택 영역을 제외한 모든 메모리를 공유하기 때문에 스레드 컨텍스트 스위칭의 경우 비용과 시간이 더 적게 든다.
프로세스는 독립적으로 실행된다. 즉, 독립 되어있다는 것은 다른 프로세스에게 영향을 받지 않는다고 말할 수 있다.(스레드는 프로세스 안에서 자원을 공유하므로 영향을 받는다)
이런 독립적 구조를 가진 프로세스 간의 통신을 해야 하는 상황이 있을 것이다. 이를 가능하도록 해주는 것이 바로 IPC 통신이다. 프로세스는 커널이 제공하는 IPC 설비를 이용해 프로세스간 통신을 할 수 있게 된다.
익명 파이프는 통신할 프로세스를 명확히 알 수 있는 경우에 사용한다.(부모-자식 프로세스 간 통신처럼)
파이프는 두 개의 프로세스를 연결하는데 하나의 프로세스는 데이터를 쓰기만 하고, 다른 하나는 데이터를 읽기만 할 수 있다(FIFO 방식). 한쪽 방향으로만 통신이 가능한 반이중 통신이라고도 부른다. 따라서 양쪽으로 모두 송/수신을 하고 싶으면 2개의 파이프를 만들어야 한다.
매우 간단하게 사용할 수 있는 장점이 있고, 단순한 데이터 흐름을 가질 땐 파이프를 사용하는 것이 효율적이다. 단점으로는 전이중 통신을 위해 2개를 만들어야 할 때는 구현이 복잡해지게 된다.
Named 파이프는 전혀 모르는 상태의 프로세스들 사이 통신에 사용한다. 즉, 익명 파이프의 확장된 상태로 부모 프로세스와 무관한 다른 프로세스와도 통신이 가능하다.(통신을 위해 이름있는 파일을 사용)
하지만, Named 파이프 역시 읽기/쓰기 동시에 불가능하기때문에 전이중 통신을 위해서는 익명 파이프처럼 2개를 만들어야 가능하다.
메시지 큐는 메시지를 큐 데이터 구조 형태로 관리하는 것을 의미한다. 이는 커널의 전역변수 형태 등 커널에서 전역적으로 관리되며 다른 IPC 방식에 비해서 사용 방법이 매우 직관적이고 간단하다.
메시지 큐는 파이프와 달리 데이터의 흐름이 아니라 메모리 공간이다. 사용할 데이터에 번호를 붙이면서 여러 프로세스가 동시에 데이터를 쉽게 다룰 수 있다. 공유 메모리를 통해 IPC를 구현할 때 쓰기 및 읽기 빈도가 높으면 동기화때문에 기능을 구현하는 것이 매우 복잡해지는데, 이 때 대안으로 메시지 큐를 사용하기도 한다.
파이프, 메시지 큐가 통신을 이용한 설비라면, 공유 메모리는 데이터 자체를 공유하도록 지원하는 설비다.
프로세스의 메모리 영역은 독립적으로 가지며 다른 프로세스가 접근하지 못하도록 반드시 보호되야한다. 하지만 다른 프로세스가 데이터를 사용하도록 해야하는 상황도 필요할 것이다. 파이프를 이용해 통신을 통해 데이터 전달도 가능하지만, 스레드처럼 메모리를 공유하도록 해준다면 더욱 편할 것이다.
공유 메모리는 여러 프로세스에 동일한 메모리 블록에 대한 접근 권한이 부여되어 프로세스가 서로 통신 할 수 있도록 공유 버퍼를 생성하는 것이다. 프로세스간 메모리 영역을 공유해서 사용할 수 있도록 허용해준다.
프로세스가 공유 메모리 할당을 커널에 요청하면, 커널은 해당 프로세스에 메모리 공간을 할당해주고 이후 모든 프로세스는 해당 메모리 영역에 접근할 수 있게 된다. 어떠한 매개체를 통해 데이터를 주고 받는 것이 아닌 메모리 자체를 공유하기 때문에 불필요한 오버헤드가 발생하지 않아서 IPC 중 가장 빠르게 동작한다. 하지만, 같은 메모리 영역을 여러 프로세스가 공유하기 때문에 동기화가 필요하다.
공유 메모리처럼 메모리를 공유해준다. 메모리 맵은 열린 파일을 메모리에 맵핑시켜서 공유하는 방식이다. (즉 공유 매개체가 파일+메모리) 주로 파일로 대용량 데이터를 공유해야 할 때 사용한다.
네트워크 소켓 통신을 통해 데이터를 공유한다. 동일한 컴퓨터의 다른 프로세스나 네트워크의 다른 컴퓨터로 네트워크 인터페이스를 통해 데이터를 전송한다.
디스크에 저장된 데이터 또는 파일 서버에서 제공한 데이터를 기반으로 프로세스간 통신을 한다.
스레드는 프로세스보다도 작은 프로세스 내 실행 흐름의 최소 단위이다.
예전에는 프로그램을 실행하는 흐름이 오로지 프로세스뿐이었으나, 소프트웨어가 진보하면서 하나의 프로그램에서 복잡한 동시 작업을 요구하기 시작하였다. 이를 위해서는 하나의 프로그램이 여러개의 프로세스를 만들어야 했는데 프로세스 특성상 하나의 프로그램이 이러한 동시 작업을 수월하게 할 수가 없었다. 그래서 프로세스보다 더 작은 실행 단위 개념이 만들어지게 되는데 이것이 스레드이다. 프로세스는 여러 개의 스레드를 가질 수 있다.
코드, 데이터, 스택, 힙을 각각 생성하는 프로세스와는 달리 스레드는 코드, 데이터, 힙은 같은 프로세스 내의 스레드끼리 서로 공유하고, 그 외의 영역은 각각 생성된다.
하나의 프로세스가 생성될 때, 기본적으로 하나의 스레드가 같이 생성된다.
멀티스레딩은 하나의 프로세스 내에서 여러 스레드를 구성해 각 스레드가 여러 개의 작업중 하나의 작업을 처리하는 것이다. 스레드들이 공유 메모리를 통해 다수의 작업을 동시에 처리하기때문에 효율성이 높다.
한 프로세스 내의 한 스레드가 중단되어도 다른 스레드는 실행상태 일 수 있기 때문에 중단되지 않은 빠른 처리가 가능하고, 동시성에도 큰 장점이 있다. 하지만, 메모리를 공유하기때문에 한 스레드에 문제가 생기면 다른 스레드에 영향을 끼쳐 작동 불능 상태가 되는 안전성 문제가 존재한다. 멀티스레드의 안전성에 대한 부분을 보완하기 위해서는 Critical Section을 이용해서 공유 메모리를 안전하게 사용해야한다.
참고. 두 개 이상의 프로세스(또는 스레드)가 동시에 공유 자원에 접근해 읽거나 쓰는 상황을 Race Condition(경쟁 상태)라고 한다. 이런 경쟁 상태에서는 공유 자원에 접근하는 타이밍이나 순서에 의해 결과값이 달라질 수 있다.
멀티스레딩 환경에서 발생할 수 있는 안전성 문제를 해결하기 위해서는 Critical Section을 이용해서 공유 메모리를 안전하게 사용해야한다고 위에서 언급했다. 여기서 Critical Section에 대해서 좀 더 알아보자.
공유 메모리, 파일, 데이터, 모니터, 프린터 등 시스템 안에서 여러 프로세스나 스레드가 함께 접근할 수 있는 자원이나 변수 등을 공유 자원이라고 한다. 공유 자원에 접근할 때 순서 등의 이유로 결과가 달라지는 영역을 Critical Section이라고 한다.
즉, 여러 프로세스가 데이터를 공유하며 수행될 때, 각 프로세스에서 공유 데이터를 접근하는 프로그램 코드 부분이 Critical Section에 해당한다.
이 Critical Section에서 자원을 안전하게 사용하기 위해서 뮤텍스, 세마포어, 모니터 세 가지의 방법을 사용할 수 있다. 이 세 가지 방법 모두 상호 배제, 한정 대기, 융통성 이라는 조건을 만족한다.
위 방법들의 토대가 되는 매커니즘은 Lock이다. 락을 이용해서 누군가 Critical Section에 들어간 후 잠그게 된다면, 다음 순서의 누군가는 이전 사람이 락을 해제하고 나오기를 기다린 후, 락을 얻고 다음 차례로 들어가는 것이다.
상호 배제 : 한 프로세스(또는 스레드)가 임계 영역에 들어갔을 때 다른 프로세스(또는 스레드)는 들어갈 수 없다.
한정 대기 : 특정 프로세스(또는 스레드)가 영원히 임계 영역에 들어가지 못하면 안된다.
융통성 : 한 프로세스(또는 스레드)가 다른 프로세스(또는 스레드)의 일을 방해해서는 안된다.
뮤텍스는 공유 자원을 사용하기전에 설정하고 사용한 후에 해제하는 Lock이다.
Lock이 설정되면 다른 스레드는 잠긴 코드 영역에 접근할 수 없다. 또한, 뮤텍스는 하나의 상태(Lock 또는 Unlock)만을 가진다.
세마포어는 일반화된 뮤텍스이다. 간단한 정수 값과 두가지 함수(wait(), signal())로 공유 자원에 대한 접근을 처리한다.
wait() 함수는 자신의 차례가 올 때까지 기다리는 함수이며, signal() 함수는 다음 프로세스(또는 스레드)로 순서를 넘겨주는 함수이다.
프로세스가 공유 자원에 접근하면 세마포어에서 wait() 작업을 수행하고 프로세스가 공유 자원을 해제하면 세마포어에서 signal() 작업을 수행한다. 세마포어는 조건 변수가 없고 프로세스가 세마포어 값을 수정할 때 다른 프로세스는 동시에 세마포어 값을 수정할 수 없다.
모니터는 둘 이상의 스레드나 프로세스가 공유 자원에 안전하게 접근할 수 있도록 공유 자원을 숨기고 해당 접근에 대해 인터페이스만 제공한다.
모니터는 모니터큐를 통해 공유 자원에 대한 작업들을 순차적으로 처리한다.
데드락은 두 개 이상의 프로세스나 스레드가 서로 자원을 얻지 못해서 다음 처리를 하지 못하는 상태로 무한히 다음 자원을 기다리게 되는 상태를 말한다. 시스템적으로 한정된 자원을 여러 곳에서 사용하려고 할 때 발생한다.
예를 들어, 프로세스 A가 프로세스 B의 어떤 자원을 요청할 때 프로세스 B도 프로세스 A가 점유하고 있는 자원을 요청하고 있는 것이다.
아래의 4가지 조건이 모두 성립해야 데드락이 발생한다. 하나라도 성립하지 않으면 해결 가능하다.