[OS]Process

·

5 min read

1. Process 란?

우리는 현대 운영체제가 많은 작업을 동시에 수행하거나 멀티태스킹하는 것에 익숙합니다.

일반적으로 프로세스는 실행중인 프로그램으로 정의한다.

각 프로세스를 커널이 이러한 실행 중인 작업들을 추적하기 위해 유지하는 요소들의 묶음으로 생각할 수 있습니다.

*반면에 프로그램은 디스크 상에 존재하면 실행을 위한 명령어와 정적 데이터의 묶음이다. 이 명령어와 데이터 묶음을 읽고 실행하여 프로그램에 생명을 불어넣는 것이 운영체제이다.

2. Process의 요소

2.1 Process ID

Process ID (PID)는 운영 체제에 의해 할당되며 각 실행중인 프로세스마다 고유하게 갖고 있습니다.

2.2 Memory

앞으로 프로세스가 메모리를 어떻게 할당받는지 정리하려고 합니다.

이것은 운영 체제가 작동하는 방식의 가장 기본적인 부분 중 하나입니다. 하지만, 지금은 각 프로세스가 자신만의 메모리 영역을 가진다는 것을 알아두는 것으로 충분합니다.

이 메모리에는 모든 프로그램 코드와 변수, 그리고 할당된 다른 저장소가 저장됩니다.

메모리의 일부는 프로세스 간에 공유될 수 있으며*('공유 메모리'라고 함)*, 이는 종종 구형 운영 체제에서의 원래 구현을 따라 *'System Five Shared Memory(또는 SysV SHM)'*로 불립니다.

프로세스가 사용할 수 있는 또 다른 중요한 개념은 디스크 상의 파일을 메모리에 매핑하는 것입니다. 이는 파일을 열고 read()와 write() 같은 명령어를 사용하는 대신에 파일이 다른 종류의 RAM처럼 보이게 하는 것을 의미합니다. mmap된 영역은 읽기, 쓰기, 실행 등의 권한을 가지고 있으며 이를 추적해야 합니다. 우리가 알다시피, 운영 체제의 역할은 보안과 안정성을 유지하는 것이므로, 프로세스가 읽기 전용 영역에 쓰려고 할 때 이를 확인하고 오류를 반환해야 합니다.

2.2.1 코드와 데이터

프로세스는 코드데이터 섹션으로 더 세분화될 수 있습니다. 프로그램 코드와 데이터는 운영 체제로부터 다른 권한이 필요하며, 코드 공유를 용이하게 하기 위해 별도로 유지되어야 합니다(나중에 보게 될 것처럼). 운영 체제는 프로그램 코드에 읽기 및 실행 권한을 부여해야 하지만, 일반적으로 쓰기 권한은 부여하지 않습니다. 반면에 데이터(변수)는 읽기 및 쓰기 권한이 필요하지만 실행 가능해선 안 됩니다.

2.2.2 Stack

프로세스의 또 다른 매우 중요한 부분은 스택이라고 불리는 메모리 영역입니다. 이는 프로세스의 데이터 섹션의 일부로 간주될 수 있으며, 어떤 프로그램의 실행과 밀접하게 관련되어 있습니다.

스택은 접시 더미처럼 작동하는 일반적인 데이터 구조입니다; 즉, 항목을 푸시할 수 있습니다(접시 더미 위에 접시를 올림), 그러면 그 항목이 가장 위의 항목이 되거나, 항목을 팝할 수 있습니다(접시를 내려 놓고 이전의 접시를 드러냄).

스택은 함수 호출에 있어 기본적입니다. 함수가 호출될 때마다 새로운 스택 프레임을 얻습니다. 이는 메모리의 영역으로, 일반적으로 최소한 완료 시 반환할 주소, 함수에 대한 입력 인수 및 지역 변수에 대한 공간을 포함합니다.

관례적으로 스택은 보통 아래로 성장합니다. 이는 스택이 메모리의 높은 주소에서 시작하여 점차 낮아진다는 것을 의미합니다.

스택이 있음으로써 함수의 많은 특징들이 나타나는 것을 볼 수 있습니다.

  • 각 함수는 입력 인수의 자체 복사본을 가집니다. 이는 각 함수에 새로운 스택 프레임이 할당되고 그 인수들이 메모리의 새로운 영역에 위치하기 때문입니다.

  • 이것이 함수 내부에서 정의된 변수가 다른 함수에 의해 볼 수 없는 이유입니다. 전역 변수(모든 함수에서 볼 수 있는)는 데이터 메모리의 별도 영역에 유지됩니다.

  • 이는 재귀 호출을 용이하게 합니다. 이는 함수가 스스로를 다시 호출할 수 있음을 의미합니다. 왜냐하면 모든 지역 변수들을 위한 새로운 스택 프레임이 생성될 것이기 때문입니다.

  • 각 프레임은 반환할 주소를 포함합니다. C 언어는 함수에서 단일 값을 반환하는 것만을 허용하므로, 관례적으로 이 값은 스택이 아닌 지정된 레지스터를 통해 호출 함수로 반환됩니다.

  • 각 프레임이 이전 프레임을 참조하기 때문에, 디버거는 스택을 따라 거꾸로 "걸어갈" 수 있습니다. 이를 통해 디버거는 이 함수로 이어지는 모든 함수 호출을 보여주는 스택 트레이스를 생성할 수 있습니다. 이는 디버깅에 매우 유용합니다.

  • 함수가 작동하는 방식이 스택의 특성에 정확히 맞아떨어지는 것을 볼 수 있습니다. 어떤 함수도 다른 함수를 호출할 수 있으며, 그러면 그 함수가 스택의 가장 상위 함수가 됩니다(스택 위에 올려짐). 결국 그 함수는 자신을 호출한 함수로 반환합니다(스택에서 자신을 제거함).

    스택은 함수 호출을 느리게 만듭니다. 왜냐하면 값이 레지스터에서 메모리로 이동해야 하기 때문입니다. 일부 아키텍처는 인수를 직접 레지스터에 전달할 수 있지만, 각 함수가 각 인수의 고유한 복사본을 얻는다는 의미를 유지하기 위해 레지스터가 회전해야 합니다.

  • 스택 오버플로라는 용어를 들어본 적이 있을 겁니다. 이는 거짓 값들을 전달하여 시스템을 해킹하는 일반적인 방법입니다. 프로그래머로서 키보드나 네트워크를 통해 스택 변수에 임의의 입력을 허용한다면, 그 데이터의 크기를 명시적으로 지정해야 합니다.

    검증되지 않은 어떤 양의 데이터도 허용하는 것은 단순히 메모리를 덮어쓰게 됩니다. 일반적으로 이는 충돌로 이어지지만, 일부 사람들은 메모리를 충분히 덮어쓰기만 해서 스택 프레임의 반환 주소 부분에 특정 값을 위치시킬 수 있다는 것을 깨달았습니다. 함수가 완료될 때 올바른 위치(호출된 곳)로 반환하는 대신에, 그들

    이 방금 보낸 데이터로 반환하도록 만들 수 있습니다. 만약 그 데이터가 시스템을 해킹하는 바이너리 실행 코드(예: 사용자에게 루트 권한으로 터미널을 시작하는 것)를 포함한다면, 컴퓨터는 침해당한 것입니다.

    이는 스택이 아래로 성장하는 반면 데이터는 "위로" 읽히기 때문에 발생합니다(즉, 낮은 주소에서 높은 주소로).

    이를 방지하는 몇 가지 방법이 있습니다. 먼저 프로그래머로서 변수에 받는 데이터의 양을 항상 확인해야 합니다. 운영 체제는 프로그램 내부로 어떤 코드도 전달하려는 악의적 사용자가 있더라도 프로세서가 어떤 코드도 실행하지 않도록 스택을 실행 불가능으로 표시함으로써 프로그래머를 대신해 이 문제를 방지할 수 있습니다. 현대의 아키텍처와 운영 체제는 이 기능을 지원합니다.

  • 스택은 결국 컴파일러에 의해 관리됩니다. 왜냐하면 프로그램 코드를 생성하는 것이 컴파일러의 책임이기 때문입니다. 운영 체제에게는 스택이 프로세스의 다른 메모리 영역처럼 보입니다.

스택의 현재 성장을 추적하기 위해, 하드웨어는 스택 포인터로 정의된 레지스터를 정의합니다. 컴파일러(또는 어셈블리어로 작성할 때 프로그래머)는 이 레지스터를 사용하여 현재 스택의 상단을 추적합니다.

$ cat sp.c
void function(void)
{
        int i = 100;
        int j = 200;
        int k = 300;
}

$ gcc -fomit-frame-pointer -S sp.c

$ cat sp.s
        .file   "sp.c"
        .text
.globl function
        .type   function, @function
function:
        subl    $16, %esp
        movl    $100, 4(%esp)
        movl    $200, 8(%esp)
        movl    $300, 12(%esp)
        addl    $16, %esp
        ret
        .size   function, .-function
        .ident  "GCC: (GNU) 4.0.2 20050806 (prerelease) (Debian 4.0.1-4)"
        .section        .note.GNU-stack,"",@progbits

여기서는 간단한 함수가 스택에 세 개의 변수를 할당하는 것을 보여줍니다. 디스어셈블리는 x86 아키텍처에서 스택 포인터의 사용을 보여줍니다. 먼저, 지역 변수들을 위한 공간을 스택에 할당합니다. 스택이 아래로 성장하기 때문에 스택 포인터에 있는 값을 감소시킵니다. 값 16은 지역 변수들을 위해 충분히 큰 값이지만, 실제로 필요한 크기(예를 들어, 3개의 4바이트 int 값에는 실제로 12바이트만 필요함)와 정확히 일치하지 않을 수도 있습니다. 이는 컴파일러가 요구하는 특정 경계에서 메모리 상의 스택 정렬을 유지하기 위함입니다.

그런 다음 스택 메모리로 값을 이동시킵니다(그리고 실제 함수에서 이들을 사용합니다). 마지막으로, 부모 함수로 돌아가기 전에 스택에서 값을 "팝"하여 시작하기 전의 위치로 스택 포인터를 다시 이동시킵니다.

이 예제는 스택의 작동 방식을 보여주며, 함수가 지역 변수를 어떻게 관리하는지를 설명합니다. 스택은 함수 호출과 반환 과정에서 중요한 역할을 하며, 이 과정에서 스택 포인터의 조정이 필수적입니다. 이러한 메커니즘은 프로그램의 실행 흐름과 메모리 관리에 핵심적인 요소입니다.