• 운영체제와 프로세스, 스레드 이해하기

    2024. 10. 1.

    by. haong_

    이번 시간에는 운영체제 탄생 배경과 프로세스와 스레드에 대해 알아보자. 

    CPU에서 운영체제까지

    컴퓨터 핵심은 CPU(중앙처리장치)이다. CPU는 명령을 받아 그대로 실행하는 역할만 하기 때문에 혼란스러운 개념없이 컴퓨터 시스템을 이해하는 시작점이 될 수 있다. CPU는 PC(프로그램 카운터) 라고 불리는 레지스터에서 다음 실행할 명령어의 주소를 가져와 실행한다. 

    지난 시간에 컴파일러와 링커를 통해 소스 파일이 실행 파일로 변환되는 과정을 봤는데, 이 실행 파일에서 명령어를 메모리에 저장하고 메모리를 읽어 CPU가 명령어를 수행하는 흐름이다. 이때 첫 시작점은 main 이라는 함수로 이 함수의 메모리 주소를 PC 레지스터에 기록하여 프로그램이 시작된다.

    정리하면 다음과 같다.

    1. 프로그램을 적재할 수 있는 적절한 크기의 메모리 영역을 찾고
    2. CPU 레지스터를 초기화하고 함수의 진입 포인트를 찾아 PC 레지스터를 설정

    위와 같은 과정으로 직접 CPU에 프로그램을 실행하도록 할 수 있지만, 매우 번거롭고 반복적인 작업이 많고 CPU에게 한가지 작업만 시킬 수 있어 비효율적이기 때문에 운영체제를 통해 이런 작업을 대신 할 수 있게 했다. 

    운영체제는 CPU의 성능을 극대화하고 효율적인 프로그램 실행을 지원한다. CPU가 동시에 여러 작업을 수행하는 것처럼 보이지만 실제로는 빠르게 여러 작업간 전환하며 실행하는 방식으로 멀티태스킹을 하고 있다. A, B 프로그램이 동시에 실행 중일때를 예를 들어보자. A 실행중에 B의 실행요청이 오면 A의 상태를 기록해놓,고 B의 명령어를 처리한 다음 B의 상태를 기록하고, 다시 A로 돌아온다. 매우 빠르게 이 작업이 일어나 마치 동시에 프로그램이 실행되는 것처럼 보인다.

    이처럼 시스템 간의 자원 사용과 상태에 대한 조율이 필요한데 바로 이 부분을 운영체제가 처리해주고 있는 것이다. 앞서 말한 상태들을 컨텍스트(context)라고 부르며 프로그램 간 상태를 저장하고 전환하는 과정을 컨텍스트 스위칭이라고 부른다.

    프로세스(Process)

    운영체제에서 프로그램을 실행하는 기본단위를 프로세스라고 한다. 앞서 본것 처럼 각 프로그램마다 컨텍스트가 존재하며, 이를 가지고 있어야 언제든 컨텍스트 스위칭이 가능하기 때문에 운영체제에서를 프로세스라는 형태로 프로그램을 관리한다. 각 프로세스는 독립적인 메모리 공간을 가지고 있어 다른 프로세스가 영향을 미치지 못한다. 하지만 그 대신 프로세스간 통신이 어렵고, 프로세스의 생성과 종료에 많은 자원을 소모해 효율적으로 병렬처리를 구현하기 힘들다.

    스레드(Thread)

    효율적인 동시성 처리를 위해 등장한 개념이 바로 스레드이다. 하나의 프로세스에서 하나만 실행하는 것이 아닌 여러 실행 흐름이 존재할 수 있게 했는데 이 실행 흐름을 스레드라고 한다. 프로세스의 독립된 공간 때문에 통신이 어려웠는데, 스레드는 프로세스 안에서 공유 공간을 가지면서도 독립적인 스택영역을 가져 보다 효율적으로 병렬처리를 할 수 있게 되었다. 

    스레드는 프로세스와 달리 자원을 공유하기 때문에 서로 자원을 차지하기 위해 충돌이 일어날 수 있다. 여러 스레드가 동시에 자원에 접근하는 것을 막기 위해 하나의 스레드만 자원을 사용할 수 있도록 상호배제(Mutual Exclusion) 같은 제어 기술이 필요해졌다. 제어에는 뮤텍스, 세마포어 같은 Lock 기술이 있는데 간단히만 알아보자.

    • 뮤텍스: 하나의 스레드만 자원에 접근 할 수 있도록 락을 거는 방식으로, 자원을 사용하는 동안 다른 스레드는 대기
    • 세마포어: 자원에 접근할 수 있는 스레드 수를 지정하는 방식. 

    + 파이썬에서의 GIL(Global Interpreter Lock) 

    알아두면 좋을 추가내용

    개발하다보면 파이썬 병렬 처리가 안돼~ 라는 말을 들어 본 적이 있을 것이다. 그것은 파이썬 GIL이라는 특수한 메커니즘 때문에 그렇다. 다중 스레드가 되면 공유 자원 관리를 해야 하는데, 파이썬에서는 한번에 하나의 스레드만 파이썬 코드를 실행할 수 있도록 보장한다. 

    왜 그런지 이해하려면 파이썬의 메모리 관리 방법을 알아야한다. 파이썬에서는 함수 호출 시 call by object reference(객체 참조)라는 방식으로 값을 전달하며, 파이썬 객체는 모두 레퍼런스 카운팅을 통해 메모리에서 관리된다. 해당 함수의 참조 횟수를 메모리에 가지고 있는데, 이 과정에서 여러 스레드가 동시에 동일한 레퍼런스 카운트를 수정하면 잘못된 값이 될 수가 있다. 이런 데이터 경합을 방지하기 위해 GIL이 도입되 글로벌로 락을 건다. 

    그래서 파이썬에서는 멀티스레딩의 이점이 제한되며 특히 CPU 바운드 작업(복잡한 계산 등)에서는 성능제약이 발생한다. 하지만 I/O 바운드 작업(네트워크 요청, 파일 읽기/쓰기 등) GIL의 영향을 상대적으로 덜받는다. I/O 작업동안 스레드가 대기 상태로 들어가면서 GIL을 해제 하기 때문에 다른 스레드가 GIL을 획득하고 실행 할 수 있다. 

    스레드 풀 

    스레드풀(Thread Pool)은 미리 정의된 스레드 그룹을 만들어 두고, 작업이 발생할 때마다 새로운 스레드를 생성하는 대신 기존 스레드를 재사용하는 구조이다. 스레드를 반복적으로 생성하고 소멸시키는 비용을 줄이고, 스레드의 수를 제어하여 시스템 자원을 보다 효율적으로 사용할 수 있다. 

    그렇다면 스레드 풀의 적정 개수는 몇개일까? CPU 바운드 작업시에는 스레드 수가 CPU 코어수와 기본적으로 동일하다면 CPU 리소스를 충분히 활용할 수 있다. 반면 I/O 바운드 작업에는 좀 더 복잡한 계산식이 있다. 

    N X (1 + WT / CT)
    • WT(Wait Time): 입출력 대기 시간
    • CT(Computing Time): CPU 연산에 필요한 시간
    • N: 코어 개수 

    WT와 CT가 동일하다면 대략 2N개의 스레드가 있어야 CPU 리소스를 최대한 활용할 수 있다. 하지만 이는 이론적인 값에 불과하기 때문에 실제 상황을 기반으로 테스트를 실시해 필요한 스레드 수를 결정하는 것이 필요하다. 

    이상으로 오늘 내용은 마무리하며 다음 시간에는 스택영역 힙영역 등 메모리에 대한 내용으로 돌아올 예정이다. 

    'CS' 카테고리의 다른 글

    데이터베이스 index와 구조  (0) 2024.10.24
    메모리 관리: 스택영역과 힙영역  (1) 2024.10.07
    컴파일 과정과 링커 파헤치기  (1) 2024.09.25
    파이썬의 동기와 비동기 처리  (0) 2024.09.10
    CGI, WGSI, ASGI 알아보기  (0) 2024.09.04

    댓글