본문 바로가기
프로그래밍/UNIX 고급 프로그래밍

제7장 프로세스 환경

by Ohdumak 2017. 11. 2.

2017/10/24 - [프로그래밍/UNIX 고급 프로그래밍] - 제6장 시스템 자료 파일과 시스템 정보



7.1 소개


다음 장에서 프로세스 제어를 위한 기본 수단들을 살펴보기 전에, 먼저 하나의 프로세스가 실행되는 환경을 파악할 필요가 있다.




7.2 main 함수


하나의 C프로그램의 실행은 main이라는 함수의 호출로부터 시작된다. main함수의 원형(proto-type)은 다음과 같다.

int main(int argc, char *argv[]);

여기서 argc는 명령줄 인수(command-line argument)들의 개수이고 argv는 그 인수들을 가리키는 포인터들의 배열이다.

 C 프로그램을 커널이 exec류 함수들 중 하나를 이용해서 실행할 때, main함수가 호출되기 전에 특별한 시동 루틴(start-up routine)이 먼저 호출 된다. 실행 가능한 프로그램 파일에는 이 루틴이 프로그램의 시작 주소로서 지정되어 있다. 이는 C컴파일러가 실행한 링커(linker)가 설정해 둔 것이다. 이 시동 루틴은 여러 가지 값들(명령줄 인수들과 환경)을 커널로부터 받아서 방금 전에 말한 main함수의 실행에 필요한 사항들을 준비한다.



7.3 프로세스 종료


하나의 프로세스를 종료하는 방법은 여덟 가지이다.

정상적인 종료 방법

  1. main 함수로부터의 반환
  2. exit 호출
  3. _exit 또는 _Exit 호출
  4. 마지막 스레드를 시작한 루틴으로부터의 반환
  5. 마지막 스레드에서 pthread_exit 호출

비정상적인 종료 방법

  1. abort 호출
  2. 종료 신호 수신
  3. 마지막 스레드가 취소 요청에 반응함

종료 함수들

프로그램을 정상적으로 종료시키는 세 가지 함수가 있다. _exit함수와 _Exit함수는 커널로 즉시 반환되며, exit함수는 일정한 정리(cleanup)작업을 수행한 후 커널로 변환된다.

 예나 지금이나 exit는 항상 표준 입출력 라이브러리를 마무리하는 작업을 수행한다.


atexit 함수

ISO C에서 하나의 프로세스는 exit 실행 시 자동으로 호출될 함수를 적오도 32개까지 등록할 수 있다. 그런 함수를 종료 처리부(exit handler)라고 부른다. 종료 처리부를 등록할 때 사용하는 함수는 atexit이다.

#include 
int atexit(void (*func)(void));

예제

도해 7.3의 프로그램은 atexit함수의 사용법을 보여준다.


도해7.3 종료 처리부의 예

#include "apue.h"

static void my_exit1(void);
static void my_exit2(void);

int main (void) {
	if(atexit(my_exit2) != 0) {
		err_sys("can't register my_exit2");
	}

	if(atexit(my_exit1) != 0) {
		err_sys("cant't register my_exit3");
	}
	if(atexit(my_exit1) != 0) {
		err_sys("cant't register my_exit3");
	}

	printf("main is done\n");
	return(0);
}

static void my_exit1(void){
	printf("first exit handler\n");
}

static void my_exit2(void) {
	printf("second exit handler\n");
}

하나의 종료 처리부는 등록된 횟수만큼 호출된다. 도해 7.3에서 첫 종료 처리부는 두 번 등록되었기 때문에 두 번 호출된다. 이 프로그램이 exit를 호출하지 않음을 주목하기 바란다. 대신 그냥 main의 return문을 통해서 종료된다.



7.4 명령줄 인수


어떤 프로그램이 실행될 때, 실행을 위해 exec를 호출한 프로세스가 명령줄 인수들을 그 프로그램에 남겨줄 수 있다.

예제

도해7.4는 주어진 모든 명령줄 인수를 그대로 표준 출력에 '반향시키는(echo)' 프로그램이다. 보통의 echo(1)프로그램과는 달리 이 프로그램은 0번 인수도 반향시킨다는 점을 주목한다

도해7.4 모든 명령줄 인수를 표준 출력에 반향시키는 프로그램

#include "apue.h"

int main (int argc, char *argv[]) {
	int i;
	for(i = 0; i < argc; i++) {				/* 명령줄 인수들을 모두 출력 */
		printf("argv[%d]: %s\n", i, argv[i]);
	}
	exit(0);
}

argv[arg]가 널 포인터임은 ISO C와 POSIX.1모두 보장해 준다. 따라서 인수 처리 루프를 다음과 같은 형태로 짜도 된다.

for(i = 0; argv[i] != NULL; i++)



7.5 환경 목록


각 프로그램에는 환경 목록(environment list)이라는 것도 전달된다. 인수 목록처럼 환경 목록은 문자열 포인터들의 배열로, 각 포인터는 하나의 널 종료 C 문자열을 가리킨다. 그러한 포인터들의 배열의 주소가 전역 변수 environ에 들어 있다.

extern char **environ;


도해7.5는 문자열 다섯 개로 구성된 환경 목록의 예를 보여준다. 각 문자열 끝에 널 바이트를 명시적으로 표시해 두었다. environ 변수를 이제부터 환경 포인터(environment pointer)라고 부르고 포인터들의 배열을 환경 목록, 그리고 각 포인터가 가리키는 문자열을 환경 문자열(environment string)이라고 부르기로 하겠다.


 역사적으로, 대부분의 유닉스 시스템들은 환경 목록의 주소를 main함수의 셋째 인수로 제공했다.

int main(int argc, char *argv[], char *envp[]);

그러나 ISO C는 main함수의 인수가 두 개라고 명시한다. 그리고 이 셋째 인수가 전역 변수 environ보다 더 나은 무언가를 제공하지도 않는다. 그래서 POSIX.1은 셋째 인수가 제공된다고 해도 그 대신 environ을 사용해야 한다고 명시한다. 일반적으로, 특정 환경 변수를 조회하거나 설정할 때에는 environ 변수를 직접 거치는 대신 getenv 함수와 putenv 함수를 사용한다.



7.6 C 프로그램의 메모리 배치


예전부터 하나의 C프로그램은 다음과 같은 조각들로 구성된다.

  • CPU가 실행하는 기계어 명령들로 이루어진 '텍스트 구역(text segment)'. 보통의 경우 텍스트 구역은 공유가 가능하다.
  • 프로그램 안에서 구체적으로 초기화되는 변수들을 담은 '초기화된 자료 구역(initialized data segment)'. 이를 그냥 자료 구역이라고 부르는 경우가 많다. 예를 들어 함수 바깥 범위에 다음과 같은 C선언이 있다고 하자.
        int    maxcount = 99;
    그러면 이 변수와 해당 초기치가 프로그램의 초기화된 자료 구역에 저장된다.
  • 흔히 'bss'구역이라고도 부르는 '초기화되지 않은 자료 구역(uninitialized data segment).' bbs는 "block started by symbol"을 뜻하는 고대의 어셈블러 연산자 이름에서 비롯된것이다. 이 구역의 자료는 프로그램 실행 전에 커널이 수치 0또는 널 포인터를 초기화한다. 함수 바깥 범위에 다음과 같은 C선언이 있다고 하자.
        long    sum[1000];
    그러면 이 변수는 초기화 되지 않은 자료 구역에 저장된다.
  • 함수가 호출될 때마다 호출 관련 정보와 자동(automatic)변수들이 저장되는 '스택(stack)'.
    함수가 호출될 때마다 함수의 반환 주소와 호출자의 환경에 관한 특정 정보(몇몇 CPU 레지스터 등)가 스택에 저장된다. 그런 다음, 호출된 함수의 장동 변수들과 임수 변수들을 위한 공간이 스택에 마련된다. C에서 재귀 함수가 작동하는 것은 바로 이러한 스택덕분이다. 재귀함수가 자신을 호출할 때마다 새로운 스택 프레임이 마련되므로, 함수의 한 호출에서의 일단의 변수들이 그 함수의 다른 호출에서의 변수들에 간섭화하는 일이 생기지 않는다.
  • 동적 메모리 할당이 주로 일어나는 '힙(heap'. 역사적으로 힙은 초기화되지 않은 자료 구역과 스택 사이에 위치한다.
도해7.6은 이러한 구역들의 전형적인 배치를 나타낸 것이다. 32비트 Intel x86 프로세서 기반 Linux에서 텍스트 구역은 메모리 장소 0x08048000에서 시작하며, 스택의 최하단은 0xC0000000바로 밑에서 시작한다. (Intel x86 아키텍쳐에서 스택은 번지수가 높은 주소에서 낮은 주소로 자란다.) 힙 최상단과 스택 최상단 사이의 쓰이지 않은 가상 주소 공간은 도해에 나온 것보다 훨씬 크다.


도해7.6 프로그램의 전형적인 메모리 배치
size(1) 명령은 텍스트 구역과 자료 구역, bss 구역의 크기(바이트 단위)를 알려준다. 
$ size /usr/bin/cc /bin/sh
   text	   data	    bss	    dec	    hex	filename
 902577	   8048	   9696	 920321	  e0b01	/usr/bin/cc
 143301	   4792	  11312	 159405	  26ead	/bin/sh
넷째 열과 다섯째 열은 세 크기의 총합을 각각 십진수와 16진수로 나타낸 것이다.


7.7 공유 라이브러리


오늘날 대부분의 유닉스 시스템은 공유 라이브러리(shared library)를 지원한다. 공유 라이브러리를 이용하면 공통적인 라이브러리를 프로그램 실행 파일마다 담아 둘 필요 없이, 라이브러리 루틴의 복사본 하나만 메모리에 담아 두고 모든 프로세스가 참조하게 할 수 있다. 이렇게 하면 각 실행 파일의 크기가 줄어들지만, 대신 실행 시점에서 프로그램이 처음 실행될 때 또는 각 공유 라이브러리 함수가 처음 호출될 때 약간의 추가부담이 생긴다. 공유 라이브러리의 또 다른 장점은, 라이브러리를 사용하는 모든 프로그램을 수정하고 다시 링크하지 않고도 라이브럴리의 함수들을 새 버전으로 교체할 수 있다는 것이다(인수들의 개수와 형식이 변하지 않았다고 할 때).


 $ gcc -static hello1.c 
 $ ls -l a.out 
-rwxrwxr-x 1 jwpark jwpark 912704 10월 30 08:57 a.out
 $ size a.out 
   text	   data	    bss	    dec	    hex	filename
 823550	   7284	   6360	 837194	  cc64a	a.out
 $ gcc hello1.c 
 $ ls -l a.out 
-rwxrwxr-x 1 jwpark jwpark 8608 10월 30 08:58 a.out
 $ size a.out 
   text	   data	    bss	    dec	    hex	filename
   1182	    552	      8	   1742	    6ce	a.out

공유 라이브러리를 사용하도록 설정해서 프로그램을 컴파일하면 실행 파일의 텍스트 구역과 자료 구역이 크게 줄어든다



7.8 메모리 할당


ISO C 표준에 명시된 메모리 할당 함수는 다음 세 가지이다.오

  1. 메모리에서 지정된 개수의 바이트들을 할당하는 malloc. 할당된 메모리의 초기치는 불확정이다.
  2. 지정된 개수의 바이트들을 할당하되 그 바이트들을 모두 0바이트(모든 비트가 0인 바이트)로 초기화하는 calloc.
  3. 이미 할당된 영역의 크기를 늘리거나 줄이는 realloc. 크기를 늘리는 경우에는, 영역 끝에 충분한 골간을 마련하기 위해 이전에 할당된 영역을 아예 다른 어딘가로 욺길 수도 있다. 또한 크기를 늘리는 경우 기존 내용과 새 영역의 끝 사이의 공간의 초기치는 불확정이다.
이 세 할당 함수가 돌려주는 포인터는 임의의 자료 객체에 쓰일 수 있도록 적절히 정렬(alignment)된 상태임이 보장된다.
 세 alloc류 함수들은 일반적 void * 포인터를 돌려주므로, #include <stdlib.h>로 함수원형들을 도입했다면 이 함수들이 돌려준 포인터를 다른 형식의 포인터 변수에 배정할 때 명시적인 캐스팅이 필요하지 않다.
 free 함수는 ptr가 가리키는 공간의 할당을 해제(deallocation)한다. 일반적으로 해제된 공간은 가용 메모리 풀(pool)로 반환되며, 이후 세 alloc류 함수들 중 하나의 호출에서 다시 할당될 수 있다.
sbrk로 프로세스의 메모리를 늘리는 것은 물론 줄이는 것도 가능하지만, mallock과 free의 구현들 중 메모리 크기를 실제로 줄이는 것은 거의 없다.
 동적으로 할당된 버퍼의 시작이나 끝을 지나친 위치가 뭔가를 덮어 썼을 때 관리용 정보만깨지는 아니다. 동적으로 할당된 버퍼의 앞이나 뒤의 메모리를 다른 어떤 동적 할당 객체가 사용할 수 있다. 그런 객체들은 영역을 침범한 코드와는 무관할 수 있으며, 그러면 메모라기 깨진 원인을 찾기가 더욱 여려워진다.
 이미 해제된 블록을 다시 해제하거나, 세 alloc류 함수을 할당하지 않은 영역을 가리키는 포인터를 free로 해제하는것, 역시 치명적 오류로 이어질 수 있다. 한 프로세스가 malloc을 호출했으나 free 호출은 빼먹으면, 그 프로세스의 메모리 사용량이 계속 증가하게 된다. 이를 메모리 누수(leakage)라고 부른다.

대외적인 메모리 할당자들
malloc과 free를 대신하는 수단들도 많이 있다. 대안적인 메모리 할당자 구현들을 제공하는 라이브러리를 기보적으로 제공하는 시스템도 있다.

libmalloc
Solais 같은 SVR4기반 시스템들에 libmalloc 라이브러리가 포함되어 있다.

vmalloc
프로세스가 메모리의 서로 다른 영역에 대해 서로 다른 기법을 이용해서 메모리를 할당할 수 있도록 하는 메모리 할당자가 서술되어 있다.

quick-fit
표준 malloc 알고리즘은 최적 적합(best-fit)이나 최초 적합(first-fit)메모리 할당 전략을 사용했다. quick-fit 라이브러리가 사용하는 빠른 적합(quick-fit) 전략은 그 둘보다 빠르지만 메모리를 더 많이 사용하는 경향이 있다. 대부분의 현대적 할당자는 이 빠른 적합 전략에 기초한다.

jemalloc
이 라이브러리는 다중 프로세서 시스템에서 실행되는 다중 스레드 응용 프로그램에서 쓰일 때 규모가변성(scalability)이 좋도록 설계된 것이다.

TCMalloc
고성능, 규모가변성, 메모리 효율성을 위해 alloc류 함수들을 대체하는 목적으로 설계된 것이다. 이 라이브러리는 캐시에서 버퍼들을 할당하거나 해제할 때의 잠금 추가부담을 피하기 위해 스레드 지역 캐시를 활용한다. 또한 이 라이브러리에는 동적 메모리 사용의 디버깅과 분석을 위한 힙 검사기와 힙 프로파일러도 포함되어 있다. Google의 오픈소스 프로젝트이다.

alloca 함수
alloca라는 함수는 호출방법이 malloc과 동일하나, 힙에서 메모리를 할당하는 것이 아니라 현재 함수의 스택 프레임에서 메모리를 할당한다. 장점은 공간을 해제할 필요가 없다는 것이다. 할당된 공강은 함수가 반환될 때 자동으로 사라진다. alloca 함수는 스택 프레임의 크기를 늘린다.


7.9 환경 변수


환경 문자열은 다음과 같은 형태이다.

이름=값

UNIX 커널이 이런 문자열들을 직접 참조하는 일은 없다. 이 문자열의 해석은 여러 응용 프로그램의 몫이다.

 ISO C는 환경으로부터 값을 조회하는 함수를 정의한다. 단, 이 표준에 따르면 환경의 내용은 구현이 정의한다.

#include 

char *getenv(const char *name);
		// 반환값: name에 해당하는 환경 변수의 값을 가리키는 포인터, 그런 변수를 찾지 못했으면 NULL

특정 환경 변수의 값을 얻을 때에는 environ에 직접 접근하지 말고 이 getenv 함수를 사용하는 것이 바람직하다. 


환경 변수의 값을 조회하는 것뿐만 아니라 환경 변수를 설정해야 하는 경우도 종종 있다. 기존 변수의 값을 변경해야 할 수도 있고 새 변수를 환경에 추가해야 할 수도 있다.

#include 

int putenv(char *str);
		// 반환값: 성공 시 0, 오류 시 0이 아닌 값
int setenv(const char *name, const char *value, int rewirte);

int unsetenv(const char *name);
		// 반환값(둘 다): 성공 시 0, 오류 시 -1
  • putenv 함수는 주어진 이름=값 형태의 문자열을 환경 목록에 넣는다. 만일 이름이 이미 존재하면 먼저 기존 정의가 제거된다.
  • setenv 함수는 name인수에 해당하는 환경 변수의 값을 value로 설정한다. 환경에 name이 이미 존재하는 경우, (a) 만일 rewirte인수가 0이 아닌 값이면 name의 기존 정의가 먼저 제거되고, (b) 만일 rewirte가 0이면 name의 기존 정의가 제거되지 않으며 name이 새 value로 설정되지도 않는다. 또한 어떠한 오류도 발생하지 않는다.
  • unsetenv 함수는 name에 해당하는 정의를 제거한다. 그런 정의가 존재하지 않아도 오류가 아니다.
putenv와 setenv의 차이점을 주의하기 바란다. setenv는 주어진 인수들로 이름=값 문자열을 만들기 위해 반드시 메모리를 할당해야 하지만, putenv는 주어진 문자열 자체를 직접 환경에 집어넣을 수 있다.



7.10 setjum 함수와 longimp 함수


C에서, goto를 이용해서 다른 함수 안에 있는 이름표(label)로 건너뛰는 것은 허용되지 않는다. 그런 방식의 분기를 위해서는 반드시 setjmp 함수와 longjmp 함수를 사용해서 한다. 잠시 후에 보겠지만 이 두 함수는 깊게 중첩된 함수 호출 안에서 발생한 오류 조건을 처리하는 데 유용하다.


자동 변수, 레지스터 변수, 휘발성 변수

대부분의 구현은 자동 변수들과 레지스터 변수들을 원래 상태로 복원하지만, 표준은 단지 그 값들이 불확정이라고 말한다. 만일 복원되지 말아야 할 자동 변수가 있다면 volatile 키워드를 붙여서 휘발성 변수로 만들면 된다. 전역 변수나 정적 변수는 longjmp가 실행되어도 영향을 받지 않는다.

전역 변수와 정적 변수, 휘발성 변수에는 최적화가 영향을 미치지 않는다.


자동 변수의 잠재적 문제점

자동 변수와 관련해서 따라야 할 기본적인 규칙은, 자동 변수를 선언한 함수가 반환된 후에도 그 변수를 참조하는 코드가 있어서는 안 된다. 표준 입출력 라이브러리는 메모리의 그 부분을 계속해서 자신의 스트림 버퍼로 사용한다. 그래서 해당 변수는 정적으로든(static 또는 extern) 동적으로든(alloc류 함수를 이용해서) ㅈ너역 메모리에 할당해야 한다.



7.11 getrlimit 함수와 setrlimit 함수


모든 프로세스에는 일단의 자원 한계가 있으며, 그 중 일부는 getrlimit 함수와 setrlimit 함수로 조회하거나 변경할 수 있다.

자원 한계의 변경에는 다음 세 가지 규칙이 적용된다.

  1. 프로세스는 자신의 약한 한계(soft limit)를 자신의 강한 한계(hard limit)보다 작거나 같은 값으로만 변경할 수 있다.
  2. 프로세스는 자신의 강한 한계를 자신읜 약한 한계보다 크거나 같은 값으로 낮출 수 있다. 보통의 사용자는 일단 낮춘 강한 한계를 다시 높일 수 없다.
  3. 강한 한계는 오직 수퍼사용자 프로세스만 높일 수 있다.
한계를 무한대로 설정할 대에는 상수 RLIM_INFINITY를 사용한다.

RLIMIT_AS

프로세스의 총 가용 메모리의 최대 크기(바이트 단위). 

RLIMIT_CORE 

코어 파일 하나의 초대 크기. 0으로 설정하면 코어 미 생성. 

RLIMIT_CPU 

소비할 수 있는 최대 CPU 시간(초 단위). 이 약한 한계를 넘기면 프로세스에 SIGXCPU 신호가 전송된다.

RLIMIT_DATA 

자료구역의 최대 크기. 초기화된 자료와 초기화 되지 않은 자료, 힙을 합한 크기의 최대값 

RLIMIT_FSIZE 

생성 가능한 파일의 최대 크기. 이 약한 한계를 넘기면 프로세스에 SIGXFSZ 신호가 전송된다. 

RLIMIT_MEMLOCK 

프로세스가 mlock(2)를 이용해서 잠글 수 있는 메모리 블록의 최대 크기. 

RLIMIT_MSGQUEUE

프로세스가 POSIX 메시지 대기열들을 위해 할당할 수 있는 메모리 블록의 최대 크기. 

RLIMIT_NICE 

프로세서의 실행 일정 우선순위에 영향을 미치기 위해 높일 수 있는 '예의 바름'값의 최대값

RLIMIT_NOFILE 

프로세스가 열 수 있는 파일들의 최대 개수. 변경은 _SC_OPEN_MAX 인수에 대해 sysconf 함수가 돌려주는 값에 영향을 미친다.

RLIMIT_NPROC 

실제 사용자ID당 자식 프로세스 최대 개수. 변경은 _SC_CHILD_MAX 인수에 대해 sysconf 함수가 돌려주는 값에 영향을 미친다. 

RLIMIT_NPTS    한 사용자가 동시에 열어 둘 수 있는 유사 터미널의 최대 개수.
RLIMIT_RSS상주 집합 크기의 최대값. 가용 물리적 메모리 용량이 낮으면 커널은 자신의 RSS를 넘긴 프로세스의 메모리를 빼앗는다.
RLIMIT_SBSIZE주어진 한 시점에서 한 사용자가 소비할 수 있는 소켓 버퍼들의 최대 크기.
RLIMIT_SIGPENDING프로세스의 대기열에 쌓여 있을 수 있는 신호들의 최대 개수. 이 한계는 sigqueue함수에 의해 강제된다.
RLIMIT_STACK스텍의 최대 크기.
RLIMIT_SWAP한 사용자가 소비할 수 있는 교체 공간(swap space)의 최대 크기.
RLIMIT_VMEMRLIMIT_AS와 같음 


자원 한계들은 호출 프로세스에 영향을 미치며, 호출 프로세스의 모든 자식에게 상속된다. 이는 이후의 모든 프로세스에 영향을 주기 위해서는 자원 한계들의 설정 기능을 셸 자체에 내장할 필요가 있다는 뜻이다. 실제로 본 셸과 GNU 본어게인셸, 콘 셸에는 내장 ulimit명령이 있으며, C 셸에는 내장 limit 명령이 있다.

#define doit(name) pr_limits(#name, name)
doit 매크로에서 문자열 값을 생성할 때 ISO C의 문자열 생성 연산자(#)가 쓰였다. C 전처리기는 예를 들어
doit(RLIMIT_CORE);
를 다음으로 확장한다.
pr_limits("RLIMIT_CORE", RLIMIT_CORE);


7.12 요약


유닉스 시스템에서 C프로그램이 실행되는 환경을 이해하는 것은 UNIX 시스템의 프로세스 제어기능들을 이해하기 위한 선행 조건이라 할 수 있다.


728x90

댓글