프로세스와 하드웨어

Soyun Park·2023년 10월 19일
0
post-thumbnail

1. 프로세스란 무엇인가?

1-1. 컴퓨터의 구조

  • 메모리
    • 0 또는 1의 열을 저장할 수 있는 장치이다.
    • 보통 8비트를 1바이트로 통합하므로 바이트 열을 저장한다고 할 수도 있다.
    • 바이트를 담을 수 있는 상자마다 번호가 매겨져 있어 그 번호를 통해 그 내용물을 볼 수 있다. 이 번호를 메모리의 주소라고 한다.
  • CPU
    • 메모리에 저장된 바이트 열을 바꿀 수 있는 장치이다.
    • CPU의 내부에는 레지스터라는 작은 저장 장치가 있다.
    • CPU는 메모리로부터 레지스터에 데이터를 복사하여 연산을 수행한 후, 다시 메모리에 되돌려서 메모리의 값을 바꾼다.
  • 다른 장치들로는 HDD하드 디스크 드라이브, SSD솔리드 스테이트 드라이브가 있다.

1-2. 기계어

  • CPU가 읽어 들이는 바이트 열의 규칙 체계를 말한다.
  • CPU가 직접 읽고 실행할 수 있는 언어는 기계어 뿐이다.
  • C언어로 작성된 코드도 일단 기계어로 변환된 후에야 컴퓨터에서 실행할 수 있다.

1-3. 다양한 컴퓨터 아키텍처

  • CPU를 개발하고 있는 업체별로 컴퓨터 설계방식, 즉 컴퓨터 아키텍처가 달라 서로 다른 기계어 체계를 갖추고 있다.
  • 전세계의 개인용 컴퓨터 대부분의 컴퓨터 아키텍처는 x86 이다.
  • x86 이외도 스마트폰에 사용하는 ARM 아키텍처 가 있다.

1-4. 멀티태스크

  • 기본적으로 하나의 CPU와 메모리가 있을 때 어떤 한순간에 돌아갈 수 있는 프로세스는 오직 한 개뿐이다.
  • 물리적으로 CPU와 메모리의 조합을 늘린다면 여러 개의 프로세스를 동시에 돌릴 수 있다.
  • 그러나 ps -ef 를 실행해보면 일반적으로 수십, 수백 개의 프로세스를 동시에 돌리고 있다.
  • 따라서 물리적인 CPU와 메모리가 하나씩 밖에 없어도 개별 프로세스 입장에서는 전용 CPU와 메모리가 있는 것처럼 하기 위해 가상 CPU가상 메모리가 등장했다.

1-5. 가상 CPU와 가상 메모리의 원리

  • 가상 CPU
    • 매우 짧은 시간 단위0.0001초 주기로 실행하는 프로세스를 전환한다.
    • 그러면 프로세스는 자기만을 위한 CPU가 있는 것처럼 사용할 수 있게 된다.
  • 가상 메모리
    • 커널이 각 프로세스에 0번지부터 시작하는 가상의 메모리 주소 공간을 제공한다.
    • 그러면 각 프로세스가 사용하는 가상의 주소는 실제 접근이 일어날 때 커널에 의해 실제 메모리 주소로 변환된다.
    • 이 때 프로세스가 사용하는 주소를 논리 주소, 실제 주소를 물리 주소라고 한다.
  • 장점
    • 각 프로세스마다 별도의 논리적인 주소 공간을 사용하면 다른 프로세스의 메모리에 접근할 수 없기 때문에 전체 시스템의 안전성이 향상된다.
    • 프로세스가 비정상적으로 동작해도 다른 프로세스의 메모리 내용을 손상할 수 없다.


1-6. 가상 CPU

  • 각 프로세스에 할당되는 시간을 타임 슬라이스라고 한다.
  • 프로세스별로 우선순위가 달라 타임 슬라이스를 동일하게 부여받지는 않는다.
  • 커널에서 프로세스에 타임 슬라이스를 배분하는 주체를 스케쥴러 또는 디스패처라고 한다.

1-7. 가상 메모리

  • 프로세스의 주소 공간과 물리주소, 논리 주소의 매핑은 페이지 단위로 관리된다.
  • 프로세스별 주소 공간이 물리 메모리에서 연속적이지는 않고 페이지별로 위치가 다 다르다.
  • 물리적 주소로 매핑되지 않은 페이지도 있다. 이는 필요한 순간에 물리적 주소를 할당하거나 의도적으로 접근 금지 페이지로 남겨둔 것이다.
  • 접근 금지 페이지의 예로 NULL 포인터가 있다. 리눅스에서 접근을 검출하기 위해 커널이 의도적으로 논리 주소의 처음 몇 페이지에 물리적 주소를 할당하지 않는다.

1-8. 가상 메모리 매커니즘의 응용

  • 페이징
    • 페이징은 디스크를 물리 메모리 대신에 사용하는 매커니즘이다.
    • 물리 메모리가 부족하게 됐을 때 다음과 같이 동작한다.
      1. 커널이 별로 사용하지 않는 페이지를 선정하여 스토리지에 기록하고 논리 주소와의 매핑을 해제한다.
      2. 프로세스가 해당 페이지가 필요하면 그 순간에 커널이 프로세스를 중지한다.
      3. 커널은 스토리지에서 페이지를 읽어들이고 논리 주소와 매핑한 후 프로세스를 재개한다.
  • 메모리 맵 파일
    • 파일을 메모리에 매핑하여 메모리를 읽으면 파일을 읽은 것이 되고, 메모리에 쓰면 파일에는 쓰는 것이 되게 하는 매커니즘이다.
    • 처음에는 논리 주소와 물리 주소 사이에 아무런 매핑도 가지지 않지만 액세스가 생긴 시점에서 커널이 메모리에 읽어 들이고 그 메모리를 논리 주소에 매핑한다.
  • 공유 메모리
    • 특정 범위의 물리 메모리를 여러 프로세스에서 공유하는 매커니즘이다.
    • 예를 들어 거대한 이미지 데이터를 여러 프로세스에서 편집하고 싶은 경우에 사용한다.
    • 하나의 물리 메모리 페이지를 두 프로세스의 논리 주소에 매핑하는 구조이다.

1-9. 주소 공간의 구조

  • 텍스트 영역
    • 기계어 코드, 즉 프로그램 코드가 배치되는 공간을 말한다.
  • 데이터 영역
    • 전역 변수나 함수 내 정적 변수 중에서 초깃값이 있는 것과 문자열 리터럴 등이 보관된다.
  • BSS 영역
    • 전역 변수나 함수 내 정적 변수 중에서 초깃값이 없는 것이 보관된다.
    • 실행 파일에는 실제 데이터의 크기만 기록된다.
  • 힙 영역
    • malloc()이 관리하는 영역이다. 이 영역은 실행 시에 확대 또는 축소된다.
  • 스택 영역
    • 함수 호출에 따라 데이터가 쌓이는 곳이다.
    • 함수의 인자나지역 변수 등이 보관된다.

1-10. 주소 공간 들여다보기

  • 명령어 cat /proc/n/maps 는 프로세스 ID가 n인 프로세스의 메모리 사용 구조를 볼 수 있다.

    • 맨 왼쪽 열에는 논리 주소의 범위가 16진수로 표시되어 있다.
    • 맨 오른쪽 열에는 nmap()으로 메모리에 매핑된 파일의 이름이 기재된다.
    • 왼쪽에서 두 번째 열에는 메모리 영역의 속성이 표시되어 있다.
    • rwx 는 파일과 마찬가지로 읽기, 쓰기, 실행 권한이다.
    • p 는 private한 영역, 즉 그 프로세스만 액세스할 수 있음을 의미한다.
    • s 는 shared를 의미하여 공유 메모리 매커니즘에 의해 다른 프로세스와 공유되는 경우를 말한다.
    • 속성이 r-xp 고 nmap으로 매핑된 파일이 있는 영역은 텍스트 영역이다.
    • 속성이 rw-p 고 nmap으로 매핑된 파일이 있는 영역은 BSS 영역이다.
    • 속성이 rw-p 고 [stack]이라고 표시되는 영역은 스택 영역이다.
    • [vdso]나 [vsyscall]로 표시되는 영역은 시스템 콜을 위한 보조 데이터를 위해 사용한다.


2. 메모리 관리 관련 API

2-1. C언어의 메모리 관리 API

  • C언어에서 메모리를 확보하는 방법은 언제 어느 영역에 할당하는지에 따라 다음과 같이 분류할 수 있다.
    1. 빌드 시에 알고 있는 크기를 BSS 영역에서 취함
    2. 빌드 시에 알고 있는 크기를 실행 시에 스택 영역에서 취함
    3. 실행 시에 결정되는 크기를 힙 영역에서 취함
    4. 실행 시에 결정되는 크기를 스택 영역에서 취함
  • malloc() 등을 사용하여 실행 시에 메모리를 할당하는 것을 동적 할당이라고 한다.
  • 반대로 프로그램을 빌드할 때 결정되는 것을 정적이라고 한다. 전역 변수나 정적 메모리 할당에 해당한다.

2-2. malloc(3)

#include <stdlib.h>
void *malloc(size_t size);
  • malloc()은 size만큼의 바이트를 힙 영역에 할당하고 그 첫 번째 주소에 대한 포인터를 반환한다. 반환값의 타입은 void*이다.
  • 할당될 메모리에 초깃값을 지정하고 싶으면 calloc()을 사용하거나 memset()을 함께 사용한다.
  • 확보한 메모리는 free()로 반드시 해제해야 한다.

2-3. calloc(3)

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
  • calloc(3) nmemb x size 바이트의 메모리를 힙 영역에 할당하고 그 첫 주소에 대한 포인터를 반환한다.
  • 할당된 메모리는 초깃값으로 0이 설정된다.

2-4. realloc(3)

#include <stdlib.h>
void *realloc(void *ptr, size_t size);
  • realloc()은 malloc()이나 calloc()으로 할당한 메모리 영역의 크기를 인자로 지정한 size바이트로 확장 또는 축소한다.
  • ptr이 가리키는 주소가 이동할 경우 ptr의 내용이 복사된다.
  • realloc()의 반환값을 원래의 포인터에 그대로 대입해서는 안된다.
  • NULL을 반환했을 때 원래의 ptr에 액세스 할 수 없게 되어 ptr을 사용할 수도, free()도 할 수 없게 된다.

2-5. free(3)

#include <stdlib.h>
void free(void *ptr);
  • malloc(), calloc(), realloc()으로 힙 영역에 할당한 메모리 ptr을 해제한다.
  • 일단 free()로 해제하면 그 메모리에 접근해서는 안된다.

2-6. brk(2)

  • malloc()은 내부적으로 brk() 또는 sbrk()라는 시스템 콜을 사용한다.
  • malloc()을 호출하는 프로그램을 strace 명령어로 추적하면 해당 사항을 확인할 수 있다.



3. 프로그램이 완성될 때까지

3-1. 전처리

  • #include나 #ifdef, #define을 처리해서 순수한 C코드로 변환한다.
  • gcc -E 옵션을 실행하면 전처리만 수행한 결과를 표준 출력으로 출력한다.


3-2. 컴파일

  • C언어 소스코드를 어셈블리어 코드로 변환한다.
  • gcc의 경우 /usr/lib/gcc 또는 /usr/libexec/gcc 밑에 있는 ccl이라는 프로그램이 컴파일을 한다.
  • gcc -S 옵션을 실행하면 ex.c를 컴파일까지 수행한 결과를 ex.s라는 파일에 출력한다.


3-3. 어셈블

  • 어셈블리어로 된 코드*.s를 기계어를 포함한 오브젝트 파일*.o로 변환한다.
  • 오브젝트 파일의 대표적인 형식은 다음과 같다.
    • ELFExecutable and Linking Format
    • COFFCommon Object File Format
    • a.outassembler output
  • 어셈블 작업은 gcc가 아닌 binutils라는 패키지에 포함된 as라는 명령어가 담당한다.
  • gcc -c 옵션을 실행하면 ex.c를 어셈블까지 수행한 결과를 ex.o라는 파일에 출력한다.

3-4. 링크

  • 오브젝트 파일*.o로부터 실행 파일 또는 라이브러리*.a 또는 *.so을 만든다.
  • 실행 파일은 그 자체가 하나의 오브젝트 파일로, 리눅스라면 ELF 포맷이 사용된다.

3-5. 정적 링크

  • 정적 링크에 사용되는 라이브러리이며, 파일 이름은 *.a이다.
  • *.a 파일은 ar이라는 프로그램으로 만든 아카이브 파일로 안에 많은 오브젝트 파일이 포함되어 있다.
  • 정적 링크에서는 정적 라이브러리에 있는 필요한 함수가 생성하는 오브젝트 파일에 직접 삽입하므로 빌드할 때만 있으면 된다.

3-6. 동적 링크

  • 공유 라이브러리는 동적 링크에 사용되는 라이브러리이며 파일 이름은 *.so이다.
  • 공유 라이브러리는 정적 라이브러리와 달리 전체가 하나의 오브젝트 파일로 구성된다.
  • 링크 로더가 먼저 실행 파일과 공유 라이브러리를 메모리상에서 결합하므로 빌드 타임과 런타임에 모두 필요하다.
  • 현재 사용되고 있는 링크 로더는 lib64/ld-linux-x86-64.so.2 다.
  • 실행 파일에 대한 링크 로더의 이름은 실행 파일에 기술되어 있어 커널이 프로그램을 시작할 때 참조하여 처리한다.

3-7. 정적 링크와 동적 링크, 어느 쪽을 사용해야 좋은가

  • 두가지 링크 중 항상 동적 링크를 사용하는 것이 좋다.

  • 대부분의 프로그램은 암묵적으로 lib.so.6 와 동적 링크 되어 있는데 이를 file 명령어와 ldd 명령어로 확인할 수 있다.

  • file 명령어를 통해 hello 프로그램이 dynamically linked 되었음을 확인할 수 있다.

  • ldd 명령어는 동적 링크된 공유 라이브러리를 표시해주고 있는데 마지막의 /lib64/ld-linux-x86-64.so.2가 실행 시에 사용되는 링크 로더를 의미한다.

3-8. gcc에 의한 동적 링크

  • gcc -l 옵션은 라이브러리를 링크하고 싶을 때 사용한다.
  • 예를 들어 수학 함수인 sin()을 사용하고자 한다면 공유 라이브러리 libm.so를 사용하기 위해 다음과 같이 빌드한다.
    $ gcc calc.c -lm -o calc
  • -l 옵션을 사용할 때는 라이브러리 앞부분의 'lib'을 떼어내고 사용한다. 그래서 libm을 링크하고 싶다면 -lm로 지정한다.
  • libm과 연결되었는지 여부는 ldd 명령어로 확인할 수 있다.


3-9. 동적 로드

  • 동적 로드는 모든 링크 작업을 실행할 때 수행하는 방법이다.
  • 정적 로드와 달리 함수 이름 확인도 하지 않는다. 프로그램 실행 중에 '이 라이브러리에 있는 이런 함수를 사용하고 싶다'고 명시만 하면 된다.
  • 즉, 보통 컴파일러가 하는 일을 실행 시에 수행하는 방법이라고 볼 수 있다.
  • 동적 로드 방식은 ld-linux-x86-64.so.2가 실행 중에 프로세스의 메모리 공간에 있으므로 이것을 실행 중에 동작시킨다.

0개의 댓글