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-9. 주소 공간의 구조
- 텍스트 영역
- 기계어 코드, 즉 프로그램 코드가 배치되는 공간을 말한다.
- 데이터 영역
- 전역 변수나 함수 내 정적 변수 중에서 초깃값이 있는 것과 문자열 리터럴 등이 보관된다.
- BSS 영역
- 전역 변수나 함수 내 정적 변수 중에서 초깃값이 없는 것이 보관된다.
- 실행 파일에는 실제 데이터의 크기만 기록된다.
- 힙 영역
- malloc()이 관리하는 영역이다. 이 영역은 실행 시에 확대 또는 축소된다.
- 스택 영역
- 함수 호출에 따라 데이터가 쌓이는 곳이다.
- 함수의 인자나지역 변수 등이 보관된다.
1-10. 주소 공간 들여다보기
2. 메모리 관리 관련 API
2-1. C언어의 메모리 관리 API
- C언어에서 메모리를 확보하는 방법은 언제 어느 영역에 할당하는지에 따라 다음과 같이 분류할 수 있다.
- 빌드 시에 알고 있는 크기를 BSS 영역에서 취함
- 빌드 시에 알고 있는 크기를 실행 시에 스택 영역에서 취함
- 실행 시에 결정되는 크기를 힙 영역에서 취함
- 실행 시에 결정되는 크기를 스택 영역에서 취함
- 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
가 실행 중에 프로세스의 메모리 공간에 있으므로 이것을 실행 중에 동작시킨다.