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 프로세스 종료
하나의 프로세스를 종료하는 방법은 여덟 가지이다.
정상적인 종료 방법
- main 함수로부터의 반환
- exit 호출
- _exit 또는 _Exit 호출
- 마지막 스레드를 시작한 루틴으로부터의 반환
- 마지막 스레드에서 pthread_exit 호출
비정상적인 종료 방법
- abort 호출
- 종료 신호 수신
- 마지막 스레드가 취소 요청에 반응함
종료 함수들
프로그램을 정상적으로 종료시키는 세 가지 함수가 있다. _exit함수와 _Exit함수는 커널로 즉시 반환되며, exit함수는 일정한 정리(cleanup)작업을 수행한 후 커널로 변환된다.
예나 지금이나 exit는 항상 표준 입출력 라이브러리를 마무리하는 작업을 수행한다.
atexit 함수
ISO C에서 하나의 프로세스는 exit 실행 시 자동으로 호출될 함수를 적오도 32개까지 등록할 수 있다. 그런 함수를 종료 처리부(exit handler)라고 부른다. 종료 처리부를 등록할 때 사용하는 함수는 atexit이다.
#includeint 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'. 역사적으로 힙은 초기화되지 않은 자료 구역과 스택 사이에 위치한다.
$ 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
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 표준에 명시된 메모리 할당 함수는 다음 세 가지이다.오
- 메모리에서 지정된 개수의 바이트들을 할당하는 malloc. 할당된 메모리의 초기치는 불확정이다.
- 지정된 개수의 바이트들을 할당하되 그 바이트들을 모두 0바이트(모든 비트가 0인 바이트)로 초기화하는 calloc.
- 이미 할당된 영역의 크기를 늘리거나 줄이는 realloc. 크기를 늘리는 경우에는, 영역 끝에 충분한 골간을 마련하기 위해 이전에 할당된 영역을 아예 다른 어딘가로 욺길 수도 있다. 또한 크기를 늘리는 경우 기존 내용과 새 영역의 끝 사이의 공간의 초기치는 불확정이다.
7.9 환경 변수
환경 문자열은 다음과 같은 형태이다.
이름=값
UNIX 커널이 이런 문자열들을 직접 참조하는 일은 없다. 이 문자열의 해석은 여러 응용 프로그램의 몫이다.
ISO C는 환경으로부터 값을 조회하는 함수를 정의한다. 단, 이 표준에 따르면 환경의 내용은 구현이 정의한다.
#includechar *getenv(const char *name); // 반환값: name에 해당하는 환경 변수의 값을 가리키는 포인터, 그런 변수를 찾지 못했으면 NULL
특정 환경 변수의 값을 얻을 때에는 environ에 직접 접근하지 말고 이 getenv 함수를 사용하는 것이 바람직하다.
환경 변수의 값을 조회하는 것뿐만 아니라 환경 변수를 설정해야 하는 경우도 종종 있다. 기존 변수의 값을 변경해야 할 수도 있고 새 변수를 환경에 추가해야 할 수도 있다.
#includeint 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에 해당하는 정의를 제거한다. 그런 정의가 존재하지 않아도 오류가 아니다.
7.10 setjum 함수와 longimp 함수
C에서, goto를 이용해서 다른 함수 안에 있는 이름표(label)로 건너뛰는 것은 허용되지 않는다. 그런 방식의 분기를 위해서는 반드시 setjmp 함수와 longjmp 함수를 사용해서 한다. 잠시 후에 보겠지만 이 두 함수는 깊게 중첩된 함수 호출 안에서 발생한 오류 조건을 처리하는 데 유용하다.
자동 변수, 레지스터 변수, 휘발성 변수
대부분의 구현은 자동 변수들과 레지스터 변수들을 원래 상태로 복원하지만, 표준은 단지 그 값들이 불확정이라고 말한다. 만일 복원되지 말아야 할 자동 변수가 있다면 volatile 키워드를 붙여서 휘발성 변수로 만들면 된다. 전역 변수나 정적 변수는 longjmp가 실행되어도 영향을 받지 않는다.
전역 변수와 정적 변수, 휘발성 변수에는 최적화가 영향을 미치지 않는다.
자동 변수의 잠재적 문제점
자동 변수와 관련해서 따라야 할 기본적인 규칙은, 자동 변수를 선언한 함수가 반환된 후에도 그 변수를 참조하는 코드가 있어서는 안 된다. 표준 입출력 라이브러리는 메모리의 그 부분을 계속해서 자신의 스트림 버퍼로 사용한다. 그래서 해당 변수는 정적으로든(static 또는 extern) 동적으로든(alloc류 함수를 이용해서) ㅈ너역 메모리에 할당해야 한다.
7.11 getrlimit 함수와 setrlimit 함수
모든 프로세스에는 일단의 자원 한계가 있으며, 그 중 일부는 getrlimit 함수와 setrlimit 함수로 조회하거나 변경할 수 있다.
자원 한계의 변경에는 다음 세 가지 규칙이 적용된다.
- 프로세스는 자신의 약한 한계(soft limit)를 자신의 강한 한계(hard limit)보다 작거나 같은 값으로만 변경할 수 있다.
- 프로세스는 자신의 강한 한계를 자신읜 약한 한계보다 크거나 같은 값으로 낮출 수 있다. 보통의 사용자는 일단 낮춘 강한 한계를 다시 높일 수 없다.
- 강한 한계는 오직 수퍼사용자 프로세스만 높일 수 있다.
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_VMEM | RLIMIT_AS와 같음 |
7.12 요약
유닉스 시스템에서 C프로그램이 실행되는 환경을 이해하는 것은 UNIX 시스템의 프로세스 제어기능들을 이해하기 위한 선행 조건이라 할 수 있다.
'프로그래밍 > UNIX 고급 프로그래밍' 카테고리의 다른 글
제9장 프로세스 관계 (0) | 2019.12.02 |
---|---|
제8장 프로세스 제어 (0) | 2019.11.18 |
제6장 시스템 자료 파일과 시스템 정보 (0) | 2017.10.24 |
제5장 표준 입출력 라이브러리 (0) | 2017.10.18 |
제4장 파일과 디렉터리 (0) | 2017.09.20 |
댓글