7장 프로세스 환경
7.1 소개
"프로세스 제어"와 "프로세스 환경"
프로세스를 제어하는 기본적인 수단들이 있음
8장의 프로세스 제어를 보기전에 프로세스가 실행되는 환경을 조사할 필요가 있음
7장의 내용
main 함수가 호출되는 방법
명령줄(command line) 인수들이 프로그램에 전달되는 방식
프로세스의 전형적인 메모리 배치
프로세스가 추가적인 메모리를 할당하는 방법
환경변수를 사용하는 방법
프로세스를 종료하는 방법
longjmp, setjmp 함수와 스택과 상호작용하는 방식
프로세스 자원의 한계
7.2 main함수
프로그램은 main에서 시작한다. ?
main함수의 원형(prototype)
int main(int argc, char *argv[]);
argc 명령줄 인수의 개수
(argument count ?)
argv 명령줄 인수를 가리키는 포인터들의 배열
(argument vector ?)
시동루틴
exec류 함수를 통해 C프로그램을 실행시켰을때 main 함수가 호출되기전에 특별한 시동(start-up) 루틴이 호출됨
프로그램 실행파일의 프로그램 시작주소는 시동 루틴을 가리킴
이 시작주소는 C컴파일러가 호출하는 링커(linker) 프로그램이 설정한 것임
시동 루틴은 커널로부터 여러 자료들(명령줄 인수들과 환경 변수들)을 전달받고
main함수의 실행에 필요한 제반사항을 준비함
※ stackoverflow의 start-up routine의 필요성
http://stackoverflow.com/questions/4595910/what-is-the-need-for-c-startup-routine
CRT : C Run-Time Libraries
※ 유닉스ㆍ리눅스 프로그래밍 필수 유틸리티 187p
start up 코드에 대한 설명
초기화 루틴들이 들어 있는 오브젝트
흔히 프로그램을 실행시키면 main함수부터 실행되는 것으로 알고 있겠지만 실제로는 그렇지 않다.
실제로는 _start부터 시작한다.
먼저 커널의 execve() 시스템 콜이 프로그램을 메모리에 적재하고 나서 main(int argc, char **argv, char **envp) 함수에서 사용하는 인자 값들(실행 인자 개수, 인자, 환경 변수)을 프로세스의 스택에 push한다.
그리고 _start를 수행한다. _start는 커널이 저장한 정보를 꺼내고 ctr1.o에 있는 __libc_start_main을 호출한다.
__libc_start_main은 필요한 초기화를 수행한 후 비로서 진짜 main함수를 호출하는 것이다.
7.3 프로세스 종료
프로세스가 종료되는 방식 총 8가지
정상적인 프로세스의 종료 5가지
- main 함수의 반환
- exit 호출
- _exit 또는 _Exit 호출
- 마지막 스레드를 시작한 스레드 시동 루틴(11.5절)의 반환
- 마지막 스레드의 pthread_exit(11.5절) 호출
비정상적인 프로세스의 종료 3가지
- abort(10.17절) 호출
- 신호(10.2절)를 받음
- 마지막 스레드가 취소 요청에 반응함(11.5절과 12.7절)
시동 루틴은 main함수가 반환되면 exit를 호출하는 역할도 담당함
시동 루틴을 C로 작성한다면,(실제로는 어셈블러로 작성되는 경우가 많다.)
main을 호출하는 코드는 다음과 같은 모습이 된다.
exit(main(argc, argv));
종료 함수들
#include<stdlib.h>
void exit(int status);
void _Exit(int status);
#include<unistd.h>
void _exit(int status);
_exit, _Exit, exit 함수는 프로그램을 정상적으로 종료시킨다.
_exit, _Exit 함수는 바로 커널로 반환되는 반면
exit 함수는 마무리 처리를 한 후에 커널로 돌아간다.
이 함수들은 종료되는 해당 프로세스 이외의 프로세스들(자식 프로세스나 부모 프로세스)에 어떠한 영향을 미친다.
exit 함수는 표준 I/O라이브러리의 마무리(열린 모든 스트림에 대해 fclose를 호출)도 수행함.
버퍼에 남겨진 출력 자료가 모두 방출(파일로 기록)된다.
종료 상태(exit status)
위의 세 종료 함수들 모두
UNIX System의 대부분의 셸들은 프로세스의 종료 상태를 조회하는 수단을 제공함
예) bash 셸
$ echo $?
종료 상태가 정의되지 않는 경우도 있다
- 이 함수들이 종료 상태 없이(?) 호출되었을때
- 프로세스의 main함수가 반환값 없는 return문에 의해 반환
- main함수가 정수를 돌려주도록 선언되지 않은 경우
그러나 main의 반환 형식이 int이고 main이 return 문을 만나지 않고 마지막 코드에 도달해서 실행이 끝나면 프로세스의 종료 상태는 0이 됨
이러한 행동은 ISO C 표준의 1999 버전에서 새로이 도입된 것이다.
그 이전에는, 명시적인 return문이나 exit 호출을 거치지 않고 main 함수의 끝에 도달했을 때 종료 상태가 정의 되지 않았음
main에서 정수 값을 반환하는 것은 그값으로 exit를 호출하는 것과 동일한 효과를 냄
exit(0);
return(0);
은 동일한 효과를 냄
종료 처리부(exit handler)
ISO C에 의하면 프로세스는 exit 실행시 자동으로 호출될 함수들을 최대 32개까지 등록할 수 있음
이러한 함수를 종료 처리부라고 함
atexit 함수
#include<stdlib.h>
int atexit(void (*func)(void));
종료 처리부(exit handler)함수 등록
exit함수는 등록된 함수들을 등록의 역순으로 호출함
하나의 함수를 여러 번 등록할 수 있음
※ 종료 처리부는 1989년 ANSI C 표준에 처음 등장했다. ANSI C 이전의 UNIX 시스템들(SVR3, 4.3BSD 등)은 종료처리부를 지원하지 않았다.
ISO C는 시스템이 적어도 32개의 종료 처리부들을 지원해야 한다고 요구함
주어진 플랫폼이 지원하는 최대 종료 처리부 개수는 sysconf함수로 알아낼 수 있음
ISO C와 POSIX.1에 의하면 exit함수는 종료 처리부들을 먼저 호출한 후에 열린 스트림들을 모두 닫음(fclose를 호출해서)
POSIX.1은 프로그램이 exec류 함수들을 호출하는 경우 등록된 종료 처리부들이 모두 해제된다는 요구사항을 명시함으로써 ISO C표준을 확장
프로그램은 오직 커널이 exec류 함수들 중 하나를 호출함으로써 실행됨
프로세스의 자발적으로 종료하는 유일한 방법은 _exit나 _Exit(직접적 종료)를 호출하거나 또는 exit(간접적)를 호출하는 것임
신호에 의해 비작발적으로 종료될 수도 있음
- #include<stdio.h>
#include<stdlib.h> - static void my_exit1(void);
static void my_exit2(void); - int main(void)
{
if(atexit(my_exit2) != 0)
fprintf(stderr, "can't register my_exit2"); - if(atexit(my_exit1) != 0)
fprintf(stderr, "can't register my_exit1"); - if(atexit(my_exit1) != 0)
fprintf(stderr, "can't register my_exit1"); - 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");
}
main의 반환에 의해 exit함수가 호출됨
7.4 명령줄 인수들
exec를 통해서 다른 프로그램을 실행할 때, exec를 통해서 새 프로그램에게 명령줄 인수들을 전달할 수 있음
UNIX 시스템 셸로 프로그램을 실행할 때 바로 그러한 일이 일어남
- #include<stdio.h>
#include<stdlib.h> - int main(int argc, char *argv[])
{
int i; - for(i = 0 ; i < argc ; i++)
// for(i = 0 ; argv[i] != NULL ; i++)
printf("argv[%d]: %s\n", i, argv[i]);
exit(0);
}
ISO C와 POSIX.1 모두 argv[argc] 가 반드시 널 포인터임을 보장함
for( i = 0 ; argv[i] != NULL ; i++ )
7.5 환경 목록
각 프로세스에는 환경 목록(environment list)이라는 것도 전달됨
문자열 포인터들의 배열 형태임
포인터들의 배열 자체의 주소는 전역 변수 envrion에 들어 있음
extern char **environ;
관례적으로 환경은 다음과 같은 형태의 문자열들로 구성됨
이름=값
그리고 이름은 대문자를 사용함
특정 환경 변수에 접근할 때에는 environ 대신 getenv함수와 putenv함수를 사용하는 것이 일반적임
전체를 훑을때에는 envrion 환경 포인터를 사용함
- #include<stdio.h>
- extern char **environ;
- int main(int argc, char *argv[])
- {
- int i;
- for(i = 0 ; environ[i] != NULL ; i++){
- printf("%s\n", environ[i]);
- }
- return 1;
- }
7.6 C프로그램의 메모리 구성
C프로그램의 구성요소
>텍스트 구역(text segment)
CPU가 실행하는 기계어 명령어
텍스트영역은 프로세스간 공유가 가능하기 때문에(?) 자주실행되는 프로그램(텍스트 편집기, C컴파일러, 셸 등)의 복사본 하나만 메모리에 두면 됨
수정되면 안되기 때문에 읽기전용으로 지정되는 되는 경우가 많음
>초기화된 자료 구역(data segment)
그냥 자료구역이라고 부르기도 함
프로그램안에서 명시적으로 초기화된 변수들이 담김
전역으로 선언된 변수에 초기화한 것들
>초기화되지 않은 자료 구역
bss 구역이라고도 부름
"block started by symbol" 에 해당하는 고대의 어셈블러 연산자에서 따온 표현
커널에 의해 수치 0또는 널 포인터로 초기화됨
전역으로 선언된 변수에 초기화하지 않은것들
>스택(stack)
자동 변수들과 함수 호출에 대한 정보가 저장됨
함수가 호출될 때마다 반환 주소 및 호출자의 환경에 대한 일정한 정보(CPU의 몇몇 레지스터들 등)가 스택에 저장됨
새로 호출된 함수는 자신의 자동 변수들과 임시 변수들을 담을 공간을 스택에서 할당함
이러한 스택의 용법은 C에서 함수의 재귀 호출을 가능하게 만듬
activation record
permutation
C의 auto, static, register, extern, static, volatile 키워드들 정리
http://blog.naver.com/elcom2000/120038320157
>힙(heap)
동적 메모리 할당이 주로 일어나는 곳
위에서 언급한 것 외에 프로그램 실행파일(a.out)에는 기호테이블, 디버깅정보, 동적 공유라이브러리를 위한 링크 테이블 등 다양한 종류의 구역들이 존재할 수 있음
하지만 프로세스가 프로그램을 실행할 때 프로그램의 이미지의 일부로 메모리에 적재되지 않음
초기화되지 않은 자료 구역의 내용이 디스크상의 프로그램 파일에 저장되지는 않음
이 구역을 프로그램이 실행이 시작하기 전에 커널이 0으로 설정하기 때문임
프로그램을 구성하는 요소들 중 프로그램 파일에 저장될 필요가 있는 것은 텍스트 구역과 초기화된 자료구역 뿐임
size(1)명령어
텍스트 구역, 자료 구역, bss 구역의 크기를 알려줌
7.7 공유 라이브러리
요즘의 UNIX 시스템들은 대부분 공유 라이브러리를 지원함-shared library
공유라이브러리는 실행파일의 크기를 줄이는데 도움이 됨
공통의 라이브러리 루틴들의 복사본 하나를 메모리에 둔 후 실행파일에는 그 루틴으로 연결하는 데 필요한 정보만 담아두면 됨
대신 프로그램이 처음 실행되거나 공유 라이브러리 함수가 처음 호출될 때 약간의 실행시점 추가부담이 발생함
공유 라이브러리의 또 다른 장점은 어떤 라이브러리가 갱신되었을때 그 라이브러리의 함수를 사용하는 모든 프로그램을 다시 링크하지 않고도 라이브러리를 새 버전으로 대체할 수 있음
프로그램을 컴파일할때 공유라이브러리를 사용할지 지정할 수 있음
※ gcc 의 -static 옵션으로 정적라이브러를 사용할수 있음
※ 정적 라이브러리와 동적 라이브러리를 만드는 방법
.so
.o
7.8 메모리 할당
ISO C는 메모리 할당을 위해 세 가지 함수를 제공함
#include<stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
반환값(셋다) : 성공 시 널이 아닌 포인터, 오류 시 NULL
void free(void *ptr);
malloc은 지정된 개수의 바이트를 할당하고 할당된 메모리는 초기화 되지 않음
calloc은 지정된 개수의 바이트를 할당하고 할당된 메모리를 모두 0으로 초기화함
realloc은 이미 할당된 메모리의 영역의 크기를 늘리거나 줄임
realloc은 크기를 늘리는 경우 메모리 영역끝에 추가적인 공간이 더 필요하면 이전에 할당된 영역을 다른 어딘가로 이동시키는 과정을 거칠 수 있음
크기를 늘리는 경우 늘어난 영영에대해서 초기화 되지않음
메모리영역이 충분하지 않을경우 새로운 영역으로 옮겨지면서 주소가 바뀔수 있으므로 기존의 주소는 더이상 유효하지 않을수 있으므로 기존의 포인터는 더 이상 사용하지 말아야함
※ 17.36 정적배열의 한계를 극복하는 예제
마지막인수는 새 영역의 전체크기임. 기존 영역의 크기와 새 영역 크기의 차이가 아님
ptr에 널 포인터를 넣으면 realloc이 malloc과 동일하게 작동함
realloc(NULL, newsize) = malloc(newsize)
세 할당 함수가 돌려주는 포인터는 임의의 자료 객체에 사용할 수 있도록 적절히 정렬된 상태임이 보장됨(?)
예) double의 경우 반드시 8의 배수인 메모리 주소에서 시작해야 한다는 것이 특정 시스템에서 가장 제한적인 메모리 요구사항이라면, 이 세함수들이 돌려주는 포인터는 항상 주소가 8의 배수인 메모리를 가리키게됨
free함수는 ptr이 가리키는 메모리 영역을 해제함
보통의 경우 해제된 영역은 가용 메모리 풀로 되돌려지며, 따라서 이후 세 할당 함수 중 하나의 호출에 다시 할당될 수 있는 상태가 됨
대체로, 이 할당 루틴들은 sbrk(2) 시스템 콜을 이용해 구현됨
sbrk는 프로세스의 힙(heap)을 늘리거나 줄임
대부분의 구현들은 요청된 것보다 조금은 더 큰 공간을 할당해서 여분의 공간에 내부 관리 정보(할당된 블록 크기라던가 할당된 다음 블록의 포인터 등)를 저장함
침번하면 치명적인 오류의 원인이됨(C Program Language에 malloc동작 설명되있음)
이미 해제된 블록을 다시 해제하거나, 세 할당 함수 이외의 수단으로 얻은 포인터에 대해 free를 호출할 때에도 치명적인 오류가 생길 수 있음
한 프로세스가 malloc을 호출했으나 free를 호출하지 않는다면 메모리 사용량이 계속 증가하게 되는데 이것을 메모리 누수(memory leakage)라고 부름
더 이상 쓰이지 않는 공간을 free호출로 시스템에 돌려주지 않으면 프로세스의 주소공간 크기가 점점 커져서 결국에는 빈 공간이 남지 않게 됨
그러면 빈번한 페이지 교체에 의해 프로그램의 성능이 떨어짐
FreeBSD, Mac OS X, Linux는 환경 변수 설정을 통해서 추가적인 디버깅을 지원함
7.9 환경 변수
환경변수는 대부분 다음과 같은 형태임
이름=값
UNIX커널이 이런 문자열들을 직접 참조하는 경우는 결코없음
이 문자열들을 해석해서 적절한 값을 얻고 사용하는 것은 전적으로 응용 프로그램의 몫임
HOME이나 USER같은 일부 환경 변수들은 로그인 시점에서 자동으로 설정되거나, 그 외의 환경변수들은 사용자가 설정한다.
환경 변수들 중에는 셸의 행동방식을 제어하기 위한 것들이 많은데, 그런 환경 변수들은 주로 셸 시동 스크립트 파일을 통해서 설정됨
ex) Bourn 셸, GNU Bourn-again 셸, Korn 셸은 환경 변수 MAILPATH를 통해서 사용자의 편지함 위치를 알아냄
ISO C표준에는 환경변수의 값을 가져오는 함수가 하나 정의 되어있음
그러나 이 표준이 명시하는 바에 따르면, 환경 변수의 형태나 구조 자체는 구현이 정의함
#include<stdlib.h>
char *getenv(const char *name);
반환값 ; name으로 주어진 이름에 해당하는 환경 변수 값을 가리키는 포인터, 그런 이름이 없는 경우에는 NULL
이 함수는 이름=값 형태의 문자열에서 값을 가리키는 포인터를 돌려줌
환경에서 특정한 하나의 값을 얻을 때에는 항상 이 getenv 함수를 사용해야 함(environ 변수로 직접 접근하는 대신)
기존 환경 변수의 값을 변경하거나 새로운 환경 변수를 환경에 추가해야하 할 수도 있음
현재 프로세스가 환경에 가한 변화는 현재 프로세스와 그 자식 프로세스들에만 적용됨
변화가 부모 프로세스(셸인 경우가 많음)에까지 미치지는 않음 그렇긴해도 환경 목록의 수정은 여전히 유용함
#include<stdlib.h>
int putenv(char *str);
반환값 : 성공시 0, 오류시 0이 아닌 값
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
반환값(둘다) : 성공시 0, 오류시 -1
putenv는 이름=값 형태의 문자열을 받아서 그것을 환경목록에 넣음
이름이 이미 존재하면 삭제함
setenv 함수는
7.10 setjmp와 longjmp 함수
C에서는 한 함수 안의 goto 문을 이용해 함수 바깥 어딘가에 있는 레이블로 분가히는 것이 불가능함
함수 경계를 넘나드는 분기를 수행하려면 setjmp함수와 longjmp함수를 사용해야 함
배경지식
각함수의 자동 변수들은 그 함수의 스택 프레임 안에 저장됨
main함수에서 여러 번의 호출을거쳐 도달하는 곳이라면 오류발생시 main으로 바로 돌아가기가 쉽지않음
각 함수가 한 수준 위로 돌아가야 함을 의미하는 특별한 반환값을 돌려주게 할 수도 있지만, 그러면 코드가 지저분해짐
비국소(nonlocal)라는 표현은 이것이 함수 안에서 보통의 C goto문으로 분기하는 것이 아니라는데서 비롯됨
비국소 분기는 호출 프레임들을 되짚어 가면서 현재 함수로의 호출 경로에 있는 한 함수로 돌아가는것을 말함
#include<setjmp.h>
int setjmp(jmp_buf env);
반환값 : 직접 호출된 경우에는 0, longjmp를 통해서 호출된 경우에는 0이 아닌 값
void longjmp(jmp_buf env, int val);
이후 돌아오고자 하는 위치에서 setjmp를 호출함 이경우 setjmp는 0을 돌려줌
1. setjmp함수의 인수는 jmp_buf 형식인데 이 자료 형식은 스택의 상태를 나중에 longjmp가 호출되었을 때 스택 상태로 되돌리는 데 필요한 모든 정보를 담는 일종의 배열, 보통 전역변수로 사용함
2. 나중에 오류를 만나면 longjmp를 호출함. 이때 setjmp호출에 사용한 것과 동일한 변수를 넣음, val은 인수에는 0이 아닌값을 넣음, 이값이 setjmp의 반환값이 됨
이 val을 활용함으로써 setjmp 호출에 대해 여러 개의 longjmp호출을 프로그램안에 둘수 있음 어디에있는 longjmp가 수행되었는지 확인 가능해짐
- #include<setjmp.h>
- #include<stdio.h>
- #include<stdlib.h>
- static void f1(int, int, int, int);
- static void f2(void);
- static jmp_buf jmpbuffer;
- static int globval;
- int main(void)
- {
- int autoval;
- register int regival;
- volatile int volaval;
- static int statval;
- globval = 1;
- autoval = 2;
- regival = 3;
- volaval = 4;
- statval = 5;
- if(setjmp(jmpbuffer) != 0){
- printf("after longjmp:\n");
- printf("gloval = %d, autoval = %d, regival = %d, volaval = %d, statval = %d\n", globval, autoval, regival, volaval, statval);
- exit(0);
- }
- globval = 95;
- autoval = 96;
- regival = 97;
- volaval = 98;
- statval = 99;
- f1(autoval, regival, volaval, statval);
- exit(0);
- }
- static void f1(int i, int j, int k, int l)
- {
- printf("in f1():\n");
- printf("gloval = %d, autoval = %d, regival = %d, volaval = %d, statval = %d\n", globval, i, j, k, l);
- f2();
- }
- static void f2(void)
- {
- longjmp(jmpbuffer, 1);
- }
7.11 getrlimit함수와 setrlimit함수
History
Last edited on 02/23/2012 03:05 by 상훈
Comments (0)